Advanced permission management for Datasette
Project description
datasette-acl
Advanced permission management for Datasette. Highly experimental.
Installation
Install this plugin in the same environment as Datasette. This plugin requires Datasette 1.0a15 or higher.
datasette install datasette-acl
Usage
This plugin is under active development. It supports configuring permissions for individual tables — controlling the following actions — as well as for any custom resource type registered by a plugin:
insert-rowdelete-rowupdate-rowalter-tabledrop-table
Grants can target individual users, user groups, or general-access audiences like "any signed-in user".
Permissions are saved in the internal database. This means you should run Datasette with the --internal path/to/internal.db option, otherwise your permissions will be reset every time you restart Datasette.
A JSON HTTP API for reading and managing per-resource grants programmatically is documented in docs/json-api.md.
Managing permissions for a table
The interface for configuring table permissions lives at /-/acl/resource/table/<database-name>/<table-name>. It can be accessed from the table actions menu on the table page.
Permission can be granted for each of the above table actions. They can be assigned to both groups and individual users, who can be added using their actor["id"].
The page also offers a General access section for granting table actions to public audiences such as "any signed-in user". See Principals and general access.
An audit log tracks which permissions were added and removed, displayed at the bottom of the permissions page.
Custom resource types
datasette-acl is not limited to tables. It can store and resolve grants for any resource type defined by a plugin - documents, lists, workbooks, comment spaces, kanban boards and so on.
There is no dedicated hook for this. A plugin makes its resource type manageable by datasette-acl simply by registering actions whose resource_class is a Resource subclass:
from datasette import hookimpl
from datasette.permissions import Action, Resource
class Playlist(Resource):
name = "playlist"
@hookimpl
def register_actions(datasette):
return [
Action(name="playlist-view", description="View a playlist", resource_class=Playlist),
Action(name="playlist-edit", description="Edit a playlist", resource_class=Playlist),
]
datasette-acl discovers resource types from the actions registered across all plugins (datasette.actions). The action set offered for each resource type is derived dynamically from this - nothing is hardcoded.
A generic admin page for any resource type lives at:
/-/acl/resource/<resource-type>/<parent>/<child>
For example /-/acl/resource/playlist/workspace-1/playlist-42. The <child> segment is optional for parent-only resource types (/-/acl/resource/<resource-type>/<parent>). The page presents group, user, general-access grants and an audit log, with checkboxes for exactly the actions registered for that resource type. Access to this admin page is granted to users with the datasette-acl permission described below, or users who can manage that specific resource.
The page also has a General access section for exposing a resource without naming individual users — see Principals and general access below.
Grants made here flow through Datasette's permission system: once granted, await datasette.allowed(actor=..., action="playlist-edit", resource=Playlist("workspace-1", "playlist-42")) returns True.
Principals and general access
Every grant names exactly one principal, recorded in the principal_type column on each stored grant. There are five kinds — two identified by an id, and three public audiences identified by the type alone:
principal_type |
Grants to | Identified by |
|---|---|---|
actor |
An individual user | actor_id |
group |
All members of a user group | group_id |
everyone |
Anyone, signed in or not | — |
authenticated |
Any signed-in actor | — |
anonymous |
Signed-out (anonymous) visitors only | — |
The generic resource admin page presents the three audiences as its General access section (labelled "Anyone (signed in or not)", "Any signed-in user" and "Signed-out (anonymous) visitors only"), and the audit log's Principal column shows the same labels. Audiences can also be granted programmatically: pass principal_type to the JSON API, or an audience Principal (e.g. Principal.authenticated()) to the Python helpers.
Audience grants store principal_type only; their actor_id and group_id columns are both null. Permission checks match actor grants by actor_id, group grants by group_id, and general-access grants by principal_type. Code writing rows directly to the acl table must provide a valid principal_type and respect the table's CHECK constraint; prefer the Python API.
Declaring roles
Raw action grants are flexible but users think in roles. A plugin can declare friendly roles for its resource type with the datasette_acl_roles hook, mapping each role name to the bundle of actions it grants. Almost every plugin wants the same three cumulative roles - Viewer, Editor and Manager - so rather than declaring them by hand, use the standard_roles() factory.
A complete plugin declares its Resource subclass, registers an Action for each action name, then maps those same names to roles. Define the names once as constants so the two hooks can't drift apart:
from datasette import hookimpl
from datasette.permissions import Action, Resource
from datasette_acl.roles import standard_roles
VIEW = "playlist-view"
EDIT = "playlist-edit"
MANAGE = "playlist-manage"
class Playlist(Resource):
name = "playlist"
@hookimpl
def register_actions(datasette):
return [
Action(name=VIEW, description="View a playlist", resource_class=Playlist),
Action(name=EDIT, description="Edit a playlist", resource_class=Playlist),
Action(name=MANAGE, description="Manage playlist sharing", resource_class=Playlist),
]
@hookimpl
def datasette_acl_roles(datasette):
return standard_roles(
Playlist.name,
view=VIEW,
edit=EDIT,
manage=MANAGE,
)
This returns the canonical triple: Viewer (the view actions), Editor (view + edit) and Manager (view + edit + manage, marked as authorizing re-sharing). Each of view=/edit=/manage= accepts a single action name or a list, and descriptions= optionally overrides the default role descriptions by role name:
standard_roles(
"table",
view="view-table",
edit=["insert-row", "update-row", "delete-row"],
manage="manage-table",
descriptions={"Manager": "Full control, including sharing"},
)
Role names then work everywhere roles are accepted: the role= argument to the Python grant()/update_role() helpers below, and the JSON API. If you need different role names or additional roles, construct datasette_acl.roles.AclRole objects directly (or append them to the list returned by standard_roles()) - each declares a resource_type, name, actions list, a rank for ordering, and manage=True on the role whose exclusive actions allow changing sharing.
Controlling who can edit permissions
Users with the new datasette-acl permission will have the ability to access a UI for setting permissions for users and groups on a table.
To configure the root user to have this permission, add the following to your Datasette configuration:
permissions:
datasette-acl:
id: root
Alternatively you can start Datasette running like this:
datasette mydata.db --root --internal internal.db \
-s permissions.datasette-acl.id root
User groups
Users can be assigned to groups, and those groups can then be used to quickly assign permissions to all of those users at once.
To manage your groups, visit /-/acl/groups or use the "Manage user groups" item in the Datasette application menu.
Add users to a group by typing in their actor ID. Remove them using the remove user button.
The page for each group includes an audit log showing changes made to that group's list of members.
When you delete a group its members will all be removed and it will be marked as deleted. Creating a group with the same name will reuse that group's record and display its existing audit log, but will not re-add the members that were removed.
Dynamic groups
You may wish to define permission rules against groups of actors based on their actor attributes, without needing to manually add those actors to a group. This can be achieved by defining a dynamic group in the datasette-acl configuration.
Dynamic groups are defined in terms of allow blocks. The following configuration defines two dynamic groups - one called admin that contains all users with "is_admin": true in their attributes, and another called sales that explicitly includes users with "sales" as one of the values in their departments array.
plugins:
datasette-acl:
dynamic-groups:
admin:
is_admin: true
sales:
departments: ["sales"]
Any time an actor has their permissions checked they will be dynamically added to or removed from these groups based on the current value of their actor attributes.
Dynamic groups are displayed in the list of groups, but their members cannot be manually added or removed.
Table creator permissions
If you allow regular users to create tables in Datasette, you may want them to maintain a level of "ownership" over those tables, such that other users are unable to modify those tables without the creator's permission.
The table-creator-permissions plugin setting can be used to automatically configure permissions for the actor who created a table.
Enable that like this:
plugins:
datasette-acl:
table-creator-permissions:
- alter-table
- drop-table
- insert-row
- update-row
- delete-row
Configuring autocomplete against actor IDs
By default, users of this plugin can assign permissions to any actor ID by entering that ID, whether or not that ID corresponds to a user that exists elsewhere in the current Datasette configuration.
If you are running this plugin in an environment with a fixed, known list of actor IDs you can implement a plugin using the datasette_acl_valid_actors(datasette) plugin hook which returns an iterable sequence of string actor IDs or {"id": "actor-id", "display": "Actor Name"} dictionaries
These will then be used for both validation and autocomplete, ensuring users do not attach actor IDs that are not in that list.
Example plugin implementation:
from datasette import hookimpl
@hookimpl
def datasette_acl_valid_actors(datasette):
return ["paulo", "rohan", "simon"]
This function can also return an async inner function, for making async calls. This example uses the [{"id": "actor-id", "display": "Actor Name"}] format:
from datasette import hookimpl
@hookimpl
def datasette_acl_valid_actors(datasette):
async def inner():
db = datasette.get_internal_database()
return (await db.execute("select id, username as display from users")).dicts()
return inner
Python API for managing grants
datasette_acl.grants provides async helpers so other plugins can read and modify grants without writing raw SQL against the ACL tables. Each call resolves the (resource_type, parent, child) resource, writes the acl rows, and appends audit entries attributed to by_actor.
The principal — who a grant targets — is a Principal value object, built via one of its constructors: Principal.actor(id), Principal.group(id), or a public audience Principal.everyone() / Principal.authenticated() / Principal.anonymous(). Pass it as principal=, and for grant() supply exactly one of role= or actions=:
from datasette_acl.grants import grant, revoke, update_role, list_grants, Principal
# Any signed-in user becomes a Viewer
await grant(datasette, "doc", "doc1", principal=Principal.authenticated(), role="Viewer")
# A specific user becomes an Editor
await grant(datasette, "doc", "doc1", principal=Principal.actor("alice"), role="Editor")
grant(...) — give a principal access, by raw actions or by a role (idempotent). Returns the actions now held. Role names come from the datasette_acl_roles hook:
await grant(datasette, "table", "mydb", "mytable", principal=Principal.actor("alice"), actions=["insert-row"])
await grant(datasette, "table", "mydb", "mytable", principal=Principal.group(3), actions=["insert-row"], by_actor="root")
To remove one action from a principal while keeping others, first revoke(...) all grants for that principal on the resource, then call grant(..., actions=[...]) with the actions that should remain.
update_role(...) — atomically swap a principal's actions to exactly those of a registered role. Returns the new actions:
await update_role(datasette, "doc", "doc1", principal=Principal.actor("alice"), role="Viewer")
revoke(...) — remove all of a principal's grants on a resource. Returns the actions that were removed:
await revoke(datasette, "table", "mydb", "mytable", principal=Principal.actor("alice"))
list_grants(...) — list current grants on a resource as dicts ({"principal", "actor_id", "group_id", "group_name", "actions"}, where principal is the stored principal_type; audience grants carry no id):
grants = await list_grants(datasette, "table", "mydb", "mytable")
Development
To set up this plugin locally, first checkout the code. Then create a new virtual environment:
cd datasette-acl
python -m venv venv
source venv/bin/activate
Now install the dependencies and test dependencies:
pip install -e '.[test]'
To run the tests:
python -m pytest
Tips for local development
Here's how to run the plugin with all of its features enabled.
First, grab a test database:
wget https://latest.datasette.io/fixtures.db
Install the datasette-unsafe-actor-debug plugin, so you can use the http://127.0.0.1:8001/-/unsafe-actor page to quickly imitate any actor for testing purposes:
datasette install datasette-unsafe-actor-debug
And datasette-visible-internal-db to make it easy to see what's going on in the internal database:
datasette install datasette-visible-internal-db
Then start Datasette like this:
datasette fixtures.db --internal internal.db \
-s permissions.datasette-acl.id root \
-s plugins.datasette-unsafe-actor-debug.enabled 1 \
-s plugins.datasette-acl.table-creator-permissions '["insert-row", "update-row"]' \
-s plugins.datasette-acl.dynamic-groups.staff.is_staff true \
--root \
--secret 1 \
--reload
This configures Datasette to provide a URL for you to sign in as root, which will give you access to the permission editing tool.
It ensures that any user who creates a table (which you can test using the /-/api API explorer tool) will be granted initial insert-row and update-row permissions.
It sets up a dynamic group such that any actor with {"is_staff": true} in their JSON will be treated as a member of that group.
--reload means Datasette will reload on any code changes to the plugin, and --secret 1 ensures your Datasette authentication cookies will continue to work across server restarts.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file datasette_acl-0.6a1.tar.gz.
File metadata
- Download URL: datasette_acl-0.6a1.tar.gz
- Upload date:
- Size: 94.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
21bd58e1aa663f48cf5a6def80dbd4125631cac91316b7a2e082e47136a5cf28
|
|
| MD5 |
188734db90c478148c765817b24f4eab
|
|
| BLAKE2b-256 |
fc84208e362e0e68b870a6f6b7bd366d5e481748a4c306ae9b4f3fdb4a22b48e
|
Provenance
The following attestation bundles were made for datasette_acl-0.6a1.tar.gz:
Publisher:
publish.yml on datasette/datasette-acl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
datasette_acl-0.6a1.tar.gz -
Subject digest:
21bd58e1aa663f48cf5a6def80dbd4125631cac91316b7a2e082e47136a5cf28 - Sigstore transparency entry: 1914679696
- Sigstore integration time:
-
Permalink:
datasette/datasette-acl@b586db5bef9fa18efebd47bfc13a90980dd2f395 -
Branch / Tag:
refs/tags/0.6a1 - Owner: https://github.com/datasette
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b586db5bef9fa18efebd47bfc13a90980dd2f395 -
Trigger Event:
release
-
Statement type:
File details
Details for the file datasette_acl-0.6a1-py3-none-any.whl.
File metadata
- Download URL: datasette_acl-0.6a1-py3-none-any.whl
- Upload date:
- Size: 72.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
31f2bf5ad83b44724d70e0eeb92cc4027e1af17be741936c422f16a7440e07a2
|
|
| MD5 |
e89eb537dd1f491e57ff3dd1c5d5407d
|
|
| BLAKE2b-256 |
4e4da885c4a48e081d3d2abf110af5a27c4e31b4429ed66fc12215a06f251e0f
|
Provenance
The following attestation bundles were made for datasette_acl-0.6a1-py3-none-any.whl:
Publisher:
publish.yml on datasette/datasette-acl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
datasette_acl-0.6a1-py3-none-any.whl -
Subject digest:
31f2bf5ad83b44724d70e0eeb92cc4027e1af17be741936c422f16a7440e07a2 - Sigstore transparency entry: 1914679898
- Sigstore integration time:
-
Permalink:
datasette/datasette-acl@b586db5bef9fa18efebd47bfc13a90980dd2f395 -
Branch / Tag:
refs/tags/0.6a1 - Owner: https://github.com/datasette
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b586db5bef9fa18efebd47bfc13a90980dd2f395 -
Trigger Event:
release
-
Statement type: