mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Implemented view-table, refs #811
This commit is contained in:
parent
b26292a458
commit
9397d71834
4 changed files with 108 additions and 41 deletions
|
|
@ -17,6 +17,14 @@ def permission_allowed(datasette, actor, action, resource_type, resource_identif
|
||||||
if database_allow is None:
|
if database_allow is None:
|
||||||
return True
|
return True
|
||||||
return actor_matches_allow(actor, database_allow)
|
return actor_matches_allow(actor, database_allow)
|
||||||
|
elif action == "view-table":
|
||||||
|
assert resource_type == "table"
|
||||||
|
database, table = resource_identifier
|
||||||
|
tables = datasette.metadata("tables", database=database) or {}
|
||||||
|
table_allow = (tables.get(table) or {}).get("allow")
|
||||||
|
if table_allow is None:
|
||||||
|
return True
|
||||||
|
return actor_matches_allow(actor, table_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"
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
{% for table in tables %}
|
{% for table in tables %}
|
||||||
{% if show_hidden or not table.hidden %}
|
{% if show_hidden or not table.hidden %}
|
||||||
<div class="db-table">
|
<div class="db-table">
|
||||||
<h2><a href="{{ database_url(database) }}/{{ table.name|quote_plus }}">{{ table.name }}</a>{% if table.hidden %}<em> (hidden)</em>{% endif %}</h2>
|
<h2><a href="{{ database_url(database) }}/{{ table.name|quote_plus }}">{{ table.name }}</a>{% if table.private %} 🔒{% endif %}{% if table.hidden %}<em> (hidden)</em>{% endif %}</h2>
|
||||||
<p><em>{% for column in table.columns[:9] %}{{ column }}{% if not loop.last %}, {% endif %}{% endfor %}{% if table.columns|length > 9 %}...{% endif %}</em></p>
|
<p><em>{% for column in table.columns[:9] %}{{ column }}{% if not loop.last %}, {% endif %}{% endfor %}{% if table.columns|length > 9 %}...{% endif %}</em></p>
|
||||||
<p>{% if table.count is none %}Many rows{% else %}{{ "{:,}".format(table.count) }} row{% if table.count == 1 %}{% else %}s{% endif %}{% endif %}</p>
|
<p>{% if table.count is none %}Many rows{% else %}{{ "{:,}".format(table.count) }} row{% if table.count == 1 %}{% else %}s{% endif %}{% endif %}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,21 @@ class DatabaseView(DataView):
|
||||||
|
|
||||||
tables = []
|
tables = []
|
||||||
for table in table_counts:
|
for table in table_counts:
|
||||||
|
allowed = await self.ds.permission_allowed(
|
||||||
|
request.scope.get("actor"),
|
||||||
|
"view-table",
|
||||||
|
resource_type="table",
|
||||||
|
resource_identifier=(database, table),
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
if not allowed:
|
||||||
|
continue
|
||||||
|
private = not await self.ds.permission_allowed(
|
||||||
|
None,
|
||||||
|
"view-table",
|
||||||
|
resource_type="table",
|
||||||
|
resource_identifier=(database, table),
|
||||||
|
)
|
||||||
table_columns = await db.table_columns(table)
|
table_columns = await db.table_columns(table)
|
||||||
tables.append(
|
tables.append(
|
||||||
{
|
{
|
||||||
|
|
@ -52,6 +67,7 @@ class DatabaseView(DataView):
|
||||||
"hidden": table in hidden_table_names,
|
"hidden": table in hidden_table_names,
|
||||||
"fts_table": await db.fts_table(table),
|
"fts_table": await db.fts_table(table),
|
||||||
"foreign_keys": all_foreign_keys[table],
|
"foreign_keys": all_foreign_keys[table],
|
||||||
|
"private": private,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,46 +2,6 @@ from .fixtures import make_app_client
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"allow,expected_anon,expected_auth",
|
|
||||||
[(None, 200, 200), ({}, 403, 403), ({"id": "root"}, 403, 200),],
|
|
||||||
)
|
|
||||||
def test_view_query(allow, expected_anon, expected_auth):
|
|
||||||
with make_app_client(
|
|
||||||
metadata={
|
|
||||||
"databases": {
|
|
||||||
"fixtures": {"queries": {"q": {"sql": "select 1 + 1", "allow": allow}}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) as client:
|
|
||||||
anon_response = client.get("/fixtures/q")
|
|
||||||
assert expected_anon == anon_response.status
|
|
||||||
auth_response = client.get(
|
|
||||||
"/fixtures/q", cookies={"ds_actor": client.ds.sign({"id": "root"}, "actor")}
|
|
||||||
)
|
|
||||||
assert expected_auth == auth_response.status
|
|
||||||
|
|
||||||
|
|
||||||
def test_query_list_respects_view_query():
|
|
||||||
with make_app_client(
|
|
||||||
metadata={
|
|
||||||
"databases": {
|
|
||||||
"fixtures": {
|
|
||||||
"queries": {"q": {"sql": "select 1 + 1", "allow": {"id": "root"}}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) as client:
|
|
||||||
html_fragment = '<li><a href="/fixtures/q" title="select 1 + 1">q</a> 🔒</li>'
|
|
||||||
anon_response = client.get("/fixtures")
|
|
||||||
assert html_fragment not in anon_response.text
|
|
||||||
assert '"/fixtures/q"' not in anon_response.text
|
|
||||||
auth_response = client.get(
|
|
||||||
"/fixtures", cookies={"ds_actor": client.ds.sign({"id": "root"}, "actor")}
|
|
||||||
)
|
|
||||||
assert html_fragment in auth_response.text
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"allow,expected_anon,expected_auth",
|
"allow,expected_anon,expected_auth",
|
||||||
[(None, 200, 200), ({}, 403, 403), ({"id": "root"}, 403, 200),],
|
[(None, 200, 200), ({}, 403, 403), ({"id": "root"}, 403, 200),],
|
||||||
|
|
@ -96,3 +56,86 @@ def test_database_list_respects_view_database():
|
||||||
)
|
)
|
||||||
assert '<a href="/data">data</a></h2>' in auth_response.text
|
assert '<a href="/data">data</a></h2>' in auth_response.text
|
||||||
assert '<a href="/fixtures">fixtures</a> 🔒</h2>' in auth_response.text
|
assert '<a href="/fixtures">fixtures</a> 🔒</h2>' in auth_response.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"allow,expected_anon,expected_auth",
|
||||||
|
[(None, 200, 200), ({}, 403, 403), ({"id": "root"}, 403, 200),],
|
||||||
|
)
|
||||||
|
def test_view_table(allow, expected_anon, expected_auth):
|
||||||
|
with make_app_client(
|
||||||
|
metadata={
|
||||||
|
"databases": {
|
||||||
|
"fixtures": {
|
||||||
|
"tables": {"compound_three_primary_keys": {"allow": allow}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) as client:
|
||||||
|
anon_response = client.get("/fixtures/compound_three_primary_keys")
|
||||||
|
assert expected_anon == anon_response.status
|
||||||
|
auth_response = client.get(
|
||||||
|
"/fixtures/compound_three_primary_keys",
|
||||||
|
cookies={"ds_actor": client.ds.sign({"id": "root"}, "actor")},
|
||||||
|
)
|
||||||
|
assert expected_auth == auth_response.status
|
||||||
|
|
||||||
|
|
||||||
|
def test_table_list_respects_view_table():
|
||||||
|
with make_app_client(
|
||||||
|
metadata={
|
||||||
|
"databases": {
|
||||||
|
"fixtures": {
|
||||||
|
"tables": {"compound_three_primary_keys": {"allow": {"id": "root"}}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) as client:
|
||||||
|
html_fragment = '<a href="/fixtures/compound_three_primary_keys">compound_three_primary_keys</a> 🔒'
|
||||||
|
anon_response = client.get("/fixtures")
|
||||||
|
assert html_fragment not in anon_response.text
|
||||||
|
assert '"/fixtures/compound_three_primary_keys"' not in anon_response.text
|
||||||
|
auth_response = client.get(
|
||||||
|
"/fixtures", cookies={"ds_actor": client.ds.sign({"id": "root"}, "actor")}
|
||||||
|
)
|
||||||
|
assert html_fragment in auth_response.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"allow,expected_anon,expected_auth",
|
||||||
|
[(None, 200, 200), ({}, 403, 403), ({"id": "root"}, 403, 200),],
|
||||||
|
)
|
||||||
|
def test_view_query(allow, expected_anon, expected_auth):
|
||||||
|
with make_app_client(
|
||||||
|
metadata={
|
||||||
|
"databases": {
|
||||||
|
"fixtures": {"queries": {"q": {"sql": "select 1 + 1", "allow": allow}}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) as client:
|
||||||
|
anon_response = client.get("/fixtures/q")
|
||||||
|
assert expected_anon == anon_response.status
|
||||||
|
auth_response = client.get(
|
||||||
|
"/fixtures/q", cookies={"ds_actor": client.ds.sign({"id": "root"}, "actor")}
|
||||||
|
)
|
||||||
|
assert expected_auth == auth_response.status
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_list_respects_view_query():
|
||||||
|
with make_app_client(
|
||||||
|
metadata={
|
||||||
|
"databases": {
|
||||||
|
"fixtures": {
|
||||||
|
"queries": {"q": {"sql": "select 1 + 1", "allow": {"id": "root"}}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) as client:
|
||||||
|
html_fragment = '<li><a href="/fixtures/q" title="select 1 + 1">q</a> 🔒</li>'
|
||||||
|
anon_response = client.get("/fixtures")
|
||||||
|
assert html_fragment not in anon_response.text
|
||||||
|
assert '"/fixtures/q"' not in anon_response.text
|
||||||
|
auth_response = client.get(
|
||||||
|
"/fixtures", cookies={"ds_actor": client.ds.sign({"id": "root"}, "actor")}
|
||||||
|
)
|
||||||
|
assert html_fragment in auth_response.text
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue