refac
This commit is contained in:
+179
-203
@@ -37,32 +37,32 @@ log = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
# SCIM 2.0 Schema URIs
|
||||
SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
|
||||
SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"
|
||||
SCIM_LIST_RESPONSE_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:ListResponse"
|
||||
SCIM_ERROR_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:Error"
|
||||
SCIM_USER_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:User'
|
||||
SCIM_GROUP_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:Group'
|
||||
SCIM_LIST_RESPONSE_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:ListResponse'
|
||||
SCIM_ERROR_SCHEMA = 'urn:ietf:params:scim:api:messages:2.0:Error'
|
||||
|
||||
# SCIM Resource Types
|
||||
SCIM_RESOURCE_TYPE_USER = "User"
|
||||
SCIM_RESOURCE_TYPE_GROUP = "Group"
|
||||
SCIM_RESOURCE_TYPE_USER = 'User'
|
||||
SCIM_RESOURCE_TYPE_GROUP = 'Group'
|
||||
|
||||
|
||||
def scim_error(status_code: int, detail: str, scim_type: Optional[str] = None):
|
||||
"""Create a SCIM-compliant error response"""
|
||||
error_body = {
|
||||
"schemas": [SCIM_ERROR_SCHEMA],
|
||||
"status": str(status_code),
|
||||
"detail": detail,
|
||||
'schemas': [SCIM_ERROR_SCHEMA],
|
||||
'status': str(status_code),
|
||||
'detail': detail,
|
||||
}
|
||||
|
||||
if scim_type:
|
||||
error_body["scimType"] = scim_type
|
||||
error_body['scimType'] = scim_type
|
||||
elif status_code == 404:
|
||||
error_body["scimType"] = "invalidValue"
|
||||
error_body['scimType'] = 'invalidValue'
|
||||
elif status_code == 409:
|
||||
error_body["scimType"] = "uniqueness"
|
||||
error_body['scimType'] = 'uniqueness'
|
||||
elif status_code == 400:
|
||||
error_body["scimType"] = "invalidSyntax"
|
||||
error_body['scimType'] = 'invalidSyntax'
|
||||
|
||||
return JSONResponse(status_code=status_code, content=error_body)
|
||||
|
||||
@@ -101,7 +101,7 @@ class SCIMEmail(BaseModel):
|
||||
"""SCIM Email"""
|
||||
|
||||
value: str
|
||||
type: Optional[str] = "work"
|
||||
type: Optional[str] = 'work'
|
||||
primary: bool = True
|
||||
display: Optional[str] = None
|
||||
|
||||
@@ -110,7 +110,7 @@ class SCIMPhoto(BaseModel):
|
||||
"""SCIM Photo"""
|
||||
|
||||
value: str
|
||||
type: Optional[str] = "photo"
|
||||
type: Optional[str] = 'photo'
|
||||
primary: bool = True
|
||||
display: Optional[str] = None
|
||||
|
||||
@@ -119,8 +119,8 @@ class SCIMGroupMember(BaseModel):
|
||||
"""SCIM Group Member"""
|
||||
|
||||
value: str # User ID
|
||||
ref: Optional[str] = Field(None, alias="$ref")
|
||||
type: Optional[str] = "User"
|
||||
ref: Optional[str] = Field(None, alias='$ref')
|
||||
type: Optional[str] = 'User'
|
||||
display: Optional[str] = None
|
||||
|
||||
|
||||
@@ -227,13 +227,11 @@ class SCIMPatchOperation(BaseModel):
|
||||
class SCIMPatchRequest(BaseModel):
|
||||
"""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]
|
||||
|
||||
|
||||
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
|
||||
Checks for SCIM-specific bearer token configured in the system
|
||||
@@ -241,8 +239,8 @@ def get_scim_auth(
|
||||
if not authorization:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authorization header required",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
detail='Authorization header required',
|
||||
headers={'WWW-Authenticate': 'Bearer'},
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -250,42 +248,40 @@ def get_scim_auth(
|
||||
if len(parts) != 2:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authorization format. Expected: Bearer <token>",
|
||||
detail='Invalid authorization format. Expected: Bearer <token>',
|
||||
)
|
||||
|
||||
scheme, token = parts
|
||||
if scheme.lower() != "bearer":
|
||||
if scheme.lower() != 'bearer':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication scheme",
|
||||
detail='Invalid authentication scheme',
|
||||
)
|
||||
|
||||
# Check if SCIM is enabled
|
||||
enable_scim = getattr(request.app.state, "ENABLE_SCIM", False)
|
||||
log.info(
|
||||
f"SCIM auth check - raw ENABLE_SCIM: {enable_scim}, type: {type(enable_scim)}"
|
||||
)
|
||||
enable_scim = getattr(request.app.state, 'ENABLE_SCIM', False)
|
||||
log.info(f'SCIM auth check - raw ENABLE_SCIM: {enable_scim}, type: {type(enable_scim)}')
|
||||
|
||||
# Handle both PersistentConfig and direct value
|
||||
if hasattr(enable_scim, "value"):
|
||||
if hasattr(enable_scim, 'value'):
|
||||
enable_scim = enable_scim.value
|
||||
|
||||
if not enable_scim:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="SCIM is not enabled",
|
||||
detail='SCIM is not enabled',
|
||||
)
|
||||
|
||||
# Verify the SCIM token
|
||||
scim_token = getattr(request.app.state, "SCIM_TOKEN", None)
|
||||
scim_token = getattr(request.app.state, 'SCIM_TOKEN', None)
|
||||
# Handle both PersistentConfig and direct value
|
||||
if hasattr(scim_token, "value"):
|
||||
if hasattr(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:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid SCIM token",
|
||||
detail='Invalid SCIM token',
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -293,13 +289,13 @@ def get_scim_auth(
|
||||
# Re-raise HTTP exceptions as-is
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f"SCIM authentication error: {e}")
|
||||
log.error(f'SCIM authentication error: {e}')
|
||||
import traceback
|
||||
|
||||
log.error(f"Traceback: {traceback.format_exc()}")
|
||||
log.error(f'Traceback: {traceback.format_exc()}')
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication failed",
|
||||
detail='Authentication failed',
|
||||
)
|
||||
|
||||
|
||||
@@ -311,8 +307,8 @@ def get_external_id(user: UserModel) -> Optional[str]:
|
||||
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"]
|
||||
if isinstance(provider_data, dict) and 'external_id' in provider_data:
|
||||
return provider_data['external_id']
|
||||
return None
|
||||
|
||||
|
||||
@@ -324,7 +320,7 @@ def get_scim_provider() -> str:
|
||||
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",
|
||||
detail='SCIM_AUTH_PROVIDER environment variable is required when SCIM is enabled',
|
||||
)
|
||||
return SCIM_AUTH_PROVIDER
|
||||
|
||||
@@ -343,18 +339,18 @@ def find_user_by_external_id(external_id: str, db=None) -> Optional[UserModel]:
|
||||
def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser:
|
||||
"""Convert internal User model to SCIM User"""
|
||||
# Parse display name into name components
|
||||
name_parts = user.name.split(" ", 1) if user.name else ["", ""]
|
||||
given_name = name_parts[0] if name_parts else ""
|
||||
family_name = name_parts[1] if len(name_parts) > 1 else ""
|
||||
name_parts = user.name.split(' ', 1) if user.name else ['', '']
|
||||
given_name = name_parts[0] if name_parts else ''
|
||||
family_name = name_parts[1] if len(name_parts) > 1 else ''
|
||||
|
||||
# Get user's groups
|
||||
user_groups = Groups.get_groups_by_member_id(user.id, db=db)
|
||||
groups = [
|
||||
{
|
||||
"value": group.id,
|
||||
"display": group.name,
|
||||
"$ref": f"{request.base_url}api/v1/scim/v2/Groups/{group.id}",
|
||||
"type": "direct",
|
||||
'value': group.id,
|
||||
'display': group.name,
|
||||
'$ref': f'{request.base_url}api/v1/scim/v2/Groups/{group.id}',
|
||||
'type': 'direct',
|
||||
}
|
||||
for group in user_groups
|
||||
]
|
||||
@@ -370,22 +366,14 @@ def user_to_scim(user: UserModel, request: Request, db=None) -> SCIMUser:
|
||||
),
|
||||
displayName=user.name,
|
||||
emails=[SCIMEmail(value=user.email)],
|
||||
active=user.role != "pending",
|
||||
photos=(
|
||||
[SCIMPhoto(value=user.profile_image_url)]
|
||||
if user.profile_image_url
|
||||
else None
|
||||
),
|
||||
active=user.role != 'pending',
|
||||
photos=([SCIMPhoto(value=user.profile_image_url)] if user.profile_image_url else None),
|
||||
groups=groups if groups else None,
|
||||
meta=SCIMMeta(
|
||||
resourceType=SCIM_RESOURCE_TYPE_USER,
|
||||
created=datetime.fromtimestamp(
|
||||
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}",
|
||||
created=datetime.fromtimestamp(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}',
|
||||
),
|
||||
)
|
||||
|
||||
@@ -399,7 +387,7 @@ def group_to_scim(group: GroupModel, request: Request, db=None) -> SCIMGroup:
|
||||
members = [
|
||||
SCIMGroupMember(
|
||||
value=user.id,
|
||||
ref=f"{request.base_url}api/v1/scim/v2/Users/{user.id}",
|
||||
ref=f'{request.base_url}api/v1/scim/v2/Users/{user.id}',
|
||||
display=user.name,
|
||||
)
|
||||
for user in users
|
||||
@@ -411,108 +399,104 @@ def group_to_scim(group: GroupModel, request: Request, db=None) -> SCIMGroup:
|
||||
members=members,
|
||||
meta=SCIMMeta(
|
||||
resourceType=SCIM_RESOURCE_TYPE_GROUP,
|
||||
created=datetime.fromtimestamp(
|
||||
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}",
|
||||
created=datetime.fromtimestamp(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}',
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# SCIM Service Provider Config
|
||||
@router.get("/ServiceProviderConfig")
|
||||
@router.get('/ServiceProviderConfig')
|
||||
async def get_service_provider_config():
|
||||
"""Get SCIM Service Provider Configuration"""
|
||||
return {
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
|
||||
"patch": {"supported": True},
|
||||
"bulk": {"supported": False, "maxOperations": 1000, "maxPayloadSize": 1048576},
|
||||
"filter": {"supported": True, "maxResults": 200},
|
||||
"changePassword": {"supported": False},
|
||||
"sort": {"supported": False},
|
||||
"etag": {"supported": False},
|
||||
"authenticationSchemes": [
|
||||
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig'],
|
||||
'patch': {'supported': True},
|
||||
'bulk': {'supported': False, 'maxOperations': 1000, 'maxPayloadSize': 1048576},
|
||||
'filter': {'supported': True, 'maxResults': 200},
|
||||
'changePassword': {'supported': False},
|
||||
'sort': {'supported': False},
|
||||
'etag': {'supported': False},
|
||||
'authenticationSchemes': [
|
||||
{
|
||||
"type": "oauthbearertoken",
|
||||
"name": "OAuth Bearer Token",
|
||||
"description": "Authentication using OAuth 2.0 Bearer Token",
|
||||
'type': 'oauthbearertoken',
|
||||
'name': 'OAuth Bearer Token',
|
||||
'description': 'Authentication using OAuth 2.0 Bearer Token',
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# SCIM Resource Types
|
||||
@router.get("/ResourceTypes")
|
||||
@router.get('/ResourceTypes')
|
||||
async def get_resource_types(request: Request):
|
||||
"""Get SCIM Resource Types"""
|
||||
return [
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
|
||||
"id": "User",
|
||||
"name": "User",
|
||||
"endpoint": "/Users",
|
||||
"schema": SCIM_USER_SCHEMA,
|
||||
"meta": {
|
||||
"location": f"{request.base_url}api/v1/scim/v2/ResourceTypes/User",
|
||||
"resourceType": "ResourceType",
|
||||
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'],
|
||||
'id': 'User',
|
||||
'name': 'User',
|
||||
'endpoint': '/Users',
|
||||
'schema': SCIM_USER_SCHEMA,
|
||||
'meta': {
|
||||
'location': f'{request.base_url}api/v1/scim/v2/ResourceTypes/User',
|
||||
'resourceType': 'ResourceType',
|
||||
},
|
||||
},
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
|
||||
"id": "Group",
|
||||
"name": "Group",
|
||||
"endpoint": "/Groups",
|
||||
"schema": SCIM_GROUP_SCHEMA,
|
||||
"meta": {
|
||||
"location": f"{request.base_url}api/v1/scim/v2/ResourceTypes/Group",
|
||||
"resourceType": "ResourceType",
|
||||
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:ResourceType'],
|
||||
'id': 'Group',
|
||||
'name': 'Group',
|
||||
'endpoint': '/Groups',
|
||||
'schema': SCIM_GROUP_SCHEMA,
|
||||
'meta': {
|
||||
'location': f'{request.base_url}api/v1/scim/v2/ResourceTypes/Group',
|
||||
'resourceType': 'ResourceType',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# SCIM Schemas
|
||||
@router.get("/Schemas")
|
||||
@router.get('/Schemas')
|
||||
async def get_schemas():
|
||||
"""Get SCIM Schemas"""
|
||||
return [
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"],
|
||||
"id": SCIM_USER_SCHEMA,
|
||||
"name": "User",
|
||||
"description": "User Account",
|
||||
"attributes": [
|
||||
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:Schema'],
|
||||
'id': SCIM_USER_SCHEMA,
|
||||
'name': 'User',
|
||||
'description': 'User Account',
|
||||
'attributes': [
|
||||
{
|
||||
"name": "userName",
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"uniqueness": "server",
|
||||
'name': 'userName',
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'uniqueness': 'server',
|
||||
},
|
||||
{"name": "displayName", "type": "string", "required": True},
|
||||
{'name': 'displayName', 'type': 'string', 'required': True},
|
||||
{
|
||||
"name": "emails",
|
||||
"type": "complex",
|
||||
"multiValued": True,
|
||||
"required": True,
|
||||
'name': 'emails',
|
||||
'type': 'complex',
|
||||
'multiValued': 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"],
|
||||
"id": SCIM_GROUP_SCHEMA,
|
||||
"name": "Group",
|
||||
"description": "Group",
|
||||
"attributes": [
|
||||
{"name": "displayName", "type": "string", "required": True},
|
||||
'schemas': ['urn:ietf:params:scim:schemas:core:2.0:Schema'],
|
||||
'id': SCIM_GROUP_SCHEMA,
|
||||
'name': 'Group',
|
||||
'description': 'Group',
|
||||
'attributes': [
|
||||
{'name': 'displayName', 'type': 'string', 'required': True},
|
||||
{
|
||||
"name": "members",
|
||||
"type": "complex",
|
||||
"multiValued": True,
|
||||
"required": False,
|
||||
'name': 'members',
|
||||
'type': 'complex',
|
||||
'multiValued': True,
|
||||
'required': False,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -520,7 +504,7 @@ async def get_schemas():
|
||||
|
||||
|
||||
# Users endpoints
|
||||
@router.get("/Users", response_model=SCIMListResponse)
|
||||
@router.get('/Users', response_model=SCIMListResponse)
|
||||
async def get_users(
|
||||
request: Request,
|
||||
startIndex: int = Query(1),
|
||||
@@ -540,24 +524,24 @@ async def get_users(
|
||||
# Get users from database
|
||||
if filter:
|
||||
# Simple filter parsing - supports userName eq, externalId eq
|
||||
if "userName eq" in filter:
|
||||
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:
|
||||
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"]
|
||||
total = response["total"]
|
||||
users_list = response['users']
|
||||
total = response['total']
|
||||
else:
|
||||
response = Users.get_users(skip=skip, limit=limit, db=db)
|
||||
users_list = response["users"]
|
||||
total = response["total"]
|
||||
users_list = response['users']
|
||||
total = response['total']
|
||||
|
||||
# Convert to SCIM format
|
||||
scim_users = [user_to_scim(user, request, db=db) for user in users_list]
|
||||
@@ -570,7 +554,7 @@ async def get_users(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/Users/{user_id}", response_model=SCIMUser)
|
||||
@router.get('/Users/{user_id}', response_model=SCIMUser)
|
||||
async def get_user(
|
||||
user_id: str,
|
||||
request: Request,
|
||||
@@ -580,14 +564,12 @@ async def get_user(
|
||||
"""Get SCIM User by ID"""
|
||||
user = Users.get_user_by_id(user_id, db=db)
|
||||
if not user:
|
||||
return scim_error(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"User {user_id} not found"
|
||||
)
|
||||
return scim_error(status_code=status.HTTP_404_NOT_FOUND, detail=f'User {user_id} not found')
|
||||
|
||||
return user_to_scim(user, request, db=db)
|
||||
|
||||
|
||||
@router.post("/Users", response_model=SCIMUser, status_code=status.HTTP_201_CREATED)
|
||||
@router.post('/Users', response_model=SCIMUser, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(
|
||||
request: Request,
|
||||
user_data: SCIMUserCreateRequest,
|
||||
@@ -601,7 +583,7 @@ async def create_user(
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"User with externalId {user_data.externalId} already exists",
|
||||
detail=f'User with externalId {user_data.externalId} already exists',
|
||||
)
|
||||
|
||||
# Determine primary email (lowercased per RFC 5321)
|
||||
@@ -617,7 +599,7 @@ async def create_user(
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"User with email {email} already exists",
|
||||
detail=f'User with email {email} already exists',
|
||||
)
|
||||
|
||||
# Create user
|
||||
@@ -629,10 +611,10 @@ async def create_user(
|
||||
if user_data.name.formatted:
|
||||
name = user_data.name.formatted
|
||||
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
|
||||
profile_image = "/user.png"
|
||||
profile_image = '/user.png'
|
||||
if user_data.photos and len(user_data.photos) > 0:
|
||||
profile_image = user_data.photos[0].value
|
||||
|
||||
@@ -641,14 +623,14 @@ async def create_user(
|
||||
name=name,
|
||||
email=email,
|
||||
profile_image_url=profile_image,
|
||||
role="user" if user_data.active else "pending",
|
||||
role='user' if user_data.active else 'pending',
|
||||
db=db,
|
||||
)
|
||||
|
||||
if not new_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to create user",
|
||||
detail='Failed to create user',
|
||||
)
|
||||
|
||||
# Store externalId in the scim field
|
||||
@@ -660,7 +642,7 @@ async def create_user(
|
||||
return user_to_scim(new_user, request, db=db)
|
||||
|
||||
|
||||
@router.put("/Users/{user_id}", response_model=SCIMUser)
|
||||
@router.put('/Users/{user_id}', response_model=SCIMUser)
|
||||
async def update_user(
|
||||
user_id: str,
|
||||
request: Request,
|
||||
@@ -673,39 +655,37 @@ async def update_user(
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"User {user_id} not found",
|
||||
detail=f'User {user_id} not found',
|
||||
)
|
||||
|
||||
# Build update dict
|
||||
update_data = {}
|
||||
|
||||
if user_data.userName:
|
||||
update_data["email"] = user_data.userName
|
||||
update_data['email'] = user_data.userName
|
||||
|
||||
if user_data.displayName:
|
||||
update_data["name"] = user_data.displayName
|
||||
update_data['name'] = user_data.displayName
|
||||
elif user_data.name:
|
||||
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:
|
||||
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:
|
||||
update_data["email"] = user_data.emails[0].value
|
||||
update_data['email'] = user_data.emails[0].value
|
||||
|
||||
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:
|
||||
update_data["profile_image_url"] = user_data.photos[0].value
|
||||
update_data['profile_image_url'] = user_data.photos[0].value
|
||||
|
||||
updated_user = Users.update_user_by_id(user_id, update_data, db=db)
|
||||
if not updated_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update user",
|
||||
detail='Failed to update user',
|
||||
)
|
||||
|
||||
# Update externalId in the scim field
|
||||
@@ -717,7 +697,7 @@ async def update_user(
|
||||
return user_to_scim(updated_user, request, db=db)
|
||||
|
||||
|
||||
@router.patch("/Users/{user_id}", response_model=SCIMUser)
|
||||
@router.patch('/Users/{user_id}', response_model=SCIMUser)
|
||||
async def patch_user(
|
||||
user_id: str,
|
||||
request: Request,
|
||||
@@ -730,7 +710,7 @@ async def patch_user(
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"User {user_id} not found",
|
||||
detail=f'User {user_id} not found',
|
||||
)
|
||||
|
||||
update_data = {}
|
||||
@@ -740,18 +720,18 @@ async def patch_user(
|
||||
path = operation.path
|
||||
value = operation.value
|
||||
|
||||
if op == "replace":
|
||||
if path == "active":
|
||||
update_data["role"] = "user" if value else "pending"
|
||||
elif path == "userName":
|
||||
update_data["email"] = value
|
||||
elif path == "displayName":
|
||||
update_data["name"] = value
|
||||
elif path == "emails[primary eq true].value":
|
||||
update_data["email"] = value
|
||||
elif path == "name.formatted":
|
||||
update_data["name"] = value
|
||||
elif path == "externalId":
|
||||
if op == 'replace':
|
||||
if path == 'active':
|
||||
update_data['role'] = 'user' if value else 'pending'
|
||||
elif path == 'userName':
|
||||
update_data['email'] = value
|
||||
elif path == 'displayName':
|
||||
update_data['name'] = value
|
||||
elif path == 'emails[primary eq true].value':
|
||||
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)
|
||||
|
||||
@@ -761,7 +741,7 @@ async def patch_user(
|
||||
if not updated_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update user",
|
||||
detail='Failed to update user',
|
||||
)
|
||||
else:
|
||||
updated_user = user
|
||||
@@ -769,7 +749,7 @@ async def patch_user(
|
||||
return user_to_scim(updated_user, request, db=db)
|
||||
|
||||
|
||||
@router.delete("/Users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete('/Users/{user_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user(
|
||||
user_id: str,
|
||||
request: Request,
|
||||
@@ -781,21 +761,21 @@ async def delete_user(
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
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, db=db)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to delete user",
|
||||
detail='Failed to delete user',
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Groups endpoints
|
||||
@router.get("/Groups", response_model=SCIMListResponse)
|
||||
@router.get('/Groups', response_model=SCIMListResponse)
|
||||
async def get_groups(
|
||||
request: Request,
|
||||
startIndex: int = Query(1),
|
||||
@@ -830,7 +810,7 @@ async def get_groups(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/Groups/{group_id}", response_model=SCIMGroup)
|
||||
@router.get('/Groups/{group_id}', response_model=SCIMGroup)
|
||||
async def get_group(
|
||||
group_id: str,
|
||||
request: Request,
|
||||
@@ -842,13 +822,13 @@ async def get_group(
|
||||
if not group:
|
||||
raise HTTPException(
|
||||
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, db=db)
|
||||
|
||||
|
||||
@router.post("/Groups", response_model=SCIMGroup, status_code=status.HTTP_201_CREATED)
|
||||
@router.post('/Groups', response_model=SCIMGroup, status_code=status.HTTP_201_CREATED)
|
||||
async def create_group(
|
||||
request: Request,
|
||||
group_data: SCIMGroupCreateRequest,
|
||||
@@ -867,7 +847,7 @@ async def create_group(
|
||||
|
||||
form = GroupForm(
|
||||
name=group_data.displayName,
|
||||
description="",
|
||||
description='',
|
||||
)
|
||||
|
||||
# Need to get the creating user's ID - we'll use the first admin
|
||||
@@ -875,14 +855,14 @@ async def create_group(
|
||||
if not admin_user:
|
||||
raise HTTPException(
|
||||
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, db=db)
|
||||
if not new_group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to create group",
|
||||
detail='Failed to create group',
|
||||
)
|
||||
|
||||
# Add members if provided
|
||||
@@ -902,7 +882,7 @@ async def create_group(
|
||||
return group_to_scim(new_group, request, db=db)
|
||||
|
||||
|
||||
@router.put("/Groups/{group_id}", response_model=SCIMGroup)
|
||||
@router.put('/Groups/{group_id}', response_model=SCIMGroup)
|
||||
async def update_group(
|
||||
group_id: str,
|
||||
request: Request,
|
||||
@@ -915,7 +895,7 @@ async def update_group(
|
||||
if not group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Group {group_id} not found",
|
||||
detail=f'Group {group_id} not found',
|
||||
)
|
||||
|
||||
# Build update form
|
||||
@@ -936,13 +916,13 @@ async def update_group(
|
||||
if not updated_group:
|
||||
raise HTTPException(
|
||||
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, db=db)
|
||||
|
||||
|
||||
@router.patch("/Groups/{group_id}", response_model=SCIMGroup)
|
||||
@router.patch('/Groups/{group_id}', response_model=SCIMGroup)
|
||||
async def patch_group(
|
||||
group_id: str,
|
||||
request: Request,
|
||||
@@ -955,7 +935,7 @@ async def patch_group(
|
||||
if not group:
|
||||
raise HTTPException(
|
||||
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
|
||||
@@ -970,26 +950,22 @@ async def patch_group(
|
||||
path = operation.path
|
||||
value = operation.value
|
||||
|
||||
if op == "replace":
|
||||
if path == "displayName":
|
||||
if op == 'replace':
|
||||
if path == 'displayName':
|
||||
update_form.name = value
|
||||
elif path == "members":
|
||||
elif path == 'members':
|
||||
# Replace all members
|
||||
Groups.set_group_user_ids_by_id(
|
||||
group_id, [member["value"] for member in value], db=db
|
||||
)
|
||||
Groups.set_group_user_ids_by_id(group_id, [member['value'] for member in value], db=db)
|
||||
|
||||
elif op == "add":
|
||||
if path == "members":
|
||||
elif op == 'add':
|
||||
if path == 'members':
|
||||
# Add members
|
||||
if isinstance(value, list):
|
||||
for member in value:
|
||||
if isinstance(member, dict) and "value" in member:
|
||||
Groups.add_users_to_group(
|
||||
group_id, [member["value"]], db=db
|
||||
)
|
||||
elif op == "remove":
|
||||
if path and path.startswith("members[value eq"):
|
||||
if isinstance(member, dict) and 'value' in member:
|
||||
Groups.add_users_to_group(group_id, [member['value']], db=db)
|
||||
elif op == 'remove':
|
||||
if path and path.startswith('members[value eq'):
|
||||
# Remove specific member
|
||||
member_id = path.split('"')[1]
|
||||
Groups.remove_users_from_group(group_id, [member_id], db=db)
|
||||
@@ -999,13 +975,13 @@ async def patch_group(
|
||||
if not updated_group:
|
||||
raise HTTPException(
|
||||
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, db=db)
|
||||
|
||||
|
||||
@router.delete("/Groups/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete('/Groups/{group_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_group(
|
||||
group_id: str,
|
||||
request: Request,
|
||||
@@ -1017,14 +993,14 @@ async def delete_group(
|
||||
if not group:
|
||||
raise HTTPException(
|
||||
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, db=db)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to delete group",
|
||||
detail='Failed to delete group',
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user