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

django_oauth_toolkit.DjangoOAuthToolkitScheme with DRF permission_classes boolean AND and OR fails to see DOT permissions #1273

Open
n2ygk opened this issue Aug 6, 2024 · 0 comments

Comments

@n2ygk
Copy link
Contributor

n2ygk commented Aug 6, 2024

Describe the bug
When using a somewhat complex permission classes statement that uses boolean operations as shown below, the iteration over the list of permission_classes misses the oauth2 permission classes under the booleans:

    #: permissions are any one of, each requiring demo-djt-sla-* scope:
    #: 1. Authenticated Columbia user: auth-columbia scope plus required user claims.
    #: 2. Authenticated DOT user: scopes as above plus DOT required user claims.
    #: 3. Client Credentials (backend-to-backend): auth-none. No auth user. Claims don't exist.
    permission_classes = [
        TokenMatchesOASRequirements
        & (
            (IsAuthenticated & ColumbiaGroupClaimPermission & ColumbiaSubClaimPermission)
            | (IsAuthenticated & DOTGroupClaimPermission & DOTSubClaimPermission)
            # | (IsAuthenticated & DOTSubClaimPermission)
            | (~IsAuthenticated)
        )
    ]

The end result is that the security requirements object is not added to the OAS schema document.

This is because DjangoOAuthToolkitScheme.get_security_requirement() only looks across the list of permission_classes and does not descend the possible ANDs and ORs via op1 and op2:

      for permission in auto_schema.view.get_permissions():
            if isinstance(permission, TokenMatchesOASRequirements):
                alt_scopes = permission.get_required_alternate_scopes(request, view)
                alt_scopes = alt_scopes.get(auto_schema.method, [])
                return [{self.name: group} for group in alt_scopes]
            if isinstance(permission, IsAuthenticatedOrTokenHasScope):
                return {self.name: TokenHasScope().get_scopes(request, view)}
            if isinstance(permission, TokenHasScope):
                # catch-all for subclasses of TokenHasScope like TokenHasReadWriteScope
                return {self.name: permission.get_scopes(request, view)}

Perhaps a better approach would be to recursively descend all the permission_classes resulting in a flattened list and see if any of the scope-related classes is present or just look for presence of the required_alternate_scopes or the required_scopes view attributes?

I've done a temporary hack of tacking this on to the end of the above logic:

            # bail if none of the above and use view.required_alternate_scopes based on the view.action
            # this is basically the same as above.
            if getattr(view,"required_alternate_scopes") is None:
                return {self.name: []}
            return [{self.name: a} for a in view.required_alternate_scopes[auto_schema.method]]

To Reproduce

Clone the sample code at https://github.com/columbia-it/django-jsonapi-training/tree/spectacular

export OIDC_RSA_PRIVATE_KEY_FILE=./oidc.key
export SPECTACULAR=true
./manage.py spectacular --file schema.yaml

Tracing this in a debugger shows that the only permission_class in the list is the top-level <rest_framework.permissions.AND> with op1 = <oauth2_provider.contrib.rest_framework.permissions.TokenMatchesOASRequirements> and op2 = <rest_framework.permissions.OR> and so on.

Expected behavior
I expect that the security requirements object be added to the schema resulting in, for example:

    post:
      operationId: course_terms_create
      description: |-
        A specific course term (year+semester) instance.
        e.g. 20183COMSW1002
      tags:
      - course_terms
      requestBody:
        content:
          application/vnd.api+json:
            schema:
              $ref: '#/components/schemas/CourseTermRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/CourseTermRequest'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/CourseTermRequest'
        required: true
      security:
      - oauth2:
        - auth-columbia
        - demo-djt-sla-bronze
        - openid
        - https://api.columbia.edu/scope/group
        - create
      - oauth2:
        - auth-none
        - demo-djt-sla-bronze
        - create
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant