@@ -9,12 +9,15 @@ import (
9
9
"time"
10
10
11
11
"github.com/go-chi/chi/v5"
12
+ "github.com/google/cel-go/common/types"
12
13
"github.com/google/uuid"
13
14
"github.com/moby/moby/pkg/namesgenerator"
14
15
"golang.org/x/xerrors"
15
16
17
+ "cdr.dev/slog"
16
18
"github.com/coder/coder/v2/coderd/apikey"
17
19
"github.com/coder/coder/v2/coderd/audit"
20
+ celtoken"github.com/coder/coder/v2/coderd/cel"
18
21
"github.com/coder/coder/v2/coderd/database"
19
22
"github.com/coder/coder/v2/coderd/database/dbtime"
20
23
"github.com/coder/coder/v2/coderd/httpapi"
@@ -340,28 +343,14 @@ func (api *API) deleteAPIKey(rw http.ResponseWriter, r *http.Request) {
340
343
// @Router /users/{user}/keys/tokens/tokenconfig [get]
341
344
func (api * API )tokenConfig (rw http.ResponseWriter ,r * http.Request ) {
342
345
ctx := r .Context ()
343
- user := httpmw .UserParam (r )
344
346
345
- var roleIdentifiers []rbac.RoleIdentifier
346
-
347
- if user .ID != uuid .Nil {
348
- subject ,userStatus ,err := httpmw .UserRBACSubject (ctx ,api .Database ,user .ID ,rbac .ScopeAll )
349
- switch {
350
- case err != nil :
351
- api .Logger .Error (ctx ,"failed to get user RBAC subject for token config" ,"user_id" ,user .ID .String (),"error" ,err )
352
- roleIdentifiers = []rbac.RoleIdentifier {}
353
- case userStatus == database .UserStatusSuspended :
354
- roleIdentifiers = []rbac.RoleIdentifier {}
355
- default :
356
- // Extract role names from the RBAC subject and convert to internal format
357
- roleIdentifiers = subject .Roles .Names ()
358
- }
359
- }else {
360
- api .Logger .Warn (ctx ,"user ID is nil in token config request context" )
361
- roleIdentifiers = []rbac.RoleIdentifier {}
362
- }
347
+ maxLifetime := api .DeploymentValues .Sessions .MaximumTokenDuration .Value ()
363
348
364
- maxLifetime := api .getMaxTokenLifetimeForUserRoles (roleIdentifiers )
349
+ user := httpmw .UserParam (r )
350
+ subject ,_ ,err := httpmw .UserRBACSubject (ctx ,api .Database ,user .ID ,rbac .ScopeAll )
351
+ if err == nil {
352
+ maxLifetime = api .getMaxTokenLifetimeForUser (ctx ,subject )
353
+ }
365
354
366
355
httpapi .Write (
367
356
ctx ,rw ,http .StatusOK ,
@@ -378,20 +367,19 @@ func (api *API) validateAPIKeyLifetime(ctx context.Context, lifetime time.Durati
378
367
379
368
subject ,userStatus ,err := httpmw .UserRBACSubject (ctx ,api .Database ,userID ,rbac .ScopeAll )
380
369
if err != nil {
381
- api .Logger .Error (ctx ,"failed to get user RBAC subject during token validation" ,"user_id" ,userID .String (), "error" , err )
370
+ api .Logger .Error (ctx ,"failed to get user RBAC subject during token validation" ,slog . F ( "user_id" ,userID .String ()), slog . Error ( err ) )
382
371
if xerrors .Is (err ,sql .ErrNoRows ) {
383
372
return xerrors .Errorf ("user %s not found" ,userID )
384
373
}
385
374
return xerrors .Errorf ("internal server error validating token lifetime for user %s" ,userID )
386
375
}
387
376
388
- if userStatus == database .UserStatusSuspended {
389
- return xerrors .Errorf ("user %s is suspended and cannot create tokens" ,userID )
377
+ if userStatus != database .UserStatusActive {
378
+ return xerrors .Errorf ("user %s is suspendedor inactive, and cannot create tokens" ,userID )
390
379
}
391
380
392
- // Extract role names from the RBAC subject and convert to internal format
393
- roleIdentifiers := subject .Roles .Names ()
394
- maxAllowedLifetime := api .getMaxTokenLifetimeForUserRoles (roleIdentifiers )
381
+ // Get the maximum token lifetime for this user based on CEL expression
382
+ maxAllowedLifetime := api .getMaxTokenLifetimeForUser (ctx ,subject )
395
383
396
384
if lifetime > maxAllowedLifetime {
397
385
return xerrors .Errorf (
@@ -403,35 +391,47 @@ func (api *API) validateAPIKeyLifetime(ctx context.Context, lifetime time.Durati
403
391
return nil
404
392
}
405
393
406
- // getMaxTokenLifetimeForUserRoles determines the most generous token lifetime a user is entitled to
407
- // based on their roles and the CODER_ROLE_TOKEN_LIFETIMES configuration.
408
- // Roles are expected in the internal format ("rolename" or "rolename:org_id").
409
- func (api * API )getMaxTokenLifetimeForUserRoles (roles []rbac.RoleIdentifier ) time.Duration {
410
- globalMaxDefault := api .DeploymentValues .Sessions .MaximumTokenDuration .Value ()
411
-
412
- // Early return for empty config
413
- if api .DeploymentValues .Sessions .RoleTokenLifetimes .Value ()== "" ||
414
- api .DeploymentValues .Sessions .RoleTokenLifetimes .Value ()== "{}" {
415
- return globalMaxDefault
394
+ // getMaxTokenLifetimeForUser determines the maximum token lifetime a user is entitled to
395
+ // based on their attributes and the CEL expression configuration.
396
+ func (api * API )getMaxTokenLifetimeForUser (ctx context.Context ,subject rbac.Subject ) time.Duration {
397
+ // Compiled at startup no need to recheck here.
398
+ program ,_ := api .DeploymentValues .Sessions .CompiledMaximumTokenDurationProgram ()
399
+ if program == nil {
400
+ // No expression configured, use global max
401
+ return api .DeploymentValues .Sessions .MaximumTokenDuration .Value ()
416
402
}
417
403
418
- // Early return for no roles
419
- if len (roles )== 0 {
420
- return globalMaxDefault
421
- }
404
+ globalMax := api .DeploymentValues .Sessions .MaximumTokenDuration .Value ()
405
+ defaultDuration := api .DeploymentValues .Sessions .DefaultTokenDuration .Value ()
422
406
423
- // Find the maximum lifetime among all roles
424
- // This includes both role-specific lifetimes and the global default
425
- maxLifetime := globalMaxDefault
407
+ // Convert subject to CEL-friendly format
408
+ celSubject := celtoken .ConvertSubjectToCEL (subject )
426
409
427
- for _ ,role := range roles {
428
- roleDuration := api .DeploymentValues .Sessions .MaxTokenLifetimeForRole (role )
429
- if roleDuration > maxLifetime {
430
- maxLifetime = roleDuration
431
- }
410
+ // Evaluate CEL expression with typed struct
411
+ // TODO: Consider adding timeout protection in future iterations
412
+ out ,_ ,err := program .Eval (map [string ]interface {}{
413
+ "subject" :celSubject ,
414
+ "globalMaxDuration" :globalMax ,
415
+ "defaultDuration" :defaultDuration ,
416
+ })
417
+ if err != nil {
418
+ api .Logger .Error (ctx ,"the CEL evaluation failed, using default duration" ,slog .Error (err ))
419
+ return defaultDuration
432
420
}
433
421
434
- return maxLifetime
422
+ // Convert result to time.Duration
423
+ // CEL returns types.Duration, not time.Duration directly
424
+ switch v := out .Value ().(type ) {
425
+ case types.Duration :
426
+ return v .Duration
427
+ case time.Duration :
428
+ return v
429
+ default :
430
+ api .Logger .Error (ctx ,"the CEL expression did not return a duration, using default duration" ,
431
+ slog .F ("result_type" ,fmt .Sprintf ("%T" ,out .Value ())),
432
+ slog .F ("result_value" ,out .Value ()))
433
+ return defaultDuration
434
+ }
435
435
}
436
436
437
437
func (api * API )createAPIKey (ctx context.Context ,params apikey.CreateParams ) (* http.Cookie ,* database.APIKey ,error ) {