refac: scim

This commit is contained in:
Timothy Jaeryang Baek
2026-02-13 14:27:11 -06:00
parent b7549d2f6c
commit d1d1efe212
4 changed files with 183 additions and 8 deletions
+90 -8
View File
@@ -25,6 +25,9 @@ from open_webui.utils.auth import (
)
from open_webui.constants import ERROR_MESSAGES
from open_webui.config import OAUTH_PROVIDERS
from open_webui.env import SCIM_AUTH_PROVIDER
from sqlalchemy.orm import Session
from open_webui.internal.db import get_session
@@ -300,6 +303,45 @@ def get_scim_auth(
)
def get_external_id(user: UserModel) -> Optional[str]:
"""Extract externalId from a user's scim data.
Checks all stored provider entries and returns the first external_id found.
"""
if not user.scim:
return None
for provider_data in user.scim.values():
if isinstance(provider_data, dict) and "external_id" in provider_data:
return provider_data["external_id"]
return None
def get_scim_provider() -> str:
"""Return the configured SCIM auth provider.
Requires SCIM_AUTH_PROVIDER env var to be set (e.g. 'microsoft', 'oidc').
"""
if not SCIM_AUTH_PROVIDER:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="SCIM_AUTH_PROVIDER environment variable is required when SCIM is enabled",
)
return SCIM_AUTH_PROVIDER
def find_user_by_external_id(
external_id: str, db=None
) -> Optional[UserModel]:
"""Find a user by SCIM externalId, falling back to OAuth sub match."""
provider = get_scim_provider()
user = Users.get_user_by_scim_external_id(provider, external_id, db=db)
if user:
return user
# Fallback: check if externalId matches an existing OAuth sub (account linking)
return Users.get_user_by_oauth_sub(provider, external_id, db=db)
def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser:
"""Convert internal User model to SCIM User"""
# Parse display name into name components
@@ -321,6 +363,7 @@ def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser:
return SCIMUser(
id=user.id,
externalId=get_external_id(user),
userName=user.email,
name=SCIMName(
formatted=user.name,
@@ -494,13 +537,17 @@ async def get_users(
# Get users from database
if filter:
# Simple filter parsing - supports userName eq "email"
# In production, you'd want a more robust filter parser
# Simple filter parsing - supports userName eq, externalId eq
if "userName eq" in filter:
email = filter.split('"')[1]
user = Users.get_user_by_email(email, db=db)
users_list = [user] if user else []
total = 1 if user else 0
elif "externalId eq" in filter:
external_id = filter.split('"')[1]
user = find_user_by_external_id(external_id, db=db)
users_list = [user] if user else []
total = 1 if user else 0
else:
response = Users.get_users(skip=skip, limit=limit, db=db)
users_list = response["users"]
@@ -546,17 +593,33 @@ async def create_user(
db: Session = Depends(get_session),
):
"""Create SCIM User"""
# Check if user already exists
existing_user = Users.get_user_by_email(user_data.userName, db=db)
# Check for duplicate by externalId
if user_data.externalId:
existing_user = find_user_by_external_id(user_data.externalId, db=db)
if existing_user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"User with externalId {user_data.externalId} already exists",
)
# Determine primary email (lowercased per RFC 5321)
email = user_data.userName
for entry in user_data.emails:
if entry.primary:
email = entry.value
break
email = email.lower()
# Check for duplicate by email
existing_user = Users.get_user_by_email(email, db=db)
if existing_user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"User with email {user_data.userName} already exists",
detail=f"User with email {email} already exists",
)
# Create user
user_id = str(uuid.uuid4())
email = user_data.emails[0].value if user_data.emails else user_data.userName
# Parse name if provided
name = user_data.displayName
@@ -571,7 +634,6 @@ async def create_user(
if user_data.photos and len(user_data.photos) > 0:
profile_image = user_data.photos[0].value
# Create user
new_user = Users.insert_new_user(
id=user_id,
name=name,
@@ -587,6 +649,14 @@ async def create_user(
detail="Failed to create user",
)
# Store externalId in the scim field
if user_data.externalId:
provider = get_scim_provider()
Users.update_user_scim_by_id(
user_id, provider, user_data.externalId, db=db
)
new_user = Users.get_user_by_id(user_id, db=db)
return user_to_scim(new_user, request, db=db)
@@ -631,7 +701,6 @@ async def update_user(
if user_data.photos and len(user_data.photos) > 0:
update_data["profile_image_url"] = user_data.photos[0].value
# Update user
updated_user = Users.update_user_by_id(user_id, update_data, db=db)
if not updated_user:
raise HTTPException(
@@ -639,6 +708,14 @@ async def update_user(
detail="Failed to update user",
)
# Update externalId in the scim field
if user_data.externalId:
provider = get_scim_provider()
Users.update_user_scim_by_id(
user_id, provider, user_data.externalId, db=db
)
updated_user = Users.get_user_by_id(user_id, db=db)
return user_to_scim(updated_user, request, db=db)
@@ -676,6 +753,11 @@ async def patch_user(
update_data["email"] = value
elif path == "name.formatted":
update_data["name"] = value
elif path == "externalId":
provider = get_scim_provider()
Users.update_user_scim_by_id(
user_id, provider, value, db=db
)
# Update user
if update_data: