QQ, I'm seeing different results when I change a r...
# help
m
QQ, I'm seeing different results when I change a rule from the following to the other (both should be booleans). Are these not functionally equivalent ?
Copy code
match:
  any:
    of:
      - expr: V.foo
      - expr: V.bar
vs.
Copy code
match:
  expr: V.foo || V.bar
c
They should be equivalent. The caveat is that the
||
operator does not short-circuit whereas
anyOf
does. I don't see how that would affect the result of these expressions though. What are the definitions of
V.bar
and
V.foo
?
m
Copy code
isScoped: P.attr.scopedTenantQid != ''
  isUnscoped: |-
    P.attr.scopedTenantQid == ''
  isScopedToCorpTenant: |-
    P.attr.isCorpTenant && P.attr.tenantQid == P.attr.scopedTenantQid
They look like this where isCorpTenant is a boolean and fooQid variables are a string (empty or with a value).
c
I have been digging into this a bit. If one of the expressions in an
any
block fails to evaluate or returns a non-boolean value, we stop evaluating the rest of the expressions in the block and return
false
immediately. I think that's probably what you have seen. It's likely that
V.foo
returns a non-boolean value because one of the attributes used to calculate it returns a
null
. I ran a test with a principal that didn't have the
scopedTenantQid
attribute and got the following trace:
Copy code
policy=cerbos.resource.TEST.vdefault > scope="" > variables > isUnscoped=`P.attr.scopedTenantQid == ''`
    activated
    result → null

  policy=cerbos.resource.TEST.vdefault > scope="" > variables > isUnscoped=`P.attr.scopedTenantQid == ''`
    activated
    result → null

  policy=cerbos.resource.TEST.vdefault > scope="" > variables > isUnscoped=`P.attr.scopedTenantQid == ''`
    activated
    result → null

  policy=cerbos.resource.TEST.vdefault > scope="" > rule=rule-002 > action=B > condition > conditionAny > condition#0 > expr=`V.isScoped`
    activated
    result → false
    Failed to evaluate expression: unexpected result: wanted bool, got structpb.NullValue

  policy=cerbos.resource.TEST.vdefault > scope="" > rule=rule-002 > action=B > condition > conditionAny
    activated
    result → false
    Short-circuited: failed to evaluate `V.isScoped`: unexpected result: wanted bool, got structpb.NullValue

  policy=cerbos.resource.TEST.vdefault > scope="" > rule=rule-002 > action=B
    skipped
    Error evaluating condition: failed to evaluate `V.isScoped`: unexpected result: wanted bool, got structpb.NullValue
If
scopedTenantQid
doesn't exist,
scopedTenantQid == ''
evaluates to
null
and it's not a boolean, so Cerbos gives up checking the rest because it thinks there's an error in the expression and it's safer to fail than carry on.
I think we can relax this in the engine and implicitly treat non-boolean values as
false
values. But, there might be other unintended consequences of that so it needs a bit more pondering. In the mean time, using schemas to enforce required attributes could help catch problems like this early.
m
Gotcha! I'm not sure there's any need to relax the behavior. Sounds a bit dangerous.
That said, do these unexpected evaluations end up in the logs? I'd like to squash them where possible. Additionally, if I want to handle the absence of an attribute or introduce a new one. What's the best way to do that safely? Say I want to add
P.attr.foo
. If
foo
isn't present I want to the system to ignore that rule for now (avoid updating every test or weird permission issues during a deploy). Adding a
P.attr.foo == null
doesn't seem to do the trick.
Eh, to answer my own question, I guess I can add it to my principal builder and deploy the policy changes in a followup. It's all in a mono-repo so mainly trying to avoid the double PR + updating all existing tests.
c
You can do
has(P.attr.foo)
to check whether
foo
exists
We have a REPL where you can try out CEL expressions https://docs.cerbos.dev/cerbos/latest/cli/cerbos.html#repl
Copy code
-> :let x = {"y": "z"}
x = {
  "y": "z"
}

-> has(x.x)
_ = false

-> has(x.y)
_ = true
m
TIL there's more definitions in the CEL spec