mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
parent
ec99bb46f8
commit
d814e81b32
6 changed files with 335 additions and 29 deletions
109
datasette/app.py
109
datasette/app.py
|
|
@ -2363,6 +2363,12 @@ class NotFoundExplicit(NotFound):
|
|||
|
||||
|
||||
class DatasetteClient:
|
||||
"""Internal HTTP client for making requests to a Datasette instance.
|
||||
|
||||
Used for testing and for internal operations that need to make HTTP requests
|
||||
to the Datasette app without going through an actual HTTP server.
|
||||
"""
|
||||
|
||||
def __init__(self, ds):
|
||||
self.ds = ds
|
||||
self.app = ds.app()
|
||||
|
|
@ -2378,40 +2384,87 @@ class DatasetteClient:
|
|||
path = f"http://localhost{path}"
|
||||
return path
|
||||
|
||||
async def _request(self, method, path, **kwargs):
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=self.app),
|
||||
cookies=kwargs.pop("cookies", None),
|
||||
) as client:
|
||||
return await getattr(client, method)(self._fix(path), **kwargs)
|
||||
async def _request(self, method, path, skip_permission_checks=False, **kwargs):
|
||||
from datasette.permissions import SkipPermissions
|
||||
|
||||
async def get(self, path, **kwargs):
|
||||
return await self._request("get", path, **kwargs)
|
||||
if skip_permission_checks:
|
||||
with SkipPermissions():
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=self.app),
|
||||
cookies=kwargs.pop("cookies", None),
|
||||
) as client:
|
||||
return await getattr(client, method)(self._fix(path), **kwargs)
|
||||
else:
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=self.app),
|
||||
cookies=kwargs.pop("cookies", None),
|
||||
) as client:
|
||||
return await getattr(client, method)(self._fix(path), **kwargs)
|
||||
|
||||
async def options(self, path, **kwargs):
|
||||
return await self._request("options", path, **kwargs)
|
||||
async def get(self, path, skip_permission_checks=False, **kwargs):
|
||||
return await self._request(
|
||||
"get", path, skip_permission_checks=skip_permission_checks, **kwargs
|
||||
)
|
||||
|
||||
async def head(self, path, **kwargs):
|
||||
return await self._request("head", path, **kwargs)
|
||||
async def options(self, path, skip_permission_checks=False, **kwargs):
|
||||
return await self._request(
|
||||
"options", path, skip_permission_checks=skip_permission_checks, **kwargs
|
||||
)
|
||||
|
||||
async def post(self, path, **kwargs):
|
||||
return await self._request("post", path, **kwargs)
|
||||
async def head(self, path, skip_permission_checks=False, **kwargs):
|
||||
return await self._request(
|
||||
"head", path, skip_permission_checks=skip_permission_checks, **kwargs
|
||||
)
|
||||
|
||||
async def put(self, path, **kwargs):
|
||||
return await self._request("put", path, **kwargs)
|
||||
async def post(self, path, skip_permission_checks=False, **kwargs):
|
||||
return await self._request(
|
||||
"post", path, skip_permission_checks=skip_permission_checks, **kwargs
|
||||
)
|
||||
|
||||
async def patch(self, path, **kwargs):
|
||||
return await self._request("patch", path, **kwargs)
|
||||
async def put(self, path, skip_permission_checks=False, **kwargs):
|
||||
return await self._request(
|
||||
"put", path, skip_permission_checks=skip_permission_checks, **kwargs
|
||||
)
|
||||
|
||||
async def delete(self, path, **kwargs):
|
||||
return await self._request("delete", path, **kwargs)
|
||||
async def patch(self, path, skip_permission_checks=False, **kwargs):
|
||||
return await self._request(
|
||||
"patch", path, skip_permission_checks=skip_permission_checks, **kwargs
|
||||
)
|
||||
|
||||
async def delete(self, path, skip_permission_checks=False, **kwargs):
|
||||
return await self._request(
|
||||
"delete", path, skip_permission_checks=skip_permission_checks, **kwargs
|
||||
)
|
||||
|
||||
async def request(self, method, path, skip_permission_checks=False, **kwargs):
|
||||
"""Make an HTTP request with the specified method.
|
||||
|
||||
Args:
|
||||
method: HTTP method (e.g., "GET", "POST", "PUT")
|
||||
path: The path to request
|
||||
skip_permission_checks: If True, bypass all permission checks for this request
|
||||
**kwargs: Additional arguments to pass to httpx
|
||||
|
||||
Returns:
|
||||
httpx.Response: The response from the request
|
||||
"""
|
||||
from datasette.permissions import SkipPermissions
|
||||
|
||||
async def request(self, method, path, **kwargs):
|
||||
avoid_path_rewrites = kwargs.pop("avoid_path_rewrites", None)
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=self.app),
|
||||
cookies=kwargs.pop("cookies", None),
|
||||
) as client:
|
||||
return await client.request(
|
||||
method, self._fix(path, avoid_path_rewrites), **kwargs
|
||||
)
|
||||
if skip_permission_checks:
|
||||
with SkipPermissions():
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=self.app),
|
||||
cookies=kwargs.pop("cookies", None),
|
||||
) as client:
|
||||
return await client.request(
|
||||
method, self._fix(path, avoid_path_rewrites), **kwargs
|
||||
)
|
||||
else:
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=self.app),
|
||||
cookies=kwargs.pop("cookies", None),
|
||||
) as client:
|
||||
return await client.request(
|
||||
method, self._fix(path, avoid_path_rewrites), **kwargs
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,33 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, NamedTuple
|
||||
import contextvars
|
||||
|
||||
|
||||
# Context variable to track when permission checks should be skipped
|
||||
_skip_permission_checks = contextvars.ContextVar(
|
||||
"skip_permission_checks", default=False
|
||||
)
|
||||
|
||||
|
||||
class SkipPermissions:
|
||||
"""Context manager to temporarily skip permission checks.
|
||||
|
||||
This is not a stable API and may change in future releases.
|
||||
|
||||
Usage:
|
||||
with SkipPermissions():
|
||||
# Permission checks are skipped within this block
|
||||
response = await datasette.client.get("/protected")
|
||||
"""
|
||||
|
||||
def __enter__(self):
|
||||
self.token = _skip_permission_checks.set(True)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
_skip_permission_checks.reset(self.token)
|
||||
return False
|
||||
|
||||
|
||||
class Resource(ABC):
|
||||
|
|
|
|||
|
|
@ -155,6 +155,16 @@ async def _build_single_action_sql(
|
|||
action=action,
|
||||
)
|
||||
|
||||
# If permission_sqls is the sentinel, skip all permission checks
|
||||
# Return SQL that allows all resources
|
||||
from datasette.utils.permissions import SKIP_PERMISSION_CHECKS
|
||||
|
||||
if permission_sqls is SKIP_PERMISSION_CHECKS:
|
||||
cols = "parent, child, 'skip_permission_checks' AS reason"
|
||||
if include_is_private:
|
||||
cols += ", 0 AS is_private"
|
||||
return f"SELECT {cols} FROM ({base_resources_sql})", {}
|
||||
|
||||
all_params = {}
|
||||
rule_sqls = []
|
||||
restriction_sqls = []
|
||||
|
|
@ -436,6 +446,17 @@ async def build_permission_rules_sql(
|
|||
action=action,
|
||||
)
|
||||
|
||||
# If permission_sqls is the sentinel, skip all permission checks
|
||||
# Return SQL that allows everything
|
||||
from datasette.utils.permissions import SKIP_PERMISSION_CHECKS
|
||||
|
||||
if permission_sqls is SKIP_PERMISSION_CHECKS:
|
||||
return (
|
||||
"SELECT NULL AS parent, NULL AS child, 1 AS allow, 'skip_permission_checks' AS reason, 'skip' AS source_plugin",
|
||||
{},
|
||||
[],
|
||||
)
|
||||
|
||||
if not permission_sqls:
|
||||
return (
|
||||
"SELECT NULL AS parent, NULL AS child, 0 AS allow, NULL AS reason, NULL AS source_plugin WHERE 0",
|
||||
|
|
|
|||
|
|
@ -10,13 +10,26 @@ from datasette.plugins import pm
|
|||
from datasette.utils import await_me_maybe
|
||||
|
||||
|
||||
# Sentinel object to indicate permission checks should be skipped
|
||||
SKIP_PERMISSION_CHECKS = object()
|
||||
|
||||
|
||||
async def gather_permission_sql_from_hooks(
|
||||
*, datasette, actor: dict | None, action: str
|
||||
) -> List[PermissionSQL]:
|
||||
) -> List[PermissionSQL] | object:
|
||||
"""Collect PermissionSQL objects from the permission_resources_sql hook.
|
||||
|
||||
Ensures that each returned PermissionSQL has a populated ``source``.
|
||||
|
||||
Returns SKIP_PERMISSION_CHECKS sentinel if skip_permission_checks context variable
|
||||
is set, signaling that all permission checks should be bypassed.
|
||||
"""
|
||||
from datasette.permissions import _skip_permission_checks
|
||||
|
||||
# Check if we should skip permission checks BEFORE calling hooks
|
||||
# This avoids creating unawaited coroutines
|
||||
if _skip_permission_checks.get():
|
||||
return SKIP_PERMISSION_CHECKS
|
||||
|
||||
hook_caller = pm.hook.permission_resources_sql
|
||||
hookimpls = hook_caller.get_hookimpls()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue