Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More

Introduction to permissions

As soon as a database application is used by more than one user, we usually need to speak about permissions. For example, a site manager can see certain resources that a simple end user should not get to see. The application must check whether a given user has permission to see a given resource or to execute a given action. An application framework must provide a system for managing these permissions. Lino replaces Django’s user management and permission system (see Lino has its own user management if you wonder why).

Side note: Code snippets (lines starting with >>>) in this document get tested as part of our development workflow. The following initialization snippet tells you which demo project is being used in this document.

>>> from lino import startup
>>> startup('lino_book.projects.min9.settings')
>>> from lino.api.doctest import *

User roles

The basic unit for defining permissions in Lino are called user role.

user role

A responsibility that can be given to a user of a Lino application, and which grants permission to access certain functionalities of the application.

Every user (even AnonymousUser) has a user role assigned to them. Every action on a data table has a set of required roles.

Lino comes with a few built-in user roles that are defined in lino.core.roles.

A user role can inherit from one or several other user roles. For example, the SiteAdmin role inherits from SiteStaff, Supervisor and Explorer.

Plugins may define their own user roles that are subclasses of these builtin roles.

A real-world application can define many user roles. For example here is an inheritance diagram of the roles used by Lino Così:

Inheritance diagram of lino_cosi.lib.cosi.user_types

And if you think this diagram is complex, then don’t look at the following one (that of Lino Noi)…

Inheritance diagram of lino_noi.lib.noi.user_types

Defining required roles

The application developer specifies which roles are required for a given resource.

For example, the users.Users table is visible only for users who have the SiteAdmin role:

>>> users.Users.required_roles
{<class 'lino.core.roles.SiteAdmin'>}

Rather than setting required_roles directly, we recommend to use the login_required() function for specifying required roles, so the definition of the Users table is actually:

class Users(dd.Table):
    ...
    required_roles = dd.login_required(SiteAdmin)

More precisely, “resource” is one of the following:

These objects have a required_roles attribute, which must be a set() of the user roles required for getting permission to access this resource.

This set of user roles can be specified using the login_required utility function. You can also specify it manually. But it must stisfy some conditions described in check_required_roles.

See lino.modlib.users.UserType.has_required_role()

>>> from lino.core.roles import SiteUser, SiteAdmin
>>> user = SiteUser()
>>> admin = SiteAdmin()
>>> user.has_required_roles(rt.models.users.Users.required_roles)
False
>>> admin.has_required_roles(rt.models.users.Users.required_roles)
True
>>> robin = users.User.objects.get(username="robin")
>>> robin.has_required_roles({SiteAdmin})
True

User types

When creating a new user, the site manager needs to assign these roles to every user. But imagine they would have to fill out, for each user group, a multiple-choice combobox with all available roles from above examples! They would get crazy!

That’s why we have user types.

user type

Each user type is basically not much more than a name and a storable value given to a selected user role.

Here is the default list of user types, defined in lino.core.user_types:

>>> rt.show(users.UserTypes)
======= =========== ===============
 value   name        text
------- ----------- ---------------
 000     anonymous   Anonymous
 100     user        User
 900     admin       Administrator
======= =========== ===============

Every user account has a user type, stored in the user_type field of the users.User model. (If that field is empty, the user account is “inactive” and can’t be used for signing in).

>>> rt.show('users.AllUsers', column_names="username user_type")
========== =====================
 Username   User type
---------- ---------------------
 robin      900 (Administrator)
 rolf       900 (Administrator)
 romain     900 (Administrator)
========== =====================

The application developer defines the user types for their application by populating the UserTypes choicelist.

User types are a meaningful subset of all available combinations of roles a way to classify end users in order to grant them different sets of permissions.

Actually a user type contains a bit more information than a user role. It has the following fields:

  • role, the role given to users of this type

  • text, a translatable name

  • value, a value for storing it in the database

  • readonly defines a user type which shows everything that a given user role can see, but unlike the original user role it cannot change any data.

  • hidden_languages may optionally specify a set of languages to not show to users of this type. This is used on sites with more than three or four languages.

The user types module

The roles_required attribute is being ignored when user_types_module is empty.

roles.py
user_types.py

The roles.py is used for both defining roles

A user_types.py module is used for defining the user roles that we want to make available in a given application. Every user type is assigned to one and only one user role. But not every user role is made available for selection in that list.

Relation between user roles and user types

There is a built-in virtual table that shows an overview of which roles are contained for each user type. This table can be helpful for documenting the permissions granted to each user type.

>>> rt.show(users.UserRoles)
======================== ===== ===== =====
 Name                     000   100   900
------------------------ ----- ----- -----
 cal.GuestOperator              ☑     ☑
 comments.CommentsStaff               ☑
 comments.CommentsUser          ☑     ☑
 contacts.ContactsStaff               ☑
 contacts.ContactsUser          ☑     ☑
 excerpts.ExcerptsStaff               ☑
 excerpts.ExcerptsUser          ☑     ☑
 notes.NotesStaff                     ☑
 notes.NotesUser                ☑     ☑
 office.OfficeStaff                   ☑
 office.OfficeUser              ☑     ☑
 polls.PollsAdmin                     ☑
 polls.PollsUser                ☑     ☑
 xl.SiteAdmin                         ☑
 xl.SiteUser                    ☑
======================== ===== ===== =====

Accessing permissions from within your code

Just some examples…

The following two lines are equivalent. It’s a matter of taste and sometimes of context which one you choose:

>>> UserTypes = rt.models.users.UserTypes
>>> from lino.modlib.users.choicelists import UserTypes
>>> UserTypes.admin
<users.UserTypes.admin:900>
>>> UserTypes.admin.role  
<lino_xl.lib.xl.user_types.SiteAdmin object at ...>
>>> UserTypes.admin.readonly
False
>>> UserTypes.admin.hidden_languages
>>> robin = users.User.objects.get(username='robin')
>>> robin.user_type  
<users.UserTypes.admin:900>
>>> robin.user_type.role  
<lino_xl.lib.xl.user_types.SiteAdmin object at ...>

Local customizations

You may have noted that UserTypes is a choicelist, not a database table. This is because it depends on the application and is usually not locally modified.

Local site managers may nevertheless decide to change the set of available user types.

Permission debug messages

Sometimes you want to know why a given action is available (or not available) on an actor where you would not (or would) have expected it to be.

In this situation you can temporarily set the debug_permissions attributes on both the Actor and the Action to True.

This will cause Lino to log an info message for each invocation of a handler on this action.

Since you probably don’t want to have this feature accidentally activated on a production server, Lino will raise an Exception if this happens when DEBUG is False.