Плагин PermissionPlugin

Плагин Permission позволяет:

  1. Повесить декораторы на роутеры

  2. Добавить ограничительную логику при выгрузке объектов (GET запрос):
    • ограничить выгрузку по атрибутам (они не будут даже выгружаться из БД, если только специально к ним не обратиться);

    • ограничить выгрузку по строкам;

    • ограничить выгрузку по строкам в зависимости от результатов сложных фильтров, например доступны только те пользователи, которые состоят в группе, в которой данный пользователь является владельцем

  3. Предобработка данных для обновления объекта (PATCH запрос) и создания (POST запрос).

  4. Выполнить проверку на возможность удаления объекта данным пользователем.

Работа с плагином

Чтобы создать систему пермишенов для какой-либо модели, нужно:

  1. Создать класс от combojsonapi.permission.permission_system.PermissionMixin, ниже будет более подробно сказано об этом

  2. В ресурс менеджере в data_layer указать, какие методы должны использовать данный класс с пермишенами

  3. Если нам нужно, чтобы на конкретный ресурс не накручивались пермишн декораторы, необходимо в ресурсе указать атрибут disable_global_decorators

  4. Общие пермишены можно применять даже не накручивая на каждый ресурс отдельно, для этого есть:code:permission_manager https://flask-combo-jsonapi.readthedocs.io/en/latest/permission.html.Для того, чтобы отключить его, нужно у класса назначить disable_permission. Например:

class AuthResource(ResourceList):
    disable_permission = True
    disable_global_decorators = True
    ...
  1. По умолчанию из БД выгружаются только поля, указанные в запросеи разрешенные в пермишенах. Если в вашей модели есть property, который обращается к незапрошенному полю модели (но при этом это поле не нужно возвращать в запросе), то такие зависимости необходимо указать у модели в классе Meta`в атрибуте :code:`required_fields. Например:

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])
    ...

API класса PermissionMixin

Свойства:

permission_for_get: PermissionForGet

Разрешения для пользователя в методе get. Содержит свойства:

  • filters: List - список фильтров, которые нужно применить при выгрузке объектов. Например можем задать, чтобы пользователь при выгрузке видел только себя

  • joins: List - список джойнов, которые нужно присоединить к данному запросу на выгрузку. Например пользователь может увидеть только тех пользователей, которые входят в некую группу, в которую он входит.

  • allow_columns: Dict[str, int] - атрибуты модели, которые доступны и вес доступа, влияет на то что пермишены могут быть разрешающими, а могут быть запрещающими

  • forbidden_columns: Dict[str, int] - атрибуты модели, которые запрещены и вес доступа, влияет на то что пермишены могут быть разрешающими, а могут быть запрещающими

  • columns: Set[str] - атрибуты модели, доступны пользователю, после сложения разрешающий и запрещающих массивов по весу

permission_for_patch: PermissionForPatch

Разрешения для пользователя в методе patch. Содержит свойства:

  • allow_columns: Dict[str, int] - атрибуты модели, которые доступны и вес доступа, влияет на то что пермишены могут быть разрешающими, а могут быть запрещающими

  • forbidden_columns: Dict[str, int] - атрибуты модели, которые запрещены и вес доступа, влияет на то что пермишены могут быть разрешающими, а могут быть запрещающими

  • columns: Set[str] - атрибуты модели, доступны пользователю, после сложения разрешающий и запрещающих массивов по весу

permission_for_post: PermissionForPost

Разрешения для пользователя в методе post. Содержит свойства:

  • allow_columns: Dict[str, int] - атрибуты модели, которые доступны и вес доступа, влияет на то что пермишены могут быть разрешающими, а могут быть запрещающими

  • forbidden_columns: Dict[str, int] - атрибуты модели, которые запрещены и вес доступа, влияет на то что пермишены могут быть разрешающими, а могут быть запрещающими

  • columns: Set[str] - атрибуты модели, доступны пользователю, после сложения разрешающий и запрещающих массивов по весу

Методы:

get(self, *args, many=True, user_permission: PermissionUser = None, **kwargs) -> PermissionForGet

Ограничения на элементы описанные в PermissionForGet для данного пользователя в GET запросах

  • bool many - запрашивают через ResourceList или ResourceDetail

  • PermissionUser user_permission - ограничения для данного пользователя, можно получить доступ к ограничениям по другим моделям для данного пользователя для разных методов (GET, POST, PATCH).

post_data(self, *args, data=None, user_permission: PermissionUser = None, **kwargs) -> Dict

Предобработка данных в соответствие с ограничениями перед создание объекта. Должен вернуть обработанные данные для нового объекта

  • Dict data - данные для создания объекта;

  • PermissionUser user_permission - ограничения для данного пользователя, можно получить доступ к ограничениям по другим моделям для данного пользователя для разных методов (GET, POST, PATCH).

post_permission(self, *args, user_permission: PermissionUser = None, **kwargs) -> PermissionForPost

Ограничения на элементы описанные в PermissionForPost для данного пользователя в POST запросах

  • PermissionUser user_permission - ограничения для данного пользователя, можно получить доступ к ограничениям по другим моделям для данного пользователя для разных методов (GET, POST, PATCH).

patch_data(self, *args, data=None, obj=None, user_permission: PermissionUser = None, **kwargs) -> Dict

Предобработка данных в соответствие с ограничениями перед обновлением объекта. Должен вернуть обработанные данные для обновления объекта

  • Dict data - входные данные, прошедшие валидацию через схему marshmallow;

  • obj - обновляемый объект из БД;

  • PermissionUser user_permission - ограничения для данного пользователя, можно получить доступ к ограничениям по другим моделям для данного пользователя для разных методов (GET, POST, PATCH).

patch_permission(self, *args, user_permission: PermissionUser = None, **kwargs) -> PermissionForPatch

Ограничения на элементы описанные в PermissionForPatch для данного пользователя в PATCH запросах

  • PermissionUser user_permission - ограничения для данного пользователя, можно получить доступ к ограничениям по другим моделям для данного пользователя для разных методов (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). Если хотя бы одна из функций, вернёт False, то удаление не произойдёт

  • PermissionUser user_permission - ограничения для данного пользователя, можно получить доступ к ограничениям по другим моделям для данного пользователя для разных методов (GET, POST, PATCH).

Описания в ресурс менеджерах

В разделе data_layer можно указать следующие типы пермишенов:

  • permission_get: List - список классов, из которых будет запрашиваться метод get

  • permission_post: List - список классов, из которых будет запрашиваться метод post_permission и post_data

  • permission_patch: List - список классов, из которых будет запрашиваться метод patch_permission и patch_data

  • permission_delete: List - список классов, из которых будет запрашиваться метод delete

Пример подключения плагина

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(),
    ]
)

Пример выгрузки различных атрибутов объекта в зависимости от адреса, по которому был запрошен объект

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 - endpoint’ы схемы системы маршрутизации:

api_json.route(<Resource manager>, <endpoint name>, <url_1>, <url_2>, ...)