Skip to content

Commit 933b7d4

Browse files
committed
Add router to get limited user information by Orcid ID
This router exposes limited user information (first name, last name, and username/Orcid ID) to non-admin users. Previously, non-admin users could not look up users at all. Non-self user lookup by non-admin users is necessary for adding users to a collection in the UI. Use Starlette custom type convertor to determine whether the provided ID in the router path is an Orcid ID. If so, limited information about the user will be returned, whether or not the requesting user has admin privileges. If the ID in the router path is an integer (database ID), the previously existing router will be used, which requires admin privileges and returns the full admin view of the user. We may choose to combine these routers in the future, in order to only allow user lookup by Orcid ID/username, unless there is a use case for looking up by database ID. Note that the custom type convertor requires an actual Orcid ID, so usernames that are not Orcid IDs cannot be used for user lookup.
1 parent 6bcfcf1 commit 933b7d4

File tree

2 files changed

+51
-3
lines changed

2 files changed

+51
-3
lines changed

src/mavedb/lib/permissions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818

1919
class Action(Enum):
20+
LOOKUP = "lookup"
2021
READ = "read"
2122
UPDATE = "update"
2223
DELETE = "delete"
@@ -376,6 +377,15 @@ def has_permission(user_data: Optional[UserData], item: Base, action: Action) ->
376377
raise NotImplementedError(f"has_permission(User, ScoreSet, {action}, Role)")
377378

378379
elif isinstance(item, User):
380+
if action == Action.LOOKUP:
381+
# any existing user can look up any mavedb user by Orcid ID
382+
# lookup differs from read because lookup means getting the first name, last name, and orcid ID of the user,
383+
# while read means getting an admin view of the user's details
384+
if user_data is not None and user_data.user is not None:
385+
return PermissionResponse(True)
386+
else:
387+
# TODO is this inappropriately acknowledging the existence of the user?
388+
return PermissionResponse(False, 401, "Insufficient permissions for user lookup.")
379389
if action == Action.READ:
380390
if user_is_self:
381391
return PermissionResponse(True)

src/mavedb/routers/users.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from fastapi import APIRouter, Depends, HTTPException
55
from sqlalchemy.orm import Session
6+
from starlette.convertors import Convertor, register_url_convertor
67

78
from mavedb import deps
89
from mavedb.lib.authentication import UserData
@@ -21,6 +22,21 @@
2122
logger = logging.getLogger(__name__)
2223

2324

25+
# Define custom type convertor (see https://www.starlette.io/routing/#path-parameters)
26+
# in order to recognize user lookup id as an int or orcid id, and call the appropriate function
27+
class OrcidIdConverter(Convertor):
28+
regex = "\d{4}-\d{4}-\d{4}-(\d{4}|\d{3}X)"
29+
30+
def convert(self, value: str) -> str:
31+
return value
32+
33+
def to_string(self, value: str) -> str:
34+
return str(value)
35+
36+
37+
register_url_convertor("orcid_id", OrcidIdConverter())
38+
39+
2440
# Trailing slash is deliberate
2541
@router.get("/users/", status_code=200, response_model=list[user.AdminUser], responses={404: {}})
2642
async def list_users(
@@ -41,18 +57,40 @@ async def show_me(*, user_data: UserData = Depends(require_current_user)) -> Any
4157
return user_data.user
4258

4359

44-
@router.get("/users/{id}", status_code=200, response_model=user.AdminUser, responses={404: {}, 500: {}})
45-
async def show_user(
60+
@router.get("/users/{id:int}", status_code=200, response_model=user.AdminUser, responses={404: {}, 500: {}})
61+
async def show_user_admin(
4662
*, id: int, user_data: UserData = Depends(RoleRequirer([UserRole.admin])), db: Session = Depends(deps.get_db)
4763
) -> Any:
4864
"""
49-
Fetch a single user by ID.
65+
Fetch a single user by ID. Returns admin view of requested user.
5066
"""
5167
save_to_logging_context({"requested_user": id})
5268
item = db.query(User).filter(User.id == id).one_or_none()
5369
if not item:
5470
logger.warning(msg="Could not show user; Requested user does not exist.", extra=logging_context())
5571
raise HTTPException(status_code=404, detail=f"User with ID {id} not found")
72+
73+
# moving toward always accessing permissions module, even though this function does already require admin role to access
74+
assert_permission(user_data, item, Action.READ)
75+
return item
76+
77+
78+
@router.get("/users/{orcid_id:orcid_id}", status_code=200, response_model=user.User, responses={404: {}, 500: {}})
79+
async def show_user(
80+
*, orcid_id: str, user_data: UserData = Depends(require_current_user), db: Session = Depends(deps.get_db)
81+
) -> Any:
82+
"""
83+
Fetch a single user by Orcid ID. Returns limited view of user.
84+
"""
85+
save_to_logging_context({"requested_user": orcid_id})
86+
87+
item = db.query(User).filter(User.username == orcid_id).one_or_none()
88+
if not item:
89+
logger.warning(msg="Could not show user; Requested user does not exist.", extra=logging_context())
90+
raise HTTPException(status_code=404, detail=f"User with ID {orcid_id} not found")
91+
92+
# moving toward always accessing permissions module, even though this function does already require existing user in order to access
93+
assert_permission(user_data, item, Action.LOOKUP)
5694
return item
5795

5896

0 commit comments

Comments
 (0)