Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
b85facd
add bcrypt req
giancarloromeo Jan 24, 2025
e824932
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Feb 5, 2025
db3c5f3
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Mar 10, 2025
2502d80
remove bcrypt dependency
giancarloromeo Mar 11, 2025
3b69cee
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Mar 11, 2025
d96ea56
unify create api key
giancarloromeo Mar 12, 2025
181540b
remove unused
giancarloromeo Mar 12, 2025
62e0fb7
check hashed secret
giancarloromeo Mar 12, 2025
41acdaf
Merge remote-tracking branch 'upstream/master' into is6880/hash-api-k…
giancarloromeo Mar 12, 2025
612f645
fix hashed secret check
giancarloromeo Mar 12, 2025
90cfa71
refactoring
giancarloromeo Mar 12, 2025
08b9d59
fix mock
giancarloromeo Mar 12, 2025
32e9e89
fix api_key faker
giancarloromeo Mar 12, 2025
0395f71
fix faker
giancarloromeo Mar 12, 2025
89f48be
fix mocker
giancarloromeo Mar 12, 2025
2cb13ce
refactor test
giancarloromeo Mar 12, 2025
8ca8936
add index
giancarloromeo Mar 12, 2025
c1ed671
update query
giancarloromeo Mar 12, 2025
66eb011
update query
giancarloromeo Mar 12, 2025
0780f07
update query
giancarloromeo Mar 12, 2025
28ff3c3
add conflict check
giancarloromeo Mar 13, 2025
a4eff3e
add migration script
giancarloromeo Mar 13, 2025
6b77a6a
add existing secrets hash script
giancarloromeo Mar 13, 2025
fd94dcc
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Mar 13, 2025
6f5b906
add comment
giancarloromeo Mar 13, 2025
f673127
Merge branch 'is6880/hash-api-key-secret' of github.com:giancarlorome…
giancarloromeo Mar 13, 2025
d761450
Merge branch 'ITISFoundation:master' into is6880/hash-api-key-secret
giancarloromeo Mar 17, 2025
a0f8260
add key deletion
giancarloromeo Mar 17, 2025
9f4cf34
rename
giancarloromeo Mar 17, 2025
9f95970
fix import
giancarloromeo Mar 17, 2025
f470c49
update comment
giancarloromeo Mar 17, 2025
84894ad
fix endpoint
giancarloromeo Mar 17, 2025
a42e986
fix
giancarloromeo Mar 17, 2025
e13969c
always raise on conflict
giancarloromeo Mar 17, 2025
8a830ab
fix tests
giancarloromeo Mar 18, 2025
6bde4b6
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Mar 18, 2025
c3264b6
update script deps
giancarloromeo Mar 18, 2025
541342e
fix revises
giancarloromeo Mar 18, 2025
4b47f53
fix test
giancarloromeo Mar 18, 2025
12e0c7b
use model_json_schema
giancarloromeo Mar 18, 2025
00619ea
continue
giancarloromeo Mar 21, 2025
4c70476
Merge remote-tracking branch 'upstream/master' into is6880/hash-api-k…
giancarloromeo Mar 21, 2025
1c9df86
fix script
giancarloromeo Mar 21, 2025
62f0faf
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Apr 2, 2025
8f6bb8b
typecheck
giancarloromeo Apr 2, 2025
3d39b4c
remove deps
giancarloromeo Apr 2, 2025
d38c8a5
fix delete by key
giancarloromeo Apr 2, 2025
470d6a8
add product name check
giancarloromeo Apr 2, 2025
5186120
fix create
giancarloromeo Apr 2, 2025
4a554d5
Merge remote-tracking branch 'upstream/master' into is6880/hash-api-k…
giancarloromeo Apr 3, 2025
4cd7bdd
fix repo
giancarloromeo Apr 3, 2025
5dc3b4a
Merge remote-tracking branch 'upstream/master' into is6880/hash-api-k…
giancarloromeo Apr 3, 2025
3793ff2
update script
giancarloromeo Apr 3, 2025
fe5d750
fix params
giancarloromeo Apr 3, 2025
8ff8a1b
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Apr 10, 2025
d3f5dd3
update script
giancarloromeo Apr 10, 2025
bdb1264
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Apr 17, 2025
bda3db2
fix delete method
giancarloromeo Apr 17, 2025
036bbcf
Update services/api-server/tests/unit/_with_db/conftest.py
giancarloromeo Apr 17, 2025
3543d71
refactor
giancarloromeo Apr 17, 2025
1c8e2eb
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Apr 22, 2025
580ad26
Update packages/postgres-database/src/simcore_postgres_database/migra…
giancarloromeo Apr 22, 2025
efd20f7
Merge remote-tracking branch 'upstream/master' into is6880/hash-api-k…
giancarloromeo Apr 22, 2025
e071e87
Merge branch 'is6880/hash-api-key-secret' of github.com:giancarlorome…
giancarloromeo Apr 22, 2025
c4a54b9
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Apr 22, 2025
d901074
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Apr 22, 2025
f04db05
add doc
giancarloromeo Apr 22, 2025
5133ccf
fix import
giancarloromeo Apr 22, 2025
605a4d7
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Apr 22, 2025
d1da32a
rename
giancarloromeo Apr 22, 2025
f834310
Merge branch 'master' into is6880/hash-api-key-secret
giancarloromeo Apr 22, 2025
b338774
Merge remote-tracking branch 'upstream/master' into is6880/hash-api-k…
giancarloromeo Apr 22, 2025
24afffb
Merge branch 'is6880/hash-api-key-secret' of github.com:giancarlorome…
giancarloromeo Apr 22, 2025
f662580
remove cast
giancarloromeo Apr 22, 2025
94a7bad
update script
giancarloromeo Apr 23, 2025
ef6493d
typecheck
giancarloromeo Apr 23, 2025
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""hash exising api_secret data
Revision ID: 742123f0933a
Revises: b0c988e3f348
Create Date: 2025-03-13 09:39:43.895529+00:00
"""

from alembic import op

# revision identifiers, used by Alembic.
revision = "742123f0933a"
down_revision = "b0c988e3f348"
branch_labels = None
depends_on = None


def upgrade():
op.execute(
"""
UPDATE api_keys
SET api_secret = crypt(api_secret, gen_salt('bf', 10))
"""
)


def downgrade():
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""add index to api_key column

Revision ID: b0c988e3f348
Revises: 381336fa8001
Create Date: 2025-03-13 08:53:05.722855+00:00

"""

from alembic import op

# revision identifiers, used by Alembic.
revision = "b0c988e3f348"
down_revision = "381336fa8001"
branch_labels = None
depends_on = None


def upgrade():
op.create_index(op.f("ix_api_keys_api_key"), "api_keys", ["api_key"], unique=False)


def downgrade():
op.drop_index(op.f("ix_api_keys_api_key"), table_name="api_keys")
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
""" API keys to access public API
"""API keys to access public API
These keys grant the client authorization to the API resources
Expand All @@ -10,6 +10,7 @@
+--------+ +---------------+
"""

import sqlalchemy as sa
from sqlalchemy.sql import func

Expand Down Expand Up @@ -52,7 +53,7 @@
nullable=False,
doc="Identified product",
),
sa.Column("api_key", sa.String(), nullable=False),
sa.Column("api_key", sa.String(), nullable=False, index=True),
sa.Column("api_secret", sa.String(), nullable=False),
sa.Column(
"created",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ async def get_user(
self, api_key: str, api_secret: str
) -> UserAndProductTuple | None:
stmt = sa.select(tbl.api_keys.c.user_id, tbl.api_keys.c.product_name).where(
(tbl.api_keys.c.api_key == api_key)
& (tbl.api_keys.c.api_secret == api_secret),
(tbl.api_keys.c.api_key == api_key) # NOTE: keep order, api_key is indexed
& (
tbl.api_keys.c.api_secret
== sa.func.crypt(api_secret, tbl.api_keys.c.api_secret)
)
)
result: UserAndProductTuple | None = None
try:
Expand Down
24 changes: 16 additions & 8 deletions services/api-server/tests/unit/_with_db/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,31 +255,39 @@ async def create_fake_api_keys(
create_product_names: Callable[[PositiveInt], AsyncGenerator[str, None]],
) -> AsyncGenerator[Callable[[PositiveInt], AsyncGenerator[ApiKeyInDB, None]], None]:
async def _generate_fake_api_key(n: PositiveInt):
users = create_user_ids(n)
products = create_product_names(n)
users, products = create_user_ids(n), create_product_names(n)
excluded_column = "api_secret"
returning_cols = [col for col in api_keys.c if col.name != excluded_column]

for _ in range(n):
product = await anext(products)
user = await anext(users)
api_key = random_api_key(product, user)
plain_api_secret = api_key.pop("api_secret")
result = await connection.execute(
api_keys.insert()
.values(**random_api_key(product, user))
.returning(sa.literal_column("*"))
.values(
api_secret=sa.func.crypt(plain_api_secret, sa.func.gen_salt("bf")),
**api_key,
)
.returning(*returning_cols)
)
row = await result.fetchone()
assert row
_generate_fake_api_key.row_ids.append(row.id)
yield ApiKeyInDB.model_validate(row)
yield ApiKeyInDB.model_validate({"api_secret": plain_api_secret, **row})

_generate_fake_api_key.row_ids = []
yield _generate_fake_api_key

for row_id in _generate_fake_api_key.row_ids:
await connection.execute(api_keys.delete().where(api_keys.c.id == row_id))
await connection.execute(
api_keys.delete().where(api_keys.c.id.in_(_generate_fake_api_key.row_ids))
)


@pytest.fixture
async def auth(
create_fake_api_keys: Callable[[PositiveInt], AsyncGenerator[ApiKeyInDB, None]]
create_fake_api_keys: Callable[[PositiveInt], AsyncGenerator[ApiKeyInDB, None]],
) -> httpx.BasicAuth:
"""overrides auth and uses access to real repositories instead of mocks"""
async for key in create_fake_api_keys(1):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import uuid
from typing import cast
from uuid import uuid5

from aiocache import cached # type: ignore[import-untyped]
Expand All @@ -8,7 +7,7 @@
from models_library.rpc.webserver.auth.api_keys import ApiKeyGet
from models_library.users import UserID

from ._api_auth_rpc import get_or_create_api_key_and_secret
from ._api_auth_rpc import create_api_key


def create_unique_api_name_for(product_name: ProductName, user_id: UserID) -> str:
Expand All @@ -24,14 +23,14 @@ def _cache_key(fct, *_, **kwargs):


@cached(ttl=3, key_builder=_cache_key)
async def _get_or_create_for(
async def _create_for(
app: FastAPI,
*,
product_name: ProductName,
user_id: UserID,
) -> ApiKeyGet:
display_name = create_unique_api_name_for(product_name, user_id)
return await get_or_create_api_key_and_secret(
return await create_api_key(
app,
user_id=user_id,
product_name=product_name,
Expand All @@ -40,34 +39,36 @@ async def _get_or_create_for(
)


async def get_or_create_user_api_key(
async def create_user_api_key(
app: FastAPI,
product_name: ProductName,
user_id: UserID,
) -> str:
data = await _get_or_create_for(
data: ApiKeyGet = await _create_for(
app,
product_name=product_name,
user_id=user_id,
)
return cast(str, data.api_key)
assert data.api_key
return data.api_key


async def get_or_create_user_api_secret(
async def create_user_api_secret(
app: FastAPI,
product_name: ProductName,
user_id: UserID,
) -> str:
data = await _get_or_create_for(
data: ApiKeyGet = await _create_for(
app,
product_name=product_name,
user_id=user_id,
)
return cast(str, data.api_secret)
assert data.api_secret
return data.api_secret


__all__: tuple[str, ...] = (
"get_or_create_user_api_key",
"get_or_create_user_api_secret",
"create_unique_api_name_for",
"create_user_api_key",
"create_user_api_secret",
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#


async def get_or_create_api_key_and_secret(
async def create_api_key(
app: FastAPI,
*,
product_name: ProductName,
Expand All @@ -26,10 +26,11 @@ async def get_or_create_api_key_and_secret(
rpc_client = get_rabbitmq_rpc_client(app)
result = await rpc_client.request(
WEBSERVER_RPC_NAMESPACE,
TypeAdapter(RPCMethodName).validate_python("get_or_create_api_key"),
TypeAdapter(RPCMethodName).validate_python("create_api_key"),
user_id=user_id,
display_name=display_name,
expiration=expiration,
product_name=product_name,
raise_on_conflict=False,
)
return ApiKeyGet.model_validate(result)
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
resolve_variables_from_context,
)
from ..db.repositories.services_environments import ServicesEnvironmentsRepository
from ._api_auth import get_or_create_user_api_key, get_or_create_user_api_secret
from ._api_auth import create_user_api_key, create_user_api_secret
from ._user import request_user_email, request_user_role

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -130,11 +130,9 @@ def create(cls, app: FastAPI):

table.register_from_handler("OSPARC_VARIABLE_USER_EMAIL")(request_user_email)
table.register_from_handler("OSPARC_VARIABLE_USER_ROLE")(request_user_role)
table.register_from_handler("OSPARC_VARIABLE_API_KEY")(
get_or_create_user_api_key
)
table.register_from_handler("OSPARC_VARIABLE_API_KEY")(create_user_api_key)
table.register_from_handler("OSPARC_VARIABLE_API_SECRET")(
get_or_create_user_api_secret
create_user_api_secret
)

_logger.debug(
Expand Down
2 changes: 1 addition & 1 deletion services/director-v2/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ async def _create(

# mocks RPC interface
mocker.patch(
"simcore_service_director_v2.modules.osparc_variables._api_auth.get_or_create_api_key_and_secret",
"simcore_service_director_v2.modules.osparc_variables._api_auth.create_api_key",
side_effect=_create,
autospec=True,
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from datetime import timedelta

from aiohttp import web
from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE
from models_library.api_schemas_webserver.auth import ApiKeyCreateRequest
Expand All @@ -23,13 +21,15 @@ async def create_api_key(
user_id: UserID,
product_name: ProductName,
api_key: ApiKeyCreateRequest,
raise_on_conflict: bool = True,
) -> ApiKeyGet:
created_api_key: ApiKey = await _service.create_api_key(
app,
user_id=user_id,
product_name=product_name,
display_name=api_key.display_name,
expiration=api_key.expiration,
raise_on_conflict=raise_on_conflict,
)

return ApiKeyGet.model_validate(created_api_key)
Expand All @@ -52,25 +52,6 @@ async def get_api_key(
return ApiKeyGet.model_validate(api_key)


@router.expose()
async def get_or_create_api_key(
app: web.Application,
*,
user_id: UserID,
product_name: ProductName,
display_name: str,
expiration: timedelta | None = None,
) -> ApiKeyGet:
api_key: ApiKey = await _service.get_or_create_api_key(
app,
user_id=user_id,
product_name=product_name,
display_name=display_name,
expiration=expiration,
)
return ApiKeyGet.model_validate(api_key)


@router.expose()
async def delete_api_key(
app: web.Application,
Expand Down
Loading
Loading