18 KiB
🔐 Permissions system guide
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.
📖 Glossary
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 theTables
related to aDatabase
.database_table.create_row
: the action to create aRow
in aTable
.database_table.update
: the action to update aTable
Object.
Context: the Object
on which the an Operation is applied. For instance:
- for the Operation
database.list_tables
the context object is theDatabase
we want theTable
list for. - for the Operation
database_table.create_row
the context object is theTable
we want to create theRow
for. - for the Operation
database_table.update
the context object is also theTable
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.
📃 Main principles
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:
- Allow the Permission Request. Then the operation can be executed.
- Disallow the Permission Request. The operation is canceled and an error is raised.
- Passthrough the Permission Request. The Permission Request is then tested by the next Permission manager
If none of the Permission managers have allowed or disallowed the Permission, then the permission is disallowed by default.
Example:
- If a non admin user wants to create
Table
in aDatabase
of hisWorkspace
, the following permission is tested:(user, "database.create_table", database)
by the Permission system. 2) The Permission request is first given to theCorePermissionManager
that can't take a decision, because it's not a core operation. Then it is given to theStaffOnlyPermissionManager
that also can't decide because this is not a staff only operation. 3) The last permission manager the permission will be tested is theBasicPermissionManager
that will allows the Permission request because it's not an Admin only operation so the user can execute it.
⚙️ The backend
Most of the Permission system is driven by the backend. The main components of the Permissions system are the following:
OperationType
: you need anOperationType
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 aSubjectType
.ObjectScopeType
: every Context object must be part of the Baserow Object hierarchy. By now it's implemented by having a relatedObjectScopeType
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.
Check a Permission on the backend
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.
Filter a Django Queryset
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.
Declare a new operation
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.
Create a backend Permission manager
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 valueTrue
if the permission manager can accept the permission or an instance ofPermissionException
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.
📺 The frontend
How to check a Permission
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 permissions object
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.
Creating a 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.
📝 Conclusion
If you want to create a new way to validate Permissions, you'll have to:
- Create a backend permission manager
- Implement its methods
- Register the permission manager
- Add missing operations if any
- Create a frontend permission manager
- Implement its methods
- register the frontend permisison manager
- Test everything
🤔 A few considerations
The Permission system has been designed with these constraints in mind:
- Must be extensible (to support RBAC from enterprise folder)
- Must be as much compatible with the previous system (The
.has_user
method) as possible - Must play well with realtime
- Must be able to work with object but also a collection of objects
- Must be performant
- Must avoid code duplication between backend and frontend
That may explain some of the decisions that has been made.
More technically:
- The parent of a
Database
is not the group has we could imagine first but the "more generic" type which is theApplication
. It solves a lot of issues (but also creates some if we don't pay attention). - For a
User
Actor, the Basic permission manager has two "roles",ADMIN
andMEMBER
which is compatible with the previous permission system. The Role name is stored in theGroupUser.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 theBasicPermissionManagerType
, TheADMIN
value in this property means the user isADMIN
. For any other values theUser
is treated as a simpleMEMBER
of the Workspace. - The current Personnal API Token has been partially migrated to the current permission system.
- The
AnonymousUser
is aSubjectType
that can be handled by some permission manager.
TBD:
- Add a new Authentication Token to replace the old one that really use the permission system.
- Renaming the
.permissions
field to something more understantable. - Handle Public views with a new Permission manager.
- Create a few more Roles