add patch endpoint for users
This commit is contained in:
parent
b0531ef223
commit
54b8ccb39c
3 changed files with 97 additions and 4 deletions
|
|
@ -2,9 +2,11 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from decouple import config
|
from decouple import config
|
||||||
from fastapi import BackgroundTasks, HTTPException
|
from fastapi import BackgroundTasks, HTTPException
|
||||||
|
from psycopg2.extensions import AsIs
|
||||||
from psycopg2.extras import Json
|
from psycopg2.extras import Json
|
||||||
from pydantic import BaseModel, model_validator
|
from pydantic import BaseModel, model_validator
|
||||||
from starlette import status
|
from starlette import status
|
||||||
|
|
@ -482,6 +484,7 @@ def restore_scim_user(
|
||||||
role_id = %(role_id)s,
|
role_id = %(role_id)s,
|
||||||
deleted_at = NULL,
|
deleted_at = NULL,
|
||||||
created_at = default,
|
created_at = default,
|
||||||
|
updated_at = default,
|
||||||
api_key = default,
|
api_key = default,
|
||||||
jwt_iat = NULL,
|
jwt_iat = NULL,
|
||||||
weekly_report = default
|
weekly_report = default
|
||||||
|
|
@ -523,7 +526,8 @@ def update_scim_user(
|
||||||
email = %(email)s,
|
email = %(email)s,
|
||||||
name = %(name)s,
|
name = %(name)s,
|
||||||
internal_id = %(internal_id)s,
|
internal_id = %(internal_id)s,
|
||||||
role_id = %(role_id)s
|
role_id = %(role_id)s,
|
||||||
|
updated_at = default
|
||||||
WHERE
|
WHERE
|
||||||
users.user_id = %(user_id)s
|
users.user_id = %(user_id)s
|
||||||
AND users.tenant_id = %(tenant_id)s
|
AND users.tenant_id = %(tenant_id)s
|
||||||
|
|
@ -548,6 +552,39 @@ def update_scim_user(
|
||||||
return helper.dict_to_camel_case(cur.fetchone())
|
return helper.dict_to_camel_case(cur.fetchone())
|
||||||
|
|
||||||
|
|
||||||
|
def patch_scim_user(
|
||||||
|
user_id: int,
|
||||||
|
tenant_id: int,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
with pg_client.PostgresClient() as cur:
|
||||||
|
set_fragments = []
|
||||||
|
kwargs["updated_at"] = datetime.now()
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
fragment = cur.mogrify(
|
||||||
|
"%s = %s",
|
||||||
|
(AsIs(k), v),
|
||||||
|
).decode("utf-8")
|
||||||
|
set_fragments.append(fragment)
|
||||||
|
set_clause = ", ".join(set_fragments)
|
||||||
|
query = f"""
|
||||||
|
WITH u AS (
|
||||||
|
UPDATE public.users
|
||||||
|
SET {set_clause}
|
||||||
|
WHERE
|
||||||
|
users.user_id = {user_id}
|
||||||
|
AND users.tenant_id = {tenant_id}
|
||||||
|
AND users.deleted_at IS NULL
|
||||||
|
RETURNING *
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
u.*,
|
||||||
|
roles.name as role_name
|
||||||
|
FROM u LEFT JOIN public.roles USING (role_id);"""
|
||||||
|
cur.execute(query)
|
||||||
|
return helper.dict_to_camel_case(cur.fetchone())
|
||||||
|
|
||||||
|
|
||||||
def generate_new_api_key(user_id):
|
def generate_new_api_key(user_id):
|
||||||
with pg_client.PostgresClient() as cur:
|
with pg_client.PostgresClient() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|
@ -1224,7 +1261,9 @@ def soft_delete_scim_user_by_id(user_id, tenant_id):
|
||||||
cur.mogrify(
|
cur.mogrify(
|
||||||
"""
|
"""
|
||||||
UPDATE public.users
|
UPDATE public.users
|
||||||
SET deleted_at = NULL
|
SET
|
||||||
|
deleted_at = NULL,
|
||||||
|
updated_at = default
|
||||||
WHERE
|
WHERE
|
||||||
users.user_id = %(user_id)s
|
users.user_id = %(user_id)s
|
||||||
AND users.tenant_id = %(tenant_id)s
|
AND users.tenant_id = %(tenant_id)s
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
|
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
|
||||||
],
|
],
|
||||||
"patch": {
|
"patch": {
|
||||||
"supported": false
|
"supported": true
|
||||||
},
|
},
|
||||||
"bulk": {
|
"bulk": {
|
||||||
"supported": false,
|
"supported": false,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from copy import deepcopy
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from decouple import config
|
from decouple import config
|
||||||
from fastapi import Depends, HTTPException, Header, Query, Response, Request
|
from fastapi import Depends, HTTPException, Header, Query, Response, Request
|
||||||
|
|
@ -263,6 +264,15 @@ def _parse_scim_user_input(data: dict[str, Any], tenant_id: str) -> dict[str, An
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_user_patch_operations(data: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
result = {}
|
||||||
|
operations = data["Operations"]
|
||||||
|
for operation in operations:
|
||||||
|
if operation["op"] == "replace" and "active" in operation["value"]:
|
||||||
|
result["deleted_at"] = None if operation["value"]["active"] is True else datetime.now()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _serialize_db_user_to_scim_user(db_user: dict[str, Any]) -> dict[str, Any]:
|
def _serialize_db_user_to_scim_user(db_user: dict[str, Any]) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"id": str(db_user["userId"]),
|
"id": str(db_user["userId"]),
|
||||||
|
|
@ -331,6 +341,8 @@ RESOURCE_TYPE_TO_RESOURCE_CONFIG = {
|
||||||
"delete_resource": users.soft_delete_scim_user_by_id,
|
"delete_resource": users.soft_delete_scim_user_by_id,
|
||||||
"parse_put_payload": _parse_scim_user_input,
|
"parse_put_payload": _parse_scim_user_input,
|
||||||
"update_resource": users.update_scim_user,
|
"update_resource": users.update_scim_user,
|
||||||
|
"parse_patch_operations": _parse_user_patch_operations,
|
||||||
|
"patch_resource": users.patch_scim_user,
|
||||||
},
|
},
|
||||||
"Groups": {
|
"Groups": {
|
||||||
"max_items_per_page": 10,
|
"max_items_per_page": 10,
|
||||||
|
|
@ -430,6 +442,7 @@ class PostResourceType(str, Enum):
|
||||||
GROUPS = "Groups"
|
GROUPS = "Groups"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@public_app.post("/{resource_type}")
|
@public_app.post("/{resource_type}")
|
||||||
async def create_resource(
|
async def create_resource(
|
||||||
resource_type: PostResourceType,
|
resource_type: PostResourceType,
|
||||||
|
|
@ -492,7 +505,7 @@ class PutResourceType(str, Enum):
|
||||||
|
|
||||||
|
|
||||||
@public_app.put("/{resource_type}/{resource_id}")
|
@public_app.put("/{resource_type}/{resource_id}")
|
||||||
async def update_resource(
|
async def put_resource(
|
||||||
resource_type: PutResourceType,
|
resource_type: PutResourceType,
|
||||||
resource_id: str,
|
resource_id: str,
|
||||||
r: Request,
|
r: Request,
|
||||||
|
|
@ -539,3 +552,44 @@ async def update_resource(
|
||||||
return _uniqueness_error_response()
|
return _uniqueness_error_response()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _internal_server_error_response(str(e))
|
return _internal_server_error_response(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
class PatchResourceType(str, Enum):
|
||||||
|
USERS = "Users"
|
||||||
|
|
||||||
|
|
||||||
|
@public_app.patch("/{resource_type}/{resource_id}")
|
||||||
|
async def patch_resource(
|
||||||
|
resource_type: PatchResourceType,
|
||||||
|
resource_id: str,
|
||||||
|
r: Request,
|
||||||
|
tenant_id=Depends(auth_required),
|
||||||
|
):
|
||||||
|
resource_config = RESOURCE_TYPE_TO_RESOURCE_CONFIG[resource_type]
|
||||||
|
db_resource = resource_config["get_unique_resource"](resource_id, tenant_id)
|
||||||
|
if not db_resource:
|
||||||
|
return _not_found_error_response(resource_id)
|
||||||
|
current_scim_resource = (
|
||||||
|
_serialize_db_resource_to_scim_resource_with_attribute_awareness(
|
||||||
|
db_resource,
|
||||||
|
resource_config["schema_id"],
|
||||||
|
resource_config["db_to_scim_serializer"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
payload = await r.json()
|
||||||
|
parsed_payload = resource_config["parse_patch_operations"](payload)
|
||||||
|
# note(jon): we don't need to handle uniqueness contraints and etc. like in PUT
|
||||||
|
# because we are only covering the User resource and the field `active`
|
||||||
|
updated_db_resource = resource_config["patch_resource"](
|
||||||
|
resource_id,
|
||||||
|
tenant_id,
|
||||||
|
**parsed_payload,
|
||||||
|
)
|
||||||
|
updated_scim_resource = (
|
||||||
|
_serialize_db_resource_to_scim_resource_with_attribute_awareness(
|
||||||
|
updated_db_resource,
|
||||||
|
resource_config["schema_id"],
|
||||||
|
resource_config["db_to_scim_serializer"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return JSONResponse(status_code=200, content=updated_scim_resource)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue