Skip to content

Commit 8c5e6fa

Browse files
committed
For table and query permissions blocks, a boolean value (true or false) will immediately return that value, overriding any other permission checks. Fixes simonw#2402
1 parent dc28805 commit 8c5e6fa

File tree

3 files changed

+202
-27
lines changed

3 files changed

+202
-27
lines changed

datasette/default_permissions.py

+78-27
Original file line numberDiff line numberDiff line change
@@ -173,56 +173,107 @@ async def inner():
173173

174174

175175
async def _resolve_config_permissions_blocks(datasette, actor, action, resource):
176-
# Check custom permissions: blocks
176+
"""
177+
Resolve custom permissions blocks defined in the Datasette configuration.
178+
179+
This function checks for permission blocks at different levels of the configuration:
180+
root, database, table, and query. It returns the result of the first matching
181+
permission block found, or None if no matching block is found.
182+
183+
Args:
184+
datasette (Datasette): The Datasette instance.
185+
actor (dict): The actor (user) requesting the action.
186+
action (str): The action being requested (e.g., "view-table", "execute-sql").
187+
resource (str or tuple): The resource the action is being performed on.
188+
Can be a string (database name) or a tuple (database name, table/query name).
189+
190+
Returns:
191+
bool or None:
192+
- True if the action is explicitly allowed
193+
- False if the action is explicitly denied
194+
- None if no matching permission block is found
195+
196+
Note:
197+
This function checks permission blocks in the following order:
198+
1. Root-level block for the action
199+
2. Database-specific block for the action
200+
3. Table-specific block for the action (if applicable)
201+
4. Query-specific block for the action (if applicable)
202+
203+
For table and query blocks, a boolean value (True or False) will immediately
204+
return that value, overriding any other permission checks.
205+
"""
177206
config = datasette.config or {}
178207
root_block = (config.get("permissions", None) or {}).get(action)
179208
if root_block:
180209
root_result = actor_matches_allow(actor, root_block)
181210
if root_result is not None:
182211
return root_result
183-
# Now try database-specific blocks
212+
184213
if not resource:
185214
return None
215+
216+
table_or_query = None
186217
if isinstance(resource, str):
187218
database = resource
219+
elif isinstance(resource, tuple):
220+
database, table_or_query = resource
188221
else:
189-
database = resource[0]
222+
return None
223+
190224
database_block = (
191225
(config.get("databases", {}).get(database, {}).get("permissions", None)) or {}
192226
).get(action)
227+
228+
table_block = None
229+
query_block = None
230+
if table_or_query:
231+
table_block = (
232+
(
233+
config.get("databases", {})
234+
.get(database, {})
235+
.get("tables", {})
236+
.get(table_or_query, {})
237+
.get("permissions", None)
238+
)
239+
or {}
240+
).get(action)
241+
242+
query_block = (
243+
(
244+
config.get("databases", {})
245+
.get(database, {})
246+
.get("queries", {})
247+
.get(table_or_query, {})
248+
.get("permissions", None)
249+
)
250+
or {}
251+
).get(action)
252+
253+
# For table and query blocks, a boolean value (True or False) will immediately
254+
# return that value, overriding any other permission checks.
255+
#
256+
# For example, `insert-row: true` permission at the table/query level should
257+
# over-ride `insert-row` permission check at the database level.
258+
# Github Issue #2402
259+
if isinstance(table_block, bool):
260+
return table_block
261+
elif isinstance(query_block, bool):
262+
return query_block
263+
264+
# First try database-specific permissions blocks
193265
if database_block:
194266
database_result = actor_matches_allow(actor, database_block)
195267
if database_result is not None:
196268
return database_result
197-
# Finally try table/query specific blocks
198-
if not isinstance(resource, tuple):
199-
return None
200-
database, table_or_query = resource
201-
table_block = (
202-
(
203-
config.get("databases", {})
204-
.get(database, {})
205-
.get("tables", {})
206-
.get(table_or_query, {})
207-
.get("permissions", None)
208-
)
209-
or {}
210-
).get(action)
269+
270+
# Then try table/query specific permissions blocks
211271
if table_block:
212272
table_result = actor_matches_allow(actor, table_block)
213273
if table_result is not None:
214274
return table_result
275+
215276
# Finally the canned queries
216-
query_block = (
217-
(
218-
config.get("databases", {})
219-
.get(database, {})
220-
.get("queries", {})
221-
.get(table_or_query, {})
222-
.get("permissions", None)
223-
)
224-
or {}
225-
).get(action)
226277
if query_block:
227278
query_result = actor_matches_allow(actor, query_block)
228279
if query_result is not None:

docs/authentication.rst

+56
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,62 @@ And for ``insert-row`` against the ``reports`` table in that ``docs`` database:
880880
}
881881
.. [[[end]]]
882882
883+
For table and query-level permissions blocks, a boolean value (``true`` or ``false``) will immediately return that value, overriding any other permission checks. Anyone can insert a row into ``my-table``:
884+
885+
.. [[[cog
886+
config_example(cog, """
887+
databases:
888+
my-db:
889+
permissions:
890+
insert-row:
891+
id: root
892+
tables:
893+
my-table:
894+
permissions:
895+
insert-row: true
896+
""")
897+
.. ]]]
898+
899+
.. tab:: datasette.yaml
900+
901+
.. code-block:: yaml
902+
903+
904+
databases:
905+
my-db:
906+
permissions:
907+
insert-row:
908+
id: root
909+
tables:
910+
my-table:
911+
permissions:
912+
insert-row: true
913+
914+
915+
.. tab:: datasette.json
916+
917+
.. code-block:: json
918+
919+
{
920+
"databases": {
921+
"my-db": {
922+
"permissions": {
923+
"insert-row": {
924+
"id": "root"
925+
}
926+
},
927+
"tables": {
928+
"my-table": {
929+
"permissions": {
930+
"insert-row": true
931+
}
932+
}
933+
}
934+
}
935+
}
936+
}
937+
.. [[[end]]]
938+
883939
The :ref:`permissions debug tool <PermissionsDebugView>` can be useful for helping test permissions that you have configured in this way.
884940

885941
.. _CreateTokenView:

tests/test_permissions.py

+68
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,74 @@ async def test_actor_restricted_permissions(
810810
resource=("perms_ds_one", "t1"),
811811
expected_result=True,
812812
),
813+
# insert-row: true permission at the table level should over-ride insert-row at the database level
814+
# With no actor => True
815+
# Github Issue #2402
816+
PermConfigTestCase(
817+
config={
818+
"databases": {
819+
"perms_ds_one": {
820+
"permissions": {"insert-row": {"id": "user"}},
821+
"tables": {"t1": {"permissions": {"insert-row": True}}},
822+
}
823+
}
824+
},
825+
actor=None,
826+
action="insert-row",
827+
resource=("perms_ds_one", "t1"),
828+
expected_result=True,
829+
),
830+
# insert-row: true permission at the table level should over-ride insert-row at the database level
831+
# With different actor then set in the database-level permissions => True
832+
# Github Issue #2402
833+
PermConfigTestCase(
834+
config={
835+
"databases": {
836+
"perms_ds_one": {
837+
"permissions": {"insert-row": {"id": "user"}},
838+
"tables": {"t1": {"permissions": {"insert-row": True}}},
839+
}
840+
}
841+
},
842+
actor={"id": "user2"},
843+
action="insert-row",
844+
resource=("perms_ds_one", "t1"),
845+
expected_result=True,
846+
),
847+
# insert-row: false permission at the table level should over-ride insert-row at the database level #2402
848+
# With actor set in the database-level permissions => False
849+
# Github Issue #2402
850+
PermConfigTestCase(
851+
config={
852+
"databases": {
853+
"perms_ds_one": {
854+
"permissions": {"insert-row": {"id": "user"}},
855+
"tables": {"t1": {"permissions": {"insert-row": False}}},
856+
}
857+
}
858+
},
859+
actor={"id": "user"},
860+
action="insert-row",
861+
resource=("perms_ds_one", "t1"),
862+
expected_result=False,
863+
),
864+
# insert-row: false permission at the table level should over-ride insert-row at the database level #2402
865+
# With no actor => False
866+
# Github Issue #2402
867+
PermConfigTestCase(
868+
config={
869+
"databases": {
870+
"perms_ds_one": {
871+
"permissions": {"insert-row": {"id": "user"}},
872+
"tables": {"t1": {"permissions": {"insert-row": False}}},
873+
}
874+
}
875+
},
876+
actor=None,
877+
action="insert-row",
878+
resource=("perms_ds_one", "t1"),
879+
expected_result=False,
880+
),
813881
# view-query on canned query, wrong actor
814882
PermConfigTestCase(
815883
config={

0 commit comments

Comments
 (0)