refac
This commit is contained in:
@@ -114,7 +114,7 @@ async def has_access(
|
||||
Check if a user has the specified permission using an in-memory access_grants list.
|
||||
|
||||
Used for config-driven resources (arena models, tool servers) that store
|
||||
access control as JSON in ConfigVar rather than in the access_grant DB table.
|
||||
access control as JSON config rather than in the access_grant DB table.
|
||||
|
||||
Semantics:
|
||||
- None or [] → private (owner-only, deny all)
|
||||
|
||||
@@ -39,6 +39,7 @@ from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
from open_webui.env import CUSTOM_API_KEY_HEADER
|
||||
from open_webui.internal.db import ScopedSession
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.utils.auth import get_http_authorization_cred
|
||||
from starlette.datastructures import MutableHeaders
|
||||
from starlette.requests import Request
|
||||
@@ -165,7 +166,7 @@ class AuthTokenMiddleware:
|
||||
token = HTTPAuthorizationCredentials(scheme='Bearer', credentials=api_key)
|
||||
|
||||
request.state.token = token
|
||||
request.state.enable_api_keys = self._fastapi_app.state.config.ENABLE_API_KEYS
|
||||
request.state.enable_api_keys = await Config.get('auth.enable_api_keys')
|
||||
|
||||
async def send_with_timing(message: Message) -> None:
|
||||
if message['type'] == 'http.response.start':
|
||||
|
||||
@@ -35,6 +35,7 @@ from open_webui.env import (
|
||||
pk,
|
||||
)
|
||||
from open_webui.models.auths import Auths
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.utils.access_control import has_permission
|
||||
from pytz import UTC
|
||||
@@ -409,12 +410,16 @@ async def get_current_user_by_api_key(request, api_key: str):
|
||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||
)
|
||||
|
||||
user_permissions = await Config.get('user.permissions')
|
||||
enable_endpoint_restrictions = await Config.get('auth.api_key.endpoint_restrictions')
|
||||
allowed_endpoints = await Config.get('auth.api_key.allowed_endpoints', '')
|
||||
|
||||
if not request.state.enable_api_keys or (
|
||||
user.role != 'admin'
|
||||
and not await has_permission(
|
||||
user.id,
|
||||
'features.api_keys',
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
user_permissions,
|
||||
)
|
||||
):
|
||||
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.API_KEY_NOT_ALLOWED)
|
||||
@@ -422,10 +427,8 @@ async def get_current_user_by_api_key(request, api_key: str):
|
||||
# Enforce endpoint restrictions — checked here (not in middleware)
|
||||
# so it applies regardless of how the API key was transported
|
||||
# (Authorization header, cookie, x-api-key header, etc.).
|
||||
if request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS:
|
||||
allowed_paths = [
|
||||
path.strip() for path in str(request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS).split(',') if path.strip()
|
||||
]
|
||||
if enable_endpoint_restrictions:
|
||||
allowed_paths = [path.strip() for path in str(allowed_endpoints).split(',') if path.strip()]
|
||||
request_path = request.scope['path'] # Use raw ASGI path — not spoofable via Host header (CVE-2026-48710)
|
||||
is_allowed = any(request_path == allowed or request_path.startswith(allowed + '/') for allowed in allowed_paths)
|
||||
if not is_allowed:
|
||||
|
||||
@@ -29,6 +29,7 @@ from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.internal.db import get_async_db
|
||||
from open_webui.models.automations import AutomationModel, AutomationRuns, Automations
|
||||
from open_webui.models.chats import ChatForm, Chats
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.models.users import Users
|
||||
from open_webui.utils.task import prompt_template
|
||||
from starlette.datastructures import Headers
|
||||
@@ -172,7 +173,7 @@ async def scheduler_worker_loop(app) -> None:
|
||||
while True:
|
||||
try:
|
||||
# ── Automations ──
|
||||
if getattr(app.state.config, 'ENABLE_AUTOMATIONS', False):
|
||||
if await Config.get('automations.enable'):
|
||||
try:
|
||||
async with get_async_db() as db:
|
||||
batch = await Automations.claim_due(int(time.time_ns()), limit=10, db=db)
|
||||
@@ -184,7 +185,7 @@ async def scheduler_worker_loop(app) -> None:
|
||||
log.exception('Scheduler: automation error')
|
||||
|
||||
# ── Calendar Alerts ──
|
||||
if getattr(app.state.config, 'ENABLE_CALENDAR', False):
|
||||
if await Config.get('calendar.enable'):
|
||||
try:
|
||||
await _check_calendar_alerts(app)
|
||||
except Exception:
|
||||
@@ -239,7 +240,7 @@ def _resolve_model_tool_ids(app, model_id: str) -> list[str]:
|
||||
return list(tool_ids) if tool_ids else []
|
||||
|
||||
|
||||
def _resolve_model_features(app, model_id: str) -> dict:
|
||||
async def _resolve_model_features(app, model_id: str) -> dict:
|
||||
"""Read model default features from model config.
|
||||
|
||||
The frontend does this in Chat.svelte (model.info.meta.defaultFeatureIds
|
||||
@@ -256,14 +257,13 @@ def _resolve_model_features(app, model_id: str) -> dict:
|
||||
return {}
|
||||
|
||||
capabilities = meta.get('capabilities', {})
|
||||
config = app.state.config
|
||||
features = {}
|
||||
|
||||
# code_interpreter is excluded: it requires the frontend event emitter
|
||||
# and does not work in headless backend execution.
|
||||
feature_checks = {
|
||||
'web_search': getattr(config, 'ENABLE_WEB_SEARCH', False),
|
||||
'image_generation': getattr(config, 'ENABLE_IMAGE_GENERATION', False),
|
||||
'web_search': await Config.get('rag.web.search.enable'),
|
||||
'image_generation': await Config.get('image_generation.enable'),
|
||||
}
|
||||
|
||||
for feature_id in default_feature_ids:
|
||||
@@ -364,7 +364,7 @@ async def execute_automation(app, automation: AutomationModel) -> None:
|
||||
|
||||
if user.role not in ('user', 'admin') or (
|
||||
user.role != 'admin'
|
||||
and not await has_permission(user.id, 'features.automations', app.state.config.USER_PERMISSIONS)
|
||||
and not await has_permission(user.id, 'features.automations', await Config.get('user.permissions'))
|
||||
):
|
||||
await _record_run(automation.id, 'error', error='Owner no longer permitted to run automations')
|
||||
return
|
||||
@@ -436,7 +436,7 @@ async def execute_automation(app, automation: AutomationModel) -> None:
|
||||
|
||||
# Resolve model defaults (frontend does this, backend doesn't)
|
||||
tool_ids = _resolve_model_tool_ids(app, model_id)
|
||||
features = _resolve_model_features(app, model_id)
|
||||
features = await _resolve_model_features(app, model_id)
|
||||
filter_ids = _resolve_model_filter_ids(app, model_id)
|
||||
|
||||
# Resolve terminal from model config
|
||||
@@ -561,7 +561,7 @@ async def _check_calendar_alerts(app) -> None:
|
||||
# Send webhook notification if user has one configured
|
||||
try:
|
||||
webui_name = getattr(app.state, 'WEBUI_NAME', 'Open WebUI')
|
||||
enable_user_webhooks = getattr(app.state.config, 'ENABLE_USER_WEBHOOKS', False)
|
||||
enable_user_webhooks = await Config.get('ui.enable_user_webhooks')
|
||||
|
||||
if enable_user_webhooks:
|
||||
user = await Users.get_user_by_id(event.user_id)
|
||||
|
||||
@@ -40,6 +40,7 @@ from open_webui.env import (
|
||||
RAG_SYSTEM_CONTEXT,
|
||||
)
|
||||
from open_webui.models.chats import Chats
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.models.folders import Folders
|
||||
from open_webui.models.functions import Functions
|
||||
from open_webui.models.models import Models
|
||||
@@ -980,13 +981,13 @@ async def apply_source_context_to_messages(
|
||||
|
||||
if RAG_SYSTEM_CONTEXT:
|
||||
return add_or_update_system_message(
|
||||
await rag_template(request.app.state.config.RAG_TEMPLATE, context, user_message),
|
||||
await rag_template(await Config.get('rag.template'), context, user_message),
|
||||
messages,
|
||||
append=True,
|
||||
)
|
||||
else:
|
||||
return add_or_update_user_message(
|
||||
await rag_template(request.app.state.config.RAG_TEMPLATE, context, user_message),
|
||||
await rag_template(await Config.get('rag.template'), context, user_message),
|
||||
messages,
|
||||
append=False,
|
||||
)
|
||||
@@ -1290,8 +1291,8 @@ async def chat_completion_tools_handler(
|
||||
|
||||
task_model_id = get_task_model_id(
|
||||
body['model'],
|
||||
request.app.state.config.TASK_MODEL,
|
||||
request.app.state.config.TASK_MODEL_EXTERNAL,
|
||||
await Config.get('task.model.default'),
|
||||
await Config.get('task.model.external'),
|
||||
models,
|
||||
)
|
||||
|
||||
@@ -1301,8 +1302,8 @@ async def chat_completion_tools_handler(
|
||||
specs = [tool['spec'] for tool in tools.values()]
|
||||
tools_specs = json.dumps(specs, ensure_ascii=False)
|
||||
|
||||
if request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE != '':
|
||||
template = request.app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||
if await Config.get('task.tools.prompt_template') != '':
|
||||
template = await Config.get('task.tools.prompt_template')
|
||||
else:
|
||||
template = DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
|
||||
|
||||
@@ -1792,7 +1793,7 @@ async def chat_image_generation_handler(request: Request, form_data: dict, extra
|
||||
|
||||
system_message_content = ''
|
||||
|
||||
if len(input_images) > 0 and request.app.state.config.ENABLE_IMAGE_EDIT:
|
||||
if len(input_images) > 0 and await Config.get('images.edit.enable'):
|
||||
# Edit image(s)
|
||||
try:
|
||||
images = await image_edits(
|
||||
@@ -1852,7 +1853,7 @@ async def chat_image_generation_handler(request: Request, form_data: dict, extra
|
||||
|
||||
else:
|
||||
# Create image(s)
|
||||
if request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION:
|
||||
if await Config.get('image_generation.prompt.enable'):
|
||||
try:
|
||||
res = await generate_image_prompt(
|
||||
request,
|
||||
@@ -2018,17 +2019,17 @@ async def chat_completion_files_handler(
|
||||
embedding_function=lambda query, prefix: request.app.state.EMBEDDING_FUNCTION(
|
||||
query, prefix=prefix, user=user
|
||||
),
|
||||
k=request.app.state.config.TOP_K,
|
||||
k=await Config.get('rag.top_k'),
|
||||
reranking_function=(
|
||||
(lambda query, documents: request.app.state.RERANKING_FUNCTION(query, documents, user=user))
|
||||
if request.app.state.RERANKING_FUNCTION
|
||||
else None
|
||||
),
|
||||
k_reranker=request.app.state.config.TOP_K_RERANKER,
|
||||
r=request.app.state.config.RELEVANCE_THRESHOLD,
|
||||
hybrid_bm25_weight=request.app.state.config.HYBRID_BM25_WEIGHT,
|
||||
hybrid_search=request.app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
||||
full_context=all_full_context or request.app.state.config.RAG_FULL_CONTEXT,
|
||||
k_reranker=await Config.get('rag.top_k_reranker'),
|
||||
r=await Config.get('rag.relevance_threshold'),
|
||||
hybrid_bm25_weight=await Config.get('rag.hybrid_bm25_weight'),
|
||||
hybrid_search=await Config.get('rag.enable_hybrid_search'),
|
||||
full_context=all_full_context or await Config.get('rag.full_context'),
|
||||
user=user,
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -2269,7 +2270,7 @@ async def connect_mcp_server(
|
||||
Returns None if the server is not found or access is denied.
|
||||
"""
|
||||
mcp_server_connection = None
|
||||
for server_connection in request.app.state.config.TOOL_SERVER_CONNECTIONS:
|
||||
for server_connection in await Config.get('tool_server.connections', []):
|
||||
if server_connection.get('type', '') == 'mcp' and server_connection.get('info', {}).get('id') == server_id:
|
||||
mcp_server_connection = server_connection
|
||||
break
|
||||
@@ -2442,8 +2443,8 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
|
||||
task_model_id = get_task_model_id(
|
||||
form_data['model'],
|
||||
request.app.state.config.TASK_MODEL,
|
||||
request.app.state.config.TASK_MODEL_EXTERNAL,
|
||||
await Config.get('task.model.default'),
|
||||
await Config.get('task.model.external'),
|
||||
models,
|
||||
)
|
||||
|
||||
@@ -2550,9 +2551,9 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
extra_params['__features__'] = features
|
||||
if features:
|
||||
if 'voice' in features and features['voice']:
|
||||
if getattr(request.app.state.config, 'ENABLE_VOICE_MODE_PROMPT', True):
|
||||
if request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE:
|
||||
template = request.app.state.config.VOICE_MODE_PROMPT_TEMPLATE
|
||||
if await Config.get('task.voice.prompt.enable'):
|
||||
if await Config.get('task.voice.prompt_template'):
|
||||
template = await Config.get('task.voice.prompt_template')
|
||||
else:
|
||||
template = DEFAULT_VOICE_MODE_PROMPT_TEMPLATE
|
||||
|
||||
@@ -2577,14 +2578,14 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||
form_data = await chat_image_generation_handler(request, form_data, extra_params, user)
|
||||
|
||||
if 'code_interpreter' in features and features['code_interpreter']:
|
||||
engine = getattr(request.app.state.config, 'CODE_INTERPRETER_ENGINE', 'pyodide')
|
||||
engine = await Config.get('code_interpreter.engine', 'pyodide')
|
||||
|
||||
# Skip XML-tag prompt injection when native FC is enabled —
|
||||
# execute_code will be injected as a builtin tool instead
|
||||
if metadata.get('params', {}).get('function_calling') == 'legacy':
|
||||
prompt = (
|
||||
request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE
|
||||
if request.app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE != ''
|
||||
await Config.get('code_interpreter.prompt_template')
|
||||
if await Config.get('code_interpreter.prompt_template') != ''
|
||||
else DEFAULT_CODE_INTERPRETER_PROMPT
|
||||
)
|
||||
|
||||
@@ -3534,18 +3535,19 @@ async def non_streaming_chat_response_handler(response, ctx):
|
||||
)
|
||||
|
||||
# Send a webhook notification if the user is not active
|
||||
if request.app.state.config.ENABLE_USER_WEBHOOKS and not await Users.is_user_active(user.id):
|
||||
if await Config.get('ui.enable_user_webhooks') and not await Users.is_user_active(user.id):
|
||||
webhook_url = await Users.get_user_webhook_url_by_id(user.id)
|
||||
if webhook_url:
|
||||
webui_url = await Config.get('webui.url')
|
||||
await post_webhook(
|
||||
request.app.state.WEBUI_NAME,
|
||||
webhook_url,
|
||||
f'{content}\n\n{title} - {request.app.state.config.WEBUI_URL}/c/{metadata["chat_id"]}',
|
||||
f'{content}\n\n{title} - {webui_url}/c/{metadata["chat_id"]}',
|
||||
{
|
||||
'action': 'chat',
|
||||
'message': content,
|
||||
'title': title,
|
||||
'url': f'{request.app.state.config.WEBUI_URL}/c/{metadata["chat_id"]}',
|
||||
'url': f'{webui_url}/c/{metadata["chat_id"]}',
|
||||
},
|
||||
)
|
||||
|
||||
@@ -3883,14 +3885,14 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
DETECT_CODE_INTERPRETER = (
|
||||
bool(features.get('code_interpreter'))
|
||||
and builtin_tools_meta.get('code_interpreter', True)
|
||||
and getattr(request.app.state.config, 'ENABLE_CODE_INTERPRETER', True)
|
||||
and await Config.get('code_interpreter.enable')
|
||||
and model_capabilities.get('code_interpreter', True)
|
||||
and (
|
||||
getattr(user, 'role', None) == 'admin'
|
||||
or await has_permission(
|
||||
getattr(user, 'id', ''),
|
||||
'features.code_interpreter',
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
await Config.get('user.permissions'),
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -4784,7 +4786,7 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
source_context = source_context.strip()
|
||||
if source_context:
|
||||
rag_content = await rag_template(
|
||||
request.app.state.config.RAG_TEMPLATE,
|
||||
await Config.get('rag.template'),
|
||||
source_context,
|
||||
user_message,
|
||||
)
|
||||
@@ -4978,7 +4980,7 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
""")
|
||||
code = blocking_code + '\n' + code
|
||||
|
||||
if request.app.state.config.CODE_INTERPRETER_ENGINE == 'pyodide':
|
||||
if await Config.get('code_interpreter.engine') == 'pyodide':
|
||||
ci_output = await event_caller(
|
||||
{
|
||||
'type': 'execute:python',
|
||||
@@ -4990,21 +4992,21 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
},
|
||||
}
|
||||
)
|
||||
elif request.app.state.config.CODE_INTERPRETER_ENGINE == 'jupyter':
|
||||
elif await Config.get('code_interpreter.engine') == 'jupyter':
|
||||
ci_output = await execute_code_jupyter(
|
||||
request.app.state.config.CODE_INTERPRETER_JUPYTER_URL,
|
||||
await Config.get('code_interpreter.jupyter.url'),
|
||||
code,
|
||||
(
|
||||
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN
|
||||
if request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH == 'token'
|
||||
await Config.get('code_interpreter.jupyter.auth_token')
|
||||
if await Config.get('code_interpreter.jupyter.auth') == 'token'
|
||||
else None
|
||||
),
|
||||
(
|
||||
request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
|
||||
if request.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH == 'password'
|
||||
await Config.get('code_interpreter.jupyter.auth_password')
|
||||
if await Config.get('code_interpreter.jupyter.auth') == 'password'
|
||||
else None
|
||||
),
|
||||
request.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
|
||||
await Config.get('code_interpreter.jupyter.timeout'),
|
||||
)
|
||||
else:
|
||||
ci_output = {'stdout': 'Code interpreter engine not configured.'}
|
||||
@@ -5148,18 +5150,19 @@ async def streaming_chat_response_handler(response, ctx):
|
||||
)
|
||||
|
||||
# Send a webhook notification if the user is not active
|
||||
if request.app.state.config.ENABLE_USER_WEBHOOKS and not await Users.is_user_active(user.id):
|
||||
if await Config.get('ui.enable_user_webhooks') and not await Users.is_user_active(user.id):
|
||||
webhook_url = await Users.get_user_webhook_url_by_id(user.id)
|
||||
if webhook_url:
|
||||
webui_url = await Config.get('webui.url')
|
||||
await post_webhook(
|
||||
request.app.state.WEBUI_NAME,
|
||||
webhook_url,
|
||||
f'{content}\n\n{title} - {request.app.state.config.WEBUI_URL}/c/{metadata["chat_id"]}',
|
||||
f'{content}\n\n{title} - {webui_url}/c/{metadata["chat_id"]}',
|
||||
{
|
||||
'action': 'chat',
|
||||
'message': content,
|
||||
'title': title,
|
||||
'url': f'{request.app.state.config.WEBUI_URL}/c/{metadata["chat_id"]}',
|
||||
'url': f'{webui_url}/c/{metadata["chat_id"]}',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from open_webui.config import (
|
||||
from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, GLOBAL_LOG_LEVEL
|
||||
from open_webui.functions import get_function_models
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.models.functions import Functions
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.models import Models
|
||||
@@ -52,14 +53,15 @@ async def fetch_openai_models(request: Request, user: UserModel = None):
|
||||
|
||||
|
||||
async def get_all_base_models(request: Request, user: UserModel = None):
|
||||
config = await Config.get_many('openai.enable', 'ollama.enable')
|
||||
openai_task = (
|
||||
fetch_openai_models(request, user)
|
||||
if request.app.state.config.ENABLE_OPENAI_API
|
||||
if config.get('openai.enable')
|
||||
else asyncio.sleep(0, result=[])
|
||||
)
|
||||
ollama_task = (
|
||||
fetch_ollama_models(request, user)
|
||||
if request.app.state.config.ENABLE_OLLAMA_API
|
||||
if config.get('ollama.enable')
|
||||
else asyncio.sleep(0, result=[])
|
||||
)
|
||||
function_task = get_function_models(request)
|
||||
@@ -70,10 +72,15 @@ async def get_all_base_models(request: Request, user: UserModel = None):
|
||||
|
||||
|
||||
async def get_all_models(request, refresh: bool = False, user: UserModel = None):
|
||||
config = await Config.get_many(
|
||||
'models.base_models_cache',
|
||||
'evaluation.arena.enable',
|
||||
'evaluation.arena.models',
|
||||
)
|
||||
if (
|
||||
request.app.state.MODELS
|
||||
and request.app.state.BASE_MODELS
|
||||
and (request.app.state.config.ENABLE_BASE_MODELS_CACHE and not refresh)
|
||||
and (config.get('models.base_models_cache') and not refresh)
|
||||
):
|
||||
base_models = request.app.state.BASE_MODELS
|
||||
else:
|
||||
@@ -88,9 +95,10 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
||||
return []
|
||||
|
||||
# Add arena models
|
||||
if request.app.state.config.ENABLE_EVALUATION_ARENA_MODELS:
|
||||
if config.get('evaluation.arena.enable'):
|
||||
arena_models = []
|
||||
if len(request.app.state.config.EVALUATION_ARENA_MODELS) > 0:
|
||||
arena_config = config.get('evaluation.arena.models') or []
|
||||
if len(arena_config) > 0:
|
||||
arena_models = [
|
||||
{
|
||||
'id': model['id'],
|
||||
@@ -103,7 +111,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
||||
'owned_by': 'arena',
|
||||
'arena': True,
|
||||
}
|
||||
for model in request.app.state.config.EVALUATION_ARENA_MODELS
|
||||
for model in arena_config
|
||||
]
|
||||
else:
|
||||
# Add default arena model
|
||||
@@ -289,7 +297,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
||||
|
||||
# Apply global model defaults to all models
|
||||
# Per-model overrides take precedence over global defaults
|
||||
default_metadata = getattr(request.app.state.config, 'DEFAULT_MODEL_METADATA', None) or {}
|
||||
default_metadata = await Config.get('models.default_metadata', {}) or {}
|
||||
|
||||
if default_metadata:
|
||||
for model in models:
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import base64
|
||||
import copy
|
||||
import fnmatch
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import re
|
||||
import secrets
|
||||
import sys
|
||||
import time
|
||||
import urllib
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
from typing import Literal, Optional
|
||||
|
||||
import aiohttp
|
||||
@@ -62,7 +61,6 @@ from open_webui.config import (
|
||||
OAUTH_UPDATE_PICTURE_ON_LOGIN,
|
||||
OAUTH_USERNAME_CLAIM,
|
||||
WEBHOOK_URL,
|
||||
AppConfig,
|
||||
)
|
||||
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
||||
from open_webui.env import (
|
||||
@@ -78,6 +76,7 @@ from open_webui.env import (
|
||||
WEBUI_NAME,
|
||||
)
|
||||
from open_webui.models.auths import Auths
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.models.groups import GroupForm, GroupModel, Groups, GroupUpdateForm
|
||||
from open_webui.models.oauth_sessions import OAuthSessions
|
||||
from open_webui.models.users import Users
|
||||
@@ -111,31 +110,73 @@ from open_webui.env import GLOBAL_LOG_LEVEL
|
||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
auth_manager_config = AppConfig()
|
||||
auth_manager_config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||
auth_manager_config.ENABLE_OAUTH_SIGNUP = ENABLE_OAUTH_SIGNUP
|
||||
auth_manager_config.OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE = OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE
|
||||
auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL = OAUTH_MERGE_ACCOUNTS_BY_EMAIL
|
||||
auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT = ENABLE_OAUTH_ROLE_MANAGEMENT
|
||||
auth_manager_config.ENABLE_OAUTH_GROUP_MANAGEMENT = ENABLE_OAUTH_GROUP_MANAGEMENT
|
||||
auth_manager_config.ENABLE_OAUTH_GROUP_CREATION = ENABLE_OAUTH_GROUP_CREATION
|
||||
auth_manager_config.OAUTH_GROUP_DEFAULT_SHARE = OAUTH_GROUP_DEFAULT_SHARE
|
||||
auth_manager_config.OAUTH_BLOCKED_GROUPS = OAUTH_BLOCKED_GROUPS
|
||||
auth_manager_config.OAUTH_ROLES_CLAIM = OAUTH_ROLES_CLAIM
|
||||
auth_manager_config.OAUTH_SUB_CLAIM = OAUTH_SUB_CLAIM
|
||||
auth_manager_config.OAUTH_GROUPS_CLAIM = OAUTH_GROUPS_CLAIM
|
||||
auth_manager_config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM
|
||||
auth_manager_config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
|
||||
auth_manager_config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
|
||||
auth_manager_config.OAUTH_ALLOWED_ROLES = OAUTH_ALLOWED_ROLES
|
||||
auth_manager_config.OAUTH_ADMIN_ROLES = OAUTH_ADMIN_ROLES
|
||||
auth_manager_config.OAUTH_ALLOWED_DOMAINS = OAUTH_ALLOWED_DOMAINS
|
||||
auth_manager_config.WEBHOOK_URL = WEBHOOK_URL
|
||||
auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
||||
auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN = OAUTH_UPDATE_PICTURE_ON_LOGIN
|
||||
auth_manager_config.OAUTH_UPDATE_NAME_ON_LOGIN = OAUTH_UPDATE_NAME_ON_LOGIN
|
||||
auth_manager_config.OAUTH_UPDATE_EMAIL_ON_LOGIN = OAUTH_UPDATE_EMAIL_ON_LOGIN
|
||||
auth_manager_config.OAUTH_AUDIENCE = OAUTH_AUDIENCE
|
||||
OAUTH_RUNTIME_CONFIG = {
|
||||
'DEFAULT_USER_ROLE': ('ui.default_user_role', DEFAULT_USER_ROLE),
|
||||
'ENABLE_OAUTH_SIGNUP': ('oauth.enable_signup', ENABLE_OAUTH_SIGNUP),
|
||||
'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE': (
|
||||
'oauth.refresh_token.include_scope',
|
||||
OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE,
|
||||
),
|
||||
'OAUTH_MERGE_ACCOUNTS_BY_EMAIL': (
|
||||
'oauth.merge_accounts_by_email',
|
||||
OAUTH_MERGE_ACCOUNTS_BY_EMAIL,
|
||||
),
|
||||
'ENABLE_OAUTH_ROLE_MANAGEMENT': (
|
||||
'oauth.enable_role_mapping',
|
||||
ENABLE_OAUTH_ROLE_MANAGEMENT,
|
||||
),
|
||||
'ENABLE_OAUTH_GROUP_MANAGEMENT': (
|
||||
'oauth.enable_group_mapping',
|
||||
ENABLE_OAUTH_GROUP_MANAGEMENT,
|
||||
),
|
||||
'ENABLE_OAUTH_GROUP_CREATION': (
|
||||
'oauth.enable_group_creation',
|
||||
ENABLE_OAUTH_GROUP_CREATION,
|
||||
),
|
||||
'OAUTH_GROUP_DEFAULT_SHARE': (
|
||||
'oauth.group_default_share',
|
||||
OAUTH_GROUP_DEFAULT_SHARE,
|
||||
),
|
||||
'OAUTH_BLOCKED_GROUPS': ('oauth.blocked_groups', OAUTH_BLOCKED_GROUPS),
|
||||
'OAUTH_ROLES_CLAIM': ('oauth.roles_claim', OAUTH_ROLES_CLAIM),
|
||||
'OAUTH_SUB_CLAIM': ('oauth.sub_claim', OAUTH_SUB_CLAIM),
|
||||
'OAUTH_GROUPS_CLAIM': ('oauth.group_claim', OAUTH_GROUPS_CLAIM),
|
||||
'OAUTH_EMAIL_CLAIM': ('oauth.email_claim', OAUTH_EMAIL_CLAIM),
|
||||
'OAUTH_PICTURE_CLAIM': ('oauth.picture_claim', OAUTH_PICTURE_CLAIM),
|
||||
'OAUTH_USERNAME_CLAIM': ('oauth.username_claim', OAUTH_USERNAME_CLAIM),
|
||||
'OAUTH_ALLOWED_ROLES': ('oauth.allowed_roles', OAUTH_ALLOWED_ROLES),
|
||||
'OAUTH_ADMIN_ROLES': ('oauth.admin_roles', OAUTH_ADMIN_ROLES),
|
||||
'OAUTH_ALLOWED_DOMAINS': ('oauth.allowed_domains', OAUTH_ALLOWED_DOMAINS),
|
||||
'WEBHOOK_URL': ('webhook_url', WEBHOOK_URL),
|
||||
'JWT_EXPIRES_IN': ('auth.jwt_expiry', JWT_EXPIRES_IN),
|
||||
'OAUTH_UPDATE_PICTURE_ON_LOGIN': (
|
||||
'oauth.update_picture_on_login',
|
||||
OAUTH_UPDATE_PICTURE_ON_LOGIN,
|
||||
),
|
||||
'OAUTH_UPDATE_NAME_ON_LOGIN': (
|
||||
'oauth.update_name_on_login',
|
||||
OAUTH_UPDATE_NAME_ON_LOGIN,
|
||||
),
|
||||
'OAUTH_UPDATE_EMAIL_ON_LOGIN': (
|
||||
'oauth.update_email_on_login',
|
||||
OAUTH_UPDATE_EMAIL_ON_LOGIN,
|
||||
),
|
||||
'OAUTH_AUDIENCE': ('oauth.audience', OAUTH_AUDIENCE),
|
||||
}
|
||||
|
||||
|
||||
def _default_value(value):
|
||||
return getattr(value, 'value', value)
|
||||
|
||||
|
||||
async def get_oauth_runtime_config() -> SimpleNamespace:
|
||||
keys = [key for key, _default in OAUTH_RUNTIME_CONFIG.values()]
|
||||
stored = await Config.get_many(*keys)
|
||||
values = {
|
||||
name: stored.get(key, _default_value(default))
|
||||
for name, (key, default) in OAUTH_RUNTIME_CONFIG.items()
|
||||
}
|
||||
return SimpleNamespace(**values)
|
||||
|
||||
|
||||
# Conservative default when the provider omits both expires_in and expires_at.
|
||||
@@ -423,7 +464,8 @@ async def get_oauth_client_info_with_dynamic_client_registration(
|
||||
oauth_server_metadata = None
|
||||
oauth_server_metadata_url = None
|
||||
|
||||
redirect_base_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/')
|
||||
webui_url = await Config.get('webui.url')
|
||||
redirect_base_url = (str(webui_url or request.base_url)).rstrip('/')
|
||||
|
||||
oauth_client_metadata = OAuthClientMetadata(
|
||||
client_name='Open WebUI',
|
||||
@@ -549,7 +591,8 @@ async def get_oauth_client_info_with_static_credentials(
|
||||
oauth_server_metadata = None
|
||||
oauth_server_metadata_url = None
|
||||
|
||||
redirect_base_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/')
|
||||
webui_url = await Config.get('webui.url')
|
||||
redirect_base_url = (str(webui_url or request.base_url)).rstrip('/')
|
||||
redirect_uri = f'{redirect_base_url}/oauth/clients/{client_id}/callback'
|
||||
|
||||
# Discover server metadata (authorization endpoint, token endpoint, scopes, etc.)
|
||||
@@ -636,7 +679,7 @@ class OAuthClientManager:
|
||||
'client_secret': oauth_client_info.client_secret,
|
||||
'client_kwargs': {
|
||||
'follow_redirects': True,
|
||||
**({'timeout': int(OAUTH_CLIENT_TIMEOUT.value)} if OAUTH_CLIENT_TIMEOUT.value else {}),
|
||||
**({'timeout': int(OAUTH_CLIENT_TIMEOUT)} if OAUTH_CLIENT_TIMEOUT else {}),
|
||||
**({'scope': oauth_client_info.scope} if oauth_client_info.scope else {}),
|
||||
**(
|
||||
{'token_endpoint_auth_method': oauth_client_info.token_endpoint_auth_method}
|
||||
@@ -668,7 +711,7 @@ class OAuthClientManager:
|
||||
}
|
||||
return self.clients[client_id]
|
||||
|
||||
def ensure_client_from_config(self, client_id):
|
||||
async def ensure_client_from_config(self, client_id):
|
||||
"""
|
||||
Lazy-load an OAuth client from the current TOOL_SERVER_CONNECTIONS
|
||||
config if it hasn't been registered on this node yet.
|
||||
@@ -677,7 +720,7 @@ class OAuthClientManager:
|
||||
return self.clients[client_id]['client']
|
||||
|
||||
try:
|
||||
connections = getattr(self.app.state.config, 'TOOL_SERVER_CONNECTIONS', [])
|
||||
connections = await Config.get('tool_server.connections', [])
|
||||
except Exception:
|
||||
connections = []
|
||||
|
||||
@@ -783,22 +826,22 @@ class OAuthClientManager:
|
||||
|
||||
return True
|
||||
|
||||
def get_client(self, client_id):
|
||||
async def get_client(self, client_id):
|
||||
if client_id not in self.clients:
|
||||
self.ensure_client_from_config(client_id)
|
||||
await self.ensure_client_from_config(client_id)
|
||||
|
||||
client = self.clients.get(client_id)
|
||||
return client['client'] if client else None
|
||||
|
||||
def get_client_info(self, client_id):
|
||||
async def get_client_info(self, client_id):
|
||||
if client_id not in self.clients:
|
||||
self.ensure_client_from_config(client_id)
|
||||
await self.ensure_client_from_config(client_id)
|
||||
|
||||
client = self.clients.get(client_id)
|
||||
return client['client_info'] if client else None
|
||||
|
||||
def get_server_metadata_url(self, client_id):
|
||||
client = self.get_client(client_id)
|
||||
async def get_server_metadata_url(self, client_id):
|
||||
client = await self.get_client(client_id)
|
||||
if not client:
|
||||
return None
|
||||
|
||||
@@ -881,6 +924,7 @@ class OAuthClientManager:
|
||||
Returns:
|
||||
dict: New token data, or None if refresh failed
|
||||
"""
|
||||
auth_config = await get_oauth_runtime_config()
|
||||
client_id = session.provider
|
||||
token_data = session.token
|
||||
|
||||
@@ -889,14 +933,14 @@ class OAuthClientManager:
|
||||
return None
|
||||
|
||||
try:
|
||||
client = self.get_client(client_id)
|
||||
client = await self.get_client(client_id)
|
||||
if not client:
|
||||
log.error(f'No OAuth client found for provider {client_id}')
|
||||
return None
|
||||
|
||||
token_endpoint = None
|
||||
async with aiohttp.ClientSession(trust_env=True) as session_http:
|
||||
async with session_http.get(self.get_server_metadata_url(client_id)) as r:
|
||||
async with session_http.get(await self.get_server_metadata_url(client_id)) as r:
|
||||
if r.status == 200:
|
||||
openid_data = await r.json()
|
||||
token_endpoint = openid_data.get('token_endpoint')
|
||||
@@ -913,7 +957,7 @@ class OAuthClientManager:
|
||||
'client_id': client.client_id,
|
||||
}
|
||||
# RFC 8707: include resource indicator so refreshed tokens retain correct audience
|
||||
client_info = self.get_client_info(client_id)
|
||||
client_info = await self.get_client_info(client_id)
|
||||
if client_info and client_info.resource:
|
||||
refresh_data['resource'] = client_info.resource
|
||||
|
||||
@@ -924,7 +968,7 @@ class OAuthClientManager:
|
||||
if (
|
||||
hasattr(client, 'client_kwargs')
|
||||
and client.client_kwargs.get('scope')
|
||||
and getattr(self.app.state.config, 'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE', False)
|
||||
and auth_config.OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE
|
||||
):
|
||||
refresh_data['scope'] = client.client_kwargs['scope']
|
||||
|
||||
@@ -957,13 +1001,13 @@ class OAuthClientManager:
|
||||
return None
|
||||
|
||||
async def handle_authorize(self, request, client_id: str) -> RedirectResponse:
|
||||
client = self.get_client(client_id) or self.ensure_client_from_config(client_id)
|
||||
client = await self.get_client(client_id)
|
||||
if client is None:
|
||||
raise HTTPException(404)
|
||||
client_info = self.get_client_info(client_id)
|
||||
client_info = await self.get_client_info(client_id)
|
||||
if client_info is None:
|
||||
# ensure_client_from_config registers client_info too
|
||||
client_info = self.get_client_info(client_id)
|
||||
# get_client registers client_info too
|
||||
client_info = await self.get_client_info(client_id)
|
||||
if client_info is None:
|
||||
raise HTTPException(404)
|
||||
|
||||
@@ -976,13 +1020,13 @@ class OAuthClientManager:
|
||||
return await client.authorize_redirect(request, redirect_uri_str, **kwargs)
|
||||
|
||||
async def handle_callback(self, request, client_id: str, user_id: str, response):
|
||||
client = self.get_client(client_id) or self.ensure_client_from_config(client_id)
|
||||
client = await self.get_client(client_id)
|
||||
if client is None:
|
||||
raise HTTPException(404)
|
||||
|
||||
error_message = None
|
||||
try:
|
||||
client_info = self.get_client_info(client_id)
|
||||
client_info = await self.get_client_info(client_id)
|
||||
|
||||
# Note: Do NOT pass client_id/client_secret explicitly here.
|
||||
# The Authlib client already has these configured during add_client().
|
||||
@@ -1035,7 +1079,8 @@ class OAuthClientManager:
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
redirect_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/')
|
||||
webui_url = await Config.get('webui.url')
|
||||
redirect_url = (str(webui_url or request.base_url)).rstrip('/')
|
||||
|
||||
if error_message:
|
||||
log.debug(error_message)
|
||||
@@ -1202,7 +1247,7 @@ class OAuthManager:
|
||||
if (
|
||||
hasattr(client, 'client_kwargs')
|
||||
and client.client_kwargs.get('scope')
|
||||
and auth_manager_config.OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE
|
||||
and auth_config.OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE
|
||||
):
|
||||
refresh_data['scope'] = client.client_kwargs['scope']
|
||||
|
||||
@@ -1235,6 +1280,7 @@ class OAuthManager:
|
||||
return None
|
||||
|
||||
async def get_user_role(self, user, user_data):
|
||||
auth_config = await get_oauth_runtime_config()
|
||||
user_count = await Users.get_num_users()
|
||||
if user and user_count == 1:
|
||||
# If the user is the only user, assign the role "admin" - actually repairs role for single user on login
|
||||
@@ -1246,16 +1292,16 @@ class OAuthManager:
|
||||
# default role here (not 'admin') — admin promotion happens
|
||||
# race-safely *after* insert via get_num_users() == 1.
|
||||
log.debug('First user bootstrap: using default role (admin promotion deferred to post-insert)')
|
||||
return auth_manager_config.DEFAULT_USER_ROLE
|
||||
return auth_config.DEFAULT_USER_ROLE
|
||||
|
||||
if auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT:
|
||||
if auth_config.ENABLE_OAUTH_ROLE_MANAGEMENT:
|
||||
log.debug('Running OAUTH Role management')
|
||||
oauth_claim = auth_manager_config.OAUTH_ROLES_CLAIM
|
||||
oauth_allowed_roles = auth_manager_config.OAUTH_ALLOWED_ROLES
|
||||
oauth_admin_roles = auth_manager_config.OAUTH_ADMIN_ROLES
|
||||
oauth_claim = auth_config.OAUTH_ROLES_CLAIM
|
||||
oauth_allowed_roles = auth_config.OAUTH_ALLOWED_ROLES
|
||||
oauth_admin_roles = auth_config.OAUTH_ADMIN_ROLES
|
||||
oauth_roles = []
|
||||
# Default/fallback role if no matching roles are found
|
||||
role = auth_manager_config.DEFAULT_USER_ROLE
|
||||
role = auth_config.DEFAULT_USER_ROLE
|
||||
|
||||
# Next block extracts the roles from the user data, accepting nested claims of any depth
|
||||
if oauth_claim and oauth_allowed_roles and oauth_admin_roles:
|
||||
@@ -1313,7 +1359,7 @@ class OAuthManager:
|
||||
else:
|
||||
if not user:
|
||||
# If role management is disabled, use the default role for new users
|
||||
role = auth_manager_config.DEFAULT_USER_ROLE
|
||||
role = auth_config.DEFAULT_USER_ROLE
|
||||
else:
|
||||
# If role management is disabled, use the existing role for existing users
|
||||
role = user.role
|
||||
@@ -1321,11 +1367,12 @@ class OAuthManager:
|
||||
return role
|
||||
|
||||
async def update_user_groups(self, user, user_data, default_permissions, db=None):
|
||||
auth_config = await get_oauth_runtime_config()
|
||||
log.debug('Running OAUTH Group management')
|
||||
oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM
|
||||
oauth_claim = auth_config.OAUTH_GROUPS_CLAIM
|
||||
|
||||
try:
|
||||
blocked_groups = json.loads(auth_manager_config.OAUTH_BLOCKED_GROUPS)
|
||||
blocked_groups = json.loads(auth_config.OAUTH_BLOCKED_GROUPS)
|
||||
except Exception as e:
|
||||
log.exception(f'Error loading OAUTH_BLOCKED_GROUPS: {e}')
|
||||
blocked_groups = []
|
||||
@@ -1353,7 +1400,7 @@ class OAuthManager:
|
||||
all_available_groups: list[GroupModel] = await Groups.get_all_groups(db=db)
|
||||
|
||||
# Create groups if they don't exist and creation is enabled
|
||||
if auth_manager_config.ENABLE_OAUTH_GROUP_CREATION:
|
||||
if auth_config.ENABLE_OAUTH_GROUP_CREATION:
|
||||
log.debug('Checking for missing groups to create...')
|
||||
all_group_names = {g.name for g in all_available_groups}
|
||||
groups_created = False
|
||||
@@ -1370,7 +1417,7 @@ class OAuthManager:
|
||||
name=group_name,
|
||||
description=f"Group '{group_name}' created automatically via OAuth.",
|
||||
permissions=default_permissions, # Use default permissions from function args
|
||||
data={'config': {'share': auth_manager_config.OAUTH_GROUP_DEFAULT_SHARE}},
|
||||
data={'config': {'share': auth_config.OAUTH_GROUP_DEFAULT_SHARE}},
|
||||
)
|
||||
# Use determined creator ID (admin or fallback to current user)
|
||||
created_group = await Groups.insert_new_group(creator_id, new_group_form, db=db)
|
||||
@@ -1496,6 +1543,7 @@ class OAuthManager:
|
||||
return '/user.png'
|
||||
|
||||
async def handle_login(self, request, provider):
|
||||
auth_config = await get_oauth_runtime_config()
|
||||
if provider not in OAUTH_PROVIDERS:
|
||||
raise HTTPException(404)
|
||||
# If the provider has a custom redirect URL, use that, otherwise automatically generate one
|
||||
@@ -1507,14 +1555,15 @@ class OAuthManager:
|
||||
)
|
||||
|
||||
kwargs = {}
|
||||
if auth_manager_config.OAUTH_AUDIENCE:
|
||||
kwargs['audience'] = auth_manager_config.OAUTH_AUDIENCE
|
||||
if auth_config.OAUTH_AUDIENCE:
|
||||
kwargs['audience'] = auth_config.OAUTH_AUDIENCE
|
||||
if OAUTH_AUTHORIZE_PARAMS:
|
||||
kwargs.update(OAUTH_AUTHORIZE_PARAMS)
|
||||
|
||||
return await client.authorize_redirect(request, redirect_uri, **kwargs)
|
||||
|
||||
async def handle_callback(self, request, provider, response, db=None):
|
||||
auth_config = await get_oauth_runtime_config()
|
||||
if provider not in OAUTH_PROVIDERS:
|
||||
raise HTTPException(404)
|
||||
|
||||
@@ -1568,8 +1617,8 @@ class OAuthManager:
|
||||
id_token_claims = dict(user_data) if user_data else {}
|
||||
if (
|
||||
(not user_data)
|
||||
or (auth_manager_config.OAUTH_EMAIL_CLAIM not in user_data)
|
||||
or (auth_manager_config.OAUTH_USERNAME_CLAIM not in user_data)
|
||||
or (auth_config.OAUTH_EMAIL_CLAIM not in user_data)
|
||||
or (auth_config.OAUTH_USERNAME_CLAIM not in user_data)
|
||||
):
|
||||
user_data: UserInfo = await client.userinfo(token=token)
|
||||
# Merge back ID token claims that the userinfo endpoint doesn't
|
||||
@@ -1585,8 +1634,8 @@ class OAuthManager:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||
|
||||
# Extract the "sub" claim, using custom claim if configured
|
||||
if auth_manager_config.OAUTH_SUB_CLAIM:
|
||||
sub = user_data.get(auth_manager_config.OAUTH_SUB_CLAIM)
|
||||
if auth_config.OAUTH_SUB_CLAIM:
|
||||
sub = user_data.get(auth_config.OAUTH_SUB_CLAIM)
|
||||
else:
|
||||
# Fallback to the default sub claim if not configured
|
||||
sub = user_data.get(OAUTH_PROVIDERS[provider].get('sub_claim', 'sub'))
|
||||
@@ -1600,7 +1649,7 @@ class OAuthManager:
|
||||
}
|
||||
|
||||
# Email extraction
|
||||
email_claim = auth_manager_config.OAUTH_EMAIL_CLAIM
|
||||
email_claim = auth_config.OAUTH_EMAIL_CLAIM
|
||||
email = user_data.get(email_claim, '')
|
||||
# We currently mandate that email addresses are provided
|
||||
if not email:
|
||||
@@ -1642,8 +1691,8 @@ class OAuthManager:
|
||||
email = email.lower()
|
||||
# If allowed domains are configured, check if the email domain is in the list
|
||||
if (
|
||||
'*' not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
|
||||
and email.split('@')[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
|
||||
'*' not in auth_config.OAUTH_ALLOWED_DOMAINS
|
||||
and email.split('@')[-1] not in auth_config.OAUTH_ALLOWED_DOMAINS
|
||||
):
|
||||
log.warning(f'OAuth callback failed, e-mail domain is not in the list of allowed domains: {user_data}')
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||
@@ -1652,7 +1701,7 @@ class OAuthManager:
|
||||
user = await Users.get_user_by_oauth_sub(provider, sub, db=db)
|
||||
if not user:
|
||||
# If the user does not exist, check if merging is enabled
|
||||
if auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL:
|
||||
if auth_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL:
|
||||
# Check if the user exists by email
|
||||
user = await Users.get_user_by_email(email, db=db)
|
||||
if user:
|
||||
@@ -1667,8 +1716,8 @@ class OAuthManager:
|
||||
# to avoid problems with the ENABLE_OAUTH_GROUP_MANAGEMENT check below
|
||||
user.role = determined_role
|
||||
|
||||
if auth_manager_config.OAUTH_UPDATE_NAME_ON_LOGIN:
|
||||
username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
|
||||
if auth_config.OAUTH_UPDATE_NAME_ON_LOGIN:
|
||||
username_claim = auth_config.OAUTH_USERNAME_CLAIM
|
||||
if username_claim:
|
||||
new_name = user_data.get(username_claim)
|
||||
if new_name and new_name != user.name:
|
||||
@@ -1676,8 +1725,8 @@ class OAuthManager:
|
||||
user.name = new_name
|
||||
log.debug(f'Updated name for user {user.email}')
|
||||
|
||||
if auth_manager_config.OAUTH_UPDATE_EMAIL_ON_LOGIN:
|
||||
email_claim = auth_manager_config.OAUTH_EMAIL_CLAIM
|
||||
if auth_config.OAUTH_UPDATE_EMAIL_ON_LOGIN:
|
||||
email_claim = auth_config.OAUTH_EMAIL_CLAIM
|
||||
if email_claim:
|
||||
new_email = user_data.get(email_claim)
|
||||
if new_email and new_email.lower() != user.email.lower():
|
||||
@@ -1692,8 +1741,8 @@ class OAuthManager:
|
||||
log.debug(f'Updated email for user {user.id}')
|
||||
|
||||
# Update profile picture if enabled and different from current
|
||||
if auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN:
|
||||
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
|
||||
if auth_config.OAUTH_UPDATE_PICTURE_ON_LOGIN:
|
||||
picture_claim = auth_config.OAUTH_PICTURE_CLAIM
|
||||
if picture_claim:
|
||||
new_picture_url = user_data.get(
|
||||
picture_claim,
|
||||
@@ -1707,13 +1756,13 @@ class OAuthManager:
|
||||
log.debug(f'Updated profile picture for user {user.email}')
|
||||
else:
|
||||
# If the user does not exist, check if signups are enabled
|
||||
if auth_manager_config.ENABLE_OAUTH_SIGNUP:
|
||||
if auth_config.ENABLE_OAUTH_SIGNUP:
|
||||
# Check if an existing user with the same email already exists
|
||||
existing_user = await Users.get_user_by_email(email, db=db)
|
||||
if existing_user:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
|
||||
|
||||
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
|
||||
picture_claim = auth_config.OAUTH_PICTURE_CLAIM
|
||||
if picture_claim:
|
||||
picture_url = user_data.get(
|
||||
picture_claim,
|
||||
@@ -1722,7 +1771,7 @@ class OAuthManager:
|
||||
picture_url = await self._process_picture_url(picture_url, token.get('access_token'))
|
||||
else:
|
||||
picture_url = '/user.png'
|
||||
username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
|
||||
username_claim = auth_config.OAUTH_USERNAME_CLAIM
|
||||
|
||||
name = user_data.get(username_claim)
|
||||
if not name:
|
||||
@@ -1749,10 +1798,10 @@ class OAuthManager:
|
||||
await Users.update_user_role_by_id(user.id, 'admin', db=db)
|
||||
user = await Users.get_user_by_id(user.id, db=db)
|
||||
|
||||
if auth_manager_config.WEBHOOK_URL:
|
||||
if auth_config.WEBHOOK_URL:
|
||||
await post_webhook(
|
||||
WEBUI_NAME,
|
||||
auth_manager_config.WEBHOOK_URL,
|
||||
auth_config.WEBHOOK_URL,
|
||||
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
|
||||
{
|
||||
'action': 'signup',
|
||||
@@ -1761,7 +1810,8 @@ class OAuthManager:
|
||||
},
|
||||
)
|
||||
|
||||
await apply_default_group_assignment(request.app.state.config.DEFAULT_GROUP_ID, user.id, db=db)
|
||||
default_group_id = await Config.get('ui.default_group_id')
|
||||
await apply_default_group_assignment(default_group_id, user.id, db=db)
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
@@ -1771,13 +1821,13 @@ class OAuthManager:
|
||||
|
||||
jwt_token = create_token(
|
||||
data={'id': user.id},
|
||||
expires_delta=parse_duration(auth_manager_config.JWT_EXPIRES_IN),
|
||||
expires_delta=parse_duration(auth_config.JWT_EXPIRES_IN),
|
||||
)
|
||||
if auth_manager_config.ENABLE_OAUTH_GROUP_MANAGEMENT:
|
||||
if auth_config.ENABLE_OAUTH_GROUP_MANAGEMENT:
|
||||
await self.update_user_groups(
|
||||
user=user,
|
||||
user_data=user_data,
|
||||
default_permissions=request.app.state.config.USER_PERMISSIONS,
|
||||
default_permissions=await Config.get('user.permissions'),
|
||||
db=db,
|
||||
)
|
||||
|
||||
@@ -1789,7 +1839,8 @@ class OAuthManager:
|
||||
else ERROR_MESSAGES.DEFAULT('Error during OAuth process')
|
||||
)
|
||||
|
||||
redirect_base_url = (str(request.app.state.config.WEBUI_URL or request.base_url)).rstrip('/')
|
||||
webui_url = await Config.get('webui.url')
|
||||
redirect_base_url = (str(webui_url or request.base_url)).rstrip('/')
|
||||
redirect_url = f'{redirect_base_url}/auth'
|
||||
|
||||
if error_message:
|
||||
@@ -1799,7 +1850,7 @@ class OAuthManager:
|
||||
response = RedirectResponse(url=redirect_url, headers=response.headers)
|
||||
|
||||
# Compute cookie expiry from JWT lifetime
|
||||
expires_delta = parse_duration(auth_manager_config.JWT_EXPIRES_IN)
|
||||
expires_delta = parse_duration(auth_config.JWT_EXPIRES_IN)
|
||||
cookie_max_age = int(expires_delta.total_seconds()) if expires_delta else None
|
||||
|
||||
# Set the cookie token
|
||||
|
||||
@@ -42,6 +42,7 @@ from open_webui.env import (
|
||||
REDIS_KEY_PREFIX,
|
||||
)
|
||||
from open_webui.models.access_grants import AccessGrants
|
||||
from open_webui.models.config import Config
|
||||
from open_webui.models.groups import Groups
|
||||
from open_webui.models.tools import Tools
|
||||
from open_webui.models.users import UserModel
|
||||
@@ -345,7 +346,7 @@ async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extr
|
||||
continue
|
||||
|
||||
tool_server_idx = tool_server_data.get('idx', 0)
|
||||
connections = request.app.state.config.TOOL_SERVER_CONNECTIONS
|
||||
connections = await Config.get('tool_server.connections', [])
|
||||
if tool_server_idx >= len(connections):
|
||||
log.warning(
|
||||
f'Tool server index {tool_server_idx} out of range '
|
||||
@@ -451,6 +452,16 @@ async def get_builtin_tools(
|
||||
|
||||
# Helper to check user-level feature permission (admins always pass)
|
||||
user = extra_params.get('__user__', {})
|
||||
config = await Config.get_many(
|
||||
'rag.web.search.enable',
|
||||
'image_generation.enable',
|
||||
'images.edit.enable',
|
||||
'code_interpreter.enable',
|
||||
'notes.enable',
|
||||
'channels.enable',
|
||||
'automations.enable',
|
||||
'calendar.enable',
|
||||
)
|
||||
|
||||
async def has_user_permission(feature_key: str) -> bool:
|
||||
if user.get('role') == 'admin':
|
||||
@@ -458,7 +469,7 @@ async def get_builtin_tools(
|
||||
return await has_permission(
|
||||
user.get('id', ''),
|
||||
f'features.{feature_key}',
|
||||
request.app.state.config.USER_PERMISSIONS,
|
||||
await Config.get('user.permissions'),
|
||||
)
|
||||
|
||||
# Time utilities - available for date calculations
|
||||
@@ -533,7 +544,7 @@ async def get_builtin_tools(
|
||||
# Add web search tools if builtin category enabled AND enabled globally AND model has web_search capability
|
||||
if (
|
||||
is_builtin_tool_enabled('web_search')
|
||||
and getattr(request.app.state.config, 'ENABLE_WEB_SEARCH', False)
|
||||
and config.get('rag.web.search.enable')
|
||||
and get_model_capability('web_search')
|
||||
and features.get('web_search')
|
||||
and await has_user_permission('web_search')
|
||||
@@ -543,7 +554,7 @@ async def get_builtin_tools(
|
||||
# Add image generation/edit tools if builtin category enabled AND enabled globally AND model has image_generation capability
|
||||
if (
|
||||
is_builtin_tool_enabled('image_generation')
|
||||
and getattr(request.app.state.config, 'ENABLE_IMAGE_GENERATION', False)
|
||||
and config.get('image_generation.enable')
|
||||
and get_model_capability('image_generation')
|
||||
and features.get('image_generation')
|
||||
and await has_user_permission('image_generation')
|
||||
@@ -551,7 +562,7 @@ async def get_builtin_tools(
|
||||
builtin_functions.append(generate_image)
|
||||
if (
|
||||
is_builtin_tool_enabled('image_generation')
|
||||
and getattr(request.app.state.config, 'ENABLE_IMAGE_EDIT', False)
|
||||
and config.get('images.edit.enable')
|
||||
and get_model_capability('image_generation')
|
||||
and features.get('image_generation')
|
||||
and await has_user_permission('image_generation')
|
||||
@@ -561,7 +572,7 @@ async def get_builtin_tools(
|
||||
# Add code interpreter tool if builtin category enabled AND enabled globally AND model has code_interpreter capability
|
||||
if (
|
||||
is_builtin_tool_enabled('code_interpreter')
|
||||
and getattr(request.app.state.config, 'ENABLE_CODE_INTERPRETER', True)
|
||||
and config.get('code_interpreter.enable')
|
||||
and get_model_capability('code_interpreter')
|
||||
and features.get('code_interpreter')
|
||||
and await has_user_permission('code_interpreter')
|
||||
@@ -571,7 +582,7 @@ async def get_builtin_tools(
|
||||
# Notes tools - search, view, create, and update user's notes
|
||||
if (
|
||||
is_builtin_tool_enabled('notes')
|
||||
and getattr(request.app.state.config, 'ENABLE_NOTES', False)
|
||||
and config.get('notes.enable')
|
||||
and await has_user_permission('notes')
|
||||
):
|
||||
builtin_functions.extend([search_notes, view_note, write_note, replace_note_content])
|
||||
@@ -579,7 +590,7 @@ async def get_builtin_tools(
|
||||
# Channels tools - search channels and messages
|
||||
if (
|
||||
is_builtin_tool_enabled('channels')
|
||||
and getattr(request.app.state.config, 'ENABLE_CHANNELS', False)
|
||||
and config.get('channels.enable')
|
||||
and await has_user_permission('channels')
|
||||
):
|
||||
builtin_functions.extend(
|
||||
@@ -602,7 +613,7 @@ async def get_builtin_tools(
|
||||
# Automation tools - create and manage scheduled automations from chat
|
||||
if (
|
||||
is_builtin_tool_enabled('automations')
|
||||
and getattr(request.app.state.config, 'ENABLE_AUTOMATIONS', False)
|
||||
and config.get('automations.enable')
|
||||
and await has_user_permission('automations')
|
||||
):
|
||||
builtin_functions.extend(
|
||||
@@ -612,7 +623,7 @@ async def get_builtin_tools(
|
||||
# Calendar tools - search/create/update/delete events
|
||||
if (
|
||||
is_builtin_tool_enabled('calendar')
|
||||
and getattr(request.app.state.config, 'ENABLE_CALENDAR', False)
|
||||
and config.get('calendar.enable')
|
||||
and await has_user_permission('calendar')
|
||||
):
|
||||
builtin_functions.extend(
|
||||
@@ -958,7 +969,7 @@ def convert_openapi_to_tool_payload(openapi_spec):
|
||||
|
||||
async def set_tool_servers(request: Request):
|
||||
try:
|
||||
request.app.state.TOOL_SERVERS = await get_tool_servers_data(request.app.state.config.TOOL_SERVER_CONNECTIONS)
|
||||
request.app.state.TOOL_SERVERS = await get_tool_servers_data(await Config.get('tool_server.connections', []))
|
||||
except Exception as e:
|
||||
log.error(f'Error fetching tool server data: {e}')
|
||||
request.app.state.TOOL_SERVERS = getattr(request.app.state, 'TOOL_SERVERS', None) or []
|
||||
@@ -1055,7 +1066,7 @@ async def get_terminal_system_prompt(
|
||||
|
||||
async def set_terminal_servers(request: Request):
|
||||
"""Load and cache OpenAPI specs from all TERMINAL_SERVER_CONNECTIONS."""
|
||||
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
|
||||
connections = await Config.get('terminal_server.connections', []) or []
|
||||
|
||||
# Build server configs compatible with get_tool_servers_data
|
||||
# Terminal connections store id/name at top level; translate to info dict
|
||||
@@ -1148,7 +1159,7 @@ async def get_terminal_tools(
|
||||
- Loads specs from cache
|
||||
- Builds callables that route through the terminal proxy
|
||||
"""
|
||||
connections = request.app.state.config.TERMINAL_SERVER_CONNECTIONS or []
|
||||
connections = await Config.get('terminal_server.connections', []) or []
|
||||
connection = next((c for c in connections if c.get('id') == terminal_id), None)
|
||||
if connection is None:
|
||||
log.warning(f'Terminal server not found: {terminal_id}')
|
||||
|
||||
Reference in New Issue
Block a user