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

۱. مقدمه

سی ای ال چیست؟

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

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

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

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

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

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

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

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

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

آنچه یاد خواهید گرفت

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

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

پیش‌نیازها

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

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

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

go --help

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

کاربردها

CEL یک زبان همه منظوره است و برای کاربردهای متنوعی، از مسیریابی RPCها گرفته تا تعریف سیاست‌های امنیتی، مورد استفاده قرار گرفته است. CEL قابل توسعه، مستقل از برنامه کاربردی و برای گردش‌های کاری یکبار کامپایل و ارزیابی چندگانه بهینه شده است.

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

گردآوری

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

عبارات

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

در مثال زیر، عبارت یک شیء درخواست (request object) می‌گیرد و درخواست شامل یک توکن ادعا (claims token) است. این عبارت یک مقدار بولی (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 lexer/parser از یک عبارت قابل خواندن توسط انسان به یک درخت نحوی انتزاعی تجزیه می‌شود. مرحله تجزیه، یک درخت نحوی انتزاعی مبتنی بر proto منتشر می‌کند که در آن هر گره Expr در AST حاوی یک شناسه عدد صحیح است که برای فهرست‌بندی در فراداده‌های تولید شده در طول تجزیه و بررسی استفاده می‌شود. syntax.proto تولید شده در طول تجزیه، به طور دقیق نمایش انتزاعی آنچه در فرم رشته‌ای عبارت تایپ شده است را نشان می‌دهد.

پس از تجزیه یک عبارت، می‌توان آن را با محیط بررسی کرد تا اطمینان حاصل شود که تمام شناسه‌های متغیر و تابع در عبارت به درستی تعریف شده و مورد استفاده قرار می‌گیرند. بررسی‌کننده نوع، یک checked.proto تولید می‌کند که شامل ابرداده‌های تفکیک نوع، متغیر و تابع است که می‌تواند کارایی ارزیابی را به طور چشمگیری بهبود بخشد.

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

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

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

۳. راه‌اندازی

کد این codelab در پوشه codelab از مخزن cel-go قرار دارد. راه‌حل (solution) آن نیز در پوشه codelab/solution از همان مخزن موجود است.

کپی کنید و به مخزن cd بروید:

git clone https://github.com/google/cel-go.git 
cd cel-go/codelab

کد را با استفاده از go run اجرا کنید:

go run .

شما باید خروجی زیر را ببینید:

=== Exercise 1: Hello World ===

=== Exercise 2: Variables ===

=== Exercise 3: Logical AND/OR ===

=== Exercise 4: Customization ===

=== Exercise 5: Building JSON ===

=== Exercise 6: Building Protos ===

=== Exercise 7: Macros ===

=== Exercise 8: Tuning ===

بسته‌های CEL کجا هستند؟

در ویرایشگر خود، codelab/codelab.go را باز کنید. باید تابع اصلی که اجرای تمرین‌ها را در این codelab هدایت می‌کند، و پس از آن سه بلوک از توابع کمکی را ببینید. اولین مجموعه از توابع کمکی به مراحل ارزیابی CEL کمک می‌کنند:

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

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

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

بسته

محل منبع

توضیحات

سل

سل-برو/سل

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

مرجع

سل-گو/معمول/انواع/مرجع

رابط‌های مرجع

انواع

سل-گو/معمول/انواع

مقادیر نوع زمان اجرا

۴. سلام دنیا!

طبق سنت همه زبان‌های برنامه‌نویسی، با ایجاد و ارزیابی «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() برابر با 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 .

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

=== Exercise 1: Hello World ===

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

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

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

اکثر برنامه‌های 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'
 | ^

بررسی‌کننده‌ی نوع، خطایی برای شیء درخواست ایجاد می‌کند که به راحتی قطعه کد منبعی را که خطا در آن رخ داده است، در بر می‌گیرد.

۶. متغیرها را تعریف کنید

افزودن 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)

برای مرور، ما فقط یک متغیر برای یک خطا تعریف کردیم، یک توصیفگر نوع به آن اختصاص دادیم و سپس در ارزیابی عبارت به متغیر ارجاع دادیم.

۷. عملگرهای منطقی AND/OR

یکی از ویژگی‌های منحصر به فرد CEL استفاده از عملگرهای منطقی جابجایی‌پذیر است. هر دو طرف یک شاخه شرطی می‌توانند ارزیابی را حتی در مواجهه با خطاها یا ورودی جزئی، قطع کنند.

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

ما یک مثال AND/OR اضافه خواهیم کرد و سپس آن را با ورودی‌های مختلف امتحان خواهیم کرد تا بفهمیم که چگونه CEL ارزیابی را اتصال کوتاه می‌کند.

تابع را ایجاد کنید

در ویرایشگر خود، محتوای زیر را به تمرین ۳ اضافه کنید:

// 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 باشد یا شناسه ایمیل خاصی داشته باشد، مقدار true را برمی‌گرداند:

// 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 به صورت پیش‌فرض روی error تنظیم می‌شود.

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

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

در این تمرین، بررسی خواهیم کرد که چگونه یک تابع را برای بسته‌بندی بررسی‌های متداول، در معرض نمایش قرار دهیم.

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

ابتدا، کدی را برای تنظیم یک override به نام contains ایجاد کنید که مشخص می‌کند آیا یک کلید در یک map وجود دارد و دارای مقدار خاصی است یا خیر. برای تعریف تابع و اتصال تابع، placeholders را در نظر بگیرید:

// 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 را به لیست اعلان‌هایی که در حال حاضر متغیر request را اعلان می‌کنند، اضافه کنیم.

با اضافه کردن ۳ خط زیر، یک نوع پارامتری تعریف کنید. (این کار به پیچیدگی هر نوع سربارگذاری تابع برای 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

پیاده‌سازی تابع را با استفاده از تابع 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)

وقتی ادعا وجود داشته باشد چه اتفاقی می‌افتد؟

برای اعتبار بیشتر، سعی کنید admin claim را روی ورودی تنظیم کنید تا مطمئن شوید که overload حاوی در صورت وجود Claim مقدار 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() یک محافظ نوع در زمان اجرا اضافه می‌کند تا اطمینان حاصل شود که قرارداد زمان اجرا با اعلان بررسی‌شده نوع در محیط مطابقت دارد.

۹. ساخت 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"
  }

۱۰. ساخت پروتوها

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>

برای نام‌های مطلق، مرجع متغیر، نوع یا تابع را با یک نقطه (dot) در ابتدای آن قرار دهید. در مثال، عبارت .<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() روی پیام، نوع را از حالت فشرده خارج کنید تا ببینید نتیجه چگونه تغییر می‌کند.

۱۱. ماکروها

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

ماکروها می‌توانند برای پیاده‌سازی منطق پیچیده در AST که نمی‌توان مستقیماً در CEL نوشت، استفاده شوند. برای مثال، ماکروی has امکان آزمایش حضور فیلد را فراهم می‌کند. ماکروهای درک مطلب مانند exists و همگی یک فراخوانی تابع را با تکرار محدود روی یک لیست ورودی یا نقشه جایگزین می‌کنند. هیچ یک از این مفاهیم در سطح نحوی امکان‌پذیر نیستند، اما از طریق بسط‌های ماکرو امکان‌پذیر هستند.

تمرین بعدی را اضافه و اجرا کنید:

// 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(متغیر، شرایط)

بررسی کنید که آیا مقدار cond برای همه متغیرهای موجود در محدوده r درست است یا خیر.

وجود دارد

r.exists(var, cond)

بررسی کنید که آیا مقدار cond برای هر متغیری در محدوده r درست است یا خیر.

موجود_یک

r.exists_one(متغیر، وضعیت)

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

فیلتر

r.filter(var, cond)

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

نقشه

r.map(متغیر، توضیح)

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

r.map(var، cond، expr)

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

دارد

دارد(ab)

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

وقتی آرگومان محدوده r از نوع map باشد، var کلید map خواهد بود و برای مقادیر نوع list ، var مقدار عنصر لیست خواهد بود. ماکروهای all ، exists ، exists_one ، filter و map یک بازنویسی AST انجام می‌دهند که یک تکرار for-each را انجام می‌دهد که توسط اندازه ورودی محدود شده است.

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

۱۲. تنظیم

در حال حاضر تعداد انگشت‌شماری از ویژگی‌ها منحصر به 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)

شناسه عبارت ۲ مربوط به نتیجه عملگر in در شاخه اول است، و شناسه عبارت ۱۱ مربوط به عملگر == در شاخه دوم است. در ارزیابی عادی، عبارت پس از محاسبه ۲ اتصال کوتاه می‌شد. اگر y از نوع uint نبود، آنگاه وضعیت دو دلیل برای عدم موفقیت عبارت نشان می‌داد و نه فقط یک دلیل.

۱۳. چه چیزهایی پوشش داده شد؟

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

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

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

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