update PUT /Users endpoint

This commit is contained in:
Jonathan Griffin 2025-04-18 09:51:22 +02:00
parent 2cbac647b8
commit 6bee490312
3 changed files with 106 additions and 35 deletions

View file

@ -143,6 +143,37 @@ def reset_member(tenant_id, editor_id, user_id_to_update):
return {"data": {"invitationLink": generate_new_invitation(user_id_to_update)}}
def update_scim_user(
user_id: int,
tenant_id: int,
email: str,
):
with pg_client.PostgresClient() as cur:
cur.execute(
cur.mogrify(
"""
WITH u AS (
UPDATE public.users
SET email = %(email)s
WHERE
users.user_id = %(user_id)s
AND users.tenant_id = %(tenant_id)s
AND users.deleted_at IS NULL
RETURNING *
)
SELECT *
FROM u;
""",
{
"tenant_id": tenant_id,
"user_id": user_id,
"email": email,
}
)
)
return helper.dict_to_camel_case(cur.fetchone())
def update(tenant_id, user_id, changes, output=True):
AUTH_KEYS = ["password", "invitationToken", "invitedAt", "changePwdExpireAt", "changePwdToken"]
if len(changes.keys()) == 0:
@ -1124,7 +1155,7 @@ def restore_scim_user(
api_key = default,
jwt_iat = NULL,
weekly_report = default
WHERE user_id = %(user_id)s
WHERE users.user_id = %(user_id)s
RETURNING *
)
SELECT *

View file

@ -84,6 +84,18 @@ def _uniqueness_error_response():
)
def _mutability_error_response():
return JSONResponse(
status_code=400,
content={
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"detail": "The attempted modification is not compatible with the target attribute's mutability or current state.",
"status": "400",
"scimType": "mutability",
}
)
@public_app.get("/ResourceTypes", dependencies=[Depends(auth_required)])
async def get_resource_types(filter_param: str | None = Query(None, alias="filter")):
if filter_param is not None:
@ -329,42 +341,33 @@ async def create_user(r: UserRequest, tenant_id = Depends(auth_required)):
return response
@public_app.put("/Users/{user_id}", dependencies=[Depends(auth_required)])
def update_user(user_id: str, r: UserRequest):
"""Update SCIM User"""
tenant_id = 1
user = users.get_by_uuid(user_id, tenant_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
changes = r.model_dump(mode='json', exclude={"schemas", "emails", "name", "locale", "groups", "password", "active"}) # some of these should be added later if necessary
nested_changes = r.model_dump(mode='json', include={"name", "emails"})
mapping = {"userName": "email", "displayName": "name", "externalId": "internal_id"} # mapping between scim schema field names and local database model, can be done as config?
for k, v in mapping.items():
if k in changes:
changes[v] = changes.pop(k)
changes["data"] = {}
for k, v in nested_changes.items():
value_to_insert = v[0] if k == "emails" else v
changes["data"][k] = value_to_insert
@public_app.put("/Users/{user_id}")
def update_user(user_id: str, r: UserRequest, tenant_id = Depends(auth_required)):
db_resource = users.get_scim_user_by_id(user_id, tenant_id)
if not db_resource:
return _not_found_error_response(user_id)
current_scim_resource = _convert_db_user_to_scim_user(db_resource).model_dump(mode="json", exclude_none=True)
changes = r.model_dump(mode="json")
schema = SCHEMA_IDS_TO_SCHEMA_DETAILS["urn:ietf:params:scim:schemas:core:2.0:User"]
try:
users.update(tenant_id, user["userId"], changes)
res = UserResponse(
schemas = ["urn:ietf:params:scim:schemas:core:2.0:User"],
id = user["data"]["userId"],
userName = r.userName,
name = r.name,
emails = r.emails,
displayName = r.displayName,
locale = r.locale,
externalId = r.externalId,
active = r.active, # ignore for now, since, can't insert actual timestamp
groups = [], # ignore
valid_mutable_changes = scim_helpers.filter_mutable_attributes(schema, changes, current_scim_resource)
except ValueError:
# todo(jon): will need to add a test for this once we have an immutable field
return _mutability_error_response()
try:
updated_db_resource = users.update_scim_user(
user_id,
tenant_id,
email=valid_mutable_changes["userName"],
)
return JSONResponse(status_code=201, content=res.model_dump(mode='json'))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
updated_scim_resource = _convert_db_user_to_scim_user(updated_db_resource)
return JSONResponse(
status_code=200,
content=updated_scim_resource.model_dump(mode="json", exclude_none=True),
)
except Exception:
# note(jon): for now, this is the only error that would happen when updating the scim user
return _uniqueness_error_response()
@public_app.delete("/Users/{user_id}")

View file

@ -103,3 +103,40 @@ def exclude_attributes(resource: dict[str, Any], exclude_list: list[str]) -> dic
else:
new_resource[key] = value
return new_resource
def filter_mutable_attributes(schema: dict[str, Any], requested_changes: dict[str, Any], current: dict[str, Any]) -> dict[str, Any]:
attributes = {attr.get("name"): attr for attr in schema.get("attributes", [])}
valid_changes = {}
for attr_name, new_value in requested_changes.items():
attr_def = attributes.get(attr_name)
if not attr_def:
# Unknown attribute: ignore per RFC 7644
continue
mutability = attr_def.get("mutability", "readWrite")
if mutability == "readWrite" or mutability == "writeOnly":
valid_changes[attr_name] = new_value
elif mutability == "readOnly":
# Cannot modify read-only attributes: ignore
continue
elif mutability == "immutable":
# Only valid if the new value matches the current value exactly
current_value = current_values.get(attr_name)
if new_value != current_value:
raise ValueError(
f"Attribute '{attr_name}' is immutable (cannot change). "
f"Current value: {current_value!r}, attempted change: {new_value!r}"
)
# If it matches, no change is needed (already set)
else:
# Unknown mutability: default to safe behavior (ignore)
continue
return valid_changes