درس تطبيقي حول الترميز CEL-Go: تعبيرات سريعة وآمنة ومضمّنة

1. مقدمة

ما هي CEL؟

‫CEL هي لغة تعبير غير كاملة من الناحية الحسابية، وهي مصمَّمة لتكون سريعة وقابلة للنقل وآمنة التنفيذ. يمكن استخدام CEL بشكل مستقل أو تضمينها في منتج أكبر.

تم تصميم CEL كلغة آمنة لتنفيذ رمز المستخدم. على الرغم من أنّ استدعاء eval() بشكل أعمى في رمز Python الخاص بالمستخدم أمر خطير، يمكنك تنفيذ رمز CEL الخاص بالمستخدم بأمان. وبما أنّ CEL تمنع السلوك الذي قد يؤدي إلى انخفاض الأداء، يتم تقييمها بأمان في غضون أجزاء من الثانية إلى ميكروثانية، ما يجعلها مثالية للتطبيقات التي تتطلّب أداءً عاليًا.

تقيّم لغة التعبير المشتركة (CEL) التعبيرات المشابهة للدوال ذات السطر الواحد أو تعبيرات lambda. على الرغم من أنّ CEL تُستخدَم عادةً لاتّخاذ قرارات منطقية، يمكن استخدامها أيضًا لإنشاء عناصر أكثر تعقيدًا، مثل رسائل JSON أو protobuf.

هل CEL مناسبة لمشروعك؟

بما أنّ CEL يقيّم تعبيرًا من شجرة بناء الجملة المجردة (AST) بالنانو ثانية إلى الميكرو ثانية، فإنّ حالات الاستخدام المثالية لـ CEL هي التطبيقات التي تتضمّن مسارات حساسة للأداء. يجب عدم تجميع رمز CEL في شجرة بنية مجردة (AST) في المسارات الحرجة، والتطبيقات المثالية هي تلك التي يتم فيها تنفيذ الإعداد بشكل متكرر وتعديلها بشكل غير متكرر نسبيًا.

على سبيل المثال، يُعدّ تنفيذ سياسة أمان مع كل طلب HTTP إلى إحدى الخدمات حالة استخدام مثالية للغة CEL لأنّ سياسة الأمان تتغيّر نادرًا، ولن يكون للغة CEL تأثير ضئيل على وقت الاستجابة. في هذه الحالة، تعرض لغة CEL قيمة منطقية، أي ما إذا كان يجب السماح بالطلب أم لا، ولكن يمكن أن تعرض رسالة أكثر تعقيدًا.

المواضيع التي يغطيها هذا الدرس التطبيقي حول الترميز

تتناول الخطوة الأولى من هذا الدرس التطبيقي حول الترميز الدافع وراء استخدام CEL والمفاهيم الأساسية. أما الباقي، فهو مخصّص للتدريبات على الترميز التي تغطي حالات الاستخدام الشائعة. للحصول على نظرة أكثر تفصيلاً على اللغة والدلالات والميزات، يُرجى الاطّلاع على تعريف لغة CEL على GitHub ومستندات CEL Go.

هذا الدرس التطبيقي حول الترميز مخصّص للمطوّرين الذين يريدون تعلُّم لغة CEL من أجل استخدام الخدمات التي تتوافق معها. لا يوضّح هذا الدرس العملي كيفية دمج CEL في مشروعك.

ما ستتعلمه

  • المفاهيم الأساسية من CEL
  • Hello, World: استخدام CEL لتقييم سلسلة
  • إنشاء متغيرات
  • فهم الاختصار في عمليات AND/OR المنطقية في CEL
  • كيفية استخدام CEL لإنشاء JSON
  • كيفية استخدام CEL لإنشاء Protobuffers
  • إنشاء وحدات ماكرو
  • طُرق تحسين تعبيرات CEL

المتطلبات

المتطلبات الأساسية

يعتمد هذا الدرس التطبيقي حول الترميز على فهم أساسي لمخازن البروتوكولات المؤقتة ولغة Go.

إذا لم تكن على دراية بـ Protocol Buffers، سيمنحك التمرين الأول فكرة عن طريقة عمل CEL، ولكن بما أنّ الأمثلة الأكثر تقدّمًا تستخدم Protocol Buffers كمدخل إلى CEL، قد يكون من الصعب فهمها. ننصحك بالاطّلاع على أحد هذه البرامج التعليمية أولاً. يُرجى العِلم أنّ استخدام Protocol Buffers ليس شرطًا لاستخدام CEL، ولكن يتم استخدامها على نطاق واسع في هذا الدرس العملي.

يمكنك اختبار تثبيت Go من خلال تنفيذ الأمر التالي:

go --help

2. المفاهيم الرئيسية

التطبيقات

إنّ CEL هي لغة للأغراض العامة، وقد تم استخدامها في تطبيقات متنوعة، بدءًا من توجيه طلبات RPC، وصولاً إلى تحديد سياسة الأمان. يمكن توسيع CEL، وهي لا تعتمد على التطبيق، وتم تحسينها لتناسب مسارات العمل التي يتم فيها التجميع مرة واحدة والتقييم عدة مرات.

تُقيّم العديد من الخدمات والتطبيقات الإعدادات التعريفية. على سبيل المثال، التحكّم في الوصول المستند إلى الدور (RBAC) هو إعداد تعريفي ينتج عنه قرار بشأن الوصول استنادًا إلى دور ومجموعة من المستخدمين. إذا كانت الإعدادات التعريفية تمثّل% 80 من حالات الاستخدام، ستكون لغة التعبير الشائعة (CEL) أداة مفيدة في إكمال النسبة المتبقية البالغة% 20 عندما يحتاج المستخدمون إلى المزيد من القدرة التعبيرية.

موسيقى مجمّعة

يتم تجميع تعبير مقابل بيئة. تنتج خطوة التجميع شجرة بناء الجملة المجردة (AST) بتنسيق protobuf. يتم عادةً تخزين التعبيرات المجمّعة لاستخدامها في المستقبل من أجل إبقاء عملية التقييم بأسرع ما يمكن. يمكن تقييم تعبير مجمَّع واحد باستخدام العديد من المدخلات المختلفة.

التعبيرات

يحدد المستخدمون التعبيرات، بينما تحدد الخدمات والتطبيقات البيئة التي يتم تشغيلها فيها. تحدّد توقيع الدالة المدخلات، ويتم كتابته خارج تعبير CEL. يتم استيراد مكتبة الدوال المتاحة للغة CEL تلقائيًا.

في المثال التالي، يتلقّى التعبير عنصر طلب، ويتضمّن الطلب رمزًا مميزًا للمطالبات. تعرض الدالة قيمة منطقية تشير إلى ما إذا كان الرمز المميز للمطالبات لا يزال صالحًا.

// Check whether a JSON Web Token has expired by inspecting the 'exp' claim.
//
// Args:
//   claims - authentication claims.
//   now    - timestamp indicating the current system time.
// Returns: true if the token has expired.
//
timestamp(claims["exp"]) < now

البيئة

يتم تحديد البيئات حسب الخدمات. تحدّد الخدمات والتطبيقات التي تتضمّن CEL بيئة التعبير. البيئة هي مجموعة المتغيرات والدوال التي يمكن استخدامها في التعبيرات.

يستخدم مدقق الأنواع في CEL الإعدادات المستندة إلى البروتوكول لضمان تعريف جميع مراجع المعرّفات والدوال في التعبير واستخدامها بشكل صحيح.

المراحل الثلاث لتحليل تعبير

هناك ثلاث مراحل لمعالجة تعبير: التحليل والتحقّق والتقييم. إنّ النمط الأكثر شيوعًا للغة CEL هو أن تحلّل لوحة التحكّم العبارات وتتحقّق منها في وقت الإعداد، ثم تخزّن شجرة البنية المجردة.

c71fc08068759f81.png

أثناء وقت التشغيل، تستردّ "مستوى البيانات" شجرة بناء الجملة المجردة وتقيّمها بشكل متكرّر. تم تحسين CEL لتحقيق كفاءة وقت التشغيل، ولكن يجب عدم إجراء التحليل والتحقّق في مسارات الرموز البرمجية التي تتطلّب وقت استجابة سريعًا.

49ab7d8517143b66.png

يتم تحليل CEL من تعبير قابل للقراءة إلى شجرة بنية مجردة باستخدام قواعد تحليل ANTLR. وتنتج مرحلة التحليل شجرة بنية مجردة مستندة إلى بروتوكول، حيث تحتوي كل عقدة Expr في شجرة البنية المجردة على معرّف عدد صحيح يُستخدم للفهرسة في البيانات الوصفية التي يتم إنشاؤها أثناء التحليل والتحقّق. يمثّل ملف syntax.proto الذي تم إنشاؤه أثناء التحليل التمثيلي المجرد لما تم كتابته في شكل السلسلة من التعبير.

بعد تحليل عبارة، يمكن التحقّق منها في البيئة للتأكّد من أنّه تم تعريف جميع معرّفات المتغيرات والدوال في العبارة ويتم استخدامها بشكل صحيح. تنتج أداة التحقّق من النوع الملف checked.proto الذي يتضمّن بيانات وصفية لحلّ النوع والمتغيّر والدالة، ما يمكن أن يحسّن بشكل كبير من كفاءة التقييم.

يحتاج مقيّم CEL إلى 3 عناصر:

  • عمليات ربط الدوال لأي إضافات مخصّصة
  • عمليات الربط بين المتغيرات
  • شجرة بنية مجردة (AST) لتقييمها

يجب أن تتطابق روابط الدالة والمتغير مع ما تم استخدامه لتجميع شجرة بناء الجملة المجردة. يمكن إعادة استخدام أيّ من هذه المدخلات في عمليات تقييم متعدّدة، مثل تقييم شجرة بناء الجملة المجردة (AST) في العديد من مجموعات ربط المتغيرات، أو استخدام المتغيرات نفسها مع العديد من أشجار بناء الجملة المجردة، أو استخدام عمليات ربط الدوال على مدار عمر العملية (وهي حالة شائعة).

3- إعداد

يتوفّر الرمز البرمجي لهذا الدرس التطبيقي حول الترميز في 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

cel-go/cel

واجهات المستوى الأعلى

ref

cel-go/common/types/ref

واجهات مرجعية

الأنواع

cel-go/common/types

قيم نوع بيئة التشغيل

4. مرحبًا بالجميع

على غرار جميع لغات البرمجة، سنبدأ بإنشاء عبارة "Hello World!" وتقييمها.

ضبط البيئة

في المحرّر، ابحث عن تعريف exercise1، واملأ ما يلي لإعداد البيئة:

// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
    fmt.Println("=== Exercise 1: Hello World ===\n")
    // Create the standard environment.
    env, err := cel.NewEnv()
    if err != nil {
        glog.Exitf("env error: %v", err)
    }
    // Will add the parse and check steps here
}

تقيّم تطبيقات CEL تعبيرًا مقابل بيئة. تضبط env, err := cel.NewEnv() البيئة العادية.

يمكن تخصيص البيئة من خلال توفير الخيارات 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 أو نوعًا معروفًا من Protocol Buffers أو أي نوع رسالة من Protocol Buffers طالما تم توفير واصفه أيضًا إلى CEL.

إضافة الدالة

في المحرّر، ابحث عن تعريف exercise2 وأضِف ما يلي:

// exercise2 shows how to declare and use variables in expressions.
//
// Given a request of type google.rpc.context.AttributeContext.Request
// determine whether a specific auth claim is set.
func exercise2() {
  fmt.Println("=== Exercise 2: Variables ===\n")
   env, err := cel.NewEnv(
    // Add cel.EnvOptions values here.
  )
  if err != nil {
    glog.Exit(err)
  }
  ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
  program, _ := env.Program(ast)

  // Evaluate a request object that sets the proper group claim.
  claims := map[string]string{"group": "admin"}
  eval(program, request(auth("user:me@acme.co", claims), time.Now()))
  fmt.Println()
}

إعادة التشغيل وفهم الخطأ

أعِد تشغيل البرنامج باتّباع الخطوات التالية:

go run .

من المفترض أن يظهر لك الناتج التالي:

ERROR: <input>:1:1: undeclared reference to 'request' (in container '')
 | request.auth.claims.group == 'admin'
 | ^

ينتج مدقق الأنواع خطأً في كائن الطلب يتضمّن بشكل ملائم مقتطف المصدر الذي يحدث فيه الخطأ.

6. تعريف المتغيّرات

إضافة EnvOptions

في المحرّر، لنصلح الخطأ الناتج من خلال تقديم تعريف لكائن الطلب كرسالة من النوع google.rpc.context.AttributeContext.Request على النحو التالي:

// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
  fmt.Println("=== Exercise 2: Variables ===\n")
  env, err := cel.NewEnv(
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )
  if err != nil {
    glog.Exit(err)
  }
  ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
  program, _ := env.Program(ast)

  // Evaluate a request object that sets the proper group claim.
  claims := map[string]string{"group": "admin"}
  eval(program, request(auth("user:me@acme.co", claims), time.Now()))
  fmt.Println()
}

إعادة التشغيل وفهم الخطأ

إعادة تشغيل البرنامج:

go run .

من المفترض أن يظهر لك الخطأ التالي:

ERROR: <input>:1:8: [internal] unexpected failed resolution of 'google.rpc.context.AttributeContext.Request'
 | request.auth.claims.group == 'admin'
 | .......^

لاستخدام متغيرات تشير إلى رسائل protobuf، يجب أن يعرف مدقق الأنواع أيضًا واصف النوع.

استخدِم cel.Types لتحديد واصف الطلب في الدالة:

// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
  fmt.Println("=== Exercise 2: Variables ===\n")
   env, err := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}), 
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )

  if err != nil {
    glog.Exit(err)
  }
  ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
  program, _ := env.Program(ast)

  // Evaluate a request object that sets the proper group claim.
  claims := map[string]string{"group": "admin"}
  eval(program, request(auth("user:me@acme.co", claims), time.Now()))
  fmt.Println()
}

تمت إعادة التشغيل بنجاح.

أعِد تشغيل البرنامج:

go run .

من المفترض أن يظهر لك ما يلي:

=== Exercise 2: Variables ===

request.auth.claims.group == 'admin'

------ input ------
request = time: <
  seconds: 1569255569
>
auth: <
  principal: "user:me@acme.co"
  claims: <
    fields: <
      key: "group"
      value: <
        string_value: "admin"
      >
    >
  >
>

------ result ------
value: true (types.Bool)

للمراجعة، لقد عرّفنا للتو متغيّرًا للخطأ، وخصصنا له واصف نوع، ثم أشرنا إلى المتغيّر في تقييم التعبير.

7. Logical AND/OR

من الميزات الفريدة في 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، نعرف الحقول والأنواع التي نتوقّعها. في قيم الخرائط وJSON، لا نعرف ما إذا كان سيتم عرض مفتاح. بما أنّه لا توجد قيمة تلقائية آمنة لمفتاح غير متوفّر، يتم ضبط CEL تلقائيًا على الخطأ.

8. الدوال المخصّصة

على الرغم من أنّ CEL تتضمّن العديد من الدوال المضمّنة، إلا أنّ هناك حالات يكون فيها استخدام دالة مخصّصة مفيدًا. على سبيل المثال، يمكن استخدام الدوال المخصّصة لتحسين تجربة المستخدم في الحالات الشائعة أو عرض الحالة الحساسة للسياق.

في هذا التمرين، سنتعرّف على كيفية عرض دالة لتجميع عمليات التحقّق الشائعة الاستخدام.

استدعاء دالة مخصّصة

أولاً، أنشئ الرمز البرمجي لإعداد عملية إلغاء تُسمى contains تحدّد ما إذا كان المفتاح متوفّرًا في الخريطة ولديه قيمة معيّنة. اترك عناصر نائبة لتعريف الدالة وربطها:

// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
  fmt.Println("=== Exercise 4: Customization ===\n")
  // Determine whether an optional claim is set to the proper value. The
  // custom map.contains(key, value) function is used as an alternative to:
  //   key in map && map[key] == value
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    // Declare the request.
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
    // Add the custom function declaration and binding here with cel.Function()
  )
  ast := compile(env,
    `request.auth.claims.contains('group', 'admin')`,
    cel.BoolType)

  // Construct the program plan and provide the 'contains' function impl.
  // Output: false
  program, _ := env.Program(ast)
  emptyClaims := map[string]string{}
  eval(
    program,
    request(auth("user:me@acme.co", emptyClaims), time.Now()),
  )
  fmt.Println()
}  

تشغيل الرمز البرمجي وفهم الخطأ

عند إعادة تشغيل الرمز، من المفترض أن يظهر لك الخطأ التالي:

ERROR: <input>:1:29: found no matching overload for 'contains' applied to 'map(string, dyn).(string, string)'
 | request.auth.claims.contains('group', 'admin')
 | ............................^

لإصلاح الخطأ، علينا إضافة الدالة contains إلى قائمة التعريفات التي تحدّد حاليًا متغيّر الطلب.

حدِّد نوعًا يتضمّن مَعلمات عن طريق إضافة الأسطر الثلاثة التالية. (هذا هو الحد الأقصى لتعقيد أي عملية تحميل زائد للدالة في CEL)

// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
  fmt.Println("=== Exercise 4: Customization ===\n")
  // Determine whether an optional claim is set to the proper value. The custom
  // map.contains(key, value) function is used as an alternative to:
  //   key in map && map[key] == value

  // Useful components of the type-signature for 'contains'.
  typeParamA := cel.TypeParamType("A")
  typeParamB := cel.TypeParamType("B")
  mapAB := cel.MapType(typeParamA, typeParamB)

  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    // Declare the request.
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
    // Add the custom function declaration and binding here with cel.Function()
  )
  ast := compile(env,
    `request.auth.claims.contains('group', 'admin')`,
    cel.BoolType)

  // Construct the program plan.
  // Output: false
  program, _ := env.Program(ast)
  emptyClaims := map[string]string{}
  eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
  fmt.Println()
}

إضافة الدالة المخصّصة

بعد ذلك، سنضيف دالة contains جديدة ستستخدم الأنواع المحدّدة المَعلمات:

// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
  fmt.Println("=== Exercise 4: Customization ===\n")
  // Determine whether an optional claim is set to the proper value. The custom
  // map.contains(key, value) function is used as an alternative to:
  //   key in map && map[key] == value

  // Useful components of the type-signature for 'contains'.
  typeParamA := cel.TypeParamType("A")
  typeParamB := cel.TypeParamType("B")
  mapAB := cel.MapType(typeParamA, typeParamB)
 
  // Env declaration.
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    // Declare the request.
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),   
    // Declare the custom contains function and its implementation.
    cel.Function("contains",
      cel.MemberOverload(
        "map_contains_key_value",
        []*cel.Type{mapAB, typeParamA, typeParamB},
        cel.BoolType,
        // Provide the implementation using cel.FunctionBinding()
      ),
    ),
  )
  ast := compile(env,
    `request.auth.claims.contains('group', 'admin')`,
    cel.BoolType)

  // Construct the program plan and provide the 'contains' function impl.
  // Output: false
  program, _ := env.Program(ast)
  emptyClaims := map[string]string{}
  eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
  fmt.Println()
}

تشغيل البرنامج لفهم الخطأ

نفِّذ التمرين. من المفترض أن يظهر لك الخطأ التالي بشأن الدالة المفقودة في وقت التشغيل:

------ result ------
error: no such overload: contains

قدِّم تنفيذ الدالة إلى تعريف NewEnv باستخدام الدالة cel.FunctionBinding():

// exercise4 demonstrates how to extend CEL with custom functions.
//
// Declare a contains member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
  fmt.Println("=== Exercise 4: Customization ===\n")
  // Determine whether an optional claim is set to the proper value. The custom
  // map.contains(key, value) function is used as an alternative to:
  //   key in map && map[key] == value

  // Useful components of the type-signature for 'contains'.
  typeParamA := cel.TypeParamType("A")
  typeParamB := cel.TypeParamType("B")
  mapAB := cel.MapType(typeParamA, typeParamB)

  // Env declaration.
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    // Declare the request.
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),   
    // Declare the custom contains function and its implementation.
    cel.Function("contains",
      cel.MemberOverload(
        "map_contains_key_value",
        []*cel.Type{mapAB, typeParamA, typeParamB},
        cel.BoolType,
        cel.FunctionBinding(mapContainsKeyValue)),
    ),
  )
  ast := compile(env, 
    `request.auth.claims.contains('group', 'admin')`, 
    cel.BoolType)

  // Construct the program plan.
  // Output: false
  program, err := env.Program(ast)
  if err != nil {
    glog.Exit(err)
  }

  eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
  claims := map[string]string{"group": "admin"}
  eval(program, request(auth("user:me@acme.co", claims), time.Now()))
  fmt.Println()
}

من المفترض أن يتم تشغيل البرنامج الآن بنجاح:

=== Exercise 4: Custom Functions ===

request.auth.claims.contains('group', 'admin')

------ input ------
request = time: <
  seconds: 1569302377
>
auth: <
  principal: "user:me@acme.co"
  claims: <
  >
>

------ result ------
value: false (types.Bool)

ماذا يحدث عندما تكون المطالبة قائمة؟

للحصول على نقاط إضافية، جرِّب ضبط مطالبة المشرف على الإدخال للتحقّق من أنّ عملية التحميل الزائد contains تعرض أيضًا القيمة true عندما تكون المطالبة متوفّرة. من المفترض أن يظهر لك الناتج التالي:

=== Exercise 4: Customization ===

request.auth.claims.contains('group','admin')

------ input ------
request = time: <
  seconds: 1588991010
>
auth: <
  principal: "user:me@acme.co"
  claims: <
  >
>

------ result ------
value: false (types.Bool)

------ input ------
request = time: <
  seconds: 1588991010
>
auth: <
  principal: "user:me@acme.co"
  claims: <
    fields: <
      key: "group"
      value: <
        string_value: "admin"
      >
    >
  >
>

------ result ------
value: true (types.Bool)

قبل المتابعة، من المفيد فحص الدالة mapContainsKeyValue نفسها:

// mapContainsKeyValue implements the custom function:
//   map.contains(key, value) -> bool.
func mapContainsKeyValue(args ...ref.Val) ref.Val {
  // The declaration of the function ensures that only arguments which match
  // the mapContainsKey signature will be provided to the function.
  m := args[0].(traits.Mapper)

  // CEL has many interfaces for dealing with different type abstractions.
  // The traits.Mapper interface unifies field presence testing on proto
  // messages and maps.
  key := args[1]
  v, found := m.Find(key)

  // If not found and the value was non-nil, the value is an error per the
  // `Find` contract. Propagate it accordingly. Such an error might occur with
  // a map whose key-type is listed as 'dyn'.
  if !found {
    if v != nil {
      return types.ValOrErr(v, "unsupported key type")
    }
    // Return CEL False if the key was not found.
    return types.False
  }
  // Otherwise whether the value at the key equals the value provided.
  return v.Equal(args[2])
}

لتوفير أكبر قدر من سهولة التوسيع، تتوقّع توقيع الدوال المخصّصة وسيطات من النوع ref.Val. الموازنة هنا هي أنّ سهولة التوسيع تفرض عبئًا على المنفِّذ لضمان التعامل مع جميع أنواع القيم بشكل صحيح. عندما لا تتطابق أنواع وسيطات الإدخال أو عددها مع تعريف الدالة، يجب عرض الخطأ no such overload.

يضيف cel.FunctionBinding() حارس نوع وقت التشغيل للتأكّد من أنّ عقد وقت التشغيل يطابق تعريف النوع الذي تم التحقّق منه في البيئة.

9- إنشاء JSON

يمكن أن تنتج لغة CEL أيضًا نواتج غير منطقية، مثل JSON. أضِف ما يلي إلى الدالة:

// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
    fmt.Println("=== Exercise 5: Building JSON ===\n")
    env, _ := cel.NewEnv(
      // Declare the 'now' variable as a Timestamp.
      // cel.Variable("now", cel.TimestampType),
    )
    // Note the quoted keys in the CEL map literal. For proto messages the
    // field names are unquoted as they represent well-defined identifiers.
    ast := compile(env, `
        {'sub': 'serviceAccount:delegate@acme.co',
         'aud': 'my-project',
         'iss': 'auth.acme.com:12350',
         'iat': now,
         'nbf': now,
         'exp': now + duration('300s'),
         'extra_claims': {
             'group': 'admin'
         }}`,
        cel.MapType(cel.StringType, cel.DynType))

    program, _ := env.Program(ast)
    out, _, _ := eval(
        program,
        map[string]interface{}{
            "now": &tpb.Timestamp{Seconds: time.Now().Unix()},
        },
    )
    fmt.Printf("------ type conversion ------\n%v\n", out)
    fmt.Println()
}

تشغيل الرمز البرمجي

عند إعادة تشغيل الرمز، من المفترض أن يظهر لك الخطأ التالي:

ERROR: <input>:5:11: undeclared reference to 'now' (in container '')
 |    'iat': now,
 | ..........^
... and more ...

أضِف بيانًا للمتغير now من النوع cel.TimestampType إلى cel.NewEnv() وأعِد التشغيل:

// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
    fmt.Println("=== Exercise 5: Building JSON ===\n")
    env, _ := cel.NewEnv(
      cel.Variable("now", cel.TimestampType),
    )
    // Note the quoted keys in the CEL map literal. For proto messages the
    // field names are unquoted as they represent well-defined identifiers.
    ast := compile(env, `
        {'sub': 'serviceAccount:delegate@acme.co',
         'aud': 'my-project',
         'iss': 'auth.acme.com:12350',
         'iat': now,
         'nbf': now,
         'exp': now + duration('300s'),
         'extra_claims': {
             'group': 'admin'
         }}`,
        cel.MapType(cel.StringType, cel.DynType))

     // Hint:
     // Convert `out` to JSON using the valueToJSON() helper method.
     // The valueToJSON() function calls ConvertToNative(&structpb.Value{})
     // to adapt the CEL value to a protobuf JSON representation and then
     // uses the jsonpb.Marshaler utilities on the output to render the JSON
     // string.
    program, _ := env.Program(ast)
    out, _, _ := eval(
        program,
        map[string]interface{}{
            "now": time.Now(),
        },
    )
    fmt.Printf("------ type conversion ------\n%v\n", out)
    fmt.Println()
}

أعِد تشغيل الرمز، ومن المفترض أن تنجح العملية:

=== Exercise 5: Building JSON ===

  {'aud': 'my-project',
   'exp': now + duration('300s'),
   'extra_claims': {
    'group': 'admin'
   },
   'iat': now,
   'iss': 'auth.acme.com:12350',
   'nbf': now,
   'sub': 'serviceAccount:delegate@acme.co'
   }

------ input ------
now = seconds: 1569302377

------ result ------
value: &{0xc0002eaf00 map[aud:my-project exp:{0xc0000973c0} extra_claims:0xc000097400 iat:{0xc000097300} iss:auth.acme.com:12350 nbf:{0xc000097300} sub:serviceAccount:delegate@acme.co] {0x8c8f60 0xc00040a6c0 21}} (*types.baseMap)

------ type conversion ------
&{0xc000313510 map[aud:my-project exp:{0xc000442510} extra_claims:0xc0004acdc0 iat:{0xc000442450} iss:auth.acme.com:12350 nbf:{0xc000442450} sub:serviceAccount:delegate@acme.co] {0x9d0ce0 0xc0004424b0 21}}

يتم تشغيل البرنامج، ولكن يجب تحويل قيمة الإخراج out إلى JSON بشكل صريح. في هذه الحالة، يكون التمثيل الداخلي لـ CEL قابلاً للتحويل إلى JSON لأنّه يشير فقط إلى الأنواع التي يمكن أن يتيحها JSON أو التي يتوفّر لها تطابق معروف من Proto إلى JSON.

// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
    fmt.Println("=== Exercise 5: Building JSON ===\n")
...
    fmt.Printf("------ type conversion ------\n%v\n", valueToJSON(out))
    fmt.Println()
}

بعد تحويل النوع باستخدام الدالة المساعدة valueToJSON ضِمن الملف codelab.go، من المفترض أن يظهر لك الناتج الإضافي التالي:

------ type conversion ------
  {
   "aud": "my-project",
   "exp": "2019-10-13T05:54:29Z",
   "extra_claims": {
      "group": "admin"
     },
   "iat": "2019-10-13T05:49:29Z",
   "iss": "auth.acme.com:12350",
   "nbf": "2019-10-13T05:49:29Z",
   "sub": "serviceAccount:delegate@acme.co"
  }

10. إنشاء نماذج أولية

يمكن أن تنشئ CEL رسائل protobuf لأي نوع رسالة تم تجميعها في التطبيق. أضِف الدالة لإنشاء google.rpc.context.AttributeContext.Request من jwt إدخال

// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
  fmt.Println("=== Exercise 6: Building Protos ===\n")

  // Construct an environment and indicate that the container for all references
  // within the expression is `google.rpc.context.AttributeContext`.
  requestType := &rpcpb.AttributeContext_Request{}
  env, _ := cel.NewEnv(
      // Add cel.Container() option for 'google.rpc.context.AttributeContext'
      cel.Types(requestType),
      // Add cel.Variable() option for 'jwt' as a map(string, Dyn) type
      // and for 'now' as a timestamp.
  )

  // Compile the Request message construction expression and validate that
  // the resulting expression type matches the fully qualified message name.
  //
  // Note: the field names within the proto message types are not quoted as they
  // are well-defined names composed of valid identifier characters. Also, note
  // that when building nested proto objects, the message name needs to prefix 
  // the object construction.
  ast := compile(env, `
    Request{
        auth: Auth{
            principal: jwt.iss + '/' + jwt.sub,
            audiences: [jwt.aud],
            presenter: 'azp' in jwt ? jwt.azp : "",
            claims: jwt
        },
        time: now
    }`,
    cel.ObjectType("google.rpc.context.AttributeContext.Request"),
  )
  program, _ := env.Program(ast)

  // Construct the message. The result is a ref.Val that returns a dynamic
  // proto message.
  out, _, _ := eval(
      program,
      map[string]interface{}{
          "jwt": map[string]interface{}{
              "sub": "serviceAccount:delegate@acme.co",
              "aud": "my-project",
              "iss": "auth.acme.com:12350",
              "extra_claims": map[string]string{
                  "group": "admin",
              },
          },
          "now": time.Now(),
      },
  )

  // Hint: Unwrap the CEL value to a proto. Make sure to use the
  // `ConvertToNative(reflect.TypeOf(requestType))` to convert the dynamic proto
  // message to the concrete proto message type expected.
  fmt.Printf("------ type unwrap ------\n%v\n", out)
  fmt.Println()
}

تشغيل الرمز البرمجي

عند إعادة تشغيل الرمز، من المفترض أن يظهر لك الخطأ التالي:

ERROR: <input>:2:10: undeclared reference to 'Request' (in container '')
 |   Request{
 | .........^

الحاوية هي في الأساس ما يعادل مساحة الاسم أو الحزمة، ولكن يمكن أن تكون دقيقة مثل اسم رسالة protobuf. تستخدم حاويات CEL قواعد تحليل مساحة الاسم نفسها المستخدَمة في Protobuf وC++‎ لتحديد مكان تعريف اسم متغيّر أو دالة أو نوع معيّن.

بالنظر إلى الحاوية google.rpc.context.AttributeContext، سيحاول مدقق الأنواع والمقيّم استخدام أسماء المعرّفات التالية لجميع المتغيرات والأنواع والدوال:

  • google.rpc.context.AttributeContext.<id>
  • google.rpc.context.<id>
  • google.rpc.<id>
  • google.<id>
  • <id>

بالنسبة إلى الأسماء المطلقة، أضِف نقطة في بداية مرجع المتغيّر أو النوع أو الدالة. في المثال، لن يبحث التعبير .<id> إلا عن المعرّف <id> ذي المستوى الأعلى بدون التحقّق أولاً داخل الحاوية.

جرِّب تحديد الخيار cel.Container("google.rpc.context.AttributeContext") لبيئة CEL وأعِد التشغيل:

// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
  fmt.Println("=== Exercise 6: Building Protos ===\n")
  // Construct an environment and indicate that the container for all references
  // within the expression is `google.rpc.context.AttributeContext`.
  requestType := &rpcpb.AttributeContext_Request{}
  env, _ := cel.NewEnv(
    // Add cel.Container() option for 'google.rpc.context.AttributeContext'
    cel.Container("google.rpc.context.AttributeContext.Request"),
    cel.Types(requestType),
    // Later, add cel.Variable() options for 'jwt' as a map(string, Dyn) type
    // and for 'now' as a timestamp.
    // cel.Variable("now", cel.TimestampType),
    // cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
  )

 ...
}

من المفترض أن يظهر لك الناتج التالي:

ERROR: <input>:4:16: undeclared reference to 'jwt' (in container 'google.rpc.context.AttributeContext')
 |     principal: jwt.iss + '/' + jwt.sub,
 | ...............^

... والعديد من الأخطاء الأخرى...

بعد ذلك، عرِّف المتغيّرين jwt وnow، وسيعمل البرنامج على النحو المتوقّع:

=== Exercise 6: Building Protos ===

  Request{
   auth: Auth{
    principal: jwt.iss + '/' + jwt.sub,
    audiences: [jwt.aud],
    presenter: 'azp' in jwt ? jwt.azp : "",
    claims: jwt
   },
   time: now
  }

------ input ------
jwt = {
  "aud": "my-project",
  "extra_claims": {
    "group": "admin"
  },
  "iss": "auth.acme.com:12350",
  "sub": "serviceAccount:delegate@acme.co"
}
now = seconds: 1588993027

------ result ------
value: &{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false} (*types.protoObj)

----- type unwrap ----
&{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false}

للحصول على نقاط إضافية، عليك أيضًا فك تغليف النوع عن طريق استدعاء out.Value() على الرسالة لمعرفة كيف تتغير النتيجة.

11. وحدات ماكرو

يمكن استخدام وحدات الماكرو لمعالجة برنامج CEL في وقت التحليل. تتطابق وحدات الماكرو مع توقيع استدعاء وتعدّل الاستدعاء المُدخَل ووسيطاته من أجل إنشاء شجرة بنية مجردة لتعبير فرعي جديد.

يمكن استخدام وحدات الماكرو لتنفيذ منطق معقّد في شجرة البنية المجردة لا يمكن كتابته مباشرةً في CEL. على سبيل المثال، يتيح ماكرو has اختبار توفّر الحقل. تستبدل وحدات الماكرو الخاصة بالفهم، مثل exists وall، استدعاء الدالة بالتكرار المحدود على قائمة أو خريطة إدخال. لا يمكن تحقيق أي من المفهومين على مستوى بناء الجملة، ولكن يمكن تحقيقهما من خلال عمليات توسيع وحدات الماكرو.

أضِف التمرين التالي ونفِّذه:

// exercise7 introduces macros for dealing with repeated fields and maps.
//
// Determine whether the jwt.extra_claims has at least one key that starts
// with the group prefix, and ensure that all group-like keys have list
// values containing only strings that end with '@acme.co`.
func exercise7() {
    fmt.Println("=== Exercise 7: Macros ===\n")
    env, _ := cel.NewEnv(
      cel.ClearMacros(),
      cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
    )
    ast := compile(env,
        `jwt.extra_claims.exists(c, c.startsWith('group'))
                && jwt.extra_claims
                      .filter(c, c.startsWith('group'))
                      .all(c, jwt.extra_claims[c]
                                 .all(g, g.endsWith('@acme.co')))`,
        cel.BoolType)
    program, _ := env.Program(ast)

    // Evaluate a complex-ish JWT with two groups that satisfy the criteria.
    // Output: true.
    eval(program,
        map[string]interface{}{
            "jwt": map[string]interface{}{
                "sub": "serviceAccount:delegate@acme.co",
                "aud": "my-project",
                "iss": "auth.acme.com:12350",
                "extra_claims": map[string][]string{
                    "group1": {"admin@acme.co", "analyst@acme.co"},
                    "labels": {"metadata", "prod", "pii"},
                    "groupN": {"forever@acme.co"},
                },
            },
        })

    fmt.Println()
}

من المفترض أن تظهر لك الأخطاء التالية:

ERROR: <input>:1:25: undeclared reference to 'c' (in container '')
 | jwt.extra_claims.exists(c, c.startsWith('group'))
 | ........................^

... والمزيد ...

تحدث هذه الأخطاء لأنّ وحدات الماكرو لم يتم تفعيلها بعد. لتفعيل وحدات الماكرو، أزِل cel.ClearMacros()، ثم شغِّل النص البرمجي مرة أخرى:

=== Exercise 7: Macros ===

jwt.extra_claims.exists(c, c.startsWith('group'))
    && jwt.extra_claims
       .filter(c, c.startsWith('group'))
       .all(c, jwt.extra_claims[c]
              .all(g, g.endsWith('@acme.co')))

------ input ------
jwt = {
  "aud": "my-project",
  "extra_claims": {
    "group1": [
      "admin@acme.co",
      "analyst@acme.co"
    ],
    "groupN": [
      "forever@acme.co"
    ],
    "labels": [
      "metadata",
      "prod",
      "pii"
    ]
  },
  "iss": "auth.acme.com:12350",
  "sub": "serviceAccount:delegate@acme.co"
}

------ result ------
value: true (types.Bool)

في ما يلي وحدات الماكرو المتوافقة حاليًا:

وحدة الماكرو

التوقيع

الوصف

الكل

r.all(var, cond)

اختبار ما إذا كان الشرط cond يعرض القيمة "صحيح" لكل متغير var في النطاق r

exists

r.exists(var, cond)

اختبار ما إذا كان الشرط cond يعرض القيمة "صحيح" لأي متغير في النطاق r.

exists_one

r.exists_one(var, cond)

اختبِر ما إذا كان الشرط cond يتم تقييمه على أنّه صحيح لمتغير واحد فقط في النطاق r.

تصفية

r.filter(var, cond)

بالنسبة إلى القوائم، أنشئ قائمة جديدة يستوفي فيها كل عنصر var في النطاق r الشرط cond. بالنسبة إلى الخرائط، أنشئ قائمة جديدة يستوفي فيها كل متغير مفتاح في النطاق r الشرط cond.

خريطة

r.map(var, expr)

تنشئ هذه الدالة قائمة جديدة يتم فيها تحويل كل متغيّر في النطاق r باستخدام expr.

r.map(var, cond, expr)

هي نفسها الدالة map ذات الوسيطتَين، ولكن مع فلتر شرطي cond قبل تحويل القيمة.

تحتوي على

has(a.b)

اختبار التواجد لـ b في القيمة a : للخرائط، تعريف اختبارات json. بالنسبة إلى protos، تختبر قيمة أولية غير تلقائية أو حقل رسالة أو مجموعة من حقول الرسائل.

عندما يكون وسيط النطاق 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()
}

أدوات تحسين الأداء من Google

عند تفعيل علامة التحسين، ستستغرق CEL وقتًا إضافيًا لإنشاء قائمة وتعيين القيم الحرفية مسبقًا وتحسين بعض الاستدعاءات، مثل عامل التشغيل in، ليكون اختبارًا حقيقيًا لعضوية المجموعة:

    // Turn on optimization.
    trueVars := map[string]interface{}{"x": int64(4), "y": uint64(2)}
    program, _ := env.Program(ast, cel.EvalOptions(cel.OptOptimize))
    // Try benchmarking with the optimization flag on and off.
    eval(program, trueVars)

عندما يتم تقييم البرنامج نفسه عدة مرات مقابل مدخلات مختلفة، يكون التحسين خيارًا جيدًا. ومع ذلك، عندما يتم تقييم البرنامج مرة واحدة فقط، سيؤدي التحسين إلى إضافة حمل زائد.

التقييم الشامل

يمكن أن تكون Exhaustive Eval مفيدة لتصحيح أخطاء سلوك تقييم التعبير لأنّها تقدّم إحصاءات عن القيمة المرصودة في كل خطوة من خطوات تقييم التعبير.

    // Turn on exhaustive eval to see what the evaluation state looks like.
    // The input is structure to show a false on the first branch, and true
    // on the second.
    falseVars := map[string]interface{}{"x": int64(6), "y": uint64(2)}
    program, _ = env.Program(ast, cel.EvalOptions(cel.OptExhaustiveEval))
    eval(program, falseVars)

من المفترض أن تظهر لك قائمة بحالة تقييم التعبير لكل رقم تعريف تعبير:

------ eval states ------
1: 6 (types.Int)
2: false (types.Bool)
3: &{0xc0000336b0 [1 2 3 4 5] {0x89f020 0xc000434760 151}} (*types.baseList)
4: 1 (types.Int)
5: 2 (types.Int)
6: 3 (types.Int)
7: 4 (types.Int)
8: 5 (types.Int)
9: uint (*types.Type)
10: 2 (types.Uint)
11: true (types.Bool)
12: uint (*types.Type)
13: false (types.Bool)

يتوافق معرّف التعبير 2 مع نتيجة عامل التشغيل in في الفرع الأول، ويتوافق معرّف التعبير 11 مع عامل التشغيل == في الفرع الثاني. في التقييم العادي، كان سيتم إيقاف التعبير بعد احتساب القيمة 2. إذا لم يكن y من النوع uint، كانت الحالة ستعرض سببَين لتعذُّر تنفيذ التعبير وليس سببًا واحدًا فقط.

13. ما الذي تمّت تغطيته؟

إذا كنت بحاجة إلى محرك تعبير، ننصحك باستخدام CEL. تُعدّ CEL مثالية للمشاريع التي تحتاج إلى تنفيذ إعدادات المستخدم حيث يكون الأداء بالغ الأهمية.

في التمارين السابقة، نأمل أن تكون قد أصبحت معتادًا على تمرير بياناتك إلى CEL واسترداد الناتج أو القرار.

نأمل أن تكون لديك فكرة عن أنواع العمليات التي يمكنك تنفيذها، بدءًا من اتّخاذ قرار منطقي إلى إنشاء رسائل JSON وProtobuffer.

نأمل أن تكون لديك فكرة عن كيفية التعامل مع التعبيرات وما تفعله. ونحن على دراية بالطرق الشائعة لتمديدها.