@@ -9,12 +9,14 @@ 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
16
17
"github.com/coder/coder/v2/coderd/apikey"
17
18
"github.com/coder/coder/v2/coderd/audit"
19
+ celtoken"github.com/coder/coder/v2/coderd/cel"
18
20
"github.com/coder/coder/v2/coderd/database"
19
21
"github.com/coder/coder/v2/coderd/database/dbtime"
20
22
"github.com/coder/coder/v2/coderd/httpapi"
@@ -340,29 +342,19 @@ func (api *API) deleteAPIKey(rw http.ResponseWriter, r *http.Request) {
340
342
// @Router /users/{user}/keys/tokens/tokenconfig [get]
341
343
func (api * API )tokenConfig (rw http.ResponseWriter ,r * http.Request ) {
342
344
ctx := r .Context ()
343
- user := httpmw .UserParam (r )
344
345
345
- var roleIdentifiers []rbac. RoleIdentifier
346
+ maxLifetime := api . DeploymentValues . Sessions . MaximumTokenDuration . Value ()
346
347
348
+ user := httpmw .UserParam (r )
347
349
if user .ID != uuid .Nil {
348
350
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 ()
351
+ if err == nil && userStatus != database .UserStatusSuspended {
352
+ maxLifetime = api .getMaxTokenLifetimeForUser (ctx ,subject )
358
353
}
359
354
}else {
360
355
api .Logger .Warn (ctx ,"user ID is nil in token config request context" )
361
- roleIdentifiers = []rbac.RoleIdentifier {}
362
356
}
363
357
364
- maxLifetime := api .getMaxTokenLifetimeForUserRoles (roleIdentifiers )
365
-
366
358
httpapi .Write (
367
359
ctx ,rw ,http .StatusOK ,
368
360
codersdk.TokenConfig {
@@ -389,9 +381,8 @@ func (api *API) validateAPIKeyLifetime(ctx context.Context, lifetime time.Durati
389
381
return xerrors .Errorf ("user %s is suspended and cannot create tokens" ,userID )
390
382
}
391
383
392
- // Extract role names from the RBAC subject and convert to internal format
393
- roleIdentifiers := subject .Roles .Names ()
394
- maxAllowedLifetime := api .getMaxTokenLifetimeForUserRoles (roleIdentifiers )
384
+ // Get the maximum token lifetime for this user based on CEL expression
385
+ maxAllowedLifetime := api .getMaxTokenLifetimeForUser (ctx ,subject )
395
386
396
387
if lifetime > maxAllowedLifetime {
397
388
return xerrors .Errorf (
@@ -403,35 +394,46 @@ func (api *API) validateAPIKeyLifetime(ctx context.Context, lifetime time.Durati
403
394
return nil
404
395
}
405
396
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
397
+ // getMaxTokenLifetimeForUser determines the maximum token lifetime a user is entitled to
398
+ // based on their attributes and the CEL expression configuration.
399
+ func (api * API )getMaxTokenLifetimeForUser (ctx context.Context ,subject rbac.Subject ) time.Duration {
400
+ program := api .DeploymentValues .Sessions .CompiledMaximumTokenDurationProgram ()
401
+ if program == nil {
402
+ // No expression configured, use global max
403
+ return api .DeploymentValues .Sessions .MaximumTokenDuration .Value ()
416
404
}
417
405
418
- // Early return for no roles
419
- if len (roles )== 0 {
420
- return globalMaxDefault
421
- }
406
+ globalMax := api .DeploymentValues .Sessions .MaximumTokenDuration .Value ()
407
+ defaultDuration := api .DeploymentValues .Sessions .DefaultTokenDuration .Value ()
422
408
423
- // Find the maximum lifetime among all roles
424
- // This includes both role-specific lifetimes and the global default
425
- maxLifetime := globalMaxDefault
409
+ // Convert subject to CEL-friendly format
410
+ celSubject := celtoken .ConvertSubjectToCEL (subject )
426
411
427
- for _ ,role := range roles {
428
- roleDuration := api .DeploymentValues .Sessions .MaxTokenLifetimeForRole (role )
429
- if roleDuration > maxLifetime {
430
- maxLifetime = roleDuration
431
- }
412
+ // Evaluate CEL expression with typed struct
413
+ // TODO: Consider adding timeout protection in future iterations
414
+ out ,_ ,err := program .Eval (map [string ]interface {}{
415
+ "subject" :celSubject ,
416
+ "globalMaxDuration" :globalMax ,
417
+ "defaultDuration" :defaultDuration ,
418
+ })
419
+ if err != nil {
420
+ api .Logger .Error (ctx ,"cel evaluation failed, using default duration" ,"error" ,err )
421
+ return defaultDuration
432
422
}
433
423
434
- return maxLifetime
424
+ // Convert result to time.Duration
425
+ // CEL returns types.Duration, not time.Duration directly
426
+ switch v := out .Value ().(type ) {
427
+ case types.Duration :
428
+ return v .Duration
429
+ case time.Duration :
430
+ return v
431
+ default :
432
+ api .Logger .Error (ctx ,"cel expression did not return a duration, using default duration" ,
433
+ "result_type" ,fmt .Sprintf ("%T" ,out .Value ()),
434
+ "result_value" ,out .Value ())
435
+ return defaultDuration
436
+ }
435
437
}
436
438
437
439
func (api * API )createAPIKey (ctx context.Context ,params apikey.CreateParams ) (* http.Cookie ,* database.APIKey ,error ) {