fix: has() on non-container types returns error instead of false#1298
fix: has() on non-container types returns error instead of false#1298kodareef5 wants to merge 1 commit intogoogle:masterfrom
Conversation
| }, | ||
| { | ||
| expr: `has({'foo': optional.none()}.foo.value)`, | ||
| out: types.False, |
There was a problem hiding this comment.
This is the expected behavior for accesses within an optional value. If the test were for a null return value, then you'd expect a no such key error
755923c to
84f0136
Compare
|
Good catch, thank you. You're right — I've updated the fix to narrow the scope. The
No test expectations changed. Full suite passes. |
| default: | ||
| if presenceTest && !errorOnBadPresenceTest { | ||
| return nil, false, nil | ||
| if _, isOpt := celVal.(*types.Optional); isOpt || !presenceOnly { |
There was a problem hiding this comment.
Could you add a test case that demonstrates the behavior addressed by this change? The presenceOnly flag is just an optimization to indicate that the return value isn't necessary - only the presence bit.
There was a problem hiding this comment.
Added test cases for has() on int, string, bool, and null via dyn():
{expr: `has(dyn(42).field)`, out: "no such key: field"},
{expr: `has(dyn('hello').field)`, out: "no such key: field"},
{expr: `has(dyn(true).field)`, out: "no such key: field"},
{expr: `has(dyn(null).field)`, out: "no such key: field"},On the presenceOnly flag -- you're right that it's an optimization. In this context it serves as the only available signal that distinguishes has() (where testOnlyQualifier always passes presenceOnly=true) from ?. optional select (where applyQualifiers passes presenceOnly=false). The ?. path needs to keep returning not-present on non-container types to preserve optional.none() chaining, while has() on a raw primitive like dyn(42).field should error rather than silently returning false.
I initially tried dropping the presenceOnly check and erroring for both paths, but that breaks the ?. tests ({0: dyn(0)}[?0].?invalid expects optional.none(), not an error). Happy to restructure if you'd prefer a different approach, e.g. threading a separate flag through the qualifier chain.
There was a problem hiding this comment.
These cases error properly though when EnableErrorOnBadPresenceTest(true) is enabled, so I think that's why I'm not clear on what the issue is that you're trying to address.
has() applied to a non-container, non-optional value (e.g., an integer or string) previously returned false silently when errorOnBadPresenceTest was not enabled. This caused !has(x.field) to evaluate to true when x was an unexpected type, creating a fail-open condition in policy expressions that use presence guards on dynamic inputs. This change narrows the default case in refQualify so that: - has() on a non-container, non-optional type (presenceOnly=true): returns missingKey error, preventing silent false returns - has() on an optional value (e.g. optional.none()): continues to return false, preserving correct optional semantics - Optional field selection (x.?field, presenceOnly=false): continues to return not-present for optional.none() compatibility - EnableErrorOnBadPresenceTest(true): errors for all cases (unchanged) No test expectations changed. Full suite green. Signed-off-by: Koda Reef <koda.reef5@gmail.com>
84f0136 to
9294dcf
Compare
Summary
has()applied to a non-container type (integer, string, boolean, null) previously returnedfalsesilently whenEnableErrorOnBadPresenceTestwas not set. This caused!has(x.field)to evaluate totruewhenxhad an unexpected type, creating a fail-open condition in policy expressions that use presence guards on dynamically typed inputs.This change splits the
defaultcase inrefQualify(interpreter/attributes.go) so that:has()(presenceOnly=true): errors withmissingKeyon non-container types, preventing silent false returns?.(presenceOnly=false): continues to return not-present, preservingoptional.none()compatibilityThe
EnableErrorOnBadPresenceTestoption continues to work as before when explicitly set.Example
Test changes
One test expectation updated:
has({'foo': optional.none()}.foo.value)now expects an error ("no such key: value") instead offalse, sinceoptional.none()is not a container type andhas()should not silently return false on it.All existing tests pass. Full suite green.