Skip to content

Commit f3d1c43

Browse files
committed
fix: correct a rebase merge
1 parent 782dcff commit f3d1c43

File tree

9 files changed

+156
-40
lines changed

9 files changed

+156
-40
lines changed

DEVELOPING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ function if you prefer to keep your favorite shell.
117117
You can run style checks using `make style_checks`.
118118
To run the test suite, use `make tests` (you likely need to run in the devcontainer for this to work, as it needs some
119119
surrounding services to run).
120-
* Run a specific test e.g.: `poetry run pytest test/bases/renku_data_services/data_api/test_storage_v2.py::test_storage_v2_create_openbis_secret`
120+
* Run a specific test e.g.: `poetry run pytest test/bases/renku_data_services/data_api/test_data_connectors.py::test_create_openbis_data_connector`
121121
* Also run tests marked with `@pytest.mark.myskip`: `PYTEST_FORCE_RUN_MYSKIPS=1 make tests`
122122

123123
## Migrations

components/renku_data_services/data_connectors/blueprints.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""Data connectors blueprint."""
22

33
from dataclasses import dataclass
4+
from datetime import datetime
45
from typing import Any
56

67
from sanic import Request
78
from sanic.response import HTTPResponse, JSONResponse
89
from sanic_ext import validate
910
from ulid import ULID
1011

11-
from renku_data_services import base_models
12+
from renku_data_services import base_models, errors
1213
from renku_data_services.base_api.auth import (
1314
authenticate,
1415
only_authenticated,
@@ -31,6 +32,7 @@
3132
DataConnectorSecretRepository,
3233
)
3334
from renku_data_services.storage.rclone import RCloneValidator
35+
from renku_data_services.utils.core import get_openbis_pat
3436

3537

3638
@dataclass(kw_only=True)
@@ -310,10 +312,55 @@ async def _patch_secrets(
310312
user: base_models.APIUser,
311313
data_connector_id: ULID,
312314
body: apispec.DataConnectorSecretPatchList,
315+
validator: RCloneValidator,
313316
) -> JSONResponse:
314317
unsaved_secrets = validate_data_connector_secrets_patch(put=body)
318+
data_connector = await self.data_connector_repo.get_data_connector(
319+
user=user, data_connector_id=data_connector_id
320+
)
321+
storage = data_connector.storage
322+
provider = validator.providers[storage.storage_type]
323+
sensitive_lookup = [o.name for o in provider.options if o.sensitive]
324+
for secret in unsaved_secrets:
325+
if secret.name in sensitive_lookup:
326+
continue
327+
raise errors.ValidationError(
328+
message=f"The '{secret.name}' property is not marked sensitive and can not be saved in the secret "
329+
f"storage."
330+
)
331+
expiration_timestamp = None
332+
333+
if storage.storage_type == "openbis":
334+
335+
async def openbis_transform_session_token_to_pat() -> (
336+
tuple[list[models.DataConnectorSecretUpdate], datetime]
337+
):
338+
if len(unsaved_secrets) == 1 and unsaved_secrets[0].name == "session_token":
339+
if unsaved_secrets[0].value is not None:
340+
try:
341+
openbis_pat = await get_openbis_pat(
342+
storage.configuration["host"], unsaved_secrets[0].value
343+
)
344+
return (
345+
[models.DataConnectorSecretUpdate(name="session_token", value=openbis_pat[0])],
346+
openbis_pat[1],
347+
)
348+
except Exception as e:
349+
raise errors.ProgrammingError(message=str(e))
350+
raise errors.ValidationError(message="The openBIS session token must be a string value.")
351+
352+
raise errors.ValidationError(message="The openBIS storage has only one secret: session_token")
353+
354+
(
355+
unsaved_secrets,
356+
expiration_timestamp,
357+
) = await openbis_transform_session_token_to_pat()
358+
315359
secrets = await self.data_connector_secret_repo.patch_data_connector_secrets(
316-
user=user, data_connector_id=data_connector_id, secrets=unsaved_secrets
360+
user=user,
361+
data_connector_id=data_connector_id,
362+
secrets=unsaved_secrets,
363+
expiration_timestamp=expiration_timestamp,
317364
)
318365
return validated_json(
319366
apispec.DataConnectorSecretsList, [self._dump_data_connector_secret(secret) for secret in secrets]

components/renku_data_services/data_connectors/db.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import random
44
import string
55
from collections.abc import AsyncIterator, Callable
6+
from datetime import datetime
67
from typing import TypeVar, cast
78

89
from cryptography.hazmat.primitives.asymmetric import rsa
@@ -629,7 +630,11 @@ async def get_data_connector_secrets(
629630
return [secret.dump() for secret in secrets]
630631

631632
async def patch_data_connector_secrets(
632-
self, user: base_models.APIUser, data_connector_id: ULID, secrets: list[models.DataConnectorSecretUpdate]
633+
self,
634+
user: base_models.APIUser,
635+
data_connector_id: ULID,
636+
secrets: list[models.DataConnectorSecretUpdate],
637+
expiration_timestamp: datetime | None,
633638
) -> list[models.DataConnectorSecret]:
634639
"""Create, update or remove data connector secrets."""
635640
if user.id is None:
@@ -679,7 +684,9 @@ async def patch_data_connector_secrets(
679684

680685
if data_connector_secret_orm := existing_secrets_as_dict.get(name):
681686
data_connector_secret_orm.secret.update(
682-
encrypted_value=encrypted_value, encrypted_key=encrypted_key
687+
encrypted_value=encrypted_value,
688+
encrypted_key=encrypted_key,
689+
expiration_timestamp=expiration_timestamp,
683690
)
684691
else:
685692
secret_name = f"{data_connector.name[:45]} - {name[:45]}"
@@ -693,6 +700,7 @@ async def patch_data_connector_secrets(
693700
encrypted_value=encrypted_value,
694701
encrypted_key=encrypted_key,
695702
kind=SecretKind.storage,
703+
expiration_timestamp=expiration_timestamp,
696704
)
697705
data_connector_secret_orm = schemas.DataConnectorSecretORM(
698706
name=name,
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
"""add secret expiration timestamp
22
3-
Revision ID: f98f5bdbb532
3+
Revision ID: d19cf323482e
44
Revises: d1cdcbb2adc3
5-
Create Date: 2025-01-09 14:22:31.660503
5+
Create Date: 2025-01-09 15:53:52.947037
66
77
"""
88

99
import sqlalchemy as sa
1010
from alembic import op
1111

1212
# revision identifiers, used by Alembic.
13-
revision = "f98f5bdbb532"
13+
revision = "d19cf323482e"
1414
down_revision = "d1cdcbb2adc3"
1515
branch_labels = None
1616
depends_on = None

components/renku_data_services/storage/core.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

components/renku_data_services/utils/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ async def get_openbis_session_token(
105105
) -> str:
106106
"""Requests an openBIS session token with the user's login credentials."""
107107
login = {"method": "login", "params": [username, password], "id": "2", "jsonrpc": "2.0"}
108-
async with httpx.AsyncClient(verify=get_ssl_context()) as client:
108+
async with httpx.AsyncClient(verify=get_ssl_context(), timeout=5) as client:
109109
response = await client.post(_get_url(host), json=login, timeout=timeout)
110110
if response.status_code == 200:
111111
json: dict[str, str] = response.json()
@@ -126,7 +126,7 @@ async def get_openbis_pat(
126126
"""Requests an openBIS PAT with an openBIS session ID."""
127127
url = _get_url(host)
128128

129-
async with httpx.AsyncClient(verify=get_ssl_context()) as client:
129+
async with httpx.AsyncClient(verify=get_ssl_context(), timeout=5) as client:
130130
get_server_information = {"method": "getServerInformation", "params": [session_id], "id": "2", "jsonrpc": "2.0"}
131131
response = await client.post(url, json=get_server_information, timeout=timeout)
132132
if response.status_code == 200:

test/bases/renku_data_services/data_api/conftest.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,39 @@ async def create_data_connector_helper(
371371
return create_data_connector_helper
372372

373373

374+
@pytest_asyncio.fixture
375+
def create_openbis_data_connector(sanic_client: SanicASGITestClient, regular_user: UserInfo, user_headers):
376+
async def create_openbis_data_connector_helper(
377+
name: str, session_token: str, user: UserInfo | None = None, headers: dict[str, str] | None = None, **payload
378+
) -> Any:
379+
user = user or regular_user
380+
headers = headers or user_headers
381+
dc_payload = {
382+
"name": name,
383+
"description": "An openBIS data connector",
384+
"visibility": "private",
385+
"namespace": user.namespace.slug,
386+
"storage": {
387+
"configuration": {
388+
"type": "openbis",
389+
"host": "openbis-eln-lims.ethz.ch", # Public openBIS demo instance.
390+
"session_token": session_token,
391+
},
392+
"source_path": "/",
393+
"target_path": "my/target",
394+
},
395+
"keywords": ["keyword 1", "keyword.2", "keyword-3", "KEYWORD_4"],
396+
}
397+
dc_payload.update(payload)
398+
399+
_, response = await sanic_client.post("/api/data/data_connectors", headers=headers, json=dc_payload)
400+
401+
assert response.status_code == 201, response.text
402+
return response.json
403+
404+
return create_openbis_data_connector_helper
405+
406+
374407
@pytest_asyncio.fixture
375408
async def create_data_connector_and_link_project(
376409
sanic_client, regular_user, user_headers, admin_user, admin_headers, create_data_connector

test/bases/renku_data_services/data_api/test_data_connectors.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from sanic_testing.testing import SanicASGITestClient
33

44
from renku_data_services.users.models import UserInfo
5+
from renku_data_services.utils.core import get_openbis_session_token
56
from test.bases.renku_data_services.data_api.utils import merge_headers
67

78

@@ -1129,6 +1130,14 @@ async def test_patch_data_connector_secrets(
11291130
assert len(secrets) == 2
11301131
assert {s["name"] for s in secrets} == {"access_key_id", "secret_access_key"}
11311132

1133+
payload = [
1134+
{"name": "not_sensitive", "value": "not_sensitive_value"},
1135+
]
1136+
_, response = await sanic_client.patch(
1137+
f"/api/data/data_connectors/{data_connector_id}/secrets", headers=user_headers, json=payload
1138+
)
1139+
assert response.status_code == 422, response.json
1140+
11321141
# Check that the data connector is referenced from the first user secret
11331142
user_secret_id = secrets[0]["secret_id"]
11341143
_, response = await sanic_client.get(f"/api/data/user/secrets/{user_secret_id}", headers=user_headers)
@@ -1206,7 +1215,7 @@ async def test_patch_data_connector_secrets_add_and_remove_secrets(
12061215
payload = [
12071216
{"name": "access_key_id", "value": "new access key id value"},
12081217
{"name": "secret_access_key", "value": None},
1209-
{"name": "password", "value": "password"},
1218+
{"name": "sse_kms_key_id", "value": "password"},
12101219
]
12111220
_, response = await sanic_client.patch(
12121221
f"/api/data/data_connectors/{data_connector_id}/secrets", headers=user_headers, json=payload
@@ -1216,7 +1225,7 @@ async def test_patch_data_connector_secrets_add_and_remove_secrets(
12161225
assert response.json is not None
12171226
secrets = response.json
12181227
assert len(secrets) == 2
1219-
assert {s["name"] for s in secrets} == {"access_key_id", "password"}
1228+
assert {s["name"] for s in secrets} == {"access_key_id", "sse_kms_key_id"}
12201229
new_access_key_id_secret_id = next(filter(lambda s: s["name"] == "access_key_id", secrets), None)
12211230
assert new_access_key_id_secret_id == access_key_id_secret_id
12221231

@@ -1226,15 +1235,14 @@ async def test_patch_data_connector_secrets_add_and_remove_secrets(
12261235
assert response.json is not None
12271236
secrets = response.json
12281237
assert len(secrets) == 2
1229-
assert {s["name"] for s in secrets} == {"access_key_id", "password"}
1238+
assert {s["name"] for s in secrets} == {"access_key_id", "sse_kms_key_id"}
12301239

12311240
# Check the associated secrets
12321241
_, response = await sanic_client.get("/api/data/user/secrets", params={"kind": "storage"}, headers=user_headers)
1233-
12341242
assert response.status_code == 200
12351243
assert response.json is not None
12361244
assert len(response.json) == 2
1237-
assert {s["name"] for s in secrets} == {"access_key_id", "password"}
1245+
assert {s["name"] for s in secrets} == {"access_key_id", "sse_kms_key_id"}
12381246

12391247

12401248
@pytest.mark.asyncio
@@ -1274,6 +1282,51 @@ async def test_delete_data_connector_secrets(
12741282
assert response.json == [], response.json
12751283

12761284

1285+
@pytest.mark.myskip(1 == 1, reason="Depends on a remote openBIS host which may not always be available.")
1286+
@pytest.mark.asyncio
1287+
async def test_create_openbis_data_connector(sanic_client, create_openbis_data_connector, user_headers) -> None:
1288+
openbis_session_token = await get_openbis_session_token(
1289+
host="openbis-eln-lims.ethz.ch", # Public openBIS demo instance.
1290+
username="observer",
1291+
password="1234",
1292+
)
1293+
data_connector = await create_openbis_data_connector(
1294+
"openBIS data connector 1", session_token=openbis_session_token
1295+
)
1296+
data_connector_id = data_connector["id"]
1297+
1298+
payload = [
1299+
{"name": "session_token", "value": openbis_session_token},
1300+
]
1301+
_, response = await sanic_client.patch(
1302+
f"/api/data/data_connectors/{data_connector_id}/secrets", headers=user_headers, json=payload
1303+
)
1304+
assert response.status_code == 200, response.json
1305+
assert {s["name"] for s in response.json} == {"session_token"}
1306+
created_secret_ids = {s["secret_id"] for s in response.json}
1307+
assert len(created_secret_ids) == 1
1308+
assert response.json[0].keys() == {"secret_id", "name"}
1309+
1310+
1311+
@pytest.mark.myskip(1 == 1, reason="Depends on a remote openBIS host which may not always be available.")
1312+
@pytest.mark.asyncio
1313+
async def test_create_openbis_data_connector_with_invalid_session_token(
1314+
sanic_client, create_openbis_data_connector, user_headers
1315+
) -> None:
1316+
invalid_openbis_session_token = "1234"
1317+
data_connector = await create_openbis_data_connector("openBIS data connector 1", invalid_openbis_session_token)
1318+
data_connector_id = data_connector["id"]
1319+
1320+
payload = [
1321+
{"name": "session_token", "value": invalid_openbis_session_token},
1322+
]
1323+
_, response = await sanic_client.patch(
1324+
f"/api/data/data_connectors/{data_connector_id}/secrets", headers=user_headers, json=payload
1325+
)
1326+
assert response.status_code == 500, response.json
1327+
assert response.json["error"]["message"] == "An openBIS personal access token related request failed."
1328+
1329+
12771330
@pytest.mark.asyncio
12781331
async def test_get_project_permissions_unauthorized(
12791332
sanic_client, create_data_connector, admin_headers, admin_user, user_headers

test/bases/renku_data_services/data_api/test_storage_v2.py

Whitespace-only changes.

0 commit comments

Comments
 (0)