CEL-Go Codelab: นิพจน์แบบฝังที่รวดเร็ว ปลอดภัย และฝังตัว

1. บทนำ

CEL คืออะไร

CEL คือภาษาการแสดงออกอย่างสมบูรณ์แบบที่ไม่ใช่ Tour ซึ่งออกแบบมาให้รวดเร็ว พกพาได้ และปลอดภัย คุณสามารถใช้ CEL โดยลำพังหรือฝังลงในผลิตภัณฑ์ขนาดใหญ่ก็ได้

CEL ได้รับการออกแบบมาให้เป็นภาษาที่เรียกใช้รหัสผู้ใช้ได้อย่างปลอดภัย แม้ว่าการเรียก eval() โดยการปิดบังด้วยโค้ด Python ของผู้ใช้จะเป็นอันตราย แต่คุณสามารถเรียกใช้โค้ด CEL ของผู้ใช้ได้อย่างปลอดภัย และเนื่องจาก CEL ป้องกันพฤติกรรมที่อาจทำให้ประสิทธิภาพลดลง จึงประเมินตามลำดับของนาโนวินาทีถึงไมโครวินาทีอย่างปลอดภัย เหมาะสำหรับแอปพลิเคชันที่ต้องการประสิทธิภาพ

CEL จะประเมินนิพจน์ซึ่งคล้ายกับฟังก์ชันบรรทัดเดียวหรือนิพจน์แลมบ์ดา แม้ว่ามักใช้ CEL สำหรับการตัดสินใจแบบบูลีน แต่ก็ยังใช้เพื่อสร้างออบเจ็กต์ที่ซับซ้อนมากขึ้นได้ เช่น ข้อความ JSON หรือข้อความ Protobuf

CEL เหมาะกับโปรเจ็กต์ของคุณไหม

เนื่องจาก CEL จะประเมินนิพจน์จาก AST ในหน่วยนาโนวินาทีไปจนถึงไมโครวินาที การใช้งานที่เหมาะที่สุดสำหรับ CEL คือแอปพลิเคชันที่มีเส้นทางที่สำคัญต่อประสิทธิภาพ ไม่ควรคอมไพล์โค้ด CEL ลงใน AST ในเส้นทางวิกฤต แอปพลิเคชันที่เหมาะสมคือแอปพลิเคชันที่มีการดำเนินการกำหนดค่าบ่อยและมีการแก้ไขไม่บ่อยนัก

ตัวอย่างเช่น การใช้นโยบายความปลอดภัยกับคำขอ HTTP แต่ละรายการไปยังบริการเป็นกรณีการใช้งานที่เหมาะกับ CEL เนื่องจากนโยบายความปลอดภัยมีการเปลี่ยนแปลงน้อยมาก และ CEL จะมีผลเพียงเล็กน้อยต่อเวลาในการตอบกลับ ในกรณีนี้ CEL จะแสดงผลบูลีนหากควรอนุญาตหรือไม่อนุญาตคำขอ แต่อาจแสดงข้อความที่ซับซ้อนกว่า

Codelab นี้ครอบคลุมอะไรบ้าง

ขั้นตอนแรกของ Codelab นี้จะบ่งบอกถึงแรงจูงใจในการใช้ CEL และแนวคิดหลักของ CEL ส่วนที่เหลือจะมีไว้สำหรับการเขียนโค้ดแบบฝึกหัดที่ครอบคลุมกรณีการใช้งานทั่วไป หากต้องการข้อมูลเชิงลึกเกี่ยวกับภาษา ความหมาย และฟีเจอร์ โปรดดูคำจำกัดความของภาษา CEL ใน GitHub และ CEL Go Docs

Codelab นี้มีเป้าหมายสำหรับนักพัฒนาซอฟต์แวร์ที่ต้องการเรียนรู้ CEL เพื่อใช้บริการที่รองรับ CEL อยู่แล้ว Codelab นี้ไม่ได้กล่าวถึงวิธีผสานรวม CEL เข้ากับโปรเจ็กต์ของคุณเอง

สิ่งที่คุณจะได้เรียนรู้

  • แนวคิดหลักของ CEL
  • สวัสดีทุกคน: การใช้ CEL เพื่อประเมินสตริง
  • การสร้างตัวแปร
  • ทำความเข้าใจการลัดวงจรของ CEL ในการดำเนินการตรรกะ AND/OR
  • วิธีใช้ CEL เพื่อสร้าง JSON
  • วิธีใช้ CEL เพื่อสร้าง Protobuffers
  • การสร้างมาโคร
  • วิธีปรับแต่งนิพจน์ CEL

สิ่งที่คุณต้องมี

ข้อกำหนดเบื้องต้น

Codelab นี้สร้างขึ้นจากความเข้าใจขั้นพื้นฐานเกี่ยวกับบัฟเฟอร์โปรโตคอลและ Go Lang

หากคุณไม่คุ้นเคยกับ Protocol Buffers แบบฝึกหัดแรกจะช่วยให้คุณเข้าใจว่า CEL ทำงานอย่างไร แต่เนื่องจากตัวอย่างขั้นสูงกว่านั้นใช้ Protocol Buffers เป็นข้อมูลที่ป้อนใน CEL จึงอาจเข้าใจยากกว่า โปรดลองทำตามบทแนะนำเหล่านี้ก่อน โปรดทราบว่าบัฟเฟอร์โปรโตคอลไม่จำเป็นต้องใช้ CEL แต่มีการใช้กันอย่างแพร่หลายใน Codelab นี้

คุณทดสอบว่ามีการติดตั้งเวอร์ชันดังกล่าวได้โดยการเรียกใช้

go --help

2. หัวข้อสำคัญ

แอปพลิเคชัน

CEL เป็นเครื่องมือทั่วไปและมีการใช้งานสำหรับแอปพลิเคชันที่หลากหลาย ตั้งแต่การกำหนดเส้นทาง RPC ไปจนถึงการกำหนดนโยบายการรักษาความปลอดภัย CEL นั้นมีความยืดหยุ่น ใช้ได้กับแอปพลิเคชันมากมาย และเพิ่มประสิทธิภาพสำหรับเวิร์กโฟลว์การคอมไพล์ครั้งเดียวและประเมินได้จำนวนมาก

บริการและแอปพลิเคชันมากมายจะประเมินการกำหนดค่าเชิงประกาศ เช่น การควบคุมการเข้าถึงตามบทบาท (RBAC) เป็นการกำหนดค่าเชิงประกาศที่สร้างการตัดสินใจในการเข้าถึงบทบาทและกลุ่มผู้ใช้ หากการกำหนดค่าเชิงประกาศเป็นกรณีการใช้งาน 80% CEL จะเป็นเครื่องมือที่มีประโยชน์ในการปัดเศษ 20% ที่เหลือเมื่อผู้ใช้ต้องการพลังในการแสดงออกมากขึ้น

การรวบรวม

นิพจน์จะถูกคอมไพล์กับสภาพแวดล้อม ขั้นตอนการคอมไพล์จะสร้างแผนผังไวยากรณ์นามธรรม (AST) ในรูปแบบ Protobuf โดยทั่วไป นิพจน์ที่คอมไพล์แล้วจะได้รับการเก็บไว้สำหรับการใช้งานในอนาคตเพื่อให้การประเมินเป็นไปโดยเร็วที่สุด นิพจน์ที่คอมไพล์รายการเดียวจะประเมินได้ด้วยอินพุตหลายรายการ

นิพจน์

ผู้ใช้กำหนดนิพจน์ บริการและแอปพลิเคชันจะกำหนดสภาพแวดล้อมที่ใช้งาน ลายเซ็นของฟังก์ชันจะประกาศอินพุตและเขียนนอกนิพจน์ CEL ระบบจะนำเข้าไลบรารีของฟังก์ชันที่ใช้ได้กับ CEL โดยอัตโนมัติ

ในตัวอย่างต่อไปนี้ นิพจน์จะใช้ออบเจ็กต์คำขอ และคำขอมีโทเค็นการอ้างสิทธิ์ นิพจน์นี้จะแสดงค่าบูลีนที่ระบุว่าโทเค็นการอ้างสิทธิ์ยังคงใช้งานได้หรือไม่

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

สภาพแวดล้อม

สภาพแวดล้อมจะกำหนดโดยบริการ บริการและแอปพลิเคชันที่ฝัง CEL จะประกาศสภาพแวดล้อมของนิพจน์ สภาพแวดล้อมคือคอลเล็กชันของตัวแปรและฟังก์ชันที่ใช้ในนิพจน์ได้

เครื่องมือตรวจสอบประเภท CEL จะใช้การประกาศแบบโปรโตเพื่อให้แน่ใจว่ามีการประกาศและใช้การอ้างอิงตัวระบุและฟังก์ชันทั้งหมดภายในนิพจน์อย่างถูกต้อง

การแยกวิเคราะห์นิพจน์ 3 ขั้นตอน

การประมวลผลนิพจน์มี 3 ขั้นตอน ได้แก่ แยกวิเคราะห์ ตรวจสอบ และประเมิน รูปแบบที่พบบ่อยที่สุดสำหรับ CEL คือระนาบควบคุมที่จะแยกวิเคราะห์และตรวจสอบนิพจน์ในเวลากำหนดค่า และจัดเก็บ AST

c71fc08068759f81.png

ขณะรันไทม์ ระนาบข้อมูลจะดึงและประเมิน AST ซ้ำๆ CEL ได้รับการเพิ่มประสิทธิภาพเพื่อประสิทธิภาพรันไทม์ แต่ไม่ควรแยกวิเคราะห์และตรวจสอบในเส้นทางโค้ดที่สำคัญของเวลาในการตอบสนอง

49ab7d8517143b66.png

CEL จะแยกวิเคราะห์จากนิพจน์ที่มนุษย์อ่านได้เป็นแผนผังไวยากรณ์นามธรรมโดยใช้ไวยากรณ์ lexer / โปรแกรมแยกวิเคราะห์ ANTLR เฟสการแยกวิเคราะห์จะปล่อยโครงสร้างไวยากรณ์นามธรรมที่อิงตามโปรโต ซึ่งโหนด Expr แต่ละรายการใน AST จะมีรหัสจำนวนเต็มที่ใช้ในการจัดทำดัชนีเป็นข้อมูลเมตาที่สร้างขึ้นระหว่างการแยกวิเคราะห์และตรวจสอบ syntax.proto ที่สร้างขึ้นระหว่างการแยกวิเคราะห์แสดงถึงการนำเสนอแบบนามธรรมของสิ่งที่พิมพ์ในรูปแบบสตริงของนิพจน์

เมื่อมีการแยกวิเคราะห์นิพจน์แล้ว อาจมีการตรวจสอบนิพจน์นั้นกับสภาพแวดล้อมเพื่อให้แน่ใจว่ามีการประกาศตัวแปรและตัวระบุฟังก์ชันทั้งหมดในนิพจน์นั้นและใช้งานอย่างถูกต้อง เครื่องมือตรวจสอบประเภทจะสร้าง checked.proto ที่มีข้อมูลเมตาประเภท ตัวแปร และความละเอียดของฟังก์ชันที่ช่วยปรับปรุงประสิทธิภาพการประเมินได้อย่างมาก

ผู้ประเมิน CEL ต้องการ 3 สิ่งต่อไปนี้

  • การเชื่อมโยงฟังก์ชันสำหรับส่วนขยายที่กำหนดเอง
  • การเชื่อมโยงตัวแปร
  • AST ในการประเมิน

การเชื่อมโยงฟังก์ชันและตัวแปรควรตรงกับที่ใช้ในการคอมไพล์ AST อินพุตใดๆ เหล่านี้สามารถนํามาใช้ซ้ำในการประเมินได้หลายรายการ เช่น AST ที่ได้รับการประเมินในการเชื่อมโยงตัวแปรหลายชุด หรือใช้ตัวแปรเดียวกับที่ใช้กับ AST หลายรายการ หรือการเชื่อมโยงฟังก์ชันที่ใช้ตลอดอายุของกระบวนการ (กรณีทั่วไป)

3. ตั้งค่า

โค้ดสำหรับ Codelab นี้อยู่ในโฟลเดอร์ codelab ของที่เก็บ cel-go มีวิธีแก้ปัญหาอยู่ในโฟลเดอร์ codelab/solution ของที่เก็บเดียวกัน

โคลนและซีดีลงในที่เก็บ:

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

เรียกใช้โค้ดโดยใช้ go run:

go run .

คุณควรจะเห็นผลลัพธ์ต่อไปนี้

=== Exercise 1: Hello World ===

=== Exercise 2: Variables ===

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

=== Exercise 4: Customization ===

=== Exercise 5: Building JSON ===

=== Exercise 6: Building Protos ===

=== Exercise 7: Macros ===

=== Exercise 8: Tuning ===

แพ็กเกจ CEL อยู่ตรงไหน

เปิด codelab/codelab.go ในเครื่องมือแก้ไข คุณควรจะเห็นฟังก์ชันหลักที่กระตุ้นการดำเนินการทำแบบฝึกหัดใน Codelab นี้ ตามด้วยฟังก์ชันตัวช่วย 3 บล็อก ผู้ช่วยเหลือชุดแรกจะให้ความช่วยเหลือในขั้นตอนการประเมิน 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 จาก Pro โดยใช้ฟังก์ชัน 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 Built-In, ประเภทบัฟเฟอร์โปรโตคอลที่รู้จัก หรือประเภทข้อความ 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'
 | .......^

หากต้องการใช้ตัวแปรที่อ้างถึงข้อความ protobuf เครื่องมือตรวจสอบประเภทจะต้องรู้จักตัวบอกประเภทด้วย

ใช้ cel.Types เพื่อระบุข้อบ่งชี้สำหรับคำขอในฟังก์ชัน

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

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

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

เรียกใช้อีกครั้งเรียบร้อยแล้ว

เรียกใช้โปรแกรมอีกครั้ง

go run .

คุณควรจะเห็นสิ่งต่อไปนี้

=== Exercise 2: Variables ===

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

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

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

ในการตรวจสอบ เราเพียงประกาศตัวแปรสำหรับข้อผิดพลาด กำหนดข้อบ่งชี้ประเภท แล้วอ้างอิงตัวแปรในการประเมินนิพจน์

7. ตรรกะ AND/OR

ฟีเจอร์ที่ไม่เหมือนใครอย่างหนึ่งของ CEL คือการใช้โอเปอเรเตอร์ตรรกะแบบสับเปลี่ยน ทั้ง 2 ฝั่งของสาขาแบบมีเงื่อนไขอาจทำให้การประเมินถูกตัดให้สั้นลง แม้อาจเกิดข้อผิดพลาดหรือมีอินพุตเพียงบางส่วน

กล่าวคือ 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"),
    ),
  )
}

ถัดไป ให้ใส่คำสั่ง "หรือ" ที่จะแสดงผลเป็น "จริง" หากผู้ใช้เป็นสมาชิกของกลุ่ม admin หรือมีตัวระบุอีเมลที่เฉพาะเจาะจง

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

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

และสุดท้าย ให้เพิ่มเคส eval ที่ประเมินผู้ใช้ด้วยชุดการอ้างสิทธิ์ที่ว่างเปล่า ดังนี้

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

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

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

เรียกใช้โค้ดด้วยชุดการอ้างสิทธิ์ที่ว่างเปล่า

การเรียกใช้โปรแกรมอีกครั้ง คุณควรเห็นผลลัพธ์ใหม่ต่อไปนี้

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

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

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

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

อัปเดตกรณีการประเมิน

ถัดไป ให้อัปเดตกรณีการประเมินเพื่อส่งต่อผู้ใช้หลักรายอื่นที่มีชุดการอ้างสิทธิ์ที่ว่างเปล่า ดังนี้

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

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

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

เรียกใช้โค้ดที่มีเวลา

การเรียกใช้โปรแกรมอีกครั้ง

go run .

คุณควรเห็นข้อผิดพลาดต่อไปนี้

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

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

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

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

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

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

ใน Protobuf เราทราบว่าต้องพบกับฟิลด์และประเภทใดบ้าง ในค่าการแมปและ JSON เราไม่ทราบว่าจะมีคีย์หรือไม่ เนื่องจาก CEL ไม่มีค่าเริ่มต้นที่ปลอดภัยสำหรับคีย์ที่หายไป จึงไม่มีค่าเริ่มต้นเป็นข้อผิดพลาด

8. ฟังก์ชันที่กำหนดเอง

แม้ว่า CEL จะมีฟังก์ชันในตัวมากมาย แต่ก็อาจมีบางครั้งที่ฟังก์ชันที่กำหนดเองมีประโยชน์ เช่น สามารถใช้ฟังก์ชันที่กำหนดเองเพื่อปรับปรุงประสบการณ์ของผู้ใช้สำหรับเงื่อนไขทั่วไปหรือแสดงสถานะที่คำนึงถึงบริบท

ในแบบฝึกหัดนี้ เราจะศึกษาวิธีแสดงฟังก์ชันเพื่อรวมการตรวจสอบที่ใช้กันทั่วไปไว้ด้วยกัน

เรียกใช้ฟังก์ชันที่กำหนดเอง

ขั้นแรก ให้สร้างรหัสเพื่อตั้งค่าการลบล้างที่ชื่อ contains ซึ่งกำหนดว่ามีคีย์อยู่ในแผนที่หรือไม่และมีค่าเฉพาะหรือไม่ ปล่อยตัวยึดตำแหน่งสำหรับคำจำกัดความของฟังก์ชันและการเชื่อมโยงฟังก์ชัน

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

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

เรียกใช้โค้ดและทำความเข้าใจข้อผิดพลาด

เรียกใช้โค้ดอีกครั้ง คุณควรพบข้อผิดพลาดต่อไปนี้

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

ในการแก้ไขข้อผิดพลาด เราต้องเพิ่มฟังก์ชัน contains ลงในรายการการประกาศที่ประกาศตัวแปรคำขอในปัจจุบัน

ประกาศประเภทที่ทำเป็นพารามิเตอร์โดยเพิ่ม 3 บรรทัดต่อไปนี้ (ซึ่งมีความซับซ้อนพอๆ กับที่ฟังก์ชันโอเวอร์โหลดจะเป็นสำหรับ CEL)

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

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

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

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

เพิ่มฟังก์ชันที่กำหนดเอง

ต่อไป เราจะเพิ่มฟังก์ชัน "มี" ใหม่ที่ใช้ประเภทพารามิเตอร์

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

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

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

เรียกใช้โปรแกรมเพื่อทำความเข้าใจข้อผิดพลาด

เรียกใช้การออกกำลังกาย คุณควรพบข้อผิดพลาดต่อไปนี้เกี่ยวกับฟังก์ชันรันไทม์ที่ขาดหายไป

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

ติดตั้งใช้งานฟังก์ชันกับการประกาศ 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. การสร้าง Protos

CEL สามารถสร้างข้อความ Protobuf สำหรับข้อความทุกประเภทที่คอมไพล์ลงในแอปพลิเคชัน เพิ่มฟังก์ชันเพื่อสร้าง google.rpc.context.AttributeContext.Request จากอินพุต jwt

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

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

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

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

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

เรียกใช้โค้ด

เรียกใช้โค้ดอีกครั้ง คุณควรพบข้อผิดพลาดต่อไปนี้

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

โดยพื้นฐานแล้ว คอนเทนเนอร์นี้เทียบเท่ากับเนมสเปซหรือแพ็กเกจ แต่ก็อาจมีรายละเอียดได้ไม่ต่างจากชื่อข้อความ protobuf คอนเทนเนอร์ CEL ใช้กฎการแปลงเนมสเปซเดียวกันกับโปรโตคอลและ 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 ประเมินค่า true สำหรับ var all ในช่วง r หรือไม่

มีอยู่

r.exists(var, cond)

ทดสอบว่า cond ประเมินค่า true สำหรับตัวแปรใดก็ได้ ในช่วง r

exists_one

r.exists_one(var, cond)

ทดสอบว่า cond ประเมินค่าจริงสำหรับตัวแปรเพียงรายการเดียว ในช่วง r หรือไม่

ตัวกรอง

r.filter(var, cond)

สำหรับรายการ ให้สร้างรายการใหม่ที่ตัวแปรองค์ประกอบแต่ละรายการในช่วง r เป็นไปตามเงื่อนไข สำหรับแผนที่ ให้สร้างรายการใหม่ที่แต่ละคีย์ตัวแปรในช่วง r เป็นไปตามเงื่อนไข

แผนที่

r.map(var, expr)

สร้างรายการใหม่ที่แต่ละตัวแปรในช่วง r จะมีการแปลงโดย expr

r.map(var, cond, expr)

เหมือนกับแผนที่ 2 อาร์กิวเมนต์ แต่มีตัวกรองเงื่อนไขแบบมีเงื่อนไขก่อนที่ระบบจะเปลี่ยนรูปแบบค่า

มี

มี(ก.ข)

การทดสอบการปรากฏของ b กับค่า a : สำหรับแผนที่ คำจำกัดความการทดสอบ JSON สำหรับ Proto ให้ทดสอบค่าพื้นฐานที่ไม่ใช่ค่าเริ่มต้น หรือช่องข้อความหรือช่องข้อความที่ตั้งไว้

เมื่ออาร์กิวเมนต์ 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()
}

Optimize

เมื่อเปิด Flag การเพิ่มประสิทธิภาพ 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)

เมื่อมีการประเมินโปรแกรมเดียวกันหลายครั้งโดยดูจากอินพุตที่ต่างกัน การเพิ่มประสิทธิภาพก็เป็นทางเลือกที่ดี แต่หากโปรแกรมได้รับการประเมินเพียงครั้งเดียว การเพิ่มประสิทธิภาพก็จะเพิ่มค่าใช้จ่ายในการดำเนินการ

การประเมินเมื่อเหนื่อยล้า

Exsurtive 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 ในตอนแรก Branch และรหัสนิพจน์ 11 จะสอดคล้องกับโอเปอเรเตอร์ == ในค่าที่สอง ภายใต้การประเมินปกติ นิพจน์จะถูกตัดไฟฟ้าหลังจากคำนวณ 2 ครั้งแล้ว หาก y ไม่มีการ uint สถานะจะแสดงสาเหตุ 2 ประการที่ทำให้นิพจน์ล้มเหลว ไม่ใช่เพียง 1 ข้อ

13. เนื้อหาที่ครอบคลุมเรื่องใดบ้าง

หากต้องการใช้เครื่องมือนิพจน์ ให้ลองใช้ CEL CEL เหมาะสำหรับโปรเจ็กต์ที่จำเป็นต้องดำเนินการกับการกำหนดค่าของผู้ใช้ซึ่งจำเป็นต้องดำเนินการเพื่อเพิ่มประสิทธิภาพ

ในแบบฝึกหัดก่อนหน้า เราหวังว่าคุณจะมั่นใจที่จะส่งต่อข้อมูลไปยัง CEL และได้รับข้อมูลหรือการตัดสินใจกลับมา

เราหวังว่าคุณจะเข้าใจประเภทการดำเนินการที่คุณสามารถทำได้ ตั้งแต่การตัดสินใจแบบบูลีนไปจนถึงการสร้างข้อความ JSON และ Protobuffer

เราหวังว่าคุณจะเข้าใจวิธีใช้การแสดงออกและสิ่งที่พวกเขากำลังทำ และเราก็เข้าใจวิธีทั่วไปในการขยายการใช้งานเครื่องมือนี้ด้วย