Skip to content

Commit f080b23

Browse files
committed
refactor: replace CEL with expr for token lifetime expressions
Change-Id: I2dcfa21535a8c5d4b2276622617782f0b2c47603 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent e0261c5 commit f080b23

17 files changed

+607
-995
lines changed

cli/testdata/coder_server_--help.golden

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,13 @@ OPTIONS:
5151
all available experiments.
5252

5353
--max-token-lifetime-expression string, $CODER_MAX_TOKEN_LIFETIME_EXPRESSION
54-
A CEL expression that determines the maximum token lifetime based on
55-
user attributes. The expression has access to 'subject'
56-
(rbac.Subject), 'globalMaxDuration' (time.Duration),
57-
and'defaultDuration' (time.Duration). Must return a duration string
58-
(e.g., duration("168h")). Example: 'subject.roles.exists(r, r.name ==
59-
"owner") ? duration(globalMaxDuration) : duration(defaultDuration)'.
60-
See https://github.com/google/cel-spec for CEL expression syntax and
61-
examples.
54+
An expr expression that determines the maximum token lifetime based on
55+
user attributes. The expression has access to 'subject' (Subject),
56+
'globalMaxDuration' (time.Duration), and 'defaultDuration'
57+
(time.Duration). Must return a duration (e.g., duration("168h")).
58+
Example: 'any(subject.Roles, .Name == "owner") ? duration("720h") :
59+
duration("168h")'. See https://github.com/expr-lang/expr for expr
60+
expression syntax and examples.
6261

6362
--postgres-auth password|awsiamrds, $CODER_PG_AUTH (default: password)
6463
Type of auth to use when connecting to postgres. For AWS RDS, using

cli/testdata/server-config.yaml.golden

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -445,13 +445,12 @@ experiments: []
445445
# performed once per day.
446446
# (default: false, type: bool)
447447
updateCheck: false
448-
# A CEL expression that determines the maximum token lifetime based on user
449-
# attributes. The expression has access to 'subject' (rbac.Subject),
450-
# 'globalMaxDuration' (time.Duration), and'defaultDuration' (time.Duration). Must
451-
# return a duration string (e.g., duration("168h")). Example:
452-
# 'subject.roles.exists(r, r.name == "owner") ? duration(globalMaxDuration) :
453-
# duration(defaultDuration)'. See https://github.com/google/cel-spec for CEL
454-
# expression syntax and examples.
448+
# An expr expression that determines the maximum token lifetime based on user
449+
# attributes. The expression has access to 'subject' (Subject),
450+
# 'globalMaxDuration' (time.Duration), and 'defaultDuration' (time.Duration). Must
451+
# return a duration (e.g., duration("168h")). Example: 'any(subject.Roles, .Name
452+
# == "owner") ? duration("720h") : duration("168h")'. See
453+
# https://github.com/expr-lang/expr for expr expression syntax and examples.
455454
# (default: <unset>, type: string)
456455
maxTokenLifetimeExpression: ""
457456
# The default lifetime duration for API tokens. This value is used when creating a

coderd/apidoc/docs.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apikey.go

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@ import (
99
"time"
1010

1111
"github.com/go-chi/chi/v5"
12-
"github.com/google/cel-go/common/types"
1312
"github.com/google/uuid"
1413
"github.com/moby/moby/pkg/namesgenerator"
1514
"golang.org/x/xerrors"
1615

16+
"github.com/expr-lang/expr"
17+
1718
"cdr.dev/slog"
1819
"github.com/coder/coder/v2/coderd/apikey"
1920
"github.com/coder/coder/v2/coderd/audit"
20-
celtoken "github.com/coder/coder/v2/coderd/cel"
2121
"github.com/coder/coder/v2/coderd/database"
2222
"github.com/coder/coder/v2/coderd/database/dbtime"
23+
exprtoken "github.com/coder/coder/v2/coderd/expr"
2324
"github.com/coder/coder/v2/coderd/httpapi"
2425
"github.com/coder/coder/v2/coderd/httpmw"
2526
"github.com/coder/coder/v2/coderd/rbac"
@@ -392,7 +393,7 @@ func (api *API) validateAPIKeyLifetime(ctx context.Context, lifetime time.Durati
392393
}
393394

394395
// getMaxTokenLifetimeForUser determines the maximum token lifetime a user is entitled to
395-
// based on their attributes and the CEL expression configuration.
396+
// based on their attributes and the expr expression configuration.
396397
func (api *API) getMaxTokenLifetimeForUser(ctx context.Context, subject rbac.Subject) time.Duration {
397398
// Compiled at startup no need to recheck here.
398399
program, _ := api.DeploymentValues.Sessions.CompiledMaximumTokenDurationProgram()
@@ -404,34 +405,30 @@ func (api *API) getMaxTokenLifetimeForUser(ctx context.Context, subject rbac.Sub
404405
globalMax := api.DeploymentValues.Sessions.MaximumTokenDuration.Value()
405406
defaultDuration := api.DeploymentValues.Sessions.DefaultTokenDuration.Value()
406407

407-
// Convert subject to CEL-friendly format
408-
celSubject := celtoken.ConvertSubjectToCEL(subject)
408+
// Convert subject to expr-friendly format
409+
exprSubject := exprtoken.ConvertSubjectToExpr(subject)
409410

410-
// Evaluate CEL expression with typed struct
411+
// Evaluate expr expression with typed struct
411412
// 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,
413+
out, err := expr.Run(program, map[string]interface{}{
414+
"subject": exprSubject,
415+
"globalMaxDuration": int64(globalMax),
416+
"defaultDuration": int64(defaultDuration),
416417
})
417418
if err != nil {
418-
api.Logger.Error(ctx, "the CEL evaluation failed, using default duration", slog.Error(err))
419+
api.Logger.Error(ctx, "the expr evaluation failed, using default duration", slog.Error(err))
419420
return defaultDuration
420421
}
421422

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()))
423+
// Convert result to time.Duration (expr returns int64 due to AsInt64 constraint)
424+
intVal, ok := out.(int64)
425+
if !ok {
426+
api.Logger.Error(ctx, "the expr expression did not return an int64, using default duration",
427+
slog.F("result_type", fmt.Sprintf("%T", out)),
428+
slog.F("result_value", out))
433429
return defaultDuration
434430
}
431+
return time.Duration(intVal)
435432
}
436433

437434
func (api *API) createAPIKey(ctx context.Context, params apikey.CreateParams) (*http.Cookie, *database.APIKey, error) {

coderd/apikey_rolelifetime_test.go

Lines changed: 51 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import (
77

88
"github.com/stretchr/testify/require"
99

10-
"github.com/coder/coder/v2/coderd/cel"
1110
"github.com/coder/coder/v2/coderd/coderdtest"
11+
"github.com/coder/coder/v2/coderd/expr"
1212
"github.com/coder/coder/v2/codersdk"
1313
"github.com/coder/serpent"
1414
)
@@ -39,9 +39,9 @@ func createClientWithRoleTokenLifetimes(t *testing.T, roleTokenLifetimeExpressio
3939
t.Logf("MaximumTokenDuration: %v", api.DeploymentValues.Sessions.MaximumTokenDuration.Value())
4040
// Check if we have a compiled program
4141
if program != nil {
42-
t.Logf("CEL expression compiled successfully")
42+
t.Logf("expr expression compiled successfully")
4343
} else {
44-
t.Logf("No CEL expression configured")
44+
t.Logf("No expr expression configured")
4545
}
4646

4747
// Create the first user
@@ -53,29 +53,29 @@ func createClientWithRoleTokenLifetimes(t *testing.T, roleTokenLifetimeExpressio
5353
func TestRoleBasedTokenLifetimes_Integration(t *testing.T) {
5454
t.Parallel()
5555

56-
t.Run("ServerStartupWithValidCELExpressions", func(t *testing.T) {
56+
t.Run("ServerStartupWithValidExprExpressions", func(t *testing.T) {
5757
t.Parallel()
5858

59-
// Test server starts successfully with valid CEL expressions
59+
// Test server starts successfully with valid expr expressions
6060
testCases := []struct {
61-
name string
62-
celExpression string
61+
name string
62+
exprExpression string
6363
}{
6464
{
65-
name: "ValidRoleBasedExpression",
66-
celExpression: `subject.roles.exists(r, r.name == "owner") ? duration("168h") : subject.roles.exists(r, r.name == "user-admin") ? duration("72h") : duration("24h")`,
65+
name: "ValidRoleBasedExpression",
66+
exprExpression: `any(subject.Roles, .Name == "owner") ? duration("168h") : any(subject.Roles, .Name == "user-admin") ? duration("72h") : duration("24h")`,
6767
},
6868
{
69-
name: "ValidSimpleExpression",
70-
celExpression: `duration(globalMaxDuration)`,
69+
name: "ValidSimpleExpression",
70+
exprExpression: `duration(globalMaxDuration)`,
7171
},
7272
{
73-
name: "EmptyExpression",
74-
celExpression: ``,
73+
name: "EmptyExpression",
74+
exprExpression: ``,
7575
},
7676
{
77-
name: "EmailBasedExpression",
78-
celExpression: `subject.email.endsWith("@company.com") ? duration("720h") : duration("24h")`,
77+
name: "EmailBasedExpression",
78+
exprExpression: `subject.Email endsWith "@company.com") ? duration("720h") : duration("24h")`,
7979
},
8080
}
8181

@@ -85,7 +85,7 @@ func TestRoleBasedTokenLifetimes_Integration(t *testing.T) {
8585
t.Parallel()
8686

8787
dv := coderdtest.DeploymentValues(t)
88-
dv.Sessions.MaximumTokenDurationExpression = serpent.String(tc.celExpression)
88+
dv.Sessions.MaximumTokenDurationExpression = serpent.String(tc.exprExpression)
8989

9090
// Should create successfully
9191
client := coderdtest.New(t, &coderdtest.Options{
@@ -96,21 +96,21 @@ func TestRoleBasedTokenLifetimes_Integration(t *testing.T) {
9696
}
9797
})
9898

99-
t.Run("InvalidCELExpressions", func(t *testing.T) {
99+
t.Run("InvalidExprExpressions", func(t *testing.T) {
100100
t.Parallel()
101101

102-
// Test that invalid CEL expressions fail validation
102+
// Test that invalid expr expressions fail validation
103103
testCases := []struct {
104-
name string
105-
celExpression string
104+
name string
105+
exprExpression string
106106
}{
107107
{
108-
name: "InvalidCELSyntax",
109-
celExpression: `subject.roles.exists(r, r.name == "owner" ? duration("168h")`, // Missing closing parenthesis
108+
name: "InvalidExprSyntax",
109+
exprExpression: `any(subject.Roles, .Name == "owner" ? duration("168h")`, // Missing closing parenthesis
110110
},
111111
{
112-
name: "UndefinedVariable",
113-
celExpression: `unknownVariable ? duration("720h") : duration("168h")`,
112+
name: "UndefinedVariable",
113+
exprExpression: `unknownVariable ? duration("720h") : duration("168h")`,
114114
},
115115
}
116116

@@ -119,17 +119,14 @@ func TestRoleBasedTokenLifetimes_Integration(t *testing.T) {
119119
t.Run(tc.name, func(t *testing.T) {
120120
t.Parallel()
121121

122-
// For invalid CEL expressions, try to create the environment and compile
123-
env, err := cel.NewTokenLifetimeEnvironment(cel.EnvironmentOptions{})
124-
require.NoError(t, err)
125-
126-
_, issues := env.Compile(tc.celExpression)
127-
if issues != nil && issues.Err() != nil {
128-
// CEL compilation failed as expected
122+
// For invalid expr expressions, try to compile
123+
_, err := expr.CompileTokenLifetimeExpression(tc.exprExpression)
124+
if err != nil {
125+
// expr compilation failed as expected
129126
return
130127
}
131128
// If compilation succeeded but we expected failure, that's also a test failure
132-
t.Fatalf("Expected CEL expression to fail compilation but it succeeded: %s", tc.celExpression)
129+
t.Fatalf("Expected expr expression to fail compilation but it succeeded: %s", tc.exprExpression)
133130
})
134131
}
135132
})
@@ -139,8 +136,8 @@ func TestRoleBasedTokenLifetimes_Integration(t *testing.T) {
139136

140137
// Test actual token creation with various user role combinations
141138
// Note: The first user created is an "Owner" (capital O)
142-
// Global max is 720h (30 days), CEL expression provides role-specific rules
143-
client := createClientWithRoleTokenLifetimes(t, `subject.roles.exists(r, r.name == "owner") ? duration("168h") : subject.roles.exists(r, r.name == "user-admin") ? duration("72h") : subject.roles.exists(r, r.name == "member") ? duration("24h") : duration(globalMaxDuration)`, 720*time.Hour)
139+
// Global max is 720h (30 days), expr expression provides role-specific rules
140+
client := createClientWithRoleTokenLifetimes(t, `any(subject.Roles, .Name == "owner") ? duration("168h") : any(subject.Roles, .Name == "user-admin") ? duration("72h") : any(subject.Roles, .Name == "member") ? duration("24h") : duration(globalMaxDuration)`, 720*time.Hour)
144141

145142
ctx := context.Background()
146143

@@ -149,13 +146,13 @@ func TestRoleBasedTokenLifetimes_Integration(t *testing.T) {
149146
require.NoError(t, err)
150147
t.Logf("Token config max lifetime: %v (expected 720h - global max)", tokenConfig.MaxTokenLifetime)
151148

152-
// Test owner can create token up to what the CEL expression allows (168h)
149+
// Test owner can create token up to what the expr expression allows (168h)
153150
_, err = client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
154151
Lifetime: 167 * time.Hour,
155152
})
156153
require.NoError(t, err)
157154

158-
// Test owner cannot exceed what the CEL expression allows (168h)
155+
// Test owner cannot exceed what the expr expression allows (168h)
159156
_, err = client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
160157
Lifetime: 169 * time.Hour,
161158
})
@@ -167,7 +164,7 @@ func TestRoleBasedTokenLifetimes_Integration(t *testing.T) {
167164
t.Parallel()
168165

169166
// Test that users without specific role configs fall back to global max
170-
client := createClientWithRoleTokenLifetimes(t, `subject.roles.exists(r, r.name == "user-admin") ? duration("168h") : duration(globalMaxDuration)`, 48*time.Hour)
167+
client := createClientWithRoleTokenLifetimes(t, `any(subject.Roles, .Name == "user-admin") ? duration("168h") : duration(globalMaxDuration)`, 48*time.Hour)
171168

172169
ctx := context.Background()
173170

@@ -198,9 +195,9 @@ func TestRoleBasedTokenLifetimes_Integration(t *testing.T) {
198195
t.Run("RoleSpecificShorterThanGlobal", func(t *testing.T) {
199196
t.Parallel()
200197

201-
// Test CEL expression that chooses between role-specific and global max
198+
// Test expr expression that chooses between role-specific and global max
202199
// Note: The first user created is an "Owner" (capital O)
203-
client := createClientWithRoleTokenLifetimes(t, `subject.roles.exists(r, r.name == "owner") ? duration(globalMaxDuration) : duration("24h")`, 168*time.Hour) // 7 days global
200+
client := createClientWithRoleTokenLifetimes(t, `any(subject.Roles, .Name == "owner") ? duration(globalMaxDuration) : duration("24h")`, 168*time.Hour) // 7 days global
204201

205202
ctx := context.Background()
206203

@@ -214,7 +211,7 @@ func TestRoleBasedTokenLifetimes_Integration(t *testing.T) {
214211
require.NoError(t, err)
215212
t.Logf("User roles: %v", user.Roles)
216213

217-
// Owner gets the global max (168h) because the CEL expression returns globalMaxDuration
214+
// Owner gets the global max (168h) because the expr expression returns globalMaxDuration
218215
_, err = client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
219216
Lifetime: 167 * time.Hour,
220217
})
@@ -231,20 +228,20 @@ func TestRoleBasedTokenLifetimes_Integration(t *testing.T) {
231228
t.Run("OrganizationSpecificRoles", func(t *testing.T) {
232229
t.Parallel()
233230

234-
// This test verifies that organization-specific role configurations work with CEL expressions
231+
// This test verifies that organization-specific role configurations work with expr expressions
235232

236-
// Set up a client with organization-specific role configurations using CEL
237-
celExpression := `
238-
subject.roles.exists(r, r.name == "owner" && r.orgID == "") ? duration("720h") :
239-
subject.roles.exists(r, r.name == "member" && r.orgID == "") ? duration("24h") :
240-
subject.roles.exists(r, r.name == "organization-member" && r.orgID != "") ? duration("48h") :
241-
subject.roles.exists(r, r.name == "organization-admin" && r.orgID != "") ? duration("168h") :
233+
// Set up a client with organization-specific role configurations using expr
234+
exprExpression := `
235+
any(subject.Roles, .Name == "owner" && .OrgID == "") ? duration("720h") :
236+
any(subject.Roles, .Name == "member" && .OrgID == "") ? duration("24h") :
237+
any(subject.Roles, .Name == "organization-member" && .OrgID != "") ? duration("48h") :
238+
any(subject.Roles, .Name == "organization-admin" && .OrgID != "") ? duration("168h") :
242239
duration(defaultDuration)
243240
`
244241

245242
dv := coderdtest.DeploymentValues(t)
246243
dv.Sessions.MaximumTokenDuration = serpent.Duration(720 * time.Hour)
247-
dv.Sessions.MaximumTokenDurationExpression = serpent.String(celExpression)
244+
dv.Sessions.MaximumTokenDurationExpression = serpent.String(exprExpression)
248245

249246
// Create the client, database, and get the API instance
250247
client, closer, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
@@ -254,15 +251,15 @@ func TestRoleBasedTokenLifetimes_Integration(t *testing.T) {
254251
_ = closer.Close()
255252
})
256253

257-
// Compile the CEL expression
254+
// Compile the expr expression
258255
ctx := context.Background()
259256
_, err := api.DeploymentValues.Sessions.CompiledMaximumTokenDurationProgram()
260257
require.NoError(t, err)
261258

262259
// Create the first user
263260
_ = coderdtest.CreateFirstUser(t, client)
264261

265-
// Test that the CEL expression is working for the site-wide owner role
262+
// Test that the expr expression is working for the site-wide owner role
266263
// The first user gets site-wide owner role, so should get 720h
267264
_, err = client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
268265
Lifetime: 719 * time.Hour,
@@ -280,21 +277,21 @@ func TestRoleBasedTokenLifetimes_Integration(t *testing.T) {
280277
t.Run("OrganizationRoleWithoutConfig", func(t *testing.T) {
281278
t.Parallel()
282279

283-
// Test CEL expression with fallback behavior for unconfigured roles
280+
// Test expr expression with fallback behavior for unconfigured roles
284281

285282
// Set up a client with only site-wide role configurations (no org-specific roles)
286-
client := createClientWithRoleTokenLifetimes(t, `subject.roles.exists(r, r.name == "owner") ? duration("720h") : subject.roles.exists(r, r.name == "member") ? duration("24h") : duration(globalMaxDuration)`, 168*time.Hour)
283+
client := createClientWithRoleTokenLifetimes(t, `any(subject.Roles, .Name == "owner") ? duration("720h") : any(subject.Roles, .Name == "member") ? duration("24h") : duration(globalMaxDuration)`, 168*time.Hour)
287284

288285
ctx := context.Background()
289286

290-
// Test that the first user (owner) gets 720h according to the CEL expression,
287+
// Test that the first user (owner) gets 720h according to the expr expression,
291288
// not the global max (168h)
292289
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
293290
Lifetime: 719 * time.Hour,
294291
})
295292
require.NoError(t, err)
296293

297-
// Test that owner cannot exceed what CEL expression allows (720h)
294+
// Test that owner cannot exceed what expr expression allows (720h)
298295
_, err = client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
299296
Lifetime: 721 * time.Hour,
300297
})

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy