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
ขณะรันไทม์ ระนาบข้อมูลจะดึงและประเมิน AST ซ้ำๆ CEL ได้รับการเพิ่มประสิทธิภาพเพื่อประสิทธิภาพรันไทม์ แต่ไม่ควรแยกวิเคราะห์และตรวจสอบในเส้นทางโค้ดที่สำคัญของเวลาในการตอบสนอง
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 | อินเทอร์เฟซระดับบนสุด | |
อ้างอิง | อินเทอร์เฟซข้อมูลอ้างอิง | |
ประเภท | ค่าประเภทรันไทม์ |
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
เราหวังว่าคุณจะเข้าใจวิธีใช้การแสดงออกและสิ่งที่พวกเขากำลังทำ และเราก็เข้าใจวิธีทั่วไปในการขยายการใช้งานเครื่องมือนี้ด้วย