2

I'm working on a class generator of sorts. I want to be able to create many children of BasePermission that are customizable using a required_scopes attribute. This is what I have so far.

from rest_framework.permissions import BasePermission


class ScopedPermissionMeta(type(BasePermission)):

    def __init__(self, name, bases, attrs):
        try:
            required_scopes = attrs['required_scopes']
        except KeyError:
            raise TypeError(f'{name} must include required_scopes attribute.')

        required_scopes_list = ' '.join(required_scopes)
        attrs['message'] = f'Resource requires scope={required_scopes_list}'

        def has_permission(self, request, _view):
            """Check required scopes against requested scopes."""
            try:
                requested_scopes = request.auth.claims['scope']
            except (AttributeError, KeyError):
                return False

            return all(scope in requested_scopes for scope in required_scopes)

        attrs['has_permission'] = has_permission


class ReadJwtPermission(BasePermission, metaclass=ScopedPermissionMeta):
    required_scopes = ['read:jwt']

However, I do not like how the ReadJwtPermisson class (and the many more children) must specify a metaclass. Idealy I want that detail abstracted away. I want to be able to do something like:

class ScopedPermission(BasePermission, metaclass=ScopedPermissionMeta):
    pass


class ReadJwtPermission(ScopedPermission):
    required_scopes = ['read:jwt']

but in this situation the metaclass see's ScopedPermission and no required_scopes. Is there a way to allow the metaclass to see through this inheritance relationship?

1 Answer 1

3

but in this situation the metaclass see's ScopedPermission and no required_scopes. Is there a way to allow the metaclass to see through this inheritance relationship?

At the moment ScopedPermission class is being created, there is no ReadJwtPermission class. Interpreter can't predict the future that a class is going to subclass ScopedPermission which has required_scopes attribute. But you can do a bit different.

Child classes inherit parent's metaclass. If the parent class uses that metaclass, every child classes have to have that attribute you want. Also I used __new__ to check that attribute "before" the class creation. Here is the example :

class Metaclass(type):
    def __new__(mcs, name, bases, attrs):

        # This condition skips Base class's requiement for having "required_scopes"
        # otherwise you should specify "required_scopes" for Base class as well.
        if name == 'Base':
            return super().__new__(mcs, name, bases, attrs)

        try:
            required_scopes = attrs['required_scopes']
        except KeyError:
            raise TypeError(f'{name} must include "required_scopes" attribute.')

        required_scopes_list = ' '.join(required_scopes)
        attrs['message'] = f'Resource requires scope={required_scopes_list}'

        # setting "has_permission attribute here"
        attrs['has_permission'] = mcs.has_permission()

        return super().__new__(mcs, name, bases, attrs)

    # I just removed the implementation so that I can be able to run this class.
    @staticmethod
    def has_permission():
        pass


class Base(metaclass=Metaclass):
    pass

class A(Base):
    required_scopes = ['read:jwt']

print(A.message)

output :

Resource requires scope=read:jwt

But now with :

class B(Base):
    pass

It raises error...

Traceback (most recent call last):
  File "<--->", line 9, in __new__
    required_scopes = attrs['required_scopes']
KeyError: 'required_scopes'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<--->", line 34, in <module>
    class B(Base):
  File "<--->", line 11, in __new__
    raise TypeError(f'{name} must include "required_scopes" attribute.')
TypeError: B must include "required_scopes" attribute.
Sign up to request clarification or add additional context in comments.

2 Comments

The only thing I changed in your code was class Base(BasePermission, metaclass=ScopedPermissionMeta): and class Metaclass(type(BasePermission)):, otherwise it works as expected, thanks
@BradyDean Great, I just focused on the other part.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.