Skip to content

Adventures in arithmetic #1058

@dteleguin

Description

@dteleguin

Background: I am trying to integrate CEL Policy (Java) as a policy evaluator for the Transaction Tokens implementation in Keycloak. The example below is taken from the Transaction Tokens draft:

"tctx": {
  "action": "BUY",
  "ticker": "MSFT",
  "quantity": 100,
  "customer_type": {
    "geo": "US",
    "level": "VIP"
  }
}

The contents of the tctx claim can be arbitrary (customer/application specific) and won't be defined by any schema. Therefore, we cannot rely on the Proto schema and the generated class hierarchy - only JSON parsing at runtime. My first attempt was to use Jackson:

CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder()
            .addVar("tctx", MapType.create(SimpleType.STRING, SimpleType.ANY))
            .build();

var json = """
    {
      "action": "BUY",
      "ticker": "MSFT",
      "quantity": 100,
      "customer_type": {
        "geo": "US",
        "level": "FOO"
      }
    }
""";

Map<String, Object> tctx = new ObjectMapper().readValue(json, new TypeReference<>() {});

CelAbstractSyntaxTree ast = CEL_COMPILER.compile("tctx.quantity > 100").getAst();
CelRuntime.Program program = CEL_RUNTIME.createProgram(ast);

var result = program.eval(Map.of(
        "tctx", tctx
        ));

Though CEL was able to understand the data structure, it failed while evaluating tctx.quantity > 100:

Exception in thread "main" dev.cel.runtime.CelEvaluationException: evaluation error at <input>:14: No matching overload for function '_>_'. Overload candidates: greater_int64

Then it turned out I could not perform any arithmetic with that field at all. After some debugging, I have found the root cause - the Jackson parser converts JSON numbers into Java Integers, and there are no int32 overloads in CEL. This can be boiled down to the following:

CelCompiler CEL_COMPILER = CelCompilerFactory.standardCelCompilerBuilder()
                    .addVar("x", SimpleType.INT)
                    .addVar("a32", ListType.create(SimpleType.INT))
                    .addVar("a64", ListType.create(SimpleType.INT))
                    .build();

CelAbstractSyntaxTree ast = CEL_COMPILER.compile("a32[0] > 0").getAst(); // -> No matching overload for function '_>_'. Overload candidates: greater_int64
// CelAbstractSyntaxTree ast = CEL_COMPILER.compile("a64[0] > 0").getAst(); // -> true
// CelAbstractSyntaxTree ast = CEL_COMPILER.compile("x > 0").getAst(); // -> true
CelRuntime.Program program = CEL_RUNTIME.createProgram(ast);

var result = program.eval(Map.of(
        "a32", List.of(1, 2, 3),
        "a64", List.of(1L, 2L, 3L),
        "x", 42
        ));

Interestingly, an Integer value for the variable x will be automatically cast to Long, but nested Integers (members of a32) won't. Nested Longs also work fine (as in a64).

Then, I have decided to switch to Protobuf Value for my JSON, instead of Jackson:

Value.Builder builder = Value.newBuilder();
JsonFormat.parser().merge(json, builder);
var tctx = builder.build();

This gave a different error:

Exception in thread "main" dev.cel.runtime.CelEvaluationException: evaluation error: No matching overload for function '_>=_'. Overload candidates: greater_equals_int64

Turns out that Protobuf converts all JSON numbers to Doubles, but the right-hand operand in tctx.quantity > 100 is a Long - and again, there's neither automatic casting nor overloads for double vs. int operations. Even a simplest expression like 1.0 > 0 won't work:

dev.cel.common.CelValidationException: ERROR: <input>:1:5: found no matching overload for '_>_' applied to '(double, int)' (candidates: (bool, bool),(int, int),(uint, uint),(double, double),(string, string),(bytes, bytes),(google.protobuf.Timestamp, google.protobuf.Timestamp),(google.protobuf.Duration, google.protobuf.Duration))
 | 1.0 > 0

While I can mention this double vs. int restriction in the docs, the behavior seems counter-intuitive to me as I haven't seen it in any modern languages; also, this is not observed in CEL-Go. It would be nice if we could make integer and real arithmetic in CEL more seamless and straightforward.

Apart from the above - thanks for great software 👏 Looking forward to having CEL in Keycloak in general, not limited to the Transaction Tokens use case.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions