Skip to content

Commit 34374b4

Browse files
♻️ Refactor API-keys service (#6843)
Co-authored-by: odeimaiz <[email protected]>
1 parent 6d1ee29 commit 34374b4

File tree

33 files changed

+1150
-663
lines changed

33 files changed

+1150
-663
lines changed

api/specs/web-server/_auth.py

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
from fastapi import APIRouter, status
1010
from models_library.api_schemas_webserver.auth import (
1111
AccountRequestInfo,
12-
ApiKeyCreate,
13-
ApiKeyGet,
1412
UnregisterCheck,
1513
)
1614
from models_library.generics import Envelope
@@ -264,72 +262,6 @@ async def email_confirmation(code: str):
264262
"""email link sent to user to confirm an action"""
265263

266264

267-
@router.get(
268-
"/auth/api-keys",
269-
operation_id="list_api_keys",
270-
responses={
271-
status.HTTP_200_OK: {
272-
"description": "returns the display names of API keys",
273-
"model": list[str],
274-
},
275-
status.HTTP_400_BAD_REQUEST: {
276-
"description": "key name requested is invalid",
277-
},
278-
status.HTTP_401_UNAUTHORIZED: {
279-
"description": "requires login to list keys",
280-
},
281-
status.HTTP_403_FORBIDDEN: {
282-
"description": "not enough permissions to list keys",
283-
},
284-
},
285-
)
286-
async def list_api_keys():
287-
"""lists display names of API keys by this user"""
288-
289-
290-
@router.post(
291-
"/auth/api-keys",
292-
operation_id="create_api_key",
293-
responses={
294-
status.HTTP_200_OK: {
295-
"description": "Authorization granted returning API key",
296-
"model": ApiKeyGet,
297-
},
298-
status.HTTP_400_BAD_REQUEST: {
299-
"description": "key name requested is invalid",
300-
},
301-
status.HTTP_401_UNAUTHORIZED: {
302-
"description": "requires login to list keys",
303-
},
304-
status.HTTP_403_FORBIDDEN: {
305-
"description": "not enough permissions to list keys",
306-
},
307-
},
308-
)
309-
async def create_api_key(_body: ApiKeyCreate):
310-
"""creates API keys to access public API"""
311-
312-
313-
@router.delete(
314-
"/auth/api-keys",
315-
operation_id="delete_api_key",
316-
status_code=status.HTTP_204_NO_CONTENT,
317-
responses={
318-
status.HTTP_204_NO_CONTENT: {
319-
"description": "api key successfully deleted",
320-
},
321-
status.HTTP_401_UNAUTHORIZED: {
322-
"description": "requires login to delete a key",
323-
},
324-
status.HTTP_403_FORBIDDEN: {
325-
"description": "not enough permissions to delete a key",
326-
},
327-
},
328-
)
329-
async def delete_api_key(_body: ApiKeyCreate):
330-
"""deletes API key by name"""
331-
332-
333265
@router.get(
334266
"/auth/captcha",
335267
operation_id="request_captcha",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import Annotated
2+
3+
from fastapi import APIRouter, Depends, status
4+
from models_library.api_schemas_webserver.auth import (
5+
ApiKeyCreateRequest,
6+
ApiKeyCreateResponse,
7+
ApiKeyGet,
8+
)
9+
from models_library.generics import Envelope
10+
from models_library.rest_error import EnvelopedError
11+
from simcore_service_webserver._meta import API_VTAG
12+
from simcore_service_webserver.api_keys._exceptions_handlers import _TO_HTTP_ERROR_MAP
13+
from simcore_service_webserver.api_keys._rest import ApiKeysPathParams
14+
15+
router = APIRouter(
16+
prefix=f"/{API_VTAG}",
17+
tags=["auth"],
18+
responses={
19+
i.status_code: {"model": EnvelopedError} for i in _TO_HTTP_ERROR_MAP.values()
20+
},
21+
)
22+
23+
24+
@router.post(
25+
"/auth/api-keys",
26+
operation_id="create_api_key",
27+
status_code=status.HTTP_201_CREATED,
28+
response_model=Envelope[ApiKeyCreateResponse],
29+
)
30+
async def create_api_key(_body: ApiKeyCreateRequest):
31+
"""creates API keys to access public API"""
32+
33+
34+
@router.get(
35+
"/auth/api-keys",
36+
operation_id="list_api_keys",
37+
response_model=Envelope[list[ApiKeyGet]],
38+
status_code=status.HTTP_200_OK,
39+
)
40+
async def list_api_keys():
41+
"""lists API keys by this user"""
42+
43+
44+
@router.get(
45+
"/auth/api-keys/{api_key_id}",
46+
operation_id="get_api_key",
47+
response_model=Envelope[ApiKeyGet],
48+
status_code=status.HTTP_200_OK,
49+
)
50+
async def get_api_key(_path: Annotated[ApiKeysPathParams, Depends()]):
51+
"""returns the API Key with the given ID"""
52+
53+
54+
@router.delete(
55+
"/auth/api-keys/{api_key_id}",
56+
operation_id="delete_api_key",
57+
status_code=status.HTTP_204_NO_CONTENT,
58+
)
59+
async def delete_api_key(_path: Annotated[ApiKeysPathParams, Depends()]):
60+
"""deletes the API key with the given ID"""

api/specs/web-server/openapi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#
2121
# core ---
2222
"_auth",
23+
"_auth_api_keys",
2324
"_groups",
2425
"_tags",
2526
"_tags_groups", # after _tags

packages/models-library/src/models_library/api_schemas_webserver/auth.py

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from datetime import timedelta
2-
from typing import Any
2+
from typing import Annotated, Any
33

4-
from pydantic import BaseModel, ConfigDict, Field, SecretStr
4+
from models_library.basic_types import IDStr
5+
from pydantic import AliasGenerator, ConfigDict, Field, HttpUrl, SecretStr
6+
from pydantic.alias_generators import to_camel
57

68
from ..emails import LowerCaseEmailStr
7-
from ._base import InputSchema
9+
from ._base import InputSchema, OutputSchema
810

911

1012
class AccountRequestInfo(InputSchema):
@@ -51,42 +53,97 @@ class UnregisterCheck(InputSchema):
5153
#
5254

5355

54-
class ApiKeyCreate(BaseModel):
55-
display_name: str = Field(..., min_length=3)
56+
class ApiKeyCreateRequest(InputSchema):
57+
display_name: Annotated[str, Field(..., min_length=3)]
5658
expiration: timedelta | None = Field(
5759
None,
5860
description="Time delta from creation time to expiration. If None, then it does not expire.",
5961
)
6062

6163
model_config = ConfigDict(
64+
alias_generator=AliasGenerator(
65+
validation_alias=to_camel,
66+
),
67+
from_attributes=True,
68+
json_schema_extra={
69+
"examples": [
70+
{
71+
"displayName": "test-api-forever",
72+
},
73+
{
74+
"displayName": "test-api-for-one-day",
75+
"expiration": 60 * 60 * 24,
76+
},
77+
{
78+
"displayName": "test-api-for-another-day",
79+
"expiration": "24:00:00",
80+
},
81+
]
82+
},
83+
)
84+
85+
86+
class ApiKeyCreateResponse(OutputSchema):
87+
id: IDStr
88+
display_name: Annotated[str, Field(..., min_length=3)]
89+
expiration: timedelta | None = Field(
90+
None,
91+
description="Time delta from creation time to expiration. If None, then it does not expire.",
92+
)
93+
api_base_url: HttpUrl
94+
api_key: str
95+
api_secret: str
96+
97+
model_config = ConfigDict(
98+
alias_generator=AliasGenerator(
99+
serialization_alias=to_camel,
100+
),
101+
from_attributes=True,
62102
json_schema_extra={
63103
"examples": [
64104
{
105+
"id": "42",
65106
"display_name": "test-api-forever",
107+
"api_base_url": "http://api.osparc.io/v0", # NOSONAR
108+
"api_key": "key",
109+
"api_secret": "secret",
66110
},
67111
{
112+
"id": "48",
68113
"display_name": "test-api-for-one-day",
69114
"expiration": 60 * 60 * 24,
115+
"api_base_url": "http://api.sim4life.io/v0", # NOSONAR
116+
"api_key": "key",
117+
"api_secret": "secret",
70118
},
71119
{
120+
"id": "54",
72121
"display_name": "test-api-for-another-day",
73122
"expiration": "24:00:00",
123+
"api_base_url": "http://api.osparc-master.io/v0", # NOSONAR
124+
"api_key": "key",
125+
"api_secret": "secret",
74126
},
75127
]
76-
}
128+
},
77129
)
78130

79131

80-
class ApiKeyGet(BaseModel):
81-
display_name: str = Field(..., min_length=3)
82-
api_key: str
83-
api_secret: str
132+
class ApiKeyGet(OutputSchema):
133+
id: IDStr
134+
display_name: Annotated[str, Field(..., min_length=3)]
84135

85136
model_config = ConfigDict(
137+
alias_generator=AliasGenerator(
138+
serialization_alias=to_camel,
139+
),
86140
from_attributes=True,
87141
json_schema_extra={
88142
"examples": [
89-
{"display_name": "myapi", "api_key": "key", "api_secret": "secret"},
143+
{
144+
"id": "42",
145+
"display_name": "myapi",
146+
},
90147
]
91148
},
92149
)

packages/models-library/src/models_library/rpc/__init__.py

Whitespace-only changes.

packages/models-library/src/models_library/rpc/webserver/__init__.py

Whitespace-only changes.

packages/models-library/src/models_library/rpc/webserver/auth/__init__.py

Whitespace-only changes.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import datetime as dt
2+
from typing import Annotated
3+
4+
from models_library.basic_types import IDStr
5+
from pydantic import BaseModel, ConfigDict, Field
6+
7+
8+
class ApiKeyCreate(BaseModel):
9+
display_name: Annotated[str, Field(..., min_length=3)]
10+
expiration: dt.timedelta | None = None
11+
12+
model_config = ConfigDict(
13+
from_attributes=True,
14+
)
15+
16+
17+
class ApiKeyGet(BaseModel):
18+
id: IDStr
19+
display_name: Annotated[str, Field(..., min_length=3)]
20+
api_key: str | None = None
21+
api_secret: str | None = None
22+
23+
model_config = ConfigDict(
24+
from_attributes=True,
25+
json_schema_extra={
26+
"examples": [
27+
{
28+
"id": "42",
29+
"display_name": "test-api-forever",
30+
"api_key": "key",
31+
"api_secret": "secret",
32+
},
33+
]
34+
},
35+
)

packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/__init__.py

Whitespace-only changes.

packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/auth/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)