diff --git a/ee/api/chalicelib/core/roles.py b/ee/api/chalicelib/core/roles.py index 955c76af0..0bad7aade 100644 --- a/ee/api/chalicelib/core/roles.py +++ b/ee/api/chalicelib/core/roles.py @@ -1,4 +1,3 @@ -import json from typing import Optional from fastapi import HTTPException, status @@ -79,21 +78,6 @@ def update(tenant_id, user_id, role_id, data: schemas.RolePayloadSchema): return helper.dict_to_camel_case(row) -def update_group_name(tenant_id, group_id, name): - with pg_client.PostgresClient() as cur: - query = cur.mogrify("""UPDATE public.roles - SET name= %(name)s - WHERE roles.data->>'group_id' = %(group_id)s - AND tenant_id = %(tenant_id)s - AND deleted_at ISNULL - AND protected = FALSE - RETURNING *;""", - {"tenant_id": tenant_id, "group_id": group_id, "name": name }) - cur.execute(query=query) - row = cur.fetchone() - - return helper.dict_to_camel_case(row) - def create(tenant_id, user_id, data: schemas.RolePayloadSchema): admin = users.get(user_id=user_id, tenant_id=tenant_id) @@ -128,35 +112,6 @@ def create(tenant_id, user_id, data: schemas.RolePayloadSchema): row["projects"] = [r["project_id"] for r in cur.fetchall()] return helper.dict_to_camel_case(row) -def create_as_admin(tenant_id, group_id, data: schemas.RolePayloadSchema): - - if __exists_by_name(tenant_id=tenant_id, name=data.name, exclude_id=None): - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"name already exists.") - - if not data.all_projects and (data.projects is None or len(data.projects) == 0): - return {"errors": ["must specify a project or all projects"]} - if data.projects is not None and len(data.projects) > 0 and not data.all_projects: - data.projects = projects.is_authorized_batch(project_ids=data.projects, tenant_id=tenant_id) - with pg_client.PostgresClient() as cur: - query = cur.mogrify("""INSERT INTO roles(tenant_id, name, description, permissions, all_projects, data) - VALUES (%(tenant_id)s, %(name)s, %(description)s, %(permissions)s::text[], %(all_projects)s, %(data)s) - RETURNING *;""", - {"tenant_id": tenant_id, "name": data.name, "description": data.description, - "permissions": data.permissions, "all_projects": data.all_projects, "data": json.dumps({ "group_id": group_id })}) - cur.execute(query=query) - row = cur.fetchone() - row["created_at"] = TimeUTC.datetime_to_timestamp(row["created_at"]) - row["projects"] = [] - if not data.all_projects: - role_id = row["role_id"] - query = cur.mogrify(f"""INSERT INTO roles_projects(role_id, project_id) - VALUES {",".join(f"(%(role_id)s,%(project_id_{i})s)" for i in range(len(data.projects)))} - RETURNING project_id;""", - {"role_id": role_id, **{f"project_id_{i}": p for i, p in enumerate(data.projects)}}) - cur.execute(query=query) - row["projects"] = [r["project_id"] for r in cur.fetchall()] - return helper.dict_to_camel_case(row) - def get_roles(tenant_id): with pg_client.PostgresClient() as cur: @@ -178,52 +133,8 @@ def get_roles(tenant_id): r["created_at"] = TimeUTC.datetime_to_timestamp(r["created_at"]) return helper.list_to_camel_case(rows) -def get_roles_with_uuid(tenant_id): - with pg_client.PostgresClient() as cur: - query = cur.mogrify("""SELECT roles.*, COALESCE(projects, '{}') AS projects - FROM public.roles - LEFT JOIN LATERAL (SELECT array_agg(project_id) AS projects - FROM roles_projects - INNER JOIN projects USING (project_id) - WHERE roles_projects.role_id = roles.role_id - AND projects.deleted_at ISNULL ) AS role_projects ON (TRUE) - WHERE tenant_id =%(tenant_id)s - AND data ? 'group_id' - AND deleted_at IS NULL - AND not service_role - ORDER BY role_id;""", - {"tenant_id": tenant_id}) - cur.execute(query=query) - rows = cur.fetchall() - for r in rows: - r["created_at"] = TimeUTC.datetime_to_timestamp(r["created_at"]) - return helper.list_to_camel_case(rows) - -def get_roles_with_uuid_paginated(tenant_id, start_index, count=None, name=None): - with pg_client.PostgresClient() as cur: - query = cur.mogrify("""SELECT roles.*, COALESCE(projects, '{}') AS projects - FROM public.roles - LEFT JOIN LATERAL (SELECT array_agg(project_id) AS projects - FROM roles_projects - INNER JOIN projects USING (project_id) - WHERE roles_projects.role_id = roles.role_id - AND projects.deleted_at ISNULL ) AS role_projects ON (TRUE) - WHERE tenant_id =%(tenant_id)s - AND data ? 'group_id' - AND deleted_at IS NULL - AND not service_role - AND name = COALESCE(%(name)s, name) - ORDER BY role_id - LIMIT %(count)s - OFFSET %(startIndex)s;""", - {"tenant_id": tenant_id, "name": name, "startIndex": start_index - 1, "count": count}) - cur.execute(query=query) - rows = cur.fetchall() - return helper.list_to_camel_case(rows) - def get_role_by_name(tenant_id, name): - ### "name" isn't unique in database with pg_client.PostgresClient() as cur: query = cur.mogrify("""SELECT * FROM public.roles @@ -272,29 +183,6 @@ def delete(tenant_id, user_id, role_id): cur.execute(query=query) return get_roles(tenant_id=tenant_id) -def delete_scim_group(tenant_id, group_uuid): - - with pg_client.PostgresClient() as cur: - query = cur.mogrify("""SELECT 1 - FROM public.roles - WHERE data->>'group_id' = %(group_uuid)s - AND tenant_id = %(tenant_id)s - AND protected = TRUE - LIMIT 1;""", - {"tenant_id": tenant_id, "group_uuid": group_uuid}) - cur.execute(query) - if cur.fetchone() is not None: - return {"errors": ["this role is protected"]} - - query = cur.mogrify( - f"""DELETE FROM public.roles - WHERE roles.data->>'group_id' = %(group_uuid)s;""", # removed this: AND users.deleted_at IS NOT NULL - {"group_uuid": group_uuid}) - cur.execute(query) - - return get_roles(tenant_id=tenant_id) - - def get_role(tenant_id, role_id): with pg_client.PostgresClient() as cur: @@ -311,72 +199,3 @@ def get_role(tenant_id, role_id): if row is not None: row["created_at"] = TimeUTC.datetime_to_timestamp(row["created_at"]) return helper.dict_to_camel_case(row) - -def get_role_by_group_id(tenant_id, group_id): - with pg_client.PostgresClient() as cur: - query = cur.mogrify("""SELECT roles.* - FROM public.roles - WHERE tenant_id =%(tenant_id)s - AND deleted_at IS NULL - AND not service_role - AND data->>'group_id' = %(group_id)s - LIMIT 1;""", - {"tenant_id": tenant_id, "group_id": group_id}) - cur.execute(query=query) - row = cur.fetchone() - if row is not None: - row["created_at"] = TimeUTC.datetime_to_timestamp(row["created_at"]) - return helper.dict_to_camel_case(row) - -def get_users_by_group_uuid(tenant_id, group_id): - with pg_client.PostgresClient() as cur: - query = cur.mogrify("""SELECT - u.user_id, - u.name, - u.data - FROM public.roles r - LEFT JOIN public.users u USING (role_id, tenant_id) - WHERE u.tenant_id = %(tenant_id)s - AND u.deleted_at IS NULL - AND r.data->>'group_id' = %(group_id)s - """, - {"tenant_id": tenant_id, "group_id": group_id}) - cur.execute(query=query) - rows = cur.fetchall() - return helper.list_to_camel_case(rows) - -def get_member_permissions(tenant_id): - with pg_client.PostgresClient() as cur: - query = cur.mogrify("""SELECT - r.permissions - FROM public.roles r - WHERE r.tenant_id = %(tenant_id)s - AND r.name = 'Member' - AND r.deleted_at IS NULL - """, - {"tenant_id": tenant_id}) - cur.execute(query=query) - row = cur.fetchone() - return helper.dict_to_camel_case(row) - -def remove_group_membership(tenant_id, group_id, user_id): - with pg_client.PostgresClient() as cur: - query = cur.mogrify("""WITH r AS ( - SELECT role_id - FROM public.roles - WHERE data->>'group_id' = %(group_id)s - LIMIT 1 - ) - UPDATE public.users u - SET role_id= NULL - FROM r - WHERE u.data->>'user_id' = %(user_id)s - AND u.role_id = r.role_id - AND u.tenant_id = %(tenant_id)s - AND u.deleted_at IS NULL - RETURNING *;""", - {"tenant_id": tenant_id, "group_id": group_id, "user_id": user_id}) - cur.execute(query=query) - row = cur.fetchone() - - return helper.dict_to_camel_case(row) diff --git a/ee/api/routers/scim.py b/ee/api/routers/scim.py index 5908cc5ec..ec287b511 100644 --- a/ee/api/routers/scim.py +++ b/ee/api/routers/scim.py @@ -377,198 +377,3 @@ def delete_user(user_id: str, tenant_id = Depends(auth_required)): return _not_found_error_response(user_id) users.soft_delete_scim_user_by_id(user_id, tenant_id) return Response(status_code=204, content="") - - - -""" -Group endpoints -""" - -class Operation(BaseModel): - op: str - path: str = Field(default=None) - value: list[dict] | dict = Field(default=None) - -class GroupGetResponse(BaseModel): - schemas: list[str] = Field(default=["urn:ietf:params:scim:api:messages:2.0:ListResponse"]) - totalResults: int - startIndex: int - itemsPerPage: int - resources: list = Field(alias="Resources") - -class GroupRequest(BaseModel): - schemas: list[str] = Field(default=["urn:ietf:params:scim:schemas:core:2.0:Group"]) - displayName: str = Field(default=None) - members: list = Field(default=None) - operations: list[Operation] = Field(default=None, alias="Operations") - -class GroupPatchRequest(BaseModel): - schemas: list[str] = Field(default=["urn:ietf:params:scim:api:messages:2.0:PatchOp"]) - operations: list[Operation] = Field(alias="Operations") - -class GroupResponse(BaseModel): - schemas: list[str] = Field(default=["urn:ietf:params:scim:schemas:core:2.0:Group"]) - id: str - displayName: str - members: list - meta: dict = Field(default={"resourceType": "Group"}) - - -@public_app.get("/Groups", dependencies=[Depends(auth_required)]) -def get_groups( - start_index: int = Query(1, alias="startIndex"), - count: Optional[int] = Query(None, alias="count"), - group_name: Optional[str] = Query(None, alias="filter"), - ): - """Get groups""" - tenant_id = 1 - res = [] - if group_name: - group_name = group_name.split(" ")[2].strip('"') - - groups = roles.get_roles_with_uuid_paginated(tenant_id, start_index, count, group_name) - res = [{ - "id": group["data"]["groupId"], - "meta": { - "created": group["createdAt"], - "lastModified": "", # not currently a field - "version": "v1.0" - }, - "displayName": group["name"] - } for group in groups - ] - return JSONResponse( - status_code=200, - content=GroupGetResponse( - totalResults=len(groups), - startIndex=start_index, - itemsPerPage=len(groups), - Resources=res - ).model_dump(mode='json')) - -@public_app.get("/Groups/{group_id}", dependencies=[Depends(auth_required)]) -def get_group(group_id: str): - """Get a group by id""" - tenant_id = 1 - group = roles.get_role_by_group_id(tenant_id, group_id) - if not group: - raise HTTPException(status_code=404, detail="Group not found") - members = roles.get_users_by_group_uuid(tenant_id, group["data"]["groupId"]) - members = [{"value": member["data"]["userId"], "display": member["name"]} for member in members] - - return JSONResponse( - status_code=200, - content=GroupResponse( - id=group["data"]["groupId"], - displayName=group["name"], - members=members, - ).model_dump(mode='json')) - -@public_app.post("/Groups", dependencies=[Depends(auth_required)]) -def create_group(r: GroupRequest): - """Create a group""" - tenant_id = 1 - member_role = roles.get_member_permissions(tenant_id) - try: - data = schemas.RolePayloadSchema(name=r.displayName, permissions=member_role["permissions"]) # permissions by default are same as for member role - group = roles.create_as_admin(tenant_id, uuid.uuid4().hex, data) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - - added_members = [] - for member in r.members: - user = users.get_by_uuid(member["value"], tenant_id) - if user: - users.update(tenant_id, user["userId"], {"role_id": group["roleId"]}) - added_members.append({ - "value": user["data"]["userId"], - "display": user["name"] - }) - - return JSONResponse( - status_code=200, - content=GroupResponse( - id=group["data"]["groupId"], - displayName=group["name"], - members=added_members, - ).model_dump(mode='json')) - - -@public_app.put("/Groups/{group_id}", dependencies=[Depends(auth_required)]) -def update_put_group(group_id: str, r: GroupRequest): - """Update a group or members of the group (not used by anything yet)""" - tenant_id = 1 - group = roles.get_role_by_group_id(tenant_id, group_id) - if not group: - raise HTTPException(status_code=404, detail="Group not found") - - if r.operations and r.operations[0].op == "replace" and r.operations[0].path is None: - roles.update_group_name(tenant_id, group["data"]["groupId"], r.operations[0].value["displayName"]) - return Response(status_code=200, content="") - - members = r.members - modified_members = [] - for member in members: - user = users.get_by_uuid(member["value"], tenant_id) - if user: - users.update(tenant_id, user["userId"], {"role_id": group["roleId"]}) - modified_members.append({ - "value": user["data"]["userId"], - "display": user["name"] - }) - - return JSONResponse( - status_code=200, - content=GroupResponse( - id=group_id, - displayName=group["name"], - members=modified_members, - ).model_dump(mode='json')) - - -@public_app.patch("/Groups/{group_id}", dependencies=[Depends(auth_required)]) -def update_patch_group(group_id: str, r: GroupPatchRequest): - """Update a group or members of the group, used by AIW""" - tenant_id = 1 - group = roles.get_role_by_group_id(tenant_id, group_id) - if not group: - raise HTTPException(status_code=404, detail="Group not found") - if r.operations[0].op == "replace" and r.operations[0].path is None: - roles.update_group_name(tenant_id, group["data"]["groupId"], r.operations[0].value["displayName"]) - return Response(status_code=200, content="") - - modified_members = [] - for op in r.operations: - if op.op == "add" or op.op == "replace": - # Both methods work as "replace" - for u in op.value: - user = users.get_by_uuid(u["value"], tenant_id) - if user: - users.update(tenant_id, user["userId"], {"role_id": group["roleId"]}) - modified_members.append({ - "value": user["data"]["userId"], - "display": user["name"] - }) - elif op.op == "remove": - user_id = re.search(r'\[value eq \"([a-f0-9]+)\"\]', op.path).group(1) - roles.remove_group_membership(tenant_id, group_id, user_id) - return JSONResponse( - status_code=200, - content=GroupResponse( - id=group_id, - displayName=group["name"], - members=modified_members, - ).model_dump(mode='json')) - - -@public_app.delete("/Groups/{group_id}", dependencies=[Depends(auth_required)]) -def delete_group(group_id: str): - """Delete a group, hard-delete""" - # possibly need to set the user's roles to default member role, instead of null - tenant_id = 1 - group = roles.get_role_by_group_id(tenant_id, group_id) - if not group: - raise HTTPException(status_code=404, detail="Group not found") - roles.delete_scim_group(tenant_id, group["data"]["groupId"]) - - return Response(status_code=200, content="")