Skip to content

Commit

Permalink
🐛 Fixes api-keys unique constraint violation (ITISFoundation#5890)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcrespov authored May 29, 2024
1 parent 874ebfd commit 030f126
Show file tree
Hide file tree
Showing 15 changed files with 157 additions and 537 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
socketio,
storage,
)
from ..modules.osparc_variables import api_keys_manager, substitutions
from ..modules.osparc_variables import substitutions
from .errors import (
ClusterAccessForbiddenError,
ClusterNotFoundError,
Expand Down Expand Up @@ -168,7 +168,6 @@ def init_app(settings: AppSettings | None = None) -> FastAPI:
if dynamic_scheduler_enabled:
redis.setup(app)
dynamic_sidecar.setup(app)
api_keys_manager.setup(app)
socketio.setup(app)
notifier.setup(app)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
UserPreferencesFrontendRepository,
)
from ....director_v0 import DirectorV0Client
from ....osparc_variables import safe_remove_api_key_and_secret
from ...api_client import (
SidecarsClient,
get_dynamic_sidecar_service_health,
Expand Down Expand Up @@ -340,10 +339,6 @@ async def attempt_pod_removal_and_data_saving(
TaskProgress.create(), app, scheduler_data.node_uuid, settings.SWARM_STACK_NAME
)

await safe_remove_api_key_and_secret(
app, node_id=scheduler_data.node_uuid, run_id=scheduler_data.run_id
)

# remove sidecar's api client
remove_sidecars_client(app, scheduler_data.node_uuid)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +0,0 @@
from .api_keys_manager import safe_remove_api_key_and_secret

assert safe_remove_api_key_and_secret # nosec

__all__: tuple[str, ...] = ("safe_remove_api_key_and_secret",)
Original file line number Diff line number Diff line change
@@ -1,43 +1,38 @@
from typing import Any, Final, cast
from uuid import UUID, uuid5
import uuid
from typing import cast
from uuid import uuid5

from aiocache import cached
from fastapi import FastAPI
from models_library.api_schemas_webserver.auth import ApiKeyGet
from models_library.products import ProductName
from models_library.users import UserID

from ._api_auth_rpc import create_api_key_and_secret, get_api_key_and_secret
from ._api_auth_rpc import get_or_create_api_key_and_secret

_NAMESPACE: Final = UUID("ce021d45-82e6-4dfe-872c-2f452cf289f8")

def create_unique_api_name_for(product_name: ProductName, user_id: UserID) -> str:
# NOTE: The namespace chosen doesn't significantly impact the resulting UUID
# as long as it's consistently used across the same context
return f"__auto_{uuid5(uuid.NAMESPACE_DNS, f'{product_name}/{user_id}')}"

def _create_unique_identifier_from(*parts: Any) -> str:
return f"{uuid5(_NAMESPACE, '/'.join(map(str, parts)) )}"


def create_user_api_name(product_name: ProductName, user_id: UserID) -> str:
return f"__auto_{_create_unique_identifier_from(product_name, user_id)}"


def _build_cache_key(fct, *_, **kwargs):
# NOTE: Uses caching to prevent multiple calls to the external service
# when 'get_or_create_user_api_key' or 'get_or_create_user_api_secret' are invoked.
def _cache_key(fct, *_, **kwargs):
return f"{fct.__name__}_{kwargs['product_name']}_{kwargs['user_id']}"


@cached(ttl=3, key_builder=_build_cache_key)
async def _get_or_create_data(
@cached(ttl=3, key_builder=_cache_key)
async def _get_or_create_for(
app: FastAPI,
*,
product_name: ProductName,
user_id: UserID,
) -> ApiKeyGet:

name = create_user_api_name(product_name, user_id)
if data := await get_api_key_and_secret(
app, product_name=product_name, user_id=user_id, name=name
):
return data
return await create_api_key_and_secret(
name = create_unique_api_name_for(product_name, user_id)
return await get_or_create_api_key_and_secret(
app, product_name=product_name, user_id=user_id, name=name, expiration=None
)

Expand All @@ -47,7 +42,7 @@ async def get_or_create_user_api_key(
product_name: ProductName,
user_id: UserID,
) -> str:
data = await _get_or_create_data(
data = await _get_or_create_for(
app,
product_name=product_name,
user_id=user_id,
Expand All @@ -60,7 +55,7 @@ async def get_or_create_user_api_secret(
product_name: ProductName,
user_id: UserID,
) -> str:
data = await _get_or_create_data(
data = await _get_or_create_for(
app,
product_name=product_name,
user_id=user_id,
Expand All @@ -71,5 +66,5 @@ async def get_or_create_user_api_secret(
__all__: tuple[str, ...] = (
"get_or_create_user_api_key",
"get_or_create_user_api_secret",
"create_user_api_name",
"create_unique_api_name_for",
)
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from datetime import timedelta
from typing import Any

from fastapi import FastAPI
from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE
from models_library.api_schemas_webserver.auth import ApiKeyCreate, ApiKeyGet
from models_library.api_schemas_webserver.auth import ApiKeyGet
from models_library.products import ProductName
from models_library.rabbitmq_basic_types import RPCMethodName
from models_library.users import UserID
Expand All @@ -16,7 +15,7 @@
#


async def create_api_key_and_secret(
async def get_or_create_api_key_and_secret(
app: FastAPI,
*,
product_name: ProductName,
Expand All @@ -27,36 +26,10 @@ async def create_api_key_and_secret(
rpc_client = get_rabbitmq_rpc_client(app)
result = await rpc_client.request(
WEBSERVER_RPC_NAMESPACE,
parse_obj_as(RPCMethodName, "create_api_keys"),
product_name=product_name,
user_id=user_id,
new=ApiKeyCreate(display_name=name, expiration=expiration),
)
return ApiKeyGet.parse_obj(result)


async def get_api_key_and_secret(
app: FastAPI, *, product_name: ProductName, user_id: UserID, name: str
) -> ApiKeyGet | None:
rpc_client = get_rabbitmq_rpc_client(app)
result: Any | None = await rpc_client.request(
WEBSERVER_RPC_NAMESPACE,
parse_obj_as(RPCMethodName, "api_key_get"),
product_name=product_name,
user_id=user_id,
name=name,
)
return parse_obj_as(ApiKeyGet | None, result)


async def delete_api_key_and_secret(
app: FastAPI, *, product_name: ProductName, user_id: UserID, name: str
):
rpc_client = get_rabbitmq_rpc_client(app)
await rpc_client.request(
WEBSERVER_RPC_NAMESPACE,
parse_obj_as(RPCMethodName, "delete_api_keys"),
parse_obj_as(RPCMethodName, "get_or_create_api_keys"),
product_name=product_name,
user_id=user_id,
name=name,
expiration=expiration,
)
return ApiKeyGet.parse_obj(result)
Loading

0 comments on commit 030f126

Please sign in to comment.