Skip to content

Commit 0eeeed9

Browse files
Ralf GrubenmannPanaetius
authored andcommitted
refactor: add validation to crc, user and secrets endpoints
1 parent e8177cf commit 0eeeed9

File tree

6 files changed

+70
-78
lines changed

6 files changed

+70
-78
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.db import ResourcePoolRepository, UserRepository
1617
from renku_data_services.k8s.quota import QuotaRepository
@@ -35,9 +36,7 @@ async def _get_all(
3536
request: Request, user: base_models.APIUser, query: apispec.ResourcePoolsParams
3637
) -> HTTPResponse:
3738
rps = await self.rp_repo.filter_resource_pools(api_user=user, **query.model_dump())
38-
return json(
39-
[apispec.ResourcePoolWithIdFiltered.model_validate(r).model_dump(exclude_none=True) for r in rps]
40-
)
39+
return validated_json(apispec.ResourcePoolWithIdFiltered, rps)
4140

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

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

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

@@ -70,7 +69,7 @@ async def _get_one(
7069
message=f"The resource pool with id {resource_pool_id} cannot be found."
7170
)
7271
rp = rps[0]
73-
return json(apispec.ResourcePoolWithId.model_validate(rp).model_dump(exclude_none=True))
72+
return validated_json(apispec.ResourcePoolWithId, rp)
7473

7574
return "/resource_pools/<resource_pool_id>", ["GET"], _get_one
7675

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

110109
return "/resource_pools/<resource_pool_id>", ["PUT"], _put
111110

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

133132
return "/resource_pools/<resource_pool_id>", ["PATCH"], _patch
134133

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

161156
return "/resource_pools/<resource_pool_id>/users", ["GET"], _get_all
@@ -201,13 +196,9 @@ async def _put_post(
201196
user_ids=user_ids_to_add,
202197
append=post,
203198
)
204-
return json(
205-
[
206-
apispec.PoolUserWithId(id=r.keycloak_id, no_default_access=r.no_default_access).model_dump(
207-
exclude_none=True
208-
)
209-
for r in updated_users
210-
],
199+
return validated_json(
200+
apispec.PoolUserWithId,
201+
[dict(id=r.keycloak_id, no_default_access=r.no_default_access) for r in updated_users],
211202
status=201 if post else 200,
212203
)
213204

@@ -221,10 +212,9 @@ async def _get(_: Request, user: base_models.APIUser, resource_pool_id: int, use
221212
keycloak_id=user_id, resource_pool_id=resource_pool_id, api_user=user
222213
)
223214
if len(res.allowed) > 0:
224-
return json(
225-
apispec.PoolUserWithId(
226-
id=res.allowed[0].keycloak_id, no_default_access=res.allowed[0].no_default_access
227-
).model_dump(exclude_none=True)
215+
return validated_json(
216+
apispec.PoolUserWithId,
217+
dict(id=res.allowed[0].keycloak_id, no_default_access=res.allowed[0].no_default_access),
228218
)
229219
raise errors.MissingResourceError(
230220
message=f"The user with id {user_id} or resource pool with id {resource_pool_id} cannot be found."
@@ -278,7 +268,7 @@ async def _get_all(
278268
request: Request, user: base_models.APIUser, resource_pool_id: int, query: apispec.ResourceClassParams
279269
) -> HTTPResponse:
280270
res = await self.repo.get_classes(api_user=user, resource_pool_id=resource_pool_id, name=query.name)
281-
return json([apispec.ResourceClassWithId.model_validate(r).model_dump(exclude_none=True) for r in res])
271+
return validated_json(apispec.ResourceClassWithId, res)
282272

283273
return "/resource_pools/<resource_pool_id>/classes", ["GET"], _get_all
284274

@@ -296,7 +286,7 @@ async def _post(
296286
res = await self.repo.insert_resource_class(
297287
api_user=user, resource_class=cls, resource_pool_id=resource_pool_id
298288
)
299-
return json(apispec.ResourceClassWithId.model_validate(res).model_dump(exclude_none=True), 201)
289+
return validated_json(apispec.ResourceClassWithId, res, 201)
300290

301291
return "/resource_pools/<resource_pool_id>/classes", ["POST"], _post
302292

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

316306
return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["GET"], _get
317307

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

328318
return "/classes/<class_id>", ["GET"], _get_no_pool
329319

@@ -346,14 +336,15 @@ def put(self) -> BlueprintFactoryResponse:
346336

347337
@authenticate(self.authenticator)
348338
@only_admins
339+
@validate_db_ids
349340
@validate(json=apispec.ResourceClass)
350341
async def _put(
351342
_: Request, user: base_models.APIUser, body: apispec.ResourceClass, resource_pool_id: int, class_id: int
352343
) -> HTTPResponse:
353344
res = await self.repo.update_resource_class(
354345
user, resource_pool_id, class_id, put=True, **body.model_dump(exclude_none=True)
355346
)
356-
return json(apispec.ResourceClassWithId.model_validate(res).model_dump(exclude_none=True))
347+
return validated_json(apispec.ResourceClassWithId, res)
357348

358349
return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["PUT"], _put
359350

@@ -362,6 +353,7 @@ def patch(self) -> BlueprintFactoryResponse:
362353

363354
@authenticate(self.authenticator)
364355
@only_admins
356+
@validate_db_ids
365357
@validate(json=apispec.ResourceClassPatch)
366358
async def _patch(
367359
_: Request,
@@ -373,7 +365,7 @@ async def _patch(
373365
res = await self.repo.update_resource_class(
374366
user, resource_pool_id, class_id, put=False, **body.model_dump(exclude_none=True)
375367
)
376-
return json(apispec.ResourceClassWithId.model_validate(res).model_dump(exclude_none=True))
368+
return validated_json(apispec.ResourceClassWithId, res)
377369

378370
return "/resource_pools/<resource_pool_id>/classes/<class_id>", ["PATCH"], _patch
379371

@@ -382,6 +374,7 @@ def get_tolerations(self) -> BlueprintFactoryResponse:
382374

383375
@authenticate(self.authenticator)
384376
@only_admins
377+
@validate_db_ids
385378
async def _get_tolerations(
386379
_: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
387380
) -> HTTPResponse:
@@ -395,6 +388,7 @@ def delete_tolerations(self) -> BlueprintFactoryResponse:
395388

396389
@authenticate(self.authenticator)
397390
@only_admins
391+
@validate_db_ids
398392
async def _delete_tolerations(
399393
_: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
400394
) -> HTTPResponse:
@@ -408,11 +402,12 @@ def get_affinities(self) -> BlueprintFactoryResponse:
408402

409403
@authenticate(self.authenticator)
410404
@only_admins
405+
@validate_db_ids
411406
async def _get_affinities(
412407
_: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
413408
) -> HTTPResponse:
414409
res = await self.repo.get_affinities(user, resource_pool_id, class_id)
415-
return json([apispec.NodeAffinity.model_validate(i).model_dump(exclude_none=True) for i in res])
410+
return validated_json(apispec.NodeAffinity, res)
416411

417412
return "/resource_pools/<resource_pool_id>/classes/<class_id>/node_affinities", ["GET"], _get_affinities
418413

@@ -421,6 +416,7 @@ def delete_affinities(self) -> BlueprintFactoryResponse:
421416

422417
@authenticate(self.authenticator)
423418
@only_admins
419+
@validate_db_ids
424420
async def _delete_affinities(
425421
_: Request, user: base_models.APIUser, resource_pool_id: int, class_id: int
426422
) -> HTTPResponse:
@@ -454,7 +450,7 @@ async def _get(_: Request, user: base_models.APIUser, resource_pool_id: int) ->
454450
raise errors.MissingResourceError(
455451
message=f"The resource pool with ID {resource_pool_id} does not have a quota."
456452
)
457-
return json(apispec.QuotaWithId.model_validate(rp.quota).model_dump(exclude_none=True))
453+
return validated_json(apispec.QuotaWithId, rp.quota)
458454

459455
return "/resource_pools/<resource_pool_id>/quota", ["GET"], _get
460456

@@ -463,6 +459,7 @@ def put(self) -> BlueprintFactoryResponse:
463459

464460
@authenticate(self.authenticator)
465461
@only_admins
462+
@validate_db_ids
466463
@validate(json=apispec.QuotaWithId)
467464
async def _put(
468465
_: Request, user: base_models.APIUser, resource_pool_id: int, body: apispec.QuotaWithId
@@ -476,6 +473,7 @@ def patch(self) -> BlueprintFactoryResponse:
476473

477474
@authenticate(self.authenticator)
478475
@only_admins
476+
@validate_db_ids
479477
@validate(json=apispec.QuotaPatch)
480478
async def _patch(
481479
_: Request, user: base_models.APIUser, resource_pool_id: int, body: apispec.QuotaPatch
@@ -503,7 +501,7 @@ async def _put_patch(
503501
message=f"The quota {new_quota} is not compatible with the resource class {rc}."
504502
)
505503
new_quota = self.quota_repo.update_quota(new_quota)
506-
return json(apispec.QuotaWithId.model_validate(new_quota).model_dump(exclude_none=True))
504+
return validated_json(apispec.QuotaWithId, new_quota)
507505

508506

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

526524
return "/users/<user_id>/resource_pools", ["GET"], _get
527525

@@ -556,7 +554,8 @@ async def _post_put(
556554
rps = await self.repo.update_user_resource_pools(
557555
keycloak_id=user_id, resource_pool_ids=resource_pool_ids.root, append=post, api_user=api_user
558556
)
559-
return json(
560-
[apispec.ResourcePoolWithId.model_validate(rp).model_dump(exclude_none=True) for rp in rps],
557+
return validated_json(
558+
apispec.ResourcePoolWithId,
559+
rps,
561560
status=201 if post else 200,
562561
)

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)