enh/refac: permissions

This commit is contained in:
Timothy Jaeryang Baek 2025-01-15 23:01:43 -08:00
parent 2aa82d98cc
commit 56f57928c2
6 changed files with 169 additions and 47 deletions

View File

@ -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,10 +840,16 @@ USER_PERMISSIONS_CHAT_TEMPORARY = (
os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true"
)
USER_PERMISSIONS = PersistentConfig(
"USER_PERMISSIONS",
"user.permissions",
{
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,
@ -847,12 +857,22 @@ USER_PERMISSIONS = PersistentConfig(
"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",
DEFAULT_USER_PERMISSIONS,
)
ENABLE_CHANNELS = PersistentConfig(

View File

@ -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")

View File

@ -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)

View File

@ -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
}
};

View File

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

View File

@ -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>