درس تطبيقي حول الترميز 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
  • مرحبًا بالعالم: استخدام CEL لتقييم سلسلة
  • إنشاء المتغيرات
  • فهم مسار قصر CEL في العمليات المنطقية و/أو
  • كيفية استخدام CEL لإنشاء JSON
  • كيفية استخدام تقنية CEL لإنشاء مخازن مؤقتة أوّلية
  • إنشاء وحدات ماكرو
  • طرق ضبط تعبيرات CEL

المتطلبات

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

يعتمد هذا الدرس التطبيقي حول الترميز على فهم أساسيّات الترميزَين Protocol Buffers وGo Lang.

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

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

go --help

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

التطبيقات

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

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

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

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

التعبيرات

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

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

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

البيئة

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

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

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

هناك ثلاث مراحل في معالجة التعبير: التحليل والتحقّق والتقييم. النمط الأكثر شيوعًا لـ CEL هو مستوى التحكم لتحليل التعبيرات والتحقق منها في وقت التهيئة وتخزين AST.

c71fc08068759f81.png

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

49ab7d8517143b66.png

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

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

يحتاج مقيّم CEL إلى 3 أشياء:

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

يجب أن تتطابق الدالة وعمليات ربط المتغير مع ما تم استخدامه لتجميع AST. يمكن إعادة استخدام أي من هذه المدخلات عبر تقييمات متعددة، كأن يتم تقييم 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

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

المرجع

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

إضافة الدالة

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

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

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

إعادة تنفيذ الإجراء وفهم الخطأ

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

go run .

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

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

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

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

إضافة EnvOptions

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

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

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

إعادة تنفيذ الإجراء وفهم الخطأ

تشغيل البرنامج مرة أخرى:

go run .

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

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

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

استخدم 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. المنطقية 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 هذه التي ستُرجع القيمة true إذا كان المستخدم عضوًا في مجموعة admin أو لديه معرّف بريد إلكتروني معيّن:

// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper 
// user, and one that specifies an unexpected user.
func exercise3() {
  fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )

  ast := compile(env,
    `request.auth.claims.group == 'admin'
        || request.auth.principal == 'user:me@acme.co'`,
    cel.BoolType)
  program, _ := env.Program(ast)
}

أخيرًا، أضِف حالة eval التي تقيّم المستخدم من خلال مجموعة مطالبات فارغة:

// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
func exercise3() {
  fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )

  ast := compile(env,
    `request.auth.claims.group == 'admin'
        || request.auth.principal == 'user:me@acme.co'`,
    cel.BoolType)
  program, _ := env.Program(ast)

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

تشغيل الرمز مع ترك مجموعة المطالبات فارغة

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

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

request.auth.claims.group == 'admin'
    || request.auth.principal == 'user:me@acme.co'

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

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

تعديل حالة التقييم

بعد ذلك، قم بتحديث حالة التقييم لتمريرها على أساس أساسي باستخدام مجموعة مطالبات فارغة:

// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
func exercise3() {
  fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
  env, _ := cel.NewEnv(
    cel.Types(&rpcpb.AttributeContext_Request{}),
    cel.Variable("request",
      cel.ObjectType("google.rpc.context.AttributeContext.Request"),
    ),
  )

  ast := compile(env,
    `request.auth.claims.group == 'admin'
        || request.auth.principal == 'user:me@acme.co'`,
    cel.BoolType)
  program, _ := env.Program(ast)

  emptyClaims := map[string]string{}
  eval(program, request(auth("other:me@acme.co", emptyClaims), time.Now()))
}

تشغيل الرمز مع إدخال وقت

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

go run .

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

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

request.auth.claims.group == 'admin'
    || request.auth.principal == 'user:me@acme.co'

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

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

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

------ result ------
error: no such key: group

في النموذج الأولي، نعرف الحقول والأنواع التي يمكن توقعها. في قيمتَي map و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()
}

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

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

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

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

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

=== 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 أو التي يتوفّر لها ربط بتنسيق 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 إنشاء رسائل أوّلية لأي نوع من الرسائل يتم تجميعه في التطبيق. إضافة الدالة لإنشاء google.rpc.context.AttributeContext.Request من مدخل jwt

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 ...
}

من المفترض أن تحصل على الناتج التالي:

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

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

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

=== Exercise 6: Building Protos ===

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

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

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

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

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

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

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

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

أضف التمرين التالي وقم بتشغيله:

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

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

    fmt.Println()
}

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

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

... وغيرها الكثير ...

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

=== Exercise 7: Macros ===

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

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

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

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

وحدة الماكرو

التوقيع

الوصف

الكل

r.all(var, cond)

يمكنك اختبار ما إذا كانت دالة cond قيمتها صحيحة لـ all var في range r.

موجود

r.exists(var, cond)

يمكنك اختبار ما إذا كانت cond تقيّم true لـ أي var في range r.

exists_one

r.exists_one(var, cond)

يمكنك اختبار ما إذا كانت cond تقيّم "true" لخيار واحد فقط في النطاق r.

تصفية

r.filter(var, cond)

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

خريطة

r.map(var, expr)

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

r.map(var, cond, expr)

مماثلة للخريطة المكونة من وسيطتين ولكن مع فلتر شرط شرطي قبل تحويل القيمة.

تحتوي على

has(a.b)

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

عندما تكون الوسيطة range r من النوع map، سيكون var هو مفتاح الخريطة، وبالنسبة إلى قيم النوع list، سيكون var هو قيمة عنصر القائمة. تُجري وحدات ماكرو all وexists وexists_one وfilter وmap عملية إعادة كتابة AST لتنفيذ تكرار لكل تكرار مقيَّد بحجم الإدخال.

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

12. التناغم التلقائي

تتوفّر حاليًا بعض الميزات الحصرية في CEL-Go، ولكنّها تدل على الخطط المستقبلية لعمليات تنفيذ CEL الأخرى. يعرض التمرين التالي خطط برامج مختلفة لنفس مؤشر AST:

// exercise8 covers features of CEL-Go which can be used to improve
// performance and debug evaluation behavior.
//
// Turn on the optimization, exhaustive eval, and state tracking
// ProgramOption flags to see the impact on evaluation behavior.
func exercise8() {
    fmt.Println("=== Exercise 8: Tuning ===\n")
    // Declare the x and 'y' variables as input into the expression.
    env, _ := cel.NewEnv(
        cel.Variable("x", cel.IntType),
        cel.Variable("y", cel.UintType),
    )
    ast := compile(env,
        `x in [1, 2, 3, 4, 5] && type(y) == uint`,
        cel.BoolType)

    // Try the different cel.EvalOptions flags when evaluating this AST for
    // the following use cases:
    // - cel.OptOptimize: optimize the expression performance.
    // - cel.OptExhaustiveEval: turn off short-circuiting.
    // - cel.OptTrackState: track state and compute a residual using the
    //   interpreter.PruneAst function.
    program, _ := env.Program(ast)
    eval(program, cel.NoVars())

    fmt.Println()
}

أدوات تحسين الأداء من 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)

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

كمية مكثفة

يمكن أن يكون فلتر 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 مفيدة، حينئذٍ أظهرت الحالة سببين لفشل التعبير وليس سببًا واحدًا فقط.

13. ما الذي تم تناوله؟

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

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

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

نأمل أن يكون لديك فكرة عن كيفية التعامل مع التعبيرات وما تفعله. ونفهم أيضًا الطرق الشائعة لتوسيع نطاق وصولها.