Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6b3851d

Browse files
authored
feat: add check for coder:// URI authority section (#97)
Fixes #52 Checks for the authority string, i.e. `coder.example.com` in `coder://coder.example.com/v0/open/...` links matches the HTTP(S) URL we are signed into. This ensures that the names we use are properly scoped and links generated on one Coder deployment won't accidentally open workspaces on another.
1 parent a6f7bb6 commit 6b3851d

File tree

2 files changed

+124
-13
lines changed

2 files changed

+124
-13
lines changed

App/Services/UriHandler.cs

+35-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public class UriHandler(
2020
ILogger<UriHandler> logger,
2121
IRpcController rpcController,
2222
IUserNotifier userNotifier,
23-
IRdpConnector rdpConnector) : IUriHandler
23+
IRdpConnector rdpConnector,
24+
ICredentialManager credentialManager) : IUriHandler
2425
{
2526
private const string OpenWorkspacePrefix = "/v0/open/ws/";
2627

@@ -64,11 +65,13 @@ private async Task HandleUriThrowingErrors(Uri uri, CancellationToken ct = defau
6465
public async Task HandleOpenWorkspaceApp(Uri uri, CancellationToken ct = default)
6566
{
6667
const string errTitle = "Open Workspace Application Error";
68+
CheckAuthority(uri, errTitle);
69+
6770
var subpath = uri.AbsolutePath[OpenWorkspacePrefix.Length..];
6871
var components = subpath.Split("/");
6972
if (components.Length != 4 || components[1] != "agent")
7073
{
71-
logger.LogWarning("unsupported open workspace app format in URI {path}", uri.AbsolutePath);
74+
logger.LogWarning("unsupported open workspace app format in URI '{path}'", uri.AbsolutePath);
7275
throw new UriException(errTitle, $"Failed to open '{uri.AbsolutePath}' because the format is unsupported.");
7376
}
7477

@@ -120,6 +123,36 @@ public async Task HandleOpenWorkspaceApp(Uri uri, CancellationToken ct = default
120123
await OpenRDP(agent.Fqdn.First(), uri.Query, ct);
121124
}
122125

126+
private void CheckAuthority(Uri uri, string errTitle)
127+
{
128+
if (string.IsNullOrEmpty(uri.Authority))
129+
{
130+
logger.LogWarning("cannot open workspace app without a URI authority on path '{path}'", uri.AbsolutePath);
131+
throw new UriException(errTitle,
132+
$"Failed to open '{uri.AbsolutePath}' because no Coder server was given in the URI");
133+
}
134+
135+
var credentialModel = credentialManager.GetCachedCredentials();
136+
if (credentialModel.State != CredentialState.Valid)
137+
{
138+
logger.LogWarning("cannot open workspace app because credentials are '{state}'", credentialModel.State);
139+
throw new UriException(errTitle,
140+
$"Failed to open '{uri.AbsolutePath}' because you are not signed in.");
141+
}
142+
143+
// here we assume that the URL is non-null since the credentials are marked valid. If not it's an internal error
144+
// and the App will handle catching the exception and logging it.
145+
var coderUri = credentialModel.CoderUrl!;
146+
if (uri.Authority != coderUri.Authority)
147+
{
148+
logger.LogWarning(
149+
"cannot open workspace app because it was for '{uri_authority}', be we are signed into '{signed_in_authority}'",
150+
uri.Authority, coderUri.Authority);
151+
throw new UriException(errTitle,
152+
$"Failed to open workspace app because it was for '{uri.Authority}', be we are signed into '{coderUri.Authority}'");
153+
}
154+
}
155+
123156
public async Task OpenRDP(string domainName, string queryString, CancellationToken ct = default)
124157
{
125158
const string errTitle = "Workspace Remote Desktop Error";

Tests.App/Services/UriHandlerTest.cs

+89-11
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,23 @@ public void SetupMocksAndUriHandler()
2323
_mUserNotifier = new Mock<IUserNotifier>(MockBehavior.Strict);
2424
_mRdpConnector = new Mock<IRdpConnector>(MockBehavior.Strict);
2525
_mRpcController = new Mock<IRpcController>(MockBehavior.Strict);
26+
_mCredentialManager = new Mock<ICredentialManager>(MockBehavior.Strict);
2627

27-
uriHandler = new UriHandler(logger, _mRpcController.Object, _mUserNotifier.Object, _mRdpConnector.Object);
28+
uriHandler = new UriHandler(logger,
29+
_mRpcController.Object,
30+
_mUserNotifier.Object,
31+
_mRdpConnector.Object,
32+
_mCredentialManager.Object);
2833
}
2934

3035
private Mock<IUserNotifier> _mUserNotifier;
3136
private Mock<IRdpConnector> _mRdpConnector;
3237
private Mock<IRpcController> _mRpcController;
38+
private Mock<ICredentialManager> _mCredentialManager;
3339
private UriHandler uriHandler; // Unit under test.
3440

3541
[SetUp]
36-
public void AgentAndWorkspaceFixtures()
42+
public void AgentWorkspaceAndCredentialFixtures()
3743
{
3844
agent11 = new Agent();
3945
agent11.Fqdn.Add("workspace1.coder");
@@ -54,94 +60,116 @@ public void AgentAndWorkspaceFixtures()
5460
Workspaces = [workspace1],
5561
Agents = [agent11],
5662
};
63+
64+
credentialModel1 = new CredentialModel
65+
{
66+
State = CredentialState.Valid,
67+
CoderUrl = new Uri("https://coder.test"),
68+
};
5769
}
5870

5971
private Agent agent11;
6072
private Workspace workspace1;
6173
private RpcModel modelWithWorkspace1;
74+
private CredentialModel credentialModel1;
6275

6376
[Test(Description = "Open RDP with username & password")]
6477
[CancelAfter(30_000)]
6578
public async Task Mainline(CancellationToken ct)
6679
{
67-
var input = new Uri("coder:/v0/open/ws/workspace1/agent/agent11/rdp?username=testy&password=sesame");
80+
var input = new Uri(
81+
"coder://coder.test/v0/open/ws/workspace1/agent/agent11/rdp?username=testy&password=sesame");
6882

83+
_mCredentialManager.Setup(m => m.GetCachedCredentials()).Returns(credentialModel1);
6984
_mRpcController.Setup(m => m.GetState()).Returns(modelWithWorkspace1);
7085
var expectedCred = new RdpCredentials("testy", "sesame");
7186
_ = _mRdpConnector.Setup(m => m.WriteCredentials(agent11.Fqdn[0], expectedCred));
7287
_ = _mRdpConnector.Setup(m => m.Connect(agent11.Fqdn[0], IRdpConnector.DefaultPort, ct))
7388
.Returns(Task.CompletedTask);
7489
await uriHandler.HandleUri(input, ct);
90+
_mRdpConnector.Verify(m => m.WriteCredentials(It.IsAny<string>(), It.IsAny<RdpCredentials>()));
91+
_mRdpConnector.Verify(m => m.Connect(It.IsAny<string>(), It.IsAny<int>(), ct), Times.Once);
7592
}
7693

7794
[Test(Description = "Open RDP with no credentials")]
7895
[CancelAfter(30_000)]
7996
public async Task NoCredentials(CancellationToken ct)
8097
{
81-
var input = new Uri("coder:/v0/open/ws/workspace1/agent/agent11/rdp");
98+
var input = new Uri("coder://coder.test/v0/open/ws/workspace1/agent/agent11/rdp");
8299

100+
_mCredentialManager.Setup(m => m.GetCachedCredentials()).Returns(credentialModel1);
83101
_mRpcController.Setup(m => m.GetState()).Returns(modelWithWorkspace1);
84102
_ = _mRdpConnector.Setup(m => m.Connect(agent11.Fqdn[0], IRdpConnector.DefaultPort, ct))
85103
.Returns(Task.CompletedTask);
86104
await uriHandler.HandleUri(input, ct);
105+
_mRdpConnector.Verify(m => m.Connect(It.IsAny<string>(), It.IsAny<int>(), ct), Times.Once);
87106
}
88107

89108
[Test(Description = "Unknown app slug")]
90109
[CancelAfter(30_000)]
91110
public async Task UnknownApp(CancellationToken ct)
92111
{
93-
var input = new Uri("coder:/v0/open/ws/workspace1/agent/agent11/someapp");
112+
var input = new Uri("coder://coder.test/v0/open/ws/workspace1/agent/agent11/someapp");
94113

114+
_mCredentialManager.Setup(m => m.GetCachedCredentials()).Returns(credentialModel1);
95115
_mRpcController.Setup(m => m.GetState()).Returns(modelWithWorkspace1);
96116
_mUserNotifier.Setup(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsRegex("someapp"), ct))
97117
.Returns(Task.CompletedTask);
98118
await uriHandler.HandleUri(input, ct);
119+
_mUserNotifier.Verify(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsAny<string>(), ct), Times.Once());
99120
}
100121

101122
[Test(Description = "Unknown agent name")]
102123
[CancelAfter(30_000)]
103124
public async Task UnknownAgent(CancellationToken ct)
104125
{
105-
var input = new Uri("coder:/v0/open/ws/workspace1/agent/wrongagent/rdp");
126+
var input = new Uri("coder://coder.test/v0/open/ws/workspace1/agent/wrongagent/rdp");
106127

128+
_mCredentialManager.Setup(m => m.GetCachedCredentials()).Returns(credentialModel1);
107129
_mRpcController.Setup(m => m.GetState()).Returns(modelWithWorkspace1);
108130
_mUserNotifier.Setup(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsRegex("wrongagent"), ct))
109131
.Returns(Task.CompletedTask);
110132
await uriHandler.HandleUri(input, ct);
133+
_mUserNotifier.Verify(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsAny<string>(), ct), Times.Once());
111134
}
112135

113136
[Test(Description = "Unknown workspace name")]
114137
[CancelAfter(30_000)]
115138
public async Task UnknownWorkspace(CancellationToken ct)
116139
{
117-
var input = new Uri("coder:/v0/open/ws/wrongworkspace/agent/agent11/rdp");
140+
var input = new Uri("coder://coder.test/v0/open/ws/wrongworkspace/agent/agent11/rdp");
118141

142+
_mCredentialManager.Setup(m => m.GetCachedCredentials()).Returns(credentialModel1);
119143
_mRpcController.Setup(m => m.GetState()).Returns(modelWithWorkspace1);
120144
_mUserNotifier.Setup(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsRegex("wrongworkspace"), ct))
121145
.Returns(Task.CompletedTask);
122146
await uriHandler.HandleUri(input, ct);
147+
_mUserNotifier.Verify(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsAny<string>(), ct), Times.Once());
123148
}
124149

125150
[Test(Description = "Malformed Query String")]
126151
[CancelAfter(30_000)]
127152
public async Task MalformedQuery(CancellationToken ct)
128153
{
129154
// there might be some query string that gets the parser to throw an exception, but I could not find one.
130-
var input = new Uri("coder:/v0/open/ws/workspace1/agent/agent11/rdp?%&##");
155+
var input = new Uri("coder://coder.test/v0/open/ws/workspace1/agent/agent11/rdp?%&##");
131156

157+
_mCredentialManager.Setup(m => m.GetCachedCredentials()).Returns(credentialModel1);
132158
_mRpcController.Setup(m => m.GetState()).Returns(modelWithWorkspace1);
133159
// treated the same as if we just didn't include credentials
134160
_ = _mRdpConnector.Setup(m => m.Connect(agent11.Fqdn[0], IRdpConnector.DefaultPort, ct))
135161
.Returns(Task.CompletedTask);
136162
await uriHandler.HandleUri(input, ct);
163+
_mRdpConnector.Verify(m => m.Connect(It.IsAny<string>(), It.IsAny<int>(), ct), Times.Once);
137164
}
138165

139166
[Test(Description = "VPN not started")]
140167
[CancelAfter(30_000)]
141168
public async Task VPNNotStarted(CancellationToken ct)
142169
{
143-
var input = new Uri("coder:/v0/open/ws/wrongworkspace/agent/agent11/rdp");
170+
var input = new Uri("coder://coder.test/v0/open/ws/wrongworkspace/agent/agent11/rdp");
144171

172+
_mCredentialManager.Setup(m => m.GetCachedCredentials()).Returns(credentialModel1);
145173
_mRpcController.Setup(m => m.GetState()).Returns(new RpcModel
146174
{
147175
VpnLifecycle = VpnLifecycle.Starting,
@@ -150,29 +178,79 @@ public async Task VPNNotStarted(CancellationToken ct)
150178
_mUserNotifier.Setup(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsRegex("Coder Connect"), ct))
151179
.Returns(Task.CompletedTask);
152180
await uriHandler.HandleUri(input, ct);
181+
_mUserNotifier.Verify(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsAny<string>(), ct), Times.Once());
153182
}
154183

155184
[Test(Description = "Wrong number of components")]
156185
[CancelAfter(30_000)]
157186
public async Task UnknownNumComponents(CancellationToken ct)
158187
{
159-
var input = new Uri("coder:/v0/open/ws/wrongworkspace/agent11/rdp");
188+
var input = new Uri("coder://coder.test/v0/open/ws/wrongworkspace/agent11/rdp");
160189

190+
_mCredentialManager.Setup(m => m.GetCachedCredentials()).Returns(credentialModel1);
161191
_mRpcController.Setup(m => m.GetState()).Returns(modelWithWorkspace1);
162192
_mUserNotifier.Setup(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsAny<string>(), ct))
163193
.Returns(Task.CompletedTask);
164194
await uriHandler.HandleUri(input, ct);
195+
_mUserNotifier.Verify(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsAny<string>(), ct), Times.Once());
165196
}
166197

167198
[Test(Description = "Unknown prefix")]
168199
[CancelAfter(30_000)]
169200
public async Task UnknownPrefix(CancellationToken ct)
170201
{
171-
var input = new Uri("coder:/v300/open/ws/workspace1/agent/agent11/rdp");
202+
var input = new Uri("coder://coder.test/v300/open/ws/workspace1/agent/agent11/rdp");
172203

204+
_mCredentialManager.Setup(m => m.GetCachedCredentials()).Returns(credentialModel1);
173205
_mRpcController.Setup(m => m.GetState()).Returns(modelWithWorkspace1);
174206
_mUserNotifier.Setup(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsAny<string>(), ct))
175207
.Returns(Task.CompletedTask);
176208
await uriHandler.HandleUri(input, ct);
209+
_mUserNotifier.Verify(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsAny<string>(), ct), Times.Once());
210+
}
211+
212+
[Test(Description = "Unknown authority")]
213+
[CancelAfter(30_000)]
214+
public async Task UnknownAuthority(CancellationToken ct)
215+
{
216+
var input = new Uri("coder://unknown.test/v0/open/ws/workspace1/agent/agent11/rdp");
217+
218+
_mCredentialManager.Setup(m => m.GetCachedCredentials()).Returns(credentialModel1);
219+
_mRpcController.Setup(m => m.GetState()).Returns(modelWithWorkspace1);
220+
_mUserNotifier.Setup(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsRegex(@"unknown\.test"), ct))
221+
.Returns(Task.CompletedTask);
222+
await uriHandler.HandleUri(input, ct);
223+
_mUserNotifier.Verify(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsAny<string>(), ct), Times.Once());
224+
}
225+
226+
[Test(Description = "Missing authority")]
227+
[CancelAfter(30_000)]
228+
public async Task MissingAuthority(CancellationToken ct)
229+
{
230+
var input = new Uri("coder:/v0/open/ws/workspace1/agent/agent11/rdp");
231+
232+
_mCredentialManager.Setup(m => m.GetCachedCredentials()).Returns(credentialModel1);
233+
_mRpcController.Setup(m => m.GetState()).Returns(modelWithWorkspace1);
234+
_mUserNotifier.Setup(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsRegex("Coder server"), ct))
235+
.Returns(Task.CompletedTask);
236+
await uriHandler.HandleUri(input, ct);
237+
_mUserNotifier.Verify(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsAny<string>(), ct), Times.Once());
238+
}
239+
240+
[Test(Description = "Not signed in")]
241+
[CancelAfter(30_000)]
242+
public async Task NotSignedIn(CancellationToken ct)
243+
{
244+
var input = new Uri("coder://coder.test/v0/open/ws/workspace1/agent/agent11/rdp");
245+
246+
_mCredentialManager.Setup(m => m.GetCachedCredentials()).Returns(new CredentialModel()
247+
{
248+
State = CredentialState.Invalid,
249+
});
250+
_mRpcController.Setup(m => m.GetState()).Returns(modelWithWorkspace1);
251+
_mUserNotifier.Setup(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsRegex("signed in"), ct))
252+
.Returns(Task.CompletedTask);
253+
await uriHandler.HandleUri(input, ct);
254+
_mUserNotifier.Verify(m => m.ShowErrorNotification(It.IsAny<string>(), It.IsAny<string>(), ct), Times.Once());
177255
}
178256
}

0 commit comments

Comments
 (0)