Skip to content

Commit 7dbfa1f

Browse files
author
Ralf Grubenmann
committed
refactor: add validation to crc, user and secrets endpoints
1 parent 98f545c commit 7dbfa1f

File tree

6 files changed

+70
-67
lines changed

6 files changed

+70
-67
lines changed

components/renku_data_services/crc/blueprints.py

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from renku_data_services.base_api.auth import authenticate, only_admins
1212
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
1313
from renku_data_services.base_api.misc import validate_db_ids
14+
from renku_data_services.base_models.validation import validated_json
1415
from renku_data_services.crc import apispec, models
1516
from renku_data_services.crc.apispec_base import ResourceClassesFilter
1617
from renku_data_services.crc.db import ResourcePoolRepository, UserRepository
@@ -34,9 +35,7 @@ def get_all(self) -> BlueprintFactoryResponse:
3435
async def _get_all(request: Request, user: base_models.APIUser) -> HTTPResponse:
3536
res_filter = ResourceClassesFilter.model_validate(dict(request.query_args))
3637
rps = await self.rp_repo.filter_resource_pools(api_user=user, **res_filter.dict())
37-
return json(
38-
[apispec.ResourcePoolWithIdFiltered.model_validate(r).model_dump(exclude_none=True) for r in rps]
39-
)
38+
return validated_json(apispec.ResourcePoolWithIdFiltered, rps)
4039

4140
return "/resource_pools", ["GET"], _get_all
4241

@@ -49,7 +48,7 @@ def post(self) -> BlueprintFactoryResponse:
4948
async def _post(_: Request, user: base_models.APIUser, body: apispec.ResourcePool) -> HTTPResponse:
5049
rp = models.ResourcePool.from_dict(body.model_dump(exclude_none=True))
5150
res = await self.rp_repo.insert_resource_pool(api_user=user, resource_pool=rp)
52-
return json(apispec.ResourcePoolWithId.model_validate(res).model_dump(exclude_none=True), 201)
51+
return validated_json(apispec.ResourcePoolWithId, res, status=201)
5352

5453
return "/resource_pools", ["POST"], _post
5554

@@ -68,7 +67,7 @@ async def _get_one(request: Request, user: base_models.APIUser, resource_pool_id
6867
message=f"The resource pool with id {resource_pool_id} cannot be found."
6968
)
7069
rp = rps[0]
71-
return json(apispec.ResourcePoolWithId.model_validate(rp).model_dump(exclude_none=True))
70+
return validated_json(apispec.ResourcePoolWithId, rp)
7271

7372
return "/resource_pools/<resource_pool_id>", ["GET"], _get_one
7473

@@ -103,7 +102,7 @@ async def _put(
103102
raise errors.MissingResourceError(
104103
message=f"The resource pool with ID {resource_pool_id} cannot be found."
105104
)
106-
return json(apispec.ResourcePoolWithId.model_validate(res).model_dump(exclude_none=True))
105+
return validated_json(apispec.ResourcePoolWithId, res)
107106

108107
return "/resource_pools/<resource_pool_id>", ["PUT"], _put
109108

@@ -126,7 +125,7 @@ async def _patch(
126125
raise errors.MissingResourceError(
127126
message=f"The resource pool with ID {resource_pool_id} cannot be found."
128127
)
129-
return json(apispec.ResourcePoolWithId.model_validate(res).model_dump(exclude_none=True))
128+
return validated_json(apispec.ResourcePoolWithId, res)
130129

131130
return "/resource_pools/<resource_pool_id>", ["PATCH"], _patch
132131

@@ -147,13 +146,9 @@ def get_all(self) -> BlueprintFactoryResponse:
147146
@validate_db_ids
148147
async def _get_all(_: Request, user: base_models.APIUser, resource_pool_id: int) -> HTTPResponse:
149148
res = await self.repo.get_resource_pool_users(api_user=user, resource_pool_id=resource_pool_id)
150-
return json(
151-
[
152-
apispec.PoolUserWithId(id=r.keycloak_id, no_default_access=r.no_default_access).model_dump(
153-
exclude_none=True
154-
)
155-
for r in res.allowed
156-
]
149+
return validated_json(
150+
apispec.PoolUserWithId,
151+
[dict(id=r.keycloak_id, no_default_access=r.no_default_access) for r in res.allowed],
157152
)
158153

159154
return "/resource_pools/<resource_pool_id>/users", ["GET"], _get_all
@@ -199,13 +194,9 @@ async def _put_post(
199194
user_ids=user_ids_to_add,
200195
append=post,
201196
)
202-
return json(
203-
[
204-
apispec.PoolUserWithId(id=r.keycloak_id, no_default_access=r.no_default_access).model_dump(
205-
exclude_none=True
206-
)
207-
for r in updated_users
208-
],
197+
return validated_json(
198+
apispec.PoolUserWithId,
199+
[dict(id=r.keycloak_id, no_default_access=r.no_default_access) for r in updated_users],
209200
status=201 if post else 200,
210201
)
211202

@@ -219,10 +210,9 @@ async def _get(_: Request, user: base_models.APIUser, resource_pool_id: int, use
219210
keycloak_id=user_id, resource_pool_id=resource_pool_id, api_user=user
220211
)
221212
if len(res.allowed) > 0:
222-
return json(
223-
apispec.PoolUserWithId(
224-
id=res.allowed[0].keycloak_id, no_default_access=res.allowed[0].no_default_access
225-
).model_dump(exclude_none=True)
213+
return validated_json(
214+
apispec.PoolUserWithId,
215+
dict(id=res.allowed[0].keycloak_id, no_default_access=res.allowed[0].no_default_access),
226216
)
227217
raise errors.MissingResourceError(
228218
message=f"The user with id {user_id} or resource pool with id {resource_pool_id} cannot be found."
@@ -275,7 +265,7 @@ async def _get_all(request: Request, user: base_models.APIUser, resource_pool_id
275265
res = await self.repo.get_classes(
276266
api_user=user, resource_pool_id=resource_pool_id, name=request.args.get("name")
277267
)
278-
return json([apispec.ResourceClassWithId.model_validate(r).model_dump(exclude_none=True) for r in res])
268+
return validated_json(apispec.ResourceClassWithId, res)
279269

280270
return "/resource_pools/<resource_pool_id>/classes", ["GET"], _get_all
281271

@@ -293,7 +283,7 @@ async def _post(
293283
res = await self.repo.insert_resource_class(
294284
api_user=user, resource_class=cls, resource_pool_id=resource_pool_id
295285
)
296-
return json(apispec.ResourceClassWithId.model_validate(res).model_dump(exclude_none=True), 201)
286+
return validated_json(apispec.ResourceClassWithId, res, 201)
297287

298288
return "/resource_pools/<resource_pool_id>/classes", ["POST"], _post
299289

@@ -308,7 +298,7 @@ async def _get(_: Request, user: base_models.APIUser, resource_pool_id: int, cla
308298
raise errors.MissingResourceError(
309299
message=f"The class with id {class_id} or resource pool with id {resource_pool_id} cannot be found."
310300
)
311-
return json(apispec.ResourceClassWithId.model_validate(res[0]).model_dump(exclude_none=True))
301+
return validated_json(apispec.ResourceClassWithId, res[0])
312302

313303
return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["GET"], _get
314304

@@ -320,7 +310,7 @@ async def _get_no_pool(_: Request, class_id: int) -> HTTPResponse:
320310
res = await self.repo.get_classes(api_user=None, id=class_id)
321311
if len(res) < 1:
322312
raise errors.MissingResourceError(message=f"The class with id {class_id} cannot be found.")
323-
return json(apispec.ResourceClassWithId.model_validate(res[0]).model_dump(exclude_none=True))
313+
return validated_json(apispec.ResourceClassWithId, res[0])
324314

325315
return "/classes/<class_id>", ["GET"], _get_no_pool
326316

@@ -343,14 +333,15 @@ def put(self) -> BlueprintFactoryResponse:
343333

344334
@authenticate(self.authenticator)
345335
@only_admins
336+
@validate_db_ids
346337
@validate(json=apispec.ResourceClass)
347338
async def _put(
348339
_: Request, user: base_models.APIUser, body: apispec.ResourceClass, resource_pool_id: int, class_id: int
349340
) -> HTTPResponse:
350341
res = await self.repo.update_resource_class(
351342
user, resource_pool_id, class_id, put=True, **body.model_dump(exclude_none=True)
352343
)
353-
return json(apispec.ResourceClassWithId.model_validate(res).model_dump(exclude_none=True))
344+
return validated_json(apispec.ResourceClassWithId, res)
354345

355346
return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["PUT"], _put
356347

@@ -359,6 +350,7 @@ def patch(self) -> BlueprintFactoryResponse:
359350

360351
@authenticate(self.authenticator)
361352
@only_admins
353+
@validate_db_ids
362354
@validate(json=apispec.ResourceClassPatch)
363355
async def _patch(
364356
_: Request,
@@ -370,7 +362,7 @@ async def _patch(
370362
res = await self.repo.update_resource_class(
371363
user, resource_pool_id, class_id, put=False, **body.model_dump(exclude_none=True)
372364
)
373-
return json(apispec.ResourceClassWithId.model_validate(res).model_dump(exclude_none=True))
365+
return validated_json(apispec.ResourceClassWithId, res)
374366

375367
return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["PATCH"], _patch
376368

@@ -379,6 +371,7 @@ def get_tolerations(self) -> BlueprintFactoryResponse:
379371

380372
@authenticate(self.authenticator)
381373
@only_admins
374+
@validate_db_ids
382375
async def _get_tolerations(
383376
_: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
384377
) -> HTTPResponse:
@@ -392,6 +385,7 @@ def delete_tolerations(self) -> BlueprintFactoryResponse:
392385

393386
@authenticate(self.authenticator)
394387
@only_admins
388+
@validate_db_ids
395389
async def _delete_tolerations(
396390
_: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
397391
) -> HTTPResponse:
@@ -405,11 +399,12 @@ def get_affinities(self) -> BlueprintFactoryResponse:
405399

406400
@authenticate(self.authenticator)
407401
@only_admins
402+
@validate_db_ids
408403
async def _get_affinities(
409404
_: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
410405
) -> HTTPResponse:
411406
res = await self.repo.get_affinities(user, resource_pool_id, class_id)
412-
return json([apispec.NodeAffinity.model_validate(i).model_dump(exclude_none=True) for i in res])
407+
return validated_json(apispec.NodeAffinity, res)
413408

414409
return "/resource_pools/<resource_pool_id>/classes/<class_id>/node_affinities", ["GET"], _get_affinities
415410

@@ -418,6 +413,7 @@ def delete_affinities(self) -> BlueprintFactoryResponse:
418413

419414
@authenticate(self.authenticator)
420415
@only_admins
416+
@validate_db_ids
421417
async def _delete_affinities(
422418
_: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
423419
) -> HTTPResponse:
@@ -451,7 +447,7 @@ async def _get(_: Request, user: base_models.APIUser, resource_pool_id: int) ->
451447
raise errors.MissingResourceError(
452448
message=f"The resource pool with ID {resource_pool_id} does not have a quota."
453449
)
454-
return json(apispec.QuotaWithId.model_validate(rp.quota).model_dump(exclude_none=True))
450+
return validated_json(apispec.QuotaWithId, rp.quota)
455451

456452
return "/resource_pools/<resource_pool_id>/quota", ["GET"], _get
457453

@@ -460,6 +456,7 @@ def put(self) -> BlueprintFactoryResponse:
460456

461457
@authenticate(self.authenticator)
462458
@only_admins
459+
@validate_db_ids
463460
@validate(json=apispec.QuotaWithId)
464461
async def _put(
465462
_: Request, user: base_models.APIUser, resource_pool_id: int, body: apispec.QuotaWithId
@@ -473,6 +470,7 @@ def patch(self) -> BlueprintFactoryResponse:
473470

474471
@authenticate(self.authenticator)
475472
@only_admins
473+
@validate_db_ids
476474
@validate(json=apispec.QuotaPatch)
477475
async def _patch(
478476
_: Request, user: base_models.APIUser, resource_pool_id: int, body: apispec.QuotaPatch
@@ -500,7 +498,7 @@ async def _put_patch(
500498
message=f"The quota {new_quota} is not compatible with the resource class {rc}."
501499
)
502500
new_quota = self.quota_repo.update_quota(new_quota)
503-
return json(apispec.QuotaWithId.model_validate(new_quota).model_dump(exclude_none=True))
501+
return validated_json(apispec.QuotaWithId, new_quota)
504502

505503

506504
@dataclass(kw_only=True)
@@ -518,7 +516,7 @@ def get(self) -> BlueprintFactoryResponse:
518516
@only_admins
519517
async def _get(_: Request, user: base_models.APIUser, user_id: str) -> HTTPResponse:
520518
rps = await self.repo.get_user_resource_pools(keycloak_id=user_id, api_user=user)
521-
return json([apispec.ResourcePoolWithId.model_validate(rp).model_dump(exclude_none=True) for rp in rps])
519+
return validated_json(apispec.ResourcePoolWithId, rps)
522520

523521
return "/users/<user_id>/resource_pools", ["GET"], _get
524522

@@ -553,7 +551,8 @@ async def _post_put(
553551
rps = await self.repo.update_user_resource_pools(
554552
keycloak_id=user_id, resource_pool_ids=resource_pool_ids.root, append=post, api_user=api_user
555553
)
556-
return json(
557-
[apispec.ResourcePoolWithId.model_validate(rp).model_dump(exclude_none=True) for rp in rps],
554+
return validated_json(
555+
apispec.ResourcePoolWithId,
556+
rps,
558557
status=201 if post else 200,
559558
)

components/renku_data_services/secrets/blueprints.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from sanic import Request, json
77
from sanic.response import JSONResponse
88
from sanic_ext import validate
9+
from ulid import ULID
910

1011
import renku_data_services.base_models as base_models
1112
from renku_data_services.base_api.auth import authenticate, only_authenticated
@@ -37,7 +38,7 @@ async def _post(_: Request, user: base_models.APIUser, body: apispec.K8sSecret)
3738
owner_references = []
3839
if body.owner_references:
3940
owner_references = [OwnerReference.from_dict(o) for o in body.owner_references]
40-
secret_ids = [id.root for id in body.secret_ids]
41+
secret_ids = [ULID.from_str(id.root) for id in body.secret_ids]
4142
await create_k8s_secret(
4243
user,
4344
body.name,

components/renku_data_services/secrets/core.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from kubernetes import client as k8s_client
88
from prometheus_client import Counter, Enum
99
from sanic.log import logger
10+
from ulid import ULID
1011

1112
from renku_data_services import base_models, errors
1213
from renku_data_services.base_models.core import InternalServiceAdmin
@@ -26,7 +27,7 @@ async def create_k8s_secret(
2627
user: base_models.APIUser,
2728
secret_name: str,
2829
namespace: str,
29-
secret_ids: list[str],
30+
secret_ids: list[ULID],
3031
owner_references: list[OwnerReference],
3132
secrets_repo: UserSecretsRepo,
3233
secret_service_private_key: rsa.RSAPrivateKey,
@@ -36,7 +37,7 @@ async def create_k8s_secret(
3637
"""Creates a single k8s secret from a list of user secrets stored in the DB."""
3738
secrets = await secrets_repo.get_secrets_by_ids(requested_by=user, secret_ids=secret_ids)
3839
found_secret_ids = {str(s.id) for s in secrets}
39-
requested_secret_ids = set(secret_ids)
40+
requested_secret_ids = set(map(str, secret_ids))
4041
missing_secret_ids = requested_secret_ids - found_secret_ids
4142
if len(missing_secret_ids) > 0:
4243
raise errors.MissingResourceError(message=f"Couldn't find secrets with ids {', '.join(missing_secret_ids)}")

components/renku_data_services/secrets/db.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from sqlalchemy import delete, select
88
from sqlalchemy.exc import IntegrityError
99
from sqlalchemy.ext.asyncio import AsyncSession
10+
from ulid import ULID
1011

1112
from renku_data_services.base_api.auth import APIUser, only_authenticated
1213
from renku_data_services.base_models.core import InternalServiceAdmin, ServiceAdminId
@@ -34,21 +35,22 @@ async def get_user_secrets(self, requested_by: APIUser, kind: SecretKind) -> lis
3435
return [o.dump() for o in orm]
3536

3637
@only_authenticated
37-
async def get_secret_by_id(self, requested_by: APIUser, secret_id: str) -> Secret | None:
38+
async def get_secret_by_id(self, requested_by: APIUser, secret_id: ULID) -> Secret | None:
3839
"""Get a specific user secret from the database."""
3940
async with self.session_maker() as session:
40-
stmt = select(SecretORM).where(SecretORM.user_id == requested_by.id).where(SecretORM.id == secret_id)
41+
stmt = select(SecretORM).where(SecretORM.user_id == requested_by.id).where(SecretORM.id == str(secret_id))
4142
res = await session.execute(stmt)
4243
orm = res.scalar_one_or_none()
4344
if orm is None:
4445
return None
4546
return orm.dump()
4647

4748
@only_authenticated
48-
async def get_secrets_by_ids(self, requested_by: APIUser, secret_ids: list[str]) -> list[Secret]:
49+
async def get_secrets_by_ids(self, requested_by: APIUser, secret_ids: list[ULID]) -> list[Secret]:
4950
"""Get a specific user secrets from the database."""
51+
secret_ids_str = map(str, secret_ids)
5052
async with self.session_maker() as session:
51-
stmt = select(SecretORM).where(SecretORM.user_id == requested_by.id).where(SecretORM.id.in_(secret_ids))
53+
stmt = select(SecretORM).where(SecretORM.user_id == requested_by.id).where(SecretORM.id.in_(secret_ids_str))
5254
res = await session.execute(stmt)
5355
orms = res.scalars()
5456
return [orm.dump() for orm in orms]
@@ -83,13 +85,13 @@ async def insert_secret(self, requested_by: APIUser, secret: Secret) -> Secret:
8385

8486
@only_authenticated
8587
async def update_secret(
86-
self, requested_by: APIUser, secret_id: str, encrypted_value: bytes, encrypted_key: bytes
88+
self, requested_by: APIUser, secret_id: ULID, encrypted_value: bytes, encrypted_key: bytes
8789
) -> Secret:
8890
"""Update a secret."""
8991

9092
async with self.session_maker() as session, session.begin():
9193
result = await session.execute(
92-
select(SecretORM).where(SecretORM.id == secret_id).where(SecretORM.user_id == requested_by.id)
94+
select(SecretORM).where(SecretORM.id == str(secret_id)).where(SecretORM.user_id == requested_by.id)
9395
)
9496
secret = result.scalar_one_or_none()
9597
if secret is None:
@@ -101,12 +103,12 @@ async def update_secret(
101103
return secret.dump()
102104

103105
@only_authenticated
104-
async def delete_secret(self, requested_by: APIUser, secret_id: str) -> None:
106+
async def delete_secret(self, requested_by: APIUser, secret_id: ULID) -> None:
105107
"""Delete a secret."""
106108

107109
async with self.session_maker() as session, session.begin():
108110
result = await session.execute(
109-
select(SecretORM).where(SecretORM.id == secret_id).where(SecretORM.user_id == requested_by.id)
111+
select(SecretORM).where(SecretORM.id == str(secret_id)).where(SecretORM.user_id == requested_by.id)
110112
)
111113
secret = result.scalar_one_or_none()
112114
if secret is None:

0 commit comments

Comments
 (0)