Skip to content

Commit 52996ea

Browse files
committed
feat: add check for coder:// URI authority section
1 parent 9b8408d commit 52996ea

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 valid 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 = new Uri(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 = "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)