Prosodical Thoughts

News, announcements and thoughts from the Prosody IM team

Starring roles: Introducing dynamic permissions in Prosody

by The Prosody Team

We just pushed the first stage of our modern auth project to Prosody’s development branch!

In previous versions of Prosody (0.12 and earlier), Prosody’s internal API only really supported one type of permission check: “is this user an admin?”. Our new work replaces this with a fully flexible roles/permissions system.

Upgrading to the new system

Despite all our excitement about this new feature, the new changes are designed to be largely invisible to server admins by default. We always aim to make Prosody upgrades as smooth as possible, and we have ensured that no configuration changes are necessary.

If you previously used Prosody’s earlier experimental support for roles in 0.12.x (very unlikely) and assigned roles to users, there is a data migration command to run: prosodyctl mod_authz_internal migrate. This feature in 0.12 was undocumented and therefore unused by most deployments.

There are, however, some changes for module developers to be aware of. This may affect some community modules, though we’ve already updated the major ones ourselves. If you have developed your own custom modules, you may also need to update those. Details about the API changes are discussed later in this post. If you encounter any issues, please do report them to the relevant places.

Keeping it simple

One of our project’s primary goals has always been to keep things as light and simple as possible. Access control is an amazingly complex topic once you scratch the surface, and it’s easy to drown in a sea of roles, permissions and policies.

To keep complexity down, we made some decisions early on about what not to support, so we could instead focus on a minimalist core API and interface that can still support a range of use-cases.

For example, while some systems allow a user to have multiple roles assigned, we decided that any given session should only have one active role at a time. As well as simplifying code, this decision also makes things easier for a human to reason about (e.g. you don’t have to wonder what happens if role A forbids an action and role B permits it, and both are assigned to the same user!).

To keep some flexibility, we do allow multiple “secondary” roles to be assigned to a user. This list simply provides a list of alternative roles the user is permitted to use when requested.

Viewing and managing roles

Currently the best way to view and manage roles is via the admin console. We have added roles to the default output of c2s:show(), and new commands to show and modify the primary role of users.

prosody@prosody: ~/ $ prosodyctl shell c2s show 
Session ID           | JID                              | Role           |
c2s5618e2f92150      | admin@localhost/gajim.M1S9AUK2   | prosody:admin  |
c2s5618e38167e0      | test1@localhost/gajim.FQJLBQIN   | prosody:user   |
OK: 2 c2s sessions shown

Custom roles and permissions

A big reason for the new permissions framework is so that server admins can have more control over permissions. To achieve this, we’ve made it possible to define custom roles and permission policies directly in the config file.

For example, previously mod_announce would let you send an announcement to all users on the server only if you were a server admin. But what if you want to grant this permission to a bot or a script, without giving that bot full admin access to everything else on the server?

Simple! Create a new role for your announcement bot, let’s call it for example “announcer”. Then we just need to give it the “mod_announce:send-announcement” permission. The config looks like this:

VirtualHost "example.com"

  custom_roles = {
      {
          name = "announcer";
          inherits = { "prosody:user" };
          allow = {
              "mod_announce:send-announcement"
          }
      }
  }

This creates a new role that has all the same permissions as a normal user, but with the extra permission to send announcements. After assigning this role to your bot’s user account, it will have permission to send announcements but won’t be able to access any of the other features usually reserved for admins.

Changes for module developers

Deprecation of is_admin API

The old permissions API (usermanager.is_admin()) has been deprecated. Usually we take a more gradual approach to deprecating APIs that are used by modules, however we have special reasons for removing this one.

The deprecated API accepts only a JID (which can be a local or remote user), and returns true or false depending on whether they have admin privileges on the specified host.

The problem is that our new system allows per-session roles. It’s possible for an admin to connect with a client that they don’t want to have full admin access to the server. In this case the session would have a more restrictive role assigned.

However, any modules that continue to use the is_admin() API can only perform permission checks on the JID, and they cannot make any decisions about a specific session. This could lead to a bypass of access control.

To ease the transition, we have initially kept is_admin() working. It will continue to return true if the JID’s default role is prosody:admin or prosody:operator, though it will emit a warning and traceback in the logs for easy identification.

In the near future, it will be disabled - that is, it will log an error and always return false (even if the JID has an admin/operator role).

For admins who want to enforce the new behaviour early, or keep the current (warning only) behaviour for a bit longer, you can set the global option strict_deprecate_is_admin to true or false (it currently defaults to false, and will default to true in a future nightly build at least a week from the date of this post). This is a temporary solution though: eventually this option and the compatibility mechanism will be removed.

Switching to the new API

To ensure your module continues to work with Prosody’s development branch and future Prosody versions, you need to replace all usage of the is_admin() API. If you don’t use this API, great, there is nothing you need to do!

If you do use is_admin(), you should switch to the new role API. This is not provided by Prosody 0.12 and earlier, but to keep compatibility with those versions you can use our new module, mod_compat_roles. Simply add to the top of your module this line:

module:depends("compat_roles");

This will make available the new module API methods (module:may(), module:default_permission()). Note that it won’t provide many other features such as the new role management API in usermanager, or the ability for admins to define custom roles and permissions.

If you have code that looks like:

if usermanager.is_admin(sender_jid, module.host) then
  -- Perform some action
end

Decide on a name for the action that is being performed, and then change it to something like:

if module:may(":my-action", event) then
  -- Perform some action
end

By default nobody will have permission to perform your new action, so you will also want to add near the top of your module a default policy for this action:

-- Allow admins to perform this action by default
module:default_permission("prosody:admin", ":my-action");

You can find the full API documentation here.

Coming next

While this new feature is just a part of the current modern auth project, we’re very excited about the new possibilities it already brings to Prosody - improvements that can be used right now.

However, we’re also looking forward to the next stages. With the authorization layer now in place, acting as the foundations, we’ll be moving on to the second stage: authentication. This will allow clients to authenticate with the server using XEP-0388 (Extensible SASL Profile) with support for advanced features such as multi-factor authentication.

Stay tuned for further updates about this project in the near future!