1. מבוא
מה זה CEL?
CEL היא שפת ביטוי מלאה שלא קשורה לטיורינג, שתוכננה להיות מהירה, ניידת ובטוחה להפעלה. אפשר להשתמש ב-CEL בנפרד או להטמיע אותו במוצר גדול יותר.
CEL תוכננה כשפה שבה אפשר להריץ קוד משתמש בבטחה. לא בטוח להתקשר ל-eval()
באופן עיוור בקוד python של משתמש, אבל ניתן להריץ קוד CEL בצורה בטוחה של משתמש. המנגנון של CEL מונע התנהגות שעלולה לפגוע בביצועים שלו, ולכן הוא מבצע הערכה בטוחה בסדר גודל של ננו-שניות עד מיקרו-שניות; הוא אידיאלי לאפליקציות שחיוניות לביצועים.
CEL מעריך ביטויים, שדומים לפונקציות בשורה יחידה או לביטויי lambda. CEL משמש בדרך כלל להחלטות בוליאניות, אבל הוא יכול גם לשמש כדי לבנות אובייקטים מורכבים יותר כמו הודעות JSON או הודעות אב-טיפוס.
האם שימוש ב-CEL מתאים לפרויקט שלכם?
ב-CEL מתבצעת הערכה של ביטוי מה-AST בננו-שניות עד מיקרו-שניות, כך שהתרחיש האידיאלי ל-CEL הוא אפליקציות עם נתיבים קריטיים לביצועים. אין לבצע הידור של קוד CEL ל-AST בנתיבים קריטיים. אפליקציות אידיאליות הן אפליקציות שבהן התצורה מופעלת לעתים קרובות ומשנות לעיתים רחוקות יותר.
לדוגמה, הפעלת מדיניות אבטחה בכל בקשת HTTP לשירות היא תרחיש אידיאלי של CEL, מפני שמדיניות האבטחה משתנה לעיתים רחוקות, וההשפעה של CEL זניחה על זמן התגובה. במקרה הזה, CEL מחזירה ערך בוליאני אם הבקשה צריכה להיות מותרת או לא, אבל היא יכולה להחזיר הודעה מורכבת יותר.
מה נכלל ב-Codelab הזה?
השלב הראשון של ה-Codelab הזה ילמד את המניע לשימוש ב-CEL ובמושגי הליבה שלו. כל השאר מיועד לתכנות תרגילים שמכסים תרחישים נפוצים לדוגמה. כדי לקבל מידע מעמיק יותר על השפה, הסמנטיקה והתכונות, אפשר לעיין בהגדרת שפת CEL ב-GitHub וב-CEL Go Docs.
ה-Codelab הזה מיועד למפתחים שרוצים ללמוד CEL כדי להשתמש בשירותים שכבר תומכים ב-CEL. ה-Codelab הזה לא עוסק בשילוב של CEL בפרויקט שלכם.
מה תלמדו
- מושגי ליבה מ-CEL
- שלום, World: שימוש ב-CEL כדי להעריך מחרוזת
- יצירת משתנים
- הבנת קיצורי הדרך של CEL בפעולות לוגיות AND/OR
- איך משתמשים ב-CEL כדי ליצור JSON
- איך משתמשים ב-CEL כדי לבנות Protobuffers
- יצירת רכיבי מאקרו
- דרכים לכוונון ביטויי CEL
מה צריך להכין
דרישות מוקדמות
ה-Codelab הזה מתבסס על הבנה בסיסית של מאגרי פרוטוקולים ו-Go Lang.
אם אתם לא מכירים את מאגרי האחסון לפרוטוקולים, התרגיל הראשון ייתן לכם מושג איך CEL פועל, אבל יכול להיות שהדוגמאות המתקדמות יותר משתמשות במאגרי פרוטוקולים כקלט ב-CEL, כדי שיהיה קשה יותר להבין אותן. קודם כדאי לעיין באחד מהמדריכים האלה. שימו לב שלא חייבים להשתמש במאגרי נתונים זמניים של פרוטוקולים כדי להשתמש ב-CEL, אבל נעשה בהם שימוש נרחב ב-Codelab הזה.
כדי לבדוק אם הוא מותקן, מריצים את:
go --help
2. מושגים מרכזיים
אפליקציות
CEL מיועד לשימוש כללי, ושימש למגוון אפליקציות, החל מניתוב RPCs ועד להגדרת מדיניות אבטחה. CEL ניתן להרחבה, אגנוסטי לאפליקציות ומותאם להדרת עבודה אחת ולהערכה של הרבה תהליכי עבודה.
אפליקציות ושירותים רבים מעריכים הגדרות של הצהרות. לדוגמה: בקרת גישה מבוססת-תפקיד (RBAC) היא הגדרה הצהרתית שיוצרת החלטת גישה על סמך תפקיד וקבוצה של משתמשים. אם התרחיש לדוגמה הוא 80%, בעזרת CEL אפשר לעגל את 20% הנותרים במקרים שבהם המשתמשים צריכים יותר אקספרסיביות.
אוסף
ביטוי נוצר מול סביבה. שלב ההידור מייצר עץ תחביר מופשט (AST) בצורת פרוטובוף. ביטויים משולבים נשמרים בדרך כלל לשימוש עתידי כדי שההערכה תהיה מהירה ככל האפשר. אפשר להעריך ביטוי אחד שעבר הידור באמצעות קלטים שונים.
הבעות
המשתמשים מגדירים ביטויים; מגדירים את הסביבה שבה הוא פועל. חתימה של פונקציה כוללת הצהרה על הקלט, והיא נכתבת מחוץ לביטוי ה-CEL. ספריית הפונקציות שזמינות ל-CEL מיובאת באופן אוטומטי.
בדוגמה הבאה, הביטוי מתייחס לאובייקט בקשה, והבקשה כוללת אסימון הצהרות. הביטוי מחזיר ערך בוליאני שמציין אם אסימון ההצהרה עדיין תקף.
// Check whether a JSON Web Token has expired by inspecting the 'exp' claim.
//
// Args:
// claims - authentication claims.
// now - timestamp indicating the current system time.
// Returns: true if the token has expired.
//
timestamp(claims["exp"]) < now
סביבה
סביבות מוגדרות על ידי שירותים. שירותים ואפליקציות שמטמיעים CEL מצהירים על סביבת הביטוי. הסביבה היא אוסף של משתנים ופונקציות שאפשר להשתמש בהם בביטויים.
הצהרות מבוססות-proto משמשות את בודק הסוגים של CEL כדי לוודא שכל ההפניות למזהים ופונקציות בתוך ביטוי מוצהרות ונעשה בהן שימוש נכון.
שלושה שלבים בניתוח ביטוי
העיבוד של ביטוי כולל שלושה שלבים: ניתוח, בדיקה והערכה. הדפוס הנפוץ ביותר ב-CEL הוא שימוש במישור בקרה לניתוח ולבדיקה של ביטויים בזמן ההגדרה ולאחסון ה-AST.
בזמן ריצה, מישור הנתונים מאחזר ומעריך את ה-AST שוב ושוב. CEL מותאם ליעילות בסביבת זמן ריצה, אבל לא כדאי לבצע ניתוח ובדיקה בנתיבי קוד קריטיים של זמן אחזור.
מתבצע ניתוח של ה-CEL מביטוי קריא לאנשים לעץ תחביר מופשט באמצעות דקדוק / מנתח נתונים של ANTLR. שלב הניתוח פולט עץ תחביר מופשט מבוסס-פרוטו, שבו כל צומת Expr ב-AST מכיל מזהה מספר שלם המשמש ליצירת אינדקס למטא נתונים שנוצרו במהלך הניתוח והבדיקה. הקוד syntax.proto שהופק במהלך הניתוח מייצג נאמנה את הייצוג המופשט של מה שהוקלד במחרוזת של הביטוי.
אחרי ניתוח של ביטוי, ניתן לבדוק אותו מול הסביבה כדי לוודא שכל מזהי המשתנים והפונקציות בביטוי הוצהרו ושהם משתמשים בהם בצורה נכונה. הכלי לבדיקת סוגי הפריטים מפיק checked.proto שכולל מטא-נתונים של סוג, משתנה ופתרון פונקציה, שיכולים לשפר משמעותית את יעילות ההערכה.
לבודק ב-CEL נדרשים 3 דברים:
- קישורי פונקציות לכל תוסף בהתאמה אישית
- קישורי משתנים
- AST לבדיקה
קישורי הפונקציות וקישורי המשתנים צריכים להתאים למה ששימש להרכבת ה-AST. אפשר לעשות שימוש חוזר בכל אחד מהקלט הזה במספר הערכות, כמו הערכת AST בקבוצות רבות של קישורי משתנים, או שימוש באותם משתנים כנגד AST רבים, או קישורי הפונקציות שנמצאים בשימוש לאורך כל משך החיים של תהליך מסוים (מקרה נפוץ).
3. הגדרה
הקוד של ה-Codelab הזה נמצא בתיקייה codelab
של המאגר cel-go
. הפתרון זמין בתיקייה codelab/solution
של אותו מאגר.
שכפול ו-cd למאגר:
git clone https://github.com/google/cel-go.git
cd cel-go/codelab
מריצים את הקוד באמצעות go run
:
go run .
הפלט הבא אמור להתקבל:
=== Exercise 1: Hello World ===
=== Exercise 2: Variables ===
=== Exercise 3: Logical AND/OR ===
=== Exercise 4: Customization ===
=== Exercise 5: Building JSON ===
=== Exercise 6: Building Protos ===
=== Exercise 7: Macros ===
=== Exercise 8: Tuning ===
איפה חבילות ה-CEL?
בכלי העריכה, פותחים את codelab/codelab.go
. אתם אמורים לראות את הפונקציה הראשית שמניעה את הביצוע של התרגילים ב-Codelab הזה, ואחריה צריך לראות שלושה בלוקים של פונקציות מסייעות. הקבוצה הראשונה של עוזרים שעוזרים לבצע את השלבים של הערכת CEL:
- הפונקציה
Compile
: מנתחת ובודקת ובודקת ביטוי קלט מול סביבה - הפונקציה
Eval
: מחשבת תוכנית שעברה הידור מול קלט - הפונקציה
Report
: הדפסה יפה של תוצאת ההערכה
בנוסף, העוזרים של request
ו-auth
סופקו כדי לעזור בבניית קלט לתרגילים השונים.
התרגילים יתייחסו לחבילות לפי שם החבילה המקוצר שלהן. אם אתם רוצים להתעמק בפרטים, המיפוי מחבילה למיקום המקור במאגר של google/cel-go
מופיע למטה:
Package | מיקום המקור | תיאור |
cel | ממשקים ברמה העליונה | |
אזכור | ממשקי עזר | |
סוגים | הערכים של סוגי זמן ריצה |
4. שלום עולם!
לפי המסורת של כל שפות התכנות, נתחיל ביצירה ובהערכה של "Hello World! ".
הגדרת הסביבה
בכלי העריכה, מחפשים את ההצהרה exercise1
וממלאים את הפרטים הבאים כדי להגדיר את הסביבה:
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Will add the parse and check steps here
}
אפליקציות CEL מעריכות ביטוי מול סביבה. env, err := cel.NewEnv()
מגדיר את הסביבה הסטנדרטית.
אפשר להתאים אישית את הסביבה באמצעות האפשרויות cel.EnvOption
לשיחה. האפשרויות האלה יכולות להשבית פקודות מאקרו, להצהיר על משתנים ופונקציות מותאמים אישית וכו'.
סביבת CEL הרגילה תומכת בכל הסוגים, האופרטורים, הפונקציות ופקודות המאקרו שמוגדרים במפרט השפה.
ניתוח ובדיקה של הביטוי
אחרי הגדרת הסביבה, אפשר לנתח ולבדוק ביטויים. מוסיפים את הערכים הבאים לפונקציה:
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Check that the expression compiles and returns a String.
ast, iss := env.Parse(`"Hello, World!"`)
// Report syntactic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Type-check the expression for correctness.
checked, iss := env.Check(ast)
// Report semantic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Check the output type is a string.
if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
glog.Exitf(
"Got %v, wanted %v output type",
checked.OutputType(), cel.StringType,
)
}
// Will add the planning step here
}
הערך iss
שהוחזר על ידי הקריאות Parse
ו-Check
הוא רשימה של בעיות שעלולות להיות שגיאות. אם הערך של iss.Err()
הוא לא אפס, יש שגיאה בתחביר או בסמנטיקה, והתוכנה לא יכולה להמשיך הלאה. כשהביטוי בנוי היטב, התוצאה של הקריאות האלה היא קובץ הפעלה cel.Ast
.
מעריכים את הביטוי
אחרי שמנתחים את הביטוי ונבדק אותו ל-cel.Ast
, אפשר להמיר אותו לתוכנית שניתנת להערכה, ושניתן להתאים אישית את מצבי ההערכה וקישורי הפונקציות שלה באמצעות אפשרויות פונקציונליות. הערה, אפשר גם לקרוא cel.Ast
מ-proto באמצעות הפונקציות cel.CheckedExprToAst או cel.ParsedExprToAst.
אחרי תכנון של cel.Program
, אפשר לבדוק אותו מול קלט על ידי קריאה ל-Eval
. התוצאה של Eval
תכיל את התוצאה, את פרטי ההערכה ואת סטטוס השגיאה.
הוספת תכנון והתקשרות אל eval
:
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Check that the expression compiles and returns a String.
ast, iss := env.Parse(`"Hello, World!"`)
// Report syntactic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Type-check the expression for correctness.
checked, iss := env.Check(ast)
// Report semantic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Check the output type is a string.
if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
glog.Exitf(
"Got %v, wanted %v output type",
checked.OutputType(), cel.StringType)
}
// Plan the program.
program, err := env.Program(checked)
if err != nil {
glog.Exitf("program error: %v", err)
}
// Evaluate the program without any additional arguments.
eval(program, cel.NoVars())
fmt.Println()
}
לשם הקיצור, נשמיט את מקרי השגיאה שצוינו למעלה מתרגילים עתידיים.
הרצת הקוד
מריצים מחדש את הקוד בשורת הפקודה:
go run .
הפלט הבא אמור להופיע יחד עם placeholders לתרגילים עתידיים.
=== Exercise 1: Hello World ===
------ input ------
(interpreter.emptyActivation)
------ result ------
value: Hello, World! (types.String)
5. שימוש במשתנים בפונקציה
רוב האפליקציות ב-CEL יצביעו על משתנים שאפשר להפנות אליהם בתוך ביטויים. הצהרות על משתנים מציינות שם וסוג. סוג המשתנה יכול להיות סוג CEL Built-In, סוג ידוע של מאגר נתונים זמני של פרוטוקול או כל סוג של הודעת Protobuf, כל עוד התיאור שלו סופק ל-CEL.
מוסיפים את הפונקציה
מחפשים בכלי העריכה את ההצהרה exercise2
ומוסיפים את הפרטים הבאים:
// exercise2 shows how to declare and use variables in expressions.
//
// Given a request of type google.rpc.context.AttributeContext.Request
// determine whether a specific auth claim is set.
func exercise2() {
fmt.Println("=== Exercise 2: Variables ===\n")
env, err := cel.NewEnv(
// Add cel.EnvOptions values here.
)
if err != nil {
glog.Exit(err)
}
ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a request object that sets the proper group claim.
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
צריך להריץ מחדש ולהבין את השגיאה
מפעילים מחדש את התוכנית:
go run .
הפלט הבא אמור להתקבל:
ERROR: <input>:1:1: undeclared reference to 'request' (in container '')
| request.auth.claims.group == 'admin'
| ^
בודק הסוגים יפיק שגיאה עבור אובייקט הבקשה, שכוללת את קטע הקוד של המקור שבו אירעה השגיאה.
6. להצהיר על המשתנים
הוספת EnvOptions
כדי לתקן את השגיאה בעורך, צריך לשלוח הצהרה לגבי אובייקט הבקשה כהודעה מסוג google.rpc.context.AttributeContext.Request
, כמו בדוגמה הבאה:
// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
fmt.Println("=== Exercise 2: Variables ===\n")
env, err := cel.NewEnv(
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)
// Evaluate a request object that sets the proper group claim.
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
צריך להריץ מחדש ולהבין את השגיאה
הפעלה חוזרת של התוכנית:
go run .
השגיאה הבאה אמורה להופיע:
ERROR: <input>:1:8: [internal] unexpected failed resolution of 'google.rpc.context.AttributeContext.Request'
| request.auth.claims.group == 'admin'
| .......^
כדי להשתמש במשתנים שמפנים להודעות Protobuf, בודק הסוגים צריך לדעת גם את מתאר הסוג.
כדי לקבוע את תיאור הבקשה בפונקציה cel.Types
צריך להשתמש ב:
// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
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)
// Evaluate a request object that sets the proper group claim.
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
ההפעלה מחדש בוצעה בהצלחה!
מפעילים שוב את התוכנית:
go run .
אתם אמורים לראות את הנתונים הבאים:
=== Exercise 2: Variables ===
request.auth.claims.group == 'admin'
------ input ------
request = time: <
seconds: 1569255569
>
auth: <
principal: "user:me@acme.co"
claims: <
fields: <
key: "group"
value: <
string_value: "admin"
>
>
>
>
------ result ------
value: true (types.Bool)
לצורך בדיקה, הצהרתנו על משתנה מסוג שגיאה, הקצינו לו מתאר סוג ואז הפנינו את המשתנה בהערכת הביטויים.
7. לוגי ו/או
אחת התכונות הייחודיות של CEL היא שימוש באופרטורים לוגיים קומוטטיביים. כל אחד מהצדדים של הסתעפות מותנה יכול לקצר את תהליך ההערכה, גם במקרים של שגיאות או קלט חלקי.
כלומר, המערכת של CEL מוצאת סדר הערכה שמספק תוצאה בכל פעם שהדבר מתאפשר, תוך התעלמות משגיאות או אפילו נתונים חסרים שעשויים להתרחש בהזמנות הערכה אחרות. האפליקציות יכולות להסתמך על המאפיין הזה כדי למזער את עלות ההערכה, וכך לדחות את האיסוף של נתונים יקרים כשאפשר להגיע לתוצאה בלעדיהם.
אנחנו נוסיף דוגמה של AND/OR, ולאחר מכן ננסה אותה עם קלט אחר כדי להבין איך מתבצעת הערכה של מעגלים קצרים ב-CEL.
יצירת הפונקציה
הוסיפו בעורך שלכם את התוכן הבא לתרגיל 3:
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper
// user, and one that specifies an unexpected user.
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"),
),
)
}
בשלב הבא צריך לכלול את הצהרת OR שתחזיר את הערך True אם המשתמש חבר בקבוצה admin
או אם יש לו מזהה אימייל ספציפי:
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper
// user, and one that specifies an unexpected user.
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
שמעריך את המשתמש באמצעות קבוצת הצהרות ריקה:
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
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)
emptyClaims := map[string]string{}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
}
מריצים את הקוד עם קבוצת ההצהרות הריקה
הרצה מחדש של התוכנית אמורה להופיע בפלט החדש הבא:
=== Exercise 3: Logical AND/OR ===
request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'
------ input ------
request = time: <
seconds: 1569302377
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: true (types.Bool)
עדכון של בקשת ההערכה
בשלב הבא, מעדכנים את בקשת ההערכה כך שתועבר בחשבון משתמש אחר שבו מוגדרת ההצהרה הריקה:
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
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)
emptyClaims := map[string]string{}
eval(program, request(auth("other:me@acme.co", emptyClaims), time.Now()))
}
הרצת הקוד עם נקודת זמן
בהפעלה מחדש של התוכנית,
go run .
אתם אמורים לראות את השגיאה הבאה:
=== Exercise 3: Logical AND/OR ===
request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'
------ input ------
request = time: <
seconds: 1588989833
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: true (types.Bool)
------ input ------
request = time: <
seconds: 1588989833
>
auth: <
principal: "other:me@acme.co"
claims: <
>
>
------ result ------
error: no such key: group
ב-protobuf, אנחנו יודעים לאילו שדות וסוגים לצפות. בערכי מפה ו-json, אנחנו לא יודעים אם קיים מפתח. בגלל שאין ערך ברירת מחדל בטוח למפתח חסר, ברירת המחדל של CEL היא שגיאה.
8. פונקציות מותאמות אישית
ב-CEL יש הרבה פונקציות מובנות, אבל יש מקרים שבהם פונקציה מותאמת אישית יכולה להיות שימושית. לדוגמה, אפשר להשתמש בפונקציות מותאמות אישית כדי לשפר את חוויית המשתמש בתנאים נפוצים או כדי לחשוף מצב תלוי-הקשר
בתרגיל הזה נלמד איך לחשוף פונקציה כדי לארוז יחד בדיקות נפוצות.
קריאה לפונקציה מותאמת אישית
קודם כול, יוצרים את הקוד כדי להגדיר שינוי מברירת המחדל שנקרא contains
שקובע אם מפתח קיים במפה ויש לו ערך מסוים. משאירים ערכי placeholder עבור הגדרת הפונקציה וקישור הפונקציות:
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The
// custom map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Add the custom function declaration and binding here with cel.Function()
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan and provide the 'contains' function impl.
// Output: false
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(
program,
request(auth("user:me@acme.co", emptyClaims), time.Now()),
)
fmt.Println()
}
מריצים את הקוד ומבינים את השגיאה
כשמפעילים מחדש את הקוד, אמורה להופיע השגיאה הבאה:
ERROR: <input>:1:29: found no matching overload for 'contains' applied to 'map(string, dyn).(string, string)'
| request.auth.claims.contains('group', 'admin')
| ............................^
כדי לתקן את השגיאה, צריך להוסיף את הפונקציה contains
לרשימת ההצהרות שכוללות כרגע את משתנה הבקשה.
צריך להצהיר על סוג עם פרמטרים על ידי הוספת 3 השורות הבאות. (זה מסובך כמו כל עומס יתר של פונקציות ב-CEL)
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The custom
// map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
// Useful components of the type-signature for 'contains'.
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Add the custom function declaration and binding here with cel.Function()
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan.
// Output: false
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
fmt.Println()
}
מוסיפים את הפונקציה המותאמת אישית
בשלב הבא נוסיף פונקציה חדשה שמכילה פרמטרים, והיא תשתמש בסוגי הפרמטרים הבאים:
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The custom
// map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
// Useful components of the type-signature for 'contains'.
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
// Env declaration.
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Declare the custom contains function and its implementation.
cel.Function("contains",
cel.MemberOverload(
"map_contains_key_value",
[]*cel.Type{mapAB, typeParamA, typeParamB},
cel.BoolType,
// Provide the implementation using cel.FunctionBinding()
),
),
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan and provide the 'contains' function impl.
// Output: false
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
fmt.Println()
}
מפעילים את התוכנית כדי להבין מהי השגיאה
מריצים את התרגיל. השגיאה הבאה אמורה להופיע לגבי פונקציית זמן הריצה החסרה:
------ result ------
error: no such overload: contains
צריך לספק את הטמעת הפונקציה בהצהרה NewEnv
באמצעות הפונקציה cel.FunctionBinding()
:
// exercise4 demonstrates how to extend CEL with custom functions.
//
// Declare a contains member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The custom
// map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
// Useful components of the type-signature for 'contains'.
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
// Env declaration.
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Declare the custom contains function and its implementation.
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)
// Construct the program plan.
// Output: false
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()
}
עכשיו התוכנית אמורה לפעול בהצלחה:
=== Exercise 4: Custom Functions ===
request.auth.claims.contains('group', 'admin')
------ input ------
request = time: <
seconds: 1569302377
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: false (types.Bool)
מה קורה כשקיימת תלונה?
כדי להוסיף קרדיט, כדאי להגדיר את הצהרת האדמין בקלט כדי לוודא שהשדה 'מכיל עומס יתר' יחזיר True גם כשההצהרה קיימת. הפלט הבא אמור להתקבל:
=== Exercise 4: Customization ===
request.auth.claims.contains('group','admin')
------ input ------
request = time: <
seconds: 1588991010
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: false (types.Bool)
------ input ------
request = time: <
seconds: 1588991010
>
auth: <
principal: "user:me@acme.co"
claims: <
fields: <
key: "group"
value: <
string_value: "admin"
>
>
>
>
------ result ------
value: true (types.Bool)
לפני שממשיכים, מומלץ לבדוק את הפונקציה mapContainsKeyValue
עצמה:
// mapContainsKeyValue implements the custom function:
// map.contains(key, value) -> bool.
func mapContainsKeyValue(args ...ref.Val) ref.Val {
// The declaration of the function ensures that only arguments which match
// the mapContainsKey signature will be provided to the function.
m := args[0].(traits.Mapper)
// CEL has many interfaces for dealing with different type abstractions.
// The traits.Mapper interface unifies field presence testing on proto
// messages and maps.
key := args[1]
v, found := m.Find(key)
// If not found and the value was non-nil, the value is an error per the
// `Find` contract. Propagate it accordingly. Such an error might occur with
// a map whose key-type is listed as 'dyn'.
if !found {
if v != nil {
return types.ValOrErr(v, "unsupported key type")
}
// Return CEL False if the key was not found.
return types.False
}
// Otherwise whether the value at the key equals the value provided.
return v.Equal(args[2])
}
כדי להקל על ההוספה, החתימה של פונקציות מותאמות אישית מצפה לארגומנטים מסוג ref.Val
. היתרון כאן הוא שקלות ההוספה מוסיפה עומס על מטמיע כדי להבטיח שכל סוגי הערכים יטופלו כמו שצריך. אם סוגי הארגומנטים של הקלט או מספר הארגומנטים לא תואמים להצהרת הפונקציה, תוחזר שגיאת no such overload
.
ב-cel.FunctionBinding()
נוסף מאבטח מסוג סביבת זמן ריצה כדי לוודא שהחוזה של סביבת זמן הריצה תואם להצהרה שנבדקה בסביבה.
9. פיתוח קובץ JSON
ב-CEL אפשר גם להפיק פלטים לא בוליאניים, כמו JSON. מוסיפים את הערכים הבאים לפונקציה:
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
env, _ := cel.NewEnv(
// Declare the 'now' variable as a Timestamp.
// cel.Variable("now", cel.TimestampType),
)
// Note the quoted keys in the CEL map literal. For proto messages the
// field names are unquoted as they represent well-defined identifiers.
ast := compile(env, `
{'sub': 'serviceAccount:delegate@acme.co',
'aud': 'my-project',
'iss': 'auth.acme.com:12350',
'iat': now,
'nbf': now,
'exp': now + duration('300s'),
'extra_claims': {
'group': 'admin'
}}`,
cel.MapType(cel.StringType, cel.DynType))
program, _ := env.Program(ast)
out, _, _ := eval(
program,
map[string]interface{}{
"now": &tpb.Timestamp{Seconds: time.Now().Unix()},
},
)
fmt.Printf("------ type conversion ------\n%v\n", out)
fmt.Println()
}
הרצת הקוד
כשמפעילים מחדש את הקוד, אמורה להופיע השגיאה הבאה:
ERROR: <input>:5:11: undeclared reference to 'now' (in container '')
| 'iat': now,
| ..........^
... and more ...
צריך להוסיף הצהרה למשתנה now
מסוג cel.TimestampType
אל cel.NewEnv()
ולהריץ אותה שוב:
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
env, _ := cel.NewEnv(
cel.Variable("now", cel.TimestampType),
)
// Note the quoted keys in the CEL map literal. For proto messages the
// field names are unquoted as they represent well-defined identifiers.
ast := compile(env, `
{'sub': 'serviceAccount:delegate@acme.co',
'aud': 'my-project',
'iss': 'auth.acme.com:12350',
'iat': now,
'nbf': now,
'exp': now + duration('300s'),
'extra_claims': {
'group': 'admin'
}}`,
cel.MapType(cel.StringType, cel.DynType))
// Hint:
// Convert `out` to JSON using the valueToJSON() helper method.
// The valueToJSON() function calls ConvertToNative(&structpb.Value{})
// to adapt the CEL value to a protobuf JSON representation and then
// uses the jsonpb.Marshaler utilities on the output to render the JSON
// string.
program, _ := env.Program(ast)
out, _, _ := eval(
program,
map[string]interface{}{
"now": time.Now(),
},
)
fmt.Printf("------ type conversion ------\n%v\n", out)
fmt.Println()
}
מריצים מחדש את הקוד והוא אמור להצליח:
=== Exercise 5: Building JSON ===
{'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'
}
------ input ------
now = seconds: 1569302377
------ result ------
value: &{0xc0002eaf00 map[aud:my-project exp:{0xc0000973c0} extra_claims:0xc000097400 iat:{0xc000097300} iss:auth.acme.com:12350 nbf:{0xc000097300} sub:serviceAccount:delegate@acme.co] {0x8c8f60 0xc00040a6c0 21}} (*types.baseMap)
------ type conversion ------
&{0xc000313510 map[aud:my-project exp:{0xc000442510} extra_claims:0xc0004acdc0 iat:{0xc000442450} iss:auth.acme.com:12350 nbf:{0xc000442450} sub:serviceAccount:delegate@acme.co] {0x9d0ce0 0xc0004424b0 21}}
התוכנה פועלת, אבל צריך להמיר את ערך הפלט out
באופן מפורש ל-JSON. ייצוג ה-CEL הפנימי במקרה הזה ניתן להמרה ל-JSON, כי הוא מתייחס רק לסוגים ש-JSON יכול לתמוך בהם או שיש עבורם מיפוי ידוע של Proto ל-JSON.
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
...
fmt.Printf("------ type conversion ------\n%v\n", valueToJSON(out))
fmt.Println()
}
אחרי שממירים את הסוג באמצעות פונקציית העזרה valueToJSON
בתוך הקובץ codelab.go
, אמור להופיע הפלט הנוסף הבא:
------ type conversion ------
{
"aud": "my-project",
"exp": "2019-10-13T05:54:29Z",
"extra_claims": {
"group": "admin"
},
"iat": "2019-10-13T05:49:29Z",
"iss": "auth.acme.com:12350",
"nbf": "2019-10-13T05:49:29Z",
"sub": "serviceAccount:delegate@acme.co"
}
10. בנייה של פרוטוס
CEL יכול ליצור הודעות protobuf על כל סוג הודעה שעברה הידור באפליקציה. צריך להוסיף את הפונקציה כדי לבנות google.rpc.context.AttributeContext.Request
מהקלט jwt
// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
fmt.Println("=== Exercise 6: Building Protos ===\n")
// Construct an environment and indicate that the container for all references
// within the expression is `google.rpc.context.AttributeContext`.
requestType := &rpcpb.AttributeContext_Request{}
env, _ := cel.NewEnv(
// Add cel.Container() option for 'google.rpc.context.AttributeContext'
cel.Types(requestType),
// Add cel.Variable() option for 'jwt' as a map(string, Dyn) type
// and for 'now' as a timestamp.
)
// Compile the Request message construction expression and validate that
// the resulting expression type matches the fully qualified message name.
//
// Note: the field names within the proto message types are not quoted as they
// are well-defined names composed of valid identifier characters. Also, note
// that when building nested proto objects, the message name needs to prefix
// the object construction.
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)
// Construct the message. The result is a ref.Val that returns a dynamic
// proto message.
out, _, _ := eval(
program,
map[string]interface{}{
"jwt": map[string]interface{}{
"sub": "serviceAccount:delegate@acme.co",
"aud": "my-project",
"iss": "auth.acme.com:12350",
"extra_claims": map[string]string{
"group": "admin",
},
},
"now": time.Now(),
},
)
// Hint: Unwrap the CEL value to a proto. Make sure to use the
// `ConvertToNative(reflect.TypeOf(requestType))` to convert the dynamic proto
// message to the concrete proto message type expected.
fmt.Printf("------ type unwrap ------\n%v\n", out)
fmt.Println()
}
הרצת הקוד
כשמפעילים מחדש את הקוד, אמורה להופיע השגיאה הבאה:
ERROR: <input>:2:10: undeclared reference to 'Request' (in container '')
| Request{
| .........^
הקונטיינר הוא בעצם מקביל למרחב שמות או לחבילה, אבל הוא יכול להיות גם מפורט כמו שם של הודעת Protobuf. כדי לקבוע את המיקום שבו מוצהר על משתנה, פונקציה או שם סוג, קונטיינרים של CEL משתמשים באותם כללי רזולוציה של מרחב השמות כמו Protobuf ו-C++.
בהתאם למאגר google.rpc.context.AttributeContext
, בודק הסוגים והבודק ינסה להשתמש בשמות המזהים הבאים עבור כל המשתנים, הסוגים והפונקציות:
google.rpc.context.AttributeContext.<id>
google.rpc.context.<id>
google.rpc.<id>
google.<id>
<id>
כדי לציין שמות מוחלטים, צריך להוסיף נקודה בהתחלה לפני ההפניה של המשתנה, הסוג או הפונקציה. בדוגמה, הביטוי .<id>
יחפש רק את מזהה <id>
ברמה העליונה, בלי לבדוק קודם בתוך המאגר.
מנסים לציין את האפשרות cel.Container("google.rpc.context.AttributeContext")
לסביבת CEL ומריצים שוב:
// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
fmt.Println("=== Exercise 6: Building Protos ===\n")
// Construct an environment and indicate that the container for all references
// within the expression is `google.rpc.context.AttributeContext`.
requestType := &rpcpb.AttributeContext_Request{}
env, _ := cel.NewEnv(
// Add cel.Container() option for 'google.rpc.context.AttributeContext'
cel.Container("google.rpc.context.AttributeContext.Request"),
cel.Types(requestType),
// Later, add cel.Variable() options for 'jwt' as a map(string, Dyn) type
// and for 'now' as a timestamp.
// cel.Variable("now", cel.TimestampType),
// cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
)
...
}
הפלט הבא אמור להתקבל:
ERROR: <input>:4:16: undeclared reference to 'jwt' (in container 'google.rpc.context.AttributeContext')
| principal: jwt.iss + '/' + jwt.sub,
| ...............^
... ושגיאות רבות נוספות...
בשלב הבא מצהירים על המשתנים jwt
ו-now
והתוכנה צריכה לפעול כצפוי:
=== Exercise 6: Building Protos ===
Request{
auth: Auth{
principal: jwt.iss + '/' + jwt.sub,
audiences: [jwt.aud],
presenter: 'azp' in jwt ? jwt.azp : "",
claims: jwt
},
time: now
}
------ input ------
jwt = {
"aud": "my-project",
"extra_claims": {
"group": "admin"
},
"iss": "auth.acme.com:12350",
"sub": "serviceAccount:delegate@acme.co"
}
now = seconds: 1588993027
------ result ------
value: &{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false} (*types.protoObj)
----- type unwrap ----
&{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false}
כקרדיט נוסף, כדאי גם לפתוח את האריזה על ידי התקשרות למספר out.Value()
בהודעה כדי לראות איך התוצאה משתנה.
11. פקודות מאקרו
ניתן להשתמש ברכיבי מאקרו כדי לשנות את תוכנת CEL בזמן הניתוח. פקודות מאקרו תואמות לחתימת קריאה ומשנים את קריאת הקלט ואת הארגומנטים שלה כדי ליצור AST חדש של תת-ביטוי.
אפשר להשתמש ברכיבי מאקרו כדי להטמיע לוגיקה מורכבת ב-AST שלא ניתן לכתוב ישירות ב-CEL. לדוגמה, המאקרו has
מפעיל בדיקת נוכחות בשדה. פקודות המאקרו של ההבנה, כמו קיימות, וכולן מחליפות את הפעלת הפונקציה באיטרציה מוגבלת ברשימת קלט או במפה. אי אפשר להשתמש באף אחד מהמושגים ברמת התחביר, אבל אפשר לעשות זאת באמצעות הרחבות מאקרו.
הוסיפו ומריצים את התרגיל הבא:
// exercise7 introduces macros for dealing with repeated fields and maps.
//
// Determine whether the jwt.extra_claims has at least one key that starts
// with the group prefix, and ensure that all group-like keys have list
// values containing only strings that end with '@acme.co`.
func exercise7() {
fmt.Println("=== Exercise 7: Macros ===\n")
env, _ := cel.NewEnv(
cel.ClearMacros(),
cel.Variable("jwt", cel.MapType(cel.StringType, 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)
// Evaluate a complex-ish JWT with two groups that satisfy the criteria.
// Output: true.
eval(program,
map[string]interface{}{
"jwt": map[string]interface{}{
"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()
}
השגיאות הבאות אמורות להופיע:
ERROR: <input>:1:25: undeclared reference to 'c' (in container '')
| jwt.extra_claims.exists(c, c.startsWith('group'))
| ........................^
... ועוד רבים אחרים ...
השגיאות האלה מתרחשות מכיוון שרכיבי המאקרו עדיין לא מופעלים. כדי להפעיל רכיבי מאקרו, מסירים את cel.ClearMacros()
ומריצים שוב:
=== Exercise 7: Macros ===
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')))
------ input ------
jwt = {
"aud": "my-project",
"extra_claims": {
"group1": [
"admin@acme.co",
"analyst@acme.co"
],
"groupN": [
"forever@acme.co"
],
"labels": [
"metadata",
"prod",
"pii"
]
},
"iss": "auth.acme.com:12350",
"sub": "serviceAccount:delegate@acme.co"
}
------ result ------
value: true (types.Bool)
נכון לעכשיו, אלה פקודות המאקרו שנתמכות:
Macro | חתימה | תיאור |
הכל | r.all(var, cond) | בודקים אם הפרמטר cond מקבל את הערך true עבור all var בטווח r. |
קיים | r.exists(var, cond) | בודקים אם cond הערך של TRUE לגבי כלשהו בטווח r. |
exists_one | r.exists_one(var, cond) | בודקים אם cond הערך של true עבור var אחד בטווח r. |
סינון | r.filter(var, cond) | לרשימות, צריך ליצור רשימה חדשה שבה כל רכיב var בטווח r עומד בתנאי התנאי. למפות, צריך ליצור רשימה חדשה שבה כל משתנה מפתח בטווח r עומד בתנאי התנאי. |
מפה | r.map(var, expr) | יוצרים רשימה חדשה שבה כל משתנה בטווח r משתנה על ידי expr. |
r.map(var, cond, expr) | זהה למיפוי דו-שלבי, אבל עם מסנן cond מותנה לפני שהערך משתנה. | |
has (יש) | has(a.b) | בדיקת נוכחות עבור b בערך a : למפות, להגדיר בדיקות json. עבור Proto, מתבצעת בדיקה של ערך ראשוני שאינו ברירת המחדל או שדה הודעה מוגדר או שדה הודעה מוגדר. |
כשהארגומנט r של הטווח הוא map
, הערך var
יהיה מפתח המפה, ועבור ערכים מסוג list
הערך var
יהיה ערך רכיב הרשימה. פקודות המאקרו all
, exists
, exists_one
, filter
ו-map
מבצעים שכתוב AST שמבצע עבור כל איטרציה מוגבלת לגודל הקלט.
המשמעות של המסגרת הרעיונית מוודאת שתוכניות CEL לא יהיו השלמה של טיינג', אבל הן מבצעות הערכה בזמן סופר-לינארי ביחס לקלט. להשתמש בפקודות המאקרו האלה באופן מדוד או בכלל לא. שימוש כבד בהבנה הוא בדרך כלל סימן טוב לכך שפונקציה מותאמת אישית תספק חוויית משתמש טובה יותר וביצועים טובים יותר.
12. כוונון
יש כרגע כמה פיצ'רים בלעדיים ל-CEL-Go, שמצביעים על תוכניות עתידיות של הטמעות אחרות ב-CEL. התרגיל הבא מציג תוכניות שונות לאותו AST:
// exercise8 covers features of CEL-Go which can be used to improve
// performance and debug evaluation behavior.
//
// Turn on the optimization, exhaustive eval, and state tracking
// ProgramOption flags to see the impact on evaluation behavior.
func exercise8() {
fmt.Println("=== Exercise 8: Tuning ===\n")
// Declare the x and 'y' variables as input into the expression.
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)
// Try the different cel.EvalOptions flags when evaluating this AST for
// the following use cases:
// - cel.OptOptimize: optimize the expression performance.
// - cel.OptExhaustiveEval: turn off short-circuiting.
// - cel.OptTrackState: track state and compute a residual using the
// interpreter.PruneAst function.
program, _ := env.Program(ast)
eval(program, cel.NoVars())
fmt.Println()
}
Optimize
כשסימון האופטימיזציה מופעל, CEL מקדיש זמן נוסף ליצירת מילים מילוליות של רשימות ולמפות מראש, וביצוע אופטימיזציה לקריאות מסוימות, כמו האופרטור in, כבדיקת מינוי קבועה:
// Turn on optimization.
trueVars := map[string]interface{}{"x": int64(4), "y": uint64(2)}
program, _ := env.Program(ast, cel.EvalOptions(cel.OptOptimize))
// Try benchmarking with the optimization flag on and off.
eval(program, trueVars)
אם אותה תוכנית נבדקת הרבה פעמים ביחס לנתוני קלט שונים, מומלץ לבצע אופטימיזציה. עם זאת, במקרים שבהם אנחנו מעריכים את התוכנית רק פעם אחת, האופטימיזציה רק מוסיפה לתקורה.
הערכה מלאה
'הערכה מלאה' יכולה להיות שימושית לניפוי באגים בהתנהגות ההערכה של ביטויים, כי היא מספקת תובנות לגבי הערך שנמדד בכל שלב בהערכת הביטוי.
// Turn on exhaustive eval to see what the evaluation state looks like.
// The input is structure to show a false on the first branch, and true
// on the second.
falseVars := map[string]interface{}{"x": int64(6), "y": uint64(2)}
program, _ = env.Program(ast, cel.EvalOptions(cel.OptExhaustiveEval))
eval(program, falseVars)
אתם אמורים לראות רשימה של מצב הערכת הביטוי עבור כל מזהה ביטוי:
------ eval states ------
1: 6 (types.Int)
2: false (types.Bool)
3: &{0xc0000336b0 [1 2 3 4 5] {0x89f020 0xc000434760 151}} (*types.baseList)
4: 1 (types.Int)
5: 2 (types.Int)
6: 3 (types.Int)
7: 4 (types.Int)
8: 5 (types.Int)
9: uint (*types.Type)
10: 2 (types.Uint)
11: true (types.Bool)
12: uint (*types.Type)
13: false (types.Bool)
מזהה הביטוי 2 תואם לתוצאת האופרטור באופרטור הראשון. הסתעפות ומזהה ביטוי 11 תואם לאופרטור == בשני. בהערכה רגילה, הביטוי היה מקוצר לאחר החישוב של 2. אם y לא היה טעון, המצב היה מראה שתי סיבות לכך שהביטוי נכשל ולא רק אחת.
13. מה נכלל בקורס?
אם אתם צריכים מנוע ביטוי, כדאי להשתמש ב-CEL. שימוש ב-CEL מתאים במיוחד לפרויקטים שבהם צריך להפעיל הגדרות משתמש שבהן הביצועים קריטיים.
בתרגילים הקודמים, אנחנו מקווים שהתרגלתם להעביר את הנתונים ל-CEL ולהוציא מהם את הפלט או ההחלטה.
אנחנו מקווים שיש לכם מושג לגבי הפעולות שאתם יכולים לבצע, החל מהחלטה בוליאנית ועד ליצירת הודעות JSON ו-Protobuffer.
אנחנו מקווים שאתם מבינים איך לעבוד עם הביטויים ומה הם עושים. אנחנו גם מבינים את הדרכים הנפוצות להרחיב אותו.