This commit is contained in:
Timothy Jaeryang Baek committed 2026-06-17 02:52:35 +02:00
1 parent e8d55c0a8b
commit 5cdcdbaeec
53 files changed
+7829 -7954

No files matched your search

+263 -198
View File
@@ -8,21 +8,15 @@ import time
import urllib
import uuid
from ssl import CERT_NONE, CERT_REQUIRED, PROTOCOL_TLS
from typing import List, Optional
from aiohttp import ClientSession
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import JSONResponse, RedirectResponse, Response
from fastapi.responses import JSONResponse, Response
from ldap3 import NONE, Connection, Server, Tls
from ldap3.utils.conv import escape_filter_chars
from open_webui.config import (
ENABLE_LDAP,
ENABLE_OAUTH_SIGNUP,
ENABLE_PASSWORD_AUTH,
OAUTH_MERGE_ACCOUNTS_BY_EMAIL,
OAUTH_PROVIDERS,
OPENID_END_SESSION_ENDPOINT,
OPENID_PROVIDER_URL,
)
from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from open_webui.env import (
@@ -50,6 +44,7 @@ from open_webui.models.auths import (
Token,
UpdatePasswordForm,
)
from open_webui.models.config import Config
from open_webui.models.groups import Groups
from open_webui.models.oauth_sessions import OAuthSessions
from open_webui.models.users import (
@@ -75,7 +70,6 @@ from open_webui.utils.auth import (
)
from open_webui.utils.groups import apply_default_group_assignment
from open_webui.utils.misc import parse_duration, validate_email_format
from open_webui.utils.oauth import auth_manager_config
from open_webui.utils.rate_limit import RateLimiter
from open_webui.utils.redis import get_redis_client
from open_webui.utils.webhook import post_webhook
@@ -90,6 +84,60 @@ log = logging.getLogger(__name__)
# who exceed their allotted rate against this gate.
signin_rate_limiter = RateLimiter(redis_client=get_redis_client(), limit=5 * 3, window=60 * 3)
ADMIN_CONFIG_KEYS = {
'SHOW_ADMIN_DETAILS': 'auth.admin.show',
'ADMIN_EMAIL': 'auth.admin.email',
'WEBUI_URL': 'webui.url',
'ENABLE_SIGNUP': 'ui.enable_signup',
'ENABLE_API_KEYS': 'auth.enable_api_keys',
'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS': 'auth.api_key.endpoint_restrictions',
'API_KEYS_ALLOWED_ENDPOINTS': 'auth.api_key.allowed_endpoints',
'DEFAULT_USER_ROLE': 'ui.default_user_role',
'DEFAULT_GROUP_ID': 'ui.default_group_id',
'JWT_EXPIRES_IN': 'auth.jwt_expiry',
'ENABLE_COMMUNITY_SHARING': 'ui.enable_community_sharing',
'ENABLE_MESSAGE_RATING': 'ui.enable_message_rating',
'ENABLE_FOLDERS': 'folders.enable',
'FOLDER_MAX_FILE_COUNT': 'folders.max_file_count',
'AUTOMATION_MAX_COUNT': 'automations.max_count',
'AUTOMATION_MIN_INTERVAL': 'automations.min_interval',
'ENABLE_AUTOMATIONS': 'automations.enable',
'ENABLE_CHANNELS': 'channels.enable',
'ENABLE_CALENDAR': 'calendar.enable',
'ENABLE_MEMORIES': 'memories.enable',
'ENABLE_NOTES': 'notes.enable',
'ENABLE_USER_WEBHOOKS': 'ui.enable_user_webhooks',
'ENABLE_USER_STATUS': 'users.enable_status',
'PENDING_USER_OVERLAY_TITLE': 'ui.pending_user_overlay_title',
'PENDING_USER_OVERLAY_CONTENT': 'ui.pending_user_overlay_content',
'RESPONSE_WATERMARK': 'ui.watermark',
}
LDAP_SERVER_CONFIG_KEYS = {
'label': 'ldap.server.label',
'host': 'ldap.server.host',
'port': 'ldap.server.port',
'attribute_for_mail': 'ldap.server.attribute_for_mail',
'attribute_for_username': 'ldap.server.attribute_for_username',
'app_dn': 'ldap.server.app_dn',
'app_dn_password': 'ldap.server.app_password',
'search_base': 'ldap.server.users_dn',
'search_filters': 'ldap.server.search_filter',
'use_tls': 'ldap.server.use_tls',
'certificate_path': 'ldap.server.ca_cert_file',
'validate_cert': 'ldap.server.validate_cert',
'ciphers': 'ldap.server.ciphers',
}
async def get_config_values(key_map: dict[str, str]) -> dict:
values = await Config.get_many(*key_map.values())
return {field: values[storage_key] for field, storage_key in key_map.items() if storage_key in values}
def config_updates(data: dict, key_map: dict[str, str]) -> dict:
return {key_map[field]: value for field, value in data.items() if field in key_map}
async def create_session_response(
request: Request, user, db, response: Response = None, set_cookie: bool = False
@@ -105,7 +153,7 @@ async def create_session_response(
response: FastAPI response object (required if set_cookie is True)
set_cookie: Whether to set the auth cookie on the response
"""
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
expires_delta = parse_duration(await Config.get('auth.jwt_expiry'))
expires_at = None
if expires_delta:
expires_at = int(time.time()) + int(expires_delta.total_seconds())
@@ -128,7 +176,7 @@ async def create_session_response(
**({'max_age': max_age} if max_age is not None else {}),
)
user_permissions = await get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db)
user_permissions = await get_permissions(user.id, await Config.get('user.permissions'), db=db)
return {
'token': token,
@@ -201,7 +249,7 @@ async def get_session_user(
**({'max_age': max_age} if max_age is not None else {}),
)
user_permissions = await get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db)
user_permissions = await get_permissions(user.id, await Config.get('user.permissions'), db=db)
response_data = {
'token': token,
@@ -320,7 +368,7 @@ async def ldap_auth(
db: AsyncSession = Depends(get_async_session),
):
# Security checks FIRST - before loading any config
if not request.app.state.config.ENABLE_LDAP:
if not await Config.get('ldap.enable'):
raise HTTPException(400, detail='LDAP authentication is not enabled')
if not ENABLE_PASSWORD_AUTH:
@@ -338,19 +386,19 @@ async def ldap_auth(
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
# NOW load LDAP config variables
LDAP_SERVER_LABEL = request.app.state.config.LDAP_SERVER_LABEL
LDAP_SERVER_HOST = request.app.state.config.LDAP_SERVER_HOST
LDAP_SERVER_PORT = request.app.state.config.LDAP_SERVER_PORT
LDAP_ATTRIBUTE_FOR_MAIL = request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL
LDAP_ATTRIBUTE_FOR_USERNAME = request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME
LDAP_SEARCH_BASE = request.app.state.config.LDAP_SEARCH_BASE
LDAP_SEARCH_FILTERS = request.app.state.config.LDAP_SEARCH_FILTERS
LDAP_APP_DN = request.app.state.config.LDAP_APP_DN
LDAP_APP_PASSWORD = request.app.state.config.LDAP_APP_PASSWORD
LDAP_USE_TLS = request.app.state.config.LDAP_USE_TLS
LDAP_CA_CERT_FILE = request.app.state.config.LDAP_CA_CERT_FILE
LDAP_VALIDATE_CERT = CERT_REQUIRED if request.app.state.config.LDAP_VALIDATE_CERT else CERT_NONE
LDAP_CIPHERS = request.app.state.config.LDAP_CIPHERS if request.app.state.config.LDAP_CIPHERS else 'ALL'
LDAP_SERVER_LABEL = await Config.get('ldap.server.label')
LDAP_SERVER_HOST = await Config.get('ldap.server.host')
LDAP_SERVER_PORT = await Config.get('ldap.server.port')
LDAP_ATTRIBUTE_FOR_MAIL = await Config.get('ldap.server.attribute_for_mail')
LDAP_ATTRIBUTE_FOR_USERNAME = await Config.get('ldap.server.attribute_for_username')
LDAP_SEARCH_BASE = await Config.get('ldap.server.users_dn')
LDAP_SEARCH_FILTERS = await Config.get('ldap.server.search_filter')
LDAP_APP_DN = await Config.get('ldap.server.app_dn')
LDAP_APP_PASSWORD = await Config.get('ldap.server.app_password')
LDAP_USE_TLS = await Config.get('ldap.server.use_tls')
LDAP_CA_CERT_FILE = await Config.get('ldap.server.ca_cert_file')
LDAP_VALIDATE_CERT = CERT_REQUIRED if await Config.get('ldap.server.validate_cert') else CERT_NONE
LDAP_CIPHERS = await Config.get('ldap.server.ciphers') if await Config.get('ldap.server.ciphers') else 'ALL'
try:
tls = Tls(
@@ -381,9 +429,9 @@ async def ldap_auth(
if not await asyncio.to_thread(connection_app.bind):
raise HTTPException(400, detail='Application account bind failed')
ENABLE_LDAP_GROUP_MANAGEMENT = request.app.state.config.ENABLE_LDAP_GROUP_MANAGEMENT
ENABLE_LDAP_GROUP_CREATION = request.app.state.config.ENABLE_LDAP_GROUP_CREATION
LDAP_ATTRIBUTE_FOR_GROUPS = request.app.state.config.LDAP_ATTRIBUTE_FOR_GROUPS
ENABLE_LDAP_GROUP_MANAGEMENT = await Config.get('ldap.group.enable_management')
ENABLE_LDAP_GROUP_CREATION = await Config.get('ldap.group.enable_creation')
LDAP_ATTRIBUTE_FOR_GROUPS = await Config.get('ldap.server.attribute_for_groups')
search_attributes = [
f'{LDAP_ATTRIBUTE_FOR_USERNAME}',
@@ -500,7 +548,7 @@ async def ldap_auth(
email=email,
password=str(uuid.uuid4()),
name=cn,
role=request.app.state.config.DEFAULT_USER_ROLE,
role=await Config.get('ui.default_user_role'),
db=db,
)
@@ -514,15 +562,15 @@ async def ldap_auth(
user = await Users.get_user_by_id(user.id, db=db)
await apply_default_group_assignment(
request.app.state.config.DEFAULT_GROUP_ID,
await Config.get('ui.default_group_id'),
user.id,
db=db,
)
if request.app.state.config.WEBHOOK_URL:
if await Config.get('webhook_url'):
await post_webhook(
request.app.state.WEBUI_NAME,
request.app.state.config.WEBHOOK_URL,
await Config.get('webhook_url'),
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
{
'action': 'signup',
@@ -703,7 +751,7 @@ async def signup_handler(
password=hashed,
name=name,
profile_image_url=profile_image_url,
role=request.app.state.config.DEFAULT_USER_ROLE,
role=await Config.get('ui.default_user_role'),
db=db,
)
if not user:
@@ -714,12 +762,12 @@ async def signup_handler(
if await Users.get_num_users(db=db) == 1:
await Users.update_user_role_by_id(user.id, 'admin', db=db)
user = await Users.get_user_by_id(user.id, db=db)
request.app.state.config.ENABLE_SIGNUP = False
await Config.upsert({'ui.enable_signup': False})
if request.app.state.config.WEBHOOK_URL:
if await Config.get('webhook_url'):
await post_webhook(
request.app.state.WEBUI_NAME,
request.app.state.config.WEBHOOK_URL,
await Config.get('webhook_url'),
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
{
'action': 'signup',
@@ -729,7 +777,7 @@ async def signup_handler(
)
await apply_default_group_assignment(
request.app.state.config.DEFAULT_GROUP_ID,
await Config.get('ui.default_group_id'),
user.id,
db=db,
)
@@ -748,10 +796,10 @@ async def signup(
if WEBUI_AUTH:
if has_users:
if not request.app.state.config.ENABLE_SIGNUP or not request.app.state.config.ENABLE_LOGIN_FORM:
if not await Config.get('ui.enable_signup') or not await Config.get('ui.enable_login_form'):
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
# Don't gate the first admin on ENABLE_SIGNUP: it auto-disables and can persist stale across a DB reset.
elif not request.app.state.config.ENABLE_LOGIN_FORM and not ENABLE_INITIAL_ADMIN_SIGNUP:
elif not await Config.get('ui.enable_login_form') and not ENABLE_INITIAL_ADMIN_SIGNUP:
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
else:
if has_users:
@@ -812,19 +860,21 @@ async def signout(request: Request, response: Response, db: AsyncSession = Depen
# If a custom end_session_endpoint is configured (e.g. AWS Cognito), redirect
# there directly instead of attempting OIDC discovery.
if OPENID_END_SESSION_ENDPOINT.value:
openid_end_session_endpoint = await Config.get('oauth.end_session_endpoint')
if openid_end_session_endpoint:
return JSONResponse(
status_code=200,
content={
'status': True,
'redirect_url': OPENID_END_SESSION_ENDPOINT.value,
'redirect_url': openid_end_session_endpoint,
},
headers=response.headers,
)
openid_provider_url = await Config.get('oauth.provider_url')
oauth_server_metadata_url = (
request.app.state.oauth_manager.get_server_metadata_url(session.provider) if session else None
) or OPENID_PROVIDER_URL.value
) or openid_provider_url
if session and oauth_server_metadata_url:
oauth_id_token = session.token.get('id_token')
@@ -934,12 +984,12 @@ async def add_user(
if user:
await apply_default_group_assignment(
request.app.state.config.DEFAULT_GROUP_ID,
await Config.get('ui.default_group_id'),
user.id,
db=db,
)
expires_delta = parse_duration(request.app.state.config.JWT_EXPIRES_IN)
expires_delta = parse_duration(await Config.get('auth.jwt_expiry'))
token = create_token(data={'id': user.id}, expires_delta=expires_delta)
return {
'token': token,
@@ -968,8 +1018,8 @@ async def add_user(
async def get_admin_details(
request: Request, user=Depends(get_current_user), db: AsyncSession = Depends(get_async_session)
):
if request.app.state.config.SHOW_ADMIN_DETAILS:
admin_email = request.app.state.config.ADMIN_EMAIL
if await Config.get('auth.admin.show'):
admin_email = await Config.get('auth.admin.email')
admin_name = None
log.info(f'Admin details - Email: {admin_email}, Name: {admin_name}')
@@ -999,34 +1049,7 @@ async def get_admin_details(
@router.get('/admin/config')
async def get_admin_config(request: Request, user=Depends(get_admin_user)):
return {
'SHOW_ADMIN_DETAILS': request.app.state.config.SHOW_ADMIN_DETAILS,
'ADMIN_EMAIL': request.app.state.config.ADMIN_EMAIL,
'WEBUI_URL': request.app.state.config.WEBUI_URL,
'ENABLE_SIGNUP': request.app.state.config.ENABLE_SIGNUP,
'ENABLE_API_KEYS': request.app.state.config.ENABLE_API_KEYS,
'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS': request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS,
'API_KEYS_ALLOWED_ENDPOINTS': request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS,
'DEFAULT_USER_ROLE': request.app.state.config.DEFAULT_USER_ROLE,
'DEFAULT_GROUP_ID': request.app.state.config.DEFAULT_GROUP_ID,
'JWT_EXPIRES_IN': request.app.state.config.JWT_EXPIRES_IN,
'ENABLE_COMMUNITY_SHARING': request.app.state.config.ENABLE_COMMUNITY_SHARING,
'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING,
'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS,
'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT,
'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT,
'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL,
'ENABLE_AUTOMATIONS': request.app.state.config.ENABLE_AUTOMATIONS,
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
'ENABLE_CALENDAR': request.app.state.config.ENABLE_CALENDAR,
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS,
'ENABLE_USER_STATUS': request.app.state.config.ENABLE_USER_STATUS,
'PENDING_USER_OVERLAY_TITLE': request.app.state.config.PENDING_USER_OVERLAY_TITLE,
'PENDING_USER_OVERLAY_CONTENT': request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
'RESPONSE_WATERMARK': request.app.state.config.RESPONSE_WATERMARK,
}
return await get_config_values(ADMIN_CONFIG_KEYS)
class AdminConfig(BaseModel):
@@ -1060,81 +1083,24 @@ class AdminConfig(BaseModel):
@router.post('/admin/config')
async def update_admin_config(request: Request, form_data: AdminConfig, user=Depends(get_admin_user)):
request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
request.app.state.config.ADMIN_EMAIL = form_data.ADMIN_EMAIL
request.app.state.config.WEBUI_URL = form_data.WEBUI_URL
request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
request.app.state.config.ENABLE_API_KEYS = form_data.ENABLE_API_KEYS
request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = form_data.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS
request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS = form_data.API_KEYS_ALLOWED_ENDPOINTS
request.app.state.config.ENABLE_FOLDERS = form_data.ENABLE_FOLDERS
request.app.state.config.FOLDER_MAX_FILE_COUNT = (
int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else ''
)
request.app.state.config.AUTOMATION_MAX_COUNT = (
int(form_data.AUTOMATION_MAX_COUNT) if form_data.AUTOMATION_MAX_COUNT else ''
)
request.app.state.config.AUTOMATION_MIN_INTERVAL = (
updates = config_updates(form_data.model_dump(), ADMIN_CONFIG_KEYS)
updates['folders.max_file_count'] = int(form_data.FOLDER_MAX_FILE_COUNT) if form_data.FOLDER_MAX_FILE_COUNT else ''
updates['automations.max_count'] = int(form_data.AUTOMATION_MAX_COUNT) if form_data.AUTOMATION_MAX_COUNT else ''
updates['automations.min_interval'] = (
int(form_data.AUTOMATION_MIN_INTERVAL) if form_data.AUTOMATION_MIN_INTERVAL else ''
)
request.app.state.config.ENABLE_AUTOMATIONS = form_data.ENABLE_AUTOMATIONS
request.app.state.config.ENABLE_CHANNELS = form_data.ENABLE_CHANNELS
request.app.state.config.ENABLE_CALENDAR = form_data.ENABLE_CALENDAR
request.app.state.config.ENABLE_MEMORIES = form_data.ENABLE_MEMORIES
request.app.state.config.ENABLE_NOTES = form_data.ENABLE_NOTES
if form_data.DEFAULT_USER_ROLE in ['pending', 'user', 'admin']:
request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
request.app.state.config.DEFAULT_GROUP_ID = form_data.DEFAULT_GROUP_ID
if form_data.DEFAULT_USER_ROLE not in ['pending', 'user', 'admin']:
updates.pop('ui.default_user_role', None)
pattern = r'^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$'
# Check if the input string matches the pattern
if re.match(pattern, form_data.JWT_EXPIRES_IN):
request.app.state.config.JWT_EXPIRES_IN = form_data.JWT_EXPIRES_IN
if not re.match(pattern, form_data.JWT_EXPIRES_IN):
updates.pop('auth.jwt_expiry', None)
request.app.state.config.ENABLE_COMMUNITY_SHARING = form_data.ENABLE_COMMUNITY_SHARING
request.app.state.config.ENABLE_MESSAGE_RATING = form_data.ENABLE_MESSAGE_RATING
request.app.state.config.ENABLE_USER_WEBHOOKS = form_data.ENABLE_USER_WEBHOOKS
request.app.state.config.ENABLE_USER_STATUS = form_data.ENABLE_USER_STATUS
request.app.state.config.PENDING_USER_OVERLAY_TITLE = form_data.PENDING_USER_OVERLAY_TITLE
request.app.state.config.PENDING_USER_OVERLAY_CONTENT = form_data.PENDING_USER_OVERLAY_CONTENT
request.app.state.config.RESPONSE_WATERMARK = form_data.RESPONSE_WATERMARK
return {
'SHOW_ADMIN_DETAILS': request.app.state.config.SHOW_ADMIN_DETAILS,
'ADMIN_EMAIL': request.app.state.config.ADMIN_EMAIL,
'WEBUI_URL': request.app.state.config.WEBUI_URL,
'ENABLE_SIGNUP': request.app.state.config.ENABLE_SIGNUP,
'ENABLE_API_KEYS': request.app.state.config.ENABLE_API_KEYS,
'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS': request.app.state.config.ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS,
'API_KEYS_ALLOWED_ENDPOINTS': request.app.state.config.API_KEYS_ALLOWED_ENDPOINTS,
'DEFAULT_USER_ROLE': request.app.state.config.DEFAULT_USER_ROLE,
'DEFAULT_GROUP_ID': request.app.state.config.DEFAULT_GROUP_ID,
'JWT_EXPIRES_IN': request.app.state.config.JWT_EXPIRES_IN,
'ENABLE_COMMUNITY_SHARING': request.app.state.config.ENABLE_COMMUNITY_SHARING,
'ENABLE_MESSAGE_RATING': request.app.state.config.ENABLE_MESSAGE_RATING,
'ENABLE_FOLDERS': request.app.state.config.ENABLE_FOLDERS,
'FOLDER_MAX_FILE_COUNT': request.app.state.config.FOLDER_MAX_FILE_COUNT,
'AUTOMATION_MAX_COUNT': request.app.state.config.AUTOMATION_MAX_COUNT,
'AUTOMATION_MIN_INTERVAL': request.app.state.config.AUTOMATION_MIN_INTERVAL,
'ENABLE_AUTOMATIONS': request.app.state.config.ENABLE_AUTOMATIONS,
'ENABLE_CHANNELS': request.app.state.config.ENABLE_CHANNELS,
'ENABLE_CALENDAR': request.app.state.config.ENABLE_CALENDAR,
'ENABLE_MEMORIES': request.app.state.config.ENABLE_MEMORIES,
'ENABLE_NOTES': request.app.state.config.ENABLE_NOTES,
'ENABLE_USER_WEBHOOKS': request.app.state.config.ENABLE_USER_WEBHOOKS,
'ENABLE_USER_STATUS': request.app.state.config.ENABLE_USER_STATUS,
'PENDING_USER_OVERLAY_TITLE': request.app.state.config.PENDING_USER_OVERLAY_TITLE,
'PENDING_USER_OVERLAY_CONTENT': request.app.state.config.PENDING_USER_OVERLAY_CONTENT,
'RESPONSE_WATERMARK': request.app.state.config.RESPONSE_WATERMARK,
}
await Config.upsert(updates)
return await get_config_values(ADMIN_CONFIG_KEYS)
class LdapServerConfig(BaseModel):
@@ -1155,21 +1121,7 @@ class LdapServerConfig(BaseModel):
@router.get('/admin/config/ldap/server', response_model=LdapServerConfig)
async def get_ldap_server(request: Request, user=Depends(get_admin_user)):
return {
'label': request.app.state.config.LDAP_SERVER_LABEL,
'host': request.app.state.config.LDAP_SERVER_HOST,
'port': request.app.state.config.LDAP_SERVER_PORT,
'attribute_for_mail': request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL,
'attribute_for_username': request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME,
'app_dn': request.app.state.config.LDAP_APP_DN,
'app_dn_password': request.app.state.config.LDAP_APP_PASSWORD,
'search_base': request.app.state.config.LDAP_SEARCH_BASE,
'search_filters': request.app.state.config.LDAP_SEARCH_FILTERS,
'use_tls': request.app.state.config.LDAP_USE_TLS,
'certificate_path': request.app.state.config.LDAP_CA_CERT_FILE,
'validate_cert': request.app.state.config.LDAP_VALIDATE_CERT,
'ciphers': request.app.state.config.LDAP_CIPHERS,
}
return await get_config_values(LDAP_SERVER_CONFIG_KEYS)
@router.post('/admin/config/ldap/server')
@@ -1186,40 +1138,16 @@ async def update_ldap_server(request: Request, form_data: LdapServerConfig, user
if not value:
raise HTTPException(400, detail=ERROR_MESSAGES.REQUIRED_FIELD_EMPTY(key))
request.app.state.config.LDAP_SERVER_LABEL = form_data.label
request.app.state.config.LDAP_SERVER_HOST = form_data.host
request.app.state.config.LDAP_SERVER_PORT = form_data.port
request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL = form_data.attribute_for_mail
request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = form_data.attribute_for_username
request.app.state.config.LDAP_APP_DN = form_data.app_dn or ''
request.app.state.config.LDAP_APP_PASSWORD = form_data.app_dn_password or ''
request.app.state.config.LDAP_SEARCH_BASE = form_data.search_base
request.app.state.config.LDAP_SEARCH_FILTERS = form_data.search_filters
request.app.state.config.LDAP_USE_TLS = form_data.use_tls
request.app.state.config.LDAP_CA_CERT_FILE = form_data.certificate_path
request.app.state.config.LDAP_VALIDATE_CERT = form_data.validate_cert
request.app.state.config.LDAP_CIPHERS = form_data.ciphers
return {
'label': request.app.state.config.LDAP_SERVER_LABEL,
'host': request.app.state.config.LDAP_SERVER_HOST,
'port': request.app.state.config.LDAP_SERVER_PORT,
'attribute_for_mail': request.app.state.config.LDAP_ATTRIBUTE_FOR_MAIL,
'attribute_for_username': request.app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME,
'app_dn': request.app.state.config.LDAP_APP_DN,
'app_dn_password': request.app.state.config.LDAP_APP_PASSWORD,
'search_base': request.app.state.config.LDAP_SEARCH_BASE,
'search_filters': request.app.state.config.LDAP_SEARCH_FILTERS,
'use_tls': request.app.state.config.LDAP_USE_TLS,
'certificate_path': request.app.state.config.LDAP_CA_CERT_FILE,
'validate_cert': request.app.state.config.LDAP_VALIDATE_CERT,
'ciphers': request.app.state.config.LDAP_CIPHERS,
}
updates = config_updates(form_data.model_dump(), LDAP_SERVER_CONFIG_KEYS)
updates['ldap.server.app_dn'] = form_data.app_dn or ''
updates['ldap.server.app_password'] = form_data.app_dn_password or ''
await Config.upsert(updates)
return await get_config_values(LDAP_SERVER_CONFIG_KEYS)
@router.get('/admin/config/ldap')
async def get_ldap_config(request: Request, user=Depends(get_admin_user)):
return {'ENABLE_LDAP': request.app.state.config.ENABLE_LDAP}
return {'ENABLE_LDAP': await Config.get('ldap.enable')}
class LdapConfigForm(BaseModel):
@@ -1228,8 +1156,8 @@ class LdapConfigForm(BaseModel):
@router.post('/admin/config/ldap')
async def update_ldap_config(request: Request, form_data: LdapConfigForm, user=Depends(get_admin_user)):
request.app.state.config.ENABLE_LDAP = form_data.enable_ldap
return {'ENABLE_LDAP': request.app.state.config.ENABLE_LDAP}
await Config.upsert({'ldap.enable': form_data.enable_ldap})
return {'ENABLE_LDAP': await Config.get('ldap.enable')}
############################
@@ -1237,11 +1165,148 @@ async def update_ldap_config(request: Request, form_data: LdapConfigForm, user=D
############################
class OAuthConfigForm(BaseModel):
"""All OAuth/OIDC settings exposed to the admin panel."""
# General OAuth
ENABLE_OAUTH_SIGNUP: bool | None = None
OAUTH_MERGE_ACCOUNTS_BY_EMAIL: bool | None = None
OAUTH_AUTO_REDIRECT: bool | None = None
OAUTH_ALLOWED_DOMAINS: str | None = None
OAUTH_BLOCKED_GROUPS: str | None = None
# Role management
ENABLE_OAUTH_ROLE_MANAGEMENT: bool | None = None
OAUTH_ROLES_CLAIM: str | None = None
OAUTH_ADMIN_ROLES: str | None = None
OAUTH_ALLOWED_ROLES: str | None = None
# Group management
ENABLE_OAUTH_GROUP_MANAGEMENT: bool | None = None
ENABLE_OAUTH_GROUP_CREATION: bool | None = None
OAUTH_GROUP_CLAIM: str | None = None
OAUTH_GROUP_DEFAULT_SHARE: bool | str | None = None
# OIDC provider settings
OAUTH_PROVIDER_NAME: str | None = None
OPENID_PROVIDER_URL: str | None = None
OAUTH_CLIENT_ID: str | None = None
OAUTH_CLIENT_SECRET: str | None = None
OPENID_REDIRECT_URI: str | None = None
OAUTH_SCOPES: str | None = None
OAUTH_CODE_CHALLENGE_METHOD: str | None = None
OAUTH_TOKEN_ENDPOINT_AUTH_METHOD: str | None = None
OPENID_END_SESSION_ENDPOINT: str | None = None
OAUTH_TIMEOUT: int | str | None = None
OAUTH_CLIENT_TIMEOUT: int | str | None = None
# Claims
OAUTH_EMAIL_CLAIM: str | None = None
OAUTH_USERNAME_CLAIM: str | None = None
OAUTH_PICTURE_CLAIM: str | None = None
OAUTH_SUB_CLAIM: str | None = None
OAUTH_AUDIENCE: str | None = None
# Profile update toggles
OAUTH_UPDATE_EMAIL_ON_LOGIN: bool | None = None
OAUTH_UPDATE_NAME_ON_LOGIN: bool | None = None
OAUTH_UPDATE_PICTURE_ON_LOGIN: bool | None = None
# Token
OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE: bool | None = None
OAUTH_COMMA_LIST_FIELDS = {
'OAUTH_ALLOWED_DOMAINS',
'OAUTH_ADMIN_ROLES',
'OAUTH_ALLOWED_ROLES',
}
OAUTH_CONFIG_KEYS = {
'ENABLE_OAUTH_SIGNUP': 'oauth.enable_signup',
'OAUTH_MERGE_ACCOUNTS_BY_EMAIL': 'oauth.merge_accounts_by_email',
'OAUTH_AUTO_REDIRECT': 'oauth.auto_redirect',
'OAUTH_ALLOWED_DOMAINS': 'oauth.allowed_domains',
'OAUTH_BLOCKED_GROUPS': 'oauth.blocked_groups',
'ENABLE_OAUTH_ROLE_MANAGEMENT': 'oauth.enable_role_mapping',
'OAUTH_ROLES_CLAIM': 'oauth.roles_claim',
'OAUTH_ADMIN_ROLES': 'oauth.admin_roles',
'OAUTH_ALLOWED_ROLES': 'oauth.allowed_roles',
'ENABLE_OAUTH_GROUP_MANAGEMENT': 'oauth.enable_group_mapping',
'ENABLE_OAUTH_GROUP_CREATION': 'oauth.enable_group_creation',
'OAUTH_GROUP_CLAIM': 'oauth.group_claim',
'OAUTH_GROUP_DEFAULT_SHARE': 'oauth.group_default_share',
'OAUTH_PROVIDER_NAME': 'oauth.provider_name',
'OPENID_PROVIDER_URL': 'oauth.provider_url',
'OAUTH_CLIENT_ID': 'oauth.client_id',
'OAUTH_CLIENT_SECRET': 'oauth.client_secret',
'OPENID_REDIRECT_URI': 'oauth.redirect_uri',
'OAUTH_SCOPES': 'oauth.scopes',
'OAUTH_CODE_CHALLENGE_METHOD': 'oauth.code_challenge_method',
'OAUTH_TOKEN_ENDPOINT_AUTH_METHOD': 'oauth.token_endpoint_auth_method',
'OPENID_END_SESSION_ENDPOINT': 'oauth.end_session_endpoint',
'OAUTH_TIMEOUT': 'oauth.timeout',
'OAUTH_CLIENT_TIMEOUT': 'oauth.client.timeout',
'OAUTH_EMAIL_CLAIM': 'oauth.email_claim',
'OAUTH_USERNAME_CLAIM': 'oauth.username_claim',
'OAUTH_PICTURE_CLAIM': 'oauth.picture_claim',
'OAUTH_SUB_CLAIM': 'oauth.sub_claim',
'OAUTH_AUDIENCE': 'oauth.audience',
'OAUTH_UPDATE_EMAIL_ON_LOGIN': 'oauth.update_email_on_login',
'OAUTH_UPDATE_NAME_ON_LOGIN': 'oauth.update_name_on_login',
'OAUTH_UPDATE_PICTURE_ON_LOGIN': 'oauth.update_picture_on_login',
'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE': 'oauth.refresh_token.include_scope',
}
def _format_oauth_form_value(field: str, value):
if field in OAUTH_COMMA_LIST_FIELDS and isinstance(value, list):
return ','.join(str(item) for item in value)
return value
def _parse_oauth_update_value(field: str, value):
if field in OAUTH_COMMA_LIST_FIELDS and isinstance(value, str):
return [item.strip() for item in value.split(',') if item.strip()]
if field in {'OAUTH_TIMEOUT', 'OAUTH_CLIENT_TIMEOUT'} and value == '':
return ''
return value
async def get_oauth_config_values() -> dict:
values = await Config.get_many(*OAUTH_CONFIG_KEYS.values())
return {
field: _format_oauth_form_value(field, values[storage_key])
for field, storage_key in OAUTH_CONFIG_KEYS.items()
if storage_key in values
}
def oauth_config_updates(data: dict) -> dict:
return {
OAUTH_CONFIG_KEYS[field]: _parse_oauth_update_value(field, value)
for field, value in data.items()
if field in OAUTH_CONFIG_KEYS
}
@router.get('/admin/config/oauth', response_model=OAuthConfigForm)
async def get_oauth_config(request: Request, user=Depends(get_admin_user)):
return await get_oauth_config_values()
@router.post('/admin/config/oauth', response_model=OAuthConfigForm)
async def update_oauth_config(request: Request, form_data: OAuthConfigForm, user=Depends(get_admin_user)):
await Config.upsert(oauth_config_updates(form_data.model_dump(exclude_none=True)))
return await get_oauth_config_values()
async def _check_api_key_permission(request: Request, user, db: AsyncSession):
if not request.app.state.config.ENABLE_API_KEYS or (
if not await Config.get('auth.enable_api_keys') or (
user.role != 'admin'
and not await has_permission(
user.id, 'features.api_keys', request.app.state.config.USER_PERMISSIONS, db=db
user.id, 'features.api_keys', await Config.get('user.permissions'), db=db
)
):
raise HTTPException(
@@ -1354,11 +1419,11 @@ async def token_exchange(
)
# Extract user information from the token claims
email_claim = request.app.state.config.OAUTH_EMAIL_CLAIM
username_claim = request.app.state.config.OAUTH_USERNAME_CLAIM
email_claim = await Config.get('oauth.email_claim', 'email')
# Get sub claim
sub = user_data.get(request.app.state.config.OAUTH_SUB_CLAIM or OAUTH_PROVIDERS[provider].get('sub_claim', 'sub'))
sub_claim = await Config.get('oauth.sub_claim')
sub = user_data.get(sub_claim or OAUTH_PROVIDERS[provider].get('sub_claim', 'sub'))
if not sub:
log.warning(f'Token exchange failed: sub claim missing from user data')
raise HTTPException(
@@ -1376,10 +1441,10 @@ async def token_exchange(
email = email.lower()
# Enforce domain allowlist — same check as the normal OAuth callback
if (
'*' not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
and email.split('@')[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
):
oauth_allowed_domains = await Config.get('oauth.allowed_domains', [])
if isinstance(oauth_allowed_domains, str):
oauth_allowed_domains = [domain.strip() for domain in oauth_allowed_domains.split(',') if domain.strip()]
if '*' not in oauth_allowed_domains and email.split('@')[-1] not in oauth_allowed_domains:
log.warning(f'Token exchange denied: email domain not in allowed domains list')
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -1389,7 +1454,7 @@ async def token_exchange(
# Try to find the user by OAuth sub
user = await Users.get_user_by_oauth_sub(provider, sub, db=db)
if not user and OAUTH_MERGE_ACCOUNTS_BY_EMAIL.value:
if not user and await Config.get('oauth.merge_accounts_by_email'):
# Try to find by email if merge is enabled
user = await Users.get_user_by_email(email, db=db)
if user: