OpenID Connect (OIDC) identity and OAuth 2.0 provider with pluggable connectors
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

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")
})
}
}