RBAC + ACLs with django-rest-framework and django-guardian

Recently, I have been working on a personal project developed in Django. It was the first time I used django-rest-framework, and I got to say: it’s impressive how easy it has become to develop something with it.

Usually, RBAC is something not trivial to implement with the current technologies – lots of times you end up writing custom code here and there. Also, I see more and more often developers assigning roles to users and having a call to an ideal “has_role(whatever_role_here)” method to authorize a user. However, roles and their assignments can change over time: what needs to be checked is the particular permission needed to perform that action, not the role. A little hint here: try to go for a fine-grained, not a coarse-grained permission check, because you never know: an AdminRole may be allowed today to be able to view your billings, but tomorrow you’ll want to limit that action to an AccountingAdminRole – and this implies code changes!

Unfortunately, RBAC doesn’t allow to set specific permissions for individual objects, and sometimes you really need that – for example, you don’t want to allow any user to edit other users’ information. However, django-rest-framework and django-guardian, which is really the missing Django permission/authorization tool thanks to all the extremely useful shortcuts it provides, are two excellent tools that help you use RBAC effectively and overcome this limitation, so that you can extend the “role-delegation” behavior with custom ACLs (to have per-object permissions). In fact, by using the permissions API together with django-guardian, it’s really easy to implement multiple use cases to authorize your users. This way, you can benefit from RBAC, by assigning user/roles/permissions while at the same time you can use ACLs to assign individual permissions. Finally, although this is not really related to RBAC, there is support for third-party packages to do authentication, like JWT, or OAuth2.

In the following paragraphs we will set up authentication, authorization and filters for the entire project – meaning, for all our API endpoints/models exposed via django-rest-framework. The procedure to set custom authorization/filtering for only a few specific classes is described in the official documentation, and it’s really not difficult to set up.

1. Set Authentication type

For simplicity, we will assume we are using Basic/SessionAuthentication. Let’s start by writing down the following:


REST_FRAMEWORK = {
  'DEFAULT_AUTHENTICATION_CLASSES': (
    'rest_framework.authentication.BasicAuthentication',
    'rest_framework.authentication.SessionAuthentication',
  )
}

also, we need to add ‘guardian‘ and ‘rest_framework‘ to the list of INSTALLED_APPS.

After that, we can focus on the authorization part (django-rest-framework calls it permissions).

2. Set Authorization classes

One of the best features django-rest-framework provides is the so called “per-object permissions on models” implemented via the DjangoObjectPermissions class. For example, let’s suppose that User A creates a new Post 123, while User B creates a new Post 456, we want these two users to be able to perform actions only on the the Post they have created – we don’t want User A to mess up with Post 456.

By using DjangoObjectPermissions, we can easily map who can do what on which object. However, as the documentation says, you will need to install django-guardian.

Normally, in addition to the already existing operations that Django supports out of the box, add, change and delete, you’ll probably want to add some limitations on who can view specific objects – this works so good with the concept of filters!

You will need to add the following code somewhere in your project:


from rest_framework import permissions

class CustomObjectPermissions(permissions.DjangoObjectPermissions):
  """
  Similar to `DjangoObjectPermissions`, but adding 'view' permissions.
  """
  perms_map = {
    'GET': ['%(app_label)s.view_%(model_name)s'],
    'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
    'HEAD': ['%(app_label)s.view_%(model_name)s'],
    'POST': ['%(app_label)s.add_%(model_name)s'],
    'PUT': ['%(app_label)s.change_%(model_name)s'],
    'PATCH': ['%(app_label)s.change_%(model_name)s'],
    'DELETE': ['%(app_label)s.delete_%(model_name)s'],
  }

At this point, we have to set this class in the REST_FRAMEWORK map we have declared above:

REST_FRAMEWORK = {
  ...,
  'DEFAULT_PERMISSION_CLASSES': (
    'module_containing.CustomObjectPermissions',
  ),
}

and have the following to enable django-guardian backend:

AUTHENTICATION_BACKEND = (
    'django.contrib.auth.backends.ModelBackend', # default
    'guardian.backends.ObjectPermissionBackend',
)

So far, we have added some configuration to tell django-rest-framework that we would like to have the possibility to use permissions on individual objects. However, we have not specified yet that we want to limit the objects a logged-in user is allowed to view.

3. Add filtering for ‘view’ operations

django-rest-framework recommends to use DjangoObjectsPermissionFilter. In order to do so, we need to add one more class to the REST_FRAMEWORK map:

REST_FRAMEWORK = {
  ...,
  ...,
  'DEFAULT_FILTER_BACKENDS': (
    'rest_framework.filters.DjangoObjectPermissionsFilter',
  ),
}

4. Add ‘view’ permissions to your models

As already mentioned, Django doesn’t come with a view permission on models. Therefore, we will have to add this manually for each model, like the following code shows:

class UserProfile(models.Model):
  user = models.OneToOneField(settings.AUTH_USER_MODEL,
                              on_delete=models.CASCADE,
                              related_name='profile')

  class Meta:
    permissions = (
      ('view_userprofile', 'View UserProfile'),
    )

Once we create a User and we give it a UserProfile, the logged-in user will only be able to retrieve his own UserProfile object. Also, don’t forget to create a migration for this and for the models used by django-guardian.

5. Create views and serializers

At the end, of course, we should not forget to create the serializers and views:

class UserProfileViewSet(viewsets.ModelViewSet):
  serializer_class = UserProfileSerializer
  queryset = UserProfile.objects.all()

I leave the serializer implementation up to you. 🙂

Note: a fully functioning sample can be found here: https://github.com/markon/django-rest-quickstart.

Further Readings

 

A need for a better hiring process

Software engineering is a relatively new field. We are still trying to find a common denominator, a bunch of patterns, something that gives us a better overview about how things work, so that we can narrow them down and apply a process – like we do in any other engineering field, e.g., automotive, pharmaceuticals, etc. Something measurable, because when we have numbers we can actually do something. Without numbers it’s chaos and gut feelings, unicorns and failures.

One of those things we need to get better at is certainly the hiring process.

I am amazed by the amount of jobs out there that not always fit their own description – it almost seems they were written by a person within the company that dreams about that job day and night, a sort of Promised Land:

  • You will design and implement highly scalable services that need to serve customers all around the globe – we have already 20 million customers!
  • We work hard but play also hard – we have regular kicker tournaments and the best Espresso machine
  • We want you to learn and get better, we pay you 2000$ per year that you can spend with books and courses of your choice

These are only few of the promises. Sometimes they are quite good – I would not mind having 1k to spend in books and courses, to be honest. However, you will often find on Glassdoor that the same company offering you the Promised Land position has also the following cons, which often contradict what they claim in their job description:

  • management should trust employees more
  • no career opportunities
  • employees are expendable
  • high turnover rate

Similarly, I am quite certain that not every job applicant plays by the rules, and those boring years maintaining that legacy software will need more than some polishing.

While this is certainly a grey area, where ethics, self-promotion and personal/professional needs are different forces fighting against each other – or I should say “together”, maybe –  I am quite aware that people lie, so I prefer to focus on what the person and the company have to offer and how good their interactions are.

Ideally, everyone tells the truth during the hiring process. However, in order for this truth to come out, it needs to be told, written down somewhere, it has to become visible, and all potential ambiguities need to be cleared once for all during the interview process – questions need to be asked, especially difficult ones if you have doubts – there are lots of websites helping with what needs to be asked, just do your homework.

Also, let’s not forget that companies need to find the right candidate, because the first few months are just a cost (I think I read about it for the first time in Peopleware).

How can companies and candidates get to know each other better?

I believe that the best way to improve the hiring process in our field is by providing constant feedback and to put the candidate right in some of the team’s dynamics. Some companies I interviewed with in the last few years offer some feedback, but it’s almost never meant as a way to get to know each other. It’s like being at school over and over: how did I score? oh, cool, I made it!

For example, by analyzing what our daily tasks are, we could try to find out how candidates perform during a normal working day. This way, the feedback is mutual – candidates maybe don’t like the office, the managers, the colleagues, etc., while the company maybe doesn’t like how the candidate fits into the team. If that’s not possible, we could provide remote tools to support them. Accepting a candidate or a company shouldn’t  happen as soon as possible, but as soon as they are convinced about each other.

I do understand that we, as human beings, tend to do what we know best repeatedly, it’s smart because we use what we know already and we don’t need to invest new time. However, it doesn’t have to be that way. At least, when you find out it doesn’t work, maybe that has to be adapted a bit, at least. So, I am talking to all HR people – please, do something about it. Don’t just hire people to fulfill the expectations, to fill the empty desks, to reach the desired head count. Talk to your HR managers, to the CTOs and so forth, discuss about it. Find a better way to hire people.

Struggling with CI and CD

Continuous Integration and Continuous Delivery are two practices that are supposed to boost the development, integration and delivery of features, bug fixes, etc.

Someone may claim that they actually improve only the QA/release side of it, but I like to take the position according to which “done is released” – this is certainly not something I invented –  I can’t remember which book I have taken this from (maybe Continuous Delivery?).

Software development has changed, it has become an industry, still we struggle to reach common definitions. We are full of best practices, de-facto standards, and so forth, yet it’s so easy to end up with people without a clear understanding about certain topics, about the reasons things could be done in a certain way instead of doing it “because it has always worked for us”.

However, I have noticed a particular pattern: when something is a “practice” it gets often misunderstood. The best example is a REST web service. I really don’t want to get into this discussion again, because REST is an interesting concept, yet it has a million implementations. So many that developers in the end wind up frustrated with different ideas of REST.

Pragmatism is certainly important in our field, yet sometimes it’s important to have a common dictionary, so that we can refer to roughly the same concept, especially within the same company. This is unfortunately not always the case. CI and CD are only the tip of the iceberg, together with many other examples, because I think they are difficult to implement.

Why so? Because it’s actually difficult to understand how you want to do things and what you want to do with them. It’s a practice, not a tool, therefore it needs to be understood first, it can be adapted to your needs, why not, but it should not be seen as a savior, because it’s not going to solve all your problems.

If you want to “do” CI and CD correctly, you need to have the right problem, the right mindset, and the right tools. I would personally not do CD with medical software or space components – my experience with these fields is too little, so I can’t say much – it’s just a rough idea.

However, given the right problem (which is difficult to define!), you need to have people with the right mindset: we need to do something new, and this is going to have a severe impact on how we do things here – you won’t be anymore a QA or a DEV, but you will take care of everything from A to Z. Literally.

And, of course, you need the right tools, otherwise it becomes a pain in the *** to manage all these pipelines, tasks, failures, rollbacks, etc. Fortunately, nowadays we have plenty of them, even open source.

So, what is the sort of problems that CI/CD try to help us solve?

I would say that the first and foremost problem they help us with is the time to market – which is essential in business. Then everything else comes almost as a consequence, like “batteries included”:

  • code is always in a deployable state
    • it has passed all the QA rounds of testing, etc.
      • it offers a feeling that things are safe/green, which is always good to have
    • it was built, so it’s ready to be installed, etc.
  • tendency to have metrics-based pipelines
    • for example, if some component doesn’t reach X% of coverage etc., then it won’t be promoted to next stage

What kind of mindset does it require?

It asks people to take what they have always done, wrap it up, and throw it away. Sometimes it even asks them to wipe their *** with it. Pardon my French.

It requires people to think in a way that is deterministic, repeatable, stateless, yet in units that have to be integrated to make “the whole greater than the sum of its parts” (just to mention Aristotle).

It’s not enough to say “we need to commit on a daily basis”. It just doesn’t work that way. Same applies to “we need to achieve X% coverage so that the builds are self-testing”, where X is a ridiculously high number (considering that now the coverage is below the sea level). That will be not only counter productive, but will end up with frustration.

Depending on where you work, this may be harder or easier to implement. Having the right tools here helps a lot, educating people helps even more. In my opinion, the best way to achieve something here is by taking the time to explain the value this new approach offers, compared to what has been done so far, together with all the challenges this implies.

What’s the lesson here?

I think progress is never easy to achieve. It takes courage, an open mindset, some stubbornness, and sometimes also the honesty to say “ok, it doesn’t work this way, maybe it needs some improvements”.

Further Readings

There’s plenty of material to learn about CI/CD, however some of the most important articles about these two practices are from Martin Fowler:

Neolithic

I guess that when most people hear the word “Neolithic”, they simply think about something old, not very old, but old enough to be dated back to a past when our ancestors could not eat a juicy pizza or enjoy a fresh beer – it seems they’d be wrong in both cases.

Funny thing is that most people use the term “Paleolithic” to label something that is very old, something that has “aged”, which you wouldn’t do any more, that has most of the time a relatively negative connotation. Paleolithic and Neolithic, though, are both part of the same larger period known as Stone Age.

However, going back to Neolithic, I think it should be considered again, this time with a more correct and modern interpretation – this word should actually be associated to the “Neolithic revolution”, which according to Wikipedia was:

[…] the wide-scale transition of many human cultures from a lifestyle of hunting and gathering to one of agriculture and settlement, making possible an increasingly larger population.

Now, looking at our history, we can often find that we humans, or I should say Western people (as I don’t have enough knowledge about how this whole Stone Age thing is perceived in the East) tend to think that there are some periods that produced “wide-scale transitions […]”. One recent example is the Industrialization.

I frankly dislike to imagine that such periods happened suddenly, as if our ancestors in the Paleolithic period were all limited or not capable of farming, or as if people in the Middle Ages were all ignorant and illiterate – let’s try to remind a few things that we use and that were created roughly in such a “bad period”: glasses, universities, printed books. Progress is something that doesn’t happen right away, and if people in the Neolithic had the chances to improve their farming techniques I am quite sure that they had to thank also the “poor” guys who were born in the Paleolithic.

Why am I being biased towards Paleolithic then, claiming that they were “poor” guys? Well, that’s the point – you see. This is the main issue that we face when we look at something that happened in a relatively remote historical period – we almost always think that in the past it was all bad, all terrible, etc.

Why Neolithic then? Well, at this point you should have guessed it already. If we are who we are and we have what we have, we certainly have to say thanks to the progress and improvements created over the years. Oh, I almost forgot: of course, today is better than yesterday.