Skip to content
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

Support for different authorization/contextualization mechanisms for the same URLs having different authentication contexts #521

Closed
2 of 3 tasks
dadrus opened this issue Mar 10, 2023 · 0 comments · Fixed by #562
Labels
feature Used for new features
Milestone

Comments

@dadrus
Copy link
Owner

dadrus commented Mar 10, 2023

Preflight checklist

Describe the background of your feature request

The actual background are setups, which require different authentication mechanisms for the same URLs, even using same HTTP methods.

Imagine you have a service, which exposes an endpoint, e.g. /foo/bar and the required behavior is as follows:

  • If the read request is not authenticated (user is not logged in), the endpoint should return a representation of the resource, which is much more limited compared to a request, which would come from an authenticated user.
  • If the request is a read request and comes from an authenticated user, the service needs additional contextual information about the user (authorization properties), which again will result in different representations of the resource.
  • If the request is a write request it should be rejected for not authenticated users and evaluated for eligibility for authenticate users via an authorizer. If the user is not eligible to execute it, the request should be rejected, otherwise it should be forwarded to the upstream service.

That can be actually defined more or less by the following rule pipeline:

- id: some_service_rule
  match:
    url: http://127.0.0.1:9090/foo/bar
  methods:
      - GET
      - PUT
      - POST
  execute:
    - authenticator: cookie_session       # <-- configured to authenticate the user via a cookie
    - authenticator: anonymous             # <-- if there is no cookie, fallback to anonymous authenticator
    - contextualizer: subscription_plan  # <-- gets the information about the authorization properties required by the service
    - authorizer: allow_all
    - unifier: jwt_unifier

The problem is however, that if the request is anonymous, the subscription_plan will fail, as the corresponding backend will not find a user with ID set to anonymous (or whatever is set in the configuration of the anonymous authenticator), as long as it does not handle such specific cases. This is however usually not the case and if it is, it introduces a coupling between heimdall and that backend.

This problem becomes even more worse, if the resource does not only support reading requests (via HTTP GET), but also implements resource updates (e.g. via HTTP PUT or POST as described in the example above) and the endpoint requires different contextual or authorization mechanisms for reading and writing requests. As of today, there is no way to address such scenarios with heimdall as the only matching criteria is the url of the endpoint. Here contextualizers and authorizers can only used, if the corresponding backend can deal with anonymous requests.

Describe your idea

There are multiple options on how to support such scenarios with heimdall, respectively what needs to be implemented to support them:

  1. Extend the matching functionality to not only support the URLs, but also the HTTP method and headers cookies. This would allow definition of distinct rules for the different use cases supported by the endpoint. So the rule definition given in the previous section would be split into three rules as shown below:

    # this rule would be used if no authentication information is present in the request
    # only anonymous read access is allowed
    - id: some_service_anonymous_access
      match:
        url: http://127.0.0.1:9090/foo/bar
        # depending on which headers/cookies are used for authentication
        # in other rules, one could define something like that
        cookie_not_set:
          - some_cookie_name
        header_not_set:
          - some_header_name
      methods:
        - GET
      execute:
        - authenticator: anonymous
        - authorizer: allow_all
        - unifier: jwt_unifier
    
    # this rule would be used if authenticated write access is done
    - id: some_service_authenticated_write
      match:
        url: http://127.0.0.1:9090/foo/bar
        # depending on the requirements
        # either a cookie matcher is specified
        cookie_set:  
           - some_cookie_name
        # or a header matcher is specified. 
        # Or even both
        header_set:
           - some_header_name
        # to differentiate between reads and writes, there is also a need to match the method
        # this will make the `method` property on the rule level obsolete in such cases, as the matcher
        # would already define what is supported
        methods:
          - POST
          - PUT
      execute:
        - authenticator: cookie_session
        # `can_write` authorizer can now be used, as we have distinct rules for read and write requests
        - authorizer: can_write
        - unifier: jwt_unifier
    
    # this rule would be used if authenticated read access is done
    - id: some_service_authenticated_read
      match:
        url: http://127.0.0.1:9090/foo/bar
        # depending on the requirements
        # either a cookie matcher is specified
        cookie_set:  
           - some_cookie_name
        # or a header matcher is specified. 
        # Or even both
        header_set:
           - some_header_name
        # compare to the rule above
        methods:
          - GET
      execute:
        - authenticator: cookie_session
        - contextualizer: subscription_plan # <-- can now also be used
        - unifier: jwt_unifier

    With that approach there is a complete freedom on how to make use of authorizers and contetualizers for the upstream services.

    The following table summarizes the pros and cons of this alternative.

    Pros Cons
    The rules respectively pipelines remain very simple. Thus easy to maintain and test. Can lead to the explosion of the amount of rules, which may lead to slower look ups of rules
      Matching definitions might become complex
      Less a cons, more a challenge. It is unclear how matching of cookies/headers should happen. Should only the names be taken into account? There are cases, where values might be of interest as well. How to deal with different authentication methods, which work on the same headers?
      Introduces a breaking change for the method property on the rule level if method matching is used.
  2. Enable conditional execution of rule pipeline mechanisms by introducing an if property, which would evaluate CEL conditions and only if the condition is true, let the mechanism execute. This way, we still have one rule, but it would look like follows:

    - id: some_service_rule
      match:
        url: http://127.0.0.1:9090/foo/bar
      methods:
        - GET
        - POST
        - PUT
      execute:
        - authenticator: cookie_session
        - authenticator: anonymous
        # the next contextualizer executes only if the request is not anonymous
        - contextualizer: subscription_plan
          if: Subject.ID != "anonymous"
        # the next authorizer executes only if the request is an anonymous read request
        - authorizer: allow_all
          if: Subject.ID == "anonymous" && Request.Method == "GET"
        # the next authorizer executes only if the request is an anonymous write request
        - authorizer: deny_all
          if: Subject.ID == "anonymous" && (Request.Method == "POST" || Request.Method == "PUT")
        # the next authorizer executes only if the request is a write request
        - authorizer: can_write
          if: Request.Method == "POST" || Request.Method == "PUT" 
        - unifier: jwt_unifier

    This approach is by far less verbose compared to the previous one, might however lead to pretty long and hard to test/maintain rules compared to the approach 1 and might change the semantic of the response for HTTP methods (see deny_all authorizer above, which would lead to 403 Forbidden instead of 405 Method not Allowed). So maybe there will be a need to have response code overrides for errors on the level of the error handlers in addition. It gives however complete freedom on how to make use of authorizers and contetualizers for the upstream services as well.

    The following table summarizes the pros and cons of this alternative.

    Pros Cons
    Less verbose for relatively simple rules Can lead to long and hard to test/maintain rule pipelines
    Less rules. So faster rule look up Might change the semantic of the responses for HTTP methods if not addressed on an error handler level.
    No limitations related to header/cookie matching compared to alternative 1
    No breaking changes
  3. Extend the implementation of contextualizers to enable customization of response code verification. As of now the implementation expects only 200 codes. All other response codes are treated as error, leading to the execution of the error pipeline. This might be the simplest possible implementation, which would allow use cases as described in the background section, but is by far less powerful compared to the previous two, so that there will be still situations, which would not be supported.

    Pros Cons
    Rules remain very simple and not verbose Might not support further use cases
    Easier to test and maintain compared to alternative 2
    Less rules compared to alternative 1. So faster rule look up
    No limitations related to header/cookie matching compared to alternative 1
    No breaking changes

Are there any workarounds or alternatives?

Don't use contextualizers and authorizers in heimdall in such cases and let the upstream services implement the corresponding functionality. This is however something, heimdall wants the upstream services to help with and resolve the corresponding dependencies. If the upstream service has to deal with that complexity anyway (because heimdall does not support all such cases), why using contextualizers and authorizers at all?

Alternatively let the services used by contextualizers and authorizers cope with anonymous requests, respectively user data, which them usually don't know. Can work. Might be problematic in many cases as written in the background section.

Version

v0.6.1-alpha

Additional Context

No response

@dadrus dadrus added the feature Used for new features label Mar 10, 2023
@dadrus dadrus changed the title Support for different authentication mechanisms for the same URLs Support for different authorization/contextualization mechanisms for the same URLs having different authentication contexts Mar 10, 2023
@dadrus dadrus added this to the v0.7.0-alpha milestone Mar 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Used for new features
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant