From 72236f65f7793c7934a2cb7def4013e6ca530178 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Sat, 29 Nov 2025 00:12:30 -0800 Subject: [PATCH 1/2] refactor: remove UserSearchSpacePreference model and related relationships - Deleted UserSearchSpacePreference class and its relationships from SearchSpace and User models. - Cleaned up unused code to streamline database interactions. --- .../42_drop_user_search_space_preferences.py | 41 +++++++++++++++++ surfsense_backend/app/db.py | 45 ------------------- 2 files changed, 41 insertions(+), 45 deletions(-) create mode 100644 surfsense_backend/alembic/versions/42_drop_user_search_space_preferences.py diff --git a/surfsense_backend/alembic/versions/42_drop_user_search_space_preferences.py b/surfsense_backend/alembic/versions/42_drop_user_search_space_preferences.py new file mode 100644 index 000000000..3ce491cfc --- /dev/null +++ b/surfsense_backend/alembic/versions/42_drop_user_search_space_preferences.py @@ -0,0 +1,41 @@ +"""Drop user_search_space_preferences table + +Revision ID: 42 +Revises: 41 +Create Date: 2025-11-28 + +This table is no longer needed after RBAC implementation: +- LLM preferences are now stored on SearchSpace directly +- User-SearchSpace relationships are handled by SearchSpaceMembership +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "42" +down_revision = "41" +branch_labels = None +depends_on = None + + +def upgrade(): + # Drop the user_search_space_preferences table + op.drop_table("user_search_space_preferences") + + +def downgrade(): + # Recreate the table if rolling back + op.create_table( + "user_search_space_preferences", + sa.Column("id", sa.Integer(), primary_key=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column("user_id", sa.UUID(), sa.ForeignKey("user.id", ondelete="CASCADE"), nullable=False), + sa.Column("search_space_id", sa.Integer(), sa.ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False), + sa.Column("long_context_llm_id", sa.Integer(), nullable=True), + sa.Column("fast_llm_id", sa.Integer(), nullable=True), + sa.Column("strategic_llm_id", sa.Integer(), nullable=True), + sa.UniqueConstraint("user_id", "search_space_id", name="uq_user_searchspace"), + ) + diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index 6195bec87..f5a8fd20f 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -441,11 +441,6 @@ class SearchSpace(BaseModel, TimestampMixin): order_by="LLMConfig.id", cascade="all, delete-orphan", ) - user_preferences = relationship( - "UserSearchSpacePreference", - back_populates="search_space", - cascade="all, delete-orphan", - ) # RBAC relationships roles = relationship( @@ -527,36 +522,6 @@ class LLMConfig(BaseModel, TimestampMixin): search_space = relationship("SearchSpace", back_populates="llm_configs") -class UserSearchSpacePreference(BaseModel, TimestampMixin): - __tablename__ = "user_search_space_preferences" - __table_args__ = ( - UniqueConstraint( - "user_id", - "search_space_id", - name="uq_user_searchspace", - ), - ) - - user_id = Column( - UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False - ) - search_space_id = Column( - Integer, ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False - ) - - # User-specific LLM preferences for this search space - # Note: These can be negative IDs for global configs (from YAML) or positive IDs for custom configs (from DB) - # Foreign keys removed to support global configs with negative IDs - long_context_llm_id = Column(Integer, nullable=True) - fast_llm_id = Column(Integer, nullable=True) - strategic_llm_id = Column(Integer, nullable=True) - - # Future RBAC fields can be added here - # role = Column(String(50), nullable=True) # e.g., 'owner', 'editor', 'viewer' - # permissions = Column(JSON, nullable=True) - - user = relationship("User", back_populates="search_space_preferences") - search_space = relationship("SearchSpace", back_populates="user_preferences") class Log(BaseModel, TimestampMixin): @@ -720,11 +685,6 @@ if config.AUTH_TYPE == "GOOGLE": "OAuthAccount", lazy="joined" ) search_spaces = relationship("SearchSpace", back_populates="user") - search_space_preferences = relationship( - "UserSearchSpacePreference", - back_populates="user", - cascade="all, delete-orphan", - ) # RBAC relationships search_space_memberships = relationship( @@ -746,11 +706,6 @@ else: class User(SQLAlchemyBaseUserTableUUID, Base): search_spaces = relationship("SearchSpace", back_populates="user") - search_space_preferences = relationship( - "UserSearchSpacePreference", - back_populates="user", - cascade="all, delete-orphan", - ) # RBAC relationships search_space_memberships = relationship( From 07234d7849aab9d26ed96a5f3c7c91c8f239537b Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Sat, 29 Nov 2025 00:16:41 -0800 Subject: [PATCH 2/2] ruff format --- ...backfill_rbac_for_existing_searchspaces.py | 79 +++++++++++++------ .../42_drop_user_search_space_preferences.py | 19 ++++- surfsense_backend/app/db.py | 2 - 3 files changed, 71 insertions(+), 29 deletions(-) diff --git a/surfsense_backend/alembic/versions/41_backfill_rbac_for_existing_searchspaces.py b/surfsense_backend/alembic/versions/41_backfill_rbac_for_existing_searchspaces.py index 970f4b256..3a9b3e698 100644 --- a/surfsense_backend/alembic/versions/41_backfill_rbac_for_existing_searchspaces.py +++ b/surfsense_backend/alembic/versions/41_backfill_rbac_for_existing_searchspaces.py @@ -31,16 +31,40 @@ DEFAULT_ROLES = [ "name": "Admin", "description": "Can manage members, roles, and all content", "permissions": [ - "documents:create", "documents:read", "documents:update", "documents:delete", - "chats:create", "chats:read", "chats:update", "chats:delete", - "llm_configs:create", "llm_configs:read", "llm_configs:update", "llm_configs:delete", - "logs:read", "logs:delete", - "podcasts:create", "podcasts:read", "podcasts:update", "podcasts:delete", - "connectors:create", "connectors:read", "connectors:update", "connectors:delete", - "members:read", "members:update", "members:delete", - "roles:create", "roles:read", "roles:update", "roles:delete", - "invites:create", "invites:read", "invites:delete", - "settings:read", "settings:update", + "documents:create", + "documents:read", + "documents:update", + "documents:delete", + "chats:create", + "chats:read", + "chats:update", + "chats:delete", + "llm_configs:create", + "llm_configs:read", + "llm_configs:update", + "llm_configs:delete", + "logs:read", + "logs:delete", + "podcasts:create", + "podcasts:read", + "podcasts:update", + "podcasts:delete", + "connectors:create", + "connectors:read", + "connectors:update", + "connectors:delete", + "members:read", + "members:update", + "members:delete", + "roles:create", + "roles:read", + "roles:update", + "roles:delete", + "invites:create", + "invites:read", + "invites:delete", + "settings:read", + "settings:update", ], "is_system_role": True, "is_default": False, @@ -49,12 +73,20 @@ DEFAULT_ROLES = [ "name": "Editor", "description": "Can create and edit content", "permissions": [ - "documents:create", "documents:read", "documents:update", - "chats:create", "chats:read", "chats:update", + "documents:create", + "documents:read", + "documents:update", + "chats:create", + "chats:read", + "chats:update", "llm_configs:read", "logs:read", - "podcasts:create", "podcasts:read", "podcasts:update", - "connectors:create", "connectors:read", "connectors:update", + "podcasts:create", + "podcasts:read", + "podcasts:update", + "connectors:create", + "connectors:read", + "connectors:update", "members:read", "roles:read", ], @@ -101,8 +133,10 @@ def upgrade(): # Create default roles for each search space for role in DEFAULT_ROLES: # Convert permissions list to PostgreSQL array literal format for raw SQL - perms_literal = "ARRAY[" + ",".join(f"'{p}'" for p in role["permissions"]) + "]::TEXT[]" - + perms_literal = ( + "ARRAY[" + ",".join(f"'{p}'" for p in role["permissions"]) + "]::TEXT[]" + ) + result = connection.execute( sa.text(f""" INSERT INTO search_space_roles @@ -116,10 +150,10 @@ def upgrade(): "is_default": role["is_default"], "is_system_role": role["is_system_role"], "search_space_id": ss_id, - } + }, ) role_id = result.fetchone()[0] - + # Keep track of Owner role ID if role["name"] == "Owner": owner_role_id = role_id @@ -132,7 +166,7 @@ def upgrade(): SELECT 1 FROM search_space_memberships WHERE user_id = :user_id AND search_space_id = :search_space_id """), - {"user_id": owner_user_id, "search_space_id": ss_id} + {"user_id": owner_user_id, "search_space_id": ss_id}, ).fetchone() if not existing: @@ -146,7 +180,7 @@ def upgrade(): "user_id": owner_user_id, "search_space_id": ss_id, "role_id": owner_role_id, - } + }, ) @@ -156,7 +190,7 @@ def downgrade(): # However, this is destructive and may affect manually created data # So we only remove system roles and owner memberships that were auto-created connection = op.get_bind() - + # Remove memberships where user is owner and role is system Owner role connection.execute( sa.text(""" @@ -168,7 +202,7 @@ def downgrade(): AND ssr.name = 'Owner' """) ) - + # Remove system roles connection.execute( sa.text(""" @@ -176,4 +210,3 @@ def downgrade(): WHERE is_system_role = TRUE """) ) - diff --git a/surfsense_backend/alembic/versions/42_drop_user_search_space_preferences.py b/surfsense_backend/alembic/versions/42_drop_user_search_space_preferences.py index 3ce491cfc..9144421d8 100644 --- a/surfsense_backend/alembic/versions/42_drop_user_search_space_preferences.py +++ b/surfsense_backend/alembic/versions/42_drop_user_search_space_preferences.py @@ -30,12 +30,23 @@ def downgrade(): op.create_table( "user_search_space_preferences", sa.Column("id", sa.Integer(), primary_key=True), - sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), - sa.Column("user_id", sa.UUID(), sa.ForeignKey("user.id", ondelete="CASCADE"), nullable=False), - sa.Column("search_space_id", sa.Integer(), sa.ForeignKey("searchspaces.id", ondelete="CASCADE"), nullable=False), + sa.Column( + "created_at", sa.DateTime(timezone=True), server_default=sa.func.now() + ), + sa.Column( + "user_id", + sa.UUID(), + sa.ForeignKey("user.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "search_space_id", + sa.Integer(), + sa.ForeignKey("searchspaces.id", ondelete="CASCADE"), + nullable=False, + ), sa.Column("long_context_llm_id", sa.Integer(), nullable=True), sa.Column("fast_llm_id", sa.Integer(), nullable=True), sa.Column("strategic_llm_id", sa.Integer(), nullable=True), sa.UniqueConstraint("user_id", "search_space_id", name="uq_user_searchspace"), ) - diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index f5a8fd20f..f3147a42b 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -522,8 +522,6 @@ class LLMConfig(BaseModel, TimestampMixin): search_space = relationship("SearchSpace", back_populates="llm_configs") - - class Log(BaseModel, TimestampMixin): __tablename__ = "logs"