Permission plugin¶
Permission plugin enables features:
Attach decorators to routers.
- Restrict what data is returned on objects (GET method):
by attribute (they won’t be requested from database unless mentioned specifically);
by rows;
by rows based on complex filters, e. g. accessible for users in a specific group, or group owner.
Pre-parsing (sanitizing) input data for patching (PATCH method) and creating objects (POST method).
Check if user can delete an object.
How to use¶
To create a permission system:
Inherit a class from
combojsonapi.permission.permission_system.PermissionMixin(detailed below).In resource manager, specify which methods use this permissions class in
data_layer.If you need to disable permission decorators for the resource, set the following attribute:
disable_global_decorators.Shared permissions are applied automatically by
permission_managerhttps://flask-combo-jsonapi.readthedocs.io/en/latest/permission.html. To disable it, setdisable_permissionattribute. Example:
class AuthResource(ResourceList):
disable_permission = True
disable_global_decorators = True
...
By default only required fields and allowed by permission fields are fetched from database. If your model uses property which refers to a property which was not fetched (was not requested in your query), you need to declare required fields in the model’s
Metaclass in attributerequired_fields. Otherwise sqlalchemy will fetch these fields using additional db requests which will slow down your request. For example:
class User:
class Meta:
required_fields = {
'full_name': ['first_name', 'second_name'],
}
first_name = Column()
second_name = Column()
@property
def full_name(self):
return ' '.join([self.first_name, self.second_name])
...
PermissionMixin class API¶
Properties:
permission_for_get: PermissionForGet
User permissions for GET method. Contains properties:
filters: List- filters list to apply when requesting objects. E. g., it’s possible to allow user to view his profile only, not anyone else’s.
joins: List- models list to join when requesting objects. E. g. allow a user to view users of group he is part of.
allow_columns: Dict[str, int]- allowed model attributes and permission weight (more is higher priority), which is useful for managing more and less restrictive permissions.
forbidden_columns: Dict[str, int]- forbidden model attributes and permission weight.
columns: Set[str]- accessible model attributes after applying all permissions by weight in ascending order.
permission_for_patch: PermissionForPatch
User permissions for PATCH method. Contains properties:
allow_columns: Dict[str, int]- allowed model attributes and permission weight (more is higher priority), which is useful for managing more and less restrictive permissions.
forbidden_columns: Dict[str, int]- forbidden model attributes and permission weight.
columns: Set[str]- accessible model attributes after applying all permissions by weight in ascending order.
permission_for_post: PermissionForPost
User permissions for POST method. Contains properties:
allow_columns: Dict[str, int]- allowed model attributes and permission weight (more is higher priority), which is useful for managing more and less restrictive permissions.
forbidden_columns: Dict[str, int]- forbidden model attributes and permission weight.
columns: Set[str]- accessible model attributes after applying all permissions by weight in ascending order.
Methods:
get(self, *args, many=True, user_permission: PermissionUser = None, **kwargs) -> PermissionForGet
GET method permissions for current user, described in PermissionForGet
bool many- if model is requested via ResourceList (True) or ResourceDetail (False);
PermissionUser user_permission- permissions for current logged in user; all permissions are available, including other models and methods (GET, POST, PATCH).
post_data(self, *args, data=None, user_permission: PermissionUser = None, **kwargs) -> Dict
Pre-parses input data according to permissions. Returns parsed data for the object being created.
Dict data- unparsed data for the object being created;
PermissionUser user_permission- permissions for current logged in user; all permissions are available, including other models and methods (GET, POST, PATCH).
post_permission(self, *args, user_permission: PermissionUser = None, **kwargs) -> PermissionForPost
POST method permissions for current user, described in PermissionForGet
PermissionUser user_permission- permissions for current logged in user; all permissions are available, including other models and methods (GET, POST, PATCH).
patch_data(self, *args, data=None, obj=None, user_permission: PermissionUser = None, **kwargs) -> Dict
Pre-parses input data according to permissions. Returns parsed data for the object being updated.
Dict data- input data validated according to marshmallow schema;
obj- object being updated;
PermissionUser user_permission- permissions for current logged in user; all permissions are available, including other models and methods (GET, POST, PATCH).
patch_permission(self, *args, user_permission: PermissionUser = None, **kwargs) -> PermissionForPatch
PATCH method permissions for current user, described in PermissionForGet
PermissionUser user_permission- permissions for current logged in user; all permissions are available, including other models and methods (GET, POST, PATCH).
delete(self, *args, obj=None, user_permission: PermissionUser = None, **kwargs) -> bool
Permissions check if user is allowed to delete the
objobject. Object won’t be deleted if anydeletemethod returns False.
obj- object being deleted
PermissionUser user_permission- permissions for current logged in user; all permissions are available, including other models and methods (GET, POST, PATCH).
Resource Manager Descriptions¶
In data_layer section you can specify following permission types:
permission_get: List- list of classes, whichgetmethod will be requested from;permission_post: List- list of classes, whichpost_permissionandpost_datamethods will be requested from;permission_patch: List- list of classes, whichpatch_permissionandpatch_datamethods will be requested from;permission_delete: List- list of classes, whichdeletemethod will be requested from;
Usage example¶
model
from enum import Enum
class Role(Enum):
admin = 1
limited_user = 2
user = 3
block = 4
class User(db.Model):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
fullname = Column(String)
email = Column(String)
password = Column(String)
role = Column(Integer)
permission
from combojsonapi.permission.permission_system import PermissionMixin, PermissionForGet, \
PermissionUser, PermissionForPatch
class PermissionListUser(PermissionMixin):
ALL_FIELDS = self_json_api.model.__mapper__.column_attrs.keys()
SHORT_INFO_USER = ['id', 'name']
def get(self, *args, many=True, user_permission: PermissionUser = None, **kwargs) -> PermissionForGet:
"""Setting avatilable columns"""
if current_user.role == Role.admin.value:
self.permission_for_get.allow_columns = (self.ALL_FIELDS, 10)
elif current_user.role in [Role.limited_user.value, Role.user.value]:
# limit attributes and forbid to view blocked users
self.permission_for_get.allow_columns = (self.SHORT_INFO_USER, 0)
self.permission_for_get.filters.append(User.role != Role.block.value)
return self.permission_for_get
class PermissionDetailUser(PermissionMixin):
ALL_FIELDS = self_json_api.model.__mapper__.column_attrs.keys()
AVAILABLE_FIELDS_FOR_PATCH = ['password']
def get(self, *args, many=True, user_permission: PermissionUser = None, **kwargs) -> PermissionForGet:
"""Setting avatilable columns"""
if current_user.role in [Role.limited_user.value, Role.user.value]:
# only current user is allowed to be requested
self.permission_for_get.filters.append(User.id != current_user.id)
return self.permission_for_get
def patch_permission(self, *args, user_permission: PermissionUser = None, **kwargs) -> PermissionForPatch:
"""Only password change is allowed"""
self.permission_for_patch.allow_columns = (self.AVAILABLE_FIELDS_FOR_PATCH, 0)
return self.permission_for_patch
def patch_data(self, *args, data: Dict = None, obj: User = None, user_permission: PermissionUser = None, **kwargs) -> Dict:
# password
password = data.get('password')
if password is not None:
return {'password': hashlib.md5(password.encode()).hexdigest()}
return {}
class PermissionPatchAdminUser(PermissionMixin):
"""Allow admin user to change any field"""
ALL_FIELDS = self_json_api.model.__mapper__.column_attrs.keys()
def patch_permission(self, *args, user_permission: PermissionUser = None, **kwargs) -> PermissionForPatch:
"""Only password change is allowed"""
if current_user.role == Role.admin.value:
self.permission_for_patch.allow_columns = (self.ALL_FIELDS, 10) # задаём вес 10, это будет более приоритетно
return self.permission_for_patch
def patch_data(self, *args, data: Dict = None, obj: User = None, user_permission: PermissionUser = None, **kwargs) -> Dict:
if current_user.role == Role.admin.value:
password = data.get('password')
if password is not None:
data['password'] = hashlib.md5(password.encode()).hexdigest()
return data
return {}
views
class UserResourceList(ResourceList):
schema = UserSchema
method = ['GET']
data_layer = {
'session': db.session,
'model': User,
'short_format': ['id', 'name'],
'permission_get': [PermissionListUser],
}
class UserResourceDetail(ResourceDetail):
schema = UserSchema
method = ['GET']
data_layer = {
'session': db.session,
'model': User,
'short_format': ['id', 'name'],
'permission_get': [PermissionDetailUser],
'permission_patch': [PermissionDetailUser, PermissionPatchAdminUser],
}
__init__
api_json = Api(
app,
decorators=(login_required,),
plugins=[
PermissionPlugin(),
]
)
Example of loading various object attributes depending on the address at which the object was requested¶
permission
from combojsonapi.permission.permission_system import PermissionMixin, PermissionForGet, \
PermissionUser
class PermissionListUser(PermissionMixin):
SHORT_INFO_USER = ['id', 'name']
EXTENDED_USER_INFO = ['id', 'name', 'fullname', 'email', 'role']
ENDPOINTS_FOR_EXTENDED_INFO = ['computer_list', 'phone_list']
def get(self, *args, many=True, user_permission: PermissionUser = None, **kwargs) -> PermissionForGet:
if request.endpoint in self.ENDPOINTS_FOR_EXTENDED_INFO:
self.permission_for_get.allow_columns = (self.EXTENDED_USER_INFO, 10)
else:
self.permission_for_get.allow_columns = (self.SHORT_INFO_USER, 0)
return self.permission_for_get
computer_list, phone_list - endpoints in pattern of the routing system:
api_json.route(<Resource manager>, <endpoint name>, <url_1>, <url_2>, ...)