Mercurial > libervia-backend
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