Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨oSPARC API keys are created and removed if a service requires them #5004

Merged
Merged
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
9c83ace
added BaseOsparcGenericResourceManager
Nov 9, 2023
5af2814
Merge remote-tracking branch 'upstream/master' into pr-osparc-support…
Nov 9, 2023
973fde0
webserver now provide information if the key exists or not
Nov 9, 2023
3b6a896
refactor to support overloading on all methods
Nov 9, 2023
d680d25
added logging in case of failed removing
Nov 9, 2023
c495390
enhanced logging
Nov 9, 2023
d14d20d
lowering log level
Nov 9, 2023
e5c7951
refactor
Nov 10, 2023
a2386e1
refactor endpoint to return a single API key
Nov 10, 2023
731ab6b
Merge remote-tracking branch 'upstream/master' into pr-osparc-support…
Nov 10, 2023
31378d6
refacor api interface
Nov 10, 2023
3fbd453
added injection of identifier
Nov 13, 2023
d3aa8e6
adding API KEY
Nov 13, 2023
e2243e6
Merge remote-tracking branch 'upstream/master' into pr-osparc-support…
Nov 13, 2023
b5813c8
pylint
Nov 13, 2023
4c6b202
extend tests
Nov 13, 2023
d402435
refactor tests
Nov 13, 2023
05641ea
revert API endpoint
Nov 13, 2023
07afb9a
refactor
Nov 13, 2023
a4a9345
added tests for api_key_rpc
Nov 13, 2023
df106e0
Merge remote-tracking branch 'upstream/master' into pr-osparc-support…
Nov 13, 2023
ed07df0
fixed types
Nov 13, 2023
090677a
refactor tests
Nov 13, 2023
7a42eab
fixed test
Nov 13, 2023
9b8cc16
mypy
Nov 13, 2023
039e154
mypy complaining
Nov 13, 2023
d153615
Merge branch 'master' into pr-osparc-support-api-keyreplacement
GitHK Nov 14, 2023
654e1ad
Merge remote-tracking branch 'upstream/master' into pr-osparc-support…
Nov 14, 2023
f5da9ad
refactor
Nov 14, 2023
d8cd0c7
docstrings
Nov 14, 2023
8c49f0f
using list comprehension
Nov 14, 2023
cf5b8c1
refactor comment
Nov 14, 2023
d390279
add docstring
Nov 14, 2023
1db2737
naming
Nov 14, 2023
d2be888
rename
Nov 14, 2023
1fc6014
drop unused
Nov 14, 2023
04563cd
using plural
Nov 14, 2023
4d706a7
rename
Nov 14, 2023
9e03c41
moved to director-v2
Nov 14, 2023
638a1b3
refactor
Nov 14, 2023
5bcaa23
Merge branch 'pr-osparc-support-api-keyreplacement' of github.com:Git…
Nov 14, 2023
b8cef87
fix replacement error
Nov 15, 2023
e6dae63
Merge remote-tracking branch 'upstream/master' into pr-osparc-support…
Nov 15, 2023
5190980
rename for clarity
Nov 15, 2023
6c65f4e
updated specs
Nov 15, 2023
e7a51a2
renameing
Nov 15, 2023
e72f5de
Merge remote-tracking branch 'upstream/master' into pr-osparc-support…
Nov 15, 2023
4839723
refactored internals to remove enforced pattern
Nov 15, 2023
e3f7eff
remove todo
Nov 15, 2023
5673574
fix tests
Nov 15, 2023
f8f6a4d
fix mypy
Nov 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/specs/web-server/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ async def email_confirmation(code: str):
},
},
)
async def list_api_keys(code: str):
async def list_api_keys():
GitHK marked this conversation as resolved.
Show resolved Hide resolved
"""lists display names of API keys by this user"""


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ class ApiKey(BaseModel):
api_secret: SecretStr


class ApiKeyInDB(ApiKey):
class ApiKeyInDB(BaseModel):
api_key: str
api_secret: str
GitHK marked this conversation as resolved.
Show resolved Hide resolved

GitHK marked this conversation as resolved.
Show resolved Hide resolved
id_: int = Field(0, alias="id")
display_name: str
user_id: int
Expand Down
8 changes: 8 additions & 0 deletions services/api-server/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1580,6 +1580,7 @@
"Profile": {
"title": "Profile",
"required": [
"id",
"login",
"role"
],
Expand All @@ -1595,6 +1596,12 @@
"type": "string",
"example": "Maxwell"
},
"id": {
"title": "Id",
"exclusiveMinimum": true,
"type": "integer",
"minimum": 0
},
"login": {
"title": "Login",
"type": "string",
Expand All @@ -1614,6 +1621,7 @@
}
},
"example": {
"id": "20",
"first_name": "James",
"last_name": "Maxwell",
"login": "[email protected]",
Expand Down
6 changes: 3 additions & 3 deletions services/api-server/tests/unit/_with_db/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import yaml
from aiopg.sa.connection import SAConnection
from fastapi import FastAPI
from models_library.api_schemas_api_server.api_keys import ApiKeyInDB
from pydantic import PositiveInt
from pytest_simcore.helpers.rawdata_fakers import (
random_api_key,
Expand All @@ -36,7 +37,6 @@
from simcore_postgres_database.models.users import users
from simcore_service_api_server.core.application import init_app
from simcore_service_api_server.core.settings import PostgresSettings
from simcore_service_api_server.models.domain.api_keys import ApiKeyInDB

## POSTGRES -----

Expand Down Expand Up @@ -269,5 +269,5 @@ async def auth(
) -> httpx.BasicAuth:
"""overrides auth and uses access to real repositories instead of mocks"""
async for key in create_fake_api_keys(1):
return httpx.BasicAuth(key.api_key, key.api_secret.get_secret_value())
assert False, "Did not generate authentication"
return httpx.BasicAuth(key.api_key, key.api_secret)
GitHK marked this conversation as resolved.
Show resolved Hide resolved
pytest.fail("Did not generate authentication")
6 changes: 3 additions & 3 deletions services/api-server/tests/unit/_with_db/test_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
from faker import Faker
from fastapi import status
from fastapi.encoders import jsonable_encoder
from models_library.api_schemas_api_server.api_keys import ApiKeyInDB
from models_library.api_schemas_webserver.wallets import WalletGetWithAvailableCredits
from models_library.generics import Envelope
from models_library.users import UserID
from models_library.wallets import WalletStatus
from pydantic import PositiveInt
from simcore_service_api_server._meta import API_VTAG
from simcore_service_api_server.models.domain.api_keys import ApiKeyInDB


async def test_product_webserver(
Expand Down Expand Up @@ -70,7 +70,7 @@ def _check_key_product_compatibility(request: httpx.Request, **kwargs):
key = keys[wallet_id]
response = await client.get(
f"{API_VTAG}/wallets/{wallet_id}",
auth=httpx.BasicAuth(key.api_key, key.api_secret.get_secret_value()),
auth=httpx.BasicAuth(key.api_key, key.api_secret),
GitHK marked this conversation as resolved.
Show resolved Hide resolved
)
assert response.status_code == status.HTTP_200_OK
assert wallet_get_mock.call_count == len(keys)
Expand Down Expand Up @@ -104,7 +104,7 @@ def _get_service_side_effect(request: httpx.Request, **kwargs):
for key in keys:
response = await client.get(
f"{API_VTAG}/solvers/simcore/services/comp/isolve/releases/2.0.24",
auth=httpx.BasicAuth(key.api_key, key.api_secret.get_secret_value()),
auth=httpx.BasicAuth(key.api_key, key.api_secret),
GitHK marked this conversation as resolved.
Show resolved Hide resolved
)

assert respx_mock.call_count == len(keys)
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ..api.errors.validation_error import http422_error_handler
from ..meta import API_VERSION, API_VTAG, PROJECT_NAME, SUMMARY
from ..modules import (
api_keys_manager,
catalog,
comp_scheduler,
dask_clients_pool,
Expand Down Expand Up @@ -166,6 +167,7 @@ def init_app(settings: AppSettings | None = None) -> FastAPI:

if dynamic_scheduler_enabled:
dynamic_sidecar.setup(app)
api_keys_manager.setup(app)

if (
settings.DIRECTOR_V2_COMPUTATIONAL_BACKEND.COMPUTATIONAL_BACKEND_DASK_CLIENT_ENABLED
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from uuid import uuid5

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.products import ProductName
from models_library.projects_nodes_io import NodeID
from models_library.rabbitmq_basic_types import RPCMethodName
from models_library.users import UserID
from pydantic import parse_obj_as
from servicelib.rabbitmq import RabbitMQRPCClient

from ..utils.distributed_identifer import BaseDistributedIdentifierManager
from .rabbitmq import get_rabbitmq_rpc_client


class APIKeysManager(BaseDistributedIdentifierManager[str, ApiKeyGet]):
def __init__(self, app: FastAPI) -> None:
self.GET_OR_CREATE_INJECTS_IDENTIFIER = True
self.app = app

@property
def rpc_client(self) -> RabbitMQRPCClient:
return get_rabbitmq_rpc_client(self.app)

# pylint:disable=arguments-differ

async def get(
self, identifier: str, product_name: ProductName, user_id: UserID
) -> ApiKeyGet | None:
result = await self.rpc_client.request(
WEBSERVER_RPC_NAMESPACE,
parse_obj_as(RPCMethodName, "api_key_get"),
product_name=product_name,
user_id=user_id,
name=identifier,
)
return parse_obj_as(ApiKeyGet | None, result)

async def create(
self, identifier: str, product_name: ProductName, user_id: UserID
) -> tuple[str, ApiKeyGet]:
result = await self.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=identifier, expiration=None),
)
return identifier, ApiKeyGet.parse_obj(result)

async def destroy(
self, identifier: str, product_name: ProductName, user_id: UserID
) -> None:
await self.rpc_client.request(
WEBSERVER_RPC_NAMESPACE,
parse_obj_as(RPCMethodName, "delete_api_keys"),
product_name=product_name,
user_id=user_id,
name=identifier,
)


async def get_or_create_api_key(
app: FastAPI, *, product_name: ProductName, user_id: UserID, node_id: NodeID
) -> ApiKeyGet:
api_keys_manager = _get_api_keys_manager(app)
display_name = _get_api_key_name(node_id)

key_data: ApiKeyGet | None = await api_keys_manager.get(
identifier=display_name, product_name=product_name, user_id=user_id
)
if key_data is None:
_, key_data = await api_keys_manager.create(
identifier=display_name, product_name=product_name, user_id=user_id
)

return key_data


async def safe_remove(
app: FastAPI, *, node_id: NodeID, product_name: ProductName, user_id: UserID
) -> None:
api_keys_manager = _get_api_keys_manager(app)
display_name = _get_api_key_name(node_id)

await api_keys_manager.remove(
identifier=display_name, product_name=product_name, user_id=user_id
)


def _get_api_key_name(node_id: NodeID) -> str:
obfuscated_node_id = uuid5(node_id, f"{node_id}")
return f"_auto_{obfuscated_node_id}"


def _get_api_keys_manager(app: FastAPI) -> APIKeysManager:
api_keys_manager: APIKeysManager = app.state.api_keys_manager
return api_keys_manager


def setup(app: FastAPI) -> None:
async def on_startup() -> None:
app.state.api_keys_manager = APIKeysManager(app)

app.add_event_handler("startup", on_startup)
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

from ...core.dynamic_services_settings.egress_proxy import EgressProxySettings
from ...modules.osparc_variables_substitutions import (
resolve_and_substitute_service_lifetime_variables_in_specs,
resolve_and_substitute_session_variables_in_model,
resolve_and_substitute_session_variables_in_specs,
substitute_vendor_secrets_in_model,
Expand Down Expand Up @@ -376,11 +377,19 @@ async def assemble_spec( # pylint: disable=too-many-arguments # noqa: PLR0913
app=app,
specs=service_spec,
user_id=user_id,
safe=True,
product_name=product_name,
project_id=project_id,
node_id=node_id,
)
service_spec = await resolve_and_substitute_service_lifetime_variables_in_specs(
app=app,
specs=service_spec,
# NOTE: at this point all OsparcIdentifiers have to be replaced
# an error will be raised otherwise
safe=False,
safe=True,
product_name=product_name,
project_id=project_id,
user_id=user_id,
node_id=node_id,
)

Expand Down
Loading
Loading