Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitf080b23

Browse files
committed
refactor: replace CEL with expr for token lifetime expressions
Change-Id: I2dcfa21535a8c5d4b2276622617782f0b2c47603Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parente0261c5 commitf080b23

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 aboutcustomizing 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 aboutcustomizing 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 theCEL expression configuration.
396+
// based on their attributes and theexpr 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 toCEL-friendly format
408-
celSubject:=celtoken.ConvertSubjectToCEL(subject)
408+
// Convert subject toexpr-friendly format
409+
exprSubject:=exprtoken.ConvertSubjectToExpr(subject)
409410

410-
// EvaluateCEL expression with typed struct
411+
// Evaluateexpr 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
iferr!=nil {
418-
api.Logger.Error(ctx,"theCEL evaluation failed, using default duration",slog.Error(err))
419+
api.Logger.Error(ctx,"theexpr evaluation failed, using default duration",slog.Error(err))
419420
returndefaultDuration
420421
}
421422

422-
// Convert result to time.Duration
423-
// CEL returns types.Duration, not time.Duration directly
424-
switchv:=out.Value().(type) {
425-
case types.Duration:
426-
returnv.Duration
427-
case time.Duration:
428-
returnv
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
returndefaultDuration
434430
}
431+
returntime.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
ifprogram!=nil {
42-
t.Logf("CEL expression compiled successfully")
42+
t.Logf("expr expression compiled successfully")
4343
}else {
44-
t.Logf("NoCEL expression configured")
44+
t.Logf("Noexpr expression configured")
4545
}
4646

4747
// Create the first user
@@ -53,29 +53,29 @@ func createClientWithRoleTokenLifetimes(t *testing.T, roleTokenLifetimeExpressio
5353
funcTestRoleBasedTokenLifetimes_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 validCEL expressions
59+
// Test server starts successfully with validexpr expressions
6060
testCases:= []struct {
61-
namestring
62-
celExpressionstring
61+
namestring
62+
exprExpressionstring
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.EmailendsWith"@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 invalidCEL expressions fail validation
102+
// Test that invalidexpr expressions fail validation
103103
testCases:= []struct {
104-
namestring
105-
celExpressionstring
104+
namestring
105+
exprExpressionstring
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-
ifissues!=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+
iferr!=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("ExpectedCEL expression to fail compilation but it succeeded: %s",tc.celExpression)
129+
t.Fatalf("Expectedexpr 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 theCEL expression allows (168h)
149+
// Test owner can create token up to what theexpr 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 theCEL expression allows (168h)
155+
// Test owner cannot exceed what theexpr 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-
// TestCEL expression that chooses between role-specific and global max
198+
// Testexpr 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 theCEL expression returns globalMaxDuration
214+
// Owner gets the global max (168h) because theexpr 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 withCEL expressions
231+
// This test verifies that organization-specific role configurations work withexpr expressions
235232

236-
// Set up a client with organization-specific role configurations usingCEL
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 usingexpr
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 theCEL expression
254+
// Compile theexpr 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 theCEL expression is working for the site-wide owner role
262+
// Test that theexpr 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-
// TestCEL expression with fallback behavior for unconfigured roles
280+
// Testexpr 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 theCEL expression,
287+
// Test that the first user (owner) gets 720h according to theexpr 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 whatCEL expression allows (720h)
294+
// Test that owner cannot exceed whatexpr expression allows (720h)
298295
_,err=client.CreateToken(ctx,codersdk.Me, codersdk.CreateTokenRequest{
299296
Lifetime:721*time.Hour,
300297
})

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp