diff --git a/surfsense_backend/app/db.py b/surfsense_backend/app/db.py index 28e0f159a..365e56c8e 100644 --- a/surfsense_backend/app/db.py +++ b/surfsense_backend/app/db.py @@ -292,7 +292,7 @@ INCENTIVE_TASKS_CONFIG = { class Permission(StrEnum): """ - Granular permissions for search space resources. + Granular permissions for workspace resources. Use '*' (FULL_ACCESS) to grant all permissions. """ @@ -363,10 +363,10 @@ class Permission(StrEnum): ROLES_UPDATE = "roles:update" ROLES_DELETE = "roles:delete" - # Search Space Settings + # Workspace Settings SETTINGS_VIEW = "settings:view" SETTINGS_UPDATE = "settings:update" - SETTINGS_DELETE = "settings:delete" # Delete the entire search space + SETTINGS_DELETE = "settings:delete" # Delete the entire workspace # API Access API_ACCESS_MANAGE = "api_access:manage" @@ -515,7 +515,7 @@ class ChatVisibility(StrEnum): Visibility/sharing level for chat threads. PRIVATE: Only the creator can see/access the chat (default) - SEARCH_SPACE: All members of the search space can see/access the chat + SEARCH_SPACE: All members of the workspace can see/access the chat PUBLIC: (Future) Anyone with the link can access the chat """ @@ -605,8 +605,7 @@ class NewChatThread(BaseModel, TimestampMixin): ) # Foreign keys - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, @@ -647,7 +646,7 @@ class NewChatThread(BaseModel, TimestampMixin): # Auto model pin for this thread: concrete resolved global LLM # config id. NULL means no pin; Auto will resolve on the next turn. # Single-writer invariant: only app.services.auto_model_pin_service sets - # or clears this column (plus bulk clears when a search space's + # or clears this column (plus bulk clears when a workspace's # chat_model_id changes). Unindexed: all reads are by primary key. pinned_llm_config_id = Column(Integer, nullable=True) @@ -664,7 +663,7 @@ class NewChatThread(BaseModel, TimestampMixin): ) # Relationships - search_space = relationship("SearchSpace", back_populates="new_chat_threads") + workspace = relationship("Workspace", back_populates="new_chat_threads") created_by = relationship("User", back_populates="new_chat_threads") messages = relationship( "NewChatMessage", @@ -788,8 +787,7 @@ class ExternalChatAccount(Base, TimestampMixin): owner_user_id = Column( UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=True ) - owner_search_space_id = Column( - "owner_workspace_id", + owner_workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=True, @@ -825,8 +823,8 @@ class ExternalChatAccount(Base, TimestampMixin): ) owner = relationship("User", foreign_keys=[owner_user_id]) - owner_search_space = relationship( - "SearchSpace", foreign_keys=[owner_search_space_id] + owner_workspace = relationship( + "Workspace", foreign_keys=[owner_workspace_id] ) bindings = relationship( "ExternalChatBinding", @@ -903,8 +901,7 @@ class ExternalChatBinding(Base, TimestampMixin): user_id = Column( UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False ) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, @@ -957,7 +954,7 @@ class ExternalChatBinding(Base, TimestampMixin): account = relationship("ExternalChatAccount", back_populates="bindings") user = relationship("User", foreign_keys=[user_id]) - search_space = relationship("SearchSpace", foreign_keys=[search_space_id]) + workspace = relationship("Workspace", foreign_keys=[workspace_id]) new_chat_thread = relationship("NewChatThread", foreign_keys=[new_chat_thread_id]) threads = relationship( "NewChatThread", @@ -1122,8 +1119,7 @@ class TokenUsage(BaseModel, TimestampMixin): nullable=True, index=True, ) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, @@ -1139,7 +1135,7 @@ class TokenUsage(BaseModel, TimestampMixin): # Relationships thread = relationship("NewChatThread", back_populates="token_usages") message = relationship("NewChatMessage", back_populates="token_usage") - search_space = relationship("SearchSpace") + workspace = relationship("Workspace") user = relationship("User") @@ -1328,8 +1324,7 @@ class Folder(BaseModel, TimestampMixin): nullable=True, index=True, ) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, @@ -1351,7 +1346,7 @@ class Folder(BaseModel, TimestampMixin): folder_metadata = Column("metadata", JSONB, nullable=True) parent = relationship("Folder", remote_side="Folder.id", backref="children") - search_space = relationship("SearchSpace", back_populates="folders") + workspace = relationship("Workspace", back_populates="folders") created_by = relationship("User", back_populates="folders") documents = relationship("Document", back_populates="folder", passive_deletes=True) @@ -1368,7 +1363,7 @@ class Document(BaseModel, TimestampMixin): # filesystem two files at different paths can hold identical bytes, # and the agent's ``write_file`` flow needs that semantic to support # copy / duplicate operations. Path uniqueness lives on - # ``unique_identifier_hash`` (per search space). The hash remains + # ``unique_identifier_hash`` (per workspace). The hash remains # indexed because connector indexers consult it as a change-detection # / cross-source dedup hint via :func:`check_duplicate_document`. # See migration 133. @@ -1393,8 +1388,7 @@ class Document(BaseModel, TimestampMixin): # Track when document was last updated by indexers, processors, or editor updated_at = Column(TIMESTAMP(timezone=True), nullable=True, index=True) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, @@ -1435,7 +1429,7 @@ class Document(BaseModel, TimestampMixin): ) # Relationships - search_space = relationship("SearchSpace", back_populates="documents") + workspace = relationship("Workspace", back_populates="documents") folder = relationship("Folder", back_populates="documents") created_by = relationship("User", back_populates="documents") connector = relationship("SearchSourceConnector", back_populates="documents") @@ -1520,13 +1514,12 @@ class VideoPresentation(BaseModel, TimestampMixin): index=True, ) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, ) - search_space = relationship("SearchSpace", back_populates="video_presentations") + workspace = relationship("Workspace", back_populates="video_presentations") thread_id = Column( Integer, @@ -1550,13 +1543,12 @@ class Report(BaseModel, TimestampMixin): String(100), nullable=True ) # e.g. "executive_summary", "deep_research" - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, ) - search_space = relationship("SearchSpace", back_populates="reports") + workspace = relationship("Workspace", back_populates="reports") # Versioning: reports sharing the same report_group_id are versions of the same report. # For v1, report_group_id = the report's own id (set after insert). @@ -1581,8 +1573,7 @@ class Connection(BaseModel, TimestampMixin): scope = Column(SQLAlchemyEnum(ConnectionScope), nullable=False, index=True) enabled = Column(Boolean, nullable=False, default=True, server_default="true") - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=True, @@ -1591,7 +1582,7 @@ class Connection(BaseModel, TimestampMixin): UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=True ) - search_space = relationship("SearchSpace", back_populates="connections") + workspace = relationship("Workspace", back_populates="connections") user = relationship("User", back_populates="connections") models = relationship( "Model", @@ -1695,8 +1686,7 @@ class ImageGeneration(BaseModel, TimestampMixin): access_token = Column(String(64), nullable=True, index=True) # Foreign keys - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, @@ -1709,11 +1699,11 @@ class ImageGeneration(BaseModel, TimestampMixin): ) # Relationships - search_space = relationship("SearchSpace", back_populates="image_generations") + workspace = relationship("Workspace", back_populates="image_generations") created_by = relationship("User", back_populates="image_generations") -class SearchSpace(BaseModel, TimestampMixin): +class Workspace(BaseModel, TimestampMixin): __tablename__ = "workspaces" name = Column(String(100), nullable=False, index=True) @@ -1735,7 +1725,7 @@ class SearchSpace(BaseModel, TimestampMixin): # Note: ID values preserve the existing convention: # - 0: Auto mode # - Negative IDs: Global virtual models from global_llm_config.yaml - # - Positive IDs: User/search-space models from the models table + # - Positive IDs: User/workspace models from the models table chat_model_id = Column( Integer, nullable=True, default=0, server_default="0" ) # For agent/chat operations, defaults to Auto mode @@ -1753,71 +1743,71 @@ class SearchSpace(BaseModel, TimestampMixin): user_id = Column( UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False ) - user = relationship("User", back_populates="search_spaces") + user = relationship("User", back_populates="workspaces") folders = relationship( "Folder", - back_populates="search_space", + back_populates="workspace", order_by="Folder.position", cascade="all, delete-orphan", ) documents = relationship( "Document", - back_populates="search_space", + back_populates="workspace", order_by="Document.id", cascade="all, delete-orphan", ) new_chat_threads = relationship( "NewChatThread", - back_populates="search_space", + back_populates="workspace", order_by="NewChatThread.updated_at.desc()", cascade="all, delete-orphan", ) podcasts = relationship( "Podcast", - back_populates="search_space", + back_populates="workspace", order_by="Podcast.id.desc()", cascade="all, delete-orphan", ) video_presentations = relationship( "VideoPresentation", - back_populates="search_space", + back_populates="workspace", order_by="VideoPresentation.id.desc()", cascade="all, delete-orphan", ) reports = relationship( "Report", - back_populates="search_space", + back_populates="workspace", order_by="Report.id.desc()", cascade="all, delete-orphan", ) image_generations = relationship( "ImageGeneration", - back_populates="search_space", + back_populates="workspace", order_by="ImageGeneration.id.desc()", cascade="all, delete-orphan", ) logs = relationship( "Log", - back_populates="search_space", + back_populates="workspace", order_by="Log.id", cascade="all, delete-orphan", ) notifications = relationship( "Notification", - back_populates="search_space", + back_populates="workspace", order_by="Notification.created_at.desc()", cascade="all, delete-orphan", ) search_source_connectors = relationship( "SearchSourceConnector", - back_populates="search_space", + back_populates="workspace", order_by="SearchSourceConnector.id", cascade="all, delete-orphan", ) connections = relationship( "Connection", - back_populates="search_space", + back_populates="workspace", order_by="Connection.id", cascade="all, delete-orphan", passive_deletes=True, @@ -1825,7 +1815,7 @@ class SearchSpace(BaseModel, TimestampMixin): automations = relationship( "Automation", - back_populates="search_space", + back_populates="workspace", order_by="Automation.id", cascade="all, delete-orphan", passive_deletes=True, @@ -1833,21 +1823,21 @@ class SearchSpace(BaseModel, TimestampMixin): # RBAC relationships roles = relationship( - "SearchSpaceRole", - back_populates="search_space", - order_by="SearchSpaceRole.id", + "WorkspaceRole", + back_populates="workspace", + order_by="WorkspaceRole.id", cascade="all, delete-orphan", ) memberships = relationship( - "SearchSpaceMembership", - back_populates="search_space", - order_by="SearchSpaceMembership.id", + "WorkspaceMembership", + back_populates="workspace", + order_by="WorkspaceMembership.id", cascade="all, delete-orphan", ) invites = relationship( - "SearchSpaceInvite", - back_populates="search_space", - order_by="SearchSpaceInvite.id", + "WorkspaceInvite", + back_populates="workspace", + order_by="WorkspaceInvite.id", cascade="all, delete-orphan", ) @@ -1907,14 +1897,13 @@ class SearchSourceConnector(BaseModel, TimestampMixin): indexing_frequency_minutes = Column(Integer, nullable=True) next_scheduled_at = Column(TIMESTAMP(timezone=True), nullable=True) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, ) - search_space = relationship( - "SearchSpace", back_populates="search_source_connectors" + workspace = relationship( + "Workspace", back_populates="search_source_connectors" ) user_id = Column( @@ -1937,13 +1926,12 @@ class Log(BaseModel, TimestampMixin): ) # Service/component that generated the log log_metadata = Column(JSON, nullable=True, default={}) # Additional context data - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, ) - search_space = relationship("SearchSpace", back_populates="logs") + workspace = relationship("Workspace", back_populates="logs") class UserIncentiveTask(BaseModel, TimestampMixin): @@ -2056,10 +2044,10 @@ class CreditPurchase(Base, TimestampMixin): user = relationship("User", back_populates="credit_purchases") -class SearchSpaceRole(BaseModel, TimestampMixin): +class WorkspaceRole(BaseModel, TimestampMixin): """ - Custom roles that can be defined per search space. - Each search space can have multiple roles with different permission sets. + Custom roles that can be defined per workspace. + Each workspace can have multiple roles with different permission sets. """ __tablename__ = "workspace_roles" @@ -2080,26 +2068,25 @@ class SearchSpaceRole(BaseModel, TimestampMixin): # System roles (Owner, Editor, Viewer) cannot be deleted is_system_role = Column(Boolean, nullable=False, default=False) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, ) - search_space = relationship("SearchSpace", back_populates="roles") + workspace = relationship("Workspace", back_populates="roles") memberships = relationship( - "SearchSpaceMembership", back_populates="role", passive_deletes=True + "WorkspaceMembership", back_populates="role", passive_deletes=True ) invites = relationship( - "SearchSpaceInvite", back_populates="role", passive_deletes=True + "WorkspaceInvite", back_populates="role", passive_deletes=True ) -class SearchSpaceMembership(BaseModel, TimestampMixin): +class WorkspaceMembership(BaseModel, TimestampMixin): """ - Tracks user membership in search spaces with their assigned role. - Each user can be a member of multiple search spaces with different roles. + Tracks user membership in workspaces with their assigned role. + Each user can be a member of multiple workspaces with different roles. """ __tablename__ = "workspace_memberships" @@ -2114,8 +2101,7 @@ class SearchSpaceMembership(BaseModel, TimestampMixin): user_id = Column( UUID(as_uuid=True), ForeignKey("user.id", ondelete="CASCADE"), nullable=False ) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, @@ -2125,7 +2111,7 @@ class SearchSpaceMembership(BaseModel, TimestampMixin): ForeignKey("workspace_roles.id", ondelete="SET NULL"), nullable=True, ) - # Indicates if this user is the original creator/owner of the search space + # Indicates if this user is the original creator/owner of the workspace is_owner = Column(Boolean, nullable=False, default=False) # Timestamp when the user joined (via invite or as creator) joined_at = Column( @@ -2140,17 +2126,17 @@ class SearchSpaceMembership(BaseModel, TimestampMixin): nullable=True, ) - user = relationship("User", back_populates="search_space_memberships") - search_space = relationship("SearchSpace", back_populates="memberships") - role = relationship("SearchSpaceRole", back_populates="memberships") + user = relationship("User", back_populates="workspace_memberships") + workspace = relationship("Workspace", back_populates="memberships") + role = relationship("WorkspaceRole", back_populates="memberships") invited_by_invite = relationship( - "SearchSpaceInvite", back_populates="used_by_memberships" + "WorkspaceInvite", back_populates="used_by_memberships" ) -class SearchSpaceInvite(BaseModel, TimestampMixin): +class WorkspaceInvite(BaseModel, TimestampMixin): """ - Invite links for search spaces. + Invite links for workspaces. Users can create invite links with specific roles that others can use to join. """ @@ -2159,8 +2145,7 @@ class SearchSpaceInvite(BaseModel, TimestampMixin): # Unique invite code (used in invite URLs) invite_code = Column(String(64), nullable=False, unique=True, index=True) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, @@ -2189,11 +2174,11 @@ class SearchSpaceInvite(BaseModel, TimestampMixin): # Optional custom name/label for the invite name = Column(String(100), nullable=True) - search_space = relationship("SearchSpace", back_populates="invites") - role = relationship("SearchSpaceRole", back_populates="invites") + workspace = relationship("Workspace", back_populates="invites") + role = relationship("WorkspaceRole", back_populates="invites") created_by = relationship("User", back_populates="created_invites") used_by_memberships = relationship( - "SearchSpaceMembership", + "WorkspaceMembership", back_populates="invited_by_invite", passive_deletes=True, ) @@ -2220,8 +2205,7 @@ class Prompt(BaseModel, TimestampMixin): nullable=False, index=True, ) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=True, @@ -2238,7 +2222,7 @@ class Prompt(BaseModel, TimestampMixin): is_public = Column(Boolean, nullable=False, default=False) user = relationship("User") - search_space = relationship("SearchSpace") + workspace = relationship("Workspace") if config.AUTH_TYPE == "GOOGLE": @@ -2250,7 +2234,7 @@ if config.AUTH_TYPE == "GOOGLE": oauth_accounts: Mapped[list[OAuthAccount]] = relationship( "OAuthAccount", lazy="joined" ) - search_spaces = relationship("SearchSpace", back_populates="user") + workspaces = relationship("Workspace", back_populates="user") notifications = relationship( "Notification", back_populates="user", @@ -2259,13 +2243,13 @@ if config.AUTH_TYPE == "GOOGLE": ) # RBAC relationships - search_space_memberships = relationship( - "SearchSpaceMembership", + workspace_memberships = relationship( + "WorkspaceMembership", back_populates="user", cascade="all, delete-orphan", ) created_invites = relationship( - "SearchSpaceInvite", + "WorkspaceInvite", back_populates="created_by", passive_deletes=True, ) @@ -2387,7 +2371,7 @@ if config.AUTH_TYPE == "GOOGLE": else: class User(SQLAlchemyBaseUserTableUUID, Base): - search_spaces = relationship("SearchSpace", back_populates="user") + workspaces = relationship("Workspace", back_populates="user") notifications = relationship( "Notification", back_populates="user", @@ -2396,13 +2380,13 @@ else: ) # RBAC relationships - search_space_memberships = relationship( - "SearchSpaceMembership", + workspace_memberships = relationship( + "WorkspaceMembership", back_populates="user", cascade="all, delete-orphan", ) created_invites = relationship( - "SearchSpaceInvite", + "WorkspaceInvite", back_populates="created_by", passive_deletes=True, ) @@ -2550,8 +2534,7 @@ class AgentActionLog(BaseModel): nullable=True, index=True, ) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, @@ -2622,8 +2605,7 @@ class DocumentRevision(BaseModel): nullable=True, index=True, ) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, @@ -2664,8 +2646,7 @@ class FolderRevision(BaseModel): nullable=True, index=True, ) - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, @@ -2693,7 +2674,7 @@ class FolderRevision(BaseModel): class AgentPermissionRule(BaseModel): """Persistent permission rule consumed by :class:`PermissionMiddleware`. - Scoped at one of: search-space-wide (``user_id`` and ``thread_id`` NULL), + Scoped at one of: workspace-wide (``user_id`` and ``thread_id`` NULL), user-wide (``user_id`` set, ``thread_id`` NULL), or per-thread (``thread_id`` set). Loaded at agent build time and converted to :class:`Rule` instances inside the agent factory. @@ -2701,8 +2682,7 @@ class AgentPermissionRule(BaseModel): __tablename__ = "agent_permission_rules" - search_space_id = Column( - "workspace_id", + workspace_id = Column( Integer, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False, @@ -3080,10 +3060,10 @@ def has_all_permissions( def get_default_roles_config() -> list[dict]: """ Get the configuration for default system roles. - These roles are created automatically when a search space is created. + These roles are created automatically when a workspace is created. Only 3 roles are supported: - - Owner: Full access to everything (assigned to search space creator) + - Owner: Full access to everything (assigned to workspace creator) - Editor: Can create/update content but cannot delete, manage roles, or change settings - Viewer: Read-only access to resources (can add comments) @@ -3093,7 +3073,7 @@ def get_default_roles_config() -> list[dict]: return [ { "name": "Owner", - "description": "Full access to all search space resources and settings", + "description": "Full access to all workspace resources and settings", "permissions": DEFAULT_ROLE_PERMISSIONS["Owner"], "is_default": False, "is_system_role": True, @@ -3107,7 +3087,7 @@ def get_default_roles_config() -> list[dict]: }, { "name": "Viewer", - "description": "Read-only access to search space resources", + "description": "Read-only access to workspace resources", "permissions": DEFAULT_ROLE_PERMISSIONS["Viewer"], "is_default": False, "is_system_role": True,