diff --git a/app/api_clients/constants.py b/app/api_clients/constants.py new file mode 100644 index 0000000..35ee72f --- /dev/null +++ b/app/api_clients/constants.py @@ -0,0 +1,2 @@ +OPT_RESOURCE_TYPE_ORGANIZATION = 2 +OPT_ROLE_ORGANIZATION_ADMIN = 3 diff --git a/app/api_clients/optscale.py b/app/api_clients/optscale.py index 1246b5c..e2eb87b 100644 --- a/app/api_clients/optscale.py +++ b/app/api_clients/optscale.py @@ -55,3 +55,10 @@ async def fetch_users_for_organization(self, organization_id: UUID | str) -> htt ) response.raise_for_status() return response + + async def fetch_user_by_id(self, user_id: UUID | str) -> httpx.Response: + response = await self.httpx_client.get( + f"/employees/{user_id}?roles=true", + ) + response.raise_for_status() + return response diff --git a/app/api_clients/optscale_auth.py b/app/api_clients/optscale_auth.py index 3d78b68..8a73b78 100644 --- a/app/api_clients/optscale_auth.py +++ b/app/api_clients/optscale_auth.py @@ -2,6 +2,7 @@ from app import settings from app.api_clients.base import APIClientError, BaseAPIClient, OptscaleClusterSecretAuth +from app.api_clients.constants import OPT_RESOURCE_TYPE_ORGANIZATION, OPT_ROLE_ORGANIZATION_ADMIN class OptscaleAuthClientError(APIClientError): @@ -33,3 +34,15 @@ async def get_existing_user_info(self, email: str) -> httpx.Response: raise UserDoesNotExist(email) return response + + async def make_user_admin(self, organization_id: str, user_id: str) -> httpx.Response: + response = await self.httpx_client.post( + f"/users/{user_id}/assignment_register", + json={ + "role_id": OPT_ROLE_ORGANIZATION_ADMIN, + "type_id": OPT_RESOURCE_TYPE_ORGANIZATION, + "resource_id": organization_id, + }, + ) + response.raise_for_status() + return response diff --git a/app/routers/organizations.py b/app/routers/organizations.py index 4db469a..d2ae7e5 100644 --- a/app/routers/organizations.py +++ b/app/routers/organizations.py @@ -5,8 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi_pagination.limit_offset import LimitOffsetPage -from app.api_clients import APIModifierClient -from app.api_clients.optscale import OptscaleClient +from app.api_clients import APIModifierClient, OptscaleAuthClient, OptscaleClient from app.auth import CurrentSystem from app.db.handlers import NotFoundError from app.db.models import Organization @@ -196,3 +195,26 @@ async def get_users_by_organization_id( ) for user in users ] + + +@router.post( + "/{organization_id}/users/{user_id}/make-admin", + status_code=status.HTTP_204_NO_CONTENT, +) +async def make_organization_user_admin( + organization: Annotated[Organization, Depends(fetch_organization_or_404)], + user_id: UUID, + services: svcs.fastapi.DepContainer, +): + optscale_auth_client = await services.aget(OptscaleAuthClient) + optscale_client = await services.aget(OptscaleClient) + + async with wrap_http_error_in_502("Error making user admin in FinOps for Cloud"): + # check user exists in optscale + response = await optscale_client.fetch_user_by_id(str(user_id)) + user = response.json() + # assign admin role of current organization to the user + await optscale_auth_client.make_user_admin( + str(organization.organization_id), + user["auth_user_id"], + ) diff --git a/tests/test_users_api.py b/tests/test_users_api.py index d959a07..3f34daa 100644 --- a/tests/test_users_api.py +++ b/tests/test_users_api.py @@ -339,3 +339,130 @@ async def test_get_users_for_organization_with_optscale_error( assert response.status_code == 502 assert f"Error fetching users for organization {org.name}" in response.json()["detail"] + + +# =============== +# Make user admin +# =============== + + +async def test_make_user_admin( + organization_factory: ModelFactory[Organization], + authenticated_client: AsyncClient, + httpx_mock: HTTPXMock, +): + user_id = str(uuid.uuid4()) + org = await organization_factory( + organization_id=str(uuid.uuid4()), + ) + auth_user_id = str(uuid.uuid4()) + + httpx_mock.add_response( + method="GET", + url=f"{settings.opt_api_base_url}/employees/{user_id}?roles=true", + match_headers={"Secret": settings.opt_cluster_secret}, + status_code=200, + json={ + "deleted_at": 0, + "id": "2c2e9705-8023-437c-b09d-8cbd49f0a682", + "created_at": 1729156673, + "name": "Ciccio", + "organization_id": org.organization_id, + "auth_user_id": auth_user_id, + "default_ssh_key_id": None, + }, + ) + + httpx_mock.add_response( + method="POST", + url=f"{settings.opt_auth_base_url}/users/{auth_user_id}/assignment_register", + match_headers={"Secret": settings.opt_cluster_secret}, + status_code=200, + json={ + "created_at": 1736352461, + "deleted_at": 0, + "id": "7884c0c0-be5c-4cc7-8413-dd8033b1e4a2", + "type_id": 2, + "role_id": 3, + "user_id": auth_user_id, + "resource_id": org.organization_id, + }, + match_json={ + "role_id": 3, # Admin + "type_id": 2, # Organization + "resource_id": org.organization_id, + }, + ) + response = await authenticated_client.post( + f"/organizations/{org.id}/users/{user_id}/make-admin", + ) + assert response.status_code == 204 + + +async def test_make_user_admin_not_found( + organization_factory: ModelFactory[Organization], + authenticated_client: AsyncClient, + httpx_mock: HTTPXMock, +): + user_id = str(uuid.uuid4()) + org = await organization_factory( + organization_id=str(uuid.uuid4()), + ) + + httpx_mock.add_response( + method="GET", + url=f"{settings.opt_api_base_url}/employees/{user_id}?roles=true", + match_headers={"Secret": settings.opt_cluster_secret}, + status_code=404, + ) + + response = await authenticated_client.post( + f"/organizations/{org.id}/users/{user_id}/make-admin", + ) + assert response.status_code == 502 + assert "Error making user admin in FinOps for Cloud: 404" in response.json()["detail"] + + +async def test_make_user_admin_error_assigning_role( + organization_factory: ModelFactory[Organization], + authenticated_client: AsyncClient, + httpx_mock: HTTPXMock, +): + user_id = str(uuid.uuid4()) + org = await organization_factory( + organization_id=str(uuid.uuid4()), + ) + auth_user_id = str(uuid.uuid4()) + + httpx_mock.add_response( + method="GET", + url=f"{settings.opt_api_base_url}/employees/{user_id}?roles=true", + match_headers={"Secret": settings.opt_cluster_secret}, + status_code=200, + json={ + "deleted_at": 0, + "id": "2c2e9705-8023-437c-b09d-8cbd49f0a682", + "created_at": 1729156673, + "name": "Ciccio", + "organization_id": org.organization_id, + "auth_user_id": auth_user_id, + "default_ssh_key_id": None, + }, + ) + + httpx_mock.add_response( + method="POST", + url=f"{settings.opt_auth_base_url}/users/{auth_user_id}/assignment_register", + match_headers={"Secret": settings.opt_cluster_secret}, + status_code=400, + match_json={ + "role_id": 3, # Admin + "type_id": 2, # Organization + "resource_id": org.organization_id, + }, + ) + response = await authenticated_client.post( + f"/organizations/{org.id}/users/{user_id}/make-admin", + ) + assert response.status_code == 502 + assert "Error making user admin in FinOps for Cloud: 400" in response.json()["detail"]