mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Merge pull request from GHSA-7ch3-7pp7-7cpq
* API explorer requires view-instance permission * Check database/table permissions on /-/api page * Release notes for 1.0a4 Refs #2119, #2133, #2138, #2140 Refs https://github.com/simonw/datasette/security/advisories/GHSA-7ch3-7pp7-7cpq
This commit is contained in:
parent
943df09dcc
commit
01e0558825
5 changed files with 99 additions and 7 deletions
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1>API Explorer</h1>
|
<h1>API Explorer{% if private %} 🔒{% endif %}</h1>
|
||||||
|
|
||||||
<p>Use this tool to try out the
|
<p>Use this tool to try out the
|
||||||
{% if datasette_version %}
|
{% if datasette_version %}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
__version__ = "1.0a3"
|
__version__ = "1.0a4"
|
||||||
__version_info__ = tuple(__version__.split("."))
|
__version_info__ = tuple(__version__.split("."))
|
||||||
|
|
|
||||||
|
|
@ -354,9 +354,7 @@ class ApiExplorerView(BaseView):
|
||||||
if name == "_internal":
|
if name == "_internal":
|
||||||
continue
|
continue
|
||||||
database_visible, _ = await self.ds.check_visibility(
|
database_visible, _ = await self.ds.check_visibility(
|
||||||
request.actor,
|
request.actor, permissions=[("view-database", name), "view-instance"]
|
||||||
"view-database",
|
|
||||||
name,
|
|
||||||
)
|
)
|
||||||
if not database_visible:
|
if not database_visible:
|
||||||
continue
|
continue
|
||||||
|
|
@ -365,8 +363,11 @@ class ApiExplorerView(BaseView):
|
||||||
for table in table_names:
|
for table in table_names:
|
||||||
visible, _ = await self.ds.check_visibility(
|
visible, _ = await self.ds.check_visibility(
|
||||||
request.actor,
|
request.actor,
|
||||||
"view-table",
|
permissions=[
|
||||||
(name, table),
|
("view-table", (name, table)),
|
||||||
|
("view-database", name),
|
||||||
|
"view-instance",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
if not visible:
|
if not visible:
|
||||||
continue
|
continue
|
||||||
|
|
@ -463,6 +464,13 @@ class ApiExplorerView(BaseView):
|
||||||
return databases
|
return databases
|
||||||
|
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
|
visible, private = await self.ds.check_visibility(
|
||||||
|
request.actor,
|
||||||
|
permissions=["view-instance"],
|
||||||
|
)
|
||||||
|
if not visible:
|
||||||
|
raise Forbidden("You do not have permission to view this instance")
|
||||||
|
|
||||||
def api_path(link):
|
def api_path(link):
|
||||||
return "/-/api#{}".format(
|
return "/-/api#{}".format(
|
||||||
urllib.parse.urlencode(
|
urllib.parse.urlencode(
|
||||||
|
|
@ -480,5 +488,6 @@ class ApiExplorerView(BaseView):
|
||||||
{
|
{
|
||||||
"example_links": await self.example_links(request),
|
"example_links": await self.example_links(request),
|
||||||
"api_path": api_path,
|
"api_path": api_path,
|
||||||
|
"private": private,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,22 @@
|
||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
.. _v1_0_a4:
|
||||||
|
|
||||||
|
1.0a4 (2023-08-21)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
This alpha fixes a security issue with the ``/-/api`` API explorer. On authenticated Datasette instances (instances protected using plugins such as `datasette-auth-passwords <https://datasette.io/plugins/datasette-auth-passwords>`__) the API explorer interface could reveal the names of databases and tables within the protected instance. The data stored in those tables was not revealed.
|
||||||
|
|
||||||
|
For more information and workarounds, read `the security advisory <https://github.com/simonw/datasette/security/advisories/GHSA-7ch3-7pp7-7cpq>`__. The issue has been present in every previous alpha version of Datasette 1.0: versions 1.0a0, 1.0a1, 1.0a2 and 1.0a3.
|
||||||
|
|
||||||
|
Also in this alpha:
|
||||||
|
|
||||||
|
- The new ``datasette plugins --requirements`` option outputs a list of currently installed plugins in Python ``requirements.txt`` format, useful for duplicating that installation elsewhere. (:issue:`2133`)
|
||||||
|
- :ref:`canned_queries_writable` can now define a ``on_success_message_sql`` field in their configuration, containing a SQL query that should be executed upon successful completion of the write operation in order to generate a message to be shown to the user. (:issue:`2138`)
|
||||||
|
- The automatically generated border color for a database is now shown in more places around the application. (:issue:`2119`)
|
||||||
|
- Every instance of example shell script code in the documentation should now include a working copy button, free from additional syntax. (:issue:`2140`)
|
||||||
|
|
||||||
.. _v1_0_a3:
|
.. _v1_0_a3:
|
||||||
|
|
||||||
1.0a3 (2023-08-09)
|
1.0a3 (2023-08-09)
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ async def perms_ds():
|
||||||
(
|
(
|
||||||
"/",
|
"/",
|
||||||
"/fixtures",
|
"/fixtures",
|
||||||
|
"/-/api",
|
||||||
"/fixtures/compound_three_primary_keys",
|
"/fixtures/compound_three_primary_keys",
|
||||||
"/fixtures/compound_three_primary_keys/a,a,a",
|
"/fixtures/compound_three_primary_keys/a,a,a",
|
||||||
"/fixtures/two", # Query
|
"/fixtures/two", # Query
|
||||||
|
|
@ -951,3 +952,69 @@ def test_cli_create_token(options, expected):
|
||||||
)
|
)
|
||||||
assert 0 == result2.exit_code, result2.output
|
assert 0 == result2.exit_code, result2.output
|
||||||
assert json.loads(result2.output) == {"actor": expected}
|
assert json.loads(result2.output) == {"actor": expected}
|
||||||
|
|
||||||
|
|
||||||
|
_visible_tables_re = re.compile(r">\/((\w+)\/(\w+))\.json<\/a> - Get rows for")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"is_logged_in,metadata,expected_visible_tables",
|
||||||
|
(
|
||||||
|
# Unprotected instance logged out user sees everything:
|
||||||
|
(
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
["perms_ds_one/t1", "perms_ds_one/t2", "perms_ds_two/t1"],
|
||||||
|
),
|
||||||
|
# Fully protected instance logged out user sees nothing
|
||||||
|
(False, {"allow": {"id": "user"}}, None),
|
||||||
|
# User with visibility of just perms_ds_one sees both tables there
|
||||||
|
(
|
||||||
|
True,
|
||||||
|
{
|
||||||
|
"databases": {
|
||||||
|
"perms_ds_one": {"allow": {"id": "user"}},
|
||||||
|
"perms_ds_two": {"allow": False},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
["perms_ds_one/t1", "perms_ds_one/t2"],
|
||||||
|
),
|
||||||
|
# User with visibility of only table perms_ds_one/t1 sees just that one
|
||||||
|
(
|
||||||
|
True,
|
||||||
|
{
|
||||||
|
"databases": {
|
||||||
|
"perms_ds_one": {
|
||||||
|
"allow": {"id": "user"},
|
||||||
|
"tables": {"t2": {"allow": False}},
|
||||||
|
},
|
||||||
|
"perms_ds_two": {"allow": False},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
["perms_ds_one/t1"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def test_api_explorer_visibility(
|
||||||
|
perms_ds, is_logged_in, metadata, expected_visible_tables
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
prev_metadata = perms_ds._metadata_local
|
||||||
|
perms_ds._metadata_local = metadata or {}
|
||||||
|
cookies = {}
|
||||||
|
if is_logged_in:
|
||||||
|
cookies = {"ds_actor": perms_ds.client.actor_cookie({"id": "user"})}
|
||||||
|
response = await perms_ds.client.get("/-/api", cookies=cookies)
|
||||||
|
if expected_visible_tables:
|
||||||
|
assert response.status_code == 200
|
||||||
|
# Search HTML for stuff matching:
|
||||||
|
# '>/perms_ds_one/t2.json</a> - Get rows for'
|
||||||
|
visible_tables = [
|
||||||
|
match[0] for match in _visible_tables_re.findall(response.text)
|
||||||
|
]
|
||||||
|
assert visible_tables == expected_visible_tables
|
||||||
|
else:
|
||||||
|
assert response.status_code == 403
|
||||||
|
finally:
|
||||||
|
perms_ds._metadata_local = prev_metadata
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue