CEL-Go Codelab: ביטויים מהירים, בטוחים ומוטמעים

1. מבוא

מהי CEL?

‫CEL היא שפת ביטויים לא שלמה של טיורינג, שנועדה להיות מהירה, ניידת ובטוחה להרצה. אפשר להשתמש ב-CEL לבד או להטמיע אותה במוצר גדול יותר.

שפת CEL תוכננה כשפה שבה אפשר להריץ קוד משתמש בצורה בטוחה. מסוכן להפעיל את הפונקציה eval() באופן עיוור על קוד Python של משתמש, אבל אפשר להריץ קוד CEL של משתמש בצורה בטוחה. בנוסף, מכיוון ש-CEL מונעת התנהגות שתפגע בביצועים, היא מבצעת הערכה בבטחה בטווח של ננו-שניות עד מיקרו-שניות, ולכן היא אידיאלית לאפליקציות שבהן הביצועים הם קריטיים.

‫CEL מעריכה ביטויים, שדומים לפונקציות של שורה אחת או לביטויי למדה. למרות ש-CEL משמש בדרך כלל להחלטות בוליאניות, אפשר להשתמש בו גם כדי ליצור אובייקטים מורכבים יותר כמו הודעות JSON או protobuf.

האם CEL מתאים לפרויקט שלכם?

מכיוון ש-CEL מעריכה ביטוי מ-AST בננו-שניות עד מיקרו-שניות, תרחישי השימוש האידיאליים ב-CEL הם אפליקציות עם נתיבים קריטיים לביצועים. הקומפילציה של קוד CEL ל-AST לא צריכה להתבצע בנתיבים קריטיים. אפליקציות אידיאליות הן כאלה שההגדרה שלהן מופעלת לעיתים קרובות ומשתנה בתדירות נמוכה יחסית.

לדוגמה, הפעלה של מדיניות אבטחה עם כל בקשת HTTP לשירות היא תרחיש שימוש אידיאלי ל-CEL, כי מדיניות האבטחה משתנה לעיתים רחוקות, ול-CEL תהיה השפעה זניחה על זמן התגובה. במקרה הזה, CEL מחזירה ערך בוליאני, אם הבקשה צריכה להיות מאושרת או לא, אבל היא יכולה להחזיר הודעה מורכבת יותר.

מה נכלל ב-Codelab הזה?

בשלב הראשון של ה-codelab הזה מוסבר למה כדאי להשתמש ב-CEL ומוצגים המושגים המרכזיים שלה. החלקים הבאים מוקדשים לתרגילים בתכנות שכוללים תרחישי שימוש נפוצים. למידע נוסף על השפה, הסמנטיקה והתכונות, אפשר לעיין בהגדרת השפה ב-CEL ב-GitHub ובמסמכי התיעוד של CEL Go.

ה-codelab הזה מיועד למפתחים שרוצים ללמוד CEL כדי להשתמש בשירותים שכבר תומכים ב-CEL. ב-Codelab הזה לא מוסבר איך לשלב את CEL בפרויקט שלכם.

מה תלמדו

  • מושגי ליבה מ-CEL
  • ‫Hello, World: שימוש ב-CEL להערכת מחרוזת
  • יצירת משתנים
  • הסבר על קיצור דרך ב-CEL בפעולות לוגיות של AND/OR
  • איך משתמשים ב-CEL כדי ליצור JSON
  • איך משתמשים ב-CEL כדי ליצור Protobuffers
  • יצירת פקודות מאקרו
  • דרכים לכוונן את ביטויי ה-CEL

מה תצטרכו

דרישות מוקדמות

ה-Codelab הזה מבוסס על הבנה בסיסית של Protocol Buffers ושל Go Lang.

אם אתם לא מכירים את Protocol Buffers, התרגיל הראשון ייתן לכם מושג איך CEL עובד, אבל יכול להיות שיהיה לכם קשה להבין את הדוגמאות המתקדמות יותר כי הן משתמשות ב-Protocol Buffers כקלט ל-CEL. מומלץ לעיין קודם באחד מהמדריכים האלה. חשוב לציין שלא חייבים להשתמש ב-Protocol Buffers כדי להשתמש ב-CEL, אבל נעשה בהם שימוש נרחב ב-codelab הזה.

כדי לבדוק אם Go מותקן, מריצים את הפקודה:

go --help

2. מושגים מרכזיים

אפליקציות

‫CEL היא שפה למטרות כלליות, והיא משמשת למגוון רחב של אפליקציות, החל מניתוב של קריאות RPC ועד להגדרת מדיניות אבטחה. ‫CEL היא שפה שניתנת להרחבה, לא תלויה באפליקציה ומותאמת לתהליכי עבודה של קומפילציה חד-פעמית והערכה רב-פעמית.

הרבה שירותים ואפליקציות מעריכים הגדרות הצהרה. לדוגמה, בקרת גישה מבוססת-תפקידים (RBAC) היא הגדרה הצהרתית שמפיקה החלטת גישה בהינתן תפקיד וקבוצת משתמשים. אם הגדרות הצהרתיות מתאימות ל-80% מהתרחישים, אז CEL הוא כלי שימושי ל-20% הנותרים, כשמשתמשים צריכים יותר כוח ביטוי.

אוסף

הידור של ביטוי מתבצע בהתאם לסביבה. בשלב הקימפול נוצר עץ תחביר מופשט (AST) בפורמט protobuf. בדרך כלל, ביטויים שעברו קומפילציה מאוחסנים לשימוש עתידי כדי שההערכה תהיה מהירה ככל האפשר. אפשר להעריך ביטוי מורכב יחיד עם הרבה קלטים שונים.

הבעות

המשתמשים מגדירים ביטויים, והשירותים והאפליקציות מגדירים את הסביבה שבה הביטויים פועלים. חתימת פונקציה מכריזה על הקלטים, והיא נכתבת מחוץ לביטוי 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 מגדירים את סביבת הביטוי. הסביבה היא אוסף של משתנים ופונקציות שאפשר להשתמש בהם בביטויים.

ההצהרות מבוססות הפרוטו משמשות את בודק הטיפוסים של CEL כדי לוודא שכל ההפניות למזהים ולפונקציות בתוך ביטוי מוצהרות ומשמשות בצורה נכונה.

שלושה שלבים בניתוח של ביטוי

יש שלושה שלבים בעיבוד ביטוי: ניתוח, בדיקה והערכה. הדפוס הנפוץ ביותר ב-CEL הוא שמישור הבקרה מנתח ובודק ביטויים בזמן ההגדרה, ומאחסן את ה-AST.

c71fc08068759f81.png

בזמן הריצה, מישור הנתונים מאחזר ומעריך את ה-AST שוב ושוב. השפה CEL מותאמת ליעילות בזמן ריצה, אבל לא מומלץ לבצע ניתוח ואימות של קוד בנתיבי קוד שרגישים לזמן אחזור.

49ab7d8517143b66.png

המערכת מנתחת את CEL מביטוי שקריא לבני אדם לעץ תחביר מופשט באמצעות דקדוק של מנתח לקסיקלי / מנתח תחבירי של ANTLR. בשלב הניתוח נוצר עץ תחביר מופשט מבוסס-פרוטו, שבו כל צומת Expr בעץ מכיל מזהה מספר שלם שמשמש לאינדקס של מטא-נתונים שנוצרו במהלך הניתוח והבדיקה. הקובץ syntax.proto שנוצר במהלך הניתוח מייצג באופן מדויק את הייצוג המופשט של מה שהוקלד בצורת המחרוזת של הביטוי.

אחרי שמנתחים ביטוי, אפשר לבדוק אותו מול הסביבה כדי לוודא שכל המזהים של המשתנים והפונקציות בביטוי הוגדרו ונעשה בהם שימוש נכון. בודק הטיפוסים יוצר קובץ checked.proto שכולל מטא-נתונים של טיפוסים, משתנים ופונקציות, שיכולים לשפר באופן משמעותי את יעילות ההערכה.

הכלי להערכת CEL צריך 3 דברים:

  • קישורי פונקציות לכל התוספים המותאמים אישית
  • קשירת משתנים
  • עץ תחביר מופשט להערכה

הפונקציה והמשתנים שמוגדרים בה צריכים להיות זהים לאלה ששימשו להידור של ה-AST. אפשר להשתמש מחדש בכל אחד מהקלטות האלה בכמה הערכות, למשל, הערכה של AST בכמה קבוצות של קשרי משתנים, או שימוש באותם משתנים בכמה AST, או שימוש בקשרי הפונקציות לאורך כל משך החיים של תהליך (מקרה נפוץ).

3. הגדרה

הקוד של ה-Codelab הזה נמצא בcodelab folder של מאגר cel-go. הפתרון זמין בcodelab/solution תיקייה של אותו מאגר.

משכפלים את המאגר ועוברים אליו:

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:

חבילה

מיקום המקור

תיאור

cel

cel-go/cel

ממשקים ברמה העליונה

ref

cel-go/common/types/ref

ממשקי עזר

סוגים

cel-go/common/types

הערכים של סוג סביבת זמן הריצה

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() מגדיר את סביבת Standard.

אפשר להתאים אישית את הסביבה על ידי העברת האפשרויות 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() הוא לא nil, יש שגיאה בתחביר או בסמנטיקה, והתוכנית לא יכולה להמשיך. אם הביטוי בנוי בצורה תקינה, התוצאה של הקריאות האלה היא קובץ הפעלה 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, סוג מוכר של מאגר אחסון לפרוטוקולים או כל סוג של הודעת 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, שקובע אם מפתח קיים במפה ויש לו ערך מסוים. משאירים ערכי פלייסהולדר להגדרת הפונקציה ולקישור הפונקציה:

// 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()
}

הוספת הפונקציה המותאמת אישית

לאחר מכן, נוסיף פונקציית contains חדשה שתשתמש בסוגים עם פרמטרים:

// 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)

מה קורה כשהתלונה קיימת?

כדי לקבל נקודות בונוס, נסו להגדיר את התביעה של האדמין בקלט כדי לוודא שגם העומס העודף של הפונקציה contains מחזיר 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. יצירת קובצי Proto

‫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 שלא ניתן לכתוב ישירות ב-CEL. לדוגמה, מאקרו has מאפשר בדיקה של נוכחות שדה. פקודות מאקרו להבנה כמו exists ו-all מחליפות בקשה להפעלת פונקציה באיטרציה מוגבלת על רשימת קלט או על מיפוי. אי אפשר להשתמש באף אחד מהקונספטים האלה ברמה התחבירית, אבל אפשר להשתמש בהם באמצעות הרחבות מאקרו.

מוסיפים ומריצים את התרגיל הבא:

// 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 עבור כל המשתנים var בטווח r.

קיים

r.exists(var, cond)

הפונקציה בודקת אם התנאי cond מחזיר את הערך True עבור משתנה כלשהו בטווח r.

exists_one

r.exists_one(var, cond)

הפונקציה בודקת אם התנאי cond מחזיר את הערך True עבור רק משתנה אחד בטווח r.

סינון

r.filter(var, cond)

לרשימות, יוצרים רשימה חדשה שבה כל רכיב var בטווח r עומד בתנאי cond. למפות, ליצור רשימה חדשה שבה כל משתנה מפתח בטווח r מקיים את התנאי cond.

מפה

r.map(var, expr)

יוצרת רשימה חדשה שבה כל משתנה בטווח r עובר טרנספורמציה באמצעות expr.

r.map(var, cond, expr)

אותו דבר כמו map עם שני ארגומנטים, אבל עם מסנן cond מותנה לפני שהערך עובר טרנספורמציה.

has (יש)

has(a.b)

בדיקת נוכחות של b בערך a : להגדרת בדיקות json במפות. לפרוטוקולים, בודקים ערך פרימיטיבי שאינו ברירת מחדל או שדה הודעה מוגדר.

אם הארגומנט r של הטווח הוא מסוג map, הפונקציה var תהיה מפתח המפה, ואם הערכים הם מסוג list, הפונקציה var תהיה ערך רכיב הרשימה. רכיבי המאקרו all,‏ exists,‏ exists_one,‏ filter ו-map מבצעים שכתוב של AST שמבצע איטרציה של for-each שמוגבלת לגודל הקלט.

ההבנות המוגבלות מבטיחות שתוכניות 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)

אם אותו תוכנית מוערכת הרבה פעמים על סמך קלט שונה, כדאי לבצע אופטימיזציה. עם זאת, אם התוכנית תיבדק רק פעם אחת, האופטימיזציה רק תוסיף עומס.

Exhaustive Eval

האפשרות 'הערכה מקיפה' יכולה להיות שימושית לניפוי באגים בהתנהגות של הערכת ביטויים, כי היא מספקת תובנות לגבי הערך שנצפה בכל שלב בהערכת הביטוי.

    // 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 תואם לתוצאה של אופרטור in בענף הראשון, ומזהה הביטוי 11 תואם לאופרטור == בענף השני. בבדיקה רגילה, הביטוי היה מתקצר אחרי החישוב של 2. אם y לא היה uint, המצב היה מציג שתי סיבות לכך שהביטוי ייכשל ולא רק אחת.

13. מה נכלל?

אם אתם צריכים מנוע ביטויים, כדאי להשתמש ב-CEL. ‫CEL היא בחירה מצוינת לפרויקטים שבהם צריך להריץ הגדרות משתמשים, והביצועים הם קריטיים.

אנחנו מקווים שהתרגילים הקודמים עזרו לכם להבין איך להעביר נתונים ל-CEL ולקבל את הפלט או את ההחלטה בחזרה.

אנחנו מקווים שהבנתם אילו פעולות אפשר לבצע, החל מהחלטה בוליאנית ועד ליצירת הודעות JSON ו-Protobuffer.

אנחנו מקווים שהבנתם איך לעבוד עם הביטויים ומה הם עושים. אנחנו מבינים את הדרכים הנפוצות להארכת התקופה.