html_url,issue_url,id,node_id,user,created_at,updated_at,author_association,body,reactions,issue,performed_via_github_app https://github.com/simonw/datasette/issues/2102#issuecomment-1691842259,https://api.github.com/repos/simonw/datasette/issues/2102,1691842259,IC_kwDOBm6k_c5k13bT,9599,2023-08-24T14:55:54Z,2023-08-24T14:55:54Z,OWNER,"So what's needed to finish this is: - Tests that demonstrate that nothing is revealed that shouldn't be by tokens restricted in this way - Similar tests for other permissions like `create-table` that check that they work (and don't also need `view-instance` etc). - Documentation","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818, https://github.com/simonw/datasette/issues/2102#issuecomment-1691824713,https://api.github.com/repos/simonw/datasette/issues/2102,1691824713,IC_kwDOBm6k_c5k1zJJ,9599,2023-08-24T14:45:49Z,2023-08-24T14:45:49Z,OWNER,"I tested this out against a Datasette Cloud instance. I created a restricted token and tested it like this: ```bash curl -H ""Authorization: Bearer $TOKEN"" \ 'https://$INSTANCE/-/actor.json' | jq ``` ```json { ""actor"": { ""id"": ""245"", ""token"": ""dsatok"", ""token_id"": 2, ""_r"": { ""r"": { ""data"": { ""all_stocks"": [ ""vt"" ] } } } } } ``` It can access the `all_stocks` demo table: ```bash curl -H ""Authorization: Bearer $TOKEN"" \ 'https://$INSTANCE/data/all_stocks.json?_size=1' | jq ``` ```json { ""ok"": true, ""next"": ""1"", ""rows"": [ { ""rowid"": 1, ""Date"": ""2013-01-02"", ""Open"": 79.12, ""High"": 79.29, ""Low"": 77.38, ""Close"": 78.43, ""Volume"": 140124866, ""Name"": ""AAPL"" } ], ""truncated"": false } ``` Accessing the database returns just information about that table, even though other tables exist: ```bash curl -H ""Authorization: Bearer $TOKEN"" \ 'https://$INSTANCE/data.json?_size=1' ``` ```json { ""database"": ""data"", ""private"": true, ""path"": ""/data"", ""size"": 3796992, ""tables"": [ { ""name"": ""all_stocks"", ""columns"": [ ""Date"", ""Open"", ""High"", ""Low"", ""Close"", ""Volume"", ""Name"" ], ""primary_keys"": [], ""count"": 8813, ""hidden"": false, ""fts_table"": null, ""foreign_keys"": { ""incoming"": [], ""outgoing"": [] }, ""private"": true } ], ""hidden_count"": 0, ""views"": [], ""queries"": [], ""allow_execute_sql"": false, ""table_columns"": {} } ``` And hitting the top-level `/.json` thing does the same - it reveals that table but not any of the other tables or databases: ```bash curl -H ""Authorization: Bearer $TOKEN"" \ 'https://$INSTANCE/.json?_size=1' ``` ```json { ""data"": { ""name"": ""data"", ""hash"": null, ""color"": ""8d777f"", ""path"": ""/data"", ""tables_and_views_truncated"": [ { ""name"": ""all_stocks"", ""columns"": [ ""Date"", ""Open"", ""High"", ""Low"", ""Close"", ""Volume"", ""Name"" ], ""primary_keys"": [], ""count"": null, ""hidden"": false, ""fts_table"": null, ""num_relationships_for_sorting"": 0, ""private"": false } ], ""tables_and_views_more"": false, ""tables_count"": 1, ""table_rows_sum"": 0, ""show_table_row_counts"": false, ""hidden_table_rows_sum"": 0, ""hidden_tables_count"": 0, ""views_count"": 0, ""private"": false } } ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818, https://github.com/simonw/datasette/issues/2102#issuecomment-1691758168,https://api.github.com/repos/simonw/datasette/issues/2102,1691758168,IC_kwDOBm6k_c5k1i5Y,9599,2023-08-24T14:09:45Z,2023-08-24T14:09:45Z,OWNER,I'm going to implement this in a branch to make it easier to test out.,"{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818, https://github.com/simonw/datasette/issues/2102#issuecomment-1691045051,https://api.github.com/repos/simonw/datasette/issues/2102,1691045051,IC_kwDOBm6k_c5ky0y7,9599,2023-08-24T05:51:59Z,2023-08-24T05:51:59Z,OWNER,"With that fix in place, this works: ```bash datasette fixtures.db --get '/fixtures/facetable.json' --actor '{ ""_r"": { ""r"": { ""fixtures"": { ""facetable"": [ ""vt"" ] } } }, ""a"": ""user"" }' ``` But this fails, because it's for a table not explicitly listed: ```bash datasette fixtures.db --get '/fixtures/searchable.json' --actor '{ ""_r"": { ""r"": { ""fixtures"": { ""facetable"": [ ""vt"" ] } } }, ""a"": ""user"" }' ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818, https://github.com/simonw/datasette/issues/2102#issuecomment-1691044283,https://api.github.com/repos/simonw/datasette/issues/2102,1691044283,IC_kwDOBm6k_c5ky0m7,9599,2023-08-24T05:51:02Z,2023-08-24T05:51:02Z,OWNER,"Also need to confirm that permissions like `insert-row`, `delete-row`, `create-table` etc don't also need special cases to ensure they get through the `view-instance` etc checks, if those exist for those actions.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818, https://github.com/simonw/datasette/issues/2102#issuecomment-1691043475,https://api.github.com/repos/simonw/datasette/issues/2102,1691043475,IC_kwDOBm6k_c5ky0aT,9599,2023-08-24T05:50:04Z,2023-08-24T05:50:04Z,OWNER,"On first test this seems to work! ```diff diff --git a/datasette/default_permissions.py b/datasette/default_permissions.py index 63a66c3c..9303dac8 100644 --- a/datasette/default_permissions.py +++ b/datasette/default_permissions.py @@ -187,6 +187,30 @@ def permission_allowed_actor_restrictions(datasette, actor, action, resource): return None _r = actor.get(""_r"") + # Special case for view-instance: it's allowed if there are any view-database + # or view-table permissions defined + if action == ""view-instance"": + database_rules = _r.get(""d"") or {} + for rules in database_rules.values(): + if ""vd"" in rules or ""view-database"" in rules: + return None + # Now check resources + resource_rules = _r.get(""r"") or {} + for _database, resources in resource_rules.items(): + for rules in resources.values(): + if ""vt"" in rules or ""view-table"" in rules: + return None + + # Special case for view-database: it's allowed if there are any view-table permissions + # defined within that database + if action == ""view-database"": + database_name = resource + resource_rules = _r.get(""r"") or {} + resources_in_database = resource_rules.get(database_name) or {} + for rules in resources_in_database.values(): + if ""vt"" in rules or ""view-table"" in rules: + return None + # Does this action have an abbreviation? to_check = {action} permission = datasette.permissions.get(action) ``` Needs a LOT of testing to make sure what it's doing is sensible though.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818, https://github.com/simonw/datasette/issues/2102#issuecomment-1691037971,https://api.github.com/repos/simonw/datasette/issues/2102,1691037971,IC_kwDOBm6k_c5kyzET,9599,2023-08-24T05:42:47Z,2023-08-24T05:42:47Z,OWNER,"I applied a fun trick to help test this out: ```diff diff --git a/datasette/cli.py b/datasette/cli.py index 58f89c1c..830f47ef 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -445,6 +445,10 @@ def uninstall(packages, yes): ""--token"", help=""API token to send with --get requests"", ) +@click.option( + ""--actor"", + help=""Actor to use for --get requests"", +) @click.option(""--version-note"", help=""Additional note to show on /-/versions"") @click.option(""--help-settings"", is_flag=True, help=""Show available settings"") @click.option(""--pdb"", is_flag=True, help=""Launch debugger on any errors"") @@ -499,6 +503,7 @@ def serve( root, get, token, + actor, version_note, help_settings, pdb, @@ -611,7 +616,10 @@ def serve( headers = {} if token: headers[""Authorization""] = ""Bearer {}"".format(token) - response = client.get(get, headers=headers) + cookies = {} + if actor: + cookies[""ds_actor""] = client.actor_cookie(json.loads(actor)) + response = client.get(get, headers=headers, cookies=cookies) click.echo(response.text) exit_code = 0 if response.status == 200 else 1 sys.exit(exit_code) ``` This adds a `--actor` option to `datasette ... --get /path` which makes it easy to test an API endpoint using a fake actor with a set of `_r` restrictions. With that in place I can try this, with a token that has view-instance and view-database and view-table: ```bash datasette fixtures.db --get '/fixtures/facetable.json' --actor '{ ""_r"": { ""a"": [ ""vi"" ], ""d"": { ""fixtures"": [ ""vd"" ] }, ""r"": { ""fixtures"": { ""facetable"": [ ""vt"" ] } } }, ""a"": ""user"" }' ``` Or this, with a token that just has view-table but is missing the view-database and view-instance: ```bash datasette fixtures.db --get '/fixtures/facetable.json' --actor '{ ""_r"": { ""r"": { ""fixtures"": { ""facetable"": [ ""vt"" ] } } }, ""a"": ""user"" }' ```","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818, https://github.com/simonw/datasette/issues/2102#issuecomment-1691036559,https://api.github.com/repos/simonw/datasette/issues/2102,1691036559,IC_kwDOBm6k_c5kyyuP,9599,2023-08-24T05:40:53Z,2023-08-24T05:40:53Z,OWNER,"There might be an easier way to solve this. Here's some permission checks that run when hitting `/fixtures/facetable.json`: ``` permission_allowed: action=view-table, resource=('fixtures', 'facetable'), actor={'_r': {'a': ['vi'], 'd': {'fixtures': ['vd']}, 'r': {'fixtures': {'facetable': ['vt']}}}, 'a': 'user'} File ""/datasette/views/table.py"", line 727, in table_view_traced view_data = await table_view_data( File ""/datasette/views/table.py"", line 875, in table_view_data visible, private = await datasette.check_visibility( File ""/datasette/app.py"", line 890, in check_visibility await self.ensure_permissions(actor, permissions) permission_allowed: action=view-database, resource=fixtures, actor={'_r': {'a': ['vi'], 'd': {'fixtures': ['vd']}, 'r': {'fixtures': {'facetable': ['vt']}}}, 'a': 'user'} File ""/datasette/views/table.py"", line 727, in table_view_traced view_data = await table_view_data( File ""/datasette/views/table.py"", line 875, in table_view_data visible, private = await datasette.check_visibility( File ""/datasette/app.py"", line 890, in check_visibility await self.ensure_permissions(actor, permissions) permission_allowed: action=view-instance, resource=, actor={'_r': {'a': ['vi'], 'd': {'fixtures': ['vd']}, 'r': {'fixtures': {'facetable': ['vt']}}}, 'a': 'user'} File ""/datasette/views/table.py"", line 727, in table_view_traced view_data = await table_view_data( File ""/datasette/views/table.py"", line 875, in table_view_data visible, private = await datasette.check_visibility( File ""/datasette/app.py"", line 890, in check_visibility await self.ensure_permissions(actor, permissions) ``` That's with a token that has the view instance, view database and view table permissions required. But... what if the restrictions logic said that if you have view-table you automatically also get view-database and view-instance? Would that actually let people do anything they shouldn't be able to do? I don't think it would even let them see a list of tables that they weren't allowed to visit, so it might be OK. I'll try that and see how it works.","{""total_count"": 0, ""+1"": 0, ""-1"": 0, ""laugh"": 0, ""hooray"": 0, ""confused"": 0, ""heart"": 0, ""rocket"": 0, ""eyes"": 0}",1805076818,