-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: OR operator #427
feat: OR operator #427
Conversation
…tching authorization) Adds support for logical disjunction (aka "OR" operator) in 'when' conditions and pattern-matching authorization rules. A new field `any: Array<expression>` was introduced to the AuthConfig API to express logical disjunction, along with `all: Array<expression>` to enforce the default AND operation, where `expression` can be any of the following: - a pattern composed of `selector`, `operator` and `value`; - a reference to a named pattern (`patternRef: String`); - a nested `any` field; - a nested `all` field. For backward compatibility, the AND operator is assumed for all lists of patterns not grouped under an explicit `any` field declaration. I.e., it is equivalent to grouping the patterns under a single `all` field declaration. Example 1) To allow anonymous access (no authentication required) for all request to path `/test/*` OR method `GET` (ocasionally both): ```yaml spec: authentication: anonymous-request: when: - any: - selector: context.request.http.path operator: matches value: ^/test/.* - selector: context.request.http.method operator: eq method: GET anonymous: {} ``` In a more complex condition, with nested logical operators, to express `host == 'foo.apis.io' AND ((path =~ '/test*' AND (method == 'POST' OR method == 'PUT')) OR method == 'GET')`: ```yaml spec: authentication: anonymous-request: when: - selector: context.request.http.host operator: eq value: foo.apis.io - any: - all: - selector: context.request.http.path operator: matches value: ^/test/.* - any: - selector: context.request.http.method operator: eq value: POST - selector: context.request.http.method operator: eq value: PUT - selector: context.request.http.method operator: eq value: GET anonymous: {} ```
} | ||
} | ||
|
||
func Any(expressions ...Expression) Expression { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so all
will default to true if no experessions passed and any
will default to false if no expressions passed? Is that what is intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has surprised more than one, but yes... See https://en.wikipedia.org/wiki/Vacuous_truth
assert.Check(t, !ok) | ||
} | ||
|
||
func TestEmptyOr(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps being stupid, but not getting why an empty or
returns false but an empty and
returns true?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because every expression has 2 sides, left and right, and sometimes we instantiate expressions where the right-hand side is another empty nested expression. This is the case of expressions initialized with All()
and Any()
, where we don't want the empty nested expressions to affect the result of the outer one.
A more obvious (and possibly easier to read) alternative would be defining trivial True
and False
expressions to use instead of the empty ones.
Additionally, see #427 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes and test all look good to me, caveat I am not an expert on Authorino. One or two small questions but generally it is a looks good to me
spec: | ||
when: # auth enforced only on requests with HTTP method equals to POST or PUT | ||
- any: | ||
- selector: context.request.http.method |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
side point: Have you considered and in
operator as this would (at least from an API perspective) improve the verbosity.
- selector: context.request.http.method
operator: in
values: ["PUT","POST"]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you could also do this with regexp
and the matches operator?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
operator: matches | ||
value: ^/resources/.* | ||
- any: | ||
- selector: context.request.http.method |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: is this the best example? Seems it could be achieved also with a matches
operator? without too complex of a regex?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, many cases of disjunction can be solved with the matches
operator, and this is definitely true for patterns limited to finite sets (and many other infinite ones as well!), such as the values of the HTTP method. For other cases, users may want/have to go with any
.
So I wanted to present any
also as an alternative to matches
.
Click here if you want to learn more about the limitations of the matches
operator.
Because Golang regexp
package (behind our matches
operator) is a strict implementation of regular expressions, in the theoretical computer science sense of the term (i.e., the kind that defines a regular language), some patterns often supported by other programming languages are simply not available in Go by default.
For details, see https://github.com/google/re2/wiki/Syntax.
But that was not the only reason though. There are yet other cases where the disjunction involves patterns with different selectors. E.g.: If (method == <X>) OR (path =~ <Y>.*).
I could have used any one of those more complex cases here. I was trying to reduce the cognitive complexity of the examples by going with something simple and straightforward. I guess I didn't account for the more skilled readers, like you, who often knows a little bit more about the suject and therefore also other ways to solve the problem 🙂
- any: | ||
- selector: context.request.http.method | ||
operator: eq | ||
value: POST |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just a note I do find myself wanting to write this as a matches operator :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've gone through the verification and code and the changes here look good and make sense from my point of view
Adds support for logical disjunction (aka "OR" operator) in 'when' conditions and pattern-matching authorization rules.
Closes #424.
Introduces a new field
any: Array<expression>
to the AuthConfig API to express logical disjunction, along withall: Array<expression>
to enforce the AND operation (default if omitted). Whereexpression
can be any of the following:selector
,operator
andvalue
;patternRef: String
);any
field;all
field.For backward compatibility, the AND operator is assumed for all lists of patterns not grouped under an explicit
any
field declaration. I.e., it is equivalent to grouping the patterns under a singleall
field declaration.Examples
Example 1. To allow anonymous access (no authentication required) for all requests to path
/test/*
OR methodGET
(ocasionally both):Example 2. In a more complex condition, with nested logical operators, to express
host == 'foo.apis.io' AND ((path =~ '/test*' AND (method == 'POST' OR method == 'PUT')) OR method == 'GET')
:Verification steps
Deploy
Try the new feature
① Implicit AND (i.e. the existing syntax, which continues to be supported):
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X POST -i # HTTP/1.1 403 Forbidden
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X GET -i # HTTP/1.1 200 OK
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/bar -i # HTTP/1.1 200 OK
② Same as above with explicit AND operator (
all
):curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X POST -i # HTTP/1.1 403 Forbidden
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X GET -i # HTTP/1.1 200 OK
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/bar -i # HTTP/1.1 200 OK
③ Logical disjunction (
any
):curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X POST -i # HTTP/1.1 403 Forbidden
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X GET -i # HTTP/1.1 200 OK
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/bar -X PUT -i # HTTP/1.1 403 Forbidden
④ Nested OR and AND expression:
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X POST -i # HTTP/1.1 403 Forbidden
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X PUT -i # HTTP/1.1 403 Forbidden
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X GET -i # HTTP/1.1 200 OK
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/bar -X PUT -i # HTTP/1.1 200 OK
⑤ Same as above with implicit AND:
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X POST -i # HTTP/1.1 403 Forbidden
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X PUT -i # HTTP/1.1 403 Forbidden
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X GET -i # HTTP/1.1 200 OK
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/bar -X PUT -i # HTTP/1.1 200 OK
⑥ Edge case: Invalid condition:
Because CRDs defined containing recursive types (e.g. an object/struct that defines a field whose type is the same type as the object itself) result in invalid OAS, the new fields
all
andany
have to be marked withx-kubernetes-preserve-unknown-fields
. Therefore, Kube API won't validate the scheme of these field, effectively accepting any array. The AuthConfig controller should not fail nonetheless, but just ignore the invalid condition instead. In the future, this can be improved with further validation.curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X POST -i # HTTP/1.1 200 OK
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X PUT -i # HTTP/1.1 200 OK
curl http://talker-api-authorino.127.0.0.1.nip.io:8000/bar -X PUT -i # HTTP/1.1 200 OK