CEL-Go Codelab: عبارات سریع، ایمن و جاسازی شده

1. مقدمه

CEL چیست؟

CEL یک زبان بیان کامل غیر تورینگ است که برای اجرای سریع، قابل حمل و ایمن طراحی شده است. CEL را می توان به تنهایی استفاده کرد یا در یک محصول بزرگتر جاسازی کرد.

CEL به عنوان زبانی طراحی شده است که اجرای کد کاربر در آن ایمن است. در حالی که فراخوانی کورکورانه eval() روی کد پایتون کاربر خطرناک است، می‌توانید با خیال راحت کد CEL کاربر را اجرا کنید. و از آنجایی که CEL از رفتاری جلوگیری می کند که عملکرد آن را کاهش می دهد، به طور ایمن بر اساس نانوثانیه تا میکروثانیه ارزیابی می شود. برای برنامه های کاربردی حیاتی ایده آل است.

CEL عباراتی را که شبیه توابع تک خطی یا عبارات لامبدا هستند ارزیابی می کند. در حالی که CEL معمولا برای تصمیم گیری های بولی استفاده می شود، می تواند برای ساخت اشیاء پیچیده تری مانند JSON یا پیام های پروتوباف نیز استفاده شود.

آیا CEL برای پروژه شما مناسب است؟

از آنجایی که CEL بیانی از AST را در نانوثانیه تا میکروثانیه ارزیابی می‌کند، استفاده ایده‌آل برای CEL، کاربردهایی با مسیرهای حیاتی عملکرد هستند. کامپایل کد CEL در AST نباید در مسیرهای بحرانی انجام شود. برنامه های کاربردی ایده آل برنامه هایی هستند که در آنها پیکربندی اغلب اجرا می شود و نسبتاً به ندرت اصلاح می شود.

به عنوان مثال، اجرای یک خط مشی امنیتی با هر درخواست HTTP به یک سرویس یک مورد استفاده ایده آل برای CEL است زیرا خط مشی امنیتی به ندرت تغییر می کند و CEL تأثیر ناچیزی بر زمان پاسخ خواهد داشت. در این مورد، CEL یک Boolean برمی‌گرداند، اگر درخواست باید مجاز باشد یا خیر، اما می‌تواند پیام پیچیده‌تری را برگرداند.

چه چیزی در این Codelab پوشش داده شده است؟

اولین گام این کد آزمایشگاه انگیزه استفاده از CEL و مفاهیم اصلی آن است. بقیه به تمرین‌های کدنویسی اختصاص دارد که موارد استفاده رایج را پوشش می‌دهند. برای نگاهی عمیق‌تر به زبان، معناشناسی و ویژگی‌ها به تعریف زبان CEL در GitHub و CEL Go Docs مراجعه کنید.

هدف این کد لبه توسعه دهندگانی است که می خواهند CEL را یاد بگیرند تا از خدماتی استفاده کنند که قبلاً CEL را پشتیبانی می کنند. این کد لبه نحوه ادغام CEL در پروژه خود را پوشش نمی دهد.

چیزی که یاد خواهید گرفت

  • مفاهیم اصلی از CEL
  • سلام، جهان: استفاده از CEL برای ارزیابی یک رشته
  • ایجاد متغیرها
  • درک اتصال کوتاه CEL در عملیات منطقی و/یا
  • نحوه استفاده از CEL برای ساخت JSON
  • نحوه استفاده از CEL برای ساخت پروتوبافرها
  • ایجاد ماکرو
  • راه هایی برای تنظیم عبارات CEL

آنچه شما نیاز دارید

پیش نیازها

این آزمایشگاه کد بر اساس درک اولیه پروتکل بافر و Go Lang است.

اگر با Protocol Buffer آشنا نیستید، تمرین اول به شما این حس را می دهد که CEL چگونه کار می کند، اما چون نمونه های پیشرفته تر از Protocol Buffer به عنوان ورودی CEL استفاده می کنند، ممکن است درک آنها سخت تر باشد. ابتدا یکی از این آموزش ها را در نظر بگیرید. توجه داشته باشید که بافرهای پروتکل برای استفاده از CEL مورد نیاز نیستند، اما به طور گسترده در این آزمایشگاه کد استفاده می شوند.

با اجرای زیر می توانید تست کنید که go نصب شده است:

go --help

2. مفاهیم کلیدی

برنامه های کاربردی

CEL یک هدف کلی است و برای کاربردهای مختلف، از مسیریابی RPCها گرفته تا تعیین خط مشی امنیتی استفاده شده است. CEL توسعه‌پذیر است، برنامه کاربردی آگنوستیک است و برای یک بار کامپایل کردن، ارزیابی بسیاری از گردش‌های کاری بهینه شده است.

بسیاری از سرویس‌ها و برنامه‌ها پیکربندی‌های اعلامی را ارزیابی می‌کنند. به عنوان مثال، کنترل دسترسی مبتنی بر نقش (RBAC) یک پیکربندی اعلامی است که یک تصمیم دسترسی به یک نقش و مجموعه ای از کاربران را تولید می کند. اگر تنظیمات اعلامی 80٪ مورد استفاده هستند، CEL ابزار مفیدی برای گرد کردن 20٪ باقیمانده زمانی است که کاربران نیاز به قدرت بیان بیشتری دارند.

تالیف

یک عبارت در برابر یک محیط کامپایل می شود. مرحله کامپایل یک درخت نحو انتزاعی (AST) را به شکل پروتوباف تولید می کند. عبارات کامپایل شده به طور کلی برای استفاده در آینده ذخیره می شوند تا ارزیابی را در سریع ترین زمان ممکن حفظ کنند. یک عبارت کامپایل شده را می توان با ورودی های مختلف ارزیابی کرد.

عبارات

کاربران عبارات را تعریف می کنند. سرویس ها و برنامه ها محیطی را که در آن اجرا می شود را تعریف می کنند. امضای تابع ورودی ها را اعلام می کند و خارج از عبارت CEL نوشته می شود. کتابخانه توابع موجود برای CEL به صورت خودکار وارد می شود.

در مثال زیر عبارت یک شی درخواست را می گیرد و درخواست شامل یک نشانه ادعا می شود. عبارت یک Boolean برمی گرداند که نشان می دهد آیا نشانه ادعا هنوز معتبر است یا خیر.

// 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 /parser تجزیه می شود. فاز تجزیه یک درخت نحو انتزاعی مبتنی بر پروتو منتشر می کند که در آن هر گره Expr در AST حاوی یک شناسه عدد صحیح است که برای نمایه سازی به ابرداده تولید شده در طول تجزیه و بررسی استفاده می شود. syntax.proto تولید شده در حین تجزیه به طور صادقانه نمایش انتزاعی آنچه در شکل رشته ای عبارت تایپ شده است را نشان می دهد.

هنگامی که یک عبارت تجزیه شد، ممکن است در برابر محیط بررسی شود تا اطمینان حاصل شود که همه شناسه های متغیر و تابع در عبارت اعلام شده اند و به درستی استفاده می شوند. Type-checker یک checked.proto را تولید می کند که شامل متادیتای نوع، متغیر و وضوح تابع است که می تواند کارایی ارزیابی را به شدت بهبود بخشد.

ارزیاب CEL به 3 چیز نیاز دارد:

  • اتصالات تابع برای هر برنامه افزودنی سفارشی
  • اتصالات متغیر
  • یک AST برای ارزیابی

تابع و اتصالات متغیر باید با آنچه برای کامپایل AST استفاده شد مطابقت داشته باشد. هر یک از این ورودی‌ها را می‌توان مجدداً در چندین ارزیابی استفاده کرد، مانند AST که در بسیاری از مجموعه‌های اتصالات متغیر ارزیابی می‌شود، یا همان متغیرهایی که در برابر بسیاری از ASTها استفاده می‌شوند، یا پیوندهای تابعی که در طول عمر یک فرآیند استفاده می‌شوند (یک مورد رایج ).

3. راه اندازی کنید

کد این Codelab در پوشه codelab مخزن 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 باز کنید. شما باید تابع اصلی را که اجرای تمرینات را در این کد لبه هدایت می کند و به دنبال آن سه بلوک از توابع کمکی را مشاهده کنید. اولین مجموعه کمکی به مراحل ارزیابی CEL کمک می کند:

  • تابع Compile : تجزیه و بررسی و بررسی و عبارت ورودی در برابر یک محیط
  • تابع Eval : یک برنامه کامپایل شده را در مقابل یک ورودی ارزیابی می کند
  • عملکرد Report : نتیجه ارزیابی را به زیبایی چاپ می کند

به‌علاوه، request و کمک‌کنندگان auth برای کمک به ساخت ورودی برای تمرین‌های مختلف ارائه شده‌اند.

تمرین ها به بسته ها با نام بسته کوتاه آنها اشاره می کنند. اگر می‌خواهید جزئیات را بررسی کنید، نقشه‌برداری از بسته به مکان منبع در مخزن google/cel-go در زیر آمده است:

بسته

محل منبع

توضیحات

سلول

cel-go/cel

رابط های سطح بالا

رجوع کنید

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() محیط استاندارد را پیکربندی می کند.

محیط را می توان با ارائه گزینه های 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 از پروتو با استفاده از توابع 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 .

باید خروجی زیر را به همراه متغیرهایی برای تمرین‌های آینده ببینید.

=== Exercise 1: Hello World ===

------ input ------
(interpreter.emptyActivation)

------ result ------
value: Hello, World! (types.String)

5. از متغیرها در یک تابع استفاده کنید

اکثر برنامه های CEL متغیرهایی را که می توانند در عبارات ارجاع داده شوند، اعلام می کنند. اعلان متغیرها یک نام و یک نوع را مشخص می کند. نوع یک متغیر ممکن است یک نوع داخلی CEL ، یک نوع بافر پروتکل شناخته شده، یا هر نوع پیام پروتوباف باشد تا زمانی که توصیفگر آن نیز به 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'
 | .......^

برای استفاده از متغیرهایی که به پیام های پروتوباف اشاره می کنند، بررسی کننده نوع باید توصیفگر نوع را نیز بداند.

از 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 را اضافه کنید که اگر کاربر عضو گروه 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، ما می دانیم که چه زمینه ها و انواعی را باید انتظار داشته باشیم. در مقادیر map و json، نمی دانیم که آیا کلیدی وجود دارد یا خیر. از آنجایی که یک مقدار پیش‌فرض امن برای یک کلید از دست رفته وجود ندارد، CEL به‌طور پیش‌فرض خطا را نشان می‌دهد.

8. توابع سفارشی

در حالی که CEL شامل بسیاری از توابع داخلی است، مواردی وجود دارد که یک تابع سفارشی مفید است. به عنوان مثال، توابع سفارشی را می توان برای بهبود تجربه کاربر برای شرایط رایج یا نمایش وضعیت حساس به زمینه استفاده کرد

در این تمرین، نحوه نمایش یک تابع را برای بسته بندی چک های رایج مورد بررسی قرار خواهیم داد.

یک تابع سفارشی را فراخوانی کنید

ابتدا، کدی را ایجاد کنید تا یک override به نام 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()
}

تابع سفارشی را اضافه کنید

در مرحله بعد، یک تابع حاوی جدید اضافه می کنیم که از انواع پارامتر شده استفاده می کند:

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

با استفاده از تابع cel.FunctionBinding() اجرای تابع را در اعلان NewEnv ارائه دهید:

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

وقتی ادعا وجود دارد چه اتفاقی می افتد؟

برای اعتبار بیشتر، سعی کنید ادعای سرپرست را روی ورودی تنظیم کنید تا تأیید کنید که در صورت وجود ادعا، مقدار اضافه بار نیز درست است. شما باید خروجی زیر را ببینید:

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

هنگامی که نوع با استفاده از تابع helper 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 می تواند پیام های پروتوباف را برای هر نوع پیامی که در برنامه کامپایل شده است بسازد. تابع را برای ساختن 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{
 | .........^

کانتینر اساساً معادل فضای نام یا بسته است، اما حتی می تواند به اندازه نام پیام پروتوباف دانه ای باشد. کانتینرهای 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 and 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)

اینها ماکروهایی هستند که در حال حاضر پشتیبانی می شوند:

ماکرو

امضا

توضیحات

همه

r.all(var، cond)

تست کنید آیا cond برای همه var در محدوده r درست ارزیابی می کند.

وجود دارد

r.exists(var، cond)

تست کنید آیا cond برای هر var در محدوده r درست ارزیابی می کند.

وجود دارد_یک

r.exists_one(var، cond)

تست کنید آیا cond فقط برای یک var در محدوده r درست ارزیابی می کند.

فیلتر

r.filter(var، cond)

برای لیست ها، یک لیست جدید ایجاد کنید که در آن هر عنصر var در محدوده r شرایط شرط را برآورده کند. برای نقشه ها، یک لیست جدید ایجاد کنید که در آن هر کلید var در محدوده r شرایط شرط را برآورده کند.

نقشه

r.map(var، expr)

یک لیست جدید ایجاد کنید که در آن هر var در محدوده r توسط expr تبدیل شود.

r.map(var، cond، expr)

مانند نقشه دو آرگ اما با یک فیلتر شرطی قبل از تبدیل مقدار.

دارد

دارای (ab)

تست حضور b روی مقدار a: برای نقشه ها، json تعریف را تست می کند. برای پروتوها، مقدار اولیه غیر پیش‌فرض یا یک فیلد پیام تنظیم شده را آزمایش می‌کند.

زمانی که آرگومان range 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()
}

بهینه سازی کنید

وقتی پرچم بهینه‌سازی روشن می‌شود، 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)

عبارت id 2 مربوط به نتیجه عملگر in در اول است. شاخه، و عبارت id 11 مربوط به عملگر == در دوم است. در ارزیابی معمولی، عبارت پس از محاسبه 2، اتصال کوتاه می‌کرد. اگر y unint نبود، دولت دو دلیل برای شکست عبارت نشان می‌داد و نه فقط یک دلیل.

13. چه چیزی پوشیده شد؟

اگر به موتور بیان نیاز دارید، از CEL استفاده کنید. CEL برای پروژه هایی که نیاز به اجرای پیکربندی کاربر در جایی که عملکرد حیاتی است، ایده آل است.

در تمرین‌های قبلی، امیدواریم با انتقال داده‌های خود به CEL و بازگرداندن خروجی یا تصمیم راحت باشید.

ما امیدواریم که شما نسبت به نوع عملیاتی که می‌توانید انجام دهید، از تصمیم بولی گرفته تا تولید پیام‌های JSON و Protobuffer آگاهی داشته باشید.

ما امیدواریم که شما درک درستی از نحوه کار با عبارات و آنچه که آنها انجام می دهند داشته باشید. و ما راه های رایج برای گسترش آن را درک می کنیم.