From 44a65bb3c7142e27f15f15a69d146ff6230be9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= <eloi@yaal.coop> Date: Sat, 15 Mar 2025 11:35:57 +0100 Subject: [PATCH] fix: order SQL relationship tables --- CHANGES.rst | 1 + .../1742033764_ordered_relationships.py | 79 +++++++++++++++++++ .../backends/sql/migrations/script.py.mako | 1 + canaille/backends/sql/models.py | 13 ++- 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 canaille/backends/sql/migrations/1742033764_ordered_relationships.py diff --git a/CHANGES.rst b/CHANGES.rst index 85078639..eabba9a1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,7 @@ Fixed - Prevent clients from registering with fragment components in their redirect uri :issue:`235` - Ensure there is a `redirect_uri` in authorization requests from clients. :issue:`232` - Display client TOS uri and policy uri in authorization page if set during client registration +- User group membership is ordered with the SQL backend. :issue:`169` [0.0.64] - 2025-02-12 --------------------- diff --git a/canaille/backends/sql/migrations/1742033764_ordered_relationships.py b/canaille/backends/sql/migrations/1742033764_ordered_relationships.py new file mode 100644 index 00000000..1f27c47f --- /dev/null +++ b/canaille/backends/sql/migrations/1742033764_ordered_relationships.py @@ -0,0 +1,79 @@ +"""Ordered relationships. + +Revision ID: 1742033764 +Revises: 1740765703 +Create Date: 2025-03-15 11:16:04.256771 + +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "1742033764" +down_revision: str | None = "1740765703" +branch_labels: str | Sequence[str] | None = () +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "client_audience_association_table", + sa.Column("index", sa.Integer(), nullable=True), + ) + op.execute(""" + UPDATE client_audience_association_table + SET 'index' = s.row_num + FROM ( + SELECT audience_id, client_id, ROW_NUMBER() OVER (ORDER BY audience_id, client_id) as row_num + FROM client_audience_association_table + ) s + WHERE client_audience_association_table.audience_id = s.audience_id AND client_audience_association_table.client_id = s.client_id; + """) + with op.batch_alter_table("client_audience_association_table") as batch_op: + batch_op.alter_column("index", existing_type=sa.Integer(), autoincrement=True) + + op.add_column( + "membership_association_table", + sa.Column("index", sa.Integer(), autoincrement=True, nullable=True), + ) + op.execute(""" + UPDATE membership_association_table + SET 'index' = s.row_num + FROM ( + SELECT user_id, group_id, ROW_NUMBER() OVER (ORDER BY user_id, group_id) as row_num + FROM membership_association_table + ) s + WHERE membership_association_table.user_id = s.user_id AND membership_association_table.group_id = s.group_id; + """) + with op.batch_alter_table("membership_association_table") as batch_op: + batch_op.alter_column("index", existing_type=sa.Integer(), autoincrement=True) + + op.add_column( + "token_audience_association_table", + sa.Column("index", sa.Integer(), autoincrement=True, nullable=True), + ) + op.execute(""" + UPDATE token_audience_association_table + SET 'index' = s.row_num + FROM ( + SELECT token_id, client_id, ROW_NUMBER() OVER (ORDER BY token_id, client_id) as row_num + FROM token_audience_association_table + ) s + WHERE token_audience_association_table.token_id = s.token_id AND token_audience_association_table.client_id = s.client_id; + """) + with op.batch_alter_table("token_audience_association_table") as batch_op: + batch_op.alter_column("index", existing_type=sa.Integer(), autoincrement=True) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("token_audience_association_table", "index") + op.drop_column("membership_association_table", "index") + op.drop_column("client_audience_association_table", "index") + # ### end Alembic commands ### diff --git a/canaille/backends/sql/migrations/script.py.mako b/canaille/backends/sql/migrations/script.py.mako index 669173c0..dd852bfa 100644 --- a/canaille/backends/sql/migrations/script.py.mako +++ b/canaille/backends/sql/migrations/script.py.mako @@ -6,6 +6,7 @@ Create Date: ${create_date} """ from collections.abc import Sequence +from typing import Union import sqlalchemy as sa import sqlalchemy_utils.types.password diff --git a/canaille/backends/sql/models.py b/canaille/backends/sql/models.py index 34bfa01d..491a4d5b 100644 --- a/canaille/backends/sql/models.py +++ b/canaille/backends/sql/models.py @@ -55,6 +55,7 @@ class SqlAlchemyModel(BackendModel): membership_association_table = Table( "membership_association_table", Base.metadata, + Column("index", Integer, autoincrement=True), Column("user_id", ForeignKey("user.id"), primary_key=True), Column("group_id", ForeignKey("group.id"), primary_key=True), ) @@ -111,7 +112,9 @@ class User(canaille.core.models.User, Base, SqlAlchemyModel): title: Mapped[str] = mapped_column(String, nullable=True) organization: Mapped[str] = mapped_column(String, nullable=True) groups: Mapped[list["Group"]] = relationship( - secondary=membership_association_table, back_populates="members" + secondary=membership_association_table, + back_populates="members", + order_by=membership_association_table.c.index, ) lock_date: Mapped[datetime.datetime] = mapped_column( TZDateTime(timezone=True), nullable=True @@ -159,13 +162,16 @@ class Group(canaille.core.models.Group, Base, SqlAlchemyModel): display_name: Mapped[str] = mapped_column(String) description: Mapped[str] = mapped_column(String, nullable=True) members: Mapped[list["User"]] = relationship( - secondary=membership_association_table, back_populates="groups" + secondary=membership_association_table, + back_populates="groups", + order_by=membership_association_table.c.index, ) client_audience_association_table = Table( "client_audience_association_table", Base.metadata, + Column("index", Integer, autoincrement=True), Column("audience_id", ForeignKey("client.id"), primary_key=True, nullable=True), Column("client_id", ForeignKey("client.id"), primary_key=True, nullable=True), ) @@ -194,6 +200,7 @@ class Client(canaille.oidc.models.Client, Base, SqlAlchemyModel): secondary=client_audience_association_table, primaryjoin=id == client_audience_association_table.c.client_id, secondaryjoin=id == client_audience_association_table.c.audience_id, + order_by=client_audience_association_table.c.index, ) client_id: Mapped[str] = mapped_column(String, nullable=True) client_secret: Mapped[str] = mapped_column(String, nullable=True) @@ -257,6 +264,7 @@ class AuthorizationCode(canaille.oidc.models.AuthorizationCode, Base, SqlAlchemy token_audience_association_table = Table( "token_audience_association_table", Base.metadata, + Column("index", Integer, autoincrement=True), Column("token_id", ForeignKey("token.id"), primary_key=True, nullable=True), Column("client_id", ForeignKey("client.id"), primary_key=True, nullable=True), ) @@ -296,6 +304,7 @@ class Token(canaille.oidc.models.Token, Base, SqlAlchemyModel): secondary=token_audience_association_table, primaryjoin=id == token_audience_association_table.c.token_id, secondaryjoin=Client.id == token_audience_association_table.c.client_id, + order_by=token_audience_association_table.c.index, ) -- GitLab