Пишем web на Python3, определяем разрешения пользователей

newbie

Опубликован:  2019-03-16T09:46:59.989267Z
Отредактирован:  2019-03-19T08:33:15.123231Z

Продолжаем работу над проектом selfish. Очередной обзор посвящен разрешениям пользователей в сервисе, сейчас я разработаю основную логику, которая позволит дифференцировать авторизованных пользователей сервиса, что в свою очередь даст мне возможность на следующих этапах разработки предоставить пользователям сервиса возможности в соответствии с определёнными разрешениями.

Приложение selfish в перспективе получит некоторый набор возможных, доступных авторизованным пользователям сервиса функциональных действий в соответствии с начальным замыслом, при этом не все пользователи смогут иметь одинаковые права и, соответственно, доступ к функционалу приложения. Настало время раскрыть очередную порцию начального замысла и определиться с будущими возможностями сервиса.

Итак, selfish - это частная платформа для ведения блогов в сети Интернет и в перспективе будет предоставлять своим пользователям следующие возможности:

  • возможность быть заблокированным в случае неадекватного поведения;
  • возможность читать блоги других пользователей сервиса;
  • возможность создавать ленту и добавлять в неё наиболее интересных авторов блогов;
  • возможность отправлять приватные сообщения другим пользователям сервиса;
  • возможность оставлять публичные (доступные для чтения всем пользователям сервиса) комментарии в блогах других пользователей и в своём блоге;
  • возможность вести собственный блог;
  • возможность следить за порядком в сервисе и блокировать или удалять несоответствующие политике сервиса топики блогов и публичные комментарии, а так же изменять разрешения других пользователей;
  • возможность администрировать сервис и неограниченно выполнять любые предусмотренные сервисом действия.

Итого мне необходимо описать в терминах Питона восемь функциональных возможностей, а затем предоставить эти возможности выборочно авторизованным пользователям. Слово выборочно в данном случае означает, что какие-то пользователи будут иметь разрешение на выполнение того или иного действия в сервисе, а какие-то - нет. Так же это означает, что пользователи будут условно разделены на группы:

  • Изгнанные;
  • Читатели;
  • Комментаторы;
  • Блогеры;
  • Модераторы;
  • Администраторы.

Принадлежность к группе будет определяться наличием у конкретного пользователя разрешения на выполнение тех или иных действий. Таков начальный замысел, приступим к его осуществлению...

Открываю терминал, вхожу в базовый каталог selfish, активирую виртуальное окружение и создаю новый файл в каталоге selfish/models.

touch selfish/models/auth_units.py

Открываю этот файл в редакторе PyCharm и создаю в нём три новых объекта.

from collections import namedtuple

Permit = namedtuple(
    'Permit', ['CANNOT_LOG_IN',
               'READ_JOURNAL',
               'FOLLOW_USERS',
               'SEND_PM',
               'COMMENT',
               'WRITE_JOURNAL',
               'POLICE',
               'ADMINISTER'])

permissions = Permit(
    CANNOT_LOG_IN='заблокирован',
    READ_JOURNAL='читать журнал',
    FOLLOW_USERS='создавать ленту',
    SEND_PM='писать в приват',
    COMMENT='комментировать журнал',
    WRITE_JOURNAL='вести журнал',
    POLICE='следить за порядком',
    ADMINISTER='без ограничений')

defaults = {permissions.READ_JOURNAL: True,
            permissions.FOLLOW_USERS: True,
            permissions.SEND_PM: True,
            permissions.COMMENT: True,
            permissions.WRITE_JOURNAL: True}

Два целевых объекта этого файла: permissions и defaults определяют формулировки разрешений и набор разрешений для вновь регистрируемых пользователей. Поскольку набором разрешений вновь регистрируемых пользователей мне хочется управлять посредством web-интерфейса в будущем, создаю соответствующий объект в базе данных, для этого открываю в редакторе файл selfish/models/auth.py, импортирую в нём целевые объекты из auth_units и определяю новый класс.

...
from .auth_units import defaults, permissions
...


class Permission(db.Model):
    __tablename__ = 'permissions'

    id = db.Column(db.Integer, primary_key=True)
    permission = db.Column(db.String(32), unique=True, nullable=False)
    initial = db.Column(db.Boolean, default=False, nullable=False)

В поле permission я буду хранить формулировку разрешения, а поле initial определяет, будут ли вновь регистрируемые пользователи иметь данное разрешение. Определяю порядок инициализации экземпляров этого класса.

    def __init__(self, **kwargs):
        db.Model.__init__(self, **kwargs)
        if 'permission' not in kwargs:
            raise MissingArgument('missing required argument: "permission"')
        if not kwargs['permission'] or \
                not isinstance(kwargs['permission'], str):
            raise BadArgument('bad required argument: "permission"')

Для инициализации экземпляров класса Permission определён один обязательный аргумент - permission. Определяю возвращаемое экземпляром класса значение.

    def __repr__(self):
        return '<Permission: {}>'.format(self.permission)

С помощью специального метода определяю порядок начального заполнения этой таблицы базы данных.

    @classmethod
    def insert_permissions(cls):
        for permission in cls.query:
            if permission.permission not in permissions:
                db.session.delete(permission)
        for permission in permissions:
            p = cls.query.filter_by(permission=permission).first()
            if p is None:
                p = cls(
                    permission=permission,
                    initial=defaults.get(permission, False))
                db.session.add(p)
        db.session.commit()

Метод insert_permissions использует целевые объекты из auth_units, проверяет наличие в таблице существующих записей, их соответствие объекту permissions и в случае необходимости добавляет в таблицу отсутствующие записи.

Каждый пользователь может иметь больше одного разрешения, это необходимо отразить в соответствующей модели - User. Делаю в файле ещё один импорт и дописываю в User свойство permissions.

...
from sqlalchemy.dialects import postgresql
...


class User(UserMixin, db.Model):
    __tablename__ = 'users'

    ...
    permissions = db.Column(postgresql.ARRAY(db.String(32)))

Кроме этого, мне необходим инструмент, который мог бы определять наличие у конкретного пользователя конкретного разрешения, дописываю в User ещё один метод.

    def can(self, permission):
        return permission in self.permissions

Так как этот метод текущего пользователя достаточно часто будет использоваться в логике приложения и в шаблонах, его следует уравновесить соответствующим методом в классе AnonymousUser.

class AnonymousUser(AnonymousUserMixin):
    def can(self, permission):
        if permission:
            return False
        return False

О новых объектах приложения нужно сообщить менеджеру проекта, открываю файл manage.py, импортирую в нём новые объекты и добавляю их в контекст служебной консоли.

1tVc5HRPWu.png

Иду в терминал и делаю очередную миграцию базы данных.

python manage.py db migrate -m"Create Permission and Modify User"

2mcVpxZOLf.png

Обновляю данные в базе данных.

python manage.py db upgrade

NmNHY2y6qm.png

Подгружаю служебную консоль приложения и заполняю таблицу permissions.

>>> Permission.insert_permissions()
>>> 

В моей базе данных есть один зарегистрированный пользователь, фильтрую его.

>>> newbie = User.query.filter_by(username="newbie").first()
>>> newbie
<User: newbie>
>>> 

Когда я создавал этого пользователя, у приложения ещё не было пользовательских разрешений, поэтому у этого пользователя в свойстве permissions лежит голый None.

>>> newbie.permissions is None
True
>>> 

Даю пользователю начальные для вновь зарегистрированных разрешения.

>>> newbie.permissions = [permission.permission for permission in Permission.query if permission.initial is True]
>>> newbie.permissions
['читать журнал', 'создавать ленту', 'писать в приват', 'комментировать журнал', 'вести журнал']
>>> db.session.add(newbie)
>>> db.session.commit()
>>> 

Проверяю работу метода can на примере существующего пользователя и его наборе разрешений.

>>> newbie.can(permissions.ADMINISTER)
False
>>> newbie.can(permissions.CANNOT_LOG_IN)
False
>>> newbie.can(permissions.WRITE_JOURNAL)
True
>>> 

0W5Z3EFfy2.png

Таким образом для каждого пользователя в сервисе я имею возможность проверить наличие у него того или иного разрешения, что и будет использоваться при проектировании серверной логики приложения на следующих этапах разработки, для этого мне потребуется добавить в приложение ещё пару инструментов.

Общую для всех функций представления логику, осуществляющую проверку разрешений текущего пользователя, я выделю в декоратор. Создаю новый файл.

touch selfish/deco.py

Открываю этот файл в редакторе и пишу в него следующий код.

bp5rKfZwwo.png

Функция permission_required является декоратором и добавляет декорируемой функции представления проверку наличия заданного разрешения у совершающего запрос текущего пользователя при помощи определённого в классах User и AnonymousUser метода can. В зависимости от результата проверки permission_required будет прерывать исполнение декорируемой функции представления вызовом abort в случае, если текущий пользователь не имеет заданного разрешения. В ином случае будет исполнен код декорируемой функции представления.

Поскольку в составе selfish появился вызов abort(403) для http-ошибки 403 следует добавить соответствующий обработчик. Открываю файл selfish/main/errors.py и дописываю в нём ещё одну функцию.

@main.app_errorhandler(403)
def deny_request(e):
    return render_template(
        'error.html', error=e.code, reason='Доступ запрещён'), e.code

Кроме этого, мне понадобится проверка разрешений пользователя в логике шаблонов, для этого включаю объект permissions из модуля auth_units в контекст шаблонизатора. Открываю файл selfish/main/__init__.py и дописываю в него новую функцию.

...
from ..models.auth_units import permissions
...


@main.app_context_processor
def inject_tools():
    return {'permissions': permissions}

C этого момента в каждом шаблоне selfish объект permissions будет доступен, что позволит мне определять элементарную логику шаблона опираясь на разрешения пользователя.

Текущий код selfish можно увидеть в моём профиле на gitlab.com. Разработанные только что инструменты пригодятся мне уже в ближайшее время, и в одном из ближайших выпусков блога я покажу, как с ними обращаться. Продолжение следует и в следующем выпуске я создам в базе данных записи для новых пользователей, разделю их на группы, и у приложения появится администратор. Как всегда, вопросы, замечания и предложения можно оставлять в комментариях ниже.., с удовольствием на них отвечу.

Комментарии: