After writing far too many if has_permission(user):
statements at the head of my methods, I figured I would try my hand at writing a generic enough decorator to do it for me. This is what I created:
from inspect import signature
def permissions(callback, **perm_kwargs):
'''This class decorator is used to define permission levels for individual
methods of the decorated class.
Attempting to call a method named by this decorator will first invoke the
given callback. If the callback returns True, the user is authorized and
the method is called normally. If it returns False, a permission error is
raised instead and the method is not called.'''
def wrap(wrapped_class):
class PermissionsWrapper:
def __init__(self, *args, **kwargs):
self.wrapped = wrapped_class(*args, **kwargs)
self.permission_levels = {}
for name, level in perm_kwargs.items():
self.wrap_method(name, level)
def __getattr__(self, attr_name):
attr = getattr(self.wrapped, attr_name)
if attr_name not in self.permission_levels:
return attr
def wrapper_func(*args, **kwargs):
user = PermissionsWrapper.unpack_user(attr, *args, **kwargs)
if callback(user, self.permission_levels[attr_name]):
attr(*args, **kwargs)
else:
raise RuntimeError('Permission Denied.')
return wrapper_func
@staticmethod
def unpack_user(method, *args, **kwargs):
try:
# Verify that the arguments are lexically valid.
bindings = signature(method).bind(*args, **kwargs)
return bindings.arguments['user']
except TypeError:
# The arguments are invalid. Call the method with the bad
# arguments so that a more descriptive exception is raised.
method(*args, **kwargs)
def wrap_method(self, method_name, perm_level):
if not hasattr(wrapped_class, method_name):
error = 'No such method: {}'.format(method_name)
raise RuntimeError(error)
method = getattr(wrapped_class, method_name)
if not hasattr(method, '__call__'):
error = 'Attribute {} is not a method!'.format(method_name)
raise RuntimeError(error)
method_sig = signature(method)
if 'user' not in method_sig.parameters:
error = ('Method signature does not have a "user" argument!'
' The user argument will have its permission level'
' verified and either be allowed to use the method'
', or a permission error will be thrown.')
raise RuntimeError(error)
self.permission_levels[method_name] = perm_level
return PermissionsWrapper
return wrap
You could then use the decorator like this:
def has_permission(user, required_level):
if the_user_meets_the_required_level_of_permissions():
return True
else:
return False
@permissions(callback=has_permission,
destroy_the_company='ADMIN_ONLY', view_profile='GUEST',
edit_profile='LOGGED_IN', post_comment='GUEST')
class ProfileManager:
def edit_profile(self, user, form_data):
pass
def view_profile(self, user):
pass
def post_comment(self, user, comment_text):
pass
def destroy_the_company(self, user):
drop_all_database_tables()
overwrite_backups()
At the time of writing, it seemed obvious to me that using a decorator on the whole class would be more self-documenting and require less typing than using a separate decorator on each method. Now, I'm not so sure.
- Does this offer any advantages/disadvantages over a simpler per-method decorator?
- Are there any edge cases where this decorator will fail?
EDIT: Fixed the decorator by adding the unpack_user
method and calling it in wrapper_func
.