CEL-Go कोडलैब (कोड बनाना सीखना): तेज़, सुरक्षित, एम्बेड किए गए एक्सप्रेशन

1. परिचय

सीईएल क्या है?

सीईएल, एक नॉन-ट्यूरिंग कंपलीट एक्सप्रेशन लैंग्वेज है. इसे तेज़ी से, पोर्टेबल तरीके से, और सुरक्षित तरीके से लागू करने के लिए डिज़ाइन किया गया है. CEL का इस्तेमाल अकेले या किसी बड़े प्रॉडक्ट में एम्बेड करके किया जा सकता है.

सीईएल को एक ऐसी भाषा के तौर पर डिज़ाइन किया गया है जिसमें उपयोगकर्ता के कोड को सुरक्षित तरीके से चलाया जा सकता है. किसी उपयोगकर्ता के Python कोड पर बिना सोचे-समझे eval() को कॉल करना खतरनाक हो सकता है. हालांकि, किसी उपयोगकर्ता के CEL कोड को सुरक्षित तरीके से एक्ज़ीक्यूट किया जा सकता है. साथ ही, CEL ऐसी गतिविधियों को रोकता है जिनसे परफ़ॉर्मेंस कम हो सकती है. यह नैनोसेकंड से लेकर माइक्रोसेकंड तक के समय में सुरक्षित तरीके से काम करता है. यह परफ़ॉर्मेंस के लिहाज़ से ज़रूरी ऐप्लिकेशन के लिए सबसे सही है.

CEL, एक्सप्रेशन का आकलन करता है. ये सिंगल लाइन फ़ंक्शन या लैम्डा एक्सप्रेशन की तरह होते हैं. आम तौर पर, CEL का इस्तेमाल बूलियन फ़ैसलों के लिए किया जाता है. हालांकि, इसका इस्तेमाल JSON या protobuf मैसेज जैसे ज़्यादा कॉम्प्लेक्स ऑब्जेक्ट बनाने के लिए भी किया जा सकता है.

क्या CEL आपके प्रोजेक्ट के लिए सही है?

CEL, एएसटी से एक्सप्रेशन का आकलन नैनोसेकंड से लेकर माइक्रोसेकंड में करता है. इसलिए, CEL का सबसे सही इस्तेमाल उन ऐप्लिकेशन के लिए होता है जिनमें परफ़ॉर्मेंस से जुड़े पाथ बहुत ज़रूरी होते हैं. CEL कोड को एएसटी में कंपाइल करने का काम, अहम पाथ में नहीं किया जाना चाहिए. सबसे सही ऐप्लिकेशन वे होते हैं जिनमें कॉन्फ़िगरेशन को अक्सर लागू किया जाता है और उसमें बदलाव कम ही किया जाता है.

उदाहरण के लिए, किसी सेवा के लिए हर एचटीटीपी अनुरोध के साथ सुरक्षा नीति लागू करना, CEL का सबसे सही इस्तेमाल है. ऐसा इसलिए, क्योंकि सुरक्षा नीति में बहुत कम बदलाव होते हैं. साथ ही, CEL का इस्तेमाल करने से जवाब मिलने में लगने वाले समय पर कोई खास असर नहीं पड़ता. इस मामले में, CEL एक बूलियन वैल्यू दिखाता है. इससे पता चलता है कि अनुरोध को अनुमति दी जानी चाहिए या नहीं. हालांकि, यह ज़्यादा जटिल मैसेज भी दिखा सकता है.

इस कोडलैब में क्या-क्या शामिल है?

इस कोडलैब के पहले चरण में, CEL का इस्तेमाल करने की वजह और इसके मुख्य सिद्धांतों के बारे में बताया गया है. बाकी हिस्सा, कोडिंग की एक्सरसाइज़ के लिए है. इनमें इस्तेमाल के सामान्य उदाहरण शामिल हैं. भाषा, सिमैंटिक्स, और सुविधाओं के बारे में ज़्यादा जानने के लिए, GitHub पर CEL की भाषा की परिभाषा और CEL के Go दस्तावेज़ देखें.

यह कोडलैब उन डेवलपर के लिए है जो CEL के बारे में जानना चाहते हैं, ताकि वे उन सेवाओं का इस्तेमाल कर सकें जो पहले से ही CEL के साथ काम करती हैं. इस कोडलैब में, CEL को अपने प्रोजेक्ट में इंटिग्रेट करने का तरीका नहीं बताया गया है.

आपको क्या सीखने को मिलेगा

  • सीईएल के मुख्य सिद्धांत
  • नमस्ते, दुनिया के लोगों: स्ट्रिंग का आकलन करने के लिए सीईएल का इस्तेमाल करना
  • वैरिएबल बनाना
  • लॉजिकल AND/OR ऑपरेशंस में CEL के शॉर्ट-सर्किट को समझना
  • JSON बनाने के लिए CEL का इस्तेमाल कैसे करें
  • Protobuffers बनाने के लिए, CEL का इस्तेमाल कैसे करें
  • मैक्रो बनाना
  • अपने सीईएल एक्सप्रेशन को बेहतर बनाने के तरीके

आपको इन चीज़ों की ज़रूरत होगी

ज़रूरी शर्तें

यह कोडलैब, प्रोटोकॉल बफ़र और Go Lang की बुनियादी जानकारी पर आधारित है.

अगर आपको प्रोटोकॉल बफ़र के बारे में जानकारी नहीं है, तो पहले अभ्यास से आपको यह पता चलेगा कि CEL कैसे काम करता है. हालांकि, ज़्यादा बेहतर उदाहरणों में CEL के इनपुट के तौर पर प्रोटोकॉल बफ़र का इस्तेमाल किया जाता है. इसलिए, इन्हें समझना मुश्किल हो सकता है. सबसे पहले, इनमें से कोई एक ट्यूटोरियल देखें. ध्यान दें कि CEL का इस्तेमाल करने के लिए, प्रोटोकॉल बफ़र की ज़रूरत नहीं होती. हालांकि, इस कोडलैब में इनका इस्तेमाल बड़े पैमाने पर किया जाता है.

यह जांच की जा सकती है कि go इंस्टॉल है या नहीं. इसके लिए, यह कमांड चलाएं:

go --help

2. मुख्य सिद्धांत

ऐप्लिकेशन

CEL का इस्तेमाल सामान्य तौर पर किया जाता है. इसका इस्तेमाल अलग-अलग ऐप्लिकेशन के लिए किया जाता है. जैसे, आरपीसी को राउट करने से लेकर सुरक्षा नीति तय करने तक. CEL को बढ़ाया जा सकता है. यह किसी भी ऐप्लिकेशन के साथ काम करता है. साथ ही, इसे एक बार कंपाइल करने और कई बार आकलन करने वाले वर्कफ़्लो के लिए ऑप्टिमाइज़ किया गया है.

कई सेवाएँ और ऐप्लिकेशन, डिक्लेरेटिव कॉन्फ़िगरेशन का आकलन करते हैं. उदाहरण के लिए, भूमिका के हिसाब से ऐक्सेस कंट्रोल (आरबीएसी) एक डिक्लेरेटिव कॉन्फ़िगरेशन है. यह किसी भूमिका और उपयोगकर्ताओं के सेट के हिसाब से, ऐक्सेस करने का फ़ैसला लेता है. अगर 80% मामलों में डिक्लेरेटिव कॉन्फ़िगरेशन का इस्तेमाल किया जाता है, तो सीईएल एक काम का टूल है. इससे बाकी 20% मामलों में उपयोगकर्ताओं को ज़्यादा एक्सप्रेसिव पावर मिलती है.

कंपाइलेशन

किसी एक्सप्रेशन को एनवायरमेंट के हिसाब से कंपाइल किया जाता है. कंपाइलेशन चरण में, प्रोटॉबफ़ फ़ॉर्म में ऐब्स्ट्रैक्ट सिंटैक्स ट्री (एएसटी) बनता है. आम तौर पर, कंपाइल किए गए एक्सप्रेशन को बाद में इस्तेमाल करने के लिए सेव किया जाता है, ताकि उनका आकलन जल्द से जल्द किया जा सके. किसी एक कंपाइल किए गए एक्सप्रेशन का आकलन, कई अलग-अलग इनपुट के साथ किया जा सकता है.

एक्सप्रेशन

उपयोगकर्ता एक्सप्रेशन तय करते हैं. सेवाएं और ऐप्लिकेशन, उस एनवायरमेंट को तय करते हैं जहां एक्सप्रेशन चलता है. फ़ंक्शन सिग्नेचर, इनपुट के बारे में बताता है. इसे CEL एक्सप्रेशन के बाहर लिखा जाता है. CEL के लिए उपलब्ध फ़ंक्शन की लाइब्रेरी अपने-आप इंपोर्ट हो जाती है.

यहां दिए गए उदाहरण में, एक्सप्रेशन एक अनुरोध ऑब्जेक्ट लेता है. साथ ही, अनुरोध में दावा टोकन शामिल होता है. यह एक्सप्रेशन, बूलियन वैल्यू दिखाता है. इससे पता चलता है कि Claims Token अब भी मान्य है या नहीं.

// 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 का सबसे सामान्य पैटर्न यह है कि कंट्रोल प्लैन, कॉन्फ़िगरेशन के समय एक्सप्रेशन को पार्स और जांच करता है. साथ ही, एएसटी को सेव करता है.

c71fc08068759f81.png

रनटाइम के दौरान, डेटा प्लेन बार-बार एएसटी को वापस पाता है और उसका आकलन करता है. CEL को रनटाइम की परफ़ॉर्मेंस के लिए ऑप्टिमाइज़ किया गया है. हालांकि, पार्सिंग और जांच को इंतज़ार के समय के हिसाब से अहम कोड पाथ में नहीं किया जाना चाहिए.

49ab7d8517143b66.png

CEL को, इंसान के पढ़ने लायक एक्सप्रेशन से ऐब्स्ट्रैक्ट सिंटैक्स ट्री में पार्स किया जाता है. इसके लिए, ANTLR लेक्सर / पार्सर व्याकरण का इस्तेमाल किया जाता है. पार्स करने के दौरान, प्रोटो पर आधारित ऐब्स्ट्रैक्ट सिंटैक्स ट्री जनरेट होता है. एएसटी में मौजूद हर Expr नोड में एक पूर्णांक आईडी होता है. इसका इस्तेमाल, पार्स करने और जांच करने के दौरान जनरेट किए गए मेटाडेटा को इंडेक्स करने के लिए किया जाता है. पार्सिंग के दौरान जनरेट किया गया syntax.proto, एक्सप्रेशन के स्ट्रिंग फ़ॉर्म में टाइप किए गए कॉन्टेंट को सही तरीके से दिखाता है.

एक्सप्रेशन को पार्स करने के बाद, उसे एनवायरमेंट के हिसाब से जांचा जा सकता है. इससे यह पक्का किया जा सकता है कि एक्सप्रेशन में मौजूद सभी वैरिएबल और फ़ंक्शन आइडेंटिफ़ायर को एलान किया गया हो और उनका सही तरीके से इस्तेमाल किया जा रहा हो. टाइप-चेकर, checked.proto बनाता है. इसमें टाइप, वैरिएबल, और फ़ंक्शन रिज़ॉल्यूशन मेटाडेटा शामिल होता है. इससे आकलन की क्षमता में काफ़ी सुधार हो सकता है.

CEL का आकलन करने वाले को इन तीन चीज़ों की ज़रूरत होती है:

  • किसी भी कस्टम एक्सटेंशन के लिए फ़ंक्शन बाइंडिंग
  • वैरिएबल बाइंडिंग
  • आकलन करने के लिए एएसटी

फ़ंक्शन और वैरिएबल बाइंडिंग, एएसटी को कंपाइल करने के लिए इस्तेमाल किए गए फ़ंक्शन और वैरिएबल बाइंडिंग से मेल खानी चाहिए. इनमें से किसी भी इनपुट का इस्तेमाल, कई आकलन में फिर से किया जा सकता है. जैसे, कई वैरिएबल बाइंडिंग के सेट में किसी एएसटी का आकलन किया जा रहा है या कई एएसटी के लिए एक ही वैरिएबल का इस्तेमाल किया जा रहा है या किसी प्रोसेस के पूरे समय के लिए फ़ंक्शन बाइंडिंग का इस्तेमाल किया जा रहा है (यह एक सामान्य मामला है).

3. सेट करें

इस कोडलैब का कोड, cel-go रेपो के codelab फ़ोल्डर में मौजूद है. यह समाधान, उसी रेपो के codelab/solution फ़ोल्डर में उपलब्ध है.

रेपो को क्लोन करें और cd में जाएं:

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

go run का इस्तेमाल करके कोड चलाएं:

go run .

आपको यह आउटपुट दिखेगा:

=== Exercise 1: Hello World ===

=== Exercise 2: Variables ===

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

=== Exercise 4: Customization ===

=== Exercise 5: Building JSON ===

=== Exercise 6: Building Protos ===

=== Exercise 7: Macros ===

=== Exercise 8: Tuning ===

सीईएल पैकेज कहां हैं?

एडिटर में, 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
}

Parse और Check कॉल से मिली iss वैल्यू, समस्याओं की एक सूची होती है. इनमें गड़बड़ियां हो सकती हैं. अगर iss.Err() शून्य नहीं है, तो सिंटैक्स या सिमैंटिक्स में कोई गड़बड़ी है. इसलिए, प्रोग्राम आगे नहीं बढ़ सकता. एक्सप्रेशन सही तरीके से बनाया गया होने पर, इन कॉल का नतीजा एक एक्ज़ीक्यूटेबल cel.Ast होता है.

एक्सप्रेशन का आकलन करना

एक्सप्रेशन को पार्स करने और cel.Ast में शामिल करने के बाद, इसे ऐसे प्रोग्राम में बदला जा सकता है जिसका आकलन किया जा सकता है. इसके फ़ंक्शन बाइंडिंग और आकलन के मोड को फ़ंक्शनल विकल्पों के साथ पसंद के मुताबिक बनाया जा सकता है. ध्यान दें कि cel.CheckedExprToAst या cel.ParsedExprToAst फ़ंक्शन का इस्तेमाल करके, किसी प्रोटो से cel.Ast को भी पढ़ा जा सकता है.

cel.Program को प्लान करने के बाद, Eval को कॉल करके, इनपुट के हिसाब से इसका आकलन किया जा सकता है. Eval के नतीजे में, नतीजा, आकलन की जानकारी, और गड़बड़ी की स्थिति शामिल होगी.

प्लानिंग और कॉल eval को जोड़ें:

// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
    fmt.Println("=== Exercise 1: Hello World ===\n")
    // Create the standard environment.
    env, err := cel.NewEnv()
    if err != nil {
        glog.Exitf("env error: %v", err)
    }
    // Check that the expression compiles and returns a String.
    ast, iss := env.Parse(`"Hello, World!"`)
    // Report syntactic errors, if present.
    if iss.Err() != nil {
        glog.Exit(iss.Err())
    }
    // Type-check the expression for correctness.
    checked, iss := env.Check(ast)
    // Report semantic errors, if present.
    if iss.Err() != nil {
        glog.Exit(iss.Err())
    }
    // Check the output type is a string.
    if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
        glog.Exitf(
            "Got %v, wanted %v output type",
            checked.OutputType(), cel.StringType)
    }
    // Plan the program.
    program, err := env.Program(checked)
    if err != nil {
        glog.Exitf("program error: %v", err)
    }
    // Evaluate the program without any additional arguments.
    eval(program, cel.NoVars())
    fmt.Println()
}

हम आने वाले समय में, ऊपर दिए गए गड़बड़ी वाले मामलों को शामिल नहीं करेंगे.

कोड को एक्ज़ीक्यूट करना

कमांड लाइन पर, कोड को फिर से चलाएं:

go run .

आपको यहां दिया गया आउटपुट दिखेगा. साथ ही, आने वाले समय में किए जाने वाले कामों के लिए प्लेसहोल्डर भी दिखेंगे.

=== Exercise 1: Hello World ===

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

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

5. किसी फ़ंक्शन में वैरिएबल का इस्तेमाल करना

ज़्यादातर CEL ऐप्लिकेशन, ऐसे वैरिएबल का एलान करेंगे जिन्हें एक्सप्रेशन में रेफ़र किया जा सकता है. वैरिएबल के एलान में, नाम और टाइप की जानकारी दी जाती है. वैरिएबल का टाइप, CEL का बिल्ट-इन टाइप, प्रोटोकॉल बफ़र का वेल-नोन टाइप या कोई भी प्रोटोबफ़ मैसेज टाइप हो सकता है. हालांकि, इसके लिए यह ज़रूरी है कि CEL को उसका डिस्क्रिप्टर भी दिया गया हो.

फ़ंक्शन जोड़ना

अपने एडिटर में, exercise2 का एलान ढूंढें और यह जोड़ें:

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

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

फिर से चलाएं और गड़बड़ी को समझें

प्रोग्राम को फिर से चलाएं:

go run .

आपको यह आउटपुट दिखेगा:

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

टाइप-चेकर, अनुरोध ऑब्जेक्ट के लिए गड़बड़ी दिखाता है. इसमें सोर्स स्निपेट भी शामिल होता है, जहां गड़बड़ी हुई है.

6. वैरिएबल के बारे में जानकारी देना

EnvOptions जोड़ें

एडिटर में, अनुरोध ऑब्जेक्ट के लिए google.rpc.context.AttributeContext.Request टाइप के मैसेज के तौर पर एलान करके, गड़बड़ी को ठीक करें. इसके लिए, यह तरीका अपनाएं:

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

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

फिर से चलाएं और गड़बड़ी को समझें

प्रोग्राम को फिर से चलाने पर:

go run .

आपको यह गड़बड़ी दिखेगी:

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

प्रोटोबफ़ मैसेज को रेफ़र करने वाले वैरिएबल का इस्तेमाल करने के लिए, टाइप-चेकर को टाइप डिस्क्रिप्टर के बारे में भी पता होना चाहिए.

अपने फ़ंक्शन में अनुरोध के लिए डिस्क्रिप्टर तय करने के लिए, cel.Types का इस्तेमाल करें:

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

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

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

फिर से चलाया गया!

प्रोग्राम को फिर से चलाएं:

go run .

आपको यह जानकारी दिखेगी:

=== Exercise 2: Variables ===

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

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

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

गड़बड़ी की समीक्षा करने के लिए, हमने अभी-अभी गड़बड़ी के लिए एक वैरिएबल की जानकारी दी है. साथ ही, उसे टाइप डिस्क्रिप्टर असाइन किया है. इसके बाद, एक्सप्रेशन के आकलन में वैरिएबल का रेफ़रंस दिया है.

7. लॉजिकल AND/OR

सीईएल की सबसे खास सुविधाओं में से एक, कम्यूटेटिव लॉजिकल ऑपरेटर का इस्तेमाल करना है. शर्त के आधार पर तय होने वाली ब्रांच का कोई भी हिस्सा, गड़बड़ियों या कुछ हद तक इनपुट होने पर भी आकलन को छोटा कर सकता है.

दूसरे शब्दों में कहें, तो CEL एक ऐसा आकलन क्रम ढूंढता है जिससे जब भी संभव हो, नतीजे मिल सकें. साथ ही, यह आकलन के अन्य क्रमों में होने वाली गड़बड़ियों या डेटा के न होने की समस्या को अनदेखा करता है. ऐप्लिकेशन, इस प्रॉपर्टी पर भरोसा कर सकते हैं. इससे उन्हें आकलन की लागत को कम करने में मदद मिलती है. साथ ही, जब बिना किसी मुश्किल इनपुट के नतीजे तक पहुंचा जा सकता है, तब वे मुश्किल इनपुट इकट्ठा करने की प्रोसेस को कुछ समय के लिए रोक सकते हैं.

हम AND/OR का एक उदाहरण जोड़ेंगे. इसके बाद, अलग-अलग इनपुट के साथ इसे आज़माकर देखेंगे. इससे हमें यह समझने में मदद मिलेगी कि CEL, शॉर्ट-सर्किट का आकलन कैसे करता है.

फ़ंक्शन बनाना

अपने एडिटर में, तीसरे अभ्यास में यह कॉन्टेंट जोड़ें:

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

इसके बाद, इस OR स्टेटमेंट को शामिल करें. इससे यह पता चलेगा कि उपयोगकर्ता admin ग्रुप का सदस्य है या उसके पास कोई खास ईमेल आइडेंटिफ़ायर है:

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

प्रोटोबफ़ में, हमें पता होता है कि किस तरह के फ़ील्ड और टाइप की उम्मीद की जा सकती है. मैप और JSON वैल्यू में, हमें यह नहीं पता होता कि कोई कुंजी मौजूद होगी या नहीं. किसी कुंजी के मौजूद न होने पर, डिफ़ॉल्ट वैल्यू को सुरक्षित नहीं माना जाता. इसलिए, सीईएल में डिफ़ॉल्ट रूप से गड़बड़ी होती है.

8. कस्टम फ़ंक्शन

सीईएल में कई फ़ंक्शन पहले से मौजूद होते हैं. हालांकि, कुछ मामलों में कस्टम फ़ंक्शन का इस्तेमाल करना फ़ायदेमंद होता है. उदाहरण के लिए, कस्टम फ़ंक्शन का इस्तेमाल सामान्य स्थितियों में उपयोगकर्ता अनुभव को बेहतर बनाने या कॉन्टेक्स्ट के हिसाब से स्थिति को दिखाने के लिए किया जा सकता है

इस अभ्यास में, हम यह जानेंगे कि अक्सर इस्तेमाल की जाने वाली जांचों को एक साथ पैकेज करने के लिए, किसी फ़ंक्शन को कैसे दिखाया जाता है.

कस्टम फ़ंक्शन को कॉल करना

सबसे पहले, 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 फ़ंक्शन जोड़ना होगा. फ़िलहाल, यह सूची अनुरोध वैरिएबल का एलान करती है.

नीचे दी गई तीन लाइनें जोड़कर, पैरामीटर वाला टाइप तय करें. (यह उतना ही मुश्किल है जितना कि सीईएल के लिए कोई फ़ंक्शन ओवरलोड होगा)

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

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

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

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

कस्टम फ़ंक्शन जोड़ना

इसके बाद, हम एक नया contains फ़ंक्शन जोड़ेंगे, जो पैरामीटर वाले टाइप का इस्तेमाल करेगा:

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

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

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

गड़बड़ी को समझने के लिए प्रोग्राम चलाएं

कसरत शुरू करें. आपको रनटाइम फ़ंक्शन के मौजूद न होने की वजह से, यह गड़बड़ी दिखेगी:

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

cel.FunctionBinding() फ़ंक्शन का इस्तेमाल करके, NewEnv एलान में फ़ंक्शन लागू करें:

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

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

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

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

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

अब प्रोग्राम ठीक से चलना चाहिए:

=== Exercise 4: Custom Functions ===

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

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

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

दावा मौजूद होने पर क्या होता है?

ज़्यादा क्रेडिट पाने के लिए, इनपुट पर एडमिन का दावा सेट करें. इससे यह पुष्टि की जा सकेगी कि दावा मौजूद होने पर, contains overload भी सही वैल्यू दिखाता है. आपको यह आउटपुट दिखेगा:

=== 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 ...

cel.NewEnv() में cel.TimestampType टाइप के now वैरिएबल के लिए एलान जोड़ें और फिर से चलाएं:

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

codelab.go फ़ाइल में मौजूद valueToJSON हेल्पर फ़ंक्शन का इस्तेमाल करके टाइप को बदलने के बाद, आपको यह अतिरिक्त आउटपुट दिखेगा:

------ 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, ऐप्लिकेशन में कंपाइल किए गए किसी भी मैसेज टाइप के लिए, प्रोटोबफ़ मैसेज बना सकता है. इनपुट jwt से google.rpc.context.AttributeContext.Request बनाने के लिए, फ़ंक्शन जोड़ा गया

// 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 एनवायरमेंट के लिए cel.Container("google.rpc.context.AttributeContext") विकल्प तय करें और फिर से चलाएं:

// 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 प्रोग्राम में बदलाव करने के लिए किया जा सकता है. मैक्रो, कॉल सिग्नेचर से मैच करते हैं. साथ ही, इनपुट कॉल और उसके तर्कों में बदलाव करते हैं, ताकि नया सबएक्सप्रेशन एएसटी बनाया जा सके.

मैक्रो का इस्तेमाल, एएसटी में ऐसे जटिल लॉजिक को लागू करने के लिए किया जा सकता है जिसे सीधे तौर पर सीएलई में नहीं लिखा जा सकता. उदाहरण के लिए, 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)

जांच करता है कि क्या रेंज r में मौजूद सभी var के लिए cond की वैल्यू सही है.

मौजूद है

r.exists(var, cond)

जांच करता है कि क्या रेंज r में मौजूद किसी भी var के लिए cond की वैल्यू सही है.

exists_one

r.exists_one(var, cond)

जांच करें कि क्या रेंज r में मौजूद सिर्फ़ एक वैरिएबल के लिए cond का आकलन सही के तौर पर किया गया है.

फ़िल्टर करें

r.filter(var, cond)

सूचियों के लिए, एक नई सूची बनाएं. इसमें रेंज r में मौजूद हर एलिमेंट var, cond शर्त को पूरा करता है. मैप के लिए, एक नई सूची बनाएं. इसमें रेंज r में मौजूद हर की-वैरिएंट, cond शर्त को पूरा करता है.

मैप

r.map(var, expr)

एक नई सूची बनाता है, जिसमें रेंज r में मौजूद हर वैरिएबल को expr के हिसाब से बदला जाता है.

r.map(var, cond, expr)

यह दो आर्ग्युमेंट वाले मैप फ़ंक्शन की तरह ही होता है. हालांकि, इसमें वैल्यू को बदलने से पहले cond फ़िल्टर लगाया जाता है.

इसमें शामिल है

has(a.b)

वैल्यू a पर b की मौजूदगी की जांच : मैप के लिए, JSON टेस्ट की परिभाषा. प्रोटो के लिए, यह टेस्ट करता है कि प्रिमिटिव वैल्यू या सेट मैसेज फ़ील्ड, डिफ़ॉल्ट वैल्यू नहीं है.

जब रेंज r आर्ग्युमेंट map टाइप का होता है, तब var मैप की कुंजी होती है. साथ ही, list टाइप की वैल्यू के लिए var, सूची के एलिमेंट की वैल्यू होती है. all, exists, exists_one, filter, और map मैक्रो, एएसटी को फिर से लिखते हैं. इससे, हर बार के लिए एक इटरेशन होता है, जो इनपुट के साइज़ से बंधा होता है.

बाउंडेड कंप्रीहेंशन यह पक्का करते हैं कि CEL प्रोग्राम, ट्यूरिंग-कंप्लीट नहीं होंगे. हालांकि, वे इनपुट के हिसाब से सुपर-लीनियर समय में आकलन करते हैं. इन मैक्रो का इस्तेमाल कम से कम करें या न करें. आम तौर पर, कंप्रीहेंशन का ज़्यादा इस्तेमाल इस बात का अच्छा संकेत होता है कि कस्टम फ़ंक्शन से बेहतर उपयोगकर्ता अनुभव और बेहतर परफ़ॉर्मेंस मिलेगी.

12. ट्यूनिंग

फ़िलहाल, कुछ सुविधाएं सिर्फ़ CEL-Go में उपलब्ध हैं. हालांकि, इनसे CEL के अन्य वर्शन के लिए, आने वाले समय में उपलब्ध होने वाली सुविधाओं के बारे में पता चलता है. यहां दिए गए उदाहरण में, एक ही एएसटी के लिए अलग-अलग प्रोग्राम प्लान दिखाए गए हैं:

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

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

    fmt.Println()
}

Optimize

ऑप्टिमाइज़ेशन फ़्लैग चालू होने पर, CEL को सूची बनाने और लिटरल को पहले से मैप करने में ज़्यादा समय लगेगा. साथ ही, यह कुछ कॉल को ऑप्टिमाइज़ करेगा. जैसे, in ऑपरेटर को सेट मेंबरशिप टेस्ट के तौर पर इस्तेमाल करना:

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

जब एक ही प्रोग्राम का आकलन अलग-अलग इनपुट के आधार पर कई बार किया जाता है, तो ऑप्टिमाइज़ेशन एक अच्छा विकल्प होता है. हालांकि, जब प्रोग्राम का आकलन सिर्फ़ एक बार किया जाना हो, तो ऑप्टिमाइज़ेशन से सिर्फ़ ओवरहेड बढ़ेगा.

Exhaustive Eval

एक्सप्रेशन के आकलन के व्यवहार को डीबग करने के लिए, पूरी तरह से आकलन करने की सुविधा काम आ सकती है. ऐसा इसलिए, क्योंकि यह एक्सप्रेशन के आकलन के हर चरण में देखी गई वैल्यू के बारे में अहम जानकारी देती है.

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

आपको हर एक्सप्रेशन आईडी के लिए, एक्सप्रेशन के आकलन की स्थिति की सूची दिखेगी:

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

एक्सप्रेशन आईडी 2, पहली ब्रांच में in ऑपरेटर के नतीजे से मेल खाता है. वहीं, एक्सप्रेशन आईडी 11, दूसरी ब्रांच में == ऑपरेटर से मेल खाता है. सामान्य आकलन में, 2 का हिसाब लगाने के बाद एक्सप्रेशन का आकलन नहीं किया जाता. अगर y को uint के तौर पर तय नहीं किया गया होता, तो स्थिति में यह बताया जाता कि एक्सप्रेशन के काम न करने की दो वजहें हैं, न कि सिर्फ़ एक.

13. इसमें क्या-क्या शामिल था?

अगर आपको एक्सप्रेशन इंजन की ज़रूरत है, तो सीईएल का इस्तेमाल करें. CEL उन प्रोजेक्ट के लिए सबसे सही है जिनमें उपयोगकर्ता के कॉन्फ़िगरेशन को लागू करने की ज़रूरत होती है और परफ़ॉर्मेंस अहम होती है.

हमें उम्मीद है कि आपने पिछली एक्सरसाइज़ में, डेटा को CEL में पास करने और आउटपुट या फ़ैसले को वापस पाने का तरीका सीख लिया होगा.

हमें उम्मीद है कि आपको इस बात का अंदाज़ा लग गया होगा कि कौन-कौनसी कार्रवाइयां की जा सकती हैं. जैसे, बूलियन फ़ैसले लेने से लेकर JSON और Protobuffer मैसेज जनरेट करने तक.

हमें उम्मीद है कि आपको एक्सप्रेशन इस्तेमाल करने का तरीका समझ आ गया होगा. साथ ही, यह भी पता चल गया होगा कि ये कैसे काम करते हैं. साथ ही, हमें इसे बढ़ाने के सामान्य तरीकों के बारे में पता है.