diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 0ce4c4574..e32fc1a17 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -820,6 +820,10 @@ USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = ( os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS", "False").lower() == "true" ) +USER_PERMISSIONS_CHAT_CONTROLS = ( + os.environ.get("USER_PERMISSIONS_CHAT_CONTROLS", "True").lower() == "true" +) + USER_PERMISSIONS_CHAT_FILE_UPLOAD = ( os.environ.get("USER_PERMISSIONS_CHAT_FILE_UPLOAD", "True").lower() == "true" ) @@ -836,23 +840,39 @@ USER_PERMISSIONS_CHAT_TEMPORARY = ( os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true" ) +USER_PERMISSIONS_FEATURES_WEB_SEARCH = ( + os.environ.get("USER_PERMISSIONS_FEATURES_WEB_SEARCH", "True").lower() == "true" +) + +USER_PERMISSIONS_FEATURES_IMAGE_GENERATION = ( + os.environ.get("USER_PERMISSIONS_FEATURES_IMAGE_GENERATION", "True").lower() + == "true" +) + +DEFAULT_USER_PERMISSIONS = { + "workspace": { + "models": USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS, + "knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS, + "prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS, + "tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS, + }, + "chat": { + "controls": USER_PERMISSIONS_CHAT_CONTROLS, + "file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD, + "delete": USER_PERMISSIONS_CHAT_DELETE, + "edit": USER_PERMISSIONS_CHAT_EDIT, + "temporary": USER_PERMISSIONS_CHAT_TEMPORARY, + }, + "features": { + "web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH, + "image_generation": USER_PERMISSIONS_FEATURES_IMAGE_GENERATION, + }, +} + USER_PERMISSIONS = PersistentConfig( "USER_PERMISSIONS", "user.permissions", - { - "workspace": { - "models": USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS, - "knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS, - "prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS, - "tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS, - }, - "chat": { - "file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD, - "delete": USER_PERMISSIONS_CHAT_DELETE, - "edit": USER_PERMISSIONS_CHAT_EDIT, - "temporary": USER_PERMISSIONS_CHAT_TEMPORARY, - }, - }, + DEFAULT_USER_PERMISSIONS, ) ENABLE_CHANNELS = PersistentConfig( diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 7006091e1..b37ad4b39 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -62,27 +62,44 @@ async def get_user_permissisions(user=Depends(get_verified_user)): # User Default Permissions ############################ class WorkspacePermissions(BaseModel): - models: bool - knowledge: bool - prompts: bool - tools: bool + models: bool = False + knowledge: bool = False + prompts: bool = False + tools: bool = False class ChatPermissions(BaseModel): - file_upload: bool - delete: bool - edit: bool - temporary: bool + controls: bool = True + file_upload: bool = True + delete: bool = True + edit: bool = True + temporary: bool = True + + +class FeaturesPermissions(BaseModel): + web_search: bool = True + image_generation: bool = True class UserPermissions(BaseModel): workspace: WorkspacePermissions chat: ChatPermissions + features: FeaturesPermissions -@router.get("/default/permissions") +@router.get("/default/permissions", response_model=UserPermissions) async def get_user_permissions(request: Request, user=Depends(get_admin_user)): - return request.app.state.config.USER_PERMISSIONS + return { + "workspace": WorkspacePermissions( + **request.app.state.config.USER_PERMISSIONS.get("workspace", {}) + ), + "chat": ChatPermissions( + **request.app.state.config.USER_PERMISSIONS.get("chat", {}) + ), + "features": FeaturesPermissions( + **request.app.state.config.USER_PERMISSIONS.get("features", {}) + ), + } @router.post("/default/permissions") diff --git a/backend/open_webui/utils/access_control.py b/backend/open_webui/utils/access_control.py index da61e7fb3..1699cfaa7 100644 --- a/backend/open_webui/utils/access_control.py +++ b/backend/open_webui/utils/access_control.py @@ -1,9 +1,30 @@ from typing import Optional, Union, List, Dict, Any from open_webui.models.users import Users, UserModel from open_webui.models.groups import Groups + + +from open_webui.config import DEFAULT_USER_PERMISSIONS import json +def fill_missing_permissions( + permissions: Dict[str, Any], default_permissions: Dict[str, Any] +) -> Dict[str, Any]: + """ + Recursively fills in missing properties in the permissions dictionary + using the default permissions as a template. + """ + for key, value in default_permissions.items(): + if key not in permissions: + permissions[key] = value + elif isinstance(value, dict) and isinstance( + permissions[key], dict + ): # Both are nested dictionaries + permissions[key] = fill_missing_permissions(permissions[key], value) + + return permissions + + def get_permissions( user_id: str, default_permissions: Dict[str, Any], @@ -27,39 +48,45 @@ def get_permissions( if key not in permissions: permissions[key] = value else: - permissions[key] = permissions[key] or value + permissions[key] = ( + permissions[key] or value + ) # Use the most permissive value (True > False) return permissions user_groups = Groups.get_groups_by_member_id(user_id) - # deep copy default permissions to avoid modifying the original dict + # Deep copy default permissions to avoid modifying the original dict permissions = json.loads(json.dumps(default_permissions)) + # Combine permissions from all user groups for group in user_groups: group_permissions = group.permissions permissions = combine_permissions(permissions, group_permissions) + # Ensure all fields from default_permissions are present and filled in + permissions = fill_missing_permissions(permissions, default_permissions) + return permissions def has_permission( user_id: str, permission_key: str, - default_permissions: Dict[str, bool] = {}, + default_permissions: Dict[str, Any] = {}, ) -> bool: """ Check if a user has a specific permission by checking the group permissions - and falls back to default permissions if not found in any group. + and fall back to default permissions if not found in any group. Permission keys can be hierarchical and separated by dots ('.'). """ - def get_permission(permissions: Dict[str, bool], keys: List[str]) -> bool: + def get_permission(permissions: Dict[str, Any], keys: List[str]) -> bool: """Traverse permissions dict using a list of keys (from dot-split permission_key).""" for key in keys: if key not in permissions: return False # If any part of the hierarchy is missing, deny access - permissions = permissions[key] # Go one level deeper + permissions = permissions[key] # Traverse one level deeper return bool(permissions) # Return the boolean at the final level @@ -73,7 +100,10 @@ def has_permission( if get_permission(group_permissions, permission_hierarchy): return True - # Check default permissions afterwards if the group permissions don't allow it + # Check default permissions afterward if the group permissions don't allow it + default_permissions = fill_missing_permissions( + default_permissions, DEFAULT_USER_PERMISSIONS + ) return get_permission(default_permissions, permission_hierarchy) diff --git a/src/lib/components/admin/Users/Groups.svelte b/src/lib/components/admin/Users/Groups.svelte index 5911718ba..33b084041 100644 --- a/src/lib/components/admin/Users/Groups.svelte +++ b/src/lib/components/admin/Users/Groups.svelte @@ -53,10 +53,15 @@ tools: false }, chat: { + controls: true, file_upload: true, delete: true, edit: true, temporary: true + }, + features: { + web_search: true, + image_generation: true } }; diff --git a/src/lib/components/admin/Users/Groups/EditGroupModal.svelte b/src/lib/components/admin/Users/Groups/EditGroupModal.svelte index 6d28db992..9558dbcad 100644 --- a/src/lib/components/admin/Users/Groups/EditGroupModal.svelte +++ b/src/lib/components/admin/Users/Groups/EditGroupModal.svelte @@ -37,10 +37,15 @@ tools: false }, chat: { + controls: true, file_upload: true, delete: true, edit: true, temporary: true + }, + features: { + web_search: true, + image_generation: true } }; export let userIds = []; @@ -65,20 +70,8 @@ if (group) { name = group.name; description = group.description; - permissions = group?.permissions ?? { - workspace: { - models: false, - knowledge: false, - prompts: false, - tools: false - }, - chat: { - file_upload: true, - delete: true, - edit: true, - temporary: true - } - }; + permissions = group?.permissions ?? {}; + userIds = group?.user_ids ?? []; } }; diff --git a/src/lib/components/admin/Users/Groups/Permissions.svelte b/src/lib/components/admin/Users/Groups/Permissions.svelte index 5d67ff768..430fabaac 100644 --- a/src/lib/components/admin/Users/Groups/Permissions.svelte +++ b/src/lib/components/admin/Users/Groups/Permissions.svelte @@ -1,11 +1,12 @@
@@ -169,6 +196,14 @@
{$i18n.t('Chat Permissions')}
+
+
+ {$i18n.t('Allow Chat Controls')} +
+ + +
+
{$i18n.t('Allow File Upload')} @@ -201,4 +236,26 @@
+ +
+ +
+
{$i18n.t('Features Permissions')}
+ +
+
+ {$i18n.t('Web Search')} +
+ + +
+ +
+
+ {$i18n.t('Image Generation')} +
+ + +
+