Lớp học lập trình CEL-Go: Biểu thức được nhúng nhanh, an toàn

1. Giới thiệu

CEL là gì?

CEL là một ngôn ngữ biểu thức không hoàn chỉnh theo Turing, được thiết kế để thực thi nhanh, di động và an toàn. CEL có thể được dùng riêng hoặc được nhúng vào một sản phẩm lớn hơn.

CEL được thiết kế như một ngôn ngữ mà trong đó, việc thực thi mã người dùng là an toàn. Mặc dù việc gọi eval() một cách mù quáng trên mã python của người dùng là nguy hiểm, nhưng bạn có thể thực thi mã CEL của người dùng một cách an toàn. Và vì CEL ngăn chặn hành vi làm giảm hiệu suất, nên CEL đánh giá một cách an toàn theo thứ tự từ nano giây đến micro giây; đây là lựa chọn lý tưởng cho các ứng dụng quan trọng về hiệu suất.

CEL đánh giá các biểu thức, tương tự như các hàm một dòng hoặc biểu thức lambda. Mặc dù CEL thường được dùng cho các quyết định boolean, nhưng bạn cũng có thể dùng CEL để tạo các đối tượng phức tạp hơn như thông báo JSON hoặc protobuf.

CEL có phù hợp với dự án của bạn không?

Vì CEL đánh giá một biểu thức từ AST trong khoảng thời gian từ nano giây đến micro giây, nên trường hợp sử dụng lý tưởng cho CEL là các ứng dụng có đường dẫn quan trọng về hiệu suất. Bạn không nên biên dịch mã CEL thành AST trong các đường dẫn quan trọng; các ứng dụng lý tưởng là những ứng dụng mà cấu hình được thực thi thường xuyên và sửa đổi tương đối không thường xuyên.

Ví dụ: việc thực thi chính sách bảo mật với mỗi yêu cầu HTTP tới một dịch vụ là một trường hợp sử dụng lý tưởng cho CEL vì chính sách bảo mật hiếm khi thay đổi và CEL sẽ có tác động không đáng kể đến thời gian phản hồi. Trong trường hợp này, CEL trả về một giá trị boolean, nếu yêu cầu được phép hay không, nhưng CEL có thể trả về một thông báo phức tạp hơn.

Nội dung của Lớp học lập trình này

Bước đầu tiên của lớp học lập trình này sẽ trình bày động lực sử dụng CEL và Các khái niệm cốt lõi của CEL. Phần còn lại dành cho Bài tập lập trình bao gồm các trường hợp sử dụng phổ biến. Để tìm hiểu sâu hơn về ngôn ngữ, ngữ nghĩa và các tính năng, hãy xem Định nghĩa ngôn ngữ CEL trên GitHub và CEL Go Docs.

Lớp học lập trình này dành cho những nhà phát triển muốn tìm hiểu về CEL để sử dụng các dịch vụ đã hỗ trợ CEL. Lớp học lập trình này không đề cập đến cách tích hợp CEL vào dự án của riêng bạn.

Kiến thức bạn sẽ học được

  • Các khái niệm cốt lõi trong CEL
  • Xin chào thế giới: Sử dụng CEL để đánh giá một chuỗi
  • Tạo biến
  • Tìm hiểu về tính năng đoản mạch của CEL trong các thao tác AND/OR logic
  • Cách sử dụng CEL để tạo JSON
  • Cách sử dụng CEL để tạo Protobuffer
  • Tạo macro
  • Cách điều chỉnh biểu thức CEL

Bạn cần có

Điều kiện tiên quyết

Lớp học lập trình này dựa trên kiến thức cơ bản về Protocol BuffersGo Lang.

Nếu bạn chưa quen với Protocol Buffers, bài tập đầu tiên sẽ giúp bạn hiểu cách hoạt động của CEL. Tuy nhiên, vì các ví dụ nâng cao hơn sử dụng Protocol Buffers làm đầu vào cho CEL, nên có thể bạn sẽ khó hiểu hơn. Trước tiên, hãy cân nhắc việc xem một trong những hướng dẫn này. Xin lưu ý rằng bạn không bắt buộc phải sử dụng CEL để dùng Protocol Buffers, nhưng chúng được sử dụng rộng rãi trong lớp học lập trình này.

Bạn có thể kiểm tra xem go đã được cài đặt hay chưa bằng cách chạy:

go --help

2. Khái niệm chính

Ứng dụng

CEL là ngôn ngữ đa năng và đã được dùng cho nhiều ứng dụng, từ định tuyến RPC đến xác định chính sách bảo mật. CEL có thể mở rộng, không phụ thuộc vào ứng dụng và được tối ưu hoá cho quy trình làm việc biên dịch một lần, đánh giá nhiều lần.

Nhiều dịch vụ và ứng dụng đánh giá các cấu hình khai báo. Ví dụ: Điều khiển truy cập dựa trên vai trò (RBAC) là một cấu hình khai báo đưa ra quyết định truy cập dựa trên vai trò và một nhóm người dùng. Nếu cấu hình khai báo là trường hợp sử dụng 80%, thì CEL là một công cụ hữu ích để hoàn thành 20% còn lại khi người dùng cần có sức mạnh biểu đạt lớn hơn.

Biên dịch

Một biểu thức được biên dịch dựa trên một môi trường. Bước biên dịch tạo ra Cây cú pháp trừu tượng (AST) ở dạng protobuf. Các biểu thức đã biên dịch thường được lưu trữ để sử dụng trong tương lai nhằm duy trì tốc độ đánh giá nhanh nhất có thể. Bạn có thể đánh giá một biểu thức đã biên dịch duy nhất bằng nhiều đầu vào khác nhau.

Cụm từ

Người dùng xác định các biểu thức; các dịch vụ và ứng dụng xác định môi trường nơi biểu thức chạy. Chữ ký hàm khai báo dữ liệu đầu vào và được viết bên ngoài biểu thức CEL. Thư viện hàm có sẵn cho CEL sẽ được nhập tự động.

Trong ví dụ sau, biểu thức sẽ lấy một đối tượng yêu cầu và yêu cầu này bao gồm một mã thông báo xác nhận quyền sở hữu. Biểu thức này trả về một giá trị boolean cho biết mã thông báo xác nhận quyền sở hữu vẫn còn hiệu lực hay không.

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

Môi trường

Môi trường được xác định bằng các dịch vụ. Các dịch vụ và ứng dụng nhúng CEL sẽ khai báo môi trường biểu thức. Môi trường là tập hợp các biến và hàm có thể dùng trong biểu thức.

Trình kiểm tra kiểu CEL sử dụng các khai báo dựa trên giao thức để đảm bảo rằng tất cả các giá trị nhận dạng và hàm tham chiếu trong một biểu thức đều được khai báo và sử dụng đúng cách.

Ba giai đoạn phân tích cú pháp một biểu thức

Có 3 giai đoạn xử lý một biểu thức: phân tích cú pháp, kiểm tra và đánh giá. Mẫu phổ biến nhất cho CEL là để một mặt phẳng điều khiển phân tích cú pháp và kiểm tra các biểu thức tại thời điểm định cấu hình, đồng thời lưu trữ AST.

c71fc08068759f81.png

Trong thời gian chạy, lớp dữ liệu sẽ truy xuất và đánh giá AST nhiều lần. CEL được tối ưu hoá để đạt hiệu quả thời gian chạy, nhưng không nên phân tích cú pháp và kiểm tra trong các đường dẫn mã quan trọng về độ trễ.

49ab7d8517143b66.png

CEL được phân tích cú pháp từ một biểu thức mà con người có thể đọc được thành một cây cú pháp trừu tượng bằng cách sử dụng ngữ pháp trình phân tích cú pháp / trình phân tích từ vựng ANTLR. Giai đoạn phân tích cú pháp phát ra một cây cú pháp trừu tượng dựa trên giao thức, trong đó mỗi nút Expr trong AST chứa một mã nhận dạng số nguyên được dùng để lập chỉ mục vào siêu dữ liệu được tạo trong quá trình phân tích cú pháp và kiểm tra. syntax.proto được tạo trong quá trình phân tích cú pháp thể hiện chính xác biểu diễn trừu tượng của nội dung được nhập ở dạng chuỗi của biểu thức.

Sau khi được phân tích cú pháp, biểu thức có thể được kiểm tra dựa trên môi trường để đảm bảo tất cả các giá trị nhận dạng biến và hàm trong biểu thức đã được khai báo và đang được sử dụng đúng cách. Trình kiểm tra kiểu tạo ra một checked.proto bao gồm siêu dữ liệu phân giải kiểu, biến và hàm có thể cải thiện đáng kể hiệu quả đánh giá.

Trình đánh giá CEL cần 3 điều kiện:

  • Liên kết hàm cho mọi tiện ích tuỳ chỉnh
  • Liên kết biến
  • Một AST để đánh giá

Các liên kết hàm và biến phải khớp với những gì đã dùng để biên dịch AST. Bạn có thể sử dụng lại bất kỳ đầu vào nào trong nhiều lượt đánh giá, chẳng hạn như một AST được đánh giá trên nhiều nhóm liên kết biến, hoặc các biến giống nhau được dùng cho nhiều AST, hoặc các liên kết hàm được dùng trong suốt thời gian hoạt động của một quy trình (một trường hợp phổ biến).

3. Thiết lập

Mã cho lớp học lập trình này nằm trong thư mục codelab của kho lưu trữ cel-go. Giải pháp có trong codelab/solution thư mục của cùng một kho lưu trữ.

Sao chép và chuyển đến kho lưu trữ:

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

Chạy mã bằng go run:

go run .

Bạn sẽ thấy kết quả sau đây:

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

Các gói CEL ở đâu?

Trong trình chỉnh sửa, hãy mở codelab/codelab.go. Bạn sẽ thấy hàm chính điều khiển việc thực thi các bài tập trong lớp học lập trình này, theo sau là 3 khối hàm trợ giúp. Nhóm trợ giúp đầu tiên hỗ trợ các giai đoạn đánh giá CEL:

  • Hàm Compile: Phân tích cú pháp, kiểm tra và so sánh biểu thức đầu vào với một môi trường
  • Hàm Eval: Đánh giá một chương trình đã biên dịch dựa trên một đầu vào
  • Hàm Report: In kết quả đánh giá theo cách dễ đọc

Ngoài ra, các trình trợ giúp requestauth đã được cung cấp để hỗ trợ việc tạo đầu vào cho nhiều bài tập.

Các bài tập sẽ đề cập đến các gói bằng tên gói ngắn. Sau đây là mối liên kết từ gói đến vị trí nguồn trong kho lưu trữ google/cel-go nếu bạn muốn tìm hiểu chi tiết:

Gói

Vị trí nguồn

Mô tả

cel

cel-go/cel

Giao diện cấp cao nhất

ref

cel-go/common/types/ref

Giao diện tham chiếu

loại

cel-go/common/types

Giá trị loại thời gian chạy

4. Chào bạn!

Theo truyền thống của mọi ngôn ngữ lập trình, chúng ta sẽ bắt đầu bằng cách tạo và đánh giá "Hello World!".

Định cấu hình môi trường

Trong trình chỉnh sửa, hãy tìm khai báo exercise1 và điền thông tin sau để thiết lập môi trường:

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

Các ứng dụng CEL đánh giá một biểu thức dựa trên một Môi trường. env, err := cel.NewEnv() định cấu hình môi trường tiêu chuẩn.

Bạn có thể tuỳ chỉnh môi trường bằng cách cung cấp các lựa chọn cel.EnvOption cho lệnh gọi. Các lựa chọn đó có thể tắt macro, khai báo các biến và hàm tuỳ chỉnh, v.v.

Môi trường CEL tiêu chuẩn hỗ trợ tất cả các loại, toán tử, hàm và macro được xác định trong quy cách ngôn ngữ.

Phân tích cú pháp và kiểm tra biểu thức

Sau khi môi trường được định cấu hình, các biểu thức có thể được phân tích cú pháp và kiểm tra. Thêm nội dung sau vào hàm của bạn:

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

Giá trị iss do các lệnh gọi ParseCheck trả về là danh sách các vấn đề có thể là lỗi. Nếu iss.Err() không phải là nil, thì có lỗi về cú pháp hoặc ngữ nghĩa và chương trình không thể tiếp tục. Khi biểu thức có dạng thức phù hợp, kết quả của các lệnh gọi này là một cel.Ast có thể thực thi.

Đánh giá biểu thức

Sau khi được phân tích cú pháp và kiểm tra thành cel.Ast, biểu thức có thể được chuyển đổi thành một chương trình có thể đánh giá mà bạn có thể tuỳ chỉnh các chế độ đánh giá và liên kết hàm bằng các lựa chọn chức năng. Xin lưu ý rằng bạn cũng có thể đọc một cel.Ast từ một proto bằng cách sử dụng hàm cel.CheckedExprToAst hoặc cel.ParsedExprToAst.

Sau khi được lập kế hoạch, cel.Program có thể được đánh giá dựa trên dữ liệu đầu vào bằng cách gọi Eval. Kết quả của Eval sẽ chứa kết quả, thông tin chi tiết về việc đánh giá và trạng thái lỗi.

Thêm kế hoạch và gọi 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()
}

Để cho ngắn gọn, chúng ta sẽ bỏ qua các trường hợp lỗi được đề cập ở trên trong các bài tập sau này.

Thực thi mã

Trên dòng lệnh, hãy chạy lại mã:

go run .

Bạn sẽ thấy kết quả sau đây, cùng với các trình giữ chỗ cho các bài tập trong tương lai.

=== Exercise 1: Hello World ===

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

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

5. Sử dụng biến trong hàm

Hầu hết các ứng dụng CEL sẽ khai báo các biến có thể được tham chiếu trong các biểu thức. Khai báo biến chỉ định tên và loại. Loại của biến có thể là loại tích hợp CEL, loại vùng đệm giao thức nổi tiếng hoặc bất kỳ loại thông báo protobuf nào miễn là bạn cũng cung cấp bộ mô tả của loại đó cho CEL.

Thêm hàm

Trong trình chỉnh sửa, hãy tìm phần khai báo của exercise2 rồi thêm nội dung sau:

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

Chạy lại và tìm hiểu lỗi

Chạy lại chương trình:

go run .

Bạn sẽ thấy kết quả sau đây:

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

Trình kiểm tra kiểu tạo ra lỗi cho đối tượng yêu cầu, trong đó có đoạn mã nguồn nơi xảy ra lỗi.

6. Khai báo các biến

Thêm EnvOptions

Trong trình chỉnh sửa, hãy khắc phục lỗi xảy ra bằng cách cung cấp một khai báo cho đối tượng yêu cầu dưới dạng thông báo thuộc loại google.rpc.context.AttributeContext.Request như sau:

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

Chạy lại và tìm hiểu lỗi

Chạy lại chương trình:

go run .

Bạn sẽ thấy lỗi sau:

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

Để sử dụng các biến tham chiếu đến thông báo protobuf, trình kiểm tra loại cũng cần biết bộ mô tả loại.

Sử dụng cel.Types để xác định giá trị mô tả cho yêu cầu trong hàm:

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

Đã chạy lại thành công!

Chạy lại chương trình:

go run .

Bạn sẽ thấy những thông tin sau:

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

Để xem lại, chúng ta vừa khai báo một biến cho lỗi, gán cho biến đó một bộ mô tả loại, rồi tham chiếu đến biến đó trong quá trình đánh giá biểu thức.

7. VÀ/HOẶC logic

Một trong những tính năng độc đáo hơn của CEL là việc sử dụng các toán tử logic giao hoán. Cả hai phía của nhánh có điều kiện đều có thể rút ngắn quá trình đánh giá, ngay cả khi gặp lỗi hoặc dữ liệu đầu vào không đầy đủ.

Nói cách khác, CEL sẽ tìm một thứ tự đánh giá cho ra kết quả bất cứ khi nào có thể, bỏ qua các lỗi hoặc thậm chí dữ liệu bị thiếu có thể xảy ra trong các thứ tự đánh giá khác. Các ứng dụng có thể dựa vào thuộc tính này để giảm thiểu chi phí đánh giá, hoãn thu thập các dữ liệu đầu vào tốn kém khi có thể đạt được kết quả mà không cần đến chúng.

Chúng ta sẽ thêm một ví dụ về AND/OR, sau đó thử với các đầu vào khác nhau để hiểu cách CEL đánh giá đoản mạch.

Tạo hàm

Trong trình chỉnh sửa, hãy thêm nội dung sau vào bài tập 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"),
    ),
  )
}

Tiếp theo, hãy thêm câu lệnh OR này. Câu lệnh này sẽ trả về giá trị true nếu người dùng là thành viên của nhóm admin hoặc có một giá trị nhận dạng email cụ thể:

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

Cuối cùng, hãy thêm trường hợp eval để đánh giá người dùng bằng một tập hợp các câu lệnh trống:

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

Chạy mã với tập hợp xác nhận quyền sở hữu trống

Khi chạy lại chương trình, bạn sẽ thấy kết quả mới sau đây:

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

Cập nhật trường hợp đánh giá

Tiếp theo, hãy cập nhật trường hợp đánh giá để truyền vào một đối tượng chính khác với tập hợp xác nhận quyền sở hữu trống:

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

Chạy mã theo thời gian

Chạy lại chương trình,

go run .

bạn sẽ thấy lỗi sau:

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

Trong protobuf, chúng ta biết những trường và loại cần dự kiến. Trong các giá trị bản đồ và JSON, chúng tôi không biết liệu một khoá có xuất hiện hay không. Vì không có giá trị mặc định an toàn cho khoá bị thiếu, nên CEL mặc định là lỗi.

8. Hàm tuỳ chỉnh

Mặc dù CEL có nhiều hàm tích hợp sẵn, nhưng đôi khi bạn cần dùng một hàm tuỳ chỉnh. Ví dụ: bạn có thể dùng các hàm tuỳ chỉnh để cải thiện trải nghiệm người dùng cho các điều kiện phổ biến hoặc hiển thị trạng thái theo ngữ cảnh

Trong bài tập này, chúng ta sẽ khám phá cách hiển thị một hàm để đóng gói cùng các quy trình kiểm tra thường dùng.

Gọi một hàm tuỳ chỉnh

Trước tiên, hãy tạo mã để thiết lập một phương thức ghi đè có tên là contains. Phương thức này sẽ xác định xem một khoá có tồn tại trong bản đồ và có một giá trị cụ thể hay không. Để lại phần giữ chỗ cho định nghĩa hàm và liên kết hàm:

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

Chạy mã và tìm hiểu lỗi

Khi chạy lại mã, bạn sẽ thấy lỗi sau:

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

Để khắc phục lỗi này, chúng ta cần thêm hàm contains vào danh sách các khai báo hiện đang khai báo biến yêu cầu.

Khai báo một loại được tham số hoá bằng cách thêm 3 dòng sau. (Điều này phức tạp như mọi hàm nạp chồng sẽ có đối với 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()
}

Thêm hàm tuỳ chỉnh

Tiếp theo, chúng ta sẽ thêm một hàm chứa mới sử dụng các loại được tham số hoá:

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

Chạy chương trình để tìm hiểu về lỗi

Chạy bài tập. Bạn sẽ thấy lỗi sau đây về hàm thời gian chạy bị thiếu:

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

Cung cấp hoạt động triển khai hàm cho khai báo NewEnv bằng hàm 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()
}

Giờ đây, chương trình sẽ chạy thành công:

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

Điều gì xảy ra khi thông báo xác nhận quyền sở hữu tồn tại?

Để được cộng điểm, hãy thử đặt yêu cầu quản trị trên đầu vào để xác minh rằng hàm chứa nạp chồng cũng trả về giá trị true khi yêu cầu tồn tại. Bạn sẽ thấy kết quả sau đây:

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

Trước khi chuyển sang phần tiếp theo, bạn nên kiểm tra chính hàm 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])
}

Để mang lại sự dễ dàng mở rộng nhất, chữ ký cho các hàm tuỳ chỉnh dự kiến sẽ là các đối số thuộc loại ref.Val. Điểm đánh đổi ở đây là sự dễ dàng của việc mở rộng sẽ tạo thêm gánh nặng cho người triển khai để đảm bảo tất cả các loại giá trị đều được xử lý đúng cách. Khi các loại hoặc số lượng đối số đầu vào không khớp với khai báo hàm, lỗi no such overload sẽ được trả về.

cel.FunctionBinding() thêm một cơ chế bảo vệ loại thời gian chạy để đảm bảo rằng hợp đồng thời gian chạy khớp với khai báo đã kiểm tra loại trong môi trường.

9. Tạo JSON

CEL cũng có thể tạo ra các đầu ra không phải là boolean, chẳng hạn như JSON. Thêm nội dung sau vào hàm của bạn:

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

Chạy mã

Khi chạy lại mã, bạn sẽ thấy lỗi sau:

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

Thêm một khai báo cho biến now thuộc loại cel.TimestampType vào cel.NewEnv() rồi chạy lại:

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

Chạy lại mã và mã sẽ thành công:

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

Chương trình chạy, nhưng giá trị đầu ra out cần được chuyển đổi rõ ràng sang JSON. Trong trường hợp này, biểu diễn CEL nội bộ có thể chuyển đổi JSON vì biểu diễn này chỉ đề cập đến các loại mà JSON có thể hỗ trợ hoặc có ánh xạ Proto sang JSON đã biết.

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

Sau khi chuyển đổi loại bằng hàm trợ giúp valueToJSON trong tệp codelab.go, bạn sẽ thấy đầu ra bổ sung sau:

------ 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. Xây dựng nguyên mẫu

CEL có thể tạo thông báo protobuf cho mọi loại thông báo được biên dịch vào ứng dụng. Thêm hàm để tạo google.rpc.context.AttributeContext.Request từ jwt đầu vào

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

Chạy mã

Khi chạy lại mã, bạn sẽ thấy lỗi sau:

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

Về cơ bản, vùng chứa tương đương với một không gian tên hoặc gói, nhưng thậm chí có thể chi tiết đến mức là tên thông báo protobuf. Vùng chứa CEL sử dụng các quy tắc phân giải không gian tên giống như Protobuf và C++ để xác định vị trí khai báo một biến, hàm hoặc tên kiểu nhất định.

Với vùng chứa google.rpc.context.AttributeContext, trình kiểm tra kiểu và trình đánh giá sẽ thử các tên nhận dạng sau cho tất cả các biến, kiểu và hàm:

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

Đối với tên tuyệt đối, hãy thêm tiền tố dấu chấm vào biến, loại hoặc tham chiếu hàm. Trong ví dụ này, biểu thức .<id> sẽ chỉ tìm kiếm mã nhận dạng <id> cấp cao nhất mà không kiểm tra trong vùng chứa trước.

Hãy thử chỉ định lựa chọn cel.Container("google.rpc.context.AttributeContext") cho môi trường CEL rồi chạy lại:

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

 ...
}

Bạn sẽ nhận được kết quả sau:

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

... và nhiều lỗi khác...

Tiếp theo, hãy khai báo các biến jwtnow, chương trình sẽ chạy như mong đợi:

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

Để được cộng điểm, bạn cũng nên giải gói loại bằng cách gọi out.Value() trên thông báo để xem kết quả thay đổi như thế nào.

11. Macro

Bạn có thể dùng macro để thao tác chương trình CEL tại thời gian phân tích cú pháp. Macro khớp với chữ ký lệnh gọi và thao tác với lệnh gọi đầu vào cũng như các đối số của lệnh gọi đó để tạo ra một AST biểu thức con mới.

Bạn có thể dùng macro để triển khai logic phức tạp trong AST mà bạn không thể viết trực tiếp trong CEL. Ví dụ: macro has cho phép kiểm tra sự hiện diện của trường. Các macro thấu hiểu như exists và all sẽ thay thế một lệnh gọi hàm bằng quá trình lặp có giới hạn trên danh sách hoặc bản đồ đầu vào. Không có khái niệm nào có thể thực hiện ở cấp độ cú pháp, nhưng có thể thực hiện thông qua việc mở rộng macro.

Thêm và chạy bài tập tiếp theo:

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

Bạn sẽ thấy các lỗi sau:

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

... và nhiều người khác ...

Những lỗi này xảy ra do bạn chưa bật macro. Để bật macro, hãy xoá cel.ClearMacros() rồi chạy lại:

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

Sau đây là các macro hiện được hỗ trợ:

Macro

Chữ ký

Mô tả

tất cả

r.all(var, cond)

Kiểm tra xem cond có đánh giá là true cho tất cả var trong dải ô r hay không.

tồn tại

r.exists(var, cond)

Kiểm tra xem cond có đánh giá là true cho bất kỳ var nào trong dải ô r hay không.

exists_one

r.exists_one(var, cond)

Kiểm tra xem cond có đánh giá là true cho chỉ một var trong phạm vi r hay không.

filter

r.filter(var, cond)

Đối với danh sách, hãy tạo một danh sách mới, trong đó mỗi phần tử var trong phạm vi r đều thoả mãn điều kiện cond. Đối với bản đồ, hãy tạo một danh sách mới trong đó mỗi khoá var trong phạm vi r đều thoả mãn điều kiện cond.

map

r.map(var, expr)

Tạo một danh sách mới, trong đó mỗi biến trong dải ô r được chuyển đổi bằng biểu thức expr.

r.map(var, cond, expr)

Tương tự như hàm map hai đối số nhưng có bộ lọc cond có điều kiện trước khi giá trị được chuyển đổi.

has(a.b)

Kiểm tra sự hiện diện của b trên giá trị a : Đối với bản đồ, định nghĩa kiểm thử json. Đối với các giao thức, hãy kiểm thử giá trị nguyên thuỷ không mặc định hoặc một trường thông báo tập hợp.

Khi đối số phạm vi r là loại map, var sẽ là khoá bản đồ và đối với các giá trị loại list, var sẽ là giá trị phần tử danh sách. Các macro all, exists, exists_one, filtermap thực hiện một thao tác ghi lại AST, thao tác này thực hiện một lần lặp lại cho từng phần tử bị giới hạn theo kích thước của dữ liệu đầu vào.

Các hàm hiểu có giới hạn đảm bảo rằng các chương trình CEL sẽ không hoàn chỉnh theo Turing, nhưng chúng đánh giá trong thời gian siêu tuyến tính so với đầu vào. Hãy sử dụng các macro này một cách tiết kiệm hoặc hoàn toàn không sử dụng. Việc sử dụng nhiều comprehensions thường là một chỉ báo tốt cho thấy hàm tuỳ chỉnh sẽ mang lại trải nghiệm người dùng tốt hơn và hiệu suất cao hơn.

12. Chỉnh

Hiện tại, có một số tính năng chỉ có trong CEL-Go, nhưng những tính năng này cho thấy các kế hoạch trong tương lai đối với những cách triển khai CEL khác. Bài tập sau đây minh hoạ các kế hoạch chương trình khác nhau cho cùng một 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

Khi cờ tối ưu hoá được bật, CEL sẽ dành thêm thời gian để tạo danh sách và ánh xạ các giá trị cố định trước thời hạn và tối ưu hoá một số lệnh gọi nhất định (chẳng hạn như toán tử in) để trở thành một kiểm thử thành viên tập hợp thực:

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

Khi cùng một chương trình được đánh giá nhiều lần dựa trên các đầu vào khác nhau, thì việc tối ưu hoá là một lựa chọn phù hợp. Tuy nhiên, khi chương trình chỉ được đánh giá một lần, việc tối ưu hoá sẽ chỉ làm tăng thêm chi phí.

Đánh giá toàn diện

Exhaustive Eval có thể hữu ích cho việc gỡ lỗi hành vi đánh giá biểu thức vì nó cung cấp thông tin chi tiết về giá trị quan sát được ở mỗi bước trong quá trình đánh giá biểu thức.

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

Bạn sẽ thấy danh sách trạng thái đánh giá biểu thức cho từng mã nhận dạng biểu thức:

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

Giá trị nhận dạng biểu thức 2 tương ứng với kết quả của toán tử in trong nhánh đầu tiên và giá trị nhận dạng biểu thức 11 tương ứng với toán tử == trong nhánh thứ hai. Trong quá trình đánh giá thông thường, biểu thức sẽ được rút gọn sau khi tính toán xong 2. Nếu y không phải là uint, thì trạng thái sẽ cho thấy 2 lý do khiến biểu thức không thành công chứ không chỉ 1.

13. Nội dung được đề cập là gì?

Nếu bạn cần một công cụ biểu thức, hãy cân nhắc sử dụng CEL. CEL là lựa chọn lý tưởng cho những dự án cần thực thi cấu hình người dùng trong đó hiệu suất là yếu tố quan trọng.

Trong các bài tập trước, chúng tôi hy vọng bạn đã quen với việc truyền dữ liệu vào CEL và nhận lại đầu ra hoặc quyết định.

Chúng tôi hy vọng bạn đã nắm được các loại thao tác mà bạn có thể thực hiện, từ quyết định boolean đến việc tạo thông báo JSON và Protobuffer.

Chúng tôi hy vọng bạn đã nắm được cách sử dụng các biểu thức và chức năng của chúng. Chúng tôi cũng hiểu rõ những cách phổ biến để kéo dài thời gian này.