Skip to content

Commit 5d392be

Browse files
Merge pull request #99 from stuartcampbell/main
Fix API key generation
2 parents ccc9001 + d461398 commit 5d392be

File tree

4 files changed

+72
-16
lines changed

4 files changed

+72
-16
lines changed

src/nsls2api/api/v1/admin_api.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
validate_admin_role,
1111
generate_api_key,
1212
)
13-
from nsls2api.models.apikeys import ApiUser
13+
from nsls2api.models.apikeys import ApiUser, ApiUserRole, ApiUserResponseModel, ApiUserType
1414
from nsls2api.models.slack_models import SlackChannelCreationResponseModel
1515
from nsls2api.services import beamline_service, proposal_service, slack_service
1616

@@ -42,15 +42,15 @@ async def check_admin_validation(
4242
return admin_user.username
4343

4444

45-
@router.post("/admin/generate_api_key/{username}")
46-
async def generate_user_apikey(username: str):
45+
@router.post("/admin/generate-api-key/{username}")
46+
async def generate_user_apikey(username: str, usertype: ApiUserType = ApiUserType.user):
4747
"""
4848
Generate an API key for a given username.
4949
5050
:param username: The username for which to generate the API key.
5151
:return: The generated API key.
5252
"""
53-
return await generate_api_key(username)
53+
return await generate_api_key(username, usertype=usertype)
5454

5555

5656
@router.post("/admin/proposal/generate-test")
@@ -148,3 +148,34 @@ async def create_slack_channel(proposal_id: str) -> SlackChannelCreationResponse
148148
)
149149

150150
return response_model
151+
152+
153+
@router.put("/admin/user/{username}/role/{role}")
154+
async def update_user_role(username: str, role: ApiUserRole) -> ApiUserResponseModel:
155+
"""
156+
Update the role of a user.
157+
158+
:param username: The username of the user to update.
159+
:param role: The new role for the user.
160+
:return: The updated user object.
161+
"""
162+
user = await ApiUser.find_one(ApiUser.username==username)
163+
if user is None:
164+
raise HTTPException(
165+
status_code=fastapi.status.HTTP_404_NOT_FOUND,
166+
detail=f"User {username} not found",
167+
)
168+
169+
user.role = role
170+
await user.save()
171+
172+
response = ApiUserResponseModel(
173+
id=user.id,
174+
username=user.username,
175+
type=user.type,
176+
role=user.role,
177+
created_on=user.created_on,
178+
last_updated=user.last_updated,
179+
)
180+
181+
return response

src/nsls2api/api/v1/beamline_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from nsls2api.models.beamlines import (
1515
Beamline,
1616
BeamlineService,
17+
Detector,
1718
DetectorList,
1819
DirectoryList,
1920
)

src/nsls2api/infrastructure/security.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import calendar
22
import datetime
33
import enum
4-
import logging
54
import secrets
65
from typing import Optional
76

@@ -12,6 +11,7 @@
1211
from pydantic_settings import BaseSettings
1312

1413
from nsls2api.infrastructure.config import get_settings
14+
from nsls2api.infrastructure.logging import logger
1515
from nsls2api.models.apikeys import ApiKey, ApiUser, ApiUserType, ApiUserRole
1616

1717
TOKEN_BYTE_LENGTH = 32
@@ -20,8 +20,6 @@
2020
api_key_header = APIKeyHeader(name="Authorization", auto_error=False)
2121
api_key_query = APIKeyQuery(name="api_key", auto_error=False)
2222

23-
logger = logging.getLogger(__name__)
24-
2523

2624
def hash_api_key(api_key):
2725
return crypto.hash(api_key)
@@ -41,14 +39,14 @@ async def get_api_key(
4139
return None
4240

4341

44-
async def generate_api_key(username: str):
42+
async def generate_api_key(username: str, usertype=ApiUserType.user):
4543
try:
4644
# Is there an API user for this key to be associated with
4745
user = await ApiUser.find_one(ApiUser.username == username)
4846
# If not create one
4947
if not user:
5048
print("No user found - creating user principal")
51-
user = ApiUser(username=username, type=ApiUserType.user)
49+
user = ApiUser(username=username, type=usertype)
5250
await user.save(link_rule=WriteRules.WRITE)
5351

5452
# Actually generate the api key and add a readable prefix
@@ -69,19 +67,35 @@ async def generate_api_key(username: str):
6967
expires_after=None,
7068
)
7169

72-
# user.user_api_keys.append(new_key)
73-
await user.update(link_rule=WriteRules.WRITE)
70+
old_keys = await ApiKey.find(ApiKey.username == username).to_list()
71+
7472
await new_key.save(link_rule=WriteRules.WRITE)
7573

7674
# Now that we have saved a new key for this user, we should invalidate any other keys
75+
for old_key in old_keys:
76+
if old_key.valid:
77+
logger.info(f"Invalidating old key: {old_key.secret_key}")
78+
old_key.valid = False
79+
await old_key.save(link_rule=WriteRules.WRITE)
7780

7881
return {"key:": secret_key}
7982

8083
except Exception as e:
81-
print(e)
84+
logger.exception(e)
8285
raise e
8386

8487

88+
async def set_user_role(username: str, role: ApiUserRole):
89+
user = await ApiUser.find_one(ApiUser.username == username)
90+
if user is None:
91+
raise LookupError(f"Could not find a user with the username: {username}")
92+
93+
user.role = role
94+
await user.save(link_rule=WriteRules.WRITE)
95+
96+
return user
97+
98+
8599
async def lookup_api_key(token: str) -> ApiKey:
86100
"""
87101
:param token: The token used for API key lookup

src/nsls2api/models/apikeys.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import datetime
2-
import enum
2+
from enum import StrEnum
33
from typing import Optional, List
44
from uuid import UUID, uuid4
55

@@ -10,16 +10,24 @@
1010
from pydantic import Field
1111

1212

13-
class ApiUserType(str, enum.Enum):
13+
class ApiUserType(StrEnum):
1414
user = "user"
1515
service = "service"
1616

1717

18-
class ApiUserRole(str, enum.Enum):
18+
class ApiUserRole(StrEnum):
1919
user = "user"
2020
staff = "staff"
2121
admin = "admin"
2222

23+
class ApiUserResponseModel(pydantic.BaseModel):
24+
id: UUID
25+
username: str
26+
type: ApiUserType
27+
role: ApiUserRole
28+
created_on: datetime.datetime
29+
last_updated: datetime.datetime
30+
2331

2432
class ApiUser(beanie.Document):
2533
id: UUID = Field(default_factory=uuid4)
@@ -60,7 +68,9 @@ class ApiKey(beanie.Document):
6068
user: Link[ApiUser]
6169
username: str
6270
first_eight: pydantic.constr(min_length=8, max_length=8)
63-
secret_key: str # TODO: After development - we will not be storing this one in the database
71+
secret_key: (
72+
str # TODO: After development - we will not be storing this one in the database
73+
)
6474
hashed_key: str
6575
note: Optional[str] = ""
6676
# scopes: Optional[list[str]] = pydantic.Field(..., example=["inherit"])

0 commit comments

Comments
 (0)