Использование машины конечных состояний в Django приложениях ориентированных на пользователя

Spread the love

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

В этой статье будет представлено решение для разработки и реализации приложения, на основе модели User. Мы будем использовать конечные автоматы для большей части логики, связанной с пользовательскими состояниями, а именно fysom, библиотеку, найденной на GitHub. Я также покажу вам, как вы можете легко подключить fysom или любой подобный модуль FSM к вашему веб-приложению Django.

Краткое напоминание: зачем использовать конечные автоматы?

Конечные автоматы (Finite State Machines) – это объекты, которые моделируют состояния и переходы между этими состояниями, применяя правила перехода. Если ваши пользователи могут находиться только в одном состоянии в одно время, и переходы из одного такого состояния в другое четко регламентированы, то вы можете рассмотреть возможность обработки этой логики с помощью FSM. Сама архитектура приложения будет сигнализировать о неправильных переходах и запретить их. Это означает, что даже если хакер получит правильные учетные данные, похитит ваше API и попытается осуществить незаконный переход, ваше приложение все равно будет в безопасности. Объедините это с методами ведения журнала и / или аудита, и вы сможете легко обнаружить утечки и попытки взлома в вашей системе.

Рассмотрим следующую ситуацию. Пусть у вас есть приложение, в котором пользователя регистрируются только на основе приглашения. В нем такие нарушения безопасности, как многократные вводы неправильных паролей, устраняются путем блокировки пользователя и отключения его пароля. Администратор получает уведомление, и из панели администратора он могжет либо разблокировать эту учетную запись, либо полностью ее деактивировать. У администратора есть набор возможных переходов, таких как приглашение (inviting), разблокировка (unlocking) и деактивация пользователя (deactivating ), в то время как другие переходы происходят во время действий пользователя, таких как активация (activation) (когда пользователь принимает приглашение и устанавливает свой пароль) и блокировка (locking) (когда пользователь многократно вводит неправильные учетные данные при входе в систему). Если эти пользовательские переходы не регулируются, то администратор может сделать много ошибок, которые требуют проверки бэкэнда и сложной обработки:

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

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

fsm2

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

Звучит хорошо, но как мне это подключить?

Отлично, давай приступим к делу. Сначала я создам свою модель UserFSM:

class UserFSM(models.Model):
    user = models.OneToOneField(User, related_name='fsm')
    current_state = models.CharField(
        max_length=32, null=False, blank=False, default='invited'
    )
 
    def state_change(self, e):
        self.current_state = e.dst
        self.save()

Я сохраню только значение текущего состояния в поле current_state. Я вернусь к объяснению метода state_change немного позже. Теперь, чтобы использовать логику fysom FSM, мне нужно добавить несколько дополнительных строк в метод конструктора UserFSM. Однако, как вы, возможно, уже знаете, переопределение метода __init__ является плохой практикой. Поэтому я подключусь к сигналу post_init и добавлю свою логику следующим образом:

def extraInitForMyModel(**kwargs):
    instance = kwargs.get('instance')
 
    instance.events = [
        {
            'name': 'activate',
            'src': ['invited', 'deactivated'],
            'dst': 'active'
        },
        {
            'name': 'lock',
            'src': 'active',
            'dst': 'locked'
        },
        {
            'name': 'unlock',
            'src': 'locked',
            'dst': 'invited'
        },
        {
            'name': 'deactivate',
            'src': ['invited', 'active', 'locked'],
            'dst': 'deactivated'
        },
        {
            'name': 'invite',
            'src': 'invited',
            'dst': 'invited'
        },
    ]
 
    instance.fsm = Fysom({
        'initial': instance.current_state,
        'events': instance.events,
    })
 
    instance.fsm.onchangestate = instance.state_change
 
post_init.connect(extraInitForMyModel, UserFSM)

User.fsm = property(lambda u: UserFSM.objects.get_or_create(user=u)[0])

Что я здесь сделал? Сигнал post_init позволяет мне подключиться к экземпляру UserFSM и смоделировать его свойство fsm, которое является только моделью переходов между состояниями и не сохраняется в базе данных. Сначала я проектирую модель перехода, основанную на документации fysom, затем подключаю объект Fysom, используя начальное состояние из базы данных и мою модель перехода. Я также подключаюсь к событию fysom onchangestate, которое я делегирую методу state_change в UserFSM. Я обещал, что объясню это вам позже, поэтому вот оно: всякий раз, когда базовый объект Fysom изменяет состояния, экземпляр обновляет свое значение current_state в базе данных, обеспечивая постоянное обновление без какой-либо явной логики в коде. Это означает, что администратор может вызвать конечную точку API, которая запускает событие activate() а также обновляет информацию в базе данных. Последняя строка в этом фрагменте – это трюк создания объекта UserFSM для каждого пользователя, если он еще не существует. Это тот же трюк, который мы используем для объектов UserProfile.

Теперь, если вы похожи на меня и вам нравится краткий вызов методов, вам нужна еще одна вещь. Прямо сейчас каждый объект UserFSM имеет поле fsm а что бы его задействовать вам еще нужно вызвать его как user.fsm.fsm.activate(). Как это выглядит не очень хорошо. Давайте попробуем автоматическое делегирование для всех возможных событий. Попробуйте что-то вроде этого прописать в класс UserFSM:

def __getattr__(self, name):
        """ Allow for fsm methods to be called directly
        e.g. user.fsm.activate instead of user.fsm.fsm.activate """
        if name in [item['name'] for item in self.events]:
            return getattr(self.fsm, name)

Теперь вы можете напрямую вызывать user.fsm.activate(), user.fsm.lock() и т. д. Это вызов магического метода, детка!

Использование автоматов в API

Не существует общепринятого способа использования автоматов FSM как части конечных точек API. Однако я лично рекомендую, чтобы состояние вашего пользователя не было доступно для редактирования напрямую. Вместо этого добавьте специальную конечную точку для UserStateChange, которая использует PUT и поле типа «fsm-action», которое является не тем, каким должно быть состояние пользователя в конце, а тем, что вызывающий API * хочет * сделать * в качестве перехода. То есть одно из разрешенных событий: unlock, deactivate, activate, delete, invite.

def put(self, request, *args, **kwargs):
        user = User.objects.get(id=self.kwargs['pk'])
        action = self.request.data.get('fsm-action', None)
 
        success = False
        if action == 'unlock':
            success = unlock_user(user)
        elif action == 'deactivate':
            success = deactivate_user(user)
        elif action == 'activate':
            success = activate_user(user)
        elif action == 'invite':
            success = invite_user(user)
 
        if not success:
            raise ValidationError(
                'You cannot %s a %s user.'
                % (action, user.fsm.current_state)
                )
 
        super_ = super(UserChangeStateDetail, self)
        return super_.retrieve(request, args, kwargs)

В последней строке вы видите, что я использую метод retrieve и возвращаю тот же самый ответ JSON, как если бы пользователь выполнил обычный PUT для атрибутов пользователя, что является представлением User Detail. Кстати, это пример, написанный для Django REST Framework.

Давайте подведем итоги

Конечные автоматы являются отличными инструментами, позволяющими усилить разделение состояние и, следовательно, сконцентрировать всю вашу логику для изменения состояния пользователя в одном месте. Для моделирования класса UserFSM вы можете использовать любую библиотеку FSM и добавить соответствующую логику в свой конструктор, используя сигнал Django post_init. Там вы можете смоделировать разрешенные переходы и подключиться к нативным методам и событиям реализации FSM, как пожелаете. Если нужно, не стесняйтесь добавлять магические вызовы методов и другие приемы, чтобы сделать вашу жизнь проще. Если вы используете этот подход с REST API, не забудьте сделать состояние пользователя НЕ редактируемым напрямую и использовать метод редактирования, который ближе к концепции FSM, например, запрос перехода и соответствующая обработка ответа.

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

Оригинальная статья: Using Finite States Machines in Django User-centric applications


Spread the love

Использование машины конечных состояний в Django приложениях ориентированных на пользователя: 1 комментарий

Добавить комментарий

Ваш e-mail не будет опубликован.