From 08c1d12eb119c8f93c7d49d269c5e7d644e4ace1 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:53:36 +0530 Subject: [PATCH] fix(authz):publish zero parent tables --- .../168_publish_zero_authz_parent_tables.py | 23 +++++++++++++++++++ surfsense_backend/app/utils/rbac.py | 22 ++++++++++++++++++ surfsense_backend/app/zero_publication.py | 12 ++++++++++ 3 files changed, 57 insertions(+) create mode 100644 surfsense_backend/alembic/versions/168_publish_zero_authz_parent_tables.py diff --git a/surfsense_backend/alembic/versions/168_publish_zero_authz_parent_tables.py b/surfsense_backend/alembic/versions/168_publish_zero_authz_parent_tables.py new file mode 100644 index 000000000..f09f0f874 --- /dev/null +++ b/surfsense_backend/alembic/versions/168_publish_zero_authz_parent_tables.py @@ -0,0 +1,23 @@ +"""publish Zero authz parent tables + +Revision ID: 168 +Revises: 167 +""" + +from collections.abc import Sequence + +from alembic import op +from app.zero_publication import apply_publication + +revision: str = "168" +down_revision: str | None = "167" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + apply_publication(op.get_bind()) + + +def downgrade() -> None: + """No-op. Historical publication shapes are immutable.""" diff --git a/surfsense_backend/app/utils/rbac.py b/surfsense_backend/app/utils/rbac.py index 8777f09f6..c82c94344 100644 --- a/surfsense_backend/app/utils/rbac.py +++ b/surfsense_backend/app/utils/rbac.py @@ -80,6 +80,28 @@ async def get_user_permissions( return [] +async def get_allowed_read_space_ids( + session: AsyncSession, + auth: AuthContext, +) -> list[int]: + """Return search spaces the principal may read through sync transports. + + This mirrors the basic REST search-space access rule: membership is required, + and PAT principals are additionally constrained by the per-space API gate. + """ + stmt = ( + select(SearchSpaceMembership.search_space_id) + .join(SearchSpace, SearchSpace.id == SearchSpaceMembership.search_space_id) + .filter(SearchSpaceMembership.user_id == auth.user.id) + .order_by(SearchSpaceMembership.search_space_id) + ) + if auth.is_gated: + stmt = stmt.filter(SearchSpace.api_access_enabled == True) # noqa: E712 + + result = await session.execute(stmt) + return list(result.scalars().all()) + + async def _enforce_api_access_gate( session: AsyncSession, auth: AuthContext, diff --git a/surfsense_backend/app/zero_publication.py b/surfsense_backend/app/zero_publication.py index b14ee14d1..c16f27087 100644 --- a/surfsense_backend/app/zero_publication.py +++ b/surfsense_backend/app/zero_publication.py @@ -52,6 +52,16 @@ AUTOMATION_RUN_COLS = [ "created_at", ] +AUTOMATION_COLS = [ + "id", + "search_space_id", +] + +NEW_CHAT_THREAD_COLS = [ + "id", + "search_space_id", +] + # Enough to drive the lifecycle UI by push: status, the reviewable brief, and # its version. The bulky source_content and transcript are deliberately excluded # and fetched over REST when a gate opens. @@ -73,10 +83,12 @@ ZERO_PUBLICATION: Mapping[str, Sequence[str] | None] = { "documents": DOCUMENT_COLS, "folders": None, "search_source_connectors": None, + "new_chat_threads": NEW_CHAT_THREAD_COLS, "new_chat_messages": None, "chat_comments": None, "chat_session_state": None, "user": USER_COLS, + "automations": AUTOMATION_COLS, "automation_runs": AUTOMATION_RUN_COLS, "podcasts": PODCAST_COLS, }