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.
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.
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.
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.
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)
maleck13
left a comment
There was a problem hiding this comment.
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.
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.
I guess you could also do this with regexp and the matches operator?
| operator: matches | ||
| value: ^/resources/.* | ||
| - any: | ||
| - selector: context.request.http.method |
There was a problem hiding this comment.
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.
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.
just a note I do find myself wanting to write this as a matches operator :)
adam-cattermole
left a comment
There was a problem hiding this comment.
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). Whereexpressioncan be any of the following:selector,operatorandvalue;patternRef: String);anyfield;allfield.For backward compatibility, the AND operator is assumed for all lists of patterns not grouped under an explicit
anyfield declaration. I.e., it is equivalent to grouping the patterns under a singleallfield 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 Forbiddencurl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X GET -i # HTTP/1.1 200 OKcurl 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 Forbiddencurl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X GET -i # HTTP/1.1 200 OKcurl 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 Forbiddencurl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X GET -i # HTTP/1.1 200 OKcurl 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 Forbiddencurl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X PUT -i # HTTP/1.1 403 Forbiddencurl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X GET -i # HTTP/1.1 200 OKcurl 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 Forbiddencurl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X PUT -i # HTTP/1.1 403 Forbiddencurl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X GET -i # HTTP/1.1 200 OKcurl 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
allandanyhave 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 OKcurl http://talker-api-authorino.127.0.0.1.nip.io:8000/foo -X PUT -i # HTTP/1.1 200 OKcurl http://talker-api-authorino.127.0.0.1.nip.io:8000/bar -X PUT -i # HTTP/1.1 200 OK