mirror of
https://github.com/simonw/datasette.git
synced 2026-07-08 16:44:34 +02:00
Filter /-/databases by view-database permission
/-/databases previously listed every attached database (including filesystem paths and sizes) to any actor with view-instance, while the homepage and every other endpoint filtered by view-database. The endpoint now only lists databases the current actor is allowed to view. JsonDataView data callbacks may now be async. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrHZSypDfMnym1tM5XJAFZ
This commit is contained in:
parent
23ccdaeffc
commit
f091b6dab1
6 changed files with 55 additions and 10 deletions
|
|
@ -2168,6 +2168,18 @@ class Datasette:
|
|||
for name, d in self.databases.items()
|
||||
]
|
||||
|
||||
async def _connected_databases_for_actor(self, actor):
|
||||
page = await self.allowed_resources("view-database", actor)
|
||||
allowed_names = {resource.parent async for resource in page.all()}
|
||||
return [
|
||||
database
|
||||
for database in self._connected_databases()
|
||||
if database["name"] in allowed_names
|
||||
]
|
||||
|
||||
async def _databases_data(self, request):
|
||||
return {"databases": await self._connected_databases_for_actor(request.actor)}
|
||||
|
||||
def _versions(self):
|
||||
conn = sqlite3.connect(":memory:")
|
||||
self._prepare_connection(conn, "_memory")
|
||||
|
|
@ -2574,7 +2586,8 @@ class Datasette:
|
|||
JsonDataView.as_view(
|
||||
self,
|
||||
"databases.json",
|
||||
lambda: {"databases": self._connected_databases()},
|
||||
self._databases_data,
|
||||
needs_request=True,
|
||||
),
|
||||
r"/-/databases(\.(?P<format>json))?$",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -53,9 +53,9 @@ class JsonDataView(BaseView):
|
|||
if self.permission:
|
||||
await self.ds.ensure_permission(action=self.permission, actor=request.actor)
|
||||
if self.needs_request:
|
||||
data = self.data_callback(request)
|
||||
data = await await_me_maybe(self.data_callback(request))
|
||||
else:
|
||||
data = self.data_callback()
|
||||
data = await await_me_maybe(self.data_callback())
|
||||
|
||||
# Return JSON or HTML depending on format parameter
|
||||
as_format = request.url_vars.get("format")
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ Any keys that include the one of the following substrings in their names will be
|
|||
/-/databases
|
||||
------------
|
||||
|
||||
Shows currently attached databases. `Databases example <https://latest.datasette.io/-/databases>`_:
|
||||
Shows currently attached databases that the current actor is allowed to view, based on the ``view-database`` permission. `Databases example <https://latest.datasette.io/-/databases>`_:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
|
|
|
|||
|
|
@ -262,8 +262,7 @@ Permission `view-instance`. No parameters.
|
|||
|
||||
Response: `{"ok": true, "databases": [...]}` — each database is
|
||||
`{"name", "route", "path", "size", "is_mutable", "is_memory", "hash"}`.
|
||||
**All attached databases are listed regardless of per-database
|
||||
`view-database` permissions**.
|
||||
Only databases the actor is allowed to `view-database` are listed.
|
||||
|
||||
### GET /-/actor(.json)
|
||||
|
||||
|
|
|
|||
|
|
@ -260,12 +260,13 @@ Concerns:
|
|||
|
||||
## 6. Permissions and security consistency (P1/P2)
|
||||
|
||||
- **(P1) `/-/databases.json` ignores per-database permissions** — it lists
|
||||
- ~~**(P1) `/-/databases.json` ignores per-database permissions** — it lists
|
||||
every attached database (name, path on disk, size) to any actor holding
|
||||
`view-instance` (app.py:2157-2169), while the homepage and every other
|
||||
endpoint filter by `view-database`. On a public instance with private
|
||||
databases this leaks filesystem paths and database names. Filter it, or
|
||||
gate it behind `permissions-debug`.
|
||||
gate it behind `permissions-debug`.~~ ✅ **Done** — the endpoint now
|
||||
filters through `allowed_resources("view-database", actor)`.
|
||||
- **(P2) `/db/-/schema` checks existence before permission**
|
||||
(views/special.py:1308-1317): an actor without `view-database` can
|
||||
distinguish "database exists" (403) from "does not exist" (404).
|
||||
|
|
@ -377,8 +378,8 @@ Two details make tiering urgent rather than optional:
|
|||
SQL errors).
|
||||
4. ~~Wrap `/-/plugins`, `/-/databases`, `/-/actions` top-level arrays in
|
||||
objects (§2).~~ ✅ Done.
|
||||
5. Filter `/-/databases.json` by `view-database` or gate it behind
|
||||
`permissions-debug` (§6).
|
||||
5. ~~Filter `/-/databases.json` by `view-database` or gate it behind
|
||||
`permissions-debug` (§6).~~ ✅ Done.
|
||||
6. 401 (not silent-anonymous) for invalid/expired bearer tokens (§1c).
|
||||
7. Publish explicit stability tiers, including extras and pagination-token
|
||||
opacity (§9).
|
||||
|
|
|
|||
|
|
@ -1800,3 +1800,35 @@ async def test_root_allow_block_with_table_restricted_actor():
|
|||
actor=admin_actor,
|
||||
)
|
||||
assert result is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_databases_json_respects_view_database(tmp_path_factory):
|
||||
# https://github.com/simonw/datasette - /-/databases should not list
|
||||
# databases the actor is not allowed to view
|
||||
db_directory = tmp_path_factory.mktemp("dbs")
|
||||
from datasette.utils import sqlite3 as _sqlite3
|
||||
|
||||
paths = []
|
||||
for name in ("public", "private"):
|
||||
path = str(db_directory / "{}.db".format(name))
|
||||
conn = _sqlite3.connect(path)
|
||||
conn.execute("vacuum")
|
||||
conn.close()
|
||||
paths.append(path)
|
||||
ds = Datasette(
|
||||
paths,
|
||||
config={"databases": {"private": {"allow": {"id": "root"}}}},
|
||||
)
|
||||
ds.root_enabled = True
|
||||
await ds.invoke_startup()
|
||||
try:
|
||||
anon_response = await ds.client.get("/-/databases.json")
|
||||
assert anon_response.status_code == 200
|
||||
anon_names = {db["name"] for db in anon_response.json()["databases"]}
|
||||
assert anon_names == {"public"}
|
||||
root_response = await ds.client.get("/-/databases.json", actor={"id": "root"})
|
||||
root_names = {db["name"] for db in root_response.json()["databases"]}
|
||||
assert root_names == {"public", "private"}
|
||||
finally:
|
||||
ds.close()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue