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,