Selfish, разрешения пользователей

avm

Опубликован:  2018-03-19T09:52:36.895821Z
Отредактирован:  2018-03-19T09:50:22.360542Z
1501
Представлено пошаговое описание процесса разработки пользовательских разрешений web-приложения и продолжения создания системы авторизации пользователей. В результате выполнения описанных здесь действий Selfish научится различать пользователей по группам и предоставлять им разные возможности на основе системы пользовательских разрешений.

1. В предыдущих сериях

Selfish - web-приложение на базе Python3 и Flask. Начальная конфигурация Selfish детально описана в порядке следования в следующих топиках:

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

В этом описании будет продолжена разработка системы авторизации пользователей, будет разработана система пользовательских разрешений, пользователи будут разделены на несколько групп и получат разные возможности.

2. Модификация пользовательского аккаунта

В этом описании будут созданы инструменты необходимые для регистрации суперпользователя приложения Selfish. Чтобы подготовиться к этому, мне необходимо модифицировать модель пользовательского аккаунта. Дело в том, что в перспективе система авторизации Selfish будет предусматривать смену пользователем своего почтового ящика зарегистрированного в аккаунте. Эта возможность будет реализована в два шага: запрос на смену адреса, подтверждение смены адреса. Для хранения нового адреса в промежутке между запросом и подтверждением мне понадобится ещё одно свойство в модели аккаунта. Поскольку при регистрации суперпользователя это свойство аккаунта будет опрашиваться, модифицировать аккаунт лучше уже сейчас.

Открываю файл selfish/models/auth.py и дописываю новое свойство классу Account.

class Account(db.Model):
    ...
    address = db.Column(db.String(128), unique=True, nullable=False)
    swap = db.Column(db.String(128), default=None)                    # новая строка
    ava_hash = db.Column(db.String(32), nullable=False)
    ...

Мигрирую.

(venv) sadmin@debian:~/workspace/selfish$ python manage.py db migrate -m"Modify Account"
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.ddl.postgresql] Detected sequence named 'users_id_seq' as owned by integer column 'users(id)', assuming SERIAL and omitting
INFO  [alembic.ddl.postgresql] Detected sequence named 'accounts_id_seq' as owned by integer column 'accounts(id)', assuming SERIAL and omitting
INFO  [alembic.autogenerate.compare] Detected added column 'accounts.swap'
  Generating /home/sadmin/workspace/selfish/migrations/versions/2ca0d1617358_modif
  y_account.py ... done
(venv) sadmin@debian:~/workspace/selfish$ 

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

(venv) sadmin@debian:~/workspace/selfish$ python manage.py db upgrade
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade d1993d37afb0 -> 2ca0d1617358, Modify Account
(venv) sadmin@debian:~/workspace/selfish$ 

Подключаюсь к базе данных при помощи psql.

(venv) sadmin@debian:~/workspace/selfish$ psql -h 192.168.56.102 -d selfish_d
Пароль: 
psql (9.6.6)
SSL-соединение (протокол: TLSv1.2, шифр: ECDHE-RSA-AES256-GCM-SHA384, бит: 256, сжатие: выкл.)
Введите "help", чтобы получить справку.

selfish_d=> 

И смотрю на содержимое таблицы accounts.

selfish_d=> SELECT * FROM accounts;
 id |      address      |             ava_hash             |         requested          | user_id | swap 
----+-------------------+----------------------------------+----------------------------+---------+------
  1 | support@elardy.ru | 6c3df0f4a4ba25c87238b4d53825e07c | 2018-03-09 09:04:15.712141 |         | 
  2 | avm@elardy.ru     | a9032ef0ef224f943e4a07d09bf7eab9 | 2018-03-10 07:02:19.925446 |       1 | 
(2 строки)

selfish_d=> 

Результат устраивает, теперь можно двигаться дальше.

3. Будущий функционал web-приложения

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

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

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

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

  • Заблокированные;
  • Читатели;
  • Комментаторы;
  • Блогеры;
  • Модераторы;
  • Администраторы.

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

4. Описание разрешений пользователей

Создаю новый файл в каталоге selfish/models.

(venv) sadmin@debian:~/workspace/selfish$ touch selfish/models/auth_units.py
(venv) sadmin@debian:~/workspace/selfish$ 

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

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. Первый определяет формулировки разрешений, второй определяет набор разрешений для вновь регистрируемых пользователей.

Открываю файл selfish/models/auth.py, импортирую в нём эти объекты.

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)
    default = db.Column(db.Boolean, default=False, nullable=False)

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

Далее определяю порядок инициализации экземпляров этого класса.

    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,
                    default=defaults.get(permission, False))
                db.session.add(p)
        db.session.commit()

Метод insert_permissions, используя импортированный из auth_units.py объект permissions, в своём первом цикле проверяет, все ли существующие в базе данных разрешения имеются в составе объекта permissions, и, если какое-то существующее в базе данных разрешение не найдено в permissions, то оно удаляется из базы данных. Во втором цикле этого метода для каждого разрешения в объекте permissions фильтруется соответствующая запись из базы данных, если такая запись отсутствует, то в базу данных добавляется новая запись с соответствующими значениями, где для свойства default значение берётся из объекта defaults.

Класс Permission необходимо связать отношением с классом User. Поскольку каждый пользователь может иметь больше одного разрешения, и каждое разрешение может принадлежать более чем одному пользователю, у этого отношения с обоих сторон находится many - Many-to-Many. Создаю новую таблицу, в файле selfish/models/auth.py пишу следующее.

bonds = db.Table(
    'bonds',
    db.Column('permission_id', db.Integer, db.ForeignKey('permissions.id')),
    db.Column('user_id', db.Integer, db.ForeignKey('users.id')))

Чтобы завершить создание отношения, дописываю классу User следующее свойство.

class User(UserMixin, db.Model):
    ...
    permissions = db.relationship(
        'Permission',
        secondary=bonds,
        backref=db.backref('users', lazy='dynamic'),
        lazy='dynamic')

    ...

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

...
from selfish.models.auth import Account, Permission, User
from selfish.models.auth_units import permissions

...

def make_shell_context():
    return {'app': app, 'db': db, 'Account': Account, 'User': User,
            'Permission': Permission, 'permissions': permissions}

Мигрирую.

(venv) sadmin@debian:~/workspace/selfish$ python manage.py db migrate -m"Create Permission"
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'permissions'
INFO  [alembic.autogenerate.compare] Detected added table 'bonds'
INFO  [alembic.ddl.postgresql] Detected sequence named 'users_id_seq' as owned by integer column 'users(id)', assuming SERIAL and omitting
INFO  [alembic.ddl.postgresql] Detected sequence named 'accounts_id_seq' as owned by integer column 'accounts(id)', assuming SERIAL and omitting
  Generating /home/sadmin/workspace/selfish/migrations/versions/632025f76e86_creat
  e_permission.py ... done
(venv) sadmin@debian:~/workspace/selfish$ 

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

(venv) sadmin@debian:~/workspace/selfish$ python manage.py db upgrade
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade 2ca0d1617358 -> 632025f76e86, Create Permission
(venv) sadmin@debian:~/workspace/selfish$ 

Подключаюсь к базе данных в psql и смотрю, что изменилось. Появились новые таблицы.

selfish_d=> \dt
               Список отношений
 Схема  |       Имя       |   Тип   | Владелец 
--------+-----------------+---------+----------
 public | accounts        | таблица | sadmin
 public | alembic_version | таблица | sadmin
 public | bonds           | таблица | sadmin
 public | permissions     | таблица | sadmin
 public | users           | таблица | sadmin
(5 строк)

selfish_d=> 

Обе новые таблицы базы данных на текущий момент не имеют ни одной записи.

selfish_d=> SELECT * FROM bonds;
 permission_id | user_id 
---------------+---------
(0 строк)

selfish_d=> SELECT * FROM permissions;
 id | permission | default 
----+------------+---------
(0 строк)

selfish_d=> 

База данных готова к определению разрешений пользователей.

5. Конкретный пользователь и его разрешения

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

selfish_d=> SELECT id, username, registered FROM users;
 id | username |         registered         
----+----------+----------------------------
  1 | avm      | 2018-03-10 07:02:19.920333
(1 строка)

selfish_d=> 

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

(venv) sadmin@debian:~/workspace/selfish$ python manage.py shell
>>> 

Первым делом заполняю таблицу permissions.

>>> Permission.insert_permissions()
>>> 

В psql проверяю результат работы данного вызова.

selfish_d=> SELECT * FROM permissions;
 id |      permission       | default 
----+-----------------------+---------
  1 | заблокирован          | f
  2 | читать журнал         | t
  3 | создавать ленту       | t
  4 | писать в приват       | t
  5 | комментировать журнал | t
  6 | вести журнал          | t
  7 | следить за порядком   | f
  8 | без ограничений       | f
(8 строк)

selfish_d=> 

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

Фильтрую из базы данных единственного пользователяavm.

>>> avm = User.query.filter_by(username='avm').first()
>>> avm
<User: avm>
>>> 

И добавляю этому пользователю все разрешения с меткой default=True.

>>> for permission in Permission.query:
...     if permission.default:
...         avm.permissions.append(permission)
... 
>>> db.session.add(avm)
>>> db.session.commit()
>>> 

Теперь можно удостовериться, какие разрешения имеет пользователь avm.

>>> for permission in avm.permissions:
...   print('avm может:', permission.permission)
... 
avm может: читать журнал
avm может: создавать ленту
avm может: писать в приват
avm может: комментировать журнал
avm может: вести журнал
>>> 

В базе данных это выглядит так.

selfish_d=> SELECT u.username, p.permission
selfish_d-> FROM users AS u, permissions AS p, bonds AS b
selfish_d-> WHERE u.username='avm'
selfish_d-> AND p.id=b.permission_id
selfish_d-> AND u.id=b.user_id;
 username |      permission       
----------+-----------------------
 avm      | читать журнал
 avm      | создавать ленту
 avm      | писать в приват
 avm      | комментировать журнал
 avm      | вести журнал
(5 строк)

selfish_d=> 

Для полного счастья мне не хватает инструмента, который мог бы определять наличие конкретного разрешения у конкретного пользователя. Выхожу из консоли, открываю файл selfish/models/auth.py и дописываю классу User ещё один метод.

class User(UserMixin, db.Model):
    ...

    def can(self, permission):
        permitted = [p.permission for p in self.permissions]
        return permission in permitted

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

>>> avm = User.query.filter_by(username='avm').first()
>>> avm.can(permissions.ADMINISTER)
False
>>> avm.can(permissions.CANNOT_LOG_IN)
False
>>> avm.can(permissions.WRITE_JOURNAL)
True
>>> 

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

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

Великолепно. Осталось определить группы пользователей.

6. Группы

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

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

Group = namedtuple(
    'Group',
    ['pariah',
     'taciturn',
     'commentator',
     'blogger',
     'curator',
     'root'])

groups = Group(
    pariah='Заблокированные',
    taciturn='Читатели',
    commentator='Комментаторы',
    blogger='Блогеры',
    curator='Модераторы',
    root='Администраторы')

Объект groups даст возможность в будущем легко поправить названия групп в одном месте, если такая необходимость возникнет.

Чтобы определить принадлежность пользователя к той или иной группе, я задам классу User ещё одно свойство. Открываю файл selfish/models/auth.py, импортирую в нём объект groups.

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

И дописываю в класс User следующий код.

class User(UserMixin, db.Model):
    ...

    @property
    def group(self):
        if self.can(permissions.ADMINISTER):
            return groups.root
        if self.can(permissions.POLICE):
            return groups.curator
        if self.can(permissions.WRITE_JOURNAL):
            return groups.blogger
        if self.can(permissions.COMMENT) or self.can(permissions.SEND_PM):
            return groups.commentator
        if self.can(permissions.READ_JOURNAL):
            return groups.taciturn
        if self.can(permissions.CANNOT_LOG_IN):
            return groups.pariah

    @group.setter
    def group(self, group):
        raise AttributeError('group cannot be set directly')

Проверяю в консоли на примере пользователя avm.

>>> avm = User.query.filter_by(username='avm').first()
>>> avm
<User: avm>
>>> avm.group
'Блогеры'
>>> avm.group = 'Начальники'
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/sadmin/workspace/selfish/selfish/models/auth.py", line 159, in group
    raise AttributeError('group cannot be set directly')
AttributeError: group cannot be set directly
>>> 

С помощью этого простого свойства в дальнейшем в профиле пользователя будет указываться принадлежность к той или группе.

7. Регистрация администратора

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

Для создания администратора необходимо получить из пользовательского ввода в консоли три параметра:

  • псевдоним;
  • адрес электронной почты;
  • пароль.

При этом необходимо предотвратить ввод заведомо неверных данных.

Начинаю с псевдонима. Псевдоним пользователя должен содержать не более 16 символов из определённого набора и должен быть уникальным. Для получения псевдонима суперпользователя создаю новый файл.

(venv) sadmin@debian:~/workspace/selfish$ touch selfish/manage_tools.py
(venv) sadmin@debian:~/workspace/selfish$ 

Пишу в него следующий код.

import re

from .models.auth import User

USERNAME = 'Enter your username: '
MESSAGE = """Username must be from 3 to 16 symbols (latin letters, nubmers,
dots, hyphens, underscores) and start with any latin letter.
"""


def get_username():
    pattern = re.compile(r'^[A-Za-z][A-Za-z0-9\-_.]{2,15}$')
    username = input(USERNAME)
    while True:
        if not pattern.match(username):
            print(MESSAGE)
            username = input(USERNAME)
            continue
        if User.query.filter_by(username=username).first():
            print('This name is already registered. Sorry.\n')
            username = input(USERNAME)
            continue
        return username

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

>>> from selfish.manage_tools import get_username
>>> get_username()
Enter your username: 1first
Username must be from 3 to 16 symbols (latin letters, nubmers,
dots, hyphens, underscores) and start with any latin letter.

Enter your username: Username_too_long
Username must be from 3 to 16 symbols (latin letters, nubmers,
dots, hyphens, underscores) and start with any latin letter.

Enter your username: Usename$
Username must be from 3 to 16 symbols (latin letters, nubmers,
dots, hyphens, underscores) and start with any latin letter.

Enter your username: avm
This name is already registered. Sorry.

Enter your username: support
'support'
>>> 

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

...
from validate_email import validate_email

from .models.auth import Account, User

...
EMAIL = 'Enter your email address: '
...

def get_email():
    address = input(EMAIL)
    while True:
        if not validate_email(address):
            print('This is not a valid email address.\n')
            address = input(EMAIL)
            continue
        account = Account.query.filter_by(address=address).first()
        swapped = Account.query.filter_by(swap=address).first()
        if (account and account.user) or swapped:
            print('This email address cannot be registered. Sorry.\n')
            address = input(EMAIL)
            continue
        return address

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

>>> from selfish.manage_tools import get_email
>>> get_email()
Enter your email address: avm@elardy.ru
This email address cannot be registered. Sorry.

Enter your email address: this is not an address
This is not a valid email address.

Enter your email address: support@elardy.ru
'support@elardy.ru'
>>> 

Остался ввод пароля. Пароль вводится два раза, ввод проверяется на условие, что оба раза введена одна и та же фраза. Дописываю в этот же файл следующее.

...
from getpass import getpass
...

PASSWORD = 'Enter your password: '
...

def get_password():
    phrase = getpass(PASSWORD)
    while True:
        confirm = getpass('Confirm the password: ')
        if confirm != phrase:
            print('Passwords must match!\n')
            phrase = getpass(PASSWORD)
            continue
        return phrase

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

>>> from selfish.manage_tools import get_password
>>> get_password()
Enter your password: 
Confirm the password: 
Passwords must match!

Enter your password: 
Confirm the password: 
'abcd'
>>> 

Теперь мне необходимо каким-то образом определить набор разрешений администратора. Дописываю в файл selfish/models/auth_units.py следующее.

roots = [permission for permission in permissions
         if permission != permissions.CANNOT_LOG_IN]

Таким образом в наборе администратора будут все возможные разрешения, кроме permissions.CANNOT_LOG_IN.

Осталось воспользоваться всем этим великолепием. Открываю файл manage.py и дописываю в него такой код.

...
from sqlalchemy.exc import OperationalError, ProgrammingError

...
from selfish.manage_tools import get_email, get_password, get_username
...
from selfish.models.auth_units import permissions, roots

...

ERROR_MESSAGE = """Make sure your database and its tables exist.
Check your 'config.py' then use the 'db upgrade' command and then try again.
"""


@manager.command
def create_root():
    try:
        Permission.insert_permissions()
    except(OperationalError, ProgrammingError):
        print(ERROR_MESSAGE)
        return None
    username, address, password = get_username(), get_email(), get_password()
    account = Account.query.filter_by(address=address).first()
    if account is None:
        account = Account(address=address)
    root = User(username=username, password=password)
    root.account = account
    for permission in roots:
        p = Permission.query.filter_by(permission=permission).first()
        if p:
            root.permissions.append(p)
    db.session.add(root)
    db.session.commit()


if __name__ == '__main__':
    ...

Здесь я определил новую команду менеджера, с помощью которой можно зарегистрировать администратора web-приложения. В этой команде менеджера производится попытка заполнить таблицу permissions в базе данных необходимыми данными при помощи соответствующего метода класса Permission (блок try except). Если это удаётся, то при помощи соответствующих инструментов в базе данных регистрируется новый пользователь и получает все разрешения установленные для администратора.

Регистрирую первого администратора Selfish.

(venv) sadmin@debian:~/workspace/selfish$ python manage.py create_root
Enter your username: support
Enter your email address: support@elardy.ru
Enter your password: 
Confirm the password: 
(venv) sadmin@debian:~/workspace/selfish$ 

8. Регистрация пользователей других групп

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

(venv) sadmin@debian:~/workspace/selfish$ python manage.py shell
>>> 

Регистрирую пользователя в группу "Заблокированные".

>>> scain_acc = Account(address='scain@example.com')
>>> scain = User(username='scain', password='aa')
>>> scain.account = scain_acc
>>> p = Permission.query.filter_by(permission=permissions.CANNOT_LOG_IN).first()
>>> scain.permissions.append(p)
>>> db.session.add(scain)
>>> db.session.commit()
>>> scain.group
'Заблокированные'
>>> 

Второго пользователя зарегистрирую в группу "Читатели".

>>> fossman_acc = Account(address='fossman@example.com')
>>> fossman = User(username='fossman', password='aa')
>>> fossman.account = fossman_acc
>>> p = Permission.query.filter_by(permission=permissions.READ_JOURNAL).first()
>>> fossman.permissions.append(p)
>>> db.session.add(fossman)
>>> db.session.commit()
>>> fossman.group
'Читатели'
>>> 

Покидаю служебную консоль и подключаюсь к базе данных.

>>> exit()
(venv) sadmin@debian:~/workspace/selfish$ psql -h 192.168.56.102 -d selfish_d
Пароль: 
psql (9.6.6)
SSL-соединение (протокол: TLSv1.2, шифр: ECDHE-RSA-AES256-GCM-SHA384, бит: 256, сжатие: выкл.)
Введите "help", чтобы получить справку.

selfish_d=> 

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

selfish_d=> SELECT u.username, p.permission
selfish_d-> FROM users AS u, permissions AS p, bonds AS b
selfish_d-> WHERE b.permission_id=p.id
selfish_d-> AND b.user_id=u.id;
 username |      permission       
----------+-----------------------
 avm      | читать журнал
 avm      | создавать ленту
 avm      | писать в приват
 avm      | комментировать журнал
 avm      | вести журнал
 support  | читать журнал
 support  | создавать ленту
 support  | писать в приват
 support  | комментировать журнал
 support  | вести журнал
 support  | следить за порядком
 support  | без ограничений
 scain    | заблокирован
 fossman  | читать журнал
(14 строк)

selfish_d=> 

На примере этих пользователей далее будет продемонстрирована работа системы разрешений пользователей.

9. Дополнительные инструменты

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

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

(venv) sadmin@debian:~/workspace/selfish$ touch selfish/deco.py
(venv) sadmin@debian:~/workspace/selfish$ 

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

from functools import wraps

from flask import abort
from flask_login import current_user


def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def wrap_f(*args, **kwargs):
            if not current_user.can(permission):
                abort(403)
            return f(*args, **kwargs)
        return wrap_f
    return decorator

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

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

...
from ..models.auth_units import permissions


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

С этого момента в каждом шаблоне Selfish можно использовать объект permissions для формирования логики шаблона.

10. Блокирование пользователей

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

В настоящий момент в группе "Заблокированные" есть единственный зарегистрированный пользователь с псевдонимом scain, и он вполне успешно проходит процедуру входа в сервис.

scain-in

Напомню, аватар и псевдоним текущего авторизованного пользователя отображаются в главном меню приложения - знак, что пользователь авторизован.

Чтобы предотвратить вход в сервис пользователей из группы "Заблокированные" открываю в редакторе файл selfish/auth/views.py, дописываю в этот файл новый импорт.

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

Нахожу в этом файле функцию make_before_request и привожу её к такому виду.

@auth.before_app_request
def make_before_request():
    if current_user.is_authenticated:
        current_user.ping()
        if current_user.can(permissions.CANNOT_LOG_IN):
            logout_user()
            flash('Ваше присутствие в сервисе нежелательно.')
            return redirect(url_for('auth.login'))

Логика этой функции выполняется при каждом запросе перед логикой функции представления, к адресу которой был совершен запрос. Таким образом, я проверяю, имеет ли пользователь совершающий любой запрос в систему разрешение CANNOT_LOG_IN, и если имеет, то этого пользователя выбрасывает из сервиса функцией logout_user и перенаправляет на страницу логина. Если теперь обновить страницу браузера с текущей сессией scain, то получится следующее.

scain-got-out

А любая попытка scain залогиниться в сервис завершится аналогичным результатом.

11. Страница с дифференцированным доступом

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

Создаю новую подпрограмму, для этого мне нужен каталог и два файла в нём.

(venv) sadmin@debian:~/workspace/selfish$ mkdir selfish/admin
(venv) sadmin@debian:~/workspace/selfish$ touch selfish/admin/__init__.py
(venv) sadmin@debian:~/workspace/selfish$ touch selfish/admin/views.py
(venv) sadmin@debian:~/workspace/selfish$ 

Открываю файл selfish/admin/__init__.py и пишу в него следующий код.

from flask import Blueprint

admin = Blueprint('admin', __name__)

from . import views

Открываю файл selfish/__init__.py и регистрирую новую подпрограмму в теле функции create_app перед return.

def create_app(config_name):
    ...
    from .admin import admin as admin_blueprint
    app.register_blueprint(admin_blueprint, url_prefix='/admin')
    return app

Открываю файл selfish/admin/views.py и создаю в нём функцию представления для страницы, на которой впоследствии будут фильтроваться и отображаться все зарегистрированные пользователи сервиса. Доступ к этой странице будет закрыт разрешением FOLLOW_USERS. Пишу такой код.

from flask import render_template
from flask_login import login_required

from ..deco import permission_required
from ..models.auth_units import permissions
from . import admin


@admin.route('/society')
@login_required
@permission_required(permissions.FOLLOW_USERS)
def show_users():
    return render_template('admin/society.html')

Здесь я создал новую функцию представления, дал ей уникальный url и декорировал эту функцию представления двумя декораторами. Логика декораторов исполняется сверху вниз перед исполнением логики самой функции представления. Таким образом, соответствующая страница будет требовать входа в сервис у анонимных пользователей и транслировать ошибку всем авторизованным пользователям, не имеющим разрешения FOLLOW_USERS. На текущий момент сама функция представления не делает ничего, кроме интерпретации шаблона. Оригинальная логика в этой функции появится в следующей серии.

Создаю шаблон для этой функции представления.

(venv) sadmin@debian:~/workspace/selfish$ mkdir selfish/templates/admin
(venv) sadmin@debian:~/workspace/selfish$ touch selfish/templates/admin/society.html
(venv) sadmin@debian:~/workspace/selfish$ 

Открываю файл selfish/templates/admin/society.html и пишу в него следующий код.

{% extends "base.html" %}

{% block title %}Сообщество{% endblock title %}

{% block styles %}
  {{ super() }}
  {% assets filters='cssmin', output='generic/css/society.css',
            'css/base.css' %}
    <link rel="stylesheet" type="text/css" href="{{ ASSET_URL }}">
  {% endassets %}
{% endblock styles %}

{% block page_content %}
  <!-- admin/society.html -->
  <div class="flashed-message">
    <div class="alert alert-warning">
      <div class="message-text">
        Вы {{ current_user.username }} из группы {{ current_user.group }}.
      </div>
    </div>
  </div>
{% endblock page_content %}

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

Чтобы облегчить доступ к этой странице авторизованным пользователям, я добавлю в главное меню Selfish соответствующую ссылку. Для этого открываю файл базового шаблона, нахожу в нём следующий тег <ul class="nav navbar-nav">, на данный момент он имеет такой вид.

          <ul class="nav navbar-nav">
            <li><a href="">События</a></li>
          </ul>

Редактирую этот тег и привожу его в следующему виду.

          <ul class="nav navbar-nav">
            <li><a href="">События</a></li>
            {% if current_user.can(permissions.FOLLOW_USERS) %}
            <li class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                Генштаб <b class="caret"></b>
              </a>
              <ul class="dropdown-menu">
                <li>
                  <a href="{{ url_for('admin.show_users') }}">Сообщество</a>
                </li>
              </ul>
            </li>
            {% endif %}
          </ul>

На этом работа с логикой приложения текущего этапа разработки завершена. Отправляюсь в браузер тестировать полученное решение.

12. Тестирование в браузере

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

society-anonymous

Жму enter.

anonymous-redirec

И оказываюсь на странице входа в сервис. Обращаю внимание на адресную строку, в ней появился аргумент next в котором передаётся адрес страницы, на которую я попаду если войду в сервис. Заполняю форму данными пользователя fossman.

fossman-login

Жму кнопку формы. И получаю 403.

fossman-403

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

Выхожу fossman-ом и захожу как avm. В главном меню нахожу новую ссылку.

avm

Следую по ней.

avm-society

И оказываюсь на странице /admin/society. Обращаю внимание, что группа пользователя avm определилась верно. Этот пользователь получил доступ к странице, так как имеет разрешение FOLLOW_USERS.

Прерываю сессию avm и вхожу как support, нахожу в главном меню туже ссылку и следую по ней.

support-society

Этому пользователю тоже открыт доступ на эту страницу. Группа пользователя support определилась верно. Цель достигнута.

Текущая версия кода Selfish доступна на github.com.

Продолжение следует, в следующей серии я поработаю над страницей /admin/society и создам страницу профиля пользователя, будут рассмотрены вопросы постраничной фильтрации и отображения данных, поиск пользователя по псевдониму и AJAX.

 
Осталось: 46
Комментарии:

avm

2018-05-11T13:12:46.079215Z
Hello!