CEL-Go Codelab: 빠르고 안전한 삽입 표현식

1. 소개

CEL이란 무엇인가요?

CEL은 빠르고 이식 가능하며 안전하게 실행하도록 설계된 비 Turing의 완전 표현식 언어입니다. CEL은 단독으로 사용하거나 더 큰 제품에 삽입할 수 있습니다.

CEL은 사용자 코드를 안전하게 실행할 수 있는 언어로 설계되었습니다. 사용자의 Python 코드에서 맹목적으로 eval()를 호출하는 것은 위험하지만 사용자의 CEL 코드는 안전하게 실행할 수 있습니다. CEL은 성능을 저하시키는 동작을 방지하므로 나노초에서 마이크로초까지 안전하게 평가됩니다. 성능이 중요한 애플리케이션에 이상적입니다

CEL은 표현식을 평가하는데, 이는 단선형 함수 또는 람다 표현식과 유사합니다. CEL은 일반적으로 부울 결정에 사용되지만 JSON이나 protobuf 메시지와 같은 더 복잡한 객체를 구성하는 데 사용할 수도 있습니다.

CEL이 프로젝트에 적합한가요?

CEL은 AST의 표현식을 나노초에서 마이크로초 단위로 평가하므로 CEL의 이상적인 사용 사례는 성능이 중요한 경로를 사용하는 애플리케이션입니다. 주요 경로에서 CEL 코드를 AST로 컴파일하면 안 됩니다. 이상적인 애플리케이션은 구성이 자주 실행되고 비교적 드물게 수정되는 애플리케이션입니다.

예를 들어 서비스에 대한 각 HTTP 요청과 함께 보안 정책을 실행하는 것이 CEL의 이상적인 사용 사례입니다. 보안 정책이 거의 변경되지 않고 CEL이 응답 시간에 거의 영향을 주지 않기 때문입니다. 이 경우 CEL은 요청이 허용되어야 하는지 여부에 대한 부울 값을 반환하지만 더 복잡한 메시지를 반환할 수도 있습니다.

이 Codelab에서 다루는 내용

이 Codelab의 첫 번째 단계에서는 CEL 사용의 동기와 핵심 개념을 살펴봅니다. 나머지는 일반적인 사용 사례를 다루는 실습 코딩을 위한 것입니다. 언어, 시맨틱, 기능에 관한 자세한 내용은 GitHub의 CEL 언어 정의CEL Go 문서를 참고하세요.

이 Codelab은 이미 CEL을 지원하는 서비스를 사용하기 위해 CEL을 배우려는 개발자를 대상으로 합니다. 이 Codelab에서는 CEL을 자체 프로젝트에 통합하는 방법은 다루지 않습니다.

학습할 내용

  • CEL의 핵심 개념
  • Hello, World: CEL을 사용하여 문자열 평가
  • 변수 만들기
  • 논리곱(AND/OR) 연산에서 CEL의 단락 이해하기
  • CEL을 사용하여 JSON을 빌드하는 방법
  • CEL을 사용하여 Protobuffer를 빌드하는 방법
  • 매크로 만들기
  • CEL 표현식을 조정하는 방법

필요한 항목

기본 요건

이 Codelab은 프로토콜 버퍼Go Lang에 관한 기본적인 이해를 바탕으로 합니다.

프로토콜 버퍼에 익숙하지 않다면 첫 번째 실습에서 CEL의 작동 방식을 익힐 수 있지만, 고급 예의 경우 CEL에 입력으로 프로토콜 버퍼를 사용하기 때문에 이해하기 어려울 수 있습니다. 먼저 이러한 튜토리얼 중 하나를 시도해 보세요. 프로토콜 버퍼는 CEL을 사용할 필요가 없지만 이 Codelab에서 광범위하게 사용됩니다.

다음을 실행하여 go가 설치되었는지 테스트할 수 있습니다.

go --help

2. 주요 개념

애플리케이션

CEL은 범용이며 RPC 라우팅부터 보안 정책 정의에 이르기까지 다양한 애플리케이션에 사용되고 있습니다. CEL은 확장 가능하고 애플리케이션에 구애받지 않으며 컴파일 1회, 평가 많음 워크플로에 최적화되어 있습니다.

많은 서비스와 애플리케이션이 선언적 구성을 평가합니다. 예를 들어 역할 기반 액세스 제어 (RBAC)는 역할 및 사용자 집합에 따라 액세스 권한을 결정하는 선언적 구성입니다. 선언적 구성이 80% 사용 사례라면 CEL은 사용자가 더 많은 표현력을 발휘해야 할 때 나머지 20% 를 반올림하는 데 유용한 도구입니다.

컴파일

표현식은 환경에 대해 컴파일됩니다. 컴파일 단계에서는 protobuf 형식으로 추상 구문 트리 (AST)를 생성합니다. 컴파일된 표현식은 일반적으로 최대한 빠르게 평가를 유지하기 위해 나중에 사용할 수 있도록 저장됩니다. 컴파일된 단일 표현식은 다양한 입력으로 평가할 수 있습니다.

표현식

사용자가 표현식을 정의합니다. 서비스가 실행되는 환경을 정의합니다 함수 서명은 입력을 선언하며 CEL 표현식 외부에서 작성됩니다. CEL에서 사용할 수 있는 함수 라이브러리는 자동으로 가져옵니다.

다음 예시에서 표현식은 요청 객체를 가져오며 요청에 클레임 토큰이 포함됩니다. 표현식은 클레임 토큰이 여전히 유효한지 나타내는 불리언을 반환합니다.

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

환경

환경은 서비스로 정의됩니다. CEL을 삽입하는 서비스 및 애플리케이션은 표현식 환경을 선언합니다. 환경은 표현식에서 사용할 수 있는 변수와 함수의 모음입니다.

proto 기반 선언은 CEL 유형 검사기에서 표현식 내의 모든 식별자와 함수 참조가 올바르게 선언되고 사용되도록 하는 데 사용됩니다.

표현식 파싱의 3단계

표현식 처리에는 파싱, 확인, 평가의 세 단계가 있습니다. CEL의 가장 일반적인 패턴은 제어 영역이 구성 시 표현식을 파싱하고 확인하고 AST를 저장하는 것입니다.

c71fc08068759f81.png

런타임 시 데이터 영역은 AST를 반복적으로 검색하고 평가합니다. CEL은 런타임 효율성에 최적화되어 있지만 지연 시간이 중요한 코드 경로에서 파싱 및 검사를 실행하면 안 됩니다.

49ab7d8517143b66.png

CEL은 ANTLR 렉서 / 파서 문법을 사용하여 사람이 읽을 수 있는 표현식에서 추상 구문 트리로 파싱됩니다. 파싱 단계에서는 proto 기반 추상 구문 트리를 내보냅니다. 여기서 AST의 각 Expr 노드에는 파싱 및 확인 중에 생성된 메타데이터로 색인을 생성하는 데 사용되는 정수 ID가 포함됩니다. 파싱 중에 생성된 syntax.proto는 표현식의 문자열 형식으로 입력된 내용의 추상적인 표현을 충실하게 표현합니다.

표현식이 파싱된 후에는 표현식의 모든 변수 및 함수 식별자가 선언되었고 올바르게 사용되고 있는지 환경을 대상으로 검사할 수 있습니다. 유형 검사기는 평가 효율성을 크게 개선할 수 있는 유형, 변수, 함수 확인 메타데이터가 포함된 checked.proto를 생성합니다.

CEL 평가자는 다음 3가지 항목이 필요합니다.

  • 모든 커스텀 확장 프로그램의 함수 바인딩
  • 변수 결합
  • 평가할 AST

함수 및 변수 바인딩은 AST를 컴파일하는 데 사용된 바인딩과 일치해야 합니다. 이러한 입력 중 어느 것이든 여러 평가에서 재사용할 수 있습니다. AST를 여러 변수 바인딩 집합에서 평가하거나, 여러 AST에 대해 동일한 변수를 평가하거나, 프로세스의 전체 기간 동안 사용되는 함수 바인딩 (일반적인 경우)을 예로 들 수 있습니다.

3. 설정

이 Codelab의 코드는 cel-go 저장소의 codelab 폴더에 있습니다. 이 솔루션은 동일한 저장소의 codelab/solution 폴더에서 사용할 수 있습니다.

저장소에 클론하고 cd를 사용합니다.

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

go run를 사용하여 코드를 실행합니다.

go run .

다음과 같은 출력이 표시됩니다.

=== Exercise 1: Hello World ===

=== Exercise 2: Variables ===

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

=== Exercise 4: Customization ===

=== Exercise 5: Building JSON ===

=== Exercise 6: Building Protos ===

=== Exercise 7: Macros ===

=== Exercise 8: Tuning ===

CEL 패키지는 어디에 있나요?

편집기에서 codelab/codelab.go을 엽니다. 이 Codelab에서 연습 실행을 구동하는 main 함수와 3개의 도우미 함수 블록이 차례로 표시됩니다. 첫 번째 도우미 세트는 CEL 평가 단계를 지원합니다.

  • Compile 함수: 환경을 기준으로 표현식을 파싱하고 검사하고 입력
  • Eval 함수: 입력과 비교하여 컴파일된 프로그램을 평가합니다.
  • Report 함수: 평가 결과를 프리티 출력합니다.

또한 requestauth 도우미가 다양한 연습의 입력 구성을 지원하기 위해 제공되었습니다.

연습에서는 짧은 패키지 이름으로 패키지를 참조합니다. google/cel-go 저장소 내의 패키지에서 소스 위치로의 매핑은 세부정보를 자세히 살펴보려면 다음과 같습니다.

패키지

소스 위치

설명

cel

cel-go/cel

최상위 인터페이스

ref

cel-go/common/types/ref

참조 인터페이스

유형

cel-go/common/types

런타임 유형 값

4. 여러분, 안녕하세요?

모든 프로그래밍 언어의 전통에서는 먼저 'Hello World!'를 만들고 평가합니다.

환경 구성

편집기에서 exercise1 선언을 찾고 다음을 작성하여 환경을 설정합니다.

// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
    fmt.Println("=== Exercise 1: Hello World ===\n")
    // Create the standard environment.
    env, err := cel.NewEnv()
    if err != nil {
        glog.Exitf("env error: %v", err)
    }
    // Will add the parse and check steps here
}

CEL 애플리케이션은 환경을 기준으로 표현식을 평가합니다. env, err := cel.NewEnv()는 표준 환경을 구성합니다.

통화에 cel.EnvOption 옵션을 제공하여 환경을 맞춤설정할 수 있습니다. 이러한 옵션으로는 매크로를 사용 중지하고 맞춤 변수 및 함수 등을 선언할 수 있습니다.

표준 CEL 환경은 언어 사양 내에 정의된 모든 유형, 연산자, 함수, 매크로를 지원합니다.

표현식 파싱 및 확인

환경이 구성되면 표현식을 파싱하고 확인할 수 있습니다. 함수에 다음을 추가합니다.

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

ParseCheck 호출에서 반환된 iss 값은 오류일 수 있는 문제 목록입니다. iss.Err()가 nil이 아니면 구문 또는 의미 체계에 오류가 있는 것이므로 프로그램이 더 이상 진행할 수 없습니다. 표현식의 형식이 올바르면 이러한 호출의 결과는 실행 가능한 cel.Ast입니다.

수식을 평가합니다.

표현식이 파싱되어 cel.Ast로 체크인되면 함수 결합 및 평가 모드를 기능 옵션으로 맞춤설정할 수 있는 평가 가능한 프로그램으로 변환할 수 있습니다. cel.CheckedExprToAst 또는 cel.ParsedExprToAst 함수를 사용하여 proto에서 cel.Ast를 읽을 수도 있습니다.

cel.Program를 계획한 후에는 Eval를 호출하여 입력과 비교하여 평가할 수 있습니다. Eval의 결과에는 결과, 평가 세부정보, 오류 상태가 포함됩니다.

계획을 추가하고 eval를 호출합니다.

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

간결성을 위해 위에 포함된 오류 사례는 이후 연습에서 생략하겠습니다.

코드 실행

명령줄에서 코드를 다시 실행합니다.

go run .

다음 연습을 위한 자리표시자와 함께 다음 출력이 표시됩니다.

=== Exercise 1: Hello World ===

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

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

5. 함수에서 변수 사용

대부분의 CEL 애플리케이션은 표현식 내에서 참조할 수 있는 변수를 선언합니다. 변수 선언은 이름과 유형을 지정합니다. 변수 유형은 CEL 내장 유형, 프로토콜 버퍼 잘 알려진 유형 또는 모든 protobuf 메시지 유형일 수 있습니다. 단, 해당 설명자도 CEL에 제공되어야 합니다.

함수 추가

편집기에서 exercise2 선언을 찾아 다음을 추가합니다.

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

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

오류 재실행 및 이해

프로그램을 다시 실행합니다.

go run .

다음과 같은 출력이 표시됩니다.

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

유형 검사기는 오류가 발생하는 소스 스니펫을 편리하게 포함하는 요청 객체에 대한 오류를 생성합니다.

6. 변수 선언

EnvOptions 추가

편집기에서 다음과 같이 요청 객체의 선언을 google.rpc.context.AttributeContext.Request 유형의 메시지로 제공하여 결과 오류를 수정해 보겠습니다.

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

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

오류 재실행 및 이해

프로그램을 다시 실행합니다.

go run .

다음과 같은 오류가 표시됩니다.

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

protobuf 메시지를 참조하는 변수를 사용하려면 유형 검사기가 유형 설명자도 알아야 합니다.

cel.Types를 사용하여 함수의 요청에 대한 설명자를 결정합니다.

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

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

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

재실행되었습니다.

프로그램을 다시 실행합니다.

go run .

아래와 같이 표시됩니다.

=== Exercise 2: Variables ===

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

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

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

검토를 위해 오류의 변수를 선언하고 유형 설명자를 할당한 다음 표현식 평가에서 변수를 참조했습니다.

7. 논리곱(AND/OR)

CEL의 고유한 기능 중 하나는 가환 논리 연산자를 사용한다는 것입니다. 오류 또는 부분 입력이 발생하더라도 조건부 브랜치의 어느 한쪽이 평가를 단락시킬 수 있습니다.

즉, CEL은 가능한 경우 결과를 제공하는 평가 순서를 찾아 다른 평가 순서에서 발생할 수 있는 오류나 누락된 데이터를 무시합니다. 애플리케이션은 이 속성을 사용하여 평가 비용을 최소화할 수 있으며, 비용이 많이 드는 입력 없이도 결과에 도달할 수 있을 때 이러한 입력의 수집을 지연시킬 수 있습니다.

AND/OR 예시를 추가한 다음 다른 입력으로 시도하여 CEL 단락 평가 방식을 이해해 보겠습니다.

함수 만들기

편집기에서 실습 3에 다음 콘텐츠를 추가합니다.

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

그런 다음 사용자가 admin 그룹의 구성원이거나 특정 이메일 식별자를 가지고 있는 경우 true를 반환하는 다음 OR 문을 포함합니다.

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

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

마지막으로 클레임 세트가 비어 있는 사용자를 평가하는 eval 사례를 추가합니다.

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

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

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

클레임 세트가 비어 있는 상태로 코드 실행

프로그램을 다시 실행하면 다음과 같은 새 출력이 표시됩니다.

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

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

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

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

평가 사례 업데이트

다음으로 평가 사례를 업데이트하여 빈 클레임 집합이 있는 다른 주 구성원을 전달합니다.

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

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

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

시간과 함께 코드 실행

프로그램을 다시 실행하면

go run .

다음과 같은 오류가 표시됩니다.

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

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

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

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

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

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

protobuf에서는 예상되는 필드와 유형을 알고 있습니다. 맵 및 json 값에 키가 있는지 알 수 없습니다. 누락된 키에 대한 안전한 기본값이 없으므로 CEL은 기본값인 error(오류)가 됩니다.

8. 커스텀 함수

CEL에는 많은 기본 제공 함수가 포함되어 있지만 맞춤 함수가 유용한 경우도 있습니다. 예를 들어 맞춤 함수를 사용하여 일반적인 조건의 사용자 환경을 개선하거나 컨텍스트에 민감한 상태를 노출할 수 있습니다.

이 연습에서는 함수를 노출하여 흔히 사용되는 검사를 함께 패키징하는 방법을 살펴봅니다.

맞춤 함수 호출

먼저, 맵에 키가 있는지, 특정 값이 있는지 확인하는 contains라는 재정의를 설정하는 코드를 만듭니다. 함수 정의 및 함수 바인딩을 위한 자리표시자는 그대로 둡니다.

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

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

코드 실행 및 오류 이해

코드를 다시 실행하면 다음 오류가 표시됩니다.

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

이 오류를 수정하려면 현재 요청 변수를 선언하는 선언 목록에 contains 함수를 추가해야 합니다.

다음 세 줄을 추가하여 매개변수화된 유형을 선언합니다. (이는 모든 함수 오버로드가 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()
}

커스텀 함수 추가

다음으로 매개변수화된 유형을 사용할 새 include 함수를 추가합니다.

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

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

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

프로그램을 실행하여 오류 파악

운동을 실행합니다. 누락된 런타임 함수에 관한 다음 오류가 표시됩니다.

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

cel.FunctionBinding() 함수를 사용하여 NewEnv 선언에 함수 구현을 제공합니다.

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

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

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

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

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

이제 프로그램이 성공적으로 실행됩니다.

=== Exercise 4: Custom Functions ===

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

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

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

소유권 주장이 존재하면 어떻게 되나요?

추가 크레딧을 얻으려면 입력에 관리자 클레임을 설정하여 클레임이 존재할 때 포함 오버로드도 true를 반환하는지 확인하세요. 다음과 같은 출력이 표시됩니다.

=== Exercise 4: Customization ===

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

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

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

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

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

계속 진행하기 전에 mapContainsKeyValue 함수 자체를 검사해 보는 것이 좋습니다.

// mapContainsKeyValue implements the custom function:
//   map.contains(key, value) -> bool.
func mapContainsKeyValue(args ...ref.Val) ref.Val {
  // The declaration of the function ensures that only arguments which match
  // the mapContainsKey signature will be provided to the function.
  m := args[0].(traits.Mapper)

  // CEL has many interfaces for dealing with different type abstractions.
  // The traits.Mapper interface unifies field presence testing on proto
  // messages and maps.
  key := args[1]
  v, found := m.Find(key)

  // If not found and the value was non-nil, the value is an error per the
  // `Find` contract. Propagate it accordingly. Such an error might occur with
  // a map whose key-type is listed as 'dyn'.
  if !found {
    if v != nil {
      return types.ValOrErr(v, "unsupported key type")
    }
    // Return CEL False if the key was not found.
    return types.False
  }
  // Otherwise whether the value at the key equals the value provided.
  return v.Equal(args[2])
}

가장 쉽게 확장할 수 있도록 맞춤 함수의 서명에는 ref.Val 유형의 인수가 필요합니다. 여기서 단점은 확장의 용이성으로 인해 구현자에게 모든 값 유형이 올바르게 처리되도록 하는 부담이 가중된다는 점입니다. 입력 인수 유형 또는 개수가 함수 선언과 일치하지 않으면 no such overload 오류가 반환되어야 합니다.

cel.FunctionBinding()는 런타임 유형 가드를 추가하여 런타임 계약이 환경에서 유형이 확인된 선언과 일치하도록 합니다.

9. JSON 빌드

CEL은 JSON과 같은 불리언이 아닌 출력도 생성할 수 있습니다. 함수에 다음을 추가합니다.

// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
    fmt.Println("=== Exercise 5: Building JSON ===\n")
    env, _ := cel.NewEnv(
      // Declare the 'now' variable as a Timestamp.
      // cel.Variable("now", cel.TimestampType),
    )
    // Note the quoted keys in the CEL map literal. For proto messages the
    // field names are unquoted as they represent well-defined identifiers.
    ast := compile(env, `
        {'sub': 'serviceAccount:delegate@acme.co',
         'aud': 'my-project',
         'iss': 'auth.acme.com:12350',
         'iat': now,
         'nbf': now,
         'exp': now + duration('300s'),
         'extra_claims': {
             'group': 'admin'
         }}`,
        cel.MapType(cel.StringType, cel.DynType))

    program, _ := env.Program(ast)
    out, _, _ := eval(
        program,
        map[string]interface{}{
            "now": &tpb.Timestamp{Seconds: time.Now().Unix()},
        },
    )
    fmt.Printf("------ type conversion ------\n%v\n", out)
    fmt.Println()
}

코드 실행

코드를 다시 실행하면 다음 오류가 표시됩니다.

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

cel.TimestampType 유형의 now 변수 선언을 cel.NewEnv()에 추가하고 다시 실행합니다.

// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
    fmt.Println("=== Exercise 5: Building JSON ===\n")
    env, _ := cel.NewEnv(
      cel.Variable("now", cel.TimestampType),
    )
    // Note the quoted keys in the CEL map literal. For proto messages the
    // field names are unquoted as they represent well-defined identifiers.
    ast := compile(env, `
        {'sub': 'serviceAccount:delegate@acme.co',
         'aud': 'my-project',
         'iss': 'auth.acme.com:12350',
         'iat': now,
         'nbf': now,
         'exp': now + duration('300s'),
         'extra_claims': {
             'group': 'admin'
         }}`,
        cel.MapType(cel.StringType, cel.DynType))

     // Hint:
     // Convert `out` to JSON using the valueToJSON() helper method.
     // The valueToJSON() function calls ConvertToNative(&structpb.Value{})
     // to adapt the CEL value to a protobuf JSON representation and then
     // uses the jsonpb.Marshaler utilities on the output to render the JSON
     // string.
    program, _ := env.Program(ast)
    out, _, _ := eval(
        program,
        map[string]interface{}{
            "now": time.Now(),
        },
    )
    fmt.Printf("------ type conversion ------\n%v\n", out)
    fmt.Println()
}

코드를 다시 실행하면 성공합니다.

=== Exercise 5: Building JSON ===

  {'aud': 'my-project',
   'exp': now + duration('300s'),
   'extra_claims': {
    'group': 'admin'
   },
   'iat': now,
   'iss': 'auth.acme.com:12350',
   'nbf': now,
   'sub': 'serviceAccount:delegate@acme.co'
   }

------ input ------
now = seconds: 1569302377

------ result ------
value: &{0xc0002eaf00 map[aud:my-project exp:{0xc0000973c0} extra_claims:0xc000097400 iat:{0xc000097300} iss:auth.acme.com:12350 nbf:{0xc000097300} sub:serviceAccount:delegate@acme.co] {0x8c8f60 0xc00040a6c0 21}} (*types.baseMap)

------ type conversion ------
&{0xc000313510 map[aud:my-project exp:{0xc000442510} extra_claims:0xc0004acdc0 iat:{0xc000442450} iss:auth.acme.com:12350 nbf:{0xc000442450} sub:serviceAccount:delegate@acme.co] {0x9d0ce0 0xc0004424b0 21}}

프로그램은 실행되지만 출력 값 out을 명시적으로 JSON으로 변환해야 합니다. 이 경우 내부 CEL 표현은 JSON이 지원할 수 있거나 잘 알려진 Proto-JSON 매핑이 있는 유형만 참조하기 때문에 JSON 변환이 가능합니다.

// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
    fmt.Println("=== Exercise 5: Building JSON ===\n")
...
    fmt.Printf("------ type conversion ------\n%v\n", valueToJSON(out))
    fmt.Println()
}

codelab.go 파일 내의 valueToJSON 도우미 함수를 사용하여 유형이 변환되면 다음과 같은 추가 출력이 표시됩니다.

------ type conversion ------
  {
   "aud": "my-project",
   "exp": "2019-10-13T05:54:29Z",
   "extra_claims": {
      "group": "admin"
     },
   "iat": "2019-10-13T05:49:29Z",
   "iss": "auth.acme.com:12350",
   "nbf": "2019-10-13T05:49:29Z",
   "sub": "serviceAccount:delegate@acme.co"
  }

10. Proto 빌드

CEL은 애플리케이션에 컴파일된 모든 메시지 유형에 관한 protobuf 메시지를 빌드할 수 있습니다. 입력 jwt에서 google.rpc.context.AttributeContext.Request를 빌드하는 함수 추가

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

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

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

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

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

코드 실행

코드를 다시 실행하면 다음 오류가 표시됩니다.

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

컨테이너는 기본적으로 네임스페이스나 패키지와 같지만 protobuf 메시지 이름만큼 세분화될 수도 있습니다. CEL 컨테이너는 지정된 변수, 함수 또는 유형 이름이 선언된 위치를 결정하기 위해 Protobuf 및 C++와 동일한 네임스페이스 확인 규칙을 사용합니다.

google.rpc.context.AttributeContext 컨테이너가 주어지면 유형 검사기와 평가자는 모든 변수, 유형, 함수에 다음과 같은 식별자 이름을 시도합니다.

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

절대 이름의 경우 변수, 유형 또는 함수 참조 앞에 점을 붙입니다. 이 예에서 .<id> 표현식은 컨테이너 내에서 먼저 확인하지 않고 최상위 <id> 식별자만 검색합니다.

CEL 환경에 cel.Container("google.rpc.context.AttributeContext") 옵션을 지정하고 다시 실행해 봅니다.

// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
  fmt.Println("=== Exercise 6: Building Protos ===\n")
  // Construct an environment and indicate that the container for all references
  // within the expression is `google.rpc.context.AttributeContext`.
  requestType := &rpcpb.AttributeContext_Request{}
  env, _ := cel.NewEnv(
    // Add cel.Container() option for 'google.rpc.context.AttributeContext'
    cel.Container("google.rpc.context.AttributeContext.Request"),
    cel.Types(requestType),
    // Later, add cel.Variable() options for 'jwt' as a map(string, Dyn) type
    // and for 'now' as a timestamp.
    // cel.Variable("now", cel.TimestampType),
    // cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
  )

 ...
}

다음과 같은 결과가 출력됩니다.

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

... 그 외 많은 오류들...

그런 다음 jwtnow 변수를 선언하면 프로그램이 예상대로 실행됩니다.

=== Exercise 6: Building Protos ===

  Request{
   auth: Auth{
    principal: jwt.iss + '/' + jwt.sub,
    audiences: [jwt.aud],
    presenter: 'azp' in jwt ? jwt.azp : "",
    claims: jwt
   },
   time: now
  }

------ input ------
jwt = {
  "aud": "my-project",
  "extra_claims": {
    "group": "admin"
  },
  "iss": "auth.acme.com:12350",
  "sub": "serviceAccount:delegate@acme.co"
}
now = seconds: 1588993027

------ result ------
value: &{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false} (*types.protoObj)

----- type unwrap ----
&{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false}

추가 크레딧을 얻으려면 메시지에서 out.Value()를 호출하여 유형을 래핑 해제하여 결과가 어떻게 변경되는지 확인해야 합니다.

11. 매크로

매크로는 파싱 시 CEL 프로그램을 조작하는 데 사용될 수 있습니다. 매크로는 호출 서명과 일치하고 입력 호출과 인수를 조작하여 새로운 하위 표현식 AST를 생성합니다.

매크로는 CEL에서 직접 작성할 수 없는 복잡한 로직을 AST에서 구현하는 데 사용할 수 있습니다. 예를 들어 has 매크로는 현장 접속 테스트를 사용 설정합니다. 존재(있는)와 같은 이해 매크로는 모두 입력 목록 또는 맵에 대한 제한된 반복으로 함수 호출을 대체합니다. 두 개념 모두 구문 수준에서는 불가능하지만 매크로 확장을 통해서는 가능합니다.

다음 연습을 추가하고 실행합니다.

// exercise7 introduces macros for dealing with repeated fields and maps.
//
// Determine whether the jwt.extra_claims has at least one key that starts
// with the group prefix, and ensure that all group-like keys have list
// values containing only strings that end with '@acme.co`.
func exercise7() {
    fmt.Println("=== Exercise 7: Macros ===\n")
    env, _ := cel.NewEnv(
      cel.ClearMacros(),
      cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
    )
    ast := compile(env,
        `jwt.extra_claims.exists(c, c.startsWith('group'))
                && jwt.extra_claims
                      .filter(c, c.startsWith('group'))
                      .all(c, jwt.extra_claims[c]
                                 .all(g, g.endsWith('@acme.co')))`,
        cel.BoolType)
    program, _ := env.Program(ast)

    // Evaluate a complex-ish JWT with two groups that satisfy the criteria.
    // Output: true.
    eval(program,
        map[string]interface{}{
            "jwt": map[string]interface{}{
                "sub": "serviceAccount:delegate@acme.co",
                "aud": "my-project",
                "iss": "auth.acme.com:12350",
                "extra_claims": map[string][]string{
                    "group1": {"admin@acme.co", "analyst@acme.co"},
                    "labels": {"metadata", "prod", "pii"},
                    "groupN": {"forever@acme.co"},
                },
            },
        })

    fmt.Println()
}

그러면 다음과 같은 오류가 표시됩니다.

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

... 그 외 다수 ...

이러한 오류는 매크로를 아직 사용하도록 설정하지 않았기 때문에 발생합니다. 매크로를 사용 설정하려면 cel.ClearMacros()를 삭제하고 다시 실행하세요.

=== Exercise 7: Macros ===

jwt.extra_claims.exists(c, c.startsWith('group'))
    && jwt.extra_claims
       .filter(c, c.startsWith('group'))
       .all(c, jwt.extra_claims[c]
              .all(g, g.endsWith('@acme.co')))

------ input ------
jwt = {
  "aud": "my-project",
  "extra_claims": {
    "group1": [
      "admin@acme.co",
      "analyst@acme.co"
    ],
    "groupN": [
      "forever@acme.co"
    ],
    "labels": [
      "metadata",
      "prod",
      "pii"
    ]
  },
  "iss": "auth.acme.com:12350",
  "sub": "serviceAccount:delegate@acme.co"
}

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

다음은 현재 지원되는 매크로입니다.

Macro

서명

설명

모두

r.all(var, cond)

r 범위의 모든 변수에 대해 cond가 true로 평가되는지 테스트합니다.

존재함

r.exists(var, cond)

r 범위 내의 모든 변수에 대해 cond가 true로 평가되는지 테스트합니다.

exists_one

r.exists_one(var, cond)

cond가 r 범위의 하나의 변수만 true로 평가하는지 테스트합니다.

filter

r.filter(var, cond)

목록의 경우 범위 r의 각 요소 변수가 조건 조건을 충족하는 새 목록을 만듭니다. 맵의 경우 범위 r의 각 키 변수가 조건 조건을 충족하는 새 목록을 만듭니다.

지도

r.map(var, expr)은

범위 r에 있는 각 var가 expr에 의해 변환되는 새 목록을 만듭니다.

r.map(var, cond, expr)

두 개의 인수가 있는 맵과 동일하지만 값이 변환되기 전에 조건부 cond 필터가 있습니다.

보유

has(a.b)

값 a의 b에 대한 접속 상태 테스트 : 지도의 경우 json 테스트 정의입니다. proto의 경우 기본값이 아닌 프리미티브 값 또는 설정된 메시지 필드를 테스트합니다.

범위 r 인수가 map 유형이면 var가 맵 키가 되고 list 유형 값의 경우 var는 목록 요소 값이 됩니다. all, exists, exists_one, filter, map 매크로는 입력 크기로 제한되는 각 반복을 수행하는 AST 재작성을 실행합니다.

제한된 이해는 CEL 프로그램이 Turing에서 완전하게 진행되지 않도록 하지만 입력과 관련하여 초선형 시간으로 평가됩니다. 이러한 매크로는 조금만 사용하거나 아예 사용하지 마세요. 일반적으로 이해를 많이 사용하는 것은 맞춤 함수가 더 나은 사용자 환경과 더 나은 실적을 제공한다는 좋은 지표입니다.

12. 조정

현재 CEL-Go에만 제공되는 몇 가지 기능이 있지만 다른 CEL 구현을 위한 향후 계획을 나타냅니다. 다음 실습에서는 동일한 AST를 적용한 다양한 프로그램 계획을 보여줍니다.

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

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

    fmt.Println()
}

최적화 도구

최적화 플래그를 사용 설정하면 CEL은 목록과 맵 리터럴을 미리 빌드하고 in 연산자와 같은 특정 호출을 실제 세트 멤버십 테스트가 되도록 최적화하는 데 추가 시간을 소비합니다.

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

동일한 프로그램이 여러 입력값에 대해 여러 번 평가되는 경우 최적화하는 것이 좋습니다. 하지만 프로그램 평가가 한 번만 된다면 최적화로는 오버헤드만 추가됩니다.

포괄적인 평가

Exhaustive Eval은 표현식 평가의 각 단계에서 관찰된 값에 대한 유용한 정보를 제공하므로 표현식 평가 동작을 디버깅하는 데 유용할 수 있습니다.

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

각 표현식 ID의 표현식 평가 상태 목록이 표시됩니다.

------ 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 2는 첫 번째의 in 연산자 결과에 해당합니다. 표현식 ID 11은 두 번째의 == 연산자에 해당합니다. 정상 평가에서 2가 계산되면 표현식이 단락됩니다. y가 유닛이 아니라면 상태에 표현식이 실패한 이유가 하나가 아니라 두 가지 이유가 표시됩니다.

13. 학습 내용

표현식 엔진이 필요한 경우 CEL을 사용하는 것이 좋습니다. CEL은 성능이 중요한 사용자 구성을 실행해야 하는 프로젝트에 적합합니다.

이전 연습을 통해 데이터를 CEL에 전달하고 출력이나 결정을 다시 가져오는 데 익숙해지셨기를 바랍니다.

불리언 결정에서 JSON 및 Protobuffer 메시지 생성에 이르기까지 수행할 수 있는 작업의 종류에 관해 잘 알고 계시기를 바랍니다.

표현식과 관련된 작업 방법과 각 표현이 수행하는 작업을 잘 이해하셨기를 바랍니다. Google은 이를 확장할 수 있는 일반적인 방법을 알고 있습니다.