Skip to content

Commit 62565ba

Browse files
πŸ› Update API keys uniqueness constraint (πŸ—ƒοΈ) (#8363)
1 parent 57d948f commit 62565ba

File tree

4 files changed

+113
-56
lines changed

4 files changed

+113
-56
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Update api-keys uniqueness constraint
2+
3+
Revision ID: 7e92447558e0
4+
Revises: 06eafd25d004
5+
Create Date: 2025-09-12 09:56:45.164921+00:00
6+
7+
"""
8+
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "7e92447558e0"
13+
down_revision = "06eafd25d004"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.drop_constraint("display_name_userid_uniqueness", "api_keys", type_="unique")
21+
op.create_unique_constraint(
22+
"display_name_userid_product_name_uniqueness",
23+
"api_keys",
24+
["display_name", "user_id", "product_name"],
25+
)
26+
# ### end Alembic commands ###
27+
28+
29+
def downgrade():
30+
# ### commands auto generated by Alembic - please adjust! ###
31+
op.drop_constraint(
32+
"display_name_userid_product_name_uniqueness", "api_keys", type_="unique"
33+
)
34+
op.create_unique_constraint(
35+
"display_name_userid_uniqueness", "api_keys", ["display_name", "user_id"]
36+
)
37+
# ### end Alembic commands ###

β€Žpackages/postgres-database/src/simcore_postgres_database/models/api_keys.pyβ€Ž

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@
7575
"If set to NULL then the key does not expire.",
7676
),
7777
sa.UniqueConstraint(
78-
"display_name", "user_id", name="display_name_userid_uniqueness"
78+
"display_name",
79+
"user_id",
80+
"product_name",
81+
name="display_name_userid_product_name_uniqueness",
7982
),
8083
)
8184

β€Žservices/web/server/src/simcore_service_webserver/api_keys/_repository.pyβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async def create_api_key(
4747
expires_at=(sa.func.now() + expiration) if expiration else None,
4848
)
4949
.on_conflict_do_update(
50-
index_elements=["user_id", "display_name"],
50+
index_elements=["user_id", "display_name", "product_name"],
5151
set_={
5252
"api_key": api_key,
5353
"api_secret": _hash_secret(api_secret),

β€Žservices/web/server/tests/unit/with_dbs/01/test_api_keys.pyβ€Ž

Lines changed: 71 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
# pylint: disable=too-many-arguments
55

66
import asyncio
7-
from collections.abc import AsyncIterable
7+
from collections.abc import AsyncIterable, Awaitable, Callable
88
from datetime import timedelta
99
from http import HTTPStatus
10-
from http.client import HTTPException
1110

1211
import pytest
1312
import tenacity
@@ -39,68 +38,50 @@
3938

4039

4140
@pytest.fixture
42-
async def fake_user_api_keys(
41+
async def fake_api_key_factory(
4342
client: TestClient,
4443
logged_user: UserInfoDict,
4544
osparc_product_name: ProductName,
4645
faker: Faker,
47-
) -> AsyncIterable[list[ApiKey]]:
46+
) -> AsyncIterable[Callable[..., Awaitable[ApiKey]]]:
4847
assert client.app
4948

50-
api_keys: list[ApiKey] = [
51-
await _repository.create_api_key(
49+
created_keys: list[tuple[ApiKey, ProductName]] = []
50+
51+
async def _create(
52+
*,
53+
product_name: ProductName | None = None,
54+
display_name: str | None = None,
55+
expiration=None,
56+
api_key: str | None = None,
57+
api_secret: str | None = None,
58+
) -> ApiKey:
59+
final_product_name = product_name or osparc_product_name
60+
final_display_name = display_name or faker.pystr()
61+
final_api_key = api_key or faker.pystr()
62+
final_api_secret = api_secret or faker.pystr()
63+
64+
created_key = await _repository.create_api_key(
5265
client.app,
5366
user_id=logged_user["id"],
54-
product_name=osparc_product_name,
55-
display_name=faker.pystr(),
56-
expiration=None,
57-
api_key=faker.pystr(),
58-
api_secret=faker.pystr(),
67+
product_name=final_product_name,
68+
display_name=final_display_name,
69+
expiration=expiration,
70+
api_key=final_api_key,
71+
api_secret=final_api_secret,
5972
)
60-
for _ in range(5)
61-
]
62-
63-
yield api_keys
64-
65-
for api_key in api_keys:
66-
await _repository.delete_api_key(
67-
client.app,
68-
api_key_id=api_key.id,
69-
user_id=logged_user["id"],
70-
product_name=osparc_product_name,
71-
)
72-
73-
74-
@pytest.fixture
75-
async def fake_auto_api_keys(
76-
client: TestClient,
77-
logged_user: UserInfoDict,
78-
osparc_product_name: ProductName,
79-
faker: Faker,
80-
) -> AsyncIterable[list[ApiKey]]:
81-
assert client.app
8273

83-
api_keys: list[ApiKey] = [
84-
await _repository.create_api_key(
85-
client.app,
86-
user_id=logged_user["id"],
87-
product_name=osparc_product_name,
88-
display_name=API_KEY_AUTOGENERATED_DISPLAY_NAME_PREFIX + faker.pystr(),
89-
expiration=None,
90-
api_key=API_KEY_AUTOGENERATED_KEY_PREFIX + faker.pystr(),
91-
api_secret=faker.pystr(),
92-
)
93-
for _ in range(5)
94-
]
74+
created_keys.append((created_key, final_product_name))
75+
return created_key
9576

96-
yield api_keys
77+
yield _create
9778

98-
for api_key in api_keys:
79+
for api_key, product_name in created_keys:
9980
await _repository.delete_api_key(
10081
client.app,
10182
api_key_id=api_key.id,
10283
user_id=logged_user["id"],
103-
product_name=osparc_product_name,
84+
product_name=product_name,
10485
)
10586

10687

@@ -123,16 +104,18 @@ def _get_user_access_parametrizations(expected_authed_status_code):
123104
async def test_list_api_keys(
124105
disabled_setup_garbage_collector: MockType,
125106
client: TestClient,
126-
fake_user_api_keys: list[ApiKey],
107+
fake_api_key_factory: Callable[..., Awaitable[ApiKey]],
127108
logged_user: UserInfoDict,
128109
user_role: UserRole,
129110
expected: HTTPStatus,
130111
):
112+
fake_api_keys = [await fake_api_key_factory() for _ in range(10)]
113+
131114
resp = await client.get("/v0/auth/api-keys")
132115
data, errors = await assert_status(resp, expected)
133116

134117
if not errors:
135-
assert len(data) == len(fake_user_api_keys)
118+
assert len(data) == len(fake_api_keys)
136119

137120

138121
@pytest.mark.parametrize(
@@ -142,11 +125,20 @@ async def test_list_api_keys(
142125
async def test_list_auto_api_keys(
143126
disabled_setup_garbage_collector: MockType,
144127
client: TestClient,
145-
fake_auto_api_keys: list[ApiKey],
128+
fake_api_key_factory: Callable[..., Awaitable[ApiKey]],
146129
logged_user: UserInfoDict,
147130
user_role: UserRole,
148131
expected: HTTPStatus,
132+
faker: Faker,
149133
):
134+
fake_auto_api_keys = [
135+
await fake_api_key_factory(
136+
api_key=API_KEY_AUTOGENERATED_KEY_PREFIX + faker.pystr(),
137+
display_name=API_KEY_AUTOGENERATED_DISPLAY_NAME_PREFIX + faker.pystr(),
138+
)
139+
for _ in range(10)
140+
]
141+
150142
resp = await client.get(
151143
"/v0/auth/api-keys", params={"includeAutogenerated": "true"}
152144
)
@@ -203,19 +195,44 @@ async def test_create_api_key(
203195
async def test_delete_api_keys(
204196
disabled_setup_garbage_collector: MockType,
205197
client: TestClient,
206-
fake_user_api_keys: list[ApiKey],
198+
fake_api_key_factory: Callable[..., Awaitable[ApiKey]],
207199
logged_user: UserInfoDict,
208200
user_role: UserRole,
209201
expected: HTTPStatus,
210202
):
203+
fake_api_keys = [await fake_api_key_factory() for _ in range(10)]
204+
211205
resp = await client.delete("/v0/auth/api-keys/0")
212206
await assert_status(resp, expected)
213207

214-
for api_key in fake_user_api_keys:
208+
for api_key in fake_api_keys:
215209
resp = await client.delete(f"/v0/auth/api-keys/{api_key.id}")
216210
await assert_status(resp, expected)
217211

218212

213+
@pytest.mark.parametrize(
214+
"user_role,expected",
215+
_get_user_access_parametrizations(status.HTTP_200_OK),
216+
)
217+
async def test_create_api_keys_same_display_name_different_products(
218+
disabled_setup_garbage_collector: MockType,
219+
client: TestClient,
220+
fake_api_key_factory: Callable[..., Awaitable[ApiKey]],
221+
logged_user: UserInfoDict,
222+
app_products_names: list[str],
223+
user_role: UserRole,
224+
expected: HTTPStatus,
225+
):
226+
display_name = "foo"
227+
228+
created_keys = [
229+
await fake_api_key_factory(display_name=display_name, product_name=product_name)
230+
for product_name in app_products_names
231+
]
232+
233+
assert len(created_keys) == len(app_products_names)
234+
235+
219236
EXPIRATION_WAIT_FACTOR = 1.2
220237

221238

@@ -285,7 +302,7 @@ async def test_get_not_existing_api_key(
285302
client: TestClient,
286303
logged_user: UserInfoDict,
287304
user_role: UserRole,
288-
expected: HTTPException,
305+
expected: HTTPStatus,
289306
):
290307
resp = await client.get("/v0/auth/api-keys/42")
291308
data, errors = await assert_status(resp, expected)

0 commit comments

Comments
Β (0)