changeset 4385:a1ac33fe6b97

memory (sqla): Add columns and tables to handles permissions: Add columns to handle `access_model` and `publish_model` and a table for `affiliations`. With those new data, components handling pubsub can now manage permissions correctly. rel 462
author Goffi <goffi@goffi.org>
date Sun, 03 Aug 2025 23:45:45 +0200
parents 33468e175ade
children c055042c01e3
files libervia/backend/memory/migration/versions/8db042adb973_add_affiliations_access_model_and_.py libervia/backend/memory/sqla.py libervia/backend/memory/sqla_mapping.py
diffstat 3 files changed, 143 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/backend/memory/migration/versions/8db042adb973_add_affiliations_access_model_and_.py	Sun Aug 03 23:45:45 2025 +0200
@@ -0,0 +1,40 @@
+"""add "affiliations", "access_model" and "publish_model" to PubsubNode
+
+Revision ID: 8db042adb973
+Revises: 6af2d8f6be76
+Create Date: 2025-08-02 12:03:19.726497
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from libervia.backend.memory.sqla_mapping import JID
+
+
+# revision identifiers, used by Alembic.
+revision = '8db042adb973'
+down_revision = '6af2d8f6be76'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.create_table('pubsub_affiliations',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('node_id', sa.Integer(), nullable=False),
+    sa.Column('entity', JID(), nullable=False),
+    sa.Column('affiliation', sa.Enum('outcast', 'member', 'publisher', 'owner', name='affiliation', create_constraint=True), nullable=False),
+    sa.ForeignKeyConstraint(['node_id'], ['pubsub_nodes.id'], name=op.f('fk_pubsub_affiliations_node_id_pubsub_nodes'), ondelete='CASCADE'),
+    sa.PrimaryKeyConstraint('id', name=op.f('pk_pubsub_affiliations')),
+    sa.UniqueConstraint('node_id', 'entity', name=op.f('uq_pubsub_affiliations_node_id'))
+    )
+    with op.batch_alter_table('pubsub_nodes', schema=None) as batch_op:
+        batch_op.add_column(sa.Column('access_model', sa.Enum('open', 'whitelist', name='accessmodel', create_constraint=True), nullable=True))
+        batch_op.add_column(sa.Column('publish_model', sa.Enum('publishers', 'subscribers', 'open', name='publishmodel', create_constraint=True), nullable=True))
+
+
+def downgrade():
+    with op.batch_alter_table('pubsub_nodes', schema=None) as batch_op:
+        batch_op.drop_column('publish_model')
+        batch_op.drop_column('access_model')
+
+    op.drop_table('pubsub_affiliations')
--- a/libervia/backend/memory/sqla.py	Sun Aug 03 23:36:22 2025 +0200
+++ b/libervia/backend/memory/sqla.py	Sun Aug 03 23:45:45 2025 +0200
@@ -57,6 +57,7 @@
 from libervia.backend.memory import migration
 from libervia.backend.memory import sqla_config
 from libervia.backend.memory.sqla_mapping import (
+    AccessModel,
     Base,
     Component,
     File,
@@ -74,8 +75,11 @@
     PrivateInd,
     PrivateIndBin,
     Profile,
+    PublishModel,
+    PubsubAffiliation,
     PubsubItem,
     PubsubNode,
+    PubsubSub,
     Subject,
     SyncState,
     Thread,
@@ -1116,7 +1120,7 @@
 
         the older value will be retrieved from database, then update_cb will be applied to
         update it, and file will be updated checking that older value has not been changed
-        meanwhile by an other user. If it has changed, it tries again a couple of times
+        meanwhile by another user. If it has changed, it tries again a couple of times
         before failing
         @param column: column name (only "access" or "extra" are allowed)
         @param update_cb: method to update the value of the colum
@@ -1174,15 +1178,17 @@
         name: str,
         with_items: bool = False,
         with_subscriptions: bool = False,
+        with_affiliations: bool = False,
         create: bool = False,
-        create_kwargs: Optional[dict] = None,
-    ) -> Optional[PubsubNode]:
+        create_kwargs: dict|None = None,
+    ) -> PubsubNode|None:
         """Retrieve a PubsubNode from DB
 
         @param service: service hosting the node
         @param name: node's name
         @param with_items: retrieve items in the same query
         @param with_subscriptions: retrieve subscriptions in the same query
+        @param with_affiliations: retrieve affiliations in the same query
         @param create: if the node doesn't exist in DB, create it
         @param create_kwargs: keyword arguments to use with ``set_pubsub_node`` if the node
             needs to be created.
@@ -1197,6 +1203,8 @@
                 stmt = stmt.options(joinedload(PubsubNode.items))
             if with_subscriptions:
                 stmt = stmt.options(joinedload(PubsubNode.subscriptions))
+            if with_affiliations:
+                stmt = stmt.options(joinedload(PubsubNode.affiliations))
             result = await session.execute(stmt)
         ret = result.unique().scalar_one_or_none()
         if ret is None and create:
@@ -1219,6 +1227,7 @@
                             name,
                             with_items=with_items,
                             with_subscriptions=with_subscriptions,
+                            with_affiliations=with_affiliations,
                         )
                     )
                 else:
@@ -1227,25 +1236,59 @@
             return ret
 
     @aio
+    async def get_pubsub_nodes(
+        self,
+        client: SatXMPPEntity|None,
+        service: jid.JID|None
+    ) -> list[PubsubNode]:
+        """Retrieve pubsub nodes matching arguments.
+
+        @param client: If set, only return nodes of this client profile.
+        @param service: If set, only return nodes from this service.
+        @return: List of matching pubsub nodes.
+        """
+        async with self.session() as session:
+            stm = select(PubsubNode)
+            if client is not None:
+                profile_id = self.profiles[client.profile]
+                stm = stm.where(PubsubNode.profile_id == profile_id)
+            if service is not None:
+                stm = stm.where(PubsubNode.service == service)
+            result = await session.execute(stm)
+
+        return result.scalars().all()
+
+    @aio
     async def set_pubsub_node(
         self,
         client: SatXMPPEntity,
         service: jid.JID,
         name: str,
+        access_model: AccessModel|None = None,
+        publish_model: PublishModel|None = None,
         analyser: Optional[str] = None,
         type_: Optional[str] = None,
         subtype: Optional[str] = None,
         subscribed: bool = False,
+        extra: dict|None = None,
+        items: list[PubsubItem]|None = None,
+        affiliations: list[PubsubAffiliation]|None = None,
+        subscriptions: list[PubsubSub]|None = None,
     ) -> PubsubNode:
         node = PubsubNode(
             profile_id=self.profiles[client.profile],
             service=service,
             name=name,
+            access_model=access_model,
+            publish_model=publish_model,
             subscribed=subscribed,
             analyser=analyser,
             type_=type_,
             subtype=subtype,
-            subscriptions=[],
+            extra = extra,
+            items = items or [],
+            affiliations = affiliations or [],
+            subscriptions = subscriptions or []
         )
         async with self.session() as session:
             async with session.begin():
--- a/libervia/backend/memory/sqla_mapping.py	Sun Aug 03 23:36:22 2025 +0200
+++ b/libervia/backend/memory/sqla_mapping.py	Sun Aug 03 23:45:45 2025 +0200
@@ -105,11 +105,29 @@
     NO_SYNC = 4
 
 
+class AccessModel(enum.StrEnum):
+    open = enum.auto()
+    whitelist = enum.auto()
+
+
+class PublishModel(enum.StrEnum):
+    publishers = enum.auto()
+    subscribers = enum.auto()
+    open = enum.auto()
+
+
 class SubscriptionState(enum.Enum):
     SUBSCRIBED = 1
     PENDING = 2
 
 
+class Affiliation(enum.StrEnum):
+    outcast = enum.auto()
+    member = enum.auto()
+    publisher = enum.auto()
+    owner = enum.auto()
+
+
 class NotificationType(enum.Enum):
     chat = "chat"
     blog = "blog"
@@ -604,6 +622,20 @@
     profile_id = Column(ForeignKey("profiles.id", ondelete="CASCADE"))
     service = Column(JID)
     name = Column(Text, nullable=False)
+    access_model = Column(
+        Enum(
+            AccessModel,
+            create_constraint=True,
+        ),
+        nullable=True,
+    )
+    publish_model = Column(
+        Enum(
+            PublishModel,
+            create_constraint=True,
+        ),
+        nullable=True,
+    )
     subscribed = Column(
         Boolean(create_constraint=True, name="subscribed_bool"), nullable=False
     )
@@ -622,12 +654,36 @@
     extra = Column(JSON)
 
     items = relationship("PubsubItem", back_populates="node", passive_deletes=True)
+    affiliations = relationship("PubsubAffiliation", back_populates="node", passive_deletes=True)
     subscriptions = relationship("PubsubSub", back_populates="node", passive_deletes=True)
 
     def __str__(self):
         return f"Pubsub node {self.name!r} at {self.service}"
 
 
+class PubsubAffiliation(Base):
+    """Affiliations to pubsub nodes.
+
+    User by components managing a pubsub service.
+    """
+    __tablename__ = "pubsub_affiliations"
+    __table_args__ = (UniqueConstraint("node_id", "entity"),)
+
+
+    id = Column(Integer, primary_key=True)
+    node_id = Column(ForeignKey("pubsub_nodes.id", ondelete="CASCADE"), nullable=False)
+    entity = Column(JID, nullable=False)
+    affiliation = Column(
+        Enum(
+            Affiliation,
+            create_constraint=True,
+        ),
+        nullable=False,
+    )
+
+    node = relationship("PubsubNode", back_populates="affiliations")
+
+
 class PubsubSub(Base):
     """Subscriptions to pubsub nodes