Skip to content

Commit 7385de9

Browse files
committed
Adding api-keys handlers
1 parent 2941e63 commit 7385de9

File tree

3 files changed

+196
-0
lines changed

3 files changed

+196
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import logging
2+
import uuid as uuidlib
3+
from typing import Dict
4+
5+
import sqlalchemy as sa
6+
from aiohttp import web
7+
8+
import simcore_postgres_database.webserver_models as orm
9+
from servicelib.application_keys import APP_DB_ENGINE_KEY
10+
from servicelib.aiopg_utils import DatabaseError
11+
12+
from .decorators import RQT_USERID_KEY, login_required
13+
from .utils import get_random_string
14+
15+
log = logging.getLogger(__name__)
16+
17+
18+
def generate_api_credentials() -> Dict[str, str]:
19+
credentials: Dict = dict.fromkeys(("api_key", "api_secret"), "")
20+
for name in credentials:
21+
value = get_random_string(20)
22+
credentials[name] = str(uuidlib.uuid5(uuidlib.NAMESPACE_DNS, value))
23+
return credentials
24+
25+
26+
class CRUD:
27+
# pylint: disable=no-value-for-parameter
28+
29+
def __init__(self, request: web.Request):
30+
self.engine = request.app.get(APP_DB_ENGINE_KEY)
31+
self.userid: int = request.get(RQT_USERID_KEY, -1)
32+
33+
async def list_api_key_names(self):
34+
async with self.engine.acquire() as conn:
35+
stmt = orm.api_keys.select([orm.api_keys.c.display_name]).where(
36+
orm.users.c.user_id == self.userid
37+
)
38+
39+
res = await conn.execute(stmt)
40+
rows = await res.fetchall()
41+
return list(rows)
42+
43+
async def create(self, name: str, *, api_key: str, api_secret: str):
44+
async with self.engine.acquire() as conn:
45+
stmt = orm.api_keys.insert().values(
46+
display_name=name,
47+
user_id=self.userid,
48+
api_key=api_key,
49+
api_secret=api_secret,
50+
)
51+
await conn.execute(stmt)
52+
53+
async def delete_api_key(self, name: str):
54+
async with self.engine.acquire() as conn:
55+
stmt = orm.api_keys.delete().where(
56+
sa.and_(
57+
orm.users.c.user_id == self.userid,
58+
orm.api_keys.c.display_name == name,
59+
)
60+
)
61+
await conn.execute(stmt)
62+
63+
64+
@login_required
65+
async def list_api_keys(request: web.Request):
66+
"""
67+
GET /auth/api-keys
68+
"""
69+
crud = CRUD(request)
70+
names = await crud.list_api_key_names()
71+
return names
72+
73+
74+
@login_required
75+
async def create_api_key(request: web.Request):
76+
"""
77+
POST /auth/api-keys
78+
"""
79+
body = await request.json()
80+
display_name = body.get("display_name")
81+
82+
credentials = generate_api_credentials()
83+
try:
84+
crud = CRUD(request)
85+
await crud.create(display_name, **credentials)
86+
except DatabaseError as err:
87+
log.warning("Failed to create API key %d", display_name, exc_info=err)
88+
raise web.HTTPBadRequest(reason="Invalid API key name: already exists")
89+
90+
return {
91+
"display_name": display_name,
92+
"api_key": credentials["api_key"],
93+
"api_secret": credentials["api_secret"],
94+
}
95+
96+
97+
@login_required
98+
async def delete_api_key(request: web.Request):
99+
"""
100+
DELETE /auth/api-keys
101+
"""
102+
103+
body = await request.json()
104+
display_name = body.get("display_name")
105+
106+
try:
107+
crud = CRUD(request)
108+
await crud.delete_api_key(display_name)
109+
except DatabaseError as err:
110+
log.warning(
111+
"Failed to delete API key %d. Ignoring error", display_name, exc_info=err
112+
)
113+
114+
raise web.HTTPNoContent

services/web/server/src/simcore_service_webserver/login/routes.py

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from servicelib.rest_routing import iter_path_operations, map_handlers_with_operations
1414

1515
from . import handlers as login_handlers
16+
from . import api_keys_handlers
1617

1718
log = logging.getLogger(__name__)
1819

@@ -42,6 +43,9 @@ def include_path(tuple_object):
4243
"auth_change_email": login_handlers.change_email,
4344
"auth_change_password": login_handlers.change_password,
4445
"auth_confirmation": login_handlers.email_confirmation,
46+
"create_api_key": api_keys_handlers.create_api_key,
47+
"delete_api_key": api_keys_handlers.delete_api_key,
48+
"list_api_keys": api_keys_handlers.list_api_keys,
4549
}
4650

4751
routes = map_handlers_with_operations(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# pylint:disable=unused-variable
2+
# pylint:disable=unused-argument
3+
# pylint:disable=redefined-outer-name
4+
from aiohttp import web
5+
import pytest
6+
7+
from pytest_simcore.helpers.utils_assert import assert_status
8+
from pytest_simcore.helpers.utils_login import LoggedUser
9+
from simcore_service_webserver.db_models import UserRole
10+
11+
12+
@pytest.fixture()
13+
async def logged_user(client, user_role: UserRole):
14+
""" adds a user in db and logs in with client
15+
16+
NOTE: `user_role` fixture is defined as a parametrization below!!!
17+
"""
18+
async with LoggedUser(
19+
client,
20+
{"role": user_role.name},
21+
check_if_succeeds=user_role != UserRole.ANONYMOUS,
22+
) as user:
23+
print("-----> logged in user", user_role)
24+
yield user
25+
print("<----- logged out user", user_role)
26+
27+
28+
@pytest.mark.parametrize(
29+
"user_role,expected",
30+
[
31+
(UserRole.ANONYMOUS, web.HTTPUnauthorized),
32+
(UserRole.GUEST, web.HTTPOk),
33+
(UserRole.USER, web.HTTPOk),
34+
(UserRole.TESTER, web.HTTPOk),
35+
],
36+
)
37+
async def test_create_api_keys(client, logged_user, user_role, expected):
38+
resp = await client.post("/v0/auth/api-keys", json={"display_name": "foo"})
39+
40+
data, errors = await assert_status(resp, expected)
41+
42+
if not errors:
43+
client.app
44+
45+
46+
@pytest.mark.parametrize(
47+
"user_role,expected",
48+
[
49+
(UserRole.ANONYMOUS, web.HTTPUnauthorized),
50+
(UserRole.GUEST, web.HTTPOk),
51+
(UserRole.USER, web.HTTPOk),
52+
(UserRole.TESTER, web.HTTPOk),
53+
],
54+
)
55+
async def test_list_api_keys(client, logged_user, user_role, expected):
56+
resp = await client.get("/v0/auth/api-keys")
57+
data, errors = await assert_status(resp, expected)
58+
59+
if not errors:
60+
assert not data
61+
62+
with UserApiKeys(client.app, logged_user.user.user_id, ['foo', 'bar', 'beta']):
63+
resp = await client.get("/v0/auth/api-keys")
64+
data, _ = await assert_status(resp, expected)
65+
assert data == ['foo', 'bar', 'beta']
66+
67+
@pytest.mark.parametrize(
68+
"user_role,expected",
69+
[
70+
(UserRole.ANONYMOUS, web.HTTPUnauthorized),
71+
(UserRole.GUEST, web.HTTPNoContent),
72+
(UserRole.USER, web.HTTPNoContent),
73+
(UserRole.TESTER, web.HTTPNoContent),
74+
],
75+
)
76+
async def test_delete_api_keys(client, logged_user, user_role, expected):
77+
resp = await client.delete("/v0/auth/api-keys", json={"display_name": "foo"})
78+
data, errors = await assert_status(resp, expected)

0 commit comments

Comments
 (0)