Reputation: 3573
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?
Upvotes: 1
Views: 416
Reputation: 16476
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.
Upvotes: 2