From c1d760692f30be101d4ff2fdfa93cbf799f325fa Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 28 Nov 2025 22:48:58 -0500 Subject: [PATCH 01/34] refac: db group --- backend/open_webui/models/groups.py | 114 ++++++++++++++++--- backend/open_webui/routers/groups.py | 28 ++--- backend/open_webui/routers/scim.py | 2 +- backend/open_webui/utils/oauth.py | 4 +- src/lib/components/admin/Users/Groups.svelte | 1 + 5 files changed, 109 insertions(+), 40 deletions(-) diff --git a/backend/open_webui/models/groups.py b/backend/open_webui/models/groups.py index e5c0612639..a7900e2c78 100644 --- a/backend/open_webui/models/groups.py +++ b/backend/open_webui/models/groups.py @@ -11,7 +11,18 @@ from open_webui.models.files import FileMetadataResponse from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, String, Text, JSON, func, ForeignKey +from sqlalchemy import ( + BigInteger, + Column, + String, + Text, + JSON, + and_, + func, + ForeignKey, + cast, + or_, +) log = logging.getLogger(__name__) @@ -41,7 +52,6 @@ class Group(Base): class GroupModel(BaseModel): - model_config = ConfigDict(from_attributes=True) id: str user_id: str @@ -56,6 +66,8 @@ class GroupModel(BaseModel): created_at: int # timestamp in epoch updated_at: int # timestamp in epoch + model_config = ConfigDict(from_attributes=True) + class GroupMember(Base): __tablename__ = "group_member" @@ -84,17 +96,8 @@ class GroupMemberModel(BaseModel): #################### -class GroupResponse(BaseModel): - id: str - user_id: str - name: str - description: str - permissions: Optional[dict] = None - data: Optional[dict] = None - meta: Optional[dict] = None +class GroupResponse(GroupModel): member_count: Optional[int] = None - created_at: int # timestamp in epoch - updated_at: int # timestamp in epoch class GroupForm(BaseModel): @@ -112,6 +115,11 @@ class GroupUpdateForm(GroupForm): pass +class GroupListResponse(BaseModel): + items: list[GroupResponse] = [] + total: int = 0 + + class GroupTable: def insert_new_group( self, user_id: str, form_data: GroupForm @@ -140,13 +148,87 @@ class GroupTable: except Exception: return None - def get_groups(self) -> list[GroupModel]: + def get_all_groups(self) -> list[GroupModel]: with get_db() as db: + groups = db.query(Group).order_by(Group.updated_at.desc()).all() + return [GroupModel.model_validate(group) for group in groups] + + def get_groups(self, filter) -> list[GroupResponse]: + with get_db() as db: + query = db.query(Group) + + if filter: + if "query" in filter: + query = query.filter(Group.name.ilike(f"%{filter['query']}%")) + if "member_id" in filter: + query = query.join( + GroupMember, GroupMember.group_id == Group.id + ).filter(GroupMember.user_id == filter["member_id"]) + + if "share" in filter: + share_value = filter["share"] + json_share = Group.data["config"]["share"].as_boolean() + + if share_value: + query = query.filter( + or_( + Group.data.is_(None), + json_share.is_(None), + json_share == True, + ) + ) + else: + query = query.filter( + and_(Group.data.isnot(None), json_share == False) + ) + groups = query.order_by(Group.updated_at.desc()).all() return [ - GroupModel.model_validate(group) - for group in db.query(Group).order_by(Group.updated_at.desc()).all() + GroupResponse.model_validate( + { + **GroupModel.model_validate(group).model_dump(), + "member_count": self.get_group_member_count_by_id(group.id), + } + ) + for group in groups ] + def search_groups( + self, filter: Optional[dict] = None, skip: int = 0, limit: int = 30 + ) -> GroupListResponse: + with get_db() as db: + query = db.query(Group) + + if filter: + if "query" in filter: + query = query.filter(Group.name.ilike(f"%{filter['query']}%")) + if "member_id" in filter: + query = query.join( + GroupMember, GroupMember.group_id == Group.id + ).filter(GroupMember.user_id == filter["member_id"]) + + if "share" in filter: + # 'share' is stored in data JSON, support both sqlite and postgres + share_value = filter["share"] + print("Filtering by share:", share_value) + query = query.filter( + Group.data.op("->>")("share") == str(share_value) + ) + + total = query.count() + query = query.order_by(Group.updated_at.desc()) + groups = query.offset(skip).limit(limit).all() + + return { + "items": [ + GroupResponse.model_validate( + **GroupModel.model_validate(group).model_dump(), + member_count=self.get_group_member_count_by_id(group.id), + ) + for group in groups + ], + "total": total, + } + def get_groups_by_member_id(self, user_id: str) -> list[GroupModel]: with get_db() as db: return [ @@ -293,7 +375,7 @@ class GroupTable: ) -> list[GroupModel]: # check for existing groups - existing_groups = self.get_groups() + existing_groups = self.get_all_groups() existing_group_names = {group.name for group in existing_groups} new_groups = [] diff --git a/backend/open_webui/routers/groups.py b/backend/open_webui/routers/groups.py index b68db3a15e..05d52c5c7b 100755 --- a/backend/open_webui/routers/groups.py +++ b/backend/open_webui/routers/groups.py @@ -32,31 +32,17 @@ router = APIRouter() @router.get("/", response_model=list[GroupResponse]) async def get_groups(share: Optional[bool] = None, user=Depends(get_verified_user)): - if user.role == "admin": - groups = Groups.get_groups() - else: - groups = Groups.get_groups_by_member_id(user.id) - group_list = [] + filter = {} + if user.role != "admin": + filter["member_id"] = user.id - for group in groups: - if share is not None: - # Check if the group has data and a config with share key - if ( - group.data - and "share" in group.data.get("config", {}) - and group.data["config"]["share"] != share - ): - continue + if share is not None: + filter["share"] = share - group_list.append( - GroupResponse( - **group.model_dump(), - member_count=Groups.get_group_member_count_by_id(group.id), - ) - ) + groups = Groups.get_groups(filter=filter) - return group_list + return groups ############################ diff --git a/backend/open_webui/routers/scim.py b/backend/open_webui/routers/scim.py index b5d0e029ec..c2ee4d1c35 100644 --- a/backend/open_webui/routers/scim.py +++ b/backend/open_webui/routers/scim.py @@ -719,7 +719,7 @@ async def get_groups( ): """List SCIM Groups""" # Get all groups - groups_list = Groups.get_groups() + groups_list = Groups.get_all_groups() # Apply pagination total = len(groups_list) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 6bd955e90c..9cd329a861 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -1102,7 +1102,7 @@ class OAuthManager: user_oauth_groups = [] user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id) - all_available_groups: list[GroupModel] = Groups.get_groups() + all_available_groups: list[GroupModel] = Groups.get_all_groups() # Create groups if they don't exist and creation is enabled if auth_manager_config.ENABLE_OAUTH_GROUP_CREATION: @@ -1146,7 +1146,7 @@ class OAuthManager: # Refresh the list of all available groups if any were created if groups_created: - all_available_groups = Groups.get_groups() + all_available_groups = Groups.get_all_groups() log.debug("Refreshed list of all available groups after creation.") log.debug(f"Oauth Groups claim: {oauth_claim}") diff --git a/src/lib/components/admin/Users/Groups.svelte b/src/lib/components/admin/Users/Groups.svelte index 65e4d4d120..3239a3f462 100644 --- a/src/lib/components/admin/Users/Groups.svelte +++ b/src/lib/components/admin/Users/Groups.svelte @@ -100,6 +100,7 @@ From ff121413da6101791090119fc27824b78c950ef0 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 28 Nov 2025 22:54:00 -0500 Subject: [PATCH 02/34] refac --- backend/open_webui/models/models.py | 37 +++++++++++++++-------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index 8ddcf59d39..6d3d9858bc 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -221,29 +221,30 @@ class ModelsTable: ] def _has_write_permission(self, query, filter: dict): - if filter.get("group_ids") or filter.get("user_id"): - conditions = [] + group_ids = filter.get("group_ids", []) + user_id = filter.get("user_id") - # --- ANY group_ids match ("write".group_ids) --- - if filter.get("group_ids"): - group_ids = filter["group_ids"] - like_clauses = [] + json_group_ids = Model.access_control["write"]["group_ids"] - for gid in group_ids: - like_clauses.append( - cast(Model.access_control, String).like( - f'%"write"%"group_ids"%"{gid}"%' - ) - ) + conditions = [] + if group_ids or user_id: + conditions.append(Model.access_control.is_(None)) - # ANY → OR - conditions.append(or_(*like_clauses)) + if user_id: + conditions.append(Model.user_id == user_id) - # --- user_id match (owner) --- - if filter.get("user_id"): - conditions.append(Model.user_id == filter["user_id"]) + if group_ids: + group_conditions = [] - # Apply OR across the two groups of conditions + for gid in group_ids: + # CASE: gid IN JSON array + # SQLite → json_extract(access_control, '$.write.group_ids') LIKE '%gid%' + # Postgres → access_control->'write'->'group_ids' @> '[gid]' + group_conditions.append(json_group_ids.contains([gid])) + + conditions.append(or_(*group_conditions)) + + if conditions: query = query.filter(or_(*conditions)) return query From 6c53bf71751db700615470340f54cdff69c1ee3f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 28 Nov 2025 23:10:01 -0500 Subject: [PATCH 03/34] refac: styling --- src/lib/components/layout/Sidebar.svelte | 48 ++++++++++++++++++------ 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index 48e707302a..879b44a1c4 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -562,7 +562,7 @@ {#if !$mobile && !$showSidebar} - - - -
- -
- {#if accessGroups.length > 0} + {#if accessGroups.length > 0} +
{#each accessGroups as group} -
-
+
+
- -
- -
- {group.name} + {group.name} {group?.member_count}
@@ -260,13 +210,46 @@
{/each} - {:else} -
-
- {$i18n.t('No groups with access, add a group to grant access')} +
+ {/if} + + + +
+
+
+
+
- {/if} +
From 9d39b9b42c653ee2acf2674b2df343ecbceb4954 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 30 Nov 2025 03:47:34 -0500 Subject: [PATCH 12/34] refac: styling --- src/lib/components/AddToolServerModal.svelte | 6 +- .../Evaluations/ArenaModelModal.svelte | 6 +- .../Knowledge/CreateKnowledgeBase.svelte | 14 +- .../workspace/Models/ActionsSelector.svelte | 2 +- .../workspace/Models/Capabilities.svelte | 2 +- .../workspace/Models/DefaultFeatures.svelte | 2 +- .../workspace/Models/FiltersSelector.svelte | 2 +- .../workspace/Models/Knowledge.svelte | 12 +- .../workspace/Models/ModelEditor.svelte | 290 ++++++++-------- .../workspace/Models/PromptSuggestions.svelte | 311 ++++++++---------- .../workspace/Models/ToolsSelector.svelte | 14 +- .../workspace/common/AccessControl.svelte | 8 +- .../workspace/common/Visibility.svelte | 79 +++++ 13 files changed, 409 insertions(+), 339 deletions(-) create mode 100644 src/lib/components/workspace/common/Visibility.svelte diff --git a/src/lib/components/AddToolServerModal.svelte b/src/lib/components/AddToolServerModal.svelte index 79fe4c97fc..c9b91e2276 100644 --- a/src/lib/components/AddToolServerModal.svelte +++ b/src/lib/components/AddToolServerModal.svelte @@ -818,10 +818,8 @@
-
-
- -
+
+
{/if}
diff --git a/src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte b/src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte index e3d702e6aa..2714d4681c 100644 --- a/src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte +++ b/src/lib/components/admin/Settings/Evaluations/ArenaModelModal.svelte @@ -292,10 +292,8 @@
-
-
- -
+
+

diff --git a/src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte index 582c2f68b4..2e729f4968 100644 --- a/src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte +++ b/src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte @@ -112,14 +112,12 @@
-
- -
+
diff --git a/src/lib/components/workspace/Models/ActionsSelector.svelte b/src/lib/components/workspace/Models/ActionsSelector.svelte index f936988b30..444b5476fe 100644 --- a/src/lib/components/workspace/Models/ActionsSelector.svelte +++ b/src/lib/components/workspace/Models/ActionsSelector.svelte @@ -25,7 +25,7 @@ {#if actions.length > 0}
-
{$i18n.t('Actions')}
+
{$i18n.t('Actions')}
diff --git a/src/lib/components/workspace/Models/Capabilities.svelte b/src/lib/components/workspace/Models/Capabilities.svelte index 0628a377df..f1e9741dfe 100644 --- a/src/lib/components/workspace/Models/Capabilities.svelte +++ b/src/lib/components/workspace/Models/Capabilities.svelte @@ -57,7 +57,7 @@
-
{$i18n.t('Capabilities')}
+
{$i18n.t('Capabilities')}
{#each Object.keys(capabilityLabels) as capability} diff --git a/src/lib/components/workspace/Models/DefaultFeatures.svelte b/src/lib/components/workspace/Models/DefaultFeatures.svelte index 01826fc130..7da1d5b99c 100644 --- a/src/lib/components/workspace/Models/DefaultFeatures.svelte +++ b/src/lib/components/workspace/Models/DefaultFeatures.svelte @@ -27,7 +27,7 @@
-
{$i18n.t('Default Features')}
+
{$i18n.t('Default Features')}
{#each availableFeatures as feature} diff --git a/src/lib/components/workspace/Models/FiltersSelector.svelte b/src/lib/components/workspace/Models/FiltersSelector.svelte index 02216ca248..c5207e61b7 100644 --- a/src/lib/components/workspace/Models/FiltersSelector.svelte +++ b/src/lib/components/workspace/Models/FiltersSelector.svelte @@ -25,7 +25,7 @@ {#if filters.length > 0}
-
{$i18n.t('Filters')}
+
{$i18n.t('Filters')}
diff --git a/src/lib/components/workspace/Models/Knowledge.svelte b/src/lib/components/workspace/Models/Knowledge.svelte index 9d95d744dd..5c92859000 100644 --- a/src/lib/components/workspace/Models/Knowledge.svelte +++ b/src/lib/components/workspace/Models/Knowledge.svelte @@ -157,18 +157,14 @@
-
+
{$i18n.t('Knowledge')}
- -
- {$i18n.t('To attach knowledge base here, add them to the "Knowledge" workspace first.')} -
-
+
{#if selectedItems?.length > 0}
{#each selectedItems as file, fileIdx} @@ -228,4 +224,8 @@ {/if}
+ +
+ {$i18n.t('To attach knowledge base here, add them to the "Knowledge" workspace first.')} +
diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte index 7bdb5a0261..20d8865534 100644 --- a/src/lib/components/workspace/Models/ModelEditor.svelte +++ b/src/lib/components/workspace/Models/ModelEditor.svelte @@ -23,6 +23,8 @@ import DefaultFiltersSelector from './DefaultFiltersSelector.svelte'; import DefaultFeatures from './DefaultFeatures.svelte'; import PromptSuggestions from './PromptSuggestions.svelte'; + import AccessControlModal from '../common/AccessControlModal.svelte'; + import LockClosed from '$lib/components/icons/LockClosed.svelte'; const i18n = getContext('i18n'); @@ -42,6 +44,7 @@ let showAdvanced = false; let showPreview = false; + let showAccessControlModal = false; let loaded = false; @@ -317,6 +320,14 @@ {#if loaded} + + {#if onBack}
-
-
-
- +
+
+
+
+
+ +
+
+ +
+
+ +
+
-
- -
-
- -
-
-
- - {#if preset} -
-
{$i18n.t('Base Model (From)')}
- + + +
+ {$i18n.t('Access')} +
+
- {/if} -
-
-
{$i18n.t('Description')}
+ {#if preset} +
+
+ {$i18n.t('Base Model (From)')} +
- -
- - {#if enableDescription} -