Skip to content

Commit 2309529

Browse files
rachelmyersTristonianJonesJimLarson
authored
Add codelab (#328)
Source code for the CEL-Go codelab published here: https://codelabs.developers.google.com/codelabs/cel-go/#0 Co-authored-by: Tristan Swadell <tswadell@google.com> Co-authored-by: Jim Larson <32469398+JimLarson@users.noreply.github.com>
1 parent 43d4b0d commit 2309529

5 files changed

Lines changed: 883 additions & 17 deletions

File tree

codelab/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
### Welcome to the CEL Codelab!
2+
---
3+
4+
Find the codelab instructions [here](https://codelabs.developers.google.com/codelabs/cel-go/#0). It requires some knowledge of GoLang and Protobuf.
5+
6+
If you get stuck, check out the [solutions](https://github.com/google/cel-go/blob/master/codelab/solution/codelab.go).
7+
8+
If you find a bug or want to make an improvement, PRs and issues are welcome. Please follow the [contributing guidelines](https://github.com/google/cel-go/blob/master/CONTRIBUTING.md).

codelab/codelab.go

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
2+
// Copyright 2020 Google LLC
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
// This file contains code that demonstrates common CEL features.
17+
// This code is intended for use with the CEL Codelab: go/cel-codelab-go
18+
package main
19+
20+
import (
21+
"encoding/json"
22+
"fmt"
23+
"reflect"
24+
"sort"
25+
"strings"
26+
"time"
27+
28+
"github.com/golang/glog"
29+
"github.com/golang/protobuf/jsonpb"
30+
"github.com/golang/protobuf/proto"
31+
32+
"github.com/google/cel-go/cel"
33+
_ "github.com/google/cel-go/checker/decls"
34+
"github.com/google/cel-go/common/types"
35+
"github.com/google/cel-go/common/types/ref"
36+
"github.com/google/cel-go/common/types/traits"
37+
_ "github.com/google/cel-go/interpreter/functions"
38+
39+
structpb "github.com/golang/protobuf/ptypes/struct"
40+
tpb "github.com/golang/protobuf/ptypes/timestamp"
41+
42+
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
43+
rpcpb "google.golang.org/genproto/googleapis/rpc/context/attribute_context"
44+
)
45+
46+
func main() {
47+
exercise1()
48+
exercise2()
49+
exercise3()
50+
exercise4()
51+
exercise5()
52+
exercise6()
53+
exercise7()
54+
exercise8()
55+
}
56+
57+
// exercise1 evaluates a simple literal expression: "Hello, World!"
58+
//
59+
// Compile, eval, profit!
60+
func exercise1() {
61+
fmt.Println("=== Exercise 1: Hello World ===\n")
62+
63+
fmt.Println()
64+
}
65+
66+
// exercise2 shows how to declare and use variables in expressions.
67+
//
68+
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
69+
// determine whether a specific auth claim is set.
70+
func exercise2() {
71+
fmt.Println("=== Exercise 2: Variables ===\n")
72+
73+
fmt.Println()
74+
}
75+
76+
// exercise3 demonstrates how CEL's commutative logical operators work.
77+
//
78+
// Construct an expression which checks whether the `request.auth.claims.group`
79+
// value is equal to `admin` or the `request.auth.principal` is
80+
// `user:me@acme.co` and the `request.time` is during work hours (9:00 - 17:00)
81+
//
82+
// Evaluate the expression once with a request containing no claims but which
83+
// sets the appropriate principal and occurs at 12:00 hours. Then evaluate the
84+
// request a second time at midnight. Observe the difference in output.
85+
func exercise3() {
86+
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
87+
88+
fmt.Println()
89+
}
90+
91+
// exercise4 demonstrates how to extend CEL with custom functions.
92+
//
93+
// Declare a `contains` member function on map types that returns a boolean
94+
// indicating whether the map contains the key-value pair.
95+
func exercise4() {
96+
fmt.Println("=== Exercise 4: Customization ===\n")
97+
98+
fmt.Println()
99+
}
100+
101+
// exercise5 covers how to build complex objects as CEL literals.
102+
//
103+
// Given the input `now`, construct a JWT with an expiry of 5 minutes.
104+
func exercise5() {
105+
fmt.Println("=== Exercise 5: Building JSON ===\n")
106+
107+
fmt.Println()
108+
}
109+
110+
// exercise6 describes how to build proto message types within CEL.
111+
//
112+
// Given an input `jwt` and time `now` construct a
113+
// `google.rpc.context.AttributeContext.Request` with the `time` and `auth`
114+
// fields populated according to the go/api-attributes specification.
115+
func exercise6() {
116+
fmt.Println("=== Exercise 6: Building Protos ===\n")
117+
118+
fmt.Println()
119+
}
120+
121+
// exercise7 introduces macros for dealing with repeated fields and maps.
122+
//
123+
// Determine whether the `jwt.extra_claims` has at least one key that starts
124+
// with the `group` prefix, and ensure that all group-like keys have list
125+
// values containing only strings that end with '@acme.co`.
126+
func exercise7() {
127+
fmt.Println("=== Exercise 7: Macros ===\n")
128+
129+
fmt.Println()
130+
}
131+
132+
// exercise8 covers some useful features of CEL-Go which can be used to
133+
// improve performance and better understand evaluation behavior.
134+
//
135+
// Turn on the optimization, exhaustive eval, and state tracking
136+
// `cel.ProgramOption` flags to see the impact on evaluation behavior.
137+
//
138+
// Also, turn on the homogeneous aggregate literals flag to disable
139+
// heterogeneous list and map literals.
140+
func exercise8() {
141+
fmt.Println("=== Exercise 8: Tuning ===\n")
142+
143+
fmt.Println()
144+
}
145+
146+
// Functions to assist with CEL execution.
147+
148+
// compile will parse and check an expression `expr` against a given
149+
// environment `env` and determine whether the resulting type of the expression
150+
// matches the `exprType` provided as input.
151+
func compile(env *cel.Env, expr string, exprType *exprpb.Type) *cel.Ast {
152+
ast, iss := env.Compile(expr)
153+
if iss.Err() != nil {
154+
glog.Exit(iss.Err())
155+
}
156+
if !proto.Equal(ast.ResultType(), exprType) {
157+
glog.Exitf(
158+
"Got %v, wanted %v result type", ast.ResultType(), exprType)
159+
}
160+
fmt.Printf("%s\n\n", strings.ReplaceAll(expr, "\t", " "))
161+
return ast
162+
}
163+
164+
// eval will evaluate a given program `prg` against a set of variables `vars`
165+
// and return the output, eval details (optional), or error that results from
166+
// evaluation.
167+
func eval(prg cel.Program,
168+
vars interface{}) (out ref.Val, det *cel.EvalDetails, err error) {
169+
varMap, isMap := vars.(map[string]interface{})
170+
fmt.Println("------ input ------")
171+
if !isMap {
172+
fmt.Printf("(%T)\n", vars)
173+
} else {
174+
for k, v := range varMap {
175+
switch val := v.(type) {
176+
case proto.Message:
177+
fmt.Printf("%s = %v", k, proto.MarshalTextString(val))
178+
case map[string]interface{}:
179+
b, _ := json.MarshalIndent(v, "", " ")
180+
fmt.Printf("%s = %v\n", k, string(b))
181+
case uint64:
182+
fmt.Printf("%s = %vu\n", k, v)
183+
default:
184+
fmt.Printf("%s = %v\n", k, v)
185+
}
186+
}
187+
}
188+
fmt.Println()
189+
out, det, err = prg.Eval(vars)
190+
report(out, det, err)
191+
fmt.Println()
192+
return
193+
}
194+
195+
// report prints out the result of evaluation in human-friendly terms.
196+
func report(result ref.Val, details *cel.EvalDetails, err error) {
197+
fmt.Println("------ result ------")
198+
if err != nil {
199+
fmt.Printf("error: %s\n", err)
200+
} else {
201+
fmt.Printf("value: %v (%T)\n", result, result)
202+
}
203+
if details != nil {
204+
fmt.Printf("\n------ eval states ------\n")
205+
state := details.State()
206+
stateIDs := state.IDs()
207+
ids := make([]int, len(stateIDs), len(stateIDs))
208+
for i, id := range stateIDs {
209+
ids[i] = int(id)
210+
}
211+
sort.Ints(ids)
212+
for _, id := range ids {
213+
v, found := state.Value(int64(id))
214+
if !found {
215+
continue
216+
}
217+
fmt.Printf("%d: %v (%T)\n", id, v, v)
218+
}
219+
}
220+
}
221+
222+
// mapContainsKeyValue implements the custom function:
223+
// `map.contains(key, value) bool`.
224+
func mapContainsKeyValue(args ...ref.Val) ref.Val {
225+
// Check the argument input count.
226+
if len(args) != 3 {
227+
return types.NewErr("no such overload")
228+
}
229+
obj := args[0]
230+
m, isMap := obj.(traits.Mapper)
231+
// Ensure the argument is a CEL map type, otherwise error.
232+
// The type-checking is a best effort check to ensure that the types provided
233+
// to functions match the ones specified; however, it is always possible that
234+
// the implementation does not match the declaration. Always check arguments
235+
// types whenever there is a possibility that your function will deal with
236+
// dynamic content.
237+
if !isMap {
238+
// The helper ValOrErr ensures that errors on input are propagated.
239+
return types.ValOrErr(obj, "no such overload")
240+
}
241+
242+
// CEL has many interfaces for dealing with different type abstractions.
243+
// The traits.Mapper interface unifies field presence testing on proto
244+
// messages and maps.
245+
key := args[1]
246+
v, found := m.Find(key)
247+
// If not found and the value was non-nil, the value is an error per the
248+
// `Find` contract. Propagate it accordingly.
249+
if !found {
250+
if v != nil {
251+
return types.ValOrErr(v, "unsupported key type")
252+
}
253+
// Return CEL False if the key was not found.
254+
return types.False
255+
}
256+
// Otherwise whether the value at the key equals the value provided.
257+
return v.Equal(args[2])
258+
}
259+
260+
// Functions for constructing CEL inputs.
261+
262+
// auth constructs a `google.rpc.context.AttributeContext.Auth` message.
263+
func auth(user string, claims map[string]string) *rpcpb.AttributeContext_Auth {
264+
claimFields := make(map[string]*structpb.Value)
265+
for k, v := range claims {
266+
claimFields[k] = &structpb.Value{
267+
Kind: &structpb.Value_StringValue{
268+
StringValue: v,
269+
},
270+
}
271+
}
272+
return &rpcpb.AttributeContext_Auth{
273+
Principal: user,
274+
Claims: &structpb.Struct{Fields: claimFields},
275+
}
276+
}
277+
278+
// request constructs a `google.rpc.context.AttributeContext.Request` message.
279+
func request(auth *rpcpb.AttributeContext_Auth, t time.Time) map[string]interface{} {
280+
req := &rpcpb.AttributeContext_Request{
281+
Auth: auth,
282+
Time: &tpb.Timestamp{Seconds: t.Unix()},
283+
}
284+
return map[string]interface{}{"request": req}
285+
}
286+
287+
// valueToJSON converts the CEL type to a protobuf JSON representation and
288+
// marshals the result to a string.
289+
func valueToJSON(val ref.Val) string {
290+
v, err := val.ConvertToNative(reflect.TypeOf(&structpb.Value{}))
291+
if err != nil {
292+
glog.Exit(err)
293+
}
294+
marshaller := &jsonpb.Marshaler{Indent: " "}
295+
str, err := marshaller.MarshalToString(v.(proto.Message))
296+
if err != nil {
297+
glog.Exit(err)
298+
}
299+
return str
300+
}
301+
302+
var (
303+
emptyClaims = make(map[string]string)
304+
)

0 commit comments

Comments
 (0)