mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
PluginSQL renamed to PermissionSQL, closes #2524
This commit is contained in:
parent
cf887e0277
commit
5ed57607e5
11 changed files with 84 additions and 81 deletions
|
|
@ -115,7 +115,8 @@ from .tracer import AsgiTracer
|
||||||
from .plugins import pm, DEFAULT_PLUGINS, get_plugins
|
from .plugins import pm, DEFAULT_PLUGINS, get_plugins
|
||||||
from .version import __version__
|
from .version import __version__
|
||||||
|
|
||||||
from .utils.permissions import build_rules_union, PluginSQL
|
from .permissions import PermissionSQL
|
||||||
|
from .utils.permissions import build_rules_union
|
||||||
|
|
||||||
app_root = Path(__file__).parent.parent
|
app_root = Path(__file__).parent.parent
|
||||||
|
|
||||||
|
|
@ -1067,11 +1068,11 @@ class Datasette:
|
||||||
async def allowed_resources_sql(
|
async def allowed_resources_sql(
|
||||||
self, actor: dict | None, action: str
|
self, actor: dict | None, action: str
|
||||||
) -> tuple[str, dict]:
|
) -> tuple[str, dict]:
|
||||||
"""Combine permission_resources_sql PluginSQL blocks into a UNION query.
|
"""Combine permission_resources_sql PermissionSQL blocks into a UNION query.
|
||||||
|
|
||||||
Returns a (sql, params) tuple suitable for execution against SQLite.
|
Returns a (sql, params) tuple suitable for execution against SQLite.
|
||||||
"""
|
"""
|
||||||
plugin_blocks: List[PluginSQL] = []
|
plugin_blocks: List[PermissionSQL] = []
|
||||||
for block in pm.hook.permission_resources_sql(
|
for block in pm.hook.permission_resources_sql(
|
||||||
datasette=self,
|
datasette=self,
|
||||||
actor=actor,
|
actor=actor,
|
||||||
|
|
@ -1087,7 +1088,7 @@ class Datasette:
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
if candidate is None:
|
if candidate is None:
|
||||||
continue
|
continue
|
||||||
if not isinstance(candidate, PluginSQL):
|
if not isinstance(candidate, PermissionSQL):
|
||||||
continue
|
continue
|
||||||
plugin_blocks.append(candidate)
|
plugin_blocks.append(candidate)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from datasette import hookimpl, Permission
|
from datasette import hookimpl, Permission
|
||||||
from datasette.utils.permissions import PluginSQL
|
from datasette.permissions import PermissionSQL
|
||||||
from datasette.utils import actor_matches_allow
|
from datasette.utils import actor_matches_allow
|
||||||
import itsdangerous
|
import itsdangerous
|
||||||
import time
|
import time
|
||||||
|
|
@ -174,7 +174,7 @@ def permission_allowed_default(datasette, actor, action, resource):
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
async def permission_resources_sql(datasette, actor, action):
|
async def permission_resources_sql(datasette, actor, action):
|
||||||
rules: list[PluginSQL] = []
|
rules: list[PermissionSQL] = []
|
||||||
|
|
||||||
config_rules = await _config_permission_rules(datasette, actor, action)
|
config_rules = await _config_permission_rules(datasette, actor, action)
|
||||||
rules.extend(config_rules)
|
rules.extend(config_rules)
|
||||||
|
|
@ -191,7 +191,7 @@ async def permission_resources_sql(datasette, actor, action):
|
||||||
"SELECT NULL AS parent, NULL AS child, 1 AS allow, " f"'{reason}' AS reason"
|
"SELECT NULL AS parent, NULL AS child, 1 AS allow, " f"'{reason}' AS reason"
|
||||||
)
|
)
|
||||||
rules.append(
|
rules.append(
|
||||||
PluginSQL(
|
PermissionSQL(
|
||||||
source="default_permissions",
|
source="default_permissions",
|
||||||
sql=sql,
|
sql=sql,
|
||||||
params={},
|
params={},
|
||||||
|
|
@ -205,7 +205,7 @@ async def permission_resources_sql(datasette, actor, action):
|
||||||
return rules
|
return rules
|
||||||
|
|
||||||
|
|
||||||
async def _config_permission_rules(datasette, actor, action) -> list[PluginSQL]:
|
async def _config_permission_rules(datasette, actor, action) -> list[PermissionSQL]:
|
||||||
config = datasette.config or {}
|
config = datasette.config or {}
|
||||||
|
|
||||||
if actor is None:
|
if actor is None:
|
||||||
|
|
@ -332,7 +332,7 @@ async def _config_permission_rules(datasette, actor, action) -> list[PluginSQL]:
|
||||||
params[f"{key}_reason"] = reason
|
params[f"{key}_reason"] = reason
|
||||||
|
|
||||||
sql = "\nUNION ALL\n".join(parts)
|
sql = "\nUNION ALL\n".join(parts)
|
||||||
return [PluginSQL(source="config_permissions", sql=sql, params=params)]
|
return [PermissionSQL(source="config_permissions", sql=sql, params=params)]
|
||||||
|
|
||||||
|
|
||||||
async def _resolve_config_permissions_blocks(datasette, actor, action, resource):
|
async def _resolve_config_permissions_blocks(datasette, actor, action, resource):
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,8 @@ def permission_allowed(datasette, actor, action, resource):
|
||||||
def permission_resources_sql(datasette, actor, action):
|
def permission_resources_sql(datasette, actor, action):
|
||||||
"""Return SQL query fragments for permission checks on resources.
|
"""Return SQL query fragments for permission checks on resources.
|
||||||
|
|
||||||
Returns None, a PluginSQL object, or a list of PluginSQL objects.
|
Returns None, a PermissionSQL object, or a list of PermissionSQL objects.
|
||||||
Each PluginSQL contains SQL that should return rows with columns:
|
Each PermissionSQL contains SQL that should return rows with columns:
|
||||||
parent (str|None), child (str|None), allow (int), reason (str).
|
parent (str|None), child (str|None), allow (int), reason (str).
|
||||||
|
|
||||||
Used to efficiently check permissions across multiple resources at once.
|
Used to efficiently check permissions across multiple resources at once.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, NamedTuple
|
from typing import Any, Dict, Optional, NamedTuple
|
||||||
|
|
||||||
|
|
||||||
class Resource(ABC):
|
class Resource(ABC):
|
||||||
|
|
@ -86,6 +86,21 @@ class Action:
|
||||||
resource_class: type[Resource]
|
resource_class: type[Resource]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PermissionSQL:
|
||||||
|
"""
|
||||||
|
A plugin contributes SQL that yields:
|
||||||
|
parent TEXT NULL,
|
||||||
|
child TEXT NULL,
|
||||||
|
allow INTEGER, -- 1 allow, 0 deny
|
||||||
|
reason TEXT
|
||||||
|
"""
|
||||||
|
|
||||||
|
source: str # identifier used for auditing (e.g., plugin name)
|
||||||
|
sql: str # SQL that SELECTs the 4 columns above
|
||||||
|
params: Dict[str, Any] # bound params for the SQL (values only; no ':' prefix)
|
||||||
|
|
||||||
|
|
||||||
# This is obsolete, replaced by Action and ResourceType
|
# This is obsolete, replaced by Action and ResourceType
|
||||||
@dataclass
|
@dataclass
|
||||||
class Permission:
|
class Permission:
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ The core pattern is:
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from datasette.plugins import pm
|
from datasette.plugins import pm
|
||||||
from datasette.utils import await_me_maybe
|
from datasette.utils import await_me_maybe
|
||||||
from datasette.utils.permissions import PluginSQL
|
from datasette.permissions import PermissionSQL
|
||||||
|
|
||||||
|
|
||||||
async def build_allowed_resources_sql(
|
async def build_allowed_resources_sql(
|
||||||
|
|
@ -80,10 +80,10 @@ async def build_allowed_resources_sql(
|
||||||
continue
|
continue
|
||||||
if isinstance(result, list):
|
if isinstance(result, list):
|
||||||
for plugin_sql in result:
|
for plugin_sql in result:
|
||||||
if isinstance(plugin_sql, PluginSQL):
|
if isinstance(plugin_sql, PermissionSQL):
|
||||||
rule_sqls.append(plugin_sql.sql)
|
rule_sqls.append(plugin_sql.sql)
|
||||||
all_params.update(plugin_sql.params)
|
all_params.update(plugin_sql.params)
|
||||||
elif isinstance(result, PluginSQL):
|
elif isinstance(result, PermissionSQL):
|
||||||
rule_sqls.append(result.sql)
|
rule_sqls.append(result.sql)
|
||||||
all_params.update(result.params)
|
all_params.update(result.params)
|
||||||
|
|
||||||
|
|
@ -211,10 +211,10 @@ async def check_permission_for_resource(
|
||||||
continue
|
continue
|
||||||
if isinstance(result, list):
|
if isinstance(result, list):
|
||||||
for plugin_sql in result:
|
for plugin_sql in result:
|
||||||
if isinstance(plugin_sql, PluginSQL):
|
if isinstance(plugin_sql, PermissionSQL):
|
||||||
rule_sqls.append(plugin_sql.sql)
|
rule_sqls.append(plugin_sql.sql)
|
||||||
all_params.update(plugin_sql.params)
|
all_params.update(plugin_sql.params)
|
||||||
elif isinstance(result, PluginSQL):
|
elif isinstance(result, PermissionSQL):
|
||||||
rule_sqls.append(result.sql)
|
rule_sqls.append(result.sql)
|
||||||
all_params.update(result.params)
|
all_params.update(result.params)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,17 @@
|
||||||
# perm_utils.py
|
# perm_utils.py
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
|
from datasette.permissions import PermissionSQL
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Plugin interface & utilities
|
# Plugin interface & utilities
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PluginSQL:
|
|
||||||
"""
|
|
||||||
A plugin contributes SQL that yields:
|
|
||||||
parent TEXT NULL,
|
|
||||||
child TEXT NULL,
|
|
||||||
allow INTEGER, -- 1 allow, 0 deny
|
|
||||||
reason TEXT
|
|
||||||
"""
|
|
||||||
|
|
||||||
source: str # identifier used for auditing (e.g., plugin name)
|
|
||||||
sql: str # SQL that SELECTs the 4 columns above
|
|
||||||
params: Dict[str, Any] # bound params for the SQL (values only; no ':' prefix)
|
|
||||||
|
|
||||||
|
|
||||||
def _namespace_params(i: int, params: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
|
def _namespace_params(i: int, params: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Rewrite parameter placeholders to distinct names per plugin block.
|
Rewrite parameter placeholders to distinct names per plugin block.
|
||||||
|
|
@ -45,12 +31,12 @@ def _namespace_params(i: int, params: Dict[str, Any]) -> Tuple[str, Dict[str, An
|
||||||
return rewrite, namespaced
|
return rewrite, namespaced
|
||||||
|
|
||||||
|
|
||||||
PluginProvider = Callable[[str], PluginSQL]
|
PluginProvider = Callable[[str], PermissionSQL]
|
||||||
PluginOrFactory = Union[PluginSQL, PluginProvider]
|
PluginOrFactory = Union[PermissionSQL, PluginProvider]
|
||||||
|
|
||||||
|
|
||||||
def build_rules_union(
|
def build_rules_union(
|
||||||
actor: str, plugins: Sequence[PluginSQL]
|
actor: str, plugins: Sequence[PermissionSQL]
|
||||||
) -> Tuple[str, Dict[str, Any]]:
|
) -> Tuple[str, Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Compose plugin SQL into a UNION ALL with namespaced parameters.
|
Compose plugin SQL into a UNION ALL with namespaced parameters.
|
||||||
|
|
@ -107,8 +93,8 @@ async def resolve_permissions_from_catalog(
|
||||||
(Use child=NULL for parent-scoped actions like "execute-sql".)
|
(Use child=NULL for parent-scoped actions like "execute-sql".)
|
||||||
- *db* exposes: rows = await db.execute(sql, params)
|
- *db* exposes: rows = await db.execute(sql, params)
|
||||||
where rows is an iterable of sqlite3.Row
|
where rows is an iterable of sqlite3.Row
|
||||||
- plugins are either PluginSQL objects or callables accepting (action: str)
|
- plugins are either PermissionSQL objects or callables accepting (action: str)
|
||||||
and returning PluginSQL instances selecting (parent, child, allow, reason)
|
and returning PermissionSQL instances selecting (parent, child, allow, reason)
|
||||||
|
|
||||||
Decision policy:
|
Decision policy:
|
||||||
1) Specificity first: child (depth=2) > parent (depth=1) > root (depth=0)
|
1) Specificity first: child (depth=2) > parent (depth=1) > root (depth=0)
|
||||||
|
|
@ -121,14 +107,14 @@ async def resolve_permissions_from_catalog(
|
||||||
- parent, child, allow, reason, source_plugin, depth
|
- parent, child, allow, reason, source_plugin, depth
|
||||||
- resource (rendered "/parent/child" or "/parent" or "/")
|
- resource (rendered "/parent/child" or "/parent" or "/")
|
||||||
"""
|
"""
|
||||||
resolved_plugins: List[PluginSQL] = []
|
resolved_plugins: List[PermissionSQL] = []
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
if callable(plugin) and not isinstance(plugin, PluginSQL):
|
if callable(plugin) and not isinstance(plugin, PermissionSQL):
|
||||||
resolved = plugin(action) # type: ignore[arg-type]
|
resolved = plugin(action) # type: ignore[arg-type]
|
||||||
else:
|
else:
|
||||||
resolved = plugin # type: ignore[assignment]
|
resolved = plugin # type: ignore[assignment]
|
||||||
if not isinstance(resolved, PluginSQL):
|
if not isinstance(resolved, PermissionSQL):
|
||||||
raise TypeError("Plugin providers must return PluginSQL instances")
|
raise TypeError("Plugin providers must return PermissionSQL instances")
|
||||||
resolved_plugins.append(resolved)
|
resolved_plugins.append(resolved)
|
||||||
|
|
||||||
union_sql, rule_params = build_rules_union(actor, resolved_plugins)
|
union_sql, rule_params = build_rules_union(actor, resolved_plugins)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ from datasette.utils import (
|
||||||
tilde_encode,
|
tilde_encode,
|
||||||
tilde_decode,
|
tilde_decode,
|
||||||
)
|
)
|
||||||
from datasette.utils.permissions import PluginSQL, resolve_permissions_from_catalog
|
from datasette.permissions import PermissionSQL
|
||||||
|
from datasette.utils.permissions import resolve_permissions_from_catalog
|
||||||
from datasette.plugins import pm
|
from datasette.plugins import pm
|
||||||
from .base import BaseView, View
|
from .base import BaseView, View
|
||||||
import secrets
|
import secrets
|
||||||
|
|
@ -303,9 +304,9 @@ class AllowedResourcesView(BaseView):
|
||||||
for candidate in candidates:
|
for candidate in candidates:
|
||||||
if candidate is None:
|
if candidate is None:
|
||||||
continue
|
continue
|
||||||
if not isinstance(candidate, PluginSQL):
|
if not isinstance(candidate, PermissionSQL):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Skipping permission_resources_sql result %r from plugin; expected PluginSQL",
|
"Skipping permission_resources_sql result %r from plugin; expected PermissionSQL",
|
||||||
candidate,
|
candidate,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
from datasette.app import Datasette
|
from datasette.app import Datasette
|
||||||
from datasette.plugins import pm
|
from datasette.plugins import pm
|
||||||
from datasette.utils.permissions import PluginSQL
|
from datasette.permissions import PermissionSQL
|
||||||
from datasette.resources import TableResource
|
from datasette.resources import TableResource
|
||||||
from datasette import hookimpl
|
from datasette import hookimpl
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ async def test_allowed_resources_global_allow(test_ds):
|
||||||
def rules_callback(datasette, actor, action):
|
def rules_callback(datasette, actor, action):
|
||||||
if actor and actor.get("id") == "alice":
|
if actor and actor.get("id") == "alice":
|
||||||
sql = "SELECT NULL AS parent, NULL AS child, 1 AS allow, 'global: alice has access' AS reason"
|
sql = "SELECT NULL AS parent, NULL AS child, 1 AS allow, 'global: alice has access' AS reason"
|
||||||
return PluginSQL(source="test", sql=sql, params={})
|
return PermissionSQL(source="test", sql=sql, params={})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
plugin = PermissionRulesPlugin(rules_callback)
|
plugin = PermissionRulesPlugin(rules_callback)
|
||||||
|
|
@ -101,7 +101,7 @@ async def test_allowed_specific_resource(test_ds):
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'analytics' AS parent, NULL AS child, 1 AS allow, 'analyst access' AS reason
|
SELECT 'analytics' AS parent, NULL AS child, 1 AS allow, 'analyst access' AS reason
|
||||||
"""
|
"""
|
||||||
return PluginSQL(source="test", sql=sql, params={})
|
return PermissionSQL(source="test", sql=sql, params={})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
plugin = PermissionRulesPlugin(rules_callback)
|
plugin = PermissionRulesPlugin(rules_callback)
|
||||||
|
|
@ -145,7 +145,7 @@ async def test_allowed_resources_with_reasons(test_ds):
|
||||||
SELECT 'analytics' AS parent, 'sensitive' AS child, 0 AS allow,
|
SELECT 'analytics' AS parent, 'sensitive' AS child, 0 AS allow,
|
||||||
'child: sensitive data denied' AS reason
|
'child: sensitive data denied' AS reason
|
||||||
"""
|
"""
|
||||||
return PluginSQL(source="test", sql=sql, params={})
|
return PermissionSQL(source="test", sql=sql, params={})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
plugin = PermissionRulesPlugin(rules_callback)
|
plugin = PermissionRulesPlugin(rules_callback)
|
||||||
|
|
@ -185,7 +185,7 @@ async def test_child_deny_overrides_parent_allow(test_ds):
|
||||||
SELECT 'analytics' AS parent, 'sensitive' AS child, 0 AS allow,
|
SELECT 'analytics' AS parent, 'sensitive' AS child, 0 AS allow,
|
||||||
'child: deny sensitive' AS reason
|
'child: deny sensitive' AS reason
|
||||||
"""
|
"""
|
||||||
return PluginSQL(source="test", sql=sql, params={})
|
return PermissionSQL(source="test", sql=sql, params={})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
plugin = PermissionRulesPlugin(rules_callback)
|
plugin = PermissionRulesPlugin(rules_callback)
|
||||||
|
|
@ -233,7 +233,7 @@ async def test_child_allow_overrides_parent_deny(test_ds):
|
||||||
SELECT 'production' AS parent, 'orders' AS child, 1 AS allow,
|
SELECT 'production' AS parent, 'orders' AS child, 1 AS allow,
|
||||||
'child: carol can see orders' AS reason
|
'child: carol can see orders' AS reason
|
||||||
"""
|
"""
|
||||||
return PluginSQL(source="test", sql=sql, params={})
|
return PermissionSQL(source="test", sql=sql, params={})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
plugin = PermissionRulesPlugin(rules_callback)
|
plugin = PermissionRulesPlugin(rules_callback)
|
||||||
|
|
@ -304,7 +304,7 @@ async def test_sql_does_filtering_not_python(test_ds):
|
||||||
SELECT 'analytics' AS parent, 'users' AS child, 1 AS allow,
|
SELECT 'analytics' AS parent, 'users' AS child, 1 AS allow,
|
||||||
'specific allow' AS reason
|
'specific allow' AS reason
|
||||||
"""
|
"""
|
||||||
return PluginSQL(source="test", sql=sql, params={})
|
return PermissionSQL(source="test", sql=sql, params={})
|
||||||
|
|
||||||
plugin = PermissionRulesPlugin(rules_callback)
|
plugin = PermissionRulesPlugin(rules_callback)
|
||||||
pm.register(plugin, name="test_plugin")
|
pm.register(plugin, name="test_plugin")
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from datasette.app import Datasette
|
||||||
from datasette import cli, hookimpl, Permission
|
from datasette import cli, hookimpl, Permission
|
||||||
from datasette.filters import FilterArguments
|
from datasette.filters import FilterArguments
|
||||||
from datasette.plugins import get_plugins, DEFAULT_PLUGINS, pm
|
from datasette.plugins import get_plugins, DEFAULT_PLUGINS, pm
|
||||||
from datasette.utils.permissions import PluginSQL
|
from datasette.permissions import PermissionSQL
|
||||||
from datasette.utils.sqlite import sqlite3
|
from datasette.utils.sqlite import sqlite3
|
||||||
from datasette.utils import StartupError, await_me_maybe
|
from datasette.utils import StartupError, await_me_maybe
|
||||||
from jinja2 import ChoiceLoader, FileSystemLoader
|
from jinja2 import ChoiceLoader, FileSystemLoader
|
||||||
|
|
@ -722,7 +722,7 @@ async def test_hook_permission_resources_sql():
|
||||||
collected.append(block)
|
collected.append(block)
|
||||||
|
|
||||||
assert collected
|
assert collected
|
||||||
assert all(isinstance(item, PluginSQL) for item in collected)
|
assert all(isinstance(item, PermissionSQL) for item in collected)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
from datasette.app import Datasette
|
from datasette.app import Datasette
|
||||||
from datasette.plugins import pm
|
from datasette.plugins import pm
|
||||||
from datasette.utils.permissions import PluginSQL
|
from datasette.permissions import PermissionSQL
|
||||||
from datasette import hookimpl
|
from datasette import hookimpl
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -57,7 +57,7 @@ async def test_tables_endpoint_global_access(test_ds):
|
||||||
def rules_callback(datasette, actor, action):
|
def rules_callback(datasette, actor, action):
|
||||||
if actor and actor.get("id") == "alice":
|
if actor and actor.get("id") == "alice":
|
||||||
sql = "SELECT NULL AS parent, NULL AS child, 1 AS allow, 'global: alice has access' AS reason"
|
sql = "SELECT NULL AS parent, NULL AS child, 1 AS allow, 'global: alice has access' AS reason"
|
||||||
return PluginSQL(source="test", sql=sql, params={})
|
return PermissionSQL(source="test", sql=sql, params={})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
plugin = PermissionRulesPlugin(rules_callback)
|
plugin = PermissionRulesPlugin(rules_callback)
|
||||||
|
|
@ -97,7 +97,7 @@ async def test_tables_endpoint_database_restriction(test_ds):
|
||||||
if actor and actor.get("role") == "analyst":
|
if actor and actor.get("role") == "analyst":
|
||||||
# Allow only analytics database
|
# Allow only analytics database
|
||||||
sql = "SELECT 'analytics' AS parent, NULL AS child, 1 AS allow, 'analyst access' AS reason"
|
sql = "SELECT 'analytics' AS parent, NULL AS child, 1 AS allow, 'analyst access' AS reason"
|
||||||
return PluginSQL(source="test", sql=sql, params={})
|
return PermissionSQL(source="test", sql=sql, params={})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
plugin = PermissionRulesPlugin(rules_callback)
|
plugin = PermissionRulesPlugin(rules_callback)
|
||||||
|
|
@ -144,7 +144,7 @@ async def test_tables_endpoint_table_exception(test_ds):
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'analytics' AS parent, 'users' AS child, 1 AS allow, 'carol exception' AS reason
|
SELECT 'analytics' AS parent, 'users' AS child, 1 AS allow, 'carol exception' AS reason
|
||||||
"""
|
"""
|
||||||
return PluginSQL(source="test", sql=sql, params={})
|
return PermissionSQL(source="test", sql=sql, params={})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
plugin = PermissionRulesPlugin(rules_callback)
|
plugin = PermissionRulesPlugin(rules_callback)
|
||||||
|
|
@ -186,7 +186,7 @@ async def test_tables_endpoint_deny_overrides_allow(test_ds):
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'analytics' AS parent, 'sensitive' AS child, 0 AS allow, 'deny sensitive' AS reason
|
SELECT 'analytics' AS parent, 'sensitive' AS child, 0 AS allow, 'deny sensitive' AS reason
|
||||||
"""
|
"""
|
||||||
return PluginSQL(source="test", sql=sql, params={})
|
return PermissionSQL(source="test", sql=sql, params={})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
plugin = PermissionRulesPlugin(rules_callback)
|
plugin = PermissionRulesPlugin(rules_callback)
|
||||||
|
|
@ -252,7 +252,7 @@ async def test_tables_endpoint_specific_table_only(test_ds):
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'production' AS parent, 'orders' AS child, 1 AS allow, 'specific table 2' AS reason
|
SELECT 'production' AS parent, 'orders' AS child, 1 AS allow, 'specific table 2' AS reason
|
||||||
"""
|
"""
|
||||||
return PluginSQL(source="test", sql=sql, params={})
|
return PermissionSQL(source="test", sql=sql, params={})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
plugin = PermissionRulesPlugin(rules_callback)
|
plugin = PermissionRulesPlugin(rules_callback)
|
||||||
|
|
@ -290,7 +290,7 @@ async def test_tables_endpoint_empty_result(test_ds):
|
||||||
if actor and actor.get("id") == "blocked":
|
if actor and actor.get("id") == "blocked":
|
||||||
# Global deny
|
# Global deny
|
||||||
sql = "SELECT NULL AS parent, NULL AS child, 0 AS allow, 'global deny' AS reason"
|
sql = "SELECT NULL AS parent, NULL AS child, 0 AS allow, 'global deny' AS reason"
|
||||||
return PluginSQL(source="test", sql=sql, params={})
|
return PermissionSQL(source="test", sql=sql, params={})
|
||||||
return None
|
return None
|
||||||
|
|
||||||
plugin = PermissionRulesPlugin(rules_callback)
|
plugin = PermissionRulesPlugin(rules_callback)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import pytest
|
import pytest
|
||||||
from datasette.app import Datasette
|
from datasette.app import Datasette
|
||||||
|
from datasette.permissions import PermissionSQL
|
||||||
from datasette.utils.permissions import (
|
from datasette.utils.permissions import (
|
||||||
PluginSQL,
|
|
||||||
PluginProvider,
|
PluginProvider,
|
||||||
resolve_permissions_from_catalog,
|
resolve_permissions_from_catalog,
|
||||||
)
|
)
|
||||||
|
|
@ -26,8 +26,8 @@ NO_RULES_SQL = (
|
||||||
|
|
||||||
|
|
||||||
def plugin_allow_all_for_user(user: str) -> PluginProvider:
|
def plugin_allow_all_for_user(user: str) -> PluginProvider:
|
||||||
def provider(action: str) -> PluginSQL:
|
def provider(action: str) -> PermissionSQL:
|
||||||
return PluginSQL(
|
return PermissionSQL(
|
||||||
"allow_all",
|
"allow_all",
|
||||||
"""
|
"""
|
||||||
SELECT NULL AS parent, NULL AS child, 1 AS allow,
|
SELECT NULL AS parent, NULL AS child, 1 AS allow,
|
||||||
|
|
@ -41,8 +41,8 @@ def plugin_allow_all_for_user(user: str) -> PluginProvider:
|
||||||
|
|
||||||
|
|
||||||
def plugin_deny_specific_table(user: str, parent: str, child: str) -> PluginProvider:
|
def plugin_deny_specific_table(user: str, parent: str, child: str) -> PluginProvider:
|
||||||
def provider(action: str) -> PluginSQL:
|
def provider(action: str) -> PermissionSQL:
|
||||||
return PluginSQL(
|
return PermissionSQL(
|
||||||
"deny_specific_table",
|
"deny_specific_table",
|
||||||
"""
|
"""
|
||||||
SELECT :parent AS parent, :child AS child, 0 AS allow,
|
SELECT :parent AS parent, :child AS child, 0 AS allow,
|
||||||
|
|
@ -56,8 +56,8 @@ def plugin_deny_specific_table(user: str, parent: str, child: str) -> PluginProv
|
||||||
|
|
||||||
|
|
||||||
def plugin_org_policy_deny_parent(parent: str) -> PluginProvider:
|
def plugin_org_policy_deny_parent(parent: str) -> PluginProvider:
|
||||||
def provider(action: str) -> PluginSQL:
|
def provider(action: str) -> PermissionSQL:
|
||||||
return PluginSQL(
|
return PermissionSQL(
|
||||||
"org_policy_parent_deny",
|
"org_policy_parent_deny",
|
||||||
"""
|
"""
|
||||||
SELECT :parent AS parent, NULL AS child, 0 AS allow,
|
SELECT :parent AS parent, NULL AS child, 0 AS allow,
|
||||||
|
|
@ -70,8 +70,8 @@ def plugin_org_policy_deny_parent(parent: str) -> PluginProvider:
|
||||||
|
|
||||||
|
|
||||||
def plugin_allow_parent_for_user(user: str, parent: str) -> PluginProvider:
|
def plugin_allow_parent_for_user(user: str, parent: str) -> PluginProvider:
|
||||||
def provider(action: str) -> PluginSQL:
|
def provider(action: str) -> PermissionSQL:
|
||||||
return PluginSQL(
|
return PermissionSQL(
|
||||||
"allow_parent",
|
"allow_parent",
|
||||||
"""
|
"""
|
||||||
SELECT :parent AS parent, NULL AS child, 1 AS allow,
|
SELECT :parent AS parent, NULL AS child, 1 AS allow,
|
||||||
|
|
@ -85,8 +85,8 @@ def plugin_allow_parent_for_user(user: str, parent: str) -> PluginProvider:
|
||||||
|
|
||||||
|
|
||||||
def plugin_child_allow_for_user(user: str, parent: str, child: str) -> PluginProvider:
|
def plugin_child_allow_for_user(user: str, parent: str, child: str) -> PluginProvider:
|
||||||
def provider(action: str) -> PluginSQL:
|
def provider(action: str) -> PermissionSQL:
|
||||||
return PluginSQL(
|
return PermissionSQL(
|
||||||
"allow_child",
|
"allow_child",
|
||||||
"""
|
"""
|
||||||
SELECT :parent AS parent, :child AS child, 1 AS allow,
|
SELECT :parent AS parent, :child AS child, 1 AS allow,
|
||||||
|
|
@ -100,8 +100,8 @@ def plugin_child_allow_for_user(user: str, parent: str, child: str) -> PluginPro
|
||||||
|
|
||||||
|
|
||||||
def plugin_root_deny_for_all() -> PluginProvider:
|
def plugin_root_deny_for_all() -> PluginProvider:
|
||||||
def provider(action: str) -> PluginSQL:
|
def provider(action: str) -> PermissionSQL:
|
||||||
return PluginSQL(
|
return PermissionSQL(
|
||||||
"root_deny",
|
"root_deny",
|
||||||
"""
|
"""
|
||||||
SELECT NULL AS parent, NULL AS child, 0 AS allow, 'root deny for all on ' || :action AS reason
|
SELECT NULL AS parent, NULL AS child, 0 AS allow, 'root deny for all on ' || :action AS reason
|
||||||
|
|
@ -115,8 +115,8 @@ def plugin_root_deny_for_all() -> PluginProvider:
|
||||||
def plugin_conflicting_same_child_rules(
|
def plugin_conflicting_same_child_rules(
|
||||||
user: str, parent: str, child: str
|
user: str, parent: str, child: str
|
||||||
) -> List[PluginProvider]:
|
) -> List[PluginProvider]:
|
||||||
def allow_provider(action: str) -> PluginSQL:
|
def allow_provider(action: str) -> PermissionSQL:
|
||||||
return PluginSQL(
|
return PermissionSQL(
|
||||||
"conflict_child_allow",
|
"conflict_child_allow",
|
||||||
"""
|
"""
|
||||||
SELECT :parent AS parent, :child AS child, 1 AS allow,
|
SELECT :parent AS parent, :child AS child, 1 AS allow,
|
||||||
|
|
@ -126,8 +126,8 @@ def plugin_conflicting_same_child_rules(
|
||||||
{"parent": parent, "child": child, "user": user, "action": action},
|
{"parent": parent, "child": child, "user": user, "action": action},
|
||||||
)
|
)
|
||||||
|
|
||||||
def deny_provider(action: str) -> PluginSQL:
|
def deny_provider(action: str) -> PermissionSQL:
|
||||||
return PluginSQL(
|
return PermissionSQL(
|
||||||
"conflict_child_deny",
|
"conflict_child_deny",
|
||||||
"""
|
"""
|
||||||
SELECT :parent AS parent, :child AS child, 0 AS allow,
|
SELECT :parent AS parent, :child AS child, 0 AS allow,
|
||||||
|
|
@ -141,14 +141,14 @@ def plugin_conflicting_same_child_rules(
|
||||||
|
|
||||||
|
|
||||||
def plugin_allow_all_for_action(user: str, allowed_action: str) -> PluginProvider:
|
def plugin_allow_all_for_action(user: str, allowed_action: str) -> PluginProvider:
|
||||||
def provider(action: str) -> PluginSQL:
|
def provider(action: str) -> PermissionSQL:
|
||||||
if action != allowed_action:
|
if action != allowed_action:
|
||||||
return PluginSQL(
|
return PermissionSQL(
|
||||||
f"allow_all_{allowed_action}_noop",
|
f"allow_all_{allowed_action}_noop",
|
||||||
NO_RULES_SQL,
|
NO_RULES_SQL,
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
return PluginSQL(
|
return PermissionSQL(
|
||||||
f"allow_all_{allowed_action}",
|
f"allow_all_{allowed_action}",
|
||||||
"""
|
"""
|
||||||
SELECT NULL AS parent, NULL AS child, 1 AS allow,
|
SELECT NULL AS parent, NULL AS child, 1 AS allow,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue