@@ -29,26 +29,9 @@ const (
29
29
RedirectURIQueryParam = "redirect_uri"
30
30
)
31
31
32
- // ResolveRequest takes an app request, checks if it's valid and authenticated,
33
- // and returns a ticket with details about the app.
34
- //
35
- // The ticket is written as a signed JWT into a cookie and will be automatically
36
- // used in the next request to the same app to avoid database calls.
37
- //
38
- // Upstream code should avoid any database calls ever.
39
- func (p * Provider ) ResolveRequest (rw http.ResponseWriter , r * http.Request , appReq Request ) (* Ticket , bool ) {
40
- // nolint:gocritic // We need to make a number of database calls. Setting a system context here
41
- // // is simpler than calling dbauthz.AsSystemRestricted on every call.
42
- // // dangerousSystemCtx is only used for database calls. The actual authentication
43
- // // logic is handled in Provider.authorizeWorkspaceApp which directly checks the actor's
44
- // // permissions.
45
- dangerousSystemCtx := dbauthz .AsSystemRestricted (r .Context ())
46
- err := appReq .Validate ()
47
- if err != nil {
48
- p .writeWorkspaceApp500 (rw , r , & appReq , err , "invalid app request" )
49
- return nil , false
50
- }
51
-
32
+ // TODO: remove this temporary shim
33
+ func (p * DBTicketProvider ) ResolveRequest (rw http.ResponseWriter , r * http.Request , appReq Request ) (* Ticket , bool ) {
34
+ // TODO: this needs to be some sort of normalize function or something
52
35
if appReq .WorkspaceAndAgent != "" {
53
36
// workspace.agent
54
37
workspaceAndAgent := strings .SplitN (appReq .WorkspaceAndAgent , "." , 2 )
@@ -66,23 +49,68 @@ func (p *Provider) ResolveRequest(rw http.ResponseWriter, r *http.Request, appRe
66
49
}
67
50
}
68
51
52
+ ticket , ok := p .TicketFromRequest (r )
53
+ if ok && ticket .MatchesRequest (appReq ) {
54
+ // The request has a valid ticket and it matches the request.
55
+ return ticket , true
56
+ }
57
+
58
+ ticket , ticketStr , ok := p .CreateTicket (r .Context (), rw , r , appReq )
59
+ if ! ok {
60
+ return nil , false
61
+ }
62
+
63
+ // Write the ticket cookie. We always want this to apply to the current
64
+ // hostname (even for subdomain apps, without any wildcard shenanigans,
65
+ // because the ticket is only valid for a single app).
66
+ http .SetCookie (rw , & http.Cookie {
67
+ Name : codersdk .DevURLSessionTicketCookie ,
68
+ Value : ticketStr ,
69
+ Path : appReq .BasePath ,
70
+ Expires : ticket .Expiry ,
71
+ })
72
+
73
+ return ticket , true
74
+ }
75
+
76
+ func (p * DBTicketProvider ) TicketFromRequest (r * http.Request ) (* Ticket , bool ) {
69
77
// Get the existing ticket from the request.
70
78
ticketCookie , err := r .Cookie (codersdk .DevURLSessionTicketCookie )
71
79
if err == nil {
72
80
ticket , err := p .ParseTicket (ticketCookie .Value )
73
81
if err == nil {
74
82
err := ticket .Request .Validate ()
75
- if err == nil && ticket . MatchesRequest ( appReq ) {
83
+ if err == nil {
76
84
// The request has a ticket, which is a valid ticket signed by
77
- // us, and matches the app that the user was trying to access .
85
+ // us. The caller must check that it matches the request .
78
86
return & ticket , true
79
87
}
80
88
}
81
89
}
82
90
83
- // There's no ticket or it's invalid, so we need to check auth using the
84
- // session token, validate auth and access to the app, then generate a new
85
- // ticket.
91
+ return nil , false
92
+ }
93
+
94
+ // ResolveRequest takes an app request, checks if it's valid and authenticated,
95
+ // and returns a ticket with details about the app.
96
+ //
97
+ // The ticket is written as a signed JWT into a cookie and will be automatically
98
+ // used in the next request to the same app to avoid database calls.
99
+ //
100
+ // Upstream code should avoid any database calls ever.
101
+ func (p * DBTicketProvider ) CreateTicket (ctx context.Context , rw http.ResponseWriter , r * http.Request , appReq Request ) (* Ticket , string , bool ) {
102
+ // nolint:gocritic // We need to make a number of database calls. Setting a system context here
103
+ // // is simpler than calling dbauthz.AsSystemRestricted on every call.
104
+ // // dangerousSystemCtx is only used for database calls. The actual authentication
105
+ // // logic is handled in Provider.authorizeWorkspaceApp which directly checks the actor's
106
+ // // permissions.
107
+ dangerousSystemCtx := dbauthz .AsSystemRestricted (ctx )
108
+ err := appReq .Validate ()
109
+ if err != nil {
110
+ p .writeWorkspaceApp500 (rw , r , & appReq , err , "invalid app request" )
111
+ return nil , "" , false
112
+ }
113
+
86
114
ticket := Ticket {
87
115
Request : appReq ,
88
116
}
@@ -101,17 +129,17 @@ func (p *Provider) ResolveRequest(rw http.ResponseWriter, r *http.Request, appRe
101
129
Optional : true ,
102
130
})
103
131
if ! ok {
104
- return nil , false
132
+ return nil , "" , false
105
133
}
106
134
107
135
// Lookup workspace app details from DB.
108
136
dbReq , err := appReq .getDatabase (dangerousSystemCtx , p .Database )
109
137
if xerrors .Is (err , sql .ErrNoRows ) {
110
138
p .writeWorkspaceApp404 (rw , r , & appReq , err .Error ())
111
- return nil , false
139
+ return nil , "" , false
112
140
} else if err != nil {
113
141
p .writeWorkspaceApp500 (rw , r , & appReq , err , "get app details from database" )
114
- return nil , false
142
+ return nil , "" , false
115
143
}
116
144
ticket .UserID = dbReq .User .ID
117
145
ticket .WorkspaceID = dbReq .Workspace .ID
@@ -124,19 +152,20 @@ func (p *Provider) ResolveRequest(rw http.ResponseWriter, r *http.Request, appRe
124
152
// Verify the user has access to the app.
125
153
authed , ok := p .verifyAuthz (rw , r , authz , dbReq )
126
154
if ! ok {
127
- return nil , false
155
+ return nil , "" , false
128
156
}
129
157
if ! authed {
130
158
if apiKey != nil {
131
159
// The request has a valid API key but insufficient permissions.
132
160
p .writeWorkspaceApp404 (rw , r , & appReq , "insufficient permissions" )
133
- return nil , false
161
+ return nil , "" , false
134
162
}
135
163
136
164
// Redirect to login as they don't have permission to access the app
137
165
// and they aren't signed in.
138
166
switch appReq .AccessMethod {
139
167
case AccessMethodPath :
168
+ // TODO(@deansheather): this doesn't work on moons
140
169
httpmw .RedirectToLogin (rw , r , httpmw .SignedOutErrorMessage )
141
170
case AccessMethodSubdomain :
142
171
// Redirect to the app auth redirect endpoint with a valid redirect
@@ -156,52 +185,41 @@ func (p *Provider) ResolveRequest(rw http.ResponseWriter, r *http.Request, appRe
156
185
// Return an error.
157
186
httpapi .ResourceNotFound (rw )
158
187
}
159
- return nil , false
188
+ return nil , "" , false
160
189
}
161
190
162
191
// Check that the agent is online.
163
192
agentStatus := dbReq .Agent .Status (p .WorkspaceAgentInactiveTimeout )
164
193
if agentStatus .Status != database .WorkspaceAgentStatusConnected {
165
194
p .writeWorkspaceAppOffline (rw , r , & appReq , fmt .Sprintf ("Agent state is %q, not %q" , agentStatus .Status , database .WorkspaceAgentStatusConnected ))
166
- return nil , false
195
+ return nil , "" , false
167
196
}
168
197
169
198
// Check that the app is healthy.
170
199
if dbReq .AppHealth != "" && dbReq .AppHealth != database .WorkspaceAppHealthDisabled && dbReq .AppHealth != database .WorkspaceAppHealthHealthy {
171
200
p .writeWorkspaceAppOffline (rw , r , & appReq , fmt .Sprintf ("App health is %q, not %q" , dbReq .AppHealth , database .WorkspaceAppHealthHealthy ))
172
- return nil , false
201
+ return nil , "" , false
173
202
}
174
203
175
204
// As a sanity check, ensure the ticket we just made is valid for this
176
205
// request.
177
206
if ! ticket .MatchesRequest (appReq ) {
178
207
p .writeWorkspaceApp500 (rw , r , & appReq , nil , "fresh ticket does not match request" )
179
- return nil , false
208
+ return nil , "" , false
180
209
}
181
210
182
211
// Sign the ticket.
183
- ticketExpiry := time .Now ().Add (TicketExpiry )
184
- ticket .Expiry = ticketExpiry .Unix ()
212
+ ticket .Expiry = time .Now ().Add (TicketExpiry )
185
213
ticketStr , err := p .GenerateTicket (ticket )
186
214
if err != nil {
187
215
p .writeWorkspaceApp500 (rw , r , & appReq , err , "generate ticket" )
188
- return nil , false
216
+ return nil , "" , false
189
217
}
190
218
191
- // Write the ticket cookie. We always want this to apply to the current
192
- // hostname (even for subdomain apps, without any wildcard shenanigans,
193
- // because the ticket is only valid for a single app).
194
- http .SetCookie (rw , & http.Cookie {
195
- Name : codersdk .DevURLSessionTicketCookie ,
196
- Value : ticketStr ,
197
- Path : appReq .BasePath ,
198
- Expires : ticketExpiry ,
199
- })
200
-
201
- return & ticket , true
219
+ return & ticket , ticketStr , true
202
220
}
203
221
204
- func (p * Provider ) authorizeRequest (ctx context.Context , roles * httpmw.Authorization , dbReq * databaseRequest ) (bool , error ) {
222
+ func (p * DBTicketProvider ) authorizeRequest (ctx context.Context , roles * httpmw.Authorization , dbReq * databaseRequest ) (bool , error ) {
205
223
accessMethod := dbReq .AccessMethod
206
224
if accessMethod == "" {
207
225
accessMethod = AccessMethodPath
@@ -293,7 +311,7 @@ func (p *Provider) authorizeRequest(ctx context.Context, roles *httpmw.Authoriza
293
311
// given app share level in the given workspace. The user's authorization status
294
312
// is returned. If a server error occurs, a HTML error page is rendered and
295
313
// false is returned so the caller can return early.
296
- func (p * Provider ) verifyAuthz (rw http.ResponseWriter , r * http.Request , authz * httpmw.Authorization , dbReq * databaseRequest ) (authed bool , ok bool ) {
314
+ func (p * DBTicketProvider ) verifyAuthz (rw http.ResponseWriter , r * http.Request , authz * httpmw.Authorization , dbReq * databaseRequest ) (authed bool , ok bool ) {
297
315
ok , err := p .authorizeRequest (r .Context (), authz , dbReq )
298
316
if err != nil {
299
317
p .Logger .Error (r .Context (), "authorize workspace app" , slog .Error (err ))
@@ -312,7 +330,7 @@ func (p *Provider) verifyAuthz(rw http.ResponseWriter, r *http.Request, authz *h
312
330
313
331
// writeWorkspaceApp404 writes a HTML 404 error page for a workspace app. If
314
332
// appReq is not nil, it will be used to log the request details at debug level.
315
- func (p * Provider ) writeWorkspaceApp404 (rw http.ResponseWriter , r * http.Request , appReq * Request , msg string ) {
333
+ func (p * DBTicketProvider ) writeWorkspaceApp404 (rw http.ResponseWriter , r * http.Request , appReq * Request , msg string ) {
316
334
if appReq != nil {
317
335
slog .Helper ()
318
336
p .Logger .Debug (r .Context (),
@@ -336,7 +354,7 @@ func (p *Provider) writeWorkspaceApp404(rw http.ResponseWriter, r *http.Request,
336
354
337
355
// writeWorkspaceApp500 writes a HTML 500 error page for a workspace app. If
338
356
// appReq is not nil, it's fields will be added to the logged error message.
339
- func (p * Provider ) writeWorkspaceApp500 (rw http.ResponseWriter , r * http.Request , appReq * Request , err error , msg string ) {
357
+ func (p * DBTicketProvider ) writeWorkspaceApp500 (rw http.ResponseWriter , r * http.Request , appReq * Request , err error , msg string ) {
340
358
slog .Helper ()
341
359
ctx := r .Context ()
342
360
if appReq != nil {
@@ -364,7 +382,7 @@ func (p *Provider) writeWorkspaceApp500(rw http.ResponseWriter, r *http.Request,
364
382
365
383
// writeWorkspaceAppOffline writes a HTML 502 error page for a workspace app. If
366
384
// appReq is not nil, it will be used to log the request details at debug level.
367
- func (p * Provider ) writeWorkspaceAppOffline (rw http.ResponseWriter , r * http.Request , appReq * Request , msg string ) {
385
+ func (p * DBTicketProvider ) writeWorkspaceAppOffline (rw http.ResponseWriter , r * http.Request , appReq * Request , msg string ) {
368
386
if appReq != nil {
369
387
slog .Helper ()
370
388
p .Logger .Debug (r .Context (),
0 commit comments