mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
refac
This commit is contained in:
parent
6c06024cf9
commit
7fffecd168
1 changed files with 146 additions and 137 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
"""
|
"""
|
||||||
SCIM 2.0 Implementation for Open WebUI
|
Experimental SCIM 2.0 Implementation for Open WebUI
|
||||||
Provides System for Cross-domain Identity Management endpoints for users and groups
|
Provides System for Cross-domain Identity Management endpoints for users and groups
|
||||||
|
|
||||||
|
NOTE: This is an experimental implementation and may not fully comply with SCIM 2.0 standards, and is subject to change.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -15,7 +17,12 @@ from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
|
||||||
from open_webui.models.users import Users, UserModel
|
from open_webui.models.users import Users, UserModel
|
||||||
from open_webui.models.groups import Groups, GroupModel
|
from open_webui.models.groups import Groups, GroupModel
|
||||||
from open_webui.utils.auth import get_admin_user, get_current_user, decode_token, get_verified_user
|
from open_webui.utils.auth import (
|
||||||
|
get_admin_user,
|
||||||
|
get_current_user,
|
||||||
|
decode_token,
|
||||||
|
get_verified_user,
|
||||||
|
)
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
|
@ -40,9 +47,9 @@ def scim_error(status_code: int, detail: str, scim_type: Optional[str] = None):
|
||||||
error_body = {
|
error_body = {
|
||||||
"schemas": [SCIM_ERROR_SCHEMA],
|
"schemas": [SCIM_ERROR_SCHEMA],
|
||||||
"status": str(status_code),
|
"status": str(status_code),
|
||||||
"detail": detail
|
"detail": detail,
|
||||||
}
|
}
|
||||||
|
|
||||||
if scim_type:
|
if scim_type:
|
||||||
error_body["scimType"] = scim_type
|
error_body["scimType"] = scim_type
|
||||||
elif status_code == 404:
|
elif status_code == 404:
|
||||||
|
|
@ -51,15 +58,13 @@ def scim_error(status_code: int, detail: str, scim_type: Optional[str] = None):
|
||||||
error_body["scimType"] = "uniqueness"
|
error_body["scimType"] = "uniqueness"
|
||||||
elif status_code == 400:
|
elif status_code == 400:
|
||||||
error_body["scimType"] = "invalidSyntax"
|
error_body["scimType"] = "invalidSyntax"
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(status_code=status_code, content=error_body)
|
||||||
status_code=status_code,
|
|
||||||
content=error_body
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SCIMError(BaseModel):
|
class SCIMError(BaseModel):
|
||||||
"""SCIM Error Response"""
|
"""SCIM Error Response"""
|
||||||
|
|
||||||
schemas: List[str] = [SCIM_ERROR_SCHEMA]
|
schemas: List[str] = [SCIM_ERROR_SCHEMA]
|
||||||
status: str
|
status: str
|
||||||
scimType: Optional[str] = None
|
scimType: Optional[str] = None
|
||||||
|
|
@ -68,6 +73,7 @@ class SCIMError(BaseModel):
|
||||||
|
|
||||||
class SCIMMeta(BaseModel):
|
class SCIMMeta(BaseModel):
|
||||||
"""SCIM Resource Metadata"""
|
"""SCIM Resource Metadata"""
|
||||||
|
|
||||||
resourceType: str
|
resourceType: str
|
||||||
created: str
|
created: str
|
||||||
lastModified: str
|
lastModified: str
|
||||||
|
|
@ -77,6 +83,7 @@ class SCIMMeta(BaseModel):
|
||||||
|
|
||||||
class SCIMName(BaseModel):
|
class SCIMName(BaseModel):
|
||||||
"""SCIM User Name"""
|
"""SCIM User Name"""
|
||||||
|
|
||||||
formatted: Optional[str] = None
|
formatted: Optional[str] = None
|
||||||
familyName: Optional[str] = None
|
familyName: Optional[str] = None
|
||||||
givenName: Optional[str] = None
|
givenName: Optional[str] = None
|
||||||
|
|
@ -87,6 +94,7 @@ class SCIMName(BaseModel):
|
||||||
|
|
||||||
class SCIMEmail(BaseModel):
|
class SCIMEmail(BaseModel):
|
||||||
"""SCIM Email"""
|
"""SCIM Email"""
|
||||||
|
|
||||||
value: str
|
value: str
|
||||||
type: Optional[str] = "work"
|
type: Optional[str] = "work"
|
||||||
primary: bool = True
|
primary: bool = True
|
||||||
|
|
@ -95,6 +103,7 @@ class SCIMEmail(BaseModel):
|
||||||
|
|
||||||
class SCIMPhoto(BaseModel):
|
class SCIMPhoto(BaseModel):
|
||||||
"""SCIM Photo"""
|
"""SCIM Photo"""
|
||||||
|
|
||||||
value: str
|
value: str
|
||||||
type: Optional[str] = "photo"
|
type: Optional[str] = "photo"
|
||||||
primary: bool = True
|
primary: bool = True
|
||||||
|
|
@ -103,6 +112,7 @@ class SCIMPhoto(BaseModel):
|
||||||
|
|
||||||
class SCIMGroupMember(BaseModel):
|
class SCIMGroupMember(BaseModel):
|
||||||
"""SCIM Group Member"""
|
"""SCIM Group Member"""
|
||||||
|
|
||||||
value: str # User ID
|
value: str # User ID
|
||||||
ref: Optional[str] = Field(None, alias="$ref")
|
ref: Optional[str] = Field(None, alias="$ref")
|
||||||
type: Optional[str] = "User"
|
type: Optional[str] = "User"
|
||||||
|
|
@ -111,8 +121,9 @@ class SCIMGroupMember(BaseModel):
|
||||||
|
|
||||||
class SCIMUser(BaseModel):
|
class SCIMUser(BaseModel):
|
||||||
"""SCIM User Resource"""
|
"""SCIM User Resource"""
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
schemas: List[str] = [SCIM_USER_SCHEMA]
|
schemas: List[str] = [SCIM_USER_SCHEMA]
|
||||||
id: str
|
id: str
|
||||||
externalId: Optional[str] = None
|
externalId: Optional[str] = None
|
||||||
|
|
@ -128,8 +139,9 @@ class SCIMUser(BaseModel):
|
||||||
|
|
||||||
class SCIMUserCreateRequest(BaseModel):
|
class SCIMUserCreateRequest(BaseModel):
|
||||||
"""SCIM User Create Request"""
|
"""SCIM User Create Request"""
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
schemas: List[str] = [SCIM_USER_SCHEMA]
|
schemas: List[str] = [SCIM_USER_SCHEMA]
|
||||||
externalId: Optional[str] = None
|
externalId: Optional[str] = None
|
||||||
userName: str
|
userName: str
|
||||||
|
|
@ -143,8 +155,9 @@ class SCIMUserCreateRequest(BaseModel):
|
||||||
|
|
||||||
class SCIMUserUpdateRequest(BaseModel):
|
class SCIMUserUpdateRequest(BaseModel):
|
||||||
"""SCIM User Update Request"""
|
"""SCIM User Update Request"""
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
schemas: List[str] = [SCIM_USER_SCHEMA]
|
schemas: List[str] = [SCIM_USER_SCHEMA]
|
||||||
id: Optional[str] = None
|
id: Optional[str] = None
|
||||||
externalId: Optional[str] = None
|
externalId: Optional[str] = None
|
||||||
|
|
@ -158,8 +171,9 @@ class SCIMUserUpdateRequest(BaseModel):
|
||||||
|
|
||||||
class SCIMGroup(BaseModel):
|
class SCIMGroup(BaseModel):
|
||||||
"""SCIM Group Resource"""
|
"""SCIM Group Resource"""
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
schemas: List[str] = [SCIM_GROUP_SCHEMA]
|
schemas: List[str] = [SCIM_GROUP_SCHEMA]
|
||||||
id: str
|
id: str
|
||||||
displayName: str
|
displayName: str
|
||||||
|
|
@ -169,8 +183,9 @@ class SCIMGroup(BaseModel):
|
||||||
|
|
||||||
class SCIMGroupCreateRequest(BaseModel):
|
class SCIMGroupCreateRequest(BaseModel):
|
||||||
"""SCIM Group Create Request"""
|
"""SCIM Group Create Request"""
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
schemas: List[str] = [SCIM_GROUP_SCHEMA]
|
schemas: List[str] = [SCIM_GROUP_SCHEMA]
|
||||||
displayName: str
|
displayName: str
|
||||||
members: Optional[List[SCIMGroupMember]] = []
|
members: Optional[List[SCIMGroupMember]] = []
|
||||||
|
|
@ -178,8 +193,9 @@ class SCIMGroupCreateRequest(BaseModel):
|
||||||
|
|
||||||
class SCIMGroupUpdateRequest(BaseModel):
|
class SCIMGroupUpdateRequest(BaseModel):
|
||||||
"""SCIM Group Update Request"""
|
"""SCIM Group Update Request"""
|
||||||
|
|
||||||
model_config = ConfigDict(populate_by_name=True)
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
schemas: List[str] = [SCIM_GROUP_SCHEMA]
|
schemas: List[str] = [SCIM_GROUP_SCHEMA]
|
||||||
displayName: Optional[str] = None
|
displayName: Optional[str] = None
|
||||||
members: Optional[List[SCIMGroupMember]] = None
|
members: Optional[List[SCIMGroupMember]] = None
|
||||||
|
|
@ -187,6 +203,7 @@ class SCIMGroupUpdateRequest(BaseModel):
|
||||||
|
|
||||||
class SCIMListResponse(BaseModel):
|
class SCIMListResponse(BaseModel):
|
||||||
"""SCIM List Response"""
|
"""SCIM List Response"""
|
||||||
|
|
||||||
schemas: List[str] = [SCIM_LIST_RESPONSE_SCHEMA]
|
schemas: List[str] = [SCIM_LIST_RESPONSE_SCHEMA]
|
||||||
totalResults: int
|
totalResults: int
|
||||||
itemsPerPage: int
|
itemsPerPage: int
|
||||||
|
|
@ -196,6 +213,7 @@ class SCIMListResponse(BaseModel):
|
||||||
|
|
||||||
class SCIMPatchOperation(BaseModel):
|
class SCIMPatchOperation(BaseModel):
|
||||||
"""SCIM Patch Operation"""
|
"""SCIM Patch Operation"""
|
||||||
|
|
||||||
op: str # "add", "replace", "remove"
|
op: str # "add", "replace", "remove"
|
||||||
path: Optional[str] = None
|
path: Optional[str] = None
|
||||||
value: Optional[Any] = None
|
value: Optional[Any] = None
|
||||||
|
|
@ -203,11 +221,14 @@ class SCIMPatchOperation(BaseModel):
|
||||||
|
|
||||||
class SCIMPatchRequest(BaseModel):
|
class SCIMPatchRequest(BaseModel):
|
||||||
"""SCIM Patch Request"""
|
"""SCIM Patch Request"""
|
||||||
|
|
||||||
schemas: List[str] = ["urn:ietf:params:scim:api:messages:2.0:PatchOp"]
|
schemas: List[str] = ["urn:ietf:params:scim:api:messages:2.0:PatchOp"]
|
||||||
Operations: List[SCIMPatchOperation]
|
Operations: List[SCIMPatchOperation]
|
||||||
|
|
||||||
|
|
||||||
def get_scim_auth(request: Request, authorization: Optional[str] = Header(None)) -> bool:
|
def get_scim_auth(
|
||||||
|
request: Request, authorization: Optional[str] = Header(None)
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Verify SCIM authentication
|
Verify SCIM authentication
|
||||||
Checks for SCIM-specific bearer token configured in the system
|
Checks for SCIM-specific bearer token configured in the system
|
||||||
|
|
@ -218,7 +239,7 @@ def get_scim_auth(request: Request, authorization: Optional[str] = Header(None))
|
||||||
detail="Authorization header required",
|
detail="Authorization header required",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parts = authorization.split()
|
parts = authorization.split()
|
||||||
if len(parts) != 2:
|
if len(parts) != 2:
|
||||||
|
|
@ -226,19 +247,21 @@ def get_scim_auth(request: Request, authorization: Optional[str] = Header(None))
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid authorization format. Expected: Bearer <token>",
|
detail="Invalid authorization format. Expected: Bearer <token>",
|
||||||
)
|
)
|
||||||
|
|
||||||
scheme, token = parts
|
scheme, token = parts
|
||||||
if scheme.lower() != "bearer":
|
if scheme.lower() != "bearer":
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid authentication scheme",
|
detail="Invalid authentication scheme",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if SCIM is enabled
|
# Check if SCIM is enabled
|
||||||
scim_enabled = getattr(request.app.state.config, "SCIM_ENABLED", False)
|
scim_enabled = getattr(request.app.state.config, "SCIM_ENABLED", False)
|
||||||
log.info(f"SCIM auth check - raw SCIM_ENABLED: {scim_enabled}, type: {type(scim_enabled)}")
|
log.info(
|
||||||
|
f"SCIM auth check - raw SCIM_ENABLED: {scim_enabled}, type: {type(scim_enabled)}"
|
||||||
|
)
|
||||||
# Handle both PersistentConfig and direct value
|
# Handle both PersistentConfig and direct value
|
||||||
if hasattr(scim_enabled, 'value'):
|
if hasattr(scim_enabled, "value"):
|
||||||
scim_enabled = scim_enabled.value
|
scim_enabled = scim_enabled.value
|
||||||
log.info(f"SCIM enabled status after conversion: {scim_enabled}")
|
log.info(f"SCIM enabled status after conversion: {scim_enabled}")
|
||||||
if not scim_enabled:
|
if not scim_enabled:
|
||||||
|
|
@ -246,11 +269,11 @@ def get_scim_auth(request: Request, authorization: Optional[str] = Header(None))
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="SCIM is not enabled",
|
detail="SCIM is not enabled",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify the SCIM token
|
# Verify the SCIM token
|
||||||
scim_token = getattr(request.app.state.config, "SCIM_TOKEN", None)
|
scim_token = getattr(request.app.state.config, "SCIM_TOKEN", None)
|
||||||
# Handle both PersistentConfig and direct value
|
# Handle both PersistentConfig and direct value
|
||||||
if hasattr(scim_token, 'value'):
|
if hasattr(scim_token, "value"):
|
||||||
scim_token = scim_token.value
|
scim_token = scim_token.value
|
||||||
log.debug(f"SCIM token configured: {bool(scim_token)}")
|
log.debug(f"SCIM token configured: {bool(scim_token)}")
|
||||||
if not scim_token or token != scim_token:
|
if not scim_token or token != scim_token:
|
||||||
|
|
@ -258,7 +281,7 @@ def get_scim_auth(request: Request, authorization: Optional[str] = Header(None))
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid SCIM token",
|
detail="Invalid SCIM token",
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
# Re-raise HTTP exceptions as-is
|
# Re-raise HTTP exceptions as-is
|
||||||
|
|
@ -266,6 +289,7 @@ def get_scim_auth(request: Request, authorization: Optional[str] = Header(None))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"SCIM authentication error: {e}")
|
log.error(f"SCIM authentication error: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log.error(f"Traceback: {traceback.format_exc()}")
|
log.error(f"Traceback: {traceback.format_exc()}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
@ -273,14 +297,13 @@ def get_scim_auth(request: Request, authorization: Optional[str] = Header(None))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def user_to_scim(user: UserModel, request: Request) -> SCIMUser:
|
def user_to_scim(user: UserModel, request: Request) -> SCIMUser:
|
||||||
"""Convert internal User model to SCIM User"""
|
"""Convert internal User model to SCIM User"""
|
||||||
# Parse display name into name components
|
# Parse display name into name components
|
||||||
name_parts = user.name.split(" ", 1) if user.name else ["", ""]
|
name_parts = user.name.split(" ", 1) if user.name else ["", ""]
|
||||||
given_name = name_parts[0] if name_parts else ""
|
given_name = name_parts[0] if name_parts else ""
|
||||||
family_name = name_parts[1] if len(name_parts) > 1 else ""
|
family_name = name_parts[1] if len(name_parts) > 1 else ""
|
||||||
|
|
||||||
# Get user's groups
|
# Get user's groups
|
||||||
user_groups = Groups.get_groups_by_member_id(user.id)
|
user_groups = Groups.get_groups_by_member_id(user.id)
|
||||||
groups = [
|
groups = [
|
||||||
|
|
@ -288,11 +311,11 @@ def user_to_scim(user: UserModel, request: Request) -> SCIMUser:
|
||||||
"value": group.id,
|
"value": group.id,
|
||||||
"display": group.name,
|
"display": group.name,
|
||||||
"$ref": f"{request.base_url}api/v1/scim/v2/Groups/{group.id}",
|
"$ref": f"{request.base_url}api/v1/scim/v2/Groups/{group.id}",
|
||||||
"type": "direct"
|
"type": "direct",
|
||||||
}
|
}
|
||||||
for group in user_groups
|
for group in user_groups
|
||||||
]
|
]
|
||||||
|
|
||||||
return SCIMUser(
|
return SCIMUser(
|
||||||
id=user.id,
|
id=user.id,
|
||||||
userName=user.email,
|
userName=user.email,
|
||||||
|
|
@ -304,12 +327,20 @@ def user_to_scim(user: UserModel, request: Request) -> SCIMUser:
|
||||||
displayName=user.name,
|
displayName=user.name,
|
||||||
emails=[SCIMEmail(value=user.email)],
|
emails=[SCIMEmail(value=user.email)],
|
||||||
active=user.role != "pending",
|
active=user.role != "pending",
|
||||||
photos=[SCIMPhoto(value=user.profile_image_url)] if user.profile_image_url else None,
|
photos=(
|
||||||
|
[SCIMPhoto(value=user.profile_image_url)]
|
||||||
|
if user.profile_image_url
|
||||||
|
else None
|
||||||
|
),
|
||||||
groups=groups if groups else None,
|
groups=groups if groups else None,
|
||||||
meta=SCIMMeta(
|
meta=SCIMMeta(
|
||||||
resourceType=SCIM_RESOURCE_TYPE_USER,
|
resourceType=SCIM_RESOURCE_TYPE_USER,
|
||||||
created=datetime.fromtimestamp(user.created_at, tz=timezone.utc).isoformat(),
|
created=datetime.fromtimestamp(
|
||||||
lastModified=datetime.fromtimestamp(user.updated_at, tz=timezone.utc).isoformat(),
|
user.created_at, tz=timezone.utc
|
||||||
|
).isoformat(),
|
||||||
|
lastModified=datetime.fromtimestamp(
|
||||||
|
user.updated_at, tz=timezone.utc
|
||||||
|
).isoformat(),
|
||||||
location=f"{request.base_url}api/v1/scim/v2/Users/{user.id}",
|
location=f"{request.base_url}api/v1/scim/v2/Users/{user.id}",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -328,55 +359,43 @@ def group_to_scim(group: GroupModel, request: Request) -> SCIMGroup:
|
||||||
display=user.name,
|
display=user.name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return SCIMGroup(
|
return SCIMGroup(
|
||||||
id=group.id,
|
id=group.id,
|
||||||
displayName=group.name,
|
displayName=group.name,
|
||||||
members=members,
|
members=members,
|
||||||
meta=SCIMMeta(
|
meta=SCIMMeta(
|
||||||
resourceType=SCIM_RESOURCE_TYPE_GROUP,
|
resourceType=SCIM_RESOURCE_TYPE_GROUP,
|
||||||
created=datetime.fromtimestamp(group.created_at, tz=timezone.utc).isoformat(),
|
created=datetime.fromtimestamp(
|
||||||
lastModified=datetime.fromtimestamp(group.updated_at, tz=timezone.utc).isoformat(),
|
group.created_at, tz=timezone.utc
|
||||||
|
).isoformat(),
|
||||||
|
lastModified=datetime.fromtimestamp(
|
||||||
|
group.updated_at, tz=timezone.utc
|
||||||
|
).isoformat(),
|
||||||
location=f"{request.base_url}api/v1/scim/v2/Groups/{group.id}",
|
location=f"{request.base_url}api/v1/scim/v2/Groups/{group.id}",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# SCIM Service Provider Config
|
# SCIM Service Provider Config
|
||||||
@router.get("/ServiceProviderConfig")
|
@router.get("/ServiceProviderConfig")
|
||||||
async def get_service_provider_config():
|
async def get_service_provider_config():
|
||||||
"""Get SCIM Service Provider Configuration"""
|
"""Get SCIM Service Provider Configuration"""
|
||||||
return {
|
return {
|
||||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
|
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
|
||||||
"patch": {
|
"patch": {"supported": True},
|
||||||
"supported": True
|
"bulk": {"supported": False, "maxOperations": 1000, "maxPayloadSize": 1048576},
|
||||||
},
|
"filter": {"supported": True, "maxResults": 200},
|
||||||
"bulk": {
|
"changePassword": {"supported": False},
|
||||||
"supported": False,
|
"sort": {"supported": False},
|
||||||
"maxOperations": 1000,
|
"etag": {"supported": False},
|
||||||
"maxPayloadSize": 1048576
|
|
||||||
},
|
|
||||||
"filter": {
|
|
||||||
"supported": True,
|
|
||||||
"maxResults": 200
|
|
||||||
},
|
|
||||||
"changePassword": {
|
|
||||||
"supported": False
|
|
||||||
},
|
|
||||||
"sort": {
|
|
||||||
"supported": False
|
|
||||||
},
|
|
||||||
"etag": {
|
|
||||||
"supported": False
|
|
||||||
},
|
|
||||||
"authenticationSchemes": [
|
"authenticationSchemes": [
|
||||||
{
|
{
|
||||||
"type": "oauthbearertoken",
|
"type": "oauthbearertoken",
|
||||||
"name": "OAuth Bearer Token",
|
"name": "OAuth Bearer Token",
|
||||||
"description": "Authentication using OAuth 2.0 Bearer Token"
|
"description": "Authentication using OAuth 2.0 Bearer Token",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -393,8 +412,8 @@ async def get_resource_types(request: Request):
|
||||||
"schema": SCIM_USER_SCHEMA,
|
"schema": SCIM_USER_SCHEMA,
|
||||||
"meta": {
|
"meta": {
|
||||||
"location": f"{request.base_url}api/v1/scim/v2/ResourceTypes/User",
|
"location": f"{request.base_url}api/v1/scim/v2/ResourceTypes/User",
|
||||||
"resourceType": "ResourceType"
|
"resourceType": "ResourceType",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
|
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
|
||||||
|
|
@ -404,9 +423,9 @@ async def get_resource_types(request: Request):
|
||||||
"schema": SCIM_GROUP_SCHEMA,
|
"schema": SCIM_GROUP_SCHEMA,
|
||||||
"meta": {
|
"meta": {
|
||||||
"location": f"{request.base_url}api/v1/scim/v2/ResourceTypes/Group",
|
"location": f"{request.base_url}api/v1/scim/v2/ResourceTypes/Group",
|
||||||
"resourceType": "ResourceType"
|
"resourceType": "ResourceType",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -425,25 +444,17 @@ async def get_schemas():
|
||||||
"name": "userName",
|
"name": "userName",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"required": True,
|
"required": True,
|
||||||
"uniqueness": "server"
|
"uniqueness": "server",
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "displayName",
|
|
||||||
"type": "string",
|
|
||||||
"required": True
|
|
||||||
},
|
},
|
||||||
|
{"name": "displayName", "type": "string", "required": True},
|
||||||
{
|
{
|
||||||
"name": "emails",
|
"name": "emails",
|
||||||
"type": "complex",
|
"type": "complex",
|
||||||
"multiValued": True,
|
"multiValued": True,
|
||||||
"required": True
|
"required": True,
|
||||||
},
|
},
|
||||||
{
|
{"name": "active", "type": "boolean", "required": False},
|
||||||
"name": "active",
|
],
|
||||||
"type": "boolean",
|
|
||||||
"required": False
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"],
|
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"],
|
||||||
|
|
@ -451,19 +462,15 @@ async def get_schemas():
|
||||||
"name": "Group",
|
"name": "Group",
|
||||||
"description": "Group",
|
"description": "Group",
|
||||||
"attributes": [
|
"attributes": [
|
||||||
{
|
{"name": "displayName", "type": "string", "required": True},
|
||||||
"name": "displayName",
|
|
||||||
"type": "string",
|
|
||||||
"required": True
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "members",
|
"name": "members",
|
||||||
"type": "complex",
|
"type": "complex",
|
||||||
"multiValued": True,
|
"multiValued": True,
|
||||||
"required": False
|
"required": False,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -479,7 +486,7 @@ async def get_users(
|
||||||
"""List SCIM Users"""
|
"""List SCIM Users"""
|
||||||
skip = startIndex - 1
|
skip = startIndex - 1
|
||||||
limit = count
|
limit = count
|
||||||
|
|
||||||
# Get users from database
|
# Get users from database
|
||||||
if filter:
|
if filter:
|
||||||
# Simple filter parsing - supports userName eq "email"
|
# Simple filter parsing - supports userName eq "email"
|
||||||
|
|
@ -497,10 +504,10 @@ async def get_users(
|
||||||
response = Users.get_users(skip=skip, limit=limit)
|
response = Users.get_users(skip=skip, limit=limit)
|
||||||
users_list = response["users"]
|
users_list = response["users"]
|
||||||
total = response["total"]
|
total = response["total"]
|
||||||
|
|
||||||
# Convert to SCIM format
|
# Convert to SCIM format
|
||||||
scim_users = [user_to_scim(user, request) for user in users_list]
|
scim_users = [user_to_scim(user, request) for user in users_list]
|
||||||
|
|
||||||
return SCIMListResponse(
|
return SCIMListResponse(
|
||||||
totalResults=total,
|
totalResults=total,
|
||||||
itemsPerPage=len(scim_users),
|
itemsPerPage=len(scim_users),
|
||||||
|
|
@ -519,10 +526,9 @@ async def get_user(
|
||||||
user = Users.get_user_by_id(user_id)
|
user = Users.get_user_by_id(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
return scim_error(
|
return scim_error(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND, detail=f"User {user_id} not found"
|
||||||
detail=f"User {user_id} not found"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return user_to_scim(user, request)
|
return user_to_scim(user, request)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -540,11 +546,11 @@ async def create_user(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
detail=f"User with email {user_data.userName} already exists",
|
detail=f"User with email {user_data.userName} already exists",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create user
|
# Create user
|
||||||
user_id = str(uuid.uuid4())
|
user_id = str(uuid.uuid4())
|
||||||
email = user_data.emails[0].value if user_data.emails else user_data.userName
|
email = user_data.emails[0].value if user_data.emails else user_data.userName
|
||||||
|
|
||||||
# Parse name if provided
|
# Parse name if provided
|
||||||
name = user_data.displayName
|
name = user_data.displayName
|
||||||
if user_data.name:
|
if user_data.name:
|
||||||
|
|
@ -552,12 +558,12 @@ async def create_user(
|
||||||
name = user_data.name.formatted
|
name = user_data.name.formatted
|
||||||
elif user_data.name.givenName or user_data.name.familyName:
|
elif user_data.name.givenName or user_data.name.familyName:
|
||||||
name = f"{user_data.name.givenName or ''} {user_data.name.familyName or ''}".strip()
|
name = f"{user_data.name.givenName or ''} {user_data.name.familyName or ''}".strip()
|
||||||
|
|
||||||
# Get profile image if provided
|
# Get profile image if provided
|
||||||
profile_image = "/user.png"
|
profile_image = "/user.png"
|
||||||
if user_data.photos and len(user_data.photos) > 0:
|
if user_data.photos and len(user_data.photos) > 0:
|
||||||
profile_image = user_data.photos[0].value
|
profile_image = user_data.photos[0].value
|
||||||
|
|
||||||
# Create user
|
# Create user
|
||||||
new_user = Users.insert_new_user(
|
new_user = Users.insert_new_user(
|
||||||
id=user_id,
|
id=user_id,
|
||||||
|
|
@ -566,13 +572,13 @@ async def create_user(
|
||||||
profile_image_url=profile_image,
|
profile_image_url=profile_image,
|
||||||
role="user" if user_data.active else "pending",
|
role="user" if user_data.active else "pending",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not new_user:
|
if not new_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to create user",
|
detail="Failed to create user",
|
||||||
)
|
)
|
||||||
|
|
||||||
return user_to_scim(new_user, request)
|
return user_to_scim(new_user, request)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -590,30 +596,32 @@ async def update_user(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"User {user_id} not found",
|
detail=f"User {user_id} not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build update dict
|
# Build update dict
|
||||||
update_data = {}
|
update_data = {}
|
||||||
|
|
||||||
if user_data.userName:
|
if user_data.userName:
|
||||||
update_data["email"] = user_data.userName
|
update_data["email"] = user_data.userName
|
||||||
|
|
||||||
if user_data.displayName:
|
if user_data.displayName:
|
||||||
update_data["name"] = user_data.displayName
|
update_data["name"] = user_data.displayName
|
||||||
elif user_data.name:
|
elif user_data.name:
|
||||||
if user_data.name.formatted:
|
if user_data.name.formatted:
|
||||||
update_data["name"] = user_data.name.formatted
|
update_data["name"] = user_data.name.formatted
|
||||||
elif user_data.name.givenName or user_data.name.familyName:
|
elif user_data.name.givenName or user_data.name.familyName:
|
||||||
update_data["name"] = f"{user_data.name.givenName or ''} {user_data.name.familyName or ''}".strip()
|
update_data["name"] = (
|
||||||
|
f"{user_data.name.givenName or ''} {user_data.name.familyName or ''}".strip()
|
||||||
|
)
|
||||||
|
|
||||||
if user_data.emails and len(user_data.emails) > 0:
|
if user_data.emails and len(user_data.emails) > 0:
|
||||||
update_data["email"] = user_data.emails[0].value
|
update_data["email"] = user_data.emails[0].value
|
||||||
|
|
||||||
if user_data.active is not None:
|
if user_data.active is not None:
|
||||||
update_data["role"] = "user" if user_data.active else "pending"
|
update_data["role"] = "user" if user_data.active else "pending"
|
||||||
|
|
||||||
if user_data.photos and len(user_data.photos) > 0:
|
if user_data.photos and len(user_data.photos) > 0:
|
||||||
update_data["profile_image_url"] = user_data.photos[0].value
|
update_data["profile_image_url"] = user_data.photos[0].value
|
||||||
|
|
||||||
# Update user
|
# Update user
|
||||||
updated_user = Users.update_user_by_id(user_id, update_data)
|
updated_user = Users.update_user_by_id(user_id, update_data)
|
||||||
if not updated_user:
|
if not updated_user:
|
||||||
|
|
@ -621,7 +629,7 @@ async def update_user(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to update user",
|
detail="Failed to update user",
|
||||||
)
|
)
|
||||||
|
|
||||||
return user_to_scim(updated_user, request)
|
return user_to_scim(updated_user, request)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -639,14 +647,14 @@ async def patch_user(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"User {user_id} not found",
|
detail=f"User {user_id} not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
update_data = {}
|
update_data = {}
|
||||||
|
|
||||||
for operation in patch_data.Operations:
|
for operation in patch_data.Operations:
|
||||||
op = operation.op.lower()
|
op = operation.op.lower()
|
||||||
path = operation.path
|
path = operation.path
|
||||||
value = operation.value
|
value = operation.value
|
||||||
|
|
||||||
if op == "replace":
|
if op == "replace":
|
||||||
if path == "active":
|
if path == "active":
|
||||||
update_data["role"] = "user" if value else "pending"
|
update_data["role"] = "user" if value else "pending"
|
||||||
|
|
@ -658,7 +666,7 @@ async def patch_user(
|
||||||
update_data["email"] = value
|
update_data["email"] = value
|
||||||
elif path == "name.formatted":
|
elif path == "name.formatted":
|
||||||
update_data["name"] = value
|
update_data["name"] = value
|
||||||
|
|
||||||
# Update user
|
# Update user
|
||||||
if update_data:
|
if update_data:
|
||||||
updated_user = Users.update_user_by_id(user_id, update_data)
|
updated_user = Users.update_user_by_id(user_id, update_data)
|
||||||
|
|
@ -669,7 +677,7 @@ async def patch_user(
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
updated_user = user
|
updated_user = user
|
||||||
|
|
||||||
return user_to_scim(updated_user, request)
|
return user_to_scim(updated_user, request)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -686,14 +694,14 @@ async def delete_user(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"User {user_id} not found",
|
detail=f"User {user_id} not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
success = Users.delete_user_by_id(user_id)
|
success = Users.delete_user_by_id(user_id)
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to delete user",
|
detail="Failed to delete user",
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -709,16 +717,16 @@ async def get_groups(
|
||||||
"""List SCIM Groups"""
|
"""List SCIM Groups"""
|
||||||
# Get all groups
|
# Get all groups
|
||||||
groups_list = Groups.get_groups()
|
groups_list = Groups.get_groups()
|
||||||
|
|
||||||
# Apply pagination
|
# Apply pagination
|
||||||
total = len(groups_list)
|
total = len(groups_list)
|
||||||
start = startIndex - 1
|
start = startIndex - 1
|
||||||
end = start + count
|
end = start + count
|
||||||
paginated_groups = groups_list[start:end]
|
paginated_groups = groups_list[start:end]
|
||||||
|
|
||||||
# Convert to SCIM format
|
# Convert to SCIM format
|
||||||
scim_groups = [group_to_scim(group, request) for group in paginated_groups]
|
scim_groups = [group_to_scim(group, request) for group in paginated_groups]
|
||||||
|
|
||||||
return SCIMListResponse(
|
return SCIMListResponse(
|
||||||
totalResults=total,
|
totalResults=total,
|
||||||
itemsPerPage=len(scim_groups),
|
itemsPerPage=len(scim_groups),
|
||||||
|
|
@ -740,7 +748,7 @@ async def get_group(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"Group {group_id} not found",
|
detail=f"Group {group_id} not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
return group_to_scim(group, request)
|
return group_to_scim(group, request)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -756,15 +764,15 @@ async def create_group(
|
||||||
if group_data.members:
|
if group_data.members:
|
||||||
for member in group_data.members:
|
for member in group_data.members:
|
||||||
member_ids.append(member.value)
|
member_ids.append(member.value)
|
||||||
|
|
||||||
# Create group
|
# Create group
|
||||||
from open_webui.models.groups import GroupForm
|
from open_webui.models.groups import GroupForm
|
||||||
|
|
||||||
form = GroupForm(
|
form = GroupForm(
|
||||||
name=group_data.displayName,
|
name=group_data.displayName,
|
||||||
description="",
|
description="",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Need to get the creating user's ID - we'll use the first admin
|
# Need to get the creating user's ID - we'll use the first admin
|
||||||
admin_user = Users.get_super_admin_user()
|
admin_user = Users.get_super_admin_user()
|
||||||
if not admin_user:
|
if not admin_user:
|
||||||
|
|
@ -772,17 +780,18 @@ async def create_group(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="No admin user found",
|
detail="No admin user found",
|
||||||
)
|
)
|
||||||
|
|
||||||
new_group = Groups.insert_new_group(admin_user.id, form)
|
new_group = Groups.insert_new_group(admin_user.id, form)
|
||||||
if not new_group:
|
if not new_group:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to create group",
|
detail="Failed to create group",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add members if provided
|
# Add members if provided
|
||||||
if member_ids:
|
if member_ids:
|
||||||
from open_webui.models.groups import GroupUpdateForm
|
from open_webui.models.groups import GroupUpdateForm
|
||||||
|
|
||||||
update_form = GroupUpdateForm(
|
update_form = GroupUpdateForm(
|
||||||
name=new_group.name,
|
name=new_group.name,
|
||||||
description=new_group.description,
|
description=new_group.description,
|
||||||
|
|
@ -790,7 +799,7 @@ async def create_group(
|
||||||
)
|
)
|
||||||
Groups.update_group_by_id(new_group.id, update_form)
|
Groups.update_group_by_id(new_group.id, update_form)
|
||||||
new_group = Groups.get_group_by_id(new_group.id)
|
new_group = Groups.get_group_by_id(new_group.id)
|
||||||
|
|
||||||
return group_to_scim(new_group, request)
|
return group_to_scim(new_group, request)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -808,20 +817,20 @@ async def update_group(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"Group {group_id} not found",
|
detail=f"Group {group_id} not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build update form
|
# Build update form
|
||||||
from open_webui.models.groups import GroupUpdateForm
|
from open_webui.models.groups import GroupUpdateForm
|
||||||
|
|
||||||
update_form = GroupUpdateForm(
|
update_form = GroupUpdateForm(
|
||||||
name=group_data.displayName if group_data.displayName else group.name,
|
name=group_data.displayName if group_data.displayName else group.name,
|
||||||
description=group.description,
|
description=group.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle members if provided
|
# Handle members if provided
|
||||||
if group_data.members is not None:
|
if group_data.members is not None:
|
||||||
member_ids = [member.value for member in group_data.members]
|
member_ids = [member.value for member in group_data.members]
|
||||||
update_form.user_ids = member_ids
|
update_form.user_ids = member_ids
|
||||||
|
|
||||||
# Update group
|
# Update group
|
||||||
updated_group = Groups.update_group_by_id(group_id, update_form)
|
updated_group = Groups.update_group_by_id(group_id, update_form)
|
||||||
if not updated_group:
|
if not updated_group:
|
||||||
|
|
@ -829,7 +838,7 @@ async def update_group(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to update group",
|
detail="Failed to update group",
|
||||||
)
|
)
|
||||||
|
|
||||||
return group_to_scim(updated_group, request)
|
return group_to_scim(updated_group, request)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -847,20 +856,20 @@ async def patch_group(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"Group {group_id} not found",
|
detail=f"Group {group_id} not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
from open_webui.models.groups import GroupUpdateForm
|
from open_webui.models.groups import GroupUpdateForm
|
||||||
|
|
||||||
update_form = GroupUpdateForm(
|
update_form = GroupUpdateForm(
|
||||||
name=group.name,
|
name=group.name,
|
||||||
description=group.description,
|
description=group.description,
|
||||||
user_ids=group.user_ids.copy() if group.user_ids else [],
|
user_ids=group.user_ids.copy() if group.user_ids else [],
|
||||||
)
|
)
|
||||||
|
|
||||||
for operation in patch_data.Operations:
|
for operation in patch_data.Operations:
|
||||||
op = operation.op.lower()
|
op = operation.op.lower()
|
||||||
path = operation.path
|
path = operation.path
|
||||||
value = operation.value
|
value = operation.value
|
||||||
|
|
||||||
if op == "replace":
|
if op == "replace":
|
||||||
if path == "displayName":
|
if path == "displayName":
|
||||||
update_form.name = value
|
update_form.name = value
|
||||||
|
|
@ -881,7 +890,7 @@ async def patch_group(
|
||||||
member_id = path.split('"')[1]
|
member_id = path.split('"')[1]
|
||||||
if member_id in update_form.user_ids:
|
if member_id in update_form.user_ids:
|
||||||
update_form.user_ids.remove(member_id)
|
update_form.user_ids.remove(member_id)
|
||||||
|
|
||||||
# Update group
|
# Update group
|
||||||
updated_group = Groups.update_group_by_id(group_id, update_form)
|
updated_group = Groups.update_group_by_id(group_id, update_form)
|
||||||
if not updated_group:
|
if not updated_group:
|
||||||
|
|
@ -889,7 +898,7 @@ async def patch_group(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to update group",
|
detail="Failed to update group",
|
||||||
)
|
)
|
||||||
|
|
||||||
return group_to_scim(updated_group, request)
|
return group_to_scim(updated_group, request)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -906,12 +915,12 @@ async def delete_group(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"Group {group_id} not found",
|
detail=f"Group {group_id} not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
success = Groups.delete_group_by_id(group_id)
|
success = Groups.delete_group_by_id(group_id)
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to delete group",
|
detail="Failed to delete group",
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue