Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions provisioning/cel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package provisioning

import (
"reflect"
"slices"
"strings"
"sync"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
)

const (
conditionCostLimit uint64 = 10_000
)

const (
conditionInterruptCheckFrequency = 100
)

// conditionEnvironment holds the shared CEL environment. Construction is expensive (declares
// variables, registers hasAny/hasToken overloads) and the environment itself is immutable, so
// the struct is initialised lazily under [sync.Once] and reused across all rule evaluations.
//
//nolint:gochecknoglobals // CEL environment construction is expensive and intentionally cached process-wide.
var conditionEnvironment struct {
once sync.Once
env *cel.Env
err error
}

// getConditionEnv returns the shared CEL environment, building it on first call. Any
// construction error is captured and returned on every subsequent call so callers fail closed
// instead of receiving a half-built environment.
func getConditionEnv() (*cel.Env, error) {
conditionEnvironment.once.Do(func() {
conditionEnvironment.env, conditionEnvironment.err = cel.NewEnv(
cel.Variable("identity", cel.MapType(cel.StringType, cel.StringType)),
cel.Variable("claims", cel.MapType(cel.StringType, cel.DynType)),
cel.Function(
"hasAny",
cel.Overload(
"has_any_dyn_list_string",
[]*cel.Type{cel.DynType, cel.ListType(cel.StringType)},
cel.BoolType,
cel.BinaryBinding(func(value ref.Val, accepted ref.Val) ref.Val {
return types.Bool(valueHasAny(value, acceptedStrings(accepted)))
}),
),
),
cel.Function(
"hasToken",
cel.Overload(
"has_token_dyn_string",
[]*cel.Type{cel.DynType, cel.StringType},
cel.BoolType,
cel.BinaryBinding(func(value ref.Val, token ref.Val) ref.Val {
tokenString, ok := stringValue(token)
if !ok || tokenString == "" {
return types.False
}

return types.Bool(valueHasToken(value, tokenString))
}),
),
),
)
})

return conditionEnvironment.env, conditionEnvironment.err
}

// acceptedStrings flattens a CEL value into a set of unique strings for set-membership tests.
func acceptedStrings(value ref.Val) map[string]struct{} {
accepted := make(map[string]struct{})
forEachString(value, func(item string) {
accepted[item] = struct{}{}
})

return accepted
}

// valueHasAny reports whether any string drawn from value is present in accepted. Used by the
// hasAny CEL overload to compare a claim against a fixed allow-list.
func valueHasAny(value ref.Val, accepted map[string]struct{}) bool {
if len(accepted) == 0 {
return false
}

matched := false
forEachString(value, func(item string) {
if _, ok := accepted[item]; ok {
matched = true
}
})

return matched
}

// valueHasToken reports whether token appears as a whitespace-delimited word in any string
// drawn from value. Used by the hasToken CEL overload to inspect space-separated scope claims.
func valueHasToken(value ref.Val, token string) bool {
matched := false
forEachString(value, func(item string) {
if slices.Contains(strings.Fields(item), token) {
matched = true
}
})

return matched
}

// forEachString invokes visit for every string drawn from value. The three-tier fallback
// (direct string, traits.Lister iteration, reflection over Go native slices) covers the
// distinct shapes a JSON-decoded claim may take inside CEL.
func forEachString(value ref.Val, visit func(string)) {
if item, ok := stringValue(value); ok {
visit(item)

return
}

if list, ok := value.(traits.Lister); ok {
iter := list.Iterator()
for iter.HasNext() == types.True {
if item, ok := stringValue(iter.Next()); ok {
visit(item)
}
}

return
}

switch native := value.Value().(type) {
case []string:
for _, item := range native {
visit(item)
}
case []any:
for _, item := range native {
if text, ok := item.(string); ok {
visit(text)
}
}
case []ref.Val:
for _, item := range native {
if text, ok := stringValue(item); ok {
visit(text)
}
}
}
}

// stringValue converts a CEL ref.Val to a Go string. Returns ("", false) when value is nil or
// cannot be represented as a string by either direct assertion or CEL's native conversion.
func stringValue(value ref.Val) (string, bool) {
if value == nil {
return "", false
}

if text, ok := value.Value().(string); ok {
return text, true
}

native, err := value.ConvertToNative(reflect.TypeFor[string]())
if err != nil {
return "", false
}
text, ok := native.(string)

return text, ok
}
Loading