feat: add {{USER_GROUPS}} and {{USER_GROUP_IDS}} placeholders for custom forwarded headers (#27236)
Custom per-connection headers can now forward the user's groups to
upstream backends via two new template placeholders:
- {{USER_GROUPS}}: comma-separated group names
- {{USER_GROUP_IDS}}: comma-separated group ids
The group lookup is async, so get_custom_headers becomes an async
wrapper around the sync template substitution (parse_custom_headers)
and fetches groups lazily — only when a header value actually
references a groups placeholder. The external document loader path
runs in a worker thread without an event loop, so Loader.aload
prefetches the groups before offloading and passes them through to
ExternalDocumentLoader.
Claude-Session: https://claude.ai/code/session_01EbBEfTyu8fFJmC13rnQthT
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -13,9 +13,12 @@ from open_webui.env import (
|
||||
FORWARD_USER_INFO_HEADER_USER_NAME,
|
||||
FORWARD_USER_INFO_HEADER_USER_ROLE,
|
||||
)
|
||||
from open_webui.models.groups import Groups
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
USER_GROUPS_PLACEHOLDERS = ('{{USER_GROUPS}}', '{{USER_GROUP_IDS}}')
|
||||
|
||||
|
||||
def _mint_forward_user_jwt(user: Any) -> str:
|
||||
now = int(time.time())
|
||||
@@ -59,7 +62,34 @@ def include_user_info_headers(headers: dict, user: Optional[Any] = None) -> dict
|
||||
}
|
||||
|
||||
|
||||
def get_custom_headers(custom_headers: dict, user=None, metadata: dict = None, request=None) -> dict:
|
||||
def custom_headers_require_user_groups(custom_headers: Optional[dict]) -> bool:
|
||||
if not custom_headers or not isinstance(custom_headers, dict):
|
||||
return False
|
||||
return any(
|
||||
placeholder in str(value) for value in custom_headers.values() for placeholder in USER_GROUPS_PLACEHOLDERS
|
||||
)
|
||||
|
||||
|
||||
async def get_user_groups_for_custom_headers(custom_headers: Optional[dict], user: Optional[Any] = None) -> Optional[list]:
|
||||
"""Fetch the user's groups only when a header value actually references a groups placeholder."""
|
||||
if user is None or not custom_headers_require_user_groups(custom_headers):
|
||||
return None
|
||||
|
||||
try:
|
||||
return await Groups.get_groups_by_member_id(user.id)
|
||||
except Exception:
|
||||
log.exception('Failed to resolve user groups for custom headers')
|
||||
return None
|
||||
|
||||
|
||||
async def get_custom_headers(custom_headers: dict, user=None, metadata: dict = None, request=None) -> dict:
|
||||
user_groups = await get_user_groups_for_custom_headers(custom_headers, user)
|
||||
return parse_custom_headers(custom_headers, user, metadata, request=request, user_groups=user_groups)
|
||||
|
||||
|
||||
def parse_custom_headers(
|
||||
custom_headers: dict, user=None, metadata: dict = None, request=None, user_groups: Optional[list] = None
|
||||
) -> dict:
|
||||
if not custom_headers or not isinstance(custom_headers, dict):
|
||||
return {}
|
||||
|
||||
@@ -93,6 +123,8 @@ def get_custom_headers(custom_headers: dict, user=None, metadata: dict = None, r
|
||||
'{{USER_NAME}}': (user.name.strip() if user else '') or '',
|
||||
'{{USER_EMAIL}}': (user.email.strip() if user else '') or '',
|
||||
'{{USER_ROLE}}': (user.role if user else '') or '',
|
||||
'{{USER_GROUPS}}': ','.join(group.name.strip() for group in user_groups) if user_groups else '',
|
||||
'{{USER_GROUP_IDS}}': ','.join(group.id for group in user_groups) if user_groups else '',
|
||||
'{{USER_AGENT}}': user_agent,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user