mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-01-30 18:36:29 +00:00
444 lines
18 KiB
Markdown
444 lines
18 KiB
Markdown
# 🔐 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 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:
|
|
|
|
- for the *Operation* `database.list_tables` the context object is the
|
|
`Database` we want the `Table` list for.
|
|
- for the *Operation* `database_table.create_row` the context object is the
|
|
`Table` we want to create the `Row` for.
|
|
- for the *Operation* `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.
|
|
|
|
## 📃 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.
|
|
|
|
![Permission diagram](https://gitlab.com/baserow/baserow/-/raw/master/docs/assets/diagrams/permissionProcessus.jpg)
|
|
|
|
[source](https://viewer.diagrams.net/?tags=%7B%7D&highlight=0000ff&edit=_blank&layers=1&nav=1#R7VnZVtswEP2aPNLjJXbIIwESKEvbAxR4FLZiq5GtVFYW9%2Bs7iiXvJUkhUCgvOZrxWMvcO4uVjn0YLUccTcML5mPasQx%2F2bGPOpbVd7rwKxVppnBNO1MEnPiZyiwUV%2BQXVkpDaWfEx0nFUDBGBZlWlR6LY%2ByJig5xzhZVszGj1VWnKMANxZWHaFN7S3wRZtp9q1foTzAJQr2y6fazJxHSxuokSYh8tiip7OOOfcgZE9koWh5iKn2n%2FXJ7mt7S84k7%2Bvwt%2BYluBmfXl9%2F3ssmG27ySH4HjWDzv1FY29RzRmfKXOqtItQM5m8U%2BlpMYHXsQiojC0IThDyxEqgBHM8FAxbgIWcBiRM8Zmyq7MYuFMjOljGP%2FQAIL8gNl3iRTDQmlag2QlP0%2BSIngbJJjJyfIgZDGFD1gOkDeJFht9JBRxuFRzGIsp%2FKBDOosxeaOC%2B1gQ98qDBI24x5%2BxE4Fh0A8wI%2FN52Z2cn8lnirkRphFWPAUDDimSJB5lcxIxUSQ2xW4w0BBvwUN7AYNRrBmLJeSzsE8IklCWPwIOSQwi5AIfDVFKw8tIJ1UCVMmApx1EFCUJArGNShvh9Icc4GXj%2FpVPbUNFdypljNxUaQKU8d%2FWEoTXWNHSHRbAtKlsOrAJ3MYBnJ4ybQS1ijpP2L3SbHrbhi7urStDV5Fsj3jk%2BM4boVomlQbx7ea%2FSsjcNbChI3HCWy2Trt8E3%2FPRKfBxHuo43WGrQfpTXPwlRjW25BgabXX%2BnfJ5DbIdJpIR4S4Vl%2Bgi0SxALpYxkOaW3gzvkKgZhuhGNo3DqOOPWwmv5BFD7NkfV2q8EGybYgiQqVvTzCdY0E81FK9ECVBDIIHG4M9tBINliRxAJJbSNcrYkOi311Vs%2FrVqmb2Wspat6Wsmeau6lqvQYDrFbIMAAXOruAkkhKIUraQ%2BL%2BzTqNbw8QynFduNfYbkMi%2B4qODeEJ%2B1%2BGzvoUwt24hrDfWP%2FQ%2F%2BodX5Jf13joIvcMNWgifJHkV%2BegMqt%2B7Zu1712ypQkZLFdp%2FhirEjfDCvDm4pQejL2dWgnu20dc3UE9ICa1B35IcSriCV3l6l4c7CPdS%2BORo8WhZfniUamlJxF1pXHoLpOIlKeh3njsF6EvUF79hWr0KmROlJYOpTABJS0bQrahbJVy3V7ulXGNvWU6NYtkOnje36DgqcssVpvI2WueXGC%2F%2F%2FPXxzjrV%2Bp1Yt7thp%2BrsqlPV1Wz910M59b8zWJz6VaX9gh8Qram7GTXXaILzmBkTnvy3QZMX0R2gA2Lxr1OWA4u%2F7uzj3w%3D%3D)
|
|
|
|
Example:
|
|
|
|
1) If a non admin user wants to create `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.
|
|
|
|
## ⚙️ 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 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.
|
|
|
|
### 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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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`.
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```js
|
|
// 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:
|
|
|
|
```json
|
|
[
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```js
|
|
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.
|
|
|
|
```js
|
|
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 the `Application`. 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` 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.
|
|
- The current Personnal API Token has been partially migrated to the current
|
|
permission system.
|
|
- The `AnonymousUser` is a `SubjectType` 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
|