Skip to content

Commit d133e30

Browse files
committed
Clarify HTTP status codes for email and role authorization checks
Refactor API routers to use shared response definitions and update prefix - Updated all API routers to use the new ROUTER_BASE_PREFIX for consistent URL structure. - Replaced hardcoded response definitions with shared response mappings for better maintainability. - Consolidated error responses into shared constants for easier updates and consistency across routers. - Ensured that all routers now handle authentication and permission errors using the new shared response structure. Refactor HTTP status codes for improved clarity and consistency across various endpoints Update error handling for multiple sequences: change status code to 400 and provide explicit namespace guidance
1 parent 73de569 commit d133e30

35 files changed

+347
-474
lines changed

src/mavedb/lib/authorization.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from typing import Optional
33

44
from fastapi import Depends, HTTPException
5-
from starlette import status
65

76
from mavedb.lib.authentication import UserData, get_current_user
87
from mavedb.lib.logging.context import logging_context, save_to_logging_context
@@ -21,10 +20,7 @@ async def require_current_user(
2120
) -> UserData:
2221
if user_data is None:
2322
logger.info(msg="Non-authenticated user attempted to access protected route.", extra=logging_context())
24-
raise HTTPException(
25-
status_code=status.HTTP_401_UNAUTHORIZED,
26-
detail="Could not validate credentials",
27-
)
23+
raise HTTPException(status_code=401, detail="Could not validate credentials")
2824

2925
return user_data
3026

@@ -38,8 +34,7 @@ async def require_current_user_with_email(
3834
msg="User attempted to access email protected route without a valid email.", extra=logging_context()
3935
)
4036
raise HTTPException(
41-
status_code=status.HTTP_400_BAD_REQUEST,
42-
detail="There must be an email address associated with your account to use this feature.",
37+
status_code=403, detail="There must be an email address associated with your account to use this feature."
4338
)
4439
return user_data
4540

@@ -54,10 +49,7 @@ async def __call__(self, user_data: UserData = Depends(require_current_user)) ->
5449
logger.info(
5550
msg="User attempted to access role protected route without a required role.", extra=logging_context()
5651
)
57-
raise HTTPException(
58-
status_code=status.HTTP_401_UNAUTHORIZED,
59-
detail="You are not authorized to use this feature",
60-
)
52+
raise HTTPException(status_code=403, detail="You are not authorized to use this feature")
6153

6254
return user_data
6355

@@ -68,9 +60,6 @@ async def require_role(roles: list[UserRole], user_data: UserData = Depends(requ
6860
logger.info(
6961
msg="User attempted to access role protected route without a required role.", extra=logging_context()
7062
)
71-
raise HTTPException(
72-
status_code=status.HTTP_401_UNAUTHORIZED,
73-
detail="You are not authorized to use this feature",
74-
)
63+
raise HTTPException(status_code=403, detail="You are not authorized to use this feature")
7564

7665
return user_data

src/mavedb/lib/taxonomies.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,6 @@ async def search_NCBI_taxonomy(db: Session, search: str) -> Any:
6666
else:
6767
raise HTTPException(status_code=404, detail=f"Taxonomy with search {search_text} not found in NCBI")
6868
else:
69-
raise HTTPException(status_code=404, detail="Please enter valid searching words")
69+
raise HTTPException(status_code=400, detail="Search text is required")
7070

7171
return taxonomy_record

src/mavedb/routers/access_keys.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from fastapi import APIRouter, Depends
99
from fastapi.encoders import jsonable_encoder
1010
from fastapi.exceptions import HTTPException
11+
from sqlalchemy import and_
1112
from sqlalchemy.orm import Session
1213

1314
from mavedb import deps
@@ -17,12 +18,13 @@
1718
from mavedb.lib.logging.context import logging_context, save_to_logging_context
1819
from mavedb.models.access_key import AccessKey
1920
from mavedb.models.enums.user_role import UserRole
21+
from mavedb.routers.shared import ACCESS_CONTROL_ERROR_RESPONSES, PUBLIC_ERROR_RESPONSES, ROUTER_BASE_PREFIX
2022
from mavedb.view_models import access_key
2123

2224
router = APIRouter(
23-
prefix="/api/v1",
25+
prefix=f"{ROUTER_BASE_PREFIX}",
2426
tags=["Access Keys"],
25-
responses={404: {"description": "Not found"}, 500: {"description": "Internal server error"}},
27+
responses={**PUBLIC_ERROR_RESPONSES},
2628
route_class=LoggedRoute,
2729
)
2830

@@ -49,9 +51,7 @@ def generate_key_pair():
4951
"/users/me/access-keys",
5052
status_code=200,
5153
response_model=list[access_key.AccessKey],
52-
responses={
53-
401: {"description": "Not authenticated"},
54-
},
54+
responses={**ACCESS_CONTROL_ERROR_RESPONSES},
5555
summary="List my access keys",
5656
)
5757
def list_my_access_keys(*, user_data: UserData = Depends(require_current_user)) -> Any:
@@ -65,9 +65,7 @@ def list_my_access_keys(*, user_data: UserData = Depends(require_current_user))
6565
"/users/me/access-keys",
6666
status_code=200,
6767
response_model=access_key.NewAccessKey,
68-
responses={
69-
401: {"description": "Not authenticated"},
70-
},
68+
responses={**ACCESS_CONTROL_ERROR_RESPONSES},
7169
summary="Create a new access key for myself",
7270
)
7371
def create_my_access_key(
@@ -94,10 +92,7 @@ def create_my_access_key(
9492
"/users/me/access-keys/{role}",
9593
status_code=200,
9694
response_model=access_key.NewAccessKey,
97-
responses={
98-
401: {"description": "Not authenticated"},
99-
403: {"description": "User lacks necessary permissions"},
100-
},
95+
responses={**ACCESS_CONTROL_ERROR_RESPONSES},
10196
summary="Create a new access key for myself with a specified role",
10297
)
10398
async def create_my_access_key_with_role(
@@ -138,9 +133,8 @@ async def create_my_access_key_with_role(
138133
@router.delete(
139134
"/users/me/access-keys/{key_id}",
140135
status_code=200,
141-
responses={
142-
401: {"description": "Not authenticated"},
143-
},
136+
responses={**ACCESS_CONTROL_ERROR_RESPONSES},
137+
summary="Delete one of my access keys",
144138
)
145139
def delete_my_access_key(
146140
*,
@@ -152,7 +146,9 @@ def delete_my_access_key(
152146
Delete one of the current user's access keys.
153147
"""
154148
item = (
155-
db.query(AccessKey).filter(AccessKey.key_id == key_id and AccessKey.user_id == user_data.user.id).one_or_none()
149+
db.query(AccessKey)
150+
.filter(and_(AccessKey.key_id == key_id, AccessKey.user_id == user_data.user.id))
151+
.one_or_none()
156152
)
157153

158154
if not item:

src/mavedb/routers/api_information.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
from fastapi import APIRouter
44

55
from mavedb import __project__, __version__
6+
from mavedb.routers.shared import PUBLIC_ERROR_RESPONSES, ROUTER_BASE_PREFIX
67
from mavedb.view_models import api_version
78

89
router = APIRouter(
9-
prefix="/api/v1/api",
10+
prefix=f"{ROUTER_BASE_PREFIX}/api",
1011
tags=["API Information"],
11-
responses={404: {"description": "Not found"}, 500: {"description": "Internal server error"}},
12+
responses={**PUBLIC_ERROR_RESPONSES},
1213
)
1314

1415

src/mavedb/routers/collections.py

Lines changed: 25 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,21 @@
2424
from mavedb.models.experiment import Experiment
2525
from mavedb.models.score_set import ScoreSet
2626
from mavedb.models.user import User
27+
from mavedb.routers.shared import (
28+
ACCESS_CONTROL_ERROR_RESPONSES,
29+
BASE_400_RESPONSE,
30+
BASE_409_RESPONSE,
31+
PUBLIC_ERROR_RESPONSES,
32+
ROUTER_BASE_PREFIX,
33+
)
2734
from mavedb.view_models import collection, collection_bundle
2835

2936
logger = logging.getLogger(__name__)
3037

3138
router = APIRouter(
32-
prefix="/api/v1",
39+
prefix=f"{ROUTER_BASE_PREFIX}",
3340
tags=["Collections"],
34-
responses={404: {"description": "Not found"}, 500: {"description": "Internal server error"}},
41+
responses={**PUBLIC_ERROR_RESPONSES},
3542
route_class=LoggedRoute,
3643
)
3744

@@ -41,9 +48,7 @@
4148
status_code=200,
4249
response_model=collection_bundle.CollectionBundle,
4350
response_model_exclude_none=True,
44-
responses={
45-
401: {"description": "Not authenticated"},
46-
},
51+
responses={**ACCESS_CONTROL_ERROR_RESPONSES},
4752
summary="List my collections",
4853
)
4954
def list_my_collections(
@@ -96,10 +101,7 @@ def list_my_collections(
96101
"/collections/{urn}",
97102
status_code=200,
98103
response_model=collection.Collection,
99-
responses={
100-
401: {"description": "Not authenticated"},
101-
403: {"description": "User lacks necessary permissions"},
102-
},
104+
responses={**ACCESS_CONTROL_ERROR_RESPONSES},
103105
response_model_exclude_none=True,
104106
summary="Fetch a collection by URN",
105107
)
@@ -145,11 +147,7 @@ def fetch_collection(
145147
@router.post(
146148
"/collections/",
147149
response_model=collection.Collection,
148-
responses={
149-
400: {"description": "Bad request"},
150-
401: {"description": "Not authenticated"},
151-
403: {"description": "User lacks necessary permissions"},
152-
},
150+
responses={**BASE_400_RESPONSE, **ACCESS_CONTROL_ERROR_RESPONSES},
153151
response_model_exclude_none=True,
154152
summary="Create a collection",
155153
)
@@ -210,7 +208,7 @@ async def create_collection(
210208
save_to_logging_context(format_raised_exception_info_as_dict(e))
211209
logger.error(msg="Multiple users found with the given ORCID iD", extra=logging_context())
212210
raise HTTPException(
213-
status_code=400,
211+
status_code=500,
214212
detail="Multiple MaveDB users found with the given ORCID iD",
215213
)
216214

@@ -233,7 +231,7 @@ async def create_collection(
233231
except MultipleResultsFound as e:
234232
save_to_logging_context(format_raised_exception_info_as_dict(e))
235233
logger.error(msg="Multiple resources found with the given URN", extra=logging_context())
236-
raise HTTPException(status_code=400, detail="Multiple resources found with the given URN")
234+
raise HTTPException(status_code=500, detail="Multiple resources found with the given URN")
237235

238236
item = Collection(
239237
**jsonable_encoder(
@@ -266,11 +264,7 @@ async def create_collection(
266264
@router.patch(
267265
"/collections/{urn}",
268266
response_model=collection.Collection,
269-
responses={
270-
400: {"description": "Bad request"},
271-
401: {"description": "Not authenticated"},
272-
403: {"description": "User lacks necessary permissions"},
273-
},
267+
responses={**BASE_400_RESPONSE, **ACCESS_CONTROL_ERROR_RESPONSES},
274268
response_model_exclude_none=True,
275269
summary="Update a collection",
276270
)
@@ -421,11 +415,7 @@ async def add_score_set_to_collection(
421415
@router.delete(
422416
"/collections/{collection_urn}/score-sets/{score_set_urn}",
423417
response_model=collection.Collection,
424-
responses={
425-
400: {"description": "Bad request"},
426-
401: {"description": "Not authenticated"},
427-
403: {"description": "User lacks necessary permissions"},
428-
},
418+
responses={**ACCESS_CONTROL_ERROR_RESPONSES, **BASE_409_RESPONSE},
429419
summary="Remove a score set from a collection",
430420
)
431421
async def delete_score_set_from_collection(
@@ -463,7 +453,7 @@ async def delete_score_set_from_collection(
463453
extra=logging_context(),
464454
)
465455
raise HTTPException(
466-
status_code=400,
456+
status_code=409,
467457
detail=f"association between score set '{score_set_urn}' and collection '{collection_urn}' not found",
468458
)
469459

@@ -506,10 +496,7 @@ async def delete_score_set_from_collection(
506496
@router.post(
507497
"/collections/{collection_urn}/experiments",
508498
response_model=collection.Collection,
509-
responses={
510-
401: {"description": "Not authenticated"},
511-
403: {"description": "User lacks necessary permissions"},
512-
},
499+
responses={**ACCESS_CONTROL_ERROR_RESPONSES},
513500
summary="Add an experiment to a collection",
514501
)
515502
async def add_experiment_to_collection(
@@ -581,11 +568,7 @@ async def add_experiment_to_collection(
581568
@router.delete(
582569
"/collections/{collection_urn}/experiments/{experiment_urn}",
583570
response_model=collection.Collection,
584-
responses={
585-
400: {"description": "Bad request"},
586-
401: {"description": "Not authenticated"},
587-
403: {"description": "User lacks necessary permissions"},
588-
},
571+
responses={**ACCESS_CONTROL_ERROR_RESPONSES, **BASE_409_RESPONSE},
589572
summary="Remove an experiment from a collection",
590573
)
591574
async def delete_experiment_from_collection(
@@ -623,7 +606,7 @@ async def delete_experiment_from_collection(
623606
extra=logging_context(),
624607
)
625608
raise HTTPException(
626-
status_code=400,
609+
status_code=409,
627610
detail=f"association between experiment '{experiment_urn}' and collection '{collection_urn}' not found",
628611
)
629612

@@ -666,11 +649,7 @@ async def delete_experiment_from_collection(
666649
@router.post(
667650
"/collections/{urn}/{role}s",
668651
response_model=collection.Collection,
669-
responses={
670-
400: {"description": "Bad request"},
671-
401: {"description": "Not authenticated"},
672-
403: {"description": "User lacks necessary permissions"},
673-
},
652+
responses={**ACCESS_CONTROL_ERROR_RESPONSES, **BASE_409_RESPONSE},
674653
summary="Add a user to a collection role",
675654
)
676655
async def add_user_to_collection_role(
@@ -723,7 +702,7 @@ async def add_user_to_collection_role(
723702
extra=logging_context(),
724703
)
725704
raise HTTPException(
726-
status_code=400,
705+
status_code=409,
727706
detail=f"user with ORCID iD '{body.orcid_id}' is already a {role} for collection '{urn}'",
728707
)
729708
# A user can only be in one role per collection, so remove from any other roles
@@ -757,11 +736,7 @@ async def add_user_to_collection_role(
757736
@router.delete(
758737
"/collections/{urn}/{role}s/{orcid_id}",
759738
response_model=collection.Collection,
760-
responses={
761-
400: {"description": "Bad request"},
762-
401: {"description": "Not authenticated"},
763-
403: {"description": "User lacks necessary permissions"},
764-
},
739+
responses={**ACCESS_CONTROL_ERROR_RESPONSES, **BASE_409_RESPONSE},
765740
summary="Remove a user from a collection role",
766741
)
767742
async def remove_user_from_collection_role(
@@ -817,7 +792,7 @@ async def remove_user_from_collection_role(
817792
extra=logging_context(),
818793
)
819794
raise HTTPException(
820-
status_code=400,
795+
status_code=409,
821796
detail=f"user with ORCID iD '{orcid_id}' does not currently hold the role {role} for collection '{urn}'",
822797
)
823798

@@ -845,10 +820,7 @@ async def remove_user_from_collection_role(
845820

846821
@router.delete(
847822
"/collections/{urn}",
848-
responses={
849-
401: {"description": "Not authenticated"},
850-
403: {"description": "User lacks necessary permissions"},
851-
},
823+
responses={**ACCESS_CONTROL_ERROR_RESPONSES},
852824
summary="Delete a collection",
853825
)
854826
async def delete_collection(

src/mavedb/routers/controlled_keywords.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
from mavedb import deps
77
from mavedb.lib.keywords import search_keyword as _search_keyword
88
from mavedb.models.controlled_keyword import ControlledKeyword
9+
from mavedb.routers.shared import PUBLIC_ERROR_RESPONSES, ROUTER_BASE_PREFIX
910
from mavedb.view_models import keyword
1011

1112
router = APIRouter(
12-
prefix="/api/v1/controlled-keywords",
13+
prefix=f"{ROUTER_BASE_PREFIX}/controlled-keywords",
1314
tags=["Controlled Keywords"],
14-
responses={404: {"description": "Not found"}, 500: {"description": "Internal server error"}},
15+
responses={**PUBLIC_ERROR_RESPONSES},
1516
)
1617

1718

0 commit comments

Comments
 (0)