The permission system is used to control access to resources or functionality within Baserow. It determines who is allowed to perform certain operations or access certain data.
The permission system is pluggable and allows for the easy addition or replacement of different components that handle the authorization of user operations. It provides flexibility and modularity in the way that access to resources or functionality is controlled within Baserow.
This allows for different authorization strategies to be easily implemented and swapped out as needed without having to make significant changes to the overall code.
Before going any further, we need to agree on the definition of some terms:
Object: represents a piece of data in Baserow. One of Field, Row, Table, Database, Workspace, User, Team, Role, Webhook, …
Hierarchical Objects: in Basrow, Objects are related to each others and we
can have a parent <-> children dependency between two Objects. A full
hierarchy tree can be created with all the Baserow objects. For instance, The
Table
Object is a child of a Database
.
Actor: A generic term grouping anything that can perform Operations on an
Object in Baserow. Can be a User
but also a Personal API Token
or an
AnonymousUser
…
Operation: an action an Actor do on an Object. Some examples:
database.list_tables
: the operation to list the Tables
related to a Database
.database_table.create_row
: the action to create a Row
in a Table
.database_table.update
: the action to update a Table
Object.Context: the Object
on which the an Operation is applied. For instance:
database.list_tables
the context object is the
Database
we want the Table
list for.database_table.create_row
the context object is the
Table
we want to create the Row
for.database_table.update
the context object is also the
Table
we want to update.Permission request: a Permission request is represented by a triplet consisting of an Actor, an Operation, and a Context, which is used to determine if access to a specific resource or functionality is granted or denied by the Permission system. The Context can be omitted if the Operation doesn’t need one.
Permission system: the whole mechanism in Baserow that decides if a Permission request is allowed or not. The Permission system relies on Permission managers to take a decision regarding a specific Permission request in a given Workspace.
Permission manager: a Permission manager is a pluggable part of the
Permission system that can decide if a Permission Request is allowed or not
when some criteria are met. Each Permission manager is responsible to take a
decision in some situation. For instance the StaffOnlyPermissionManager
can
decide to disallow an Operation if the given Actor is not part of the staff.
Workspace: An Operation can take place in a specific Workspace (Formerly Group).
Subject: includes all Actors but also groups of Actors like Teams
.
Personal API Token: An authentication token which can be created by users in
their settings area in Baserow. It is owned by a User
, for a Workspace,
allowing access to some of our API endpoints.
For every Operation an Actor wants to perform on a Context, a Permission request is checked by the permission system. Behind the scene, the Permission request is tested by each Permission manager one by one always in same order. Each Permission manager can:
If none of the Permission managers have allowed or disallowed the Permission, then the permission is disallowed by default.
Example:
Table
in a Database
of his
Workspace
, the following permission is tested:
(user, "database.create_table", database)
by the Permission system. 2) The
Permission request is first given to the CorePermissionManager
that can’t take
a decision, because it’s not a core operation. Then it is given to the
StaffOnlyPermissionManager
that also can’t decide because this is not a staff
only operation. 3) The last permission manager the permission will be tested is
the BasicPermissionManager
that will allows the Permission request because
it’s not an Admin only operation so the user can execute it.Most of the Permission system is driven by the backend. The main components of the Permissions system are the following:
OperationType
: you need an OperationType
for every Operation you want to
check.PermissionManagerType
: the Permission managers are responsible for
allowing or not the Permission requests.SubjectType
: every Actor you want to use in the Permission system must
belongs to a SubjectType
.ObjectScopeType
: every Context object must be part of the Baserow Object
hierarchy. By now it’s implemented by having a related ObjectScopeType
for
each Object type.All of them are objects you can register in their related registry to extend the core functionnalities of Baserow Permission system.
When you want to test a permission on the backend, you’ll have to use the
CoreHandler.check_permission
method.
CoreHandler().check_permission(
# The actor can be the user who did the request: actor = request.user.
actor,
# CreateRowDatabaseTable is an `OperationType` class and `.type` is its name.
CreateRowDatabaseTable.type,
context=table,
group=group
)
If the permission request is allowed then this method will return True
if not,
it will raise a PermissionException
.
The workspace (formerly group) is optionnal if the operation is a core operation outside of any group.
Another common use case related to permissions is to filter a django queryset based on the permissions of the user. You can acheive queryset filtering with this method call.
CoreHandler().filter_queryset(
# The actor can be the user who did the request: actor = request.user.
actor,
# CreateRowDatabaseTable is an `OperationType` class and `.type` is its name.
ListTablesDatabaseTableOperationType.type,
queryset,
group=group
)
Here the context is the database because we are listing the tables of this
database but the queryset is a Table
queryset. This is consistent with the
object_scope
property of the ListTablesDatabaseTableOperationType
which is
TableObjectScope
. This is the purpose of object_scope
property it helps to
determine what kind of objects the operation targets.
An OperationType
instance must be registered for each Operation you want to
check. It can be declared this way:
from baserow.core.registries import OperationType
class ListTablesDatabaseTableOperationType(OperationType):
type = "database.list_tables" # Type
context_scope_name = "database" # The name of the type of context needed to check permissions
object_scope_name = "database_table" # The name of the type of the objects handled by the operation
For most of the operation the context_scope_name
and the object_scope_name
are the same, so the last can be omitted. However regarding all “list”
operations, the object_scope_name is in general one of the children of the
context object in the hierarchy of the Objects. When you want to list all
Tables
of a Database
, the context is a Database
and the objects are the
Tables
. When you list all Databases
of an Application
, the context is an
Application
and the objects are the related Databases
.
This class must be registered in the operation_type_registry
in order to be
used.
from baserow.core.registries import operation_type_registry
operation_type_registry.register(ListTablesDatabaseTableOperationType())
An Operation
instance is saved in database for each registered operation.
You can use them later in your permission manager code if necessary.
A permission manager is responsible for deciding whether or not a Permission Request is allowed for a certain application area. To ensure proper separation of concerns, a good permission manager should only handle one permission checking use case. The permission managers are then stacked to create a complex and powerful permission checking algorithm. You can think of them like being a Django middleware, but instead for a Permission Request instead.
To create a new permission manager you have to create a new
PermissionManagerType
and implement the required methods.
from baserow.core.registries import PermissionManagerType
class OwnedTablePermissionManagerType(PermissionManagerType):
type = "owned_table"
def check_multiple_permissions(self, check, workspace=None, include_trash=False):
...
def get_permissions_object(self, actor, group=None):
...
def filter_queryset(self, actor, operation_name, queryset, group=None)
...
A quick summary of these methods:
.check_multiple_permissions
is the permissions checking method itself. It
takes multiple checks at once for better performances. For each check the
result dict should have the value True
if the permission manager can accept
the permission or an instance of PermissionException
if not or it shouldn’t
include the check at all if the permission manager cannot make a decision
about this check.get_permissions_object
should return any value that will be helpfull to the
frontend permission manager to check a frontend permission. The data returned
should be sufficient for the frontend to make a decision without having to
request further data from the backend.filter_queryset
is used to filter a queryset regarding the permissions the
actor has on the Objects returned by the queryset. The method should exclude
the same Objects that the .check_permission
would exclude if it was called
for each Objects of the queryset.You can read the related docstring to learn more about these methods.
Then, you can register it in the permission_manager_type_registry
.
from baserow.core.registries import permission_manager_type_registry
permission_manager_type_registry.register(OwnedTablePermissionManagerType())
You’ll have to add your permission manager in the enabled permission manager list in the Django settings:
PERMISSION_MANAGERS = [
'core',
'staff_only',
...
'owned_table', # <- here
...
'basic'
]
The position of the permission manager in the list depends on its priority over the other permission managers. In our case we want the permission manager to answer before the basic permission manager has a chance to refuse it.
Now you can check a permission that is handled by your permission manager 🎯.
Remember that you probably need a frontend permission manager for each backend permision manager. See frontend section for more information.
On the frontend you can check a permission with the $hasPermission
method
available on the Vue instance:
// Inside a Vue component
// this.$hasPermission(<operationName>, <contextObject>, <curentGroupId>)
this.$hasPermission("database.create_table", database, group.id);
This call returns true
if the operation is granted false
otherwise.
The frontend permissions are calculated with the permission object sent by the
backend at login for each group the user has access to. Check the
.get_permissions_object
method from each backend permission manager.
The permission object looks like this:
[
{
"name": "core",
"permissions": [
"list_groups"
]
},
{
"name": "staff",
"permissions": {
"staff_only_operations": [
"settings.update"
],
"is_staff": true
}
},
{
"name": "basic",
"permissions": {
"admin_only_operations": [
"group.list_invitations",
"...",
"group_user.delete"
],
"is_admin": true
}
}
]
Each entry of the list has been generated by a permission manager on the
backend. The name
property is the .type
of the permission manager itself and
the permissions
property can be any value that helps the frontend to decide of
the permission can be granted or not. For each backend permission manager a
frontend permission manager should also be registered to handle it’s value.
To check the permissions, the frontend $hasPermission
plugin asks to each
permission manager for which the name is listed in this object, in the list
order, if the permission is granted or not given the data from the permissions
property.
For instance the
BasicPermissionManagerType.hasPermission(permissions, operation, context, workspaceId)
method will be called with the following object:
{
"admin_only_operations": [
"group.list_invitations",
"...",
"group_user.delete"
],
"is_admin": true
}
See next section to learn how to create the frontend permission manager.
For each backend permission manager you probably need a frontend permission manager (some permission managers don’t need one).
You can create a frontend permission manager this way:
import { PermissionManagerType } from '@baserow/modules/core/permissionManagerTypes'
export class OwnedTablePermissionManagerType extends PermissionManagerType {
static getType() {
return 'owned_table'
}
hasPermission(permissions, operation, context) {
// ...
}
}
Check out the documentation of the PermissionManagerType
methods to figure out
how to implement hasPermission
for your permission manager.
Then you need to register it during the Vue plugin initialisation phase in the
plugin.js
frontend file of your project.
app.$registry.register('permissionManager', new OwnedTablePermissionManagerType(context))
And that’s it, you have a fully functionnal frontend permission manager.
If you want to create a new way to validate Permissions, you’ll have to:
The Permission system has been designed with these constraints in mind:
.has_user
method)
as possibleThat may explain some of the decisions that has been made.
More technically:
Database
is not the group has we could imagine first but the
“more generic” type which is the Application
. It solves a lot of issues (but
also creates some if we don’t pay attention).User
Actor, the Basic permission manager has two “roles”, ADMIN
and
MEMBER
which is compatible with the previous permission system. The Role
name is stored in the GroupUser.permissions
field. The idea is to make the
other Role based system using this field to make them compatible and avoid
duplication of data or synchronisation when switching from one system to
another. For the BasicPermissionManagerType
, The ADMIN
value in this
property means the user is ADMIN
. For any other values the User
is treated
as a simple MEMBER
of the Workspace.AnonymousUser
is a SubjectType
that can be handled by some permission
manager.TBD:
.permissions
field to something more understantable.