Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MPT-4912 promote user to organization manager #35

Merged
merged 1 commit into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/api_clients/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
OPT_RESOURCE_TYPE_ORGANIZATION = 2
OPT_ROLE_ORGANIZATION_ADMIN = 3
7 changes: 7 additions & 0 deletions app/api_clients/optscale.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions app/api_clients/optscale_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
26 changes: 24 additions & 2 deletions app/routers/organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Comment on lines +209 to +210
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny improvement but you can also make this shorter:

optscale_auth_client, optscale_client = await services.aget(OptscaleAuthClient, OptscaleClient)

Matter of personal taste


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"],
)
127 changes: 127 additions & 0 deletions tests/test_users_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Loading