Skip to content

Commit 98466e2

Browse files
authored
Add delete:revision, delete:node scopes (#1217)
* Add delete:metadata, delete:data scopes * Add delete scopes to authn db * Add test for access control on node deletion * Add delete scopes to example config Also add missing "register" scope to example config. Finally, define a datbaase uri in the example config, since `get_database_engine` currently interprets a missing uri as being in single user mode. * Update changelog * Add delete scopes to docs * Add authn database migration for new delete scopes * Add test for scopes on metadata revision deletion * Fix linter error caused by alembic * Rename deletion scopes for clarity
1 parent eafdfe9 commit 98466e2

File tree

9 files changed

+112
-5
lines changed

9 files changed

+112
-5
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Write the date in place of the "Unreleased" in the case a new version is release
99

1010
- Optional `persist` query parameter to PUT and PATCH /array/... routes, and
1111
the corresponding DaskArrayClient methods: `write`, `write_block`, `patch`.
12+
- Added new delete:node and delete:revision scopes
1213

1314
### Changed
1415

@@ -17,6 +18,13 @@ Write the date in place of the "Unreleased" in the case a new version is release
1718
manage the deployment and associated certificates.) **The demo remains
1819
world-public, with no login required.** This change affects some
1920
documentation and one test.
21+
- Deletion of nodes or metadata revisions now requires deletion scopes,
22+
rather than writing scopes.
23+
24+
## Fixed
25+
26+
- Fixed a couple of bugs in the example config, to restore it to working order
27+
2028

2129
## v0.2.0 (2025-10-29)
2230

docs/source/reference/scopes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ with restricted scopes.
1111
* `create` --- Create a new node.
1212
* `write:metadata` --- Write metadata.
1313
* `write:data` --- Write (array, table) data.
14+
* `delete:revision` --- Delete metadata revisions
15+
* `delete:node` --- Delete a node
1416
* `apikeys` --- Manage API keys for the currently-authenticated user or service.
1517
* `metrics` --- Access Prometheus metrics.
1618
* `admin:apikeys` --- Manage API keys on behalf of any user or service.

example_configs/access_tags/tag_definitions.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ roles:
22
facility_user:
33
scopes: ["read:data", "read:metadata"]
44
facility_admin:
5-
scopes: ["read:data", "read:metadata", "write:data", "write:metadata", "create", "register"]
5+
scopes: ["read:data", "read:metadata",
6+
"write:data", "write:metadata",
7+
"delete:node", "delete:revision",
8+
"create", "register"]
69
tags:
710
data_A:
811
groups:

example_configs/toy_authentication.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ authentication:
1212
tiled_admins:
1313
- provider: toy
1414
id: admin
15+
database:
16+
uri: "sqlite:///file:authn_mem?mode=memory&cache=shared&uri=true"
17+
init_if_not_exists: true
1518
access_control:
1619
access_policy: "tiled.access_control.access_policies:TagBasedAccessPolicy"
1720
args:
@@ -21,7 +24,10 @@ access_control:
2124
- "read:data"
2225
- "write:metadata"
2326
- "write:data"
27+
- "delete:revision"
28+
- "delete:node"
2429
- "create"
30+
- "register"
2531
tags_db:
2632
uri: "file:example_configs/access_tags/compiled_tags.sqlite"
2733
access_tags_parser: "tiled.access_control.access_tags:AccessTagsParser"

tiled/_tests/test_access_control.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
"read:metadata",
6767
"write:data",
6868
"write:metadata",
69+
"delete:node",
70+
"delete:revision",
6971
"create",
7072
"register",
7173
]
@@ -651,6 +653,23 @@ def test_writing_access_control(access_control_test_context_factory):
651653
sue_client[top].write_array(arr, key="data_X", access_tags=["chemists_tag"])
652654

653655

656+
def test_deletion_access_control(access_control_test_context_factory):
657+
"""
658+
Test that deletion access control is working.
659+
Only tests that the deletion request does not fail.
660+
Does not test that data is actually deleted.
661+
"""
662+
663+
alice_client = access_control_test_context_factory("alice", "alice")
664+
chris_client = access_control_test_context_factory("chris", "chris")
665+
666+
top = "foo"
667+
alice_client[top].write_array(arr, key="data_H", access_tags=["alice_tag"])
668+
with fail_with_status_code(HTTP_403_FORBIDDEN):
669+
chris_client[top]["data_H"].delete(external_only=False)
670+
alice_client[top]["data_H"].delete(external_only=False)
671+
672+
654673
def test_user_owned_node_access_control(access_control_test_context_factory):
655674
"""
656675
Test that user-owned nodes (i.e. nodes created without access tags applied)
@@ -756,6 +775,8 @@ def test_update_node_access_control(access_control_test_context_factory):
756775
This tests the following:
757776
- Update metadata while having write access
758777
- Prevent updating metadata without having write access
778+
- Prevent deleting a metadata revision without having deletion access
779+
- Delete a metadata revision while having deletion access
759780
- Successfully add an access tag and remove an access tag
760781
- Prevent adding or removing an access tag without having write access
761782
- Prevent adding or removing access tags which the user does not own
@@ -783,6 +804,13 @@ def test_update_node_access_control(access_control_test_context_factory):
783804
)
784805
assert "Au" not in chris_client[top][data].metadata["materials"]
785806

807+
# fails to delete a metadata revision
808+
with fail_with_status_code(HTTP_403_FORBIDDEN):
809+
chris_client[top][data].metadata_revisions.delete_revision(1)
810+
811+
# succeeds to delete a metadata revision
812+
alice_client[top][data].metadata_revisions.delete_revision(1)
813+
786814
# succeeds to add a new access tag and remove the old access tag
787815
alice_client[top][data].replace_metadata(access_tags=["biologists_tag"])
788816
access_tags = alice_client[top][data].access_blob["tags"]

tiled/access_control/scopes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"read:data": {"description": "Read data."},
44
"write:metadata": {"description": "Write metadata."},
55
"write:data": {"description": "Write data."},
6+
"delete:revision": {"description": "Delete metadata revisions."},
7+
"delete:node": {"description": "Delete a node."},
68
"create": {"description": "Add a node."},
79
"register": {"description": "Register externally-managed assets."},
810
"metrics": {"description": "Access (Prometheus) metrics."},
@@ -28,6 +30,8 @@
2830
"read:data",
2931
"write:metadata",
3032
"write:data",
33+
"delete:revision",
34+
"delete:node",
3135
"create",
3236
"register",
3337
"metrics",

tiled/authn_database/core.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
# This is list of all valid alembic revisions (from current to oldest).
1515
ALL_REVISIONS = [
16+
"27e069ba3bf5",
1617
"a806cc635ab2",
1718
"0c705a02954c",
1819
"d88e91ea03f9",
@@ -38,6 +39,8 @@ async def create_default_roles(db):
3839
"create",
3940
"write:metadata",
4041
"write:data",
42+
"delete:revision",
43+
"delete:node",
4144
"apikeys",
4245
],
4346
),
@@ -51,6 +54,8 @@ async def create_default_roles(db):
5154
"register",
5255
"write:metadata",
5356
"write:data",
57+
"delete:revision",
58+
"delete:node",
5459
"admin:apikeys",
5560
"read:principals",
5661
"write:principals",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Add deletion scopes to default Roles
2+
3+
Revision ID: 27e069ba3bf5
4+
Revises: a806cc635ab2
5+
Create Date: 2025-11-06 19:53:44.355094
6+
7+
"""
8+
from alembic import op
9+
from sqlalchemy.orm.session import Session
10+
11+
from tiled.authn_database.orm import Role
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "27e069ba3bf5"
15+
down_revision = "a806cc635ab2"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
ROLES = ["admin", "user"]
21+
NEW_SCOPES = ["delete:revision", "delete:node"]
22+
23+
24+
def upgrade():
25+
"""
26+
Add new scopes to Roles.
27+
"""
28+
connection = op.get_bind()
29+
with Session(bind=connection) as db:
30+
for role_name in ROLES:
31+
role = db.query(Role).filter(Role.name == role_name).first()
32+
scopes = role.scopes.copy()
33+
scopes.extend(NEW_SCOPES)
34+
role.scopes = scopes
35+
db.commit()
36+
37+
38+
def downgrade():
39+
"""
40+
Remove new scopes from Roles, if present.
41+
"""
42+
connection = op.get_bind()
43+
with Session(bind=connection) as db:
44+
for role_name in ROLES:
45+
role = db.query(Role).filter(Role.name == role_name).first()
46+
scopes = role.scopes.copy()
47+
for scope in NEW_SCOPES:
48+
if scope in scopes:
49+
scopes.remove(scope)
50+
role.scopes = scopes
51+
db.commit()

tiled/server/router.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,11 +1692,11 @@ async def delete(
16921692
session_state: dict = Depends(get_session_state),
16931693
authn_access_tags: Optional[AccessTags] = Depends(get_current_access_tags),
16941694
authn_scopes: Scopes = Depends(get_current_scopes),
1695-
_=Security(check_scopes, scopes=["write:data", "write:metadata"]),
1695+
_=Security(check_scopes, scopes=["delete:node", "delete:revision"]),
16961696
):
16971697
entry = await get_entry(
16981698
path,
1699-
["write:data", "write:metadata"],
1699+
["delete:node", "delete:revision"],
17001700
principal,
17011701
authn_access_tags,
17021702
authn_scopes,
@@ -2252,11 +2252,11 @@ async def delete_revision(
22522252
session_state: dict = Depends(get_session_state),
22532253
authn_access_tags: Optional[AccessTags] = Depends(get_current_access_tags),
22542254
authn_scopes: Scopes = Depends(get_current_scopes),
2255-
_=Security(check_scopes, scopes=["write:metadata"]),
2255+
_=Security(check_scopes, scopes=["delete:revision"]),
22562256
):
22572257
entry = await get_entry(
22582258
path,
2259-
["write:metadata"],
2259+
["delete:revision"],
22602260
principal,
22612261
authn_access_tags,
22622262
authn_scopes,

0 commit comments

Comments
 (0)