mirror of
https://github.com/open-webui/open-webui
synced 2025-05-17 03:54:02 +00:00
enh/refac: permissions
This commit is contained in:
parent
2aa82d98cc
commit
56f57928c2
@ -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",
|
||||||
{
|
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": {
|
|
||||||
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
|
|
||||||
"delete": USER_PERMISSIONS_CHAT_DELETE,
|
|
||||||
"edit": USER_PERMISSIONS_CHAT_EDIT,
|
|
||||||
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ENABLE_CHANNELS = PersistentConfig(
|
ENABLE_CHANNELS = PersistentConfig(
|
||||||
|
@ -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
|
models: bool = False
|
||||||
knowledge: bool
|
knowledge: bool = False
|
||||||
prompts: bool
|
prompts: bool = False
|
||||||
tools: bool
|
tools: bool = False
|
||||||
|
|
||||||
|
|
||||||
class ChatPermissions(BaseModel):
|
class ChatPermissions(BaseModel):
|
||||||
file_upload: bool
|
controls: bool = True
|
||||||
delete: bool
|
file_upload: bool = True
|
||||||
edit: bool
|
delete: bool = True
|
||||||
temporary: bool
|
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")
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 ?? {
|
permissions = group?.permissions ?? {};
|
||||||
workspace: {
|
|
||||||
models: false,
|
|
||||||
knowledge: false,
|
|
||||||
prompts: false,
|
|
||||||
tools: false
|
|
||||||
},
|
|
||||||
chat: {
|
|
||||||
file_upload: true,
|
|
||||||
delete: true,
|
|
||||||
edit: true,
|
|
||||||
temporary: true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
userIds = group?.user_ids ?? [];
|
userIds = group?.user_ids ?? [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user