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à ngôn ngữ biểu thức hoàn chỉnh không có tính năng Lưu trữ, được thiết kế để thực thi nhanh, di động và an toàn. CEL có thể được sử dụng riêng hoặc nhúng vào một sản phẩm lớn hơn.

CEL được thiết kế làm ngôn ngữ an toàn khi thực thi mã người dùng. Mặc dù việc gọi eval() trên mã python của người dùng là rất 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 sẽ làm cho nó kém hiệu quả hơn, nên CEL sẽ đánh giá một cách an toàn theo thứ tự từ nano giây đến micrô giây; lý tưởng cho các ứng dụng yêu cầu hiệu suất cao.

CEL đánh giá các biểu thức, tương tự như 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 cũng có thể dùng để 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 tính bằng nano giây đến micrô giây, nên trường hợp sử dụng lý tưởng cho CEL là các ứng dụng có các đường dẫn quan trọng về hiệu suất. Không nên biên dịch mã CEL vào AST trong các đường dẫn quan trọng; ứng dụng lý tưởng là những ứng dụng trong đó cấu hình được thực thi thường xuyên và được 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 theo mỗi yêu cầu HTTP đến 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ẽ ảnh hưở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 boolean cho biết yêu cầu có được cho phép hay không, nhưng có thể trả về một thông báo phức tạp hơn.

Lớp học lập trình này gồm những nội dung gì?

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

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 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 của CEL
  • Xin chào mọi người: Sử dụng CEL để đánh giá một Chuỗi
  • Tạo biến
  • Tìm hiểu về sự cố đoản mạch của CEL trong các phép toán logic AND/OR
  • Cách sử dụng CEL để tạo JSON
  • Cách sử dụng CEL để tạo Bộ đệm Protobuffer
  • Tạo macro
  • Các cách điều chỉnh biểu cảm 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ề Vùng đệm giao thứcGo Lang.

Nếu bạn chưa quen với Vùng đệm giao thức, bài tập đầu tiên sẽ giúp bạn hiểu cách hoạt động của CEL, nhưng vì các ví dụ nâng cao hơn sử dụng Vùng đệm giao thức làm dữ liệu đầu vào vào CEL nên có thể khó hiểu hơn. Trước tiên, hãy cân nhắc thực hiện một trong các hướng dẫn sau. Lưu ý rằng Vùng đệm giao thức không bắt buộc phải sử dụng CEL như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 chưa bằng cách chạy:

go --help

2. Khái niệm chính

Ứng dụng

CEL là mục đích chung và được dùng cho nhiều ứng dụng, từ việc định tuyến RPC cho đến việc 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 biên dịch một lần và đánh giá nhiều quy trình.

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 tạo ra quyết định truy cập dựa trên một 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 trong việc làm tròn 20% còn lại khi người dùng cần có sức mạnh biểu đạt cao hơn.

Biên dịch

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

Cụm từ

Người dùng xác định biểu thức; các dịch vụ và ứng dụng sẽ xác định môi trường nơi chúng 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 dành cho CEL được nhập tự động.

Trong ví dụ sau, biểu thức nhận một đối tượng yêu cầu và yêu cầu 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 hợp lệ 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

Các môi trường do các dịch vụ xác định. 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.

Các nội dung khai báo dựa trên proto được trình kiểm tra kiểu CEL sử dụng để đảm bảo rằng mọi tham chiếu hàm và giá trị nhận dạng 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ó ba giai đoạn khi xử lý 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 phẳng điều khiển phân tích cú pháp và kiểm tra biểu thức tại thời điểm cấu hình và lưu trữ AST.

c71fc08068759f81.png

Trong thời gian chạy, mặt phẳng dữ liệu sẽ truy xuất và đánh giá AST nhiều lần. CEL được tối ưu hoá để tăng hiệu quả khi chạy, nhưng bạn 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 có độ trễ.

49ab7d8517143b66.pngS

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 cây cú pháp trừu tượng bằng cách sử dụng ngữ pháp từ vựng / trình phân tích cú pháp ANTLR. Giai đoạn phân tích cú pháp sẽ tạo ra một cây cú pháp trừu tượng dựa trên proto, trong đó mỗi nút Expr trong AST chứa một mã nhận dạng số nguyên dùng để lập chỉ mục 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 ra trong quá trình phân tích cú pháp thể hiện một cách trung thực nội dung trình bày trừu tượng về nội dung được nhập ở dạng chuỗi của biểu thức.

Sau khi phân tích cú pháp một biểu thức, có thể chúng tôi sẽ kiểm tra biểu thức đó theo môi trường để đảm bảo rằng 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à được sử dụng đúng cách. Trình kiểm tra loại tạo ra checked.proto bao gồm siêu dữ liệu độ phân giải loại, biến và hàm có thể cải thiện đáng kể hiệu quả đánh giá.

Người đánh giá CEL cần 3 điều sau:

  • 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 nội dung dùng để biên dịch AST. Bạn có thể sử dụng lại bất kỳ dữ liệu đầu vào nào trong số này trong nhiều hoạt động đánh giá, chẳng hạn như một AST được đánh giá trên nhiều tập hợp các mối liên kết biến, hoặc các biến tương tự đượ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ã dành 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 này có trong thư mục codelab/solution của cùng một kho lưu trữ.

Sao chép và cd vào 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 sẽ hỗ trợ các giai đoạn đánh giá CEL:

  • Hàm Compile: Phân tích cú pháp và kiểm tra cũng như biểu thức đầu vào so 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 dữ liệu đầu vào
  • Hàm Report: Tạo bản in đẹp cho kết quả đánh giá

Ngoài ra, trình trợ giúp requestauth cũng được cung cấp để hỗ trợ quá trình thiết lập đầu vào cho các bài tập khác nhau.

Bài tập sẽ tham chiếu đến các gói theo tên gói ngắn. Bạn có thể liên kết từ gói đến vị trí nguồn trong kho lưu trữ google/cel-go dưới đây nếu muốn tìm hiểu chi tiết hơn:

Gói

Vị trí nguồn

Mô tả

cel

cel-go/cel

Giao diện cấp cao nhất

tham chiếu

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 tất cả các 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 phần khai báo exercise1 rồi điền vào nội dung 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
}

Ứ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 chuẩn.

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

Môi trường CEL chuẩn hỗ trợ tất cả các loại, toán tử, hàm và macro được xác định trong thông số kỹ thuật của ngôn ngữ.

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

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

// 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 được lệnh gọi ParseCheck trả về là danh sách các vấn đề có thể bị lỗi. Nếu iss.Err() không phải là 0, thì tức là đã xảy ra lỗi 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 định dạng đúng, kết quả của các lệnh gọi này là một cel.Ast có thể thực thi.

Ước tính biểu thức

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

Sau khi lên 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 đánh giá chi tiết và trạng thái lỗi.

Thêm kế hoạch lập 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()
}

Để ngắn gọn, chúng tôi sẽ bỏ các trường hợp lỗi nêu trên khỏi các bài tập trong tương lai.

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 phần giữ chỗ cho các bài tập sau này.

=== Exercise 1: Hello World ===

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

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

5. Sử dụng các biến trong một hàm

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

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 gặp phải bằng cách khai báo cho đối tượng yêu cầu dưới dạng một 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 kiểu cũng cần biết mã mô tả kiểu.

Sử dụng cel.Types để xác định chỉ số mô tả cho yêu cầu trong hàm của bạn:

// 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 chỉ cần khai báo một biến cho một lỗi, gán mã mô tả kiểu cho biến đó, sau đó tham chiếu biến đó trong quá trình đánh giá biểu thức.

7. Logic VÀ/HOẶC

Một trong những tính năng độc đáo hơn của CEL là sử dụng toán tử logic giao hoán. Một trong hai bên của nhánh có điều kiện có thể làm đoản mạch quá trình đánh giá, ngay cả khi xảy ra lỗi hoặc dữ liệu đầu vào một phần.

Nói cách khác, CEL sẽ tìm một thứ tự đánh giá để đưa ra kết quả bất cứ khi nào có thể, bỏ qua các lỗi hoặc thậm chí là 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á, trì hoãn việc thu thập dữ liệu đầu vào tốn kém khi có thể đạt được kết quả mà không cần chúng.

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

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 thông báo 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("user:me@acme.co", emptyClaims), time.Now()))
}

Chạy mã có tập hợp thông báo xác nhận quyền sở hữu trống

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 một đối tượng chính khác có tập hợp thông báo 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ã có 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 có những trường và loại nào. Trong các giá trị tệp ánh xạ và tệp json, chúng tôi không biết liệu 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 có 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 hàm tuỳ chỉnh sẽ hữu ích. Ví dụ: các hàm tuỳ chỉnh có thể được dùng để cải thiện trải nghiệm người dùng trong các điều kiện phổ biến hoặc hiển thị trạng thái theo bối cảnh

Trong bài tập này, chúng ta sẽ tìm hiểu cách hiển thị một hàm để đóng gói các bước 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 cơ chế ghi đè có tên là contains. Cơ chế này sẽ xác định xem khoá có tồn tại trong tệp ánh xạ 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')
 | ............................^

Để sửa lỗi, chúng ta cần thêm hàm contains vào danh sách nội dung 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ố bằng cách thêm 3 dòng sau. (Việc này phức tạp như mọi hàm quá tải đều áp dụng cho 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. Hàm này sẽ sử dụng các kiểu có tham số:

// 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 lỗi

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

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

Cung cấp phương thức triển khai hàm cho phần 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()
}

Hiện tại, 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ì sẽ xảy ra khi thông báo xác nhận quyền sở hữu tồn tại?

Để có thêm tín dụng, hãy thử thiết lập xác nhận quyền sở hữu quản trị đối với thông tin đầu vào để xác minh rằng hàm chứa quá tải cũng trả về true khi có thông báo xác nhận quyền sở hữu. 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 tiếp tục, 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])
}

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

cel.FunctionBinding() thêm một tính năng bảo vệ kiểu trong thời gian chạy để đảm bảo hợp đồng thời gian chạy khớp với nội dung khai báo đã kiểm tra kiểu trong môi trường.

9. Xây dựng JSON

CEL cũng có thể tạo ra dữ liệu đầu ra không phải boolean, chẳng hạn như JSON. Thêm đoạn mã sau vào hàm:

// 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 phần 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 sẽ chạy, nhưng bạn cần chuyển đổi rõ ràng giá trị đầu ra out thành JSON. Cách trình bày CEL nội bộ trong trường hợp này là có thể chuyển đổi JSON vì nó chỉ tham chiếu đến các loại mà JSON có thể hỗ trợ hoặc có một phương thức ánh xạ Proto sang JSON nổi tiếng.

// 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 này bằng hàm trợ giúp valueToJSON trong tệp codelab.go, bạn sẽ thấy kết quả 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 giao thức

CEL có thể tạo thông báo protobuf cho bất kỳ loại thông báo nào được biên dịch vào ứng dụng. Thêm hàm để tạo google.rpc.context.AttributeContext.Request từ giá trị đầu vào 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()
}

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 này tương đương với không gian tên hoặc gói, nhưng thậm chí còn có thể chi tiết như tên thông báo protobuf. Vùng chứa CEL sử dụng các quy tắc giải quyết không gian tên giống như Protobuf và C++ để xác định vị trí khai báo tên biến, hàm hoặc loại 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 đây cho tất 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 một dấu chấm ở đầu vào đầu giá trị tham chiếu biến, loại hoặc hàm. Trong ví dụ, biểu thức .<id> sẽ chỉ tìm kiếm giá trị nhận dạng <id> cấp cao nhất mà không kiểm tra trước trong vùng chứa.

Hãy thử chỉ định tuỳ 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 rồi chương trình sẽ chạy như dự kiến:

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

Để tăng giá trị đóng góp, bạn cũng nên khám phá loại này 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

Có thể dùng macro để thao tác với chương trình CEL tại thời điểm phân tích cú pháp. Macro so 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 phụ mới.

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

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 vì macro chưa được bật. Để bật macro, hãy xoá cel.ClearMacros() và 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)

Dưới đâ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ó giá trị true cho all (tất cả) trong dải ô r hay không.

tồn tại

r.exists(var; cond)

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

exists_one

r.exists_one(var; cond)

Kiểm tra xem cond có giá trị true cho chỉ một var trong dải ô 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 dải ô r đáp ứng điều kiện điều kiện. Đối với bản đồ, hãy tạo một danh sách mới, trong đó mỗi biến khoá trong dải ô r đáp ứng điều kiện điều kiện.

map

r.map(var; expr)

Tạo một danh sách mới, trong đó mỗi var trong dải ô r được biến đổi bởi expr.

r.map(var; cond; expr)

Giống như ánh xạ hai đối số nhưng với bộ lọc cond có điều kiện trước khi giá trị được biế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 của hoạt động kiểm tra json. Đối với các protos, hãy kiểm thử giá trị gốc không mặc định, hoặc một hoặc một trường thông báo đã đặt.

Khi đối số dải ô r là loại map, var sẽ là khoá ánh xạ và đối với các giá trị loại list, var sẽ là giá trị phần tử trong danh sách. Các macro all, exists, exists_one, filtermap thực hiện việc viết lại AST, thực hiện một lần lặp cho mỗi lần bị giới hạn bởi kích thước của đầu vào.

Mức hiểu rõ ràng buộc đảm bảo rằng các chương trình CEL sẽ không hoàn chỉnh được Turing, nhưng các chương trình này đánh giá dữ liệu đầu vào theo thời gian siêu tuyến tính. Sử dụng các macro này một cách thận trọng hoặc hoàn toàn không sử dụng. Việc sử dụng nhiều nội dung hiểu thường là một chỉ báo tốt cho thấy chức năng 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ỉ dành riêng cho CEL-Go, nhưng đó là những tính năng cho biết những kế hoạch triển khai CEL khác trong tương lai. Bài tập sau đây trình bày các kế hoạch chương trình 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 để xây dựng danh sách và liên kết giá trị cố định trước thời hạn, đồng thời tối ưu hoá một số lệnh gọi nhất định, chẳng hạn như toán tử in thành một kiểm thử thành viên tập hợp đúng:

    // 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 so với các dữ liệu đầu vào khác nhau, thì việc tối ưu hoá là một lựa chọn tốt. Tuy nhiên, khi chương trình chỉ được đánh giá một lần, thì việc tối ưu hoá sẽ chỉ tăng thêm chi phí.

Hết sức

Eval hết có thể hữu ích để 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ã 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)

Id biểu thức 2 tương ứng với kết quả của toán tử in trong toán tử đầu tiên. nhánh và biểu thức id 11 tương ứng với toán tử == trong giây. Theo đánh giá bình thường, biểu thức sẽ bị đoản mạch sau khi 2 được tính toán. Nếu y không phải là uint, thì trạng thái sẽ hiển thị hai lý do khiến biểu thức không thành công chứ không chỉ một.

13. Nội dung bảo hiểm

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 cảm thấy thoải mái khi truyền dữ liệu của mình vào CEL và nhận lại kết quả hoặc quyết định.

Chúng tôi hy vọng bạn đã hiểu rõ các loại thao tác mà bạn có thể thực hiện, bất cứ điều gì từ quyết định boolean cho đến việc tạo thông báo JSON và Protobuffer.

Chúng tôi hy vọng bạn đã biết cách sử dụng biểu thức cũng như biết tác dụng của biểu thức. Và chúng tôi hiểu những cách phổ biến để mở rộng phạm vi tiếp cận.