mirror of
https://github.com/simonw/datasette.git
synced 2025-12-10 16:51:24 +01:00
Cascade for restricted token view-table/view-database/view-instance operations (#2154)
Closes #2102 * Permission is now a dataclass, not a namedtuple - refs https://github.com/simonw/datasette/pull/2154/#discussion_r1308087800 * datasette.get_permission() method
This commit is contained in:
parent
a1f3d75a52
commit
50da908213
8 changed files with 427 additions and 50 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import collections
|
||||
from datasette.app import Datasette
|
||||
from datasette.cli import cli
|
||||
from datasette.default_permissions import restrictions_allow_action
|
||||
from .fixtures import app_client, assert_permissions_checked, make_app_client
|
||||
from click.testing import CliRunner
|
||||
from bs4 import BeautifulSoup as Soup
|
||||
|
|
@ -35,6 +36,8 @@ async def perms_ds():
|
|||
one = ds.add_memory_database("perms_ds_one")
|
||||
two = ds.add_memory_database("perms_ds_two")
|
||||
await one.execute_write("create table if not exists t1 (id integer primary key)")
|
||||
await one.execute_write("insert or ignore into t1 (id) values (1)")
|
||||
await one.execute_write("create view if not exists v1 as select * from t1")
|
||||
await one.execute_write("create table if not exists t2 (id integer primary key)")
|
||||
await two.execute_write("create table if not exists t1 (id integer primary key)")
|
||||
return ds
|
||||
|
|
@ -585,7 +588,6 @@ DEF = "USE_DEFAULT"
|
|||
({"id": "t", "_r": {"a": ["vd"]}}, "view-database", "one", None, DEF),
|
||||
({"id": "t", "_r": {"a": ["vt"]}}, "view-table", "one", "t1", DEF),
|
||||
# But not if it's the wrong permission
|
||||
({"id": "t", "_r": {"a": ["vd"]}}, "view-instance", None, None, False),
|
||||
({"id": "t", "_r": {"a": ["vi"]}}, "view-database", "one", None, False),
|
||||
({"id": "t", "_r": {"a": ["vd"]}}, "view-table", "one", "t1", False),
|
||||
# Works at the "d" for database level:
|
||||
|
|
@ -629,11 +631,14 @@ DEF = "USE_DEFAULT"
|
|||
"t1",
|
||||
DEF,
|
||||
),
|
||||
# view-instance is granted if you have view-database
|
||||
({"id": "t", "_r": {"a": ["vd"]}}, "view-instance", None, None, DEF),
|
||||
),
|
||||
)
|
||||
async def test_actor_restricted_permissions(
|
||||
perms_ds, actor, permission, resource_1, resource_2, expected_result
|
||||
):
|
||||
perms_ds.pdb = True
|
||||
cookies = {"ds_actor": perms_ds.sign({"a": {"id": "root"}}, "actor")}
|
||||
csrftoken = (await perms_ds.client.get("/-/permissions", cookies=cookies)).cookies[
|
||||
"ds_csrftoken"
|
||||
|
|
@ -1018,3 +1023,190 @@ async def test_api_explorer_visibility(
|
|||
assert response.status_code == 403
|
||||
finally:
|
||||
perms_ds._metadata_local = prev_metadata
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_view_table_token_can_access_table(perms_ds):
|
||||
actor = {
|
||||
"id": "restricted-token",
|
||||
"token": "dstok",
|
||||
# Restricted to just view-table on perms_ds_two/t1
|
||||
"_r": {"r": {"perms_ds_two": {"t1": ["vt"]}}},
|
||||
}
|
||||
cookies = {"ds_actor": perms_ds.client.actor_cookie(actor)}
|
||||
response = await perms_ds.client.get("/perms_ds_two/t1.json", cookies=cookies)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"restrictions,verb,path,body,expected_status",
|
||||
(
|
||||
# No restrictions
|
||||
(None, "get", "/.json", None, 200),
|
||||
(None, "get", "/perms_ds_one.json", None, 200),
|
||||
(None, "get", "/perms_ds_one/t1.json", None, 200),
|
||||
(None, "get", "/perms_ds_one/t1/1.json", None, 200),
|
||||
(None, "get", "/perms_ds_one/v1.json", None, 200),
|
||||
# Restricted to just view-instance
|
||||
({"a": ["vi"]}, "get", "/.json", None, 200),
|
||||
({"a": ["vi"]}, "get", "/perms_ds_one.json", None, 403),
|
||||
({"a": ["vi"]}, "get", "/perms_ds_one/t1.json", None, 403),
|
||||
({"a": ["vi"]}, "get", "/perms_ds_one/t1/1.json", None, 403),
|
||||
({"a": ["vi"]}, "get", "/perms_ds_one/v1.json", None, 403),
|
||||
# Restricted to just view-database
|
||||
({"a": ["vd"]}, "get", "/.json", None, 200), # Can see instance too
|
||||
({"a": ["vd"]}, "get", "/perms_ds_one.json", None, 200),
|
||||
({"a": ["vd"]}, "get", "/perms_ds_one/t1.json", None, 403),
|
||||
({"a": ["vd"]}, "get", "/perms_ds_one/t1/1.json", None, 403),
|
||||
({"a": ["vd"]}, "get", "/perms_ds_one/v1.json", None, 403),
|
||||
# Restricted to just view-table for specific database
|
||||
(
|
||||
{"d": {"perms_ds_one": ["vt"]}},
|
||||
"get",
|
||||
"/.json",
|
||||
None,
|
||||
200,
|
||||
), # Can see instance
|
||||
(
|
||||
{"d": {"perms_ds_one": ["vt"]}},
|
||||
"get",
|
||||
"/perms_ds_one.json",
|
||||
None,
|
||||
200,
|
||||
), # and this database
|
||||
(
|
||||
{"d": {"perms_ds_one": ["vt"]}},
|
||||
"get",
|
||||
"/perms_ds_two.json",
|
||||
None,
|
||||
403,
|
||||
), # But not this one
|
||||
(
|
||||
# Can see the table
|
||||
{"d": {"perms_ds_one": ["vt"]}},
|
||||
"get",
|
||||
"/perms_ds_one/t1.json",
|
||||
None,
|
||||
200,
|
||||
),
|
||||
(
|
||||
# And the view
|
||||
{"d": {"perms_ds_one": ["vt"]}},
|
||||
"get",
|
||||
"/perms_ds_one/v1.json",
|
||||
None,
|
||||
200,
|
||||
),
|
||||
# view-table access to a specific table
|
||||
(
|
||||
{"r": {"perms_ds_one": {"t1": ["vt"]}}},
|
||||
"get",
|
||||
"/.json",
|
||||
None,
|
||||
200,
|
||||
),
|
||||
(
|
||||
{"r": {"perms_ds_one": {"t1": ["vt"]}}},
|
||||
"get",
|
||||
"/perms_ds_one.json",
|
||||
None,
|
||||
200,
|
||||
),
|
||||
(
|
||||
{"r": {"perms_ds_one": {"t1": ["vt"]}}},
|
||||
"get",
|
||||
"/perms_ds_one/t1.json",
|
||||
None,
|
||||
200,
|
||||
),
|
||||
# But cannot see the other table
|
||||
(
|
||||
{"r": {"perms_ds_one": {"t1": ["vt"]}}},
|
||||
"get",
|
||||
"/perms_ds_one/t2.json",
|
||||
None,
|
||||
403,
|
||||
),
|
||||
# Or the view
|
||||
(
|
||||
{"r": {"perms_ds_one": {"t1": ["vt"]}}},
|
||||
"get",
|
||||
"/perms_ds_one/v1.json",
|
||||
None,
|
||||
403,
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_actor_restrictions(
|
||||
perms_ds, restrictions, verb, path, body, expected_status
|
||||
):
|
||||
actor = {"id": "user"}
|
||||
if restrictions:
|
||||
actor["_r"] = restrictions
|
||||
method = getattr(perms_ds.client, verb)
|
||||
kwargs = {"cookies": {"ds_actor": perms_ds.client.actor_cookie(actor)}}
|
||||
if body:
|
||||
kwargs["json"] = body
|
||||
perms_ds._permission_checks.clear()
|
||||
response = await method(path, **kwargs)
|
||||
assert response.status_code == expected_status, json.dumps(
|
||||
{
|
||||
"verb": verb,
|
||||
"path": path,
|
||||
"body": body,
|
||||
"restrictions": restrictions,
|
||||
"expected_status": expected_status,
|
||||
"response_status": response.status_code,
|
||||
"checks": [
|
||||
{
|
||||
"action": check["action"],
|
||||
"resource": check["resource"],
|
||||
"result": check["result"],
|
||||
}
|
||||
for check in perms_ds._permission_checks
|
||||
],
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"restrictions,action,resource,expected",
|
||||
(
|
||||
({"a": ["view-instance"]}, "view-instance", None, True),
|
||||
# view-table and view-database implies view-instance
|
||||
({"a": ["view-table"]}, "view-instance", None, True),
|
||||
({"a": ["view-database"]}, "view-instance", None, True),
|
||||
# update-row does not imply view-instance
|
||||
({"a": ["update-row"]}, "view-instance", None, False),
|
||||
# view-table on a resource implies view-instance
|
||||
({"r": {"db1": {"t1": ["view-table"]}}}, "view-instance", None, True),
|
||||
# update-row on a resource does not imply view-instance
|
||||
({"r": {"db1": {"t1": ["update-row"]}}}, "view-instance", None, False),
|
||||
# view-database on a resource implies view-instance
|
||||
({"d": {"db1": ["view-database"]}}, "view-instance", None, True),
|
||||
# Having view-table on "a" allows access to any specific table
|
||||
({"a": ["view-table"]}, "view-table", ("dbname", "tablename"), True),
|
||||
# Ditto for on the database
|
||||
(
|
||||
{"d": {"dbname": ["view-table"]}},
|
||||
"view-table",
|
||||
("dbname", "tablename"),
|
||||
True,
|
||||
),
|
||||
# But not if it's allowed on a different database
|
||||
(
|
||||
{"d": {"dbname": ["view-table"]}},
|
||||
"view-table",
|
||||
("dbname2", "tablename"),
|
||||
False,
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_restrictions_allow_action(restrictions, action, resource, expected):
|
||||
ds = Datasette()
|
||||
await ds.invoke_startup()
|
||||
actual = restrictions_allow_action(ds, restrictions, action, resource)
|
||||
assert actual == expected
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue