Using Finite States Machines in Django User-centric applications

Using Finite States Machines in Django User-centric applications
Reading Time: 8 minutes

Most modern frameworks and languages have special tools for handling User-related use cases such as logging in, signing up, profile editing. Tutorials after tutorials and tools after tools have been written in order to help a developer handle these cases, but what happens when your use cases are not, in fact, that simple? Let’s say we want to design a banking application where users can be verified for different services, e.g. a user may be able to activate automatic tax payment if they receive their salary through that bank, but not the loaning services, for which they need to provide extra documentation. Or let’s have the example of an invite-based application where users are not allowed to sign up by their own accord and maybe need to jump some hoops to be activated or to have certain privileges.
This article will present a solution for designing and implementing an application where the User model is king. We will use Finite State Machines for most of the logic related to users’ states, namely fysom, a library found on GitHub. I will also show you how you can easily hook up fysom or any similar FSM module into your Django web application.

Brief reminder: Why use Finite State Machines?


Finite State Machines are entities which model states and transitions between these states, enforcing transition rules. If your users can be in only one state at a single time, and the transitions from one such state to another are clearly regulated, then you might want to consider handling this logic using a FSM. The construct itself will signal wrongful transitions and will disallow them. This means that even if a hacker obtains the correct credentials, hijacks your API and tries to enforce an illegal transition, your application is still safe, since the FSM will not even budge. Combine this with logging and/or auditing methods and you can detect leaks in your system.
Consider the following use case: In an invite-based application, security breaches like repeated wrong password inputs are dealt with by locking the user out and disabling their password. An administrator is notified and, from an admin panel, they are able to either unlock that account or to deactivate it completely. The administrator has a set of possible transitions such as inviting, unlocking and deactivating a user, while other transitions occur during user actions, such as activation (when the user accepts the invitation and sets their password) and locking (when the user repeatedly enters wrong credentials at login). If these user transitions are not regulated, then the administrator can make a lot of mistakes which need backend validation and complex handling:

  • a user who doesn’t have a password (yet) is activated directly and can not log in (i.e. the invite step was skipped)
  • a user who is innocent and active is reinvited and forced to change credentials
  • a user who was previously invited is duplicated instead of changed

By using a Finite State Machine, we can model the user transition like this, during the engineering phase of our project:
fsm2
This modeling will mean that, for example, if an administrator tries to lock a deactivated user, the FSM will not recognize this as a valid transition and it will not comply to the administrator’s erroneous request.

That sounds nice, but how do I hook it up?


Great, let’s get down to business. First I will create my UserFSM model:

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

I will only store the value of the current state in the database for persistence betweenn different sessions. I will return to explaining the state_change method in a bit. Now in order to leverage the fysom FSM logic I would need to add some extra lines to the constructor method of UserFSM. However, as you may already know, overriding the __init__ method is bad practice. So I will hook into the post_init signal and add my logic like this:

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

What did I do here? The post_init signal allows me to hook into the UserFSM instance and model its fsm property, which is only a model of the state transitions and is not persisted in the database. I first design the transition model inspired from the fysom documentation, then I hook up the Fysom object using the initial state from the database and my transition model. I also hook into the fysom-native event onchangestate, which I delegate to the state_change method of UserFSM. I promised I’d explain that to you later, so here it is: whenever the underlying Fysom object changes states, the instance updates its current_state in the database, allowing for round-the-clock updating without any explicit logic in the code. This means the admin can call an API endpoint which triggers the activate() event and that will update the information in the database seamlessly. The last line in this snippet is the trick of creating a UserFSM object for each User, if it doesn’t exist already. It is the same trick we use for UserProfile objects.
Now, if you’re like me and you like concise method calling, you need one more thing. Right now the UserFSM object itself has an underlying fysom-written fsm, and then you’d need to call user.fsm.fsm.activate(). Let’s try automatic delegation for all the possible events. Try something like this in the UserFSM class:

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)

Now you can directly call user.fsm.activate(), user.fsm.lock() etc. That’s magic method calling, baby!

Using FSMs in an API


There is no orthodox way of using FSMs as part of API endpoints. However, I personally recommend that your user’s state is not open to editing. Instead, add a special endpoint for UserStateChange, which uses PUT and a field like ‘fsm-action’ which is not what the user’s state should be in the end, but what the API caller *wants* to do, as a transition. I.e. one of the allowed events: 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)

You can see in the final line that I leverage the retrieve method and return the exact same JSON response as if the user had performed a regular PUT on the user’s attributes, which is a User Detail representation. By the way, this is an example written for Django REST Framework.

Let’s recap


Finite State Machines are great tools for leveraging separation of concerns and therefore concentrating all your logic for changing a user’s state in one single place. For modeling a UserFSM class you can use any online or custom-written FSM and add the corresponding logic to your constructor, using the post_init Django signal. There you can model the allowed transitions and hook into the native methods and events of the FSM implementation as you wish. If needed, feel free to add magic method calling and other tricks to make your life easier. If you use this approach with REST APIs, remember to make the User’s state NOT editable and use an editing method which is closer to the concept of FSMs, like requesting a transition and handling the response accordingly.
I personally found myself struggling with complex user state logic in many use cases. I hope this set of recommendations will help you develop similar application with much more ease.
Hungry for more Django magic? Be sure to check out Adela’s article on deleting Django unused media files and Oana’s article on solving Django migration conflicts.

We transform challenges into digital experiences

Get in touch to let us know what you’re looking for. Our policy includes 14 days risk-free!

Free project consultation