Skip to content

Commit c5820ef

Browse files
authored
feat: handle secrets for data connectors (#413)
Add support for saving and managing secrets for data connectors. Details: * Add API endpoints to list, update and delete saved secrets for a given data connector.
1 parent 7574d13 commit c5820ef

File tree

10 files changed

+497
-102
lines changed

10 files changed

+497
-102
lines changed

bases/renku_data_services/data_api/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def register_all_handlers(app: Sanic, config: Config) -> Sanic:
140140
url_prefix=url_prefix,
141141
data_connector_repo=config.data_connector_repo,
142142
data_connector_to_project_link_repo=config.data_connector_to_project_link_repo,
143+
data_connector_secret_repo=config.data_connector_secret_repo,
143144
authenticator=config.authenticator,
144145
)
145146
app.blueprint(

components/renku_data_services/app_config/config.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@
4444
ServerOptionsDefaults,
4545
generate_default_resource_pool,
4646
)
47-
from renku_data_services.data_connectors.db import DataConnectorProjectLinkRepository, DataConnectorRepository
47+
from renku_data_services.data_connectors.db import (
48+
DataConnectorProjectLinkRepository,
49+
DataConnectorRepository,
50+
DataConnectorSecretRepository,
51+
)
4852
from renku_data_services.db_config import DBConfig
4953
from renku_data_services.git.gitlab import DummyGitlabAPI, GitlabAPI
5054
from renku_data_services.k8s.clients import DummyCoreClient, DummySchedulingClient, K8sCoreClient, K8sSchedulingClient
@@ -180,6 +184,7 @@ class Config:
180184
_data_connector_to_project_link_repo: DataConnectorProjectLinkRepository | None = field(
181185
default=None, repr=False, init=False
182186
)
187+
_data_connector_secret_repo: DataConnectorSecretRepository | None = field(default=None, repr=False, init=False)
183188

184189
def __post_init__(self) -> None:
185190
spec_file = Path(renku_data_services.crc.__file__).resolve().parent / "api.spec.yaml"
@@ -427,6 +432,18 @@ def data_connector_to_project_link_repo(self) -> DataConnectorProjectLinkReposit
427432
)
428433
return self._data_connector_to_project_link_repo
429434

435+
@property
436+
def data_connector_secret_repo(self) -> DataConnectorSecretRepository:
437+
"""The DB adapter for data connector secrets."""
438+
if not self._data_connector_secret_repo:
439+
self._data_connector_secret_repo = DataConnectorSecretRepository(
440+
session_maker=self.db.async_session_maker,
441+
data_connector_repo=self.data_connector_repo,
442+
user_repo=self.kc_user_repo,
443+
secret_service_public_key=self.secrets_service_public_key,
444+
)
445+
return self._data_connector_secret_repo
446+
430447
@classmethod
431448
def from_env(cls, prefix: str = "") -> "Config":
432449
"""Create a config from environment variables."""

components/renku_data_services/data_connectors/api.spec.yaml

Lines changed: 33 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ paths:
244244
content:
245245
"application/json":
246246
schema:
247-
$ref: "#/components/schemas/CloudStorageSecretGetList"
247+
$ref: "#/components/schemas/DataConnectorSecretsList"
248248
"404":
249249
description: Storage was not found
250250
content:
@@ -255,21 +255,22 @@ paths:
255255
$ref: "#/components/responses/Error"
256256
tags:
257257
- data_connectors
258-
post:
258+
patch:
259259
summary: Save secrets for a data connector
260+
description: New secrets will be added and existing secrets will have their value updated. Using `null` as a value will remove the corresponding secret.
260261
requestBody:
261262
required: true
262263
content:
263264
application/json:
264265
schema:
265-
$ref: "#/components/schemas/CloudStorageSecretPostList"
266+
$ref: "#/components/schemas/DataConnectorSecretPatchList"
266267
responses:
267268
"201":
268269
description: The secrets for cloud storage were saved
269270
content:
270271
"application/json":
271272
schema:
272-
$ref: "#/components/schemas/CloudStorageSecretGetList"
273+
$ref: "#/components/schemas/DataConnectorSecretsList"
273274
default:
274275
$ref: "#/components/responses/Error"
275276
tags:
@@ -306,10 +307,6 @@ components:
306307
$ref: "#/components/schemas/Slug"
307308
storage:
308309
$ref: "#/components/schemas/CloudStorageCore"
309-
secrets:
310-
type: array
311-
items:
312-
$ref: "#/components/schemas/CloudStorageSecretGet"
313310
creation_date:
314311
$ref: "#/components/schemas/CreationDate"
315312
created_by:
@@ -498,67 +495,49 @@ components:
498495
$ref: "#/components/schemas/Ulid"
499496
required:
500497
- project_id
501-
CloudStorageSecretPost:
498+
DataConnectorSecretsList:
499+
description: A list of data connectors
500+
type: array
501+
items:
502+
$ref: "#/components/schemas/DataConnectorSecret"
503+
DataConnectorSecret:
504+
description: Information about a credential saved for a data connector
502505
type: object
503-
description: Data for storing secret for a storage field
504506
properties:
505507
name:
506-
type: string
507-
description: Name of the field to store credential for
508-
minLength: 1
509-
maxLength: 99
510-
value:
511-
$ref: "#/components/schemas/SecretValue"
508+
$ref: "#/components/schemas/DataConnectorSecretFieldName"
509+
secret_id:
510+
$ref: "#/components/schemas/Ulid"
512511
required:
513512
- name
514-
- value
515-
CloudStorageSecretPostList:
516-
description: List of storage secrets that are saved
517-
type: array
518-
items:
519-
$ref: "#/components/schemas/CloudStorageSecretPost"
520-
CloudStorageSecretGetList:
521-
description: List of storage secrets that are saved
513+
- secret_id
514+
DataConnectorSecretPatchList:
515+
description: List of secrets to be saved for a data connector
522516
type: array
523517
items:
524-
$ref: "#/components/schemas/CloudStorageSecretGet"
525-
CloudStorageSecretGet:
526-
type: object
527-
description: Data for saved storage secrets
518+
$ref: "#/components/schemas/DataConnectorSecretPatch"
519+
DataConnectorSecretPatch:
520+
description: Information about a credential to save for a data connector
528521
properties:
529522
name:
530-
type: string
531-
description: Name of the field to store credential for
532-
minLength: 1
533-
maxLength: 99
534-
secret_id:
535-
$ref: "#/components/schemas/Ulid"
523+
$ref: "#/components/schemas/DataConnectorSecretFieldName"
524+
value:
525+
$ref: "#/components/schemas/SecretValueNullable"
536526
required:
537527
- name
538-
- secret_id
539-
SecretValue:
528+
- value
529+
DataConnectorSecretFieldName:
530+
description: Name of the credential field
531+
type: string
532+
minLength: 1
533+
maxLength: 99
534+
example: "secret_key"
535+
SecretValueNullable:
540536
description: Secret value that can be any text
541537
type: string
542538
minLength: 1
543539
maxLength: 5000
544-
RCloneEntry:
545-
type: object
546-
description: Schema for a storage type in rclone, like S3 or Azure Blob Storage. Contains fields for that storage type.
547-
properties:
548-
name:
549-
type: string
550-
description: Human readable name of the provider
551-
description:
552-
type: string
553-
description: description of the provider
554-
prefix:
555-
type: string
556-
description: Machine readable name of the provider
557-
options:
558-
description: Fields/properties used for this storage.
559-
type: array
560-
items:
561-
$ref: "#/components/schemas/RCloneOption"
540+
nullable: true
562541
RCloneOption:
563542
type: object
564543
description: Single field on an RClone storage, like "remote" or "access_key_id"

components/renku_data_services/data_connectors/apispec.py

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# generated by datamodel-codegen:
22
# filename: api.spec.yaml
3-
# timestamp: 2024-09-17T13:48:49+00:00
3+
# timestamp: 2024-10-03T13:29:06+00:00
44

55
from __future__ import annotations
66

@@ -246,31 +246,11 @@ class DataConnectorToProjectLinkPost(BaseAPISpec):
246246
)
247247

248248

249-
class CloudStorageSecretPost(BaseAPISpec):
249+
class DataConnectorSecret(BaseAPISpec):
250250
name: str = Field(
251251
...,
252-
description="Name of the field to store credential for",
253-
max_length=99,
254-
min_length=1,
255-
)
256-
value: str = Field(
257-
...,
258-
description="Secret value that can be any text",
259-
max_length=5000,
260-
min_length=1,
261-
)
262-
263-
264-
class CloudStorageSecretPostList(RootModel[List[CloudStorageSecretPost]]):
265-
root: List[CloudStorageSecretPost] = Field(
266-
..., description="List of storage secrets that are saved"
267-
)
268-
269-
270-
class CloudStorageSecretGet(BaseAPISpec):
271-
name: str = Field(
272-
...,
273-
description="Name of the field to store credential for",
252+
description="Name of the credential field",
253+
example="secret_key",
274254
max_length=99,
275255
min_length=1,
276256
)
@@ -283,14 +263,19 @@ class CloudStorageSecretGet(BaseAPISpec):
283263
)
284264

285265

286-
class RCloneEntry(BaseAPISpec):
287-
name: Optional[str] = Field(None, description="Human readable name of the provider")
288-
description: Optional[str] = Field(None, description="description of the provider")
289-
prefix: Optional[str] = Field(
290-
None, description="Machine readable name of the provider"
266+
class DataConnectorSecretPatch(BaseAPISpec):
267+
name: str = Field(
268+
...,
269+
description="Name of the credential field",
270+
example="secret_key",
271+
max_length=99,
272+
min_length=1,
291273
)
292-
options: Optional[List[RCloneOption]] = Field(
293-
None, description="Fields/properties used for this storage."
274+
value: Optional[str] = Field(
275+
...,
276+
description="Secret value that can be any text",
277+
max_length=5000,
278+
min_length=1,
294279
)
295280

296281

@@ -337,7 +322,6 @@ class DataConnector(BaseAPISpec):
337322
pattern="^(?!.*\\.git$|.*\\.atom$|.*[\\-._][\\-._].*)[a-zA-Z0-9][a-zA-Z0-9\\-_.]*$",
338323
)
339324
storage: CloudStorageCore
340-
secrets: Optional[List[CloudStorageSecretGet]] = None
341325
creation_date: datetime = Field(
342326
...,
343327
description="The date and time the resource was created (in UTC and ISO-8601 format)",
@@ -450,9 +434,15 @@ class DataConnectorToProjectLinksList(RootModel[List[DataConnectorToProjectLink]
450434
)
451435

452436

453-
class CloudStorageSecretGetList(RootModel[List[CloudStorageSecretGet]]):
454-
root: List[CloudStorageSecretGet] = Field(
455-
..., description="List of storage secrets that are saved"
437+
class DataConnectorSecretsList(RootModel[List[DataConnectorSecret]]):
438+
root: List[DataConnectorSecret] = Field(
439+
..., description="A list of data connectors"
440+
)
441+
442+
443+
class DataConnectorSecretPatchList(RootModel[List[DataConnectorSecretPatch]]):
444+
root: List[DataConnectorSecretPatch] = Field(
445+
..., description="List of secrets to be saved for a data connector"
456446
)
457447

458448

components/renku_data_services/data_connectors/blueprints.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,21 @@
1515
)
1616
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
1717
from renku_data_services.base_api.etag import extract_if_none_match, if_match_required
18-
from renku_data_services.base_api.misc import validate_query
18+
from renku_data_services.base_api.misc import validate_body_root_model, validate_query
1919
from renku_data_services.base_api.pagination import PaginationRequest, paginate
2020
from renku_data_services.base_models.validation import validate_and_dump, validated_json
2121
from renku_data_services.data_connectors import apispec, models
2222
from renku_data_services.data_connectors.core import (
2323
dump_storage_with_sensitive_fields,
2424
validate_data_connector_patch,
25+
validate_data_connector_secrets_patch,
2526
validate_unsaved_data_connector,
2627
)
27-
from renku_data_services.data_connectors.db import DataConnectorProjectLinkRepository, DataConnectorRepository
28+
from renku_data_services.data_connectors.db import (
29+
DataConnectorProjectLinkRepository,
30+
DataConnectorRepository,
31+
DataConnectorSecretRepository,
32+
)
2833
from renku_data_services.storage.rclone import RCloneValidator
2934

3035

@@ -34,6 +39,7 @@ class DataConnectorsBP(CustomBlueprint):
3439

3540
data_connector_repo: DataConnectorRepository
3641
data_connector_to_project_link_repo: DataConnectorProjectLinkRepository
42+
data_connector_secret_repo: DataConnectorSecretRepository
3743
authenticator: base_models.Authenticator
3844

3945
def get_all(self) -> BlueprintFactoryResponse:
@@ -262,6 +268,64 @@ async def _get_all_data_connectors_links_to_project(
262268

263269
return "/projects/<project_id:ulid>/data_connector_links", ["GET"], _get_all_data_connectors_links_to_project
264270

271+
def get_secrets(self) -> BlueprintFactoryResponse:
272+
"""List all saved secrets for a data connector."""
273+
274+
@authenticate(self.authenticator)
275+
@only_authenticated
276+
async def _get_secrets(
277+
_: Request,
278+
user: base_models.APIUser,
279+
data_connector_id: ULID,
280+
) -> JSONResponse:
281+
secrets = await self.data_connector_secret_repo.get_data_connector_secrets(
282+
user=user, data_connector_id=data_connector_id
283+
)
284+
return validated_json(
285+
apispec.DataConnectorSecretsList, [self._dump_data_connector_secret(secret) for secret in secrets]
286+
)
287+
288+
return "/data_connectors/<data_connector_id:ulid>/secrets", ["GET"], _get_secrets
289+
290+
def patch_secrets(self) -> BlueprintFactoryResponse:
291+
"""Create, update or delete saved secrets for a data connector."""
292+
293+
@authenticate(self.authenticator)
294+
@only_authenticated
295+
@validate_body_root_model(json=apispec.DataConnectorSecretPatchList)
296+
async def _patch_secrets(
297+
_: Request,
298+
user: base_models.APIUser,
299+
data_connector_id: ULID,
300+
body: apispec.DataConnectorSecretPatchList,
301+
) -> JSONResponse:
302+
unsaved_secrets = validate_data_connector_secrets_patch(put=body)
303+
secrets = await self.data_connector_secret_repo.patch_data_connector_secrets(
304+
user=user, data_connector_id=data_connector_id, secrets=unsaved_secrets
305+
)
306+
return validated_json(
307+
apispec.DataConnectorSecretsList, [self._dump_data_connector_secret(secret) for secret in secrets]
308+
)
309+
310+
return "/data_connectors/<data_connector_id:ulid>/secrets", ["PATCH"], _patch_secrets
311+
312+
def delete_secrets(self) -> BlueprintFactoryResponse:
313+
"""Delete all saved secrets for a data connector."""
314+
315+
@authenticate(self.authenticator)
316+
@only_authenticated
317+
async def _delete_secrets(
318+
_: Request,
319+
user: base_models.APIUser,
320+
data_connector_id: ULID,
321+
) -> HTTPResponse:
322+
await self.data_connector_secret_repo.delete_data_connector_secrets(
323+
user=user, data_connector_id=data_connector_id
324+
)
325+
return HTTPResponse(status=204)
326+
327+
return "/data_connectors/<data_connector_id:ulid>/secrets", ["DELETE"], _delete_secrets
328+
265329
@staticmethod
266330
def _dump_data_connector(data_connector: models.DataConnector, validator: RCloneValidator) -> dict[str, Any]:
267331
"""Dumps a data connector for API responses."""
@@ -291,3 +355,11 @@ def _dump_data_connector_to_project_link(link: models.DataConnectorToProjectLink
291355
creation_date=link.creation_date,
292356
created_by=link.created_by,
293357
)
358+
359+
@staticmethod
360+
def _dump_data_connector_secret(secret: models.DataConnectorSecret) -> dict[str, Any]:
361+
"""Dumps a data connector secret for API responses."""
362+
return dict(
363+
name=secret.name,
364+
secret_id=str(secret.secret_id),
365+
)

0 commit comments

Comments
 (0)