Skip to content

Commit f49e146

Browse files
committed
refactor: add sqlalchemy ulid type to all relevant models
1 parent e33c2f8 commit f49e146

File tree

31 files changed

+241
-147
lines changed

31 files changed

+241
-147
lines changed

bases/renku_data_services/background_jobs/core.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
SubjectFilter,
1212
WriteRelationshipsRequest,
1313
)
14+
from ulid import ULID
1415

1516
from renku_data_services.authz.authz import Authz, ResourceType, _AuthzConverter, _Relation
1617
from renku_data_services.authz.models import Scope
@@ -117,7 +118,7 @@ async def fix_mismatched_project_namespace_ids(config: SyncConfig) -> None:
117118
relation=rel.relationship.relation,
118119
subject=SubjectReference(
119120
object=ObjectReference(
120-
object_type=ResourceType.group.value, object_id=correct_group_id
121+
object_type=ResourceType.group.value, object_id=str(correct_group_id)
121122
)
122123
),
123124
),
@@ -169,7 +170,7 @@ async def migrate_groups_make_all_public(config: SyncConfig) -> None:
169170
all_users = SubjectReference(object=_AuthzConverter.all_users())
170171
all_anon_users = SubjectReference(object=_AuthzConverter.anonymous_users())
171172
for group_id in groups_to_process:
172-
group_res = _AuthzConverter.group(group_id)
173+
group_res = _AuthzConverter.group(ULID.from_str(group_id))
173174
all_users_are_viewers = Relationship(
174175
resource=group_res,
175176
relation=_Relation.public_viewer.value,
@@ -228,7 +229,7 @@ async def migrate_user_namespaces_make_all_public(config: SyncConfig) -> None:
228229
all_users = SubjectReference(object=_AuthzConverter.all_users())
229230
all_anon_users = SubjectReference(object=_AuthzConverter.anonymous_users())
230231
for ns_id in namespaces_to_process:
231-
namespace_res = _AuthzConverter.user_namespace(ns_id)
232+
namespace_res = _AuthzConverter.user_namespace(ULID.from_str(ns_id))
232233
all_users_are_viewers = Relationship(
233234
resource=namespace_res,
234235
relation=_Relation.public_viewer.value,

components/renku_data_services/authz/authz.py

Lines changed: 58 additions & 42 deletions
Large diffs are not rendered by default.

components/renku_data_services/authz/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from dataclasses import dataclass
44
from enum import Enum
55

6+
from ulid import ULID
7+
68
from renku_data_services.errors import errors
79
from renku_data_services.namespace.apispec import GroupRole
810

@@ -56,7 +58,7 @@ class Member:
5658

5759
role: Role
5860
user_id: str
59-
resource_id: str
61+
resource_id: str | ULID
6062

6163

6264
class Change(Enum):

components/renku_data_services/connected_services/apispec_base.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Base models for API specifications."""
22

3-
from pydantic import BaseModel, Field
3+
from pydantic import BaseModel, Field, field_validator
4+
from ulid import ULID
45

56

67
class BaseAPISpec(BaseModel):
@@ -14,6 +15,12 @@ class Config:
1415
# this rust crate does not support lookahead regex syntax but we need it in this component
1516
regex_engine = "python-re"
1617

18+
@field_validator("id", mode="before", check_fields=False)
19+
@classmethod
20+
def serialize_id(cls, id: str | ULID) -> str:
21+
"""Custom serializer that can handle ULIDs."""
22+
return str(id)
23+
1724

1825
class AuthorizeParams(BaseAPISpec):
1926
"""The schema for the query parameters used in the authorize request."""

components/renku_data_services/connected_services/blueprints.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from sanic.log import logger
88
from sanic.response import JSONResponse
99
from sanic_ext import validate
10+
from ulid import ULID
1011

1112
import renku_data_services.base_models as base_models
1213
from renku_data_services.base_api.auth import authenticate, only_admins, only_authenticated
@@ -150,34 +151,34 @@ def get_one(self) -> BlueprintFactoryResponse:
150151
"""Get a specific OAuth2 connection."""
151152

152153
@authenticate(self.authenticator)
153-
async def _get_one(_: Request, user: base_models.APIUser, connection_id: str) -> JSONResponse:
154+
async def _get_one(_: Request, user: base_models.APIUser, connection_id: ULID) -> JSONResponse:
154155
connection = await self.connected_services_repo.get_oauth2_connection(
155156
connection_id=connection_id, user=user
156157
)
157158
return validated_json(apispec.Connection, connection)
158159

159-
return "/oauth2/connections/<connection_id>", ["GET"], _get_one
160+
return "/oauth2/connections/<connection_id:ulid>", ["GET"], _get_one
160161

161162
def get_account(self) -> BlueprintFactoryResponse:
162163
"""Get the account information for a specific OAuth2 connection."""
163164

164165
@authenticate(self.authenticator)
165-
async def _get_account(_: Request, user: base_models.APIUser, connection_id: str) -> JSONResponse:
166+
async def _get_account(_: Request, user: base_models.APIUser, connection_id: ULID) -> JSONResponse:
166167
account = await self.connected_services_repo.get_oauth2_connected_account(
167168
connection_id=connection_id, user=user
168169
)
169170
return validated_json(apispec.ConnectedAccount, account)
170171

171-
return "/oauth2/connections/<connection_id>/account", ["GET"], _get_account
172+
return "/oauth2/connections/<connection_id:ulid>/account", ["GET"], _get_account
172173

173174
def get_token(self) -> BlueprintFactoryResponse:
174175
"""Get the access token for a specific OAuth2 connection."""
175176

176177
@authenticate(self.authenticator)
177-
async def _get_token(_: Request, user: base_models.APIUser, connection_id: str) -> JSONResponse:
178+
async def _get_token(_: Request, user: base_models.APIUser, connection_id: ULID) -> JSONResponse:
178179
token = await self.connected_services_repo.get_oauth2_connection_token(
179180
connection_id=connection_id, user=user
180181
)
181182
return json(token.dump_for_api())
182183

183-
return "/oauth2/connections/<connection_id>/token", ["GET"], _get_token
184+
return "/oauth2/connections/<connection_id:ulid>/token", ["GET"], _get_token

components/renku_data_services/connected_services/db.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from sqlalchemy import select
1212
from sqlalchemy.ext.asyncio import AsyncSession
1313
from sqlalchemy.orm import selectinload
14+
from ulid import ULID
1415

1516
import renku_data_services.base_models as base_models
1617
from renku_data_services import errors
@@ -282,7 +283,7 @@ async def get_oauth2_connections(
282283
connections = result.all()
283284
return [c.dump() for c in connections]
284285

285-
async def get_oauth2_connection(self, connection_id: str, user: base_models.APIUser) -> models.OAuth2Connection:
286+
async def get_oauth2_connection(self, connection_id: ULID, user: base_models.APIUser) -> models.OAuth2Connection:
286287
"""Get one OAuth2 connection from the database."""
287288
if not user.is_authenticated or user.id is None:
288289
raise errors.MissingResourceError(
@@ -303,7 +304,7 @@ async def get_oauth2_connection(self, connection_id: str, user: base_models.APIU
303304
return connection.dump()
304305

305306
async def get_oauth2_connected_account(
306-
self, connection_id: str, user: base_models.APIUser
307+
self, connection_id: ULID, user: base_models.APIUser
307308
) -> models.ConnectedAccount:
308309
"""Get the account information from a OAuth2 connection."""
309310
async with self.get_async_oauth2_client(connection_id=connection_id, user=user) as (oauth2_client, _, adapter):
@@ -316,7 +317,9 @@ async def get_oauth2_connected_account(
316317
account = adapter.api_validate_account_response(response)
317318
return account
318319

319-
async def get_oauth2_connection_token(self, connection_id: str, user: base_models.APIUser) -> models.OAuth2TokenSet:
320+
async def get_oauth2_connection_token(
321+
self, connection_id: ULID, user: base_models.APIUser
322+
) -> models.OAuth2TokenSet:
320323
"""Get the OAuth2 access token from one connection from the database."""
321324
async with self.get_async_oauth2_client(connection_id=connection_id, user=user) as (oauth2_client, _, _):
322325
await oauth2_client.ensure_active_token(oauth2_client.token)
@@ -325,7 +328,7 @@ async def get_oauth2_connection_token(self, connection_id: str, user: base_model
325328

326329
@asynccontextmanager
327330
async def get_async_oauth2_client(
328-
self, connection_id: str, user: base_models.APIUser
331+
self, connection_id: ULID, user: base_models.APIUser
329332
) -> AsyncGenerator[tuple[AsyncOAuth2Client, schemas.OAuth2ConnectionORM, ProviderAdapter], None]:
330333
"""Get the AsyncOAuth2Client for the given connection_id and user."""
331334
if not user.is_authenticated or user.id is None:

components/renku_data_services/connected_services/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from datetime import UTC, datetime
55
from typing import Any
66

7+
from ulid import ULID
8+
79
from renku_data_services.connected_services.apispec import ConnectionStatus, ProviderKind
810

911

@@ -28,7 +30,7 @@ class OAuth2Client:
2830
class OAuth2Connection:
2931
"""OAuth2 connection model."""
3032

31-
id: str
33+
id: ULID
3234
provider_id: str
3335
status: ConnectionStatus
3436

components/renku_data_services/connected_services/orm.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from renku_data_services.connected_services import models
1313
from renku_data_services.connected_services.apispec import ConnectionStatus, ProviderKind
14+
from renku_data_services.utils.sqlalchemy import ULIDType
1415

1516
JSONVariant = JSON().with_variant(JSONB(), "postgresql")
1617

@@ -72,7 +73,7 @@ class OAuth2ConnectionORM(BaseORM):
7273
"""An OAuth2 connection."""
7374

7475
__tablename__ = "oauth2_connections"
75-
id: Mapped[str] = mapped_column("id", String(26), primary_key=True, default_factory=lambda: str(ULID()), init=False)
76+
id: Mapped[ULID] = mapped_column("id", ULIDType, primary_key=True, default_factory=lambda: str(ULID()), init=False)
7677
user_id: Mapped[str] = mapped_column("user_id", String())
7778
client_id: Mapped[str] = mapped_column(ForeignKey(OAuth2ClientORM.id, ondelete="CASCADE"), index=True)
7879
client: Mapped[OAuth2ClientORM] = relationship(init=False, repr=False)

components/renku_data_services/message_queue/converters.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ def to_events(
3535
raise errors.EventError(
3636
message=f"Cannot create an event of type {event_type} for a project which has no ID"
3737
)
38+
project_id_str = str(project.id)
3839
match event_type:
3940
case v2.ProjectCreated:
4041
return [
4142
Event(
4243
"project.created",
4344
v2.ProjectCreated(
44-
id=project.id,
45+
id=project_id_str,
4546
name=project.name,
4647
namespace=project.namespace.slug,
4748
slug=project.slug,
@@ -56,7 +57,7 @@ def to_events(
5657
Event(
5758
"projectAuth.added",
5859
v2.ProjectMemberAdded(
59-
projectId=project.id,
60+
projectId=project_id_str,
6061
userId=project.created_by,
6162
role=v2.MemberRole.OWNER,
6263
),
@@ -67,7 +68,7 @@ def to_events(
6768
Event(
6869
"project.updated",
6970
v2.ProjectUpdated(
70-
id=project.id,
71+
id=project_id_str,
7172
name=project.name,
7273
namespace=project.namespace.slug,
7374
slug=project.slug,
@@ -79,7 +80,7 @@ def to_events(
7980
)
8081
]
8182
case v2.ProjectRemoved:
82-
return [Event("project.removed", v2.ProjectRemoved(id=project.id))]
83+
return [Event("project.removed", v2.ProjectRemoved(id=project_id_str))]
8384
case _:
8485
raise errors.EventError(message=f"Trying to convert a project to an unknown event type {event_type}")
8586

@@ -145,13 +146,14 @@ class _ProjectAuthzEventConverter:
145146
def to_events(member_changes: list[authz_models.MembershipChange]) -> list[Event]:
146147
output: list[Event] = []
147148
for change in member_changes:
149+
resource_id = str(change.member.resource_id)
148150
match change.change:
149151
case authz_models.Change.UPDATE:
150152
output.append(
151153
Event(
152154
"projectAuth.updated",
153155
v2.ProjectMemberUpdated(
154-
projectId=change.member.resource_id,
156+
projectId=resource_id,
155157
userId=change.member.user_id,
156158
role=_convert_member_role(change.member.role),
157159
),
@@ -162,7 +164,7 @@ def to_events(member_changes: list[authz_models.MembershipChange]) -> list[Event
162164
Event(
163165
"projectAuth.removed",
164166
v2.ProjectMemberRemoved(
165-
projectId=change.member.resource_id,
167+
projectId=resource_id,
166168
userId=change.member.user_id,
167169
),
168170
)
@@ -172,7 +174,7 @@ def to_events(member_changes: list[authz_models.MembershipChange]) -> list[Event
172174
Event(
173175
"projectAuth.added",
174176
v2.ProjectMemberAdded(
175-
projectId=change.member.resource_id,
177+
projectId=resource_id,
176178
userId=change.member.user_id,
177179
role=_convert_member_role(change.member.role),
178180
),
@@ -191,13 +193,14 @@ class _GroupAuthzEventConverter:
191193
def to_events(member_changes: list[authz_models.MembershipChange]) -> list[Event]:
192194
output: list[Event] = []
193195
for change in member_changes:
196+
resource_id = str(change.member.resource_id)
194197
match change.change:
195198
case authz_models.Change.UPDATE:
196199
output.append(
197200
Event(
198201
"memberGroup.updated",
199202
v2.ProjectMemberUpdated(
200-
projectId=change.member.resource_id,
203+
projectId=resource_id,
201204
userId=change.member.user_id,
202205
role=_convert_member_role(change.member.role),
203206
),
@@ -208,7 +211,7 @@ def to_events(member_changes: list[authz_models.MembershipChange]) -> list[Event
208211
Event(
209212
"memberGroup.removed",
210213
v2.ProjectMemberRemoved(
211-
projectId=change.member.resource_id,
214+
projectId=resource_id,
212215
userId=change.member.user_id,
213216
),
214217
)
@@ -218,7 +221,7 @@ def to_events(member_changes: list[authz_models.MembershipChange]) -> list[Event
218221
Event(
219222
"memberGroup.added",
220223
v2.ProjectMemberAdded(
221-
projectId=change.member.resource_id,
224+
projectId=resource_id,
222225
userId=change.member.user_id,
223226
role=_convert_member_role(change.member.role),
224227
),
@@ -239,32 +242,33 @@ def to_events(group: group_models.Group, event_type: type[AvroModel] | type[even
239242
raise errors.ProgrammingError(
240243
message="Cannot send group events to the message queue for a group that does not have an ID"
241244
)
245+
group_id = str(group.id)
242246
match event_type:
243247
case v2.GroupAdded:
244248
return [
245249
Event(
246250
"group.added",
247251
v2.GroupAdded(
248-
id=group.id, name=group.name, description=group.description, namespace=group.slug
252+
id=group_id, name=group.name, description=group.description, namespace=group.slug
249253
),
250254
),
251255
Event(
252256
"memberGroup.added",
253257
v2.GroupMemberAdded(
254-
groupId=group.id,
258+
groupId=group_id,
255259
userId=group.created_by,
256260
role=v2.MemberRole.OWNER,
257261
),
258262
),
259263
]
260264
case v2.GroupRemoved:
261-
return [Event("group.removed", v2.GroupRemoved(id=group.id))]
265+
return [Event("group.removed", v2.GroupRemoved(id=group_id))]
262266
case v2.GroupUpdated:
263267
return [
264268
Event(
265269
"group.updated",
266270
v2.GroupUpdated(
267-
id=group.id, name=group.name, description=group.description, namespace=group.slug
271+
id=group_id, name=group.name, description=group.description, namespace=group.slug
268272
),
269273
)
270274
]

components/renku_data_services/namespace/apispec_base.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Base models for API specifications."""
22

3-
from pydantic import BaseModel
3+
from pydantic import BaseModel, field_validator
4+
from ulid import ULID
45

56

67
class BaseAPISpec(BaseModel):
@@ -13,3 +14,9 @@ class Config:
1314
# NOTE: By default the pydantic library does not use python for regex but a rust crate
1415
# this rust crate does not support lookahead regex syntax but we need it in this component
1516
regex_engine = "python-re"
17+
18+
@field_validator("id", mode="before", check_fields=False)
19+
@classmethod
20+
def serialize_id(cls, id: str | ULID) -> str:
21+
"""Custom serializer that can handle ULIDs."""
22+
return str(id)

0 commit comments

Comments
 (0)