mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
138 lines
4.0 KiB
138 lines
4.0 KiB
|
3 days ago
|
package cel_test
|
||
|
|
|
||
|
|
import (
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
|
||
|
|
dexcel "github.com/dexidp/dex/pkg/cel"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestEstimateCost(t *testing.T) {
|
||
|
|
vars := dexcel.IdentityVariables()
|
||
|
|
compiler, err := dexcel.NewCompiler(vars)
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
tests := map[string]struct {
|
||
|
|
expr string
|
||
|
|
}{
|
||
|
|
"simple bool": {
|
||
|
|
expr: "true",
|
||
|
|
},
|
||
|
|
"string comparison": {
|
||
|
|
expr: "identity.email == 'test@example.com'",
|
||
|
|
},
|
||
|
|
"group membership": {
|
||
|
|
expr: "identity.groups.exists(g, g == 'admin')",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for name, tc := range tests {
|
||
|
|
t.Run(name, func(t *testing.T) {
|
||
|
|
prog, err := compiler.Compile(tc.expr)
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
est, err := compiler.EstimateCost(prog)
|
||
|
|
require.NoError(t, err)
|
||
|
|
assert.True(t, est.Max >= est.Min, "max cost should be >= min cost")
|
||
|
|
assert.True(t, est.Max <= dexcel.DefaultCostBudget,
|
||
|
|
"estimated max cost %d should be within default budget %d", est.Max, dexcel.DefaultCostBudget)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCompileTimeCostAcceptsSimpleExpressions(t *testing.T) {
|
||
|
|
vars := append(dexcel.IdentityVariables(), dexcel.RequestVariables()...)
|
||
|
|
compiler, err := dexcel.NewCompiler(vars)
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
tests := map[string]string{
|
||
|
|
"literal": "true",
|
||
|
|
"email endsWith": "identity.email.endsWith('@example.com')",
|
||
|
|
"group check": "'admin' in identity.groups",
|
||
|
|
"emailDomain": `dex.emailDomain(identity.email)`,
|
||
|
|
"groupMatches": `dex.groupMatches(identity.groups, "team:*")`,
|
||
|
|
"groupFilter": `dex.groupFilter(identity.groups, ["admin", "dev"])`,
|
||
|
|
"combined policy": `identity.email.endsWith('@example.com') && 'admin' in identity.groups`,
|
||
|
|
"complex policy": `identity.email.endsWith('@example.com') &&
|
||
|
|
identity.groups.exists(g, g == 'admin') &&
|
||
|
|
request.connector_id == 'okta' &&
|
||
|
|
request.scopes.exists(s, s == 'openid')`,
|
||
|
|
"filter+map chain": `identity.groups
|
||
|
|
.filter(g, g.startsWith('team:'))
|
||
|
|
.map(g, g.replace('team:', ''))
|
||
|
|
.size() > 0`,
|
||
|
|
}
|
||
|
|
|
||
|
|
for name, expr := range tests {
|
||
|
|
t.Run(name, func(t *testing.T) {
|
||
|
|
_, err := compiler.Compile(expr)
|
||
|
|
assert.NoError(t, err, "expression should compile within default budget")
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCompileTimeCostRejection(t *testing.T) {
|
||
|
|
vars := append(dexcel.IdentityVariables(), dexcel.RequestVariables()...)
|
||
|
|
|
||
|
|
tests := map[string]struct {
|
||
|
|
budget uint64
|
||
|
|
expr string
|
||
|
|
}{
|
||
|
|
"simple exists exceeds tiny budget": {
|
||
|
|
budget: 1,
|
||
|
|
expr: "identity.groups.exists(g, g == 'admin')",
|
||
|
|
},
|
||
|
|
"endsWith exceeds tiny budget": {
|
||
|
|
budget: 2,
|
||
|
|
expr: "identity.email.endsWith('@example.com')",
|
||
|
|
},
|
||
|
|
"nested comprehension over groups exceeds moderate budget": {
|
||
|
|
// Two nested iterations over groups: O(n^2) where n=100 → ~280K
|
||
|
|
budget: 10_000,
|
||
|
|
expr: `identity.groups.exists(g1,
|
||
|
|
identity.groups.exists(g2,
|
||
|
|
g1 != g2 && g1.startsWith(g2)
|
||
|
|
)
|
||
|
|
)`,
|
||
|
|
},
|
||
|
|
"cross-variable comprehension exceeds moderate budget": {
|
||
|
|
// filter groups then check each against scopes: O(n*m) → ~162K
|
||
|
|
budget: 10_000,
|
||
|
|
expr: `identity.groups
|
||
|
|
.filter(g, g.startsWith('team:'))
|
||
|
|
.exists(g, request.scopes.exists(s, s == g))`,
|
||
|
|
},
|
||
|
|
"chained filter+map+filter+map exceeds small budget": {
|
||
|
|
budget: 1000,
|
||
|
|
expr: `identity.groups
|
||
|
|
.filter(g, g.startsWith('team:'))
|
||
|
|
.map(g, g.replace('team:', ''))
|
||
|
|
.filter(g, g.size() > 3)
|
||
|
|
.map(g, g.upperAscii())
|
||
|
|
.size() > 0`,
|
||
|
|
},
|
||
|
|
"many independent exists exceeds small budget": {
|
||
|
|
budget: 5000,
|
||
|
|
expr: `identity.groups.exists(g, g.contains('a')) &&
|
||
|
|
identity.groups.exists(g, g.contains('b')) &&
|
||
|
|
identity.groups.exists(g, g.contains('c')) &&
|
||
|
|
identity.groups.exists(g, g.contains('d')) &&
|
||
|
|
identity.groups.exists(g, g.contains('e'))`,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for name, tc := range tests {
|
||
|
|
t.Run(name, func(t *testing.T) {
|
||
|
|
compiler, err := dexcel.NewCompiler(vars, dexcel.WithCostBudget(tc.budget))
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
_, err = compiler.Compile(tc.expr)
|
||
|
|
assert.Error(t, err)
|
||
|
|
assert.Contains(t, err.Error(), "estimated cost")
|
||
|
|
assert.Contains(t, err.Error(), "exceeds budget")
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|