mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
New allowed_resources_sql plugin hook and debug tools (#2505)
* allowed_resources_sql plugin hook and infrastructure * New methods for checking permissions with the new system * New /-/allowed and /-/check and /-/rules special endpoints Still needs to be integrated more deeply into Datasette, especially for listing visible tables. Refs: #2502 --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
85da8474d4
commit
27084caa04
20 changed files with 3381 additions and 27 deletions
160
datasette/app.py
160
datasette/app.py
|
|
@ -49,6 +49,9 @@ from .views.special import (
|
|||
AllowDebugView,
|
||||
PermissionsDebugView,
|
||||
MessagesDebugView,
|
||||
AllowedResourcesView,
|
||||
PermissionRulesView,
|
||||
PermissionCheckView,
|
||||
)
|
||||
from .views.table import (
|
||||
TableInsertView,
|
||||
|
|
@ -111,6 +114,8 @@ from .tracer import AsgiTracer
|
|||
from .plugins import pm, DEFAULT_PLUGINS, get_plugins
|
||||
from .version import __version__
|
||||
|
||||
from .utils.permissions import build_rules_union, PluginSQL
|
||||
|
||||
app_root = Path(__file__).parent.parent
|
||||
|
||||
# https://github.com/simonw/datasette/issues/283#issuecomment-781591015
|
||||
|
|
@ -1030,6 +1035,149 @@ class Datasette:
|
|||
)
|
||||
return result
|
||||
|
||||
async def allowed_resources_sql(
|
||||
self, actor: dict | None, action: str
|
||||
) -> tuple[str, dict]:
|
||||
"""Combine permission_resources_sql PluginSQL blocks into a UNION query.
|
||||
|
||||
Returns a (sql, params) tuple suitable for execution against SQLite.
|
||||
"""
|
||||
plugin_blocks: List[PluginSQL] = []
|
||||
for block in pm.hook.permission_resources_sql(
|
||||
datasette=self,
|
||||
actor=actor,
|
||||
action=action,
|
||||
):
|
||||
block = await await_me_maybe(block)
|
||||
if block is None:
|
||||
continue
|
||||
if isinstance(block, (list, tuple)):
|
||||
candidates = block
|
||||
else:
|
||||
candidates = [block]
|
||||
for candidate in candidates:
|
||||
if candidate is None:
|
||||
continue
|
||||
if not isinstance(candidate, PluginSQL):
|
||||
continue
|
||||
plugin_blocks.append(candidate)
|
||||
|
||||
actor_id = actor.get("id") if actor else None
|
||||
sql, params = build_rules_union(
|
||||
actor=str(actor_id) if actor_id is not None else "",
|
||||
plugins=plugin_blocks,
|
||||
)
|
||||
return sql, params
|
||||
|
||||
async def permission_allowed_2(
|
||||
self, actor, action, resource=None, *, default=DEFAULT_NOT_SET
|
||||
):
|
||||
"""Permission check backed by permission_resources_sql rules."""
|
||||
|
||||
if default is DEFAULT_NOT_SET and action in self.permissions:
|
||||
default = self.permissions[action].default
|
||||
|
||||
if isinstance(actor, dict) or actor is None:
|
||||
actor_dict = actor
|
||||
else:
|
||||
actor_dict = {"id": actor}
|
||||
actor_id = actor_dict.get("id") if actor_dict else None
|
||||
|
||||
candidate_parent = None
|
||||
candidate_child = None
|
||||
if isinstance(resource, str):
|
||||
candidate_parent = resource
|
||||
elif isinstance(resource, (tuple, list)) and len(resource) == 2:
|
||||
candidate_parent, candidate_child = resource
|
||||
elif resource is not None:
|
||||
raise TypeError("resource must be None, str, or (parent, child) tuple")
|
||||
|
||||
union_sql, union_params = await self.allowed_resources_sql(actor_dict, action)
|
||||
|
||||
query = f"""
|
||||
WITH rules AS (
|
||||
{union_sql}
|
||||
),
|
||||
candidate AS (
|
||||
SELECT :cand_parent AS parent, :cand_child AS child
|
||||
),
|
||||
matched AS (
|
||||
SELECT
|
||||
r.allow,
|
||||
r.reason,
|
||||
r.source_plugin,
|
||||
CASE
|
||||
WHEN r.child IS NOT NULL THEN 2
|
||||
WHEN r.parent IS NOT NULL THEN 1
|
||||
ELSE 0
|
||||
END AS depth
|
||||
FROM rules r
|
||||
JOIN candidate c
|
||||
ON (r.parent IS NULL OR r.parent = c.parent)
|
||||
AND (r.child IS NULL OR r.child = c.child)
|
||||
),
|
||||
ranked AS (
|
||||
SELECT *,
|
||||
ROW_NUMBER() OVER (
|
||||
ORDER BY
|
||||
depth DESC,
|
||||
CASE WHEN allow = 0 THEN 0 ELSE 1 END,
|
||||
source_plugin
|
||||
) AS rn
|
||||
FROM matched
|
||||
),
|
||||
winner AS (
|
||||
SELECT allow, reason, source_plugin, depth
|
||||
FROM ranked
|
||||
WHERE rn = 1
|
||||
)
|
||||
SELECT allow, reason, source_plugin, depth FROM winner
|
||||
"""
|
||||
|
||||
params = {
|
||||
**union_params,
|
||||
"cand_parent": candidate_parent,
|
||||
"cand_child": candidate_child,
|
||||
}
|
||||
|
||||
rows = await self.get_internal_database().execute(query, params)
|
||||
row = rows.first()
|
||||
|
||||
reason = None
|
||||
source_plugin = None
|
||||
depth = None
|
||||
used_default = False
|
||||
|
||||
if row is None:
|
||||
result = default
|
||||
used_default = True
|
||||
else:
|
||||
allow = row["allow"]
|
||||
reason = row["reason"]
|
||||
source_plugin = row["source_plugin"]
|
||||
depth = row["depth"]
|
||||
if allow is None:
|
||||
result = default
|
||||
used_default = True
|
||||
else:
|
||||
result = bool(allow)
|
||||
|
||||
self._permission_checks.append(
|
||||
{
|
||||
"when": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
"actor": actor,
|
||||
"action": action,
|
||||
"resource": resource,
|
||||
"used_default": used_default,
|
||||
"result": result,
|
||||
"reason": reason,
|
||||
"source_plugin": source_plugin,
|
||||
"depth": depth,
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
async def ensure_permissions(
|
||||
self,
|
||||
actor: dict,
|
||||
|
|
@ -1586,6 +1734,18 @@ class Datasette:
|
|||
PermissionsDebugView.as_view(self),
|
||||
r"/-/permissions$",
|
||||
)
|
||||
add_route(
|
||||
AllowedResourcesView.as_view(self),
|
||||
r"/-/allowed(\.(?P<format>json))?$",
|
||||
)
|
||||
add_route(
|
||||
PermissionRulesView.as_view(self),
|
||||
r"/-/rules(\.(?P<format>json))?$",
|
||||
)
|
||||
add_route(
|
||||
PermissionCheckView.as_view(self),
|
||||
r"/-/check(\.(?P<format>json))?$",
|
||||
)
|
||||
add_route(
|
||||
MessagesDebugView.as_view(self),
|
||||
r"/-/messages$",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue