Skip to content

Commit 236bb14

Browse files
committed
refactor: deny helper, permission tests, and error messages for consistency and duplication
- Added unified deny action handler for reduced duplication - Removed now unused deny action tests from score calibration, score set, and user permission tests. - Updated error messages in various tests to consistently reference the entity type (e.g., "ScoreCalibration", "ScoreSet", "User") in the detail messages. - Adjusted test assertions to ensure they check for the correct error messages when permissions are insufficient or entities are not found. - Renamed tests to clarify expected outcomes, particularly for contributor permissions.
1 parent 7fef9ac commit 236bb14

22 files changed

+354
-680
lines changed

src/mavedb/lib/permissions/collection.py

Lines changed: 17 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from mavedb.lib.logging.context import save_to_logging_context
55
from mavedb.lib.permissions.actions import Action
66
from mavedb.lib.permissions.models import PermissionResponse
7-
from mavedb.lib.permissions.utils import roles_permitted
7+
from mavedb.lib.permissions.utils import deny_action_for_entity, roles_permitted
88
from mavedb.models.collection import Collection
99
from mavedb.models.enums.contribution_role import ContributionRole
1010
from mavedb.models.enums.user_role import UserRole
@@ -20,8 +20,8 @@ def has_permission(user_data: Optional[UserData], entity: Collection, action: Ac
2020
2121
Args:
2222
user_data: The user's authentication data and roles. None for anonymous users.
23-
action: The action to be performed (READ, UPDATE, DELETE, ADD_EXPERIMENT_SET).
2423
entity: The Collection entity to check permissions for.
24+
action: The action to be performed (READ, UPDATE, DELETE, ADD_EXPERIMENT, ADD_SCORE_SET, ADD_ROLE, ADD_BADGE).
2525
2626
Returns:
2727
PermissionResponse: Contains permission result, HTTP status code, and message.
@@ -107,6 +107,7 @@ def _handle_read_action(
107107
user_data: The user's authentication data.
108108
entity: The Collection entity being accessed.
109109
private: Whether the Collection is private.
110+
official_collection: Whether the Collection is an official collection.
110111
user_is_owner: Whether the user owns the Collection.
111112
collection_roles: The user's roles in this Collection (admin/editor/viewer).
112113
active_roles: List of the user's active roles.
@@ -128,7 +129,7 @@ def _handle_read_action(
128129
if roles_permitted(active_roles, [UserRole.admin]):
129130
return PermissionResponse(True)
130131

131-
return _deny_action_for_collection(entity, private, user_data, bool(collection_roles) or user_is_owner)
132+
return deny_action_for_entity(entity, private, user_data, bool(collection_roles) or user_is_owner)
132133

133134

134135
def _handle_update_action(
@@ -149,6 +150,7 @@ def _handle_update_action(
149150
user_data: The user's authentication data.
150151
entity: The Collection entity being updated.
151152
private: Whether the Collection is private.
153+
official_collection: Whether the Collection is an official collection.
152154
user_is_owner: Whether the user owns the Collection.
153155
collection_roles: The user's roles in this Collection (admin/editor/viewer).
154156
active_roles: List of the user's active roles.
@@ -167,7 +169,7 @@ def _handle_update_action(
167169
if roles_permitted(active_roles, [UserRole.admin]):
168170
return PermissionResponse(True)
169171

170-
return _deny_action_for_collection(entity, private, user_data, bool(collection_roles) or user_is_owner)
172+
return deny_action_for_entity(entity, private, user_data, bool(collection_roles) or user_is_owner)
171173

172174

173175
def _handle_delete_action(
@@ -189,6 +191,7 @@ def _handle_delete_action(
189191
user_data: The user's authentication data.
190192
entity: The Collection entity being deleted.
191193
private: Whether the Collection is private.
194+
official_collection: Whether the Collection is official.
192195
user_is_owner: Whether the user owns the Collection.
193196
collection_roles: The user's roles in this Collection (admin/editor/viewer).
194197
active_roles: List of the user's active roles.
@@ -202,16 +205,12 @@ def _handle_delete_action(
202205
return PermissionResponse(True)
203206
# Other users may only delete non-official collections.
204207
if not official_collection:
205-
# Owners may delete a collection only if it has not been published.
208+
# Owners may delete a collection only if it is still private.
206209
# Collection admins/editors/viewers may not delete collections.
207-
if user_is_owner:
208-
return PermissionResponse(
209-
private,
210-
403,
211-
f"insufficient permissions for URN '{entity.urn}'",
212-
)
210+
if user_is_owner and private:
211+
return PermissionResponse(True)
213212

214-
return _deny_action_for_collection(entity, private, user_data, bool(collection_roles) or user_is_owner)
213+
return deny_action_for_entity(entity, private, user_data, bool(collection_roles) or user_is_owner)
215214

216215

217216
def _handle_publish_action(
@@ -232,6 +231,7 @@ def _handle_publish_action(
232231
user_data: The user's authentication data.
233232
entity: The Collection entity being published.
234233
private: Whether the Collection is private.
234+
official_collection: Whether the Collection is official.
235235
user_is_owner: Whether the user owns the Collection.
236236
collection_roles: The user's roles in this Collection (admin/editor/viewer).
237237
active_roles: List of the user's active roles.
@@ -249,7 +249,7 @@ def _handle_publish_action(
249249
if roles_permitted(active_roles, [UserRole.admin]):
250250
return PermissionResponse(True)
251251

252-
return _deny_action_for_collection(entity, private, user_data, bool(collection_roles) or user_is_owner)
252+
return deny_action_for_entity(entity, private, user_data, bool(collection_roles) or user_is_owner)
253253

254254

255255
def _handle_add_experiment_action(
@@ -290,7 +290,7 @@ def _handle_add_experiment_action(
290290
if roles_permitted(active_roles, [UserRole.admin]):
291291
return PermissionResponse(True)
292292

293-
return _deny_action_for_collection(entity, private, user_data, bool(collection_roles) or user_is_owner)
293+
return deny_action_for_entity(entity, private, user_data, bool(collection_roles) or user_is_owner)
294294

295295

296296
def _handle_add_score_set_action(
@@ -330,7 +330,7 @@ def _handle_add_score_set_action(
330330
if roles_permitted(active_roles, [UserRole.admin]):
331331
return PermissionResponse(True)
332332

333-
return _deny_action_for_collection(entity, private, user_data, bool(collection_roles) or user_is_owner)
333+
return deny_action_for_entity(entity, private, user_data, bool(collection_roles) or user_is_owner)
334334

335335

336336
def _handle_add_role_action(
@@ -369,7 +369,7 @@ def _handle_add_role_action(
369369
if roles_permitted(active_roles, [UserRole.admin]):
370370
return PermissionResponse(True)
371371

372-
return _deny_action_for_collection(entity, private, user_data, bool(collection_roles) or user_is_owner)
372+
return deny_action_for_entity(entity, private, user_data, bool(collection_roles) or user_is_owner)
373373

374374

375375
def _handle_add_badge_action(
@@ -402,40 +402,4 @@ def _handle_add_badge_action(
402402
if roles_permitted(active_roles, [UserRole.admin]):
403403
return PermissionResponse(True)
404404

405-
return _deny_action_for_collection(entity, private, user_data, bool(collection_roles) or user_is_owner)
406-
407-
408-
def _deny_action_for_collection(
409-
entity: Collection,
410-
private: bool,
411-
user_data: Optional[UserData],
412-
user_may_view_private: bool = False,
413-
) -> PermissionResponse:
414-
"""
415-
Generate appropriate denial response for Collection permission checks.
416-
417-
This helper function determines the correct HTTP status code and message
418-
when denying access to a Collection based on its privacy and user authentication.
419-
420-
Args:
421-
entity: The Collection entity being accessed.
422-
private: Whether the Collection is private.
423-
user_data: The user's authentication data (None for anonymous).
424-
user_may_view_private: Whether the user has any role allowing them to view private collections.
425-
426-
Returns:
427-
PermissionResponse: Denial response with appropriate HTTP status and message.
428-
429-
Note:
430-
Returns 404 for private entities to avoid information disclosure,
431-
401 for unauthenticated users, and 403 for insufficient permissions.
432-
"""
433-
# Do not acknowledge the existence of a private collection.
434-
if private and not user_may_view_private:
435-
return PermissionResponse(False, 404, f"collection with URN '{entity.urn}' not found")
436-
# No authenticated user is present.
437-
if user_data is None or user_data.user is None:
438-
return PermissionResponse(False, 401, f"insufficient permissions for URN '{entity.urn}'")
439-
440-
# The authenticated user lacks sufficient permissions.
441-
return PermissionResponse(False, 403, f"insufficient permissions for URN '{entity.urn}'")
405+
return deny_action_for_entity(entity, private, user_data, bool(collection_roles) or user_is_owner)

src/mavedb/lib/permissions/experiment.py

Lines changed: 12 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from mavedb.lib.logging.context import save_to_logging_context
55
from mavedb.lib.permissions.actions import Action
66
from mavedb.lib.permissions.models import PermissionResponse
7-
from mavedb.lib.permissions.utils import roles_permitted
7+
from mavedb.lib.permissions.utils import deny_action_for_entity, roles_permitted
88
from mavedb.models.enums.user_role import UserRole
99
from mavedb.models.experiment import Experiment
1010

@@ -19,8 +19,8 @@ def has_permission(user_data: Optional[UserData], entity: Experiment, action: Ac
1919
2020
Args:
2121
user_data: The user's authentication data and roles. None for anonymous users.
22-
action: The action to be performed (READ, UPDATE, DELETE, ADD_SCORE_SET).
2322
entity: The Experiment entity to check permissions for.
23+
action: The action to be performed (READ, UPDATE, DELETE, ADD_SCORE_SET).
2424
2525
Returns:
2626
PermissionResponse: Contains permission result, HTTP status code, and message.
@@ -108,7 +108,7 @@ def _handle_read_action(
108108
if roles_permitted(active_roles, [UserRole.admin, UserRole.mapper]):
109109
return PermissionResponse(True)
110110

111-
return _deny_action_for_experiment(entity, private, user_data, user_is_contributor or user_is_owner)
111+
return deny_action_for_entity(entity, private, user_data, user_is_contributor or user_is_owner)
112112

113113

114114
def _handle_update_action(
@@ -143,7 +143,7 @@ def _handle_update_action(
143143
if roles_permitted(active_roles, [UserRole.admin]):
144144
return PermissionResponse(True)
145145

146-
return _deny_action_for_experiment(entity, private, user_data, user_is_contributor or user_is_owner)
146+
return deny_action_for_entity(entity, private, user_data, user_is_contributor or user_is_owner)
147147

148148

149149
def _handle_delete_action(
@@ -175,16 +175,11 @@ def _handle_delete_action(
175175
# Admins may delete any experiment.
176176
if roles_permitted(active_roles, [UserRole.admin]):
177177
return PermissionResponse(True)
178-
# Owners may delete an experiment only if it has not been published. Contributors may not delete an experiment.
179-
if user_is_owner:
180-
published = entity.published_date is not None
181-
return PermissionResponse(
182-
not published,
183-
403,
184-
f"insufficient permissions for URN '{entity.urn}'",
185-
)
178+
# Owners may delete an experiment only if it is still private. Contributors may not delete an experiment.
179+
if user_is_owner and private:
180+
return PermissionResponse(True)
186181

187-
return _deny_action_for_experiment(entity, private, user_data, user_is_contributor or user_is_owner)
182+
return deny_action_for_entity(entity, private, user_data, user_is_contributor or user_is_owner)
188183

189184

190185
def _handle_add_score_set_action(
@@ -219,41 +214,8 @@ def _handle_add_score_set_action(
219214
# Users with these specific roles may update the experiment.
220215
if roles_permitted(active_roles, [UserRole.admin]):
221216
return PermissionResponse(True)
217+
# Any authenticated user may add a score set to a non-private experiment.
218+
if not private and user_data is not None:
219+
return PermissionResponse(True)
222220

223-
return _deny_action_for_experiment(entity, private, user_data, user_is_contributor or user_is_owner)
224-
225-
226-
def _deny_action_for_experiment(
227-
entity: Experiment,
228-
private: bool,
229-
user_data: Optional[UserData],
230-
user_may_view_private: bool,
231-
) -> PermissionResponse:
232-
"""
233-
Generate appropriate denial response for Experiment permission checks.
234-
235-
This helper function determines the correct HTTP status code and message
236-
when denying access to an Experiment based on its privacy and user authentication.
237-
238-
Args:
239-
entity: The Experiment entity being accessed.
240-
private: Whether the Experiment is private.
241-
user_data: The user's authentication data (None for anonymous).
242-
user_may_view_private: Whether the user has permission to view private experiments.
243-
244-
Returns:
245-
PermissionResponse: Denial response with appropriate HTTP status and message.
246-
247-
Note:
248-
Returns 404 for private entities to avoid information disclosure,
249-
401 for unauthenticated users, and 403 for insufficient permissions.
250-
"""
251-
# Do not acknowledge the existence of a private experiment.
252-
if private and not user_may_view_private:
253-
return PermissionResponse(False, 404, f"experiment with URN '{entity.urn}' not found")
254-
# No authenticated user is present.
255-
if user_data is None or user_data.user is None:
256-
return PermissionResponse(False, 401, f"insufficient permissions for URN '{entity.urn}'")
257-
258-
# The authenticated user lacks sufficient permissions.
259-
return PermissionResponse(False, 403, f"insufficient permissions for URN '{entity.urn}'")
221+
return deny_action_for_entity(entity, private, user_data, user_is_contributor or user_is_owner)

src/mavedb/lib/permissions/experiment_set.py

Lines changed: 9 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from mavedb.lib.logging.context import save_to_logging_context
55
from mavedb.lib.permissions.actions import Action
66
from mavedb.lib.permissions.models import PermissionResponse
7-
from mavedb.lib.permissions.utils import roles_permitted
7+
from mavedb.lib.permissions.utils import deny_action_for_entity, roles_permitted
88
from mavedb.models.enums.user_role import UserRole
99
from mavedb.models.experiment_set import ExperimentSet
1010

@@ -19,8 +19,8 @@ def has_permission(user_data: Optional[UserData], entity: ExperimentSet, action:
1919
2020
Args:
2121
user_data: The user's authentication data and roles. None for anonymous users.
22-
action: The action to be performed (READ, UPDATE, DELETE, ADD_EXPERIMENT).
2322
entity: The ExperimentSet entity to check permissions for.
23+
action: The action to be performed (READ, UPDATE, DELETE, ADD_EXPERIMENT).
2424
2525
Returns:
2626
PermissionResponse: Contains permission result, HTTP status code, and message.
@@ -108,7 +108,7 @@ def _handle_read_action(
108108
if roles_permitted(active_roles, [UserRole.admin, UserRole.mapper]):
109109
return PermissionResponse(True)
110110

111-
return _deny_action_for_experiment_set(entity, private, user_data, user_is_contributor or user_is_owner)
111+
return deny_action_for_entity(entity, private, user_data, user_is_contributor or user_is_owner)
112112

113113

114114
def _handle_update_action(
@@ -143,7 +143,7 @@ def _handle_update_action(
143143
if roles_permitted(active_roles, [UserRole.admin]):
144144
return PermissionResponse(True)
145145

146-
return _deny_action_for_experiment_set(entity, private, user_data, user_is_contributor or user_is_owner)
146+
return deny_action_for_entity(entity, private, user_data, user_is_contributor or user_is_owner)
147147

148148

149149
def _handle_delete_action(
@@ -175,16 +175,11 @@ def _handle_delete_action(
175175
# Admins may delete any experiment set.
176176
if roles_permitted(active_roles, [UserRole.admin]):
177177
return PermissionResponse(True)
178-
# Owners may delete an experiment set only if it has not been published. Contributors may not delete an experiment set.
179-
if user_is_owner:
180-
published = entity.published_date is not None
181-
return PermissionResponse(
182-
not published,
183-
403,
184-
f"insufficient permissions for URN '{entity.urn}'",
185-
)
178+
# Owners may delete an experiment set only if it is still private. Contributors may not delete an experiment set.
179+
if user_is_owner and private:
180+
return PermissionResponse(True)
186181

187-
return _deny_action_for_experiment_set(entity, private, user_data, user_is_contributor or user_is_owner)
182+
return deny_action_for_entity(entity, private, user_data, user_is_contributor or user_is_owner)
188183

189184

190185
def _handle_add_experiment_action(
@@ -220,40 +215,4 @@ def _handle_add_experiment_action(
220215
if roles_permitted(active_roles, [UserRole.admin]):
221216
return PermissionResponse(True)
222217

223-
return _deny_action_for_experiment_set(entity, private, user_data, user_is_contributor or user_is_owner)
224-
225-
226-
def _deny_action_for_experiment_set(
227-
entity: ExperimentSet,
228-
private: bool,
229-
user_data: Optional[UserData],
230-
user_may_view_private: bool,
231-
) -> PermissionResponse:
232-
"""
233-
Generate appropriate denial response for ExperimentSet permission checks.
234-
235-
This helper function determines the correct HTTP status code and message
236-
when denying access to an ExperimentSet based on its privacy and user authentication.
237-
238-
Args:
239-
entity: The ExperimentSet entity being accessed.
240-
private: Whether the ExperimentSet is private.
241-
user_data: The user's authentication data (None for anonymous).
242-
user_may_view_private: Whether the user has permission to view private experiment sets.
243-
244-
Returns:
245-
PermissionResponse: Denial response with appropriate HTTP status and message.
246-
247-
Note:
248-
Returns 404 for private entities to avoid information disclosure,
249-
401 for unauthenticated users, and 403 for insufficient permissions.
250-
"""
251-
# Do not acknowledge the existence of a private experiment set.
252-
if private and not user_may_view_private:
253-
return PermissionResponse(False, 404, f"experiment set with URN '{entity.urn}' not found")
254-
# No authenticated user is present.
255-
if user_data is None or user_data.user is None:
256-
return PermissionResponse(False, 401, f"insufficient permissions for URN '{entity.urn}'")
257-
258-
# The authenticated user lacks sufficient permissions.
259-
return PermissionResponse(False, 403, f"insufficient permissions for URN '{entity.urn}'")
218+
return deny_action_for_entity(entity, private, user_data, user_is_contributor or user_is_owner)

0 commit comments

Comments
 (0)