Pārlūkot izejas kodu

enh/refac: permissions

Timothy Jaeryang Baek 3 mēneši atpakaļ
vecāks
revīzija
56f57928c2

+ 34 - 14
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"
     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 = (
 USER_PERMISSIONS_CHAT_FILE_UPLOAD = (
     os.environ.get("USER_PERMISSIONS_CHAT_FILE_UPLOAD", "True").lower() == "true"
     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"
     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 = PersistentConfig(
     "USER_PERMISSIONS",
     "USER_PERMISSIONS",
     "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(
 ENABLE_CHANNELS = PersistentConfig(

+ 27 - 10
backend/open_webui/routers/users.py

@@ -62,27 +62,44 @@ async def get_user_permissisions(user=Depends(get_verified_user)):
 # User Default Permissions
 # User Default Permissions
 ############################
 ############################
 class WorkspacePermissions(BaseModel):
 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):
 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):
 class UserPermissions(BaseModel):
     workspace: WorkspacePermissions
     workspace: WorkspacePermissions
     chat: ChatPermissions
     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)):
 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")
 @router.post("/default/permissions")

+ 37 - 7
backend/open_webui/utils/access_control.py

@@ -1,9 +1,30 @@
 from typing import Optional, Union, List, Dict, Any
 from typing import Optional, Union, List, Dict, Any
 from open_webui.models.users import Users, UserModel
 from open_webui.models.users import Users, UserModel
 from open_webui.models.groups import Groups
 from open_webui.models.groups import Groups
+
+
+from open_webui.config import DEFAULT_USER_PERMISSIONS
 import json
 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(
 def get_permissions(
     user_id: str,
     user_id: str,
     default_permissions: Dict[str, Any],
     default_permissions: Dict[str, Any],
@@ -27,39 +48,45 @@ def get_permissions(
                 if key not in permissions:
                 if key not in permissions:
                     permissions[key] = value
                     permissions[key] = value
                 else:
                 else:
-                    permissions[key] = permissions[key] or value
+                    permissions[key] = (
+                        permissions[key] or value
+                    )  # Use the most permissive value (True > False)
         return permissions
         return permissions
 
 
     user_groups = Groups.get_groups_by_member_id(user_id)
     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))
     permissions = json.loads(json.dumps(default_permissions))
 
 
+    # Combine permissions from all user groups
     for group in user_groups:
     for group in user_groups:
         group_permissions = group.permissions
         group_permissions = group.permissions
         permissions = combine_permissions(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
     return permissions
 
 
 
 
 def has_permission(
 def has_permission(
     user_id: str,
     user_id: str,
     permission_key: str,
     permission_key: str,
-    default_permissions: Dict[str, bool] = {},
+    default_permissions: Dict[str, Any] = {},
 ) -> bool:
 ) -> bool:
     """
     """
     Check if a user has a specific permission by checking the group permissions
     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 ('.').
     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)."""
         """Traverse permissions dict using a list of keys (from dot-split permission_key)."""
         for key in keys:
         for key in keys:
             if key not in permissions:
             if key not in permissions:
                 return False  # If any part of the hierarchy is missing, deny access
                 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
         return bool(permissions)  # Return the boolean at the final level
 
 
@@ -73,7 +100,10 @@ def has_permission(
         if get_permission(group_permissions, permission_hierarchy):
         if get_permission(group_permissions, permission_hierarchy):
             return True
             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)
     return get_permission(default_permissions, permission_hierarchy)
 
 
 
 

+ 5 - 0
src/lib/components/admin/Users/Groups.svelte

@@ -53,10 +53,15 @@
 			tools: false
 			tools: false
 		},
 		},
 		chat: {
 		chat: {
+			controls: true,
 			file_upload: true,
 			file_upload: true,
 			delete: true,
 			delete: true,
 			edit: true,
 			edit: true,
 			temporary: true
 			temporary: true
+		},
+		features: {
+			web_search: true,
+			image_generation: true
 		}
 		}
 	};
 	};
 
 

+ 7 - 14
src/lib/components/admin/Users/Groups/EditGroupModal.svelte

@@ -37,10 +37,15 @@
 			tools: false
 			tools: false
 		},
 		},
 		chat: {
 		chat: {
+			controls: true,
 			file_upload: true,
 			file_upload: true,
 			delete: true,
 			delete: true,
 			edit: true,
 			edit: true,
 			temporary: true
 			temporary: true
+		},
+		features: {
+			web_search: true,
+			image_generation: true
 		}
 		}
 	};
 	};
 	export let userIds = [];
 	export let userIds = [];
@@ -65,20 +70,8 @@
 		if (group) {
 		if (group) {
 			name = group.name;
 			name = group.name;
 			description = group.description;
 			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 ?? [];
 			userIds = group?.user_ids ?? [];
 		}
 		}
 	};
 	};

+ 59 - 2
src/lib/components/admin/Users/Groups/Permissions.svelte

@@ -1,11 +1,12 @@
 <script lang="ts">
 <script lang="ts">
-	import { getContext } from 'svelte';
+	import { getContext, onMount } from 'svelte';
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
 
-	export let permissions = {
+	// Default values for permissions
+	const defaultPermissions = {
 		workspace: {
 		workspace: {
 			models: false,
 			models: false,
 			knowledge: false,
 			knowledge: false,
@@ -13,12 +14,38 @@
 			tools: false
 			tools: false
 		},
 		},
 		chat: {
 		chat: {
+			controls: true,
 			delete: true,
 			delete: true,
 			edit: true,
 			edit: true,
 			temporary: true,
 			temporary: true,
 			file_upload: true
 			file_upload: true
+		},
+		features: {
+			web_search: true,
+			image_generation: true
 		}
 		}
 	};
 	};
+
+	export let permissions = {};
+
+	// Reactive statement to ensure all fields are present in `permissions`
+	$: {
+		permissions = fillMissingProperties(permissions, defaultPermissions);
+	}
+
+	function fillMissingProperties(obj: any, defaults: any) {
+		return {
+			...defaults,
+			...obj,
+			workspace: { ...defaults.workspace, ...obj.workspace },
+			chat: { ...defaults.chat, ...obj.chat },
+			features: { ...defaults.features, ...obj.features }
+		};
+	}
+
+	onMount(() => {
+		permissions = fillMissingProperties(permissions, defaultPermissions);
+	});
 </script>
 </script>
 
 
 <div>
 <div>
@@ -169,6 +196,14 @@
 	<div>
 	<div>
 		<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
 		<div class=" mb-2 text-sm font-medium">{$i18n.t('Chat Permissions')}</div>
 
 
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">
+				{$i18n.t('Allow Chat Controls')}
+			</div>
+
+			<Switch bind:state={permissions.chat.controls} />
+		</div>
+
 		<div class="  flex w-full justify-between my-2 pr-2">
 		<div class="  flex w-full justify-between my-2 pr-2">
 			<div class=" self-center text-xs font-medium">
 			<div class=" self-center text-xs font-medium">
 				{$i18n.t('Allow File Upload')}
 				{$i18n.t('Allow File Upload')}
@@ -201,4 +236,26 @@
 			<Switch bind:state={permissions.chat.temporary} />
 			<Switch bind:state={permissions.chat.temporary} />
 		</div>
 		</div>
 	</div>
 	</div>
+
+	<hr class=" border-gray-50 dark:border-gray-850 my-2" />
+
+	<div>
+		<div class=" mb-2 text-sm font-medium">{$i18n.t('Features Permissions')}</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">
+				{$i18n.t('Web Search')}
+			</div>
+
+			<Switch bind:state={permissions.features.web_search} />
+		</div>
+
+		<div class="  flex w-full justify-between my-2 pr-2">
+			<div class=" self-center text-xs font-medium">
+				{$i18n.t('Image Generation')}
+			</div>
+
+			<Switch bind:state={permissions.features.image_generation} />
+		</div>
+	</div>
 </div>
 </div>