package main
import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strings"
"time"
"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"
"github.com/golang/glog"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
rpcpb "google.golang.org/genproto/googleapis/rpc/context/attribute_context"
structpb "google.golang.org/protobuf/types/known/structpb"
tpb "google.golang.org/protobuf/types/known/timestamppb"
)
func main() {
exercise1()
exercise2()
exercise3()
exercise4()
exercise5()
exercise6()
exercise7()
exercise8()
}
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
ast, iss := env.Parse(`"Hello, World!"`)
if iss.Err() != nil {
glog.Exit(iss.Err())
}
checked, iss := env.Check(ast)
if iss.Err() != nil {
glog.Exit(iss.Err())
}
if checked.OutputType() != cel.StringType {
glog.Exitf(
"Got %v, wanted %v result type",
checked.OutputType(), cel.StringType)
}
program, err := env.Program(checked)
if err != nil {
glog.Exitf("program error: %v", err)
}
eval(program, cel.NoVars())
fmt.Println()
}
func exercise2() {
fmt.Println("=== Exercise 2: Variables ===\n")
env, err := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
if err != nil {
glog.Exit(err)
}
ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
program, _ := env.Program(ast)
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
ast := compile(env,
`request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'`,
cel.BoolType)
program, _ := env.Program(ast)
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
eval(program, request(auth("other:me@acme.co", emptyClaims), time.Now()))
fmt.Println()
}
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
cel.Function("contains",
cel.MemberOverload("map_contains_key_value",
[]*cel.Type{mapAB, typeParamA, typeParamB},
cel.BoolType,
cel.FunctionBinding(mapContainsKeyValue)),
),
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
program, err := env.Program(ast)
if err != nil {
glog.Exit(err)
}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
env, _ := cel.NewEnv(
cel.Variable("now", cel.TimestampType),
)
ast := compile(env, `
{'aud': 'my-project',
'exp': now + duration('300s'),
'extra_claims': {
'group': 'admin'
},
'iat': now,
'iss': 'auth.acme.com:12350',
'nbf': now,
'sub': 'serviceAccount:delegate@acme.co'
}`,
cel.MapType(cel.StringType, cel.DynType))
program, _ := env.Program(ast)
out, _, _ := eval(
program,
map[string]any{
"now": time.Now(),
},
)
fmt.Printf("------ type conversion ------\n%v\n", valueToJSON(out))
fmt.Println()
}
func exercise6() {
fmt.Println("=== Exercise 6: Building Protos ===\n")
requestType := &rpcpb.AttributeContext_Request{}
env, _ := cel.NewEnv(
cel.Container("google.rpc.context.AttributeContext"),
cel.Types(requestType),
cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
cel.Variable("now", cel.TimestampType),
)
ast := compile(env, `
Request{
auth: Auth{
principal: jwt.iss + '/' + jwt.sub,
audiences: [jwt.aud],
presenter: 'azp' in jwt ? jwt.azp : "",
claims: jwt
},
time: now
}`,
cel.ObjectType("google.rpc.context.AttributeContext.Request"))
program, _ := env.Program(ast)
out, _, _ := eval(
program,
map[string]any{
"jwt": map[string]any{
"sub": "serviceAccount:delegate@acme.co",
"aud": "my-project",
"iss": "auth.acme.com:12350",
"extra_claims": map[string]string{
"group": "admin",
},
},
"now": time.Now(),
},
)
req, err := out.ConvertToNative(reflect.TypeOf(requestType))
if err != nil {
glog.Exit(err)
}
bytes, err := prototext.Marshal(req.(proto.Message))
if err != nil {
glog.Exitf("failed to marshal proto to text: %v", req)
}
fmt.Printf("------ type unwrap ------\n%v\n", string(bytes))
fmt.Println()
}
func exercise7() {
fmt.Println("=== Exercise 7: Macros ===\n")
env, _ := cel.NewEnv(cel.Variable("jwt", cel.DynType))
ast := compile(env,
`jwt.extra_claims.exists(c, c.startsWith('group'))
&& jwt.extra_claims
.filter(c, c.startsWith('group'))
.all(c, jwt.extra_claims[c]
.all(g, g.endsWith('@acme.co')))`,
cel.BoolType)
program, _ := env.Program(ast)
eval(program,
map[string]any{
"jwt": map[string]any{
"sub": "serviceAccount:delegate@acme.co",
"aud": "my-project",
"iss": "auth.acme.com:12350",
"extra_claims": map[string][]string{
"group1": {"admin@acme.co", "analyst@acme.co"},
"labels": {"metadata", "prod", "pii"},
"groupN": {"forever@acme.co"},
},
},
})
fmt.Println()
}
func exercise8() {
fmt.Println("=== Exercise 8: Tuning ===\n")
env, _ := cel.NewEnv(
cel.Variable("x", cel.IntType),
cel.Variable("y", cel.UintType),
)
ast := compile(env,
`x in [1, 2, 3, 4, 5] && type(y) == uint`,
cel.BoolType)
trueVars := map[string]any{"x": int64(4), "y": uint64(2)}
program, _ := env.Program(ast, cel.EvalOptions(cel.OptOptimize))
eval(program, trueVars)
falseVars := map[string]any{"x": int64(6), "y": uint64(2)}
program, _ = env.Program(ast, cel.EvalOptions(cel.OptExhaustiveEval))
eval(program, falseVars)
xVar := map[string]any{"x": int64(3)}
partialVars, _ := cel.PartialVars(xVar, cel.AttributePattern("y"))
program, _ = env.Program(ast,
cel.EvalOptions(cel.OptPartialEval, cel.OptOptimize, cel.OptTrackState))
_, details, _ := eval(program, partialVars)
residualAst, _ := env.ResidualAst(ast, details)
residual, _ := cel.AstToString(residualAst)
fmt.Printf("------ residual ------\n%s\n", residual)
fmt.Println()
}
func compile(env *cel.Env, expr string, celType *cel.Type) *cel.Ast {
ast, iss := env.Compile(expr)
if iss.Err() != nil {
glog.Exit(iss.Err())
}
if !reflect.DeepEqual(ast.OutputType(), celType) {
glog.Exitf(
"Got %v, wanted %v result type", ast.OutputType(), celType)
}
fmt.Printf("%s\n\n", strings.ReplaceAll(expr, "\t", " "))
return ast
}
func eval(prg cel.Program,
vars any) (out ref.Val, det *cel.EvalDetails, err error) {
varMap, isMap := vars.(map[string]any)
fmt.Println("------ input ------")
if !isMap {
fmt.Printf("(%T)\n", vars)
} else {
for k, v := range varMap {
switch val := v.(type) {
case proto.Message:
bytes, err := prototext.Marshal(val)
if err != nil {
glog.Exitf("failed to marshal proto to text: %v", val)
}
fmt.Printf("%s = %s", k, string(bytes))
case map[string]any:
b, _ := json.MarshalIndent(v, "", " ")
fmt.Printf("%s = %v\n", k, string(b))
case uint64:
fmt.Printf("%s = %vu\n", k, v)
default:
fmt.Printf("%s = %v\n", k, v)
}
}
}
fmt.Println()
out, det, err = prg.Eval(vars)
report(out, det, err)
fmt.Println()
return
}
func report(result ref.Val, details *cel.EvalDetails, err error) {
fmt.Println("------ result ------")
if err != nil {
fmt.Printf("error: %s\n", err)
} else {
fmt.Printf("value: %v (%T)\n", result, result)
}
if details != nil {
fmt.Printf("\n------ eval states ------\n")
state := details.State()
stateIDs := state.IDs()
ids := make([]int, len(stateIDs), len(stateIDs))
for i, id := range stateIDs {
ids[i] = int(id)
}
sort.Ints(ids)
for _, id := range ids {
v, found := state.Value(int64(id))
if !found {
continue
}
fmt.Printf("%d: %v (%T)\n", id, v, v)
}
}
}
func mapContainsKeyValue(args ...ref.Val) ref.Val {
m := args[0].(traits.Mapper)
key := args[1]
v, found := m.Find(key)
if !found {
if v != nil {
return types.ValOrErr(v, "unsupported key type")
}
return types.False
}
return v.Equal(args[2])
}
func auth(user string, claims map[string]string) *rpcpb.AttributeContext_Auth {
claimFields := make(map[string]*structpb.Value)
for k, v := range claims {
claimFields[k] = structpb.NewStringValue(v)
}
return &rpcpb.AttributeContext_Auth{
Principal: user,
Claims: &structpb.Struct{Fields: claimFields},
}
}
func request(auth *rpcpb.AttributeContext_Auth, t time.Time) map[string]any {
req := &rpcpb.AttributeContext_Request{
Auth: auth,
Time: &tpb.Timestamp{Seconds: t.Unix()},
}
return map[string]any{"request": req}
}
func valueToJSON(val ref.Val) string {
v, err := val.ConvertToNative(reflect.TypeOf(&structpb.Value{}))
if err != nil {
glog.Exit(err)
}
marshaller := protojson.MarshalOptions{Indent: " "}
bytes, err := marshaller.Marshal(v.(proto.Message))
if err != nil {
glog.Exit(err)
}
return string(bytes)
}
var (
emptyClaims = make(map[string]string)
)