Skip to content

Commit

Permalink
♻️ Refactor API-keys service (ITISFoundation#6843)
Browse files Browse the repository at this point in the history
Co-authored-by: odeimaiz <[email protected]>
  • Loading branch information
giancarloromeo and odeimaiz authored Dec 10, 2024
1 parent 6d1ee29 commit 34374b4
Show file tree
Hide file tree
Showing 33 changed files with 1,150 additions and 663 deletions.
68 changes: 0 additions & 68 deletions api/specs/web-server/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
from fastapi import APIRouter, status
from models_library.api_schemas_webserver.auth import (
AccountRequestInfo,
ApiKeyCreate,
ApiKeyGet,
UnregisterCheck,
)
from models_library.generics import Envelope
Expand Down Expand Up @@ -264,72 +262,6 @@ async def email_confirmation(code: str):
"""email link sent to user to confirm an action"""


@router.get(
"/auth/api-keys",
operation_id="list_api_keys",
responses={
status.HTTP_200_OK: {
"description": "returns the display names of API keys",
"model": list[str],
},
status.HTTP_400_BAD_REQUEST: {
"description": "key name requested is invalid",
},
status.HTTP_401_UNAUTHORIZED: {
"description": "requires login to list keys",
},
status.HTTP_403_FORBIDDEN: {
"description": "not enough permissions to list keys",
},
},
)
async def list_api_keys():
"""lists display names of API keys by this user"""


@router.post(
"/auth/api-keys",
operation_id="create_api_key",
responses={
status.HTTP_200_OK: {
"description": "Authorization granted returning API key",
"model": ApiKeyGet,
},
status.HTTP_400_BAD_REQUEST: {
"description": "key name requested is invalid",
},
status.HTTP_401_UNAUTHORIZED: {
"description": "requires login to list keys",
},
status.HTTP_403_FORBIDDEN: {
"description": "not enough permissions to list keys",
},
},
)
async def create_api_key(_body: ApiKeyCreate):
"""creates API keys to access public API"""


@router.delete(
"/auth/api-keys",
operation_id="delete_api_key",
status_code=status.HTTP_204_NO_CONTENT,
responses={
status.HTTP_204_NO_CONTENT: {
"description": "api key successfully deleted",
},
status.HTTP_401_UNAUTHORIZED: {
"description": "requires login to delete a key",
},
status.HTTP_403_FORBIDDEN: {
"description": "not enough permissions to delete a key",
},
},
)
async def delete_api_key(_body: ApiKeyCreate):
"""deletes API key by name"""


@router.get(
"/auth/captcha",
operation_id="request_captcha",
Expand Down
60 changes: 60 additions & 0 deletions api/specs/web-server/_auth_api_keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from typing import Annotated

from fastapi import APIRouter, Depends, status
from models_library.api_schemas_webserver.auth import (
ApiKeyCreateRequest,
ApiKeyCreateResponse,
ApiKeyGet,
)
from models_library.generics import Envelope
from models_library.rest_error import EnvelopedError
from simcore_service_webserver._meta import API_VTAG
from simcore_service_webserver.api_keys._exceptions_handlers import _TO_HTTP_ERROR_MAP
from simcore_service_webserver.api_keys._rest import ApiKeysPathParams

router = APIRouter(
prefix=f"/{API_VTAG}",
tags=["auth"],
responses={
i.status_code: {"model": EnvelopedError} for i in _TO_HTTP_ERROR_MAP.values()
},
)


@router.post(
"/auth/api-keys",
operation_id="create_api_key",
status_code=status.HTTP_201_CREATED,
response_model=Envelope[ApiKeyCreateResponse],
)
async def create_api_key(_body: ApiKeyCreateRequest):
"""creates API keys to access public API"""


@router.get(
"/auth/api-keys",
operation_id="list_api_keys",
response_model=Envelope[list[ApiKeyGet]],
status_code=status.HTTP_200_OK,
)
async def list_api_keys():
"""lists API keys by this user"""


@router.get(
"/auth/api-keys/{api_key_id}",
operation_id="get_api_key",
response_model=Envelope[ApiKeyGet],
status_code=status.HTTP_200_OK,
)
async def get_api_key(_path: Annotated[ApiKeysPathParams, Depends()]):
"""returns the API Key with the given ID"""


@router.delete(
"/auth/api-keys/{api_key_id}",
operation_id="delete_api_key",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_api_key(_path: Annotated[ApiKeysPathParams, Depends()]):
"""deletes the API key with the given ID"""
1 change: 1 addition & 0 deletions api/specs/web-server/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#
# core ---
"_auth",
"_auth_api_keys",
"_groups",
"_tags",
"_tags_groups", # after _tags
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from datetime import timedelta
from typing import Any
from typing import Annotated, Any

from pydantic import BaseModel, ConfigDict, Field, SecretStr
from models_library.basic_types import IDStr
from pydantic import AliasGenerator, ConfigDict, Field, HttpUrl, SecretStr
from pydantic.alias_generators import to_camel

from ..emails import LowerCaseEmailStr
from ._base import InputSchema
from ._base import InputSchema, OutputSchema


class AccountRequestInfo(InputSchema):
Expand Down Expand Up @@ -51,42 +53,97 @@ class UnregisterCheck(InputSchema):
#


class ApiKeyCreate(BaseModel):
display_name: str = Field(..., min_length=3)
class ApiKeyCreateRequest(InputSchema):
display_name: Annotated[str, Field(..., min_length=3)]
expiration: timedelta | None = Field(
None,
description="Time delta from creation time to expiration. If None, then it does not expire.",
)

model_config = ConfigDict(
alias_generator=AliasGenerator(
validation_alias=to_camel,
),
from_attributes=True,
json_schema_extra={
"examples": [
{
"displayName": "test-api-forever",
},
{
"displayName": "test-api-for-one-day",
"expiration": 60 * 60 * 24,
},
{
"displayName": "test-api-for-another-day",
"expiration": "24:00:00",
},
]
},
)


class ApiKeyCreateResponse(OutputSchema):
id: IDStr
display_name: Annotated[str, Field(..., min_length=3)]
expiration: timedelta | None = Field(
None,
description="Time delta from creation time to expiration. If None, then it does not expire.",
)
api_base_url: HttpUrl
api_key: str
api_secret: str

model_config = ConfigDict(
alias_generator=AliasGenerator(
serialization_alias=to_camel,
),
from_attributes=True,
json_schema_extra={
"examples": [
{
"id": "42",
"display_name": "test-api-forever",
"api_base_url": "http://api.osparc.io/v0", # NOSONAR
"api_key": "key",
"api_secret": "secret",
},
{
"id": "48",
"display_name": "test-api-for-one-day",
"expiration": 60 * 60 * 24,
"api_base_url": "http://api.sim4life.io/v0", # NOSONAR
"api_key": "key",
"api_secret": "secret",
},
{
"id": "54",
"display_name": "test-api-for-another-day",
"expiration": "24:00:00",
"api_base_url": "http://api.osparc-master.io/v0", # NOSONAR
"api_key": "key",
"api_secret": "secret",
},
]
}
},
)


class ApiKeyGet(BaseModel):
display_name: str = Field(..., min_length=3)
api_key: str
api_secret: str
class ApiKeyGet(OutputSchema):
id: IDStr
display_name: Annotated[str, Field(..., min_length=3)]

model_config = ConfigDict(
alias_generator=AliasGenerator(
serialization_alias=to_camel,
),
from_attributes=True,
json_schema_extra={
"examples": [
{"display_name": "myapi", "api_key": "key", "api_secret": "secret"},
{
"id": "42",
"display_name": "myapi",
},
]
},
)
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import datetime as dt
from typing import Annotated

from models_library.basic_types import IDStr
from pydantic import BaseModel, ConfigDict, Field


class ApiKeyCreate(BaseModel):
display_name: Annotated[str, Field(..., min_length=3)]
expiration: dt.timedelta | None = None

model_config = ConfigDict(
from_attributes=True,
)


class ApiKeyGet(BaseModel):
id: IDStr
display_name: Annotated[str, Field(..., min_length=3)]
api_key: str | None = None
api_secret: str | None = None

model_config = ConfigDict(
from_attributes=True,
json_schema_extra={
"examples": [
{
"id": "42",
"display_name": "test-api-forever",
"api_key": "key",
"api_secret": "secret",
},
]
},
)
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import logging

from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE
from models_library.basic_types import IDStr
from models_library.rabbitmq_basic_types import RPCMethodName
from models_library.rpc.webserver.auth.api_keys import ApiKeyCreate, ApiKeyGet
from pydantic import TypeAdapter
from servicelib.logging_utils import log_decorator
from servicelib.rabbitmq import RabbitMQRPCClient

_logger = logging.getLogger(__name__)


@log_decorator(_logger, level=logging.DEBUG)
async def create_api_key(
rabbitmq_rpc_client: RabbitMQRPCClient,
*,
user_id: str,
product_name: str,
api_key: ApiKeyCreate,
) -> ApiKeyGet:
result: ApiKeyGet = await rabbitmq_rpc_client.request(
WEBSERVER_RPC_NAMESPACE,
TypeAdapter(RPCMethodName).validate_python("create_api_key"),
user_id=user_id,
product_name=product_name,
api_key=api_key,
)
assert isinstance(result, ApiKeyGet)
return result


@log_decorator(_logger, level=logging.DEBUG)
async def get_api_key(
rabbitmq_rpc_client: RabbitMQRPCClient,
*,
user_id: str,
product_name: str,
api_key_id: IDStr,
) -> ApiKeyGet:
result: ApiKeyGet = await rabbitmq_rpc_client.request(
WEBSERVER_RPC_NAMESPACE,
TypeAdapter(RPCMethodName).validate_python("get_api_key"),
user_id=user_id,
product_name=product_name,
api_key_id=api_key_id,
)
assert isinstance(result, ApiKeyGet)
return result


async def delete_api_key(
rabbitmq_rpc_client: RabbitMQRPCClient,
*,
user_id: str,
product_name: str,
api_key_id: IDStr,
) -> None:
result = await rabbitmq_rpc_client.request(
WEBSERVER_RPC_NAMESPACE,
TypeAdapter(RPCMethodName).validate_python("delete_api_key"),
user_id=user_id,
product_name=product_name,
api_key_id=api_key_id,
)
assert result is None
Loading

0 comments on commit 34374b4

Please sign in to comment.