refac
This commit is contained in:
1 parent
e8d55c0a8b
commit
5cdcdbaeec
53 files changed
+7829
-7954
No files matched your search
+263
-198
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user