@@ -5,81 +5,66 @@ import (
5
5
"database/sql"
6
6
"fmt"
7
7
"net/http"
8
- "strings "
8
+ "net/url "
9
9
"time"
10
10
11
11
"golang.org/x/xerrors"
12
12
13
13
"cdr.dev/slog"
14
+
14
15
"github.com/coder/coder/coderd/database"
15
16
"github.com/coder/coder/coderd/database/dbauthz"
16
17
"github.com/coder/coder/coderd/httpapi"
17
18
"github.com/coder/coder/coderd/httpmw"
18
19
"github.com/coder/coder/coderd/rbac"
19
20
"github.com/coder/coder/codersdk"
20
- "github.com/coder/coder/site"
21
21
)
22
22
23
- const (
24
- // TODO(@deansheather): configurable expiry
25
- TicketExpiry = time .Minute
26
-
27
- // RedirectURIQueryParam is the query param for the app URL to be passed
28
- // back to the API auth endpoint on the main access URL.
29
- RedirectURIQueryParam = "redirect_uri"
30
- )
23
+ // DBTicketProvider provides authentication and authorization for workspace apps
24
+ // by querying the database if the request is missing a valid ticket.
25
+ type DBTicketProvider struct {
26
+ Logger slog.Logger
27
+
28
+ AccessURL * url.URL
29
+ Authorizer rbac.Authorizer
30
+ Database database.Store
31
+ DeploymentValues * codersdk.DeploymentValues
32
+ OAuth2Configs * httpmw.OAuth2Configs
33
+ WorkspaceAgentInactiveTimeout time.Duration
34
+ TicketSigningKey []byte
35
+ }
31
36
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
35
- if appReq .WorkspaceAndAgent != "" {
36
- // workspace.agent
37
- workspaceAndAgent := strings .SplitN (appReq .WorkspaceAndAgent , "." , 2 )
38
- appReq .WorkspaceAndAgent = ""
39
- appReq .WorkspaceNameOrID = workspaceAndAgent [0 ]
40
- if len (workspaceAndAgent ) > 1 {
41
- appReq .AgentNameOrID = workspaceAndAgent [1 ]
42
- }
37
+ var _ TicketProvider = & DBTicketProvider {}
43
38
44
- // Sanity check.
45
- err := appReq .Validate ()
46
- if err != nil {
47
- p .writeWorkspaceApp500 (rw , r , & appReq , err , "invalid app request" )
48
- return nil , false
49
- }
39
+ func NewDBTicketProvider (log slog.Logger , accessURL * url.URL , authz rbac.Authorizer , db database.Store , cfg * codersdk.DeploymentValues , oauth2Cfgs * httpmw.OAuth2Configs , workspaceAgentInactiveTimeout time.Duration , ticketSigningKey []byte ) TicketProvider {
40
+ if len (ticketSigningKey ) != 64 {
41
+ panic ("ticket signing key must be 64 bytes" )
50
42
}
51
43
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
44
+ if workspaceAgentInactiveTimeout == 0 {
45
+ workspaceAgentInactiveTimeout = 1 * time .Minute
56
46
}
57
47
58
- ticket , ticketStr , ok := p .CreateTicket (r .Context (), rw , r , appReq )
59
- if ! ok {
60
- return nil , false
48
+ return & DBTicketProvider {
49
+ Logger : log ,
50
+ AccessURL : accessURL ,
51
+ Authorizer : authz ,
52
+ Database : db ,
53
+ DeploymentValues : cfg ,
54
+ OAuth2Configs : oauth2Cfgs ,
55
+ WorkspaceAgentInactiveTimeout : workspaceAgentInactiveTimeout ,
56
+ TicketSigningKey : ticketSigningKey ,
61
57
}
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
58
}
75
59
76
60
func (p * DBTicketProvider ) TicketFromRequest (r * http.Request ) (* Ticket , bool ) {
77
61
// Get the existing ticket from the request.
78
62
ticketCookie , err := r .Cookie (codersdk .DevURLSessionTicketCookie )
79
63
if err == nil {
80
- ticket , err := p . ParseTicket (ticketCookie .Value )
64
+ ticket , err := ParseTicket (p . TicketSigningKey , ticketCookie .Value )
81
65
if err == nil {
82
- err := ticket .Request .Validate ()
66
+ req := ticket .Request .Normalize ()
67
+ err := req .Validate ()
83
68
if err == nil {
84
69
// The request has a ticket, which is a valid ticket signed by
85
70
// us. The caller must check that it matches the request.
@@ -105,9 +90,11 @@ func (p *DBTicketProvider) CreateTicket(ctx context.Context, rw http.ResponseWri
105
90
// // logic is handled in Provider.authorizeWorkspaceApp which directly checks the actor's
106
91
// // permissions.
107
92
dangerousSystemCtx := dbauthz .AsSystemRestricted (ctx )
93
+
94
+ appReq = appReq .Normalize ()
108
95
err := appReq .Validate ()
109
96
if err != nil {
110
- p . writeWorkspaceApp500 ( rw , r , & appReq , err , "invalid app request" )
97
+ WriteWorkspaceApp500 ( p . Logger , p . AccessURL , rw , r , & appReq , err , "invalid app request" )
111
98
return nil , "" , false
112
99
}
113
100
@@ -135,10 +122,10 @@ func (p *DBTicketProvider) CreateTicket(ctx context.Context, rw http.ResponseWri
135
122
// Lookup workspace app details from DB.
136
123
dbReq , err := appReq .getDatabase (dangerousSystemCtx , p .Database )
137
124
if xerrors .Is (err , sql .ErrNoRows ) {
138
- p . writeWorkspaceApp404 ( rw , r , & appReq , err .Error ())
125
+ WriteWorkspaceApp404 ( p . Logger , p . AccessURL , rw , r , & appReq , err .Error ())
139
126
return nil , "" , false
140
127
} else if err != nil {
141
- p . writeWorkspaceApp500 ( rw , r , & appReq , err , "get app details from database" )
128
+ WriteWorkspaceApp500 ( p . Logger , p . AccessURL , rw , r , & appReq , err , "get app details from database" )
142
129
return nil , "" , false
143
130
}
144
131
ticket .UserID = dbReq .User .ID
@@ -150,14 +137,15 @@ func (p *DBTicketProvider) CreateTicket(ctx context.Context, rw http.ResponseWri
150
137
// is not running.
151
138
152
139
// Verify the user has access to the app.
153
- authed , ok := p .verifyAuthz (rw , r , authz , dbReq )
154
- if ! ok {
140
+ authed , err := p .authorizeRequest (r .Context (), authz , dbReq )
141
+ if err != nil {
142
+ WriteWorkspaceApp500 (p .Logger , p .AccessURL , rw , r , & appReq , err , "verify authz" )
155
143
return nil , "" , false
156
144
}
157
145
if ! authed {
158
146
if apiKey != nil {
159
147
// The request has a valid API key but insufficient permissions.
160
- p . writeWorkspaceApp404 ( rw , r , & appReq , "insufficient permissions" )
148
+ WriteWorkspaceApp404 ( p . Logger , p . AccessURL , rw , r , & appReq , "insufficient permissions" )
161
149
return nil , "" , false
162
150
}
163
151
@@ -191,28 +179,28 @@ func (p *DBTicketProvider) CreateTicket(ctx context.Context, rw http.ResponseWri
191
179
// Check that the agent is online.
192
180
agentStatus := dbReq .Agent .Status (p .WorkspaceAgentInactiveTimeout )
193
181
if agentStatus .Status != database .WorkspaceAgentStatusConnected {
194
- p . writeWorkspaceAppOffline ( rw , r , & appReq , fmt .Sprintf ("Agent state is %q, not %q" , agentStatus .Status , database .WorkspaceAgentStatusConnected ))
182
+ WriteWorkspaceAppOffline ( p . Logger , p . AccessURL , rw , r , & appReq , fmt .Sprintf ("Agent state is %q, not %q" , agentStatus .Status , database .WorkspaceAgentStatusConnected ))
195
183
return nil , "" , false
196
184
}
197
185
198
186
// Check that the app is healthy.
199
187
if dbReq .AppHealth != "" && dbReq .AppHealth != database .WorkspaceAppHealthDisabled && dbReq .AppHealth != database .WorkspaceAppHealthHealthy {
200
- p . writeWorkspaceAppOffline ( rw , r , & appReq , fmt .Sprintf ("App health is %q, not %q" , dbReq .AppHealth , database .WorkspaceAppHealthHealthy ))
188
+ WriteWorkspaceAppOffline ( p . Logger , p . AccessURL , rw , r , & appReq , fmt .Sprintf ("App health is %q, not %q" , dbReq .AppHealth , database .WorkspaceAppHealthHealthy ))
201
189
return nil , "" , false
202
190
}
203
191
204
192
// As a sanity check, ensure the ticket we just made is valid for this
205
193
// request.
206
194
if ! ticket .MatchesRequest (appReq ) {
207
- p . writeWorkspaceApp500 ( rw , r , & appReq , nil , "fresh ticket does not match request" )
195
+ WriteWorkspaceApp500 ( p . Logger , p . AccessURL , rw , r , & appReq , nil , "fresh ticket does not match request" )
208
196
return nil , "" , false
209
197
}
210
198
211
199
// Sign the ticket.
212
200
ticket .Expiry = time .Now ().Add (TicketExpiry )
213
- ticketStr , err := p . GenerateTicket (ticket )
201
+ ticketStr , err := GenerateTicket (p . TicketSigningKey , ticket )
214
202
if err != nil {
215
- p . writeWorkspaceApp500 ( rw , r , & appReq , err , "generate ticket" )
203
+ WriteWorkspaceApp500 ( p . Logger , p . AccessURL , rw , r , & appReq , err , "generate ticket" )
216
204
return nil , "" , false
217
205
}
218
206
@@ -306,100 +294,3 @@ func (p *DBTicketProvider) authorizeRequest(ctx context.Context, roles *httpmw.A
306
294
// No checks were successful.
307
295
return false , nil
308
296
}
309
-
310
- // verifyAuthz authorizes the user using api.Authorizer for a
311
- // given app share level in the given workspace. The user's authorization status
312
- // is returned. If a server error occurs, a HTML error page is rendered and
313
- // false is returned so the caller can return early.
314
- func (p * DBTicketProvider ) verifyAuthz (rw http.ResponseWriter , r * http.Request , authz * httpmw.Authorization , dbReq * databaseRequest ) (authed bool , ok bool ) {
315
- ok , err := p .authorizeRequest (r .Context (), authz , dbReq )
316
- if err != nil {
317
- p .Logger .Error (r .Context (), "authorize workspace app" , slog .Error (err ))
318
- site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
319
- Status : http .StatusInternalServerError ,
320
- Title : "Internal Server Error" ,
321
- Description : "Could not verify authorization. Please try again or contact an administrator." ,
322
- RetryEnabled : true ,
323
- DashboardURL : p .AccessURL .String (),
324
- })
325
- return false , false
326
- }
327
-
328
- return ok , true
329
- }
330
-
331
- // writeWorkspaceApp404 writes a HTML 404 error page for a workspace app. If
332
- // appReq is not nil, it will be used to log the request details at debug level.
333
- func (p * DBTicketProvider ) writeWorkspaceApp404 (rw http.ResponseWriter , r * http.Request , appReq * Request , msg string ) {
334
- if appReq != nil {
335
- slog .Helper ()
336
- p .Logger .Debug (r .Context (),
337
- "workspace app 404: " + msg ,
338
- slog .F ("username_or_id" , appReq .UsernameOrID ),
339
- slog .F ("workspace_and_agent" , appReq .WorkspaceAndAgent ),
340
- slog .F ("workspace_name_or_id" , appReq .WorkspaceNameOrID ),
341
- slog .F ("agent_name_or_id" , appReq .AgentNameOrID ),
342
- slog .F ("app_slug_or_port" , appReq .AppSlugOrPort ),
343
- )
344
- }
345
-
346
- site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
347
- Status : http .StatusNotFound ,
348
- Title : "Application Not Found" ,
349
- Description : "The application or workspace you are trying to access does not exist or you do not have permission to access it." ,
350
- RetryEnabled : false ,
351
- DashboardURL : p .AccessURL .String (),
352
- })
353
- }
354
-
355
- // writeWorkspaceApp500 writes a HTML 500 error page for a workspace app. If
356
- // appReq is not nil, it's fields will be added to the logged error message.
357
- func (p * DBTicketProvider ) writeWorkspaceApp500 (rw http.ResponseWriter , r * http.Request , appReq * Request , err error , msg string ) {
358
- slog .Helper ()
359
- ctx := r .Context ()
360
- if appReq != nil {
361
- slog .With (ctx ,
362
- slog .F ("username_or_id" , appReq .UsernameOrID ),
363
- slog .F ("workspace_and_agent" , appReq .WorkspaceAndAgent ),
364
- slog .F ("workspace_name_or_id" , appReq .WorkspaceNameOrID ),
365
- slog .F ("agent_name_or_id" , appReq .AgentNameOrID ),
366
- slog .F ("app_name_or_port" , appReq .AppSlugOrPort ),
367
- )
368
- }
369
- p .Logger .Warn (ctx ,
370
- "workspace app auth server error: " + msg ,
371
- slog .Error (err ),
372
- )
373
-
374
- site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
375
- Status : http .StatusInternalServerError ,
376
- Title : "Internal Server Error" ,
377
- Description : "An internal server error occurred." ,
378
- RetryEnabled : false ,
379
- DashboardURL : p .AccessURL .String (),
380
- })
381
- }
382
-
383
- // writeWorkspaceAppOffline writes a HTML 502 error page for a workspace app. If
384
- // appReq is not nil, it will be used to log the request details at debug level.
385
- func (p * DBTicketProvider ) writeWorkspaceAppOffline (rw http.ResponseWriter , r * http.Request , appReq * Request , msg string ) {
386
- if appReq != nil {
387
- slog .Helper ()
388
- p .Logger .Debug (r .Context (),
389
- "workspace app unavailable: " + msg ,
390
- slog .F ("username_or_id" , appReq .UsernameOrID ),
391
- slog .F ("workspace_and_agent" , appReq .WorkspaceAndAgent ),
392
- slog .F ("workspace_name_or_id" , appReq .WorkspaceNameOrID ),
393
- slog .F ("agent_name_or_id" , appReq .AgentNameOrID ),
394
- slog .F ("app_slug_or_port" , appReq .AppSlugOrPort ),
395
- )
396
- }
397
-
398
- site .RenderStaticErrorPage (rw , r , site.ErrorPageData {
399
- Status : http .StatusBadGateway ,
400
- Title : "Application Unavailable" ,
401
- Description : msg ,
402
- RetryEnabled : true ,
403
- DashboardURL : p .AccessURL .String (),
404
- })
405
- }
0 commit comments