Permission plugin

Permission plugin enables features:

  1. Attach decorators to routers.

  2. 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.

  3. Pre-parsing (sanitizing) input data for patching (PATCH method) and creating objects (POST method).

  4. Check if user can delete an object.

How to use

To create a permission system:

  1. Inherit a class from combojsonapi.permission.permission_system.PermissionMixin (detailed below).

  2. In resource manager, specify which methods use this permissions class in data_layer.

  3. If you need to disable permission decorators for the resource, set the following attribute: disable_global_decorators.

  4. Shared permissions are applied automatically by permission_manager https://flask-combo-jsonapi.readthedocs.io/en/latest/permission.html. To disable it, set disable_permission attribute. Example:

class AuthResource(ResourceList):
    disable_permission = True
    disable_global_decorators = True
    ...
  1. 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 Meta class in attribute required_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 obj object. Object won’t be deleted if any delete method 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, which get method will be requested from;

  • permission_post: List - list of classes, which post_permission and post_data methods will be requested from;

  • permission_patch: List - list of classes, which patch_permission and patch_data methods will be requested from;

  • permission_delete: List - list of classes, which delete method 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>, ...)