Kuadrant / authorino

K8s-native AuthN/AuthZ service to protect your APIs.
Apache License 2.0
194 stars 30 forks source link

Configurable evaluator response strategies for the phases of the auth pipeline #112

Open guicassolato opened 3 years ago

guicassolato commented 3 years ago

Authorino implements 3 so-called "strategies" for the 3 first phases of auth pipeline (wristband phase excluded). These strategies are:

These strategies currently map one-to-one with the first 3 phases of Authorino's auth pipeline, respectively: identity phase, metadata phase and authorization phase. I.e.,

This issue is a request to make how the 3 strategies map to the phases configurable, with the default strategies still falling back to the ones currently enforced.

Example for a possible implementation

apiVersion: config.authorino.3scale.net/v1beta1
kind: Service
metadata:
  name: my-api-protection-1
spec:
  hosts:
    - myapi.io
  identity:
    - name: auth-server-1
      oidc:
        endpoint: https://auth-server-1/auth
    - name: auth-server-2
      oidc:
        endpoint: https://auth-server-2/auth
  metadata:
    - name: semaphore-1
      http:
        method: GET
        sharedSecretRef:
          name: myapi-protection-metadata-secrets
          key: semaphore-1-shared-auth
    - name: semaphore-2
      http:
        method: GET
        sharedSecretRef:
          name: myapi-protection-metadata-secrets
          key: sempahore-2-shared-auth
  authorization:
    - name: authz-policy-1
      json:
        rules:
          - selector: auth.metadata.semaphore-1.color
            operator: eq
            value: green
          - selector: auth.metadata.semaphore-2.color
            operator: eq
            value: green
          - selector: auth.identity.role
            operator: eq
            value: restricted-user
    - name: authz-policy-1
      opa:
        inlineRego: |
          semaphore1 { input.auth.metadata.semaphore-1.color == "red" }
          semaphore2 { input.auth.metadata.semaphore-2.color == "red" }
          sunday { time.weekday(time.now_ns()) == "Sunday" }
          allow { not semaphore1; not semaphore2; sunday }
  identityStrategy: any
  metadataStrategy: all
  authorizationStrategy: atLeastOne

The example above would be for a case where all the default strategies are overridden as follows:

Special considerations

The cases for configurable strategies in the metadata and in the authorization phases are less probably needed than the one for the identity phase, due to the existing options to represent conditionals in Authorino authorization policies (both, in OPA policies and in JSON pattern-matching policies). The configurability of the strategies would nonetheless provide another way to describe the authorization logic, at the very least improving readability in some cases.

Another thing to take into account is what happens when, in the metadata phase, the chose strategy fails (e.g. atLeastOne or all enforced, but none of the sources respond)? Should Authorino reject the request as DENIED? Would this virtually be promoting the metadata sources to potential external authorizers?

eguzki commented 3 years ago

To comply with OAS (both swagger and 3.0), the identity requirements must include enforcing multiple identity methods to satisfy.

Inspired by OAS3 spec, the authorino API could be something like this:

spec:
  indentity: [ Identity Requirement Object ],    // Only one of the identity requirement objects need to be satisfied to identify a request.
  identitySchemes: Map[string, Security Scheme Object]

Having Identity Requirement Object as:

Field Pattern Type Description
{name} {} Each name MUST correspond to a identity scheme which is declared in the Identity Schemes. The value can be used to parametrize the identity method further or leave empty.

Identity Requirement Object Examples

spec:
  indentity:
  - my_api_key": {}
    my_oidc: {}
 - my_other_api_key: {}

Identity Requirement Objects that contain multiple schemes require that all schemes MUST be satisfied for a request to be identified

Having Identity scheme Object as: Field Name Type Description
Name string the name of this identity scheme to be referenced from identity requirement objects
Type string Valid types would be oauth2, oidc, apikey and kubernetes_auth
{param} {any} any param to define the identity scheme
guicassolato commented 3 years ago

Inspired by OAS3 spec, the authorino API could be something like this:

Thanks for the suggestion, @eguzki.

There are few aspects of your suggestion that I'd like to comment about (if I even got them right):

  1. Object linking, reusability and extension: the idea of having "identity schemes" that can be parametrized once used as "identity requirements"
  2. Scope: what's expected with this issue vs. OAS
  3. Use cases and implementation (regarding identity sources, but other types of evaluators as well): there are at least 4 now: "at least one evaluator is required to succeed", "all evaluators are required to succeed", "all evaluators are optional", "some (more than one) evaluators are required, some aren't"

1. Object linking, reusability and extension

In Authorino we don't use this pattern of having a base definition that can be extended when referred somewhere else in the CR. I know it's used in OAI specs, but I'd rather keep the way it is in Authorino for now. When we get to point where reusing definitions becomes a critical requirement, we'll probably be adding more specific CRDs and wiring things up through object refs instead.

Moreover, I'm afraid of people overusing those "identity scheme" definitions and later ending up with more schemes declared than actually used in "identity requirement". Again, I know this pattern is used a lot in OAI specs, but no so much of the typical UX expected when dealing with K8s CRs, IMO. Only counterexample I can think of are of volumes and volumeMounts in Deployment specs, but not cool. If an entire CR is not used ("garbage"), then it's easier to manage – delete it and done. But if the gargabe is inside of a still valid and used resource spec, then... sigh; it will stay there forever.

That said, maybe you were thinking that no "identity scheme" would become "garbage" inside a CR. If it's there, then it is being used. It is more about which schemes are optional and which ones are required... more on that further down in "3. Use cases"

2. Scope (of this issue)

In general, I'd say your suggestion goes beyond what was originally expected with this issue, which was making two things that already exist, and that are currently hard-linked to each other in Authorino, configurable. The arrays of evaluators (identity, metadata, authorization and response) are there, as well as the different strategies to move to the next phase in the auth pipeline (atLeastOne, all, any), and they already work as-is.

The only thing is that the mapping of the different types of arrays that correspond to each phase of the pipeline and their respective strategy is now always identity → atLeastOne, metadata → any, authorization → all, response → all. We wanted to make this a choice of the user, with default to how it works now if not specified otherwise. Under that original plan, stating, e.g., so all applies to the list of identity sources instead of atLeastOne would be as simple as that: identity → all, done.

I was not at all hoping this to support any OAS-specific requirement. We're not going against it, of course, but there are things that we want to cover here that are unrelated to OAS (e.g. required/optional evaluators in phases other than identity verification), and at least one thing that you mentioned that is out of scope of this issue IMO (i.e. object linking, reusability and extension).

3. Use cases and implementation

The implementation proposed in the description of the issue should suffice to cover the use cases "at least one evaluator is required", "all evaluators are required" an "all evaluators are optional (but still try them all just in case – i.e. no cancelling of context)". I admit that, until now, I hadn't thought about the use case "some (more than one) are required, some are optional".

I guess for this other (yet uncovered) use case, a possible different implementation that not only satisfies it, but also makes it a superset of all other use cases, and yet avoids the whole linking/reusing/extending of object bits that I think are out of scope of this issue, is a simple flag optional: true that all evaluators would support (with default to optional: false).

At a glance, it's simple and intuitive, I think.

In terms of changes required in the code to achieve it, on the other hand, this solution would be a lot harder than what I had in mind originally, because of the cancelling of the Go contexts of concurrent evaluators running in the same phase.

Before triggering the phase, Authorino would have to scan the flags of all evaluators of the phase, to then monitor if all required ones have finished. (This "scan" is virtually equivalent to your proposed, more explicit, list of required ones.) If at least one among the required has failed, the pipeline can be aborted. However, if all required evaluators have finished successfully, but there are still some optional ones running, can the context be cancelled and the pipeline moved to the next phase? We'd need to think about that. Cancelling is one thing; letting the thread go all the way through and just ignore the result when it fails is a whole other thing.

Anyway, it's not as straightforward as just allowing the user to choose among the existing strategies which one to apply to which phase.