Пишем 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, импортирую в нём новые объекты и добавляю их в контекст служебной консоли.
Иду в терминал и делаю очередную миграцию базы данных.
python manage.py db migrate -m"Create Permission and Modify User"
Обновляю данные в базе данных.
python manage.py db upgrade
Подгружаю служебную консоль приложения и заполняю таблицу 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 >>>
Таким образом для каждого пользователя в сервисе я имею возможность проверить наличие у него того или иного разрешения, что и будет использоваться при проектировании серверной логики приложения на следующих этапах разработки, для этого мне потребуется добавить в приложение ещё пару инструментов.
Общую для всех функций представления логику, осуществляющую проверку разрешений текущего пользователя, я выделю в декоратор. Создаю новый файл.
touch selfish/deco.py
Открываю этот файл в редакторе и пишу в него следующий код.
Функция 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. Разработанные только что инструменты пригодятся мне уже в ближайшее время, и в одном из ближайших выпусков блога я покажу, как с ними обращаться. Продолжение следует и в следующем выпуске я создам в базе данных записи для новых пользователей, разделю их на группы, и у приложения появится администратор. Как всегда, вопросы, замечания и предложения можно оставлять в комментариях ниже.., с удовольствием на них отвечу.
Метки: | python3x, web, selfish, flask, sqlalchemy |