mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
view-database permission
Also now using 🔒 to indicate private resources - resources that would not be available to the anonymous user. Refs #811
This commit is contained in:
parent
613fa551a1
commit
9b42e1a4f5
8 changed files with 69 additions and 16 deletions
|
|
@ -11,6 +11,12 @@ def permission_allowed(datasette, actor, action, resource_type, resource_identif
|
||||||
allow = datasette.metadata("allow")
|
allow = datasette.metadata("allow")
|
||||||
if allow is not None:
|
if allow is not None:
|
||||||
return actor_matches_allow(actor, allow)
|
return actor_matches_allow(actor, allow)
|
||||||
|
elif action == "view-database":
|
||||||
|
assert resource_type == "database"
|
||||||
|
database_allow = datasette.metadata("allow", database=resource_identifier)
|
||||||
|
if database_allow is None:
|
||||||
|
return True
|
||||||
|
return actor_matches_allow(actor, database_allow)
|
||||||
elif action == "view-query":
|
elif action == "view-query":
|
||||||
# Check if this query has a "allow" block in metadata
|
# Check if this query has a "allow" block in metadata
|
||||||
assert resource_type == "query"
|
assert resource_type == "query"
|
||||||
|
|
@ -20,7 +26,6 @@ def permission_allowed(datasette, actor, action, resource_type, resource_identif
|
||||||
if isinstance(queries_metadata[query_name], str):
|
if isinstance(queries_metadata[query_name], str):
|
||||||
return True
|
return True
|
||||||
allow = queries_metadata[query_name].get("allow")
|
allow = queries_metadata[query_name].get("allow")
|
||||||
print("checking allow - actor = {}, allow = {}".format(actor, allow))
|
|
||||||
if allow is None:
|
if allow is None:
|
||||||
return True
|
return True
|
||||||
return actor_matches_allow(actor, allow)
|
return actor_matches_allow(actor, allow)
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
<h2 id="queries">Queries</h2>
|
<h2 id="queries">Queries</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% for query in queries %}
|
{% for query in queries %}
|
||||||
<li><a href="{{ database_url(database) }}/{{ query.name|urlencode }}{% if query.fragment %}#{{ query.fragment }}{% endif %}" title="{{ query.description or query.sql }}">{{ query.title or query.name }}</a> {% if query.requires_auth %} - requires authentication{% endif %}</li>
|
<li><a href="{{ database_url(database) }}/{{ query.name|urlencode }}{% if query.fragment %}#{{ query.fragment }}{% endif %}" title="{{ query.description or query.sql }}">{{ query.title or query.name }}</a> {% if query.private %} 🔒{% endif %}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
|
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
|
||||||
|
|
||||||
{% for database in databases %}
|
{% for database in databases %}
|
||||||
<h2 style="padding-left: 10px; border-left: 10px solid #{{ database.color }}"><a href="{{ database.path }}">{{ database.name }}</a></h2>
|
<h2 style="padding-left: 10px; border-left: 10px solid #{{ database.color }}"><a href="{{ database.path }}">{{ database.name }}</a>{% if database.private %} 🔒{% endif %}</h2>
|
||||||
<p>
|
<p>
|
||||||
{% if database.show_table_row_counts %}{{ "{:,}".format(database.table_rows_sum) }} rows in {% endif %}{{ database.tables_count }} table{% if database.tables_count != 1 %}s{% endif %}{% if database.tables_count and database.hidden_tables_count %}, {% endif -%}
|
{% if database.show_table_row_counts %}{{ "{:,}".format(database.table_rows_sum) }} rows in {% endif %}{{ database.tables_count }} table{% if database.tables_count != 1 %}s{% endif %}{% if database.tables_count and database.hidden_tables_count %}, {% endif -%}
|
||||||
{% if database.hidden_tables_count -%}
|
{% if database.hidden_tables_count -%}
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,7 @@ class DatabaseView(DataView):
|
||||||
tables.sort(key=lambda t: (t["hidden"], t["name"]))
|
tables.sort(key=lambda t: (t["hidden"], t["name"]))
|
||||||
canned_queries = [
|
canned_queries = [
|
||||||
dict(
|
dict(
|
||||||
query,
|
query, private=not actor_matches_allow(None, query.get("allow", None)),
|
||||||
requires_auth=not actor_matches_allow(None, query.get("allow", None)),
|
|
||||||
)
|
)
|
||||||
for query in self.ds.get_canned_queries(database)
|
for query in self.ds.get_canned_queries(database)
|
||||||
if actor_matches_allow(
|
if actor_matches_allow(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import hashlib
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from datasette.utils import CustomJSONEncoder
|
from datasette.utils import CustomJSONEncoder
|
||||||
from datasette.utils.asgi import Response
|
from datasette.utils.asgi import Response, Forbidden
|
||||||
from datasette.version import __version__
|
from datasette.version import __version__
|
||||||
|
|
||||||
from .base import BaseView
|
from .base import BaseView
|
||||||
|
|
@ -25,6 +25,22 @@ class IndexView(BaseView):
|
||||||
await self.check_permission(request, "view-instance")
|
await self.check_permission(request, "view-instance")
|
||||||
databases = []
|
databases = []
|
||||||
for name, db in self.ds.databases.items():
|
for name, db in self.ds.databases.items():
|
||||||
|
# Check permission
|
||||||
|
allowed = await self.ds.permission_allowed(
|
||||||
|
request.scope.get("actor"),
|
||||||
|
"view-database",
|
||||||
|
resource_type="database",
|
||||||
|
resource_identifier=name,
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
if not allowed:
|
||||||
|
continue
|
||||||
|
private = not await self.ds.permission_allowed(
|
||||||
|
None,
|
||||||
|
"view-database",
|
||||||
|
resource_type="database",
|
||||||
|
resource_identifier=name,
|
||||||
|
)
|
||||||
table_names = await db.table_names()
|
table_names = await db.table_names()
|
||||||
hidden_table_names = set(await db.hidden_table_names())
|
hidden_table_names = set(await db.hidden_table_names())
|
||||||
views = await db.view_names()
|
views = await db.view_names()
|
||||||
|
|
@ -95,6 +111,7 @@ class IndexView(BaseView):
|
||||||
),
|
),
|
||||||
"hidden_tables_count": len(hidden_tables),
|
"hidden_tables_count": len(hidden_tables),
|
||||||
"views_count": len(views),
|
"views_count": len(views),
|
||||||
|
"private": private,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,13 +120,12 @@ def test_canned_query_permissions_on_database_page(canned_write_client):
|
||||||
)
|
)
|
||||||
assert 200 == response.status
|
assert 200 == response.status
|
||||||
assert [
|
assert [
|
||||||
{"name": "add_name", "requires_auth": False},
|
{"name": "add_name", "private": False},
|
||||||
{"name": "add_name_specify_id", "requires_auth": False},
|
{"name": "add_name_specify_id", "private": False},
|
||||||
{"name": "delete_name", "requires_auth": True},
|
{"name": "delete_name", "private": True},
|
||||||
{"name": "update_name", "requires_auth": False},
|
{"name": "update_name", "private": False},
|
||||||
] == [
|
] == [
|
||||||
{"name": q["name"], "requires_auth": q["requires_auth"]}
|
{"name": q["name"], "private": q["private"]} for q in response.json["queries"]
|
||||||
for q in response.json["queries"]
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,10 +207,7 @@ def test_row_page_does_not_truncate():
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert_permissions_checked(
|
assert_permissions_checked(
|
||||||
client.ds,
|
client.ds,
|
||||||
[
|
["view-instance", ("view-table", "table", ("fixtures", "facetable")),],
|
||||||
"view-instance",
|
|
||||||
("view-table", "table", ("fixtures", "facetable")),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
table = Soup(response.body, "html.parser").find("table")
|
table = Soup(response.body, "html.parser").find("table")
|
||||||
assert table["class"] == ["rows-and-columns"]
|
assert table["class"] == ["rows-and-columns"]
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,39 @@ def test_view_instance(allow, expected_anon, expected_auth):
|
||||||
path, cookies={"ds_actor": client.ds.sign({"id": "root"}, "actor")},
|
path, cookies={"ds_actor": client.ds.sign({"id": "root"}, "actor")},
|
||||||
)
|
)
|
||||||
assert expected_auth == auth_response.status
|
assert expected_auth == auth_response.status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"allow,expected_anon,expected_auth",
|
||||||
|
[(None, 200, 200), ({}, 403, 403), ({"id": "root"}, 403, 200),],
|
||||||
|
)
|
||||||
|
def test_view_database(allow, expected_anon, expected_auth):
|
||||||
|
with make_app_client(
|
||||||
|
metadata={"databases": {"fixtures": {"allow": allow}}}
|
||||||
|
) as client:
|
||||||
|
for path in (
|
||||||
|
"/fixtures",
|
||||||
|
"/fixtures/compound_three_primary_keys",
|
||||||
|
"/fixtures/compound_three_primary_keys/a,a,a",
|
||||||
|
):
|
||||||
|
anon_response = client.get(path)
|
||||||
|
assert expected_anon == anon_response.status
|
||||||
|
auth_response = client.get(
|
||||||
|
path, cookies={"ds_actor": client.ds.sign({"id": "root"}, "actor")},
|
||||||
|
)
|
||||||
|
assert expected_auth == auth_response.status
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_list_respects_view_database():
|
||||||
|
with make_app_client(
|
||||||
|
metadata={"databases": {"fixtures": {"allow": {"id": "root"}}}},
|
||||||
|
extra_databases={"data.db": "create table names (name text)"},
|
||||||
|
) as client:
|
||||||
|
anon_response = client.get("/")
|
||||||
|
assert '<a href="/data">data</a></h2>' in anon_response.text
|
||||||
|
assert '<a href="/fixtures">fixtures</a>' not in anon_response.text
|
||||||
|
auth_response = client.get(
|
||||||
|
"/", cookies={"ds_actor": client.ds.sign({"id": "root"}, "actor")},
|
||||||
|
)
|
||||||
|
assert '<a href="/data">data</a></h2>' in auth_response.text
|
||||||
|
assert '<a href="/fixtures">fixtures</a> 🔒</h2>' in auth_response.text
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue