datasette/docs/permission_plugins.md

4.6 KiB

Permission Plugin Examples

These snippets show how to use the new permission_resources_sql hook to contribute rows to the action-based permission resolver. Each hook receives the current actor dictionary (or None) and must return an instance of datasette.utils.permissions.PluginSQL (or a coroutine that resolves to one).

All examples assume the plugin lives in my_permission_plugin/__init__.py and is registered using the standard entry_points mechanism.

The hook may return a single PluginSQL, None, or a list/tuple of PluginSQL objects if you need to contribute multiple rows at once.

Allow Alice To View A Specific Table

This plugin grants the actor with id == "alice" permission to perform the view-table action against the sales table inside the accounting database.

from datasette import hookimpl
from datasette.utils.permissions import PluginSQL

@hookimpl
def permission_resources_sql(datasette, actor, action):
    if action != "view-table":
        return None
    if not actor or actor.get("id") != "alice":
        return None

    return PluginSQL(
        source="alice_sales_allow",
        sql="""
            SELECT
                'accounting' AS parent,
                'sales' AS child,
                1 AS allow,
                'alice can view accounting/sales' AS reason
        """,
        params={},
    )

Restrict Execute-SQL To A Database Prefix

Only allow execute-sql against databases whose name begins with analytics_. This shows how to use parameters that the permission resolver will pass through to the SQL snippet.

from datasette import hookimpl
from datasette.utils.permissions import PluginSQL

@hookimpl
def permission_resources_sql(datasette, actor, action):
    if action != "execute-sql":
        return None

    return PluginSQL(
        source="analytics_execute_sql",
        sql="""
            SELECT
                parent,
                NULL AS child,
                1 AS allow,
                'execute-sql allowed for analytics_*' AS reason
            FROM catalog_databases
            WHERE database_name LIKE :prefix
        """,
        params={
            "prefix": "analytics_%",
        },
    )

Read Permissions From A Custom Table

This example stores grants in an internal table called permission_grants with columns (actor_id, action, parent, child, allow, reason).

from datasette import hookimpl
from datasette.utils.permissions import PluginSQL

@hookimpl
def permission_resources_sql(datasette, actor, action):
    if not actor:
        return None

    return PluginSQL(
        source="permission_grants_table",
        sql="""
            SELECT
                parent,
                child,
                allow,
                COALESCE(reason, 'permission_grants table') AS reason
            FROM permission_grants
            WHERE actor_id = :actor_id
              AND action = :action
        """,
        params={
            "actor_id": actor.get("id"),
            "action": action,
        },
    )

Default Deny With An Exception

Combine a root-level deny with a specific table allow for trusted users. The resolver will automatically apply the most specific rule.

from datasette import hookimpl
from datasette.utils.permissions import PluginSQL

TRUSTED = {"alice", "bob"}

@hookimpl
def permission_resources_sql(datasette, actor, action):
    if action != "view-table":
        return None

    actor_id = (actor or {}).get("id")

    if actor_id not in TRUSTED:
        return PluginSQL(
            source="view_table_root_deny",
            sql="""
                SELECT NULL AS parent, NULL AS child, 0 AS allow,
                       'default deny view-table' AS reason
        """,
        params={},
    )

    return PluginSQL(
        source="trusted_allow",
        sql="""
            SELECT NULL AS parent, NULL AS child, 0 AS allow,
                   'default deny view-table' AS reason
            UNION ALL
            SELECT 'reports' AS parent, 'daily_metrics' AS child, 1 AS allow,
                   'trusted user access' AS reason
        """,
        params={"actor_id": actor_id},
    )

The UNION ALL ensures the deny rule is always present, while the second row adds the exception for trusted users.

Using Datasette.allowed_resources_sql()

Within Datasette itself (or a plugin that has access to a Datasette instance) you can inspect the combined rules for debugging:

sql, params = await datasette.allowed_resources_sql(
    actor={"id": "alice"},
    action="view-table",
)
print(sql)
print(params)

The SQL can then be executed directly or embedded in other queries.