Browse Source

enh/refac: permissions

Timothy Jaeryang Baek 3 months ago
parent
commit
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"
 )
 
+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(

+ 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
 ############################
 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")

+ 37 - 7
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)
 
 

+ 5 - 0
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
 		}
 	};
 

+ 7 - 14
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 ?? [];
 		}
 	};

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

@@ -1,11 +1,12 @@
 <script lang="ts">
-	import { getContext } from 'svelte';
+	import { getContext, onMount } from 'svelte';
 	const i18n = getContext('i18n');
 
 	import Switch from '$lib/components/common/Switch.svelte';
 	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
-	export let permissions = {
+	// Default values for permissions
+	const defaultPermissions = {
 		workspace: {
 			models: false,
 			knowledge: false,
@@ -13,12 +14,38 @@
 			tools: false
 		},
 		chat: {
+			controls: true,
 			delete: true,
 			edit: true,
 			temporary: 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>
 
 <div>
@@ -169,6 +196,14 @@
 	<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=" self-center text-xs font-medium">
 				{$i18n.t('Allow File Upload')}
@@ -201,4 +236,26 @@
 			<Switch bind:state={permissions.chat.temporary} />
 		</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>