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
This commit is contained in:
Goffi 2025-08-03 23:45:45 +02:00
commit 26002870a8
3 changed files with 143 additions and 4 deletions

View file

@ -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')

View file

@ -57,6 +57,7 @@ from libervia.backend.core.log import getLogger
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 @@ from libervia.backend.memory.sqla_mapping import (
PrivateInd,
PrivateIndBin,
Profile,
PublishModel,
PubsubAffiliation,
PubsubItem,
PubsubNode,
PubsubSub,
Subject,
SyncState,
Thread,
@ -1116,7 +1120,7 @@ class Storage:
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 @@ class Storage:
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 @@ class Storage:
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 @@ class Storage:
name,
with_items=with_items,
with_subscriptions=with_subscriptions,
with_affiliations=with_affiliations,
)
)
else:
@ -1226,26 +1235,60 @@ class Storage:
else:
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():

View file

@ -105,11 +105,29 @@ class SyncState(enum.Enum):
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 @@ class PubsubNode(Base):
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 @@ class PubsubNode(Base):
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