Skip to content

Commit 26c6e5b

Browse files
Mpt 6178 mtp 6179 replace error format add env prefix (#28)
* [WIP] Add initial logic for managing DataSources. Move the service's APIs and files to the API folder. Add Custom Exceptions to handle DataSources errors * Reformat the Auth error message format to be the same as the others. Remove the endpoint `/modifier/v1/invitations/users` and move the users invitation logic into the `/modifier/v1/users` Replace POST `/modifier/v1/invites/{invite_id}/decline` with PATCH `/modifier/v1/invites/{invite_id}` Add the Prefix `FFC_MODIFIER_` to ENV variables Updated tests * Add env.example * Move logic for creating new users and invited ones to a service layer to improve readability of the whole flow. Reverse the behaviour of the JTWBearer class to soon raise an exception in case or authentication's errors. Improve and Adjust tests
1 parent d8ab3e8 commit 26c6e5b

35 files changed

+443
-383
lines changed

app/api/cloud_account/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
from app.api.cloud_account.model import AddCloudAccount, AddCloudAccountResponse
1212
from app.api.invitations.api import get_bearer_token
1313
from app.core.exceptions import (
14+
APIResponseError,
1415
CloudAccountConfigError,
15-
OptScaleAPIResponseError,
1616
format_error_response,
1717
)
1818

@@ -52,6 +52,6 @@ async def add_cloud_account(
5252
status_code=response.get("status_code", http_status.HTTP_201_CREATED),
5353
content=response.get("data", {}),
5454
)
55-
except (OptScaleAPIResponseError, CloudAccountConfigError, ValueError) as error:
55+
except (APIResponseError, CloudAccountConfigError, ValueError) as error:
5656
logger.error(f"An error occurred adding the cloud account {data.type} {error}")
5757
return format_error_response(error)

app/api/cloud_account/cloud_accounts_conf/cloud_config_strategy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async def link_cloud_account_to_org(
3333
:param org_id: The user's ORG to link the Cloud Account with
3434
:param user_access_token: The user's access token
3535
:return: The response as it comes from OptScale
36-
:raise: Rethrow OptScaleAPIResponseError if an error occurred consuming the
36+
:raise: Rethrow APIResponseError if an error occurred consuming the
3737
OptScale APIs.
3838
"""
3939
response = await self.optscale_cloud_account_api.link_cloud_account_with_org(

app/api/cloud_account/cloud_accounts_manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
)
1515
from app.api.cloud_account.cloud_accounts_conf.gcp import GCPCNRConfigStrategy
1616
from app.core.exceptions import (
17+
APIResponseError,
1718
CloudAccountConfigError,
18-
OptScaleAPIResponseError,
1919
)
2020
from app.optscale_api.cloud_accounts import OptScaleCloudAccountAPI
2121

@@ -53,7 +53,7 @@ def select_strategy(self):
5353
:rtype:
5454
"""
5555
if self.type not in self.ALLOWED_PROVIDERS:
56-
raise OptScaleAPIResponseError(
56+
raise APIResponseError(
5757
title="Wrong Cloud Account",
5858
error_code="OE0436",
5959
reason=f"{self.type} is not supported",
@@ -144,7 +144,7 @@ async def add_cloud_account(
144144
"parent_id": null
145145
}
146146
raises: ValueError if the previously built CloudStrategyConfiguration is tampered.
147-
Rethrow OptScaleAPIResponseError if an error occurred during the communication with the
147+
Rethrow APIResponseError if an error occurred during the communication with the
148148
OptScale API.
149149
"""
150150
if not isinstance(config, CloudStrategyConfiguration):

app/api/invitations/api.py

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,12 @@
66
from starlette.responses import JSONResponse
77

88
from app import settings
9-
from app.api.invitations.model import (
10-
DeclineInvitation,
11-
RegisteredInvitedUserResponse,
12-
RegisterInvitedUser,
13-
)
9+
from app.api.invitations.model import DeclineInvitation
1410
from app.api.invitations.services.invitations import (
15-
register_invited_user_on_optscale,
1611
remove_user,
1712
)
1813
from app.core.exceptions import (
19-
InvitationDoesNotExist,
20-
OptScaleAPIResponseError,
14+
APIResponseError,
2115
format_error_response,
2216
)
2317
from app.optscale_api.invitation_api import OptScaleInvitationAPI
@@ -36,37 +30,8 @@ def get_bearer_token(
3630
return auth.credentials # Return the raw token
3731

3832

39-
def get_optscale_user_api() -> OptScaleUserAPI:
40-
return OptScaleUserAPI()
41-
42-
43-
@router.post(
44-
path="/users",
45-
status_code=http_status.HTTP_201_CREATED,
46-
response_model=RegisteredInvitedUserResponse,
47-
)
48-
async def register_invited(
49-
data: RegisterInvitedUser,
50-
user_api: OptScaleUserAPI = Depends(get_optscale_user_api),
51-
): # noqa: E501
52-
try:
53-
response = await register_invited_user_on_optscale(
54-
email=str(data.email),
55-
display_name=data.display_name,
56-
password=data.password,
57-
user_api=user_api,
58-
)
59-
return JSONResponse(
60-
status_code=response.get("status_code", http_status.HTTP_201_CREATED),
61-
content=response.get("data", {}),
62-
)
63-
64-
except (OptScaleAPIResponseError, InvitationDoesNotExist) as error:
65-
return format_error_response(error)
66-
67-
68-
@router.post(
69-
path="/users/invites/{invite_id}/decline",
33+
@router.patch(
34+
path="/users/invites/{invite_id}",
7035
status_code=http_status.HTTP_200_OK,
7136
)
7237
async def decline_invitation(
@@ -97,5 +62,5 @@ async def decline_invitation(
9762
status_code=response.get("status_code", http_status.HTTP_200_OK),
9863
content={"response": "Invitation declined"},
9964
)
100-
except OptScaleAPIResponseError as error:
65+
except APIResponseError as error:
10166
return format_error_response(error)

app/api/invitations/services/invitations.py

Lines changed: 3 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,18 @@
11
from __future__ import annotations
22

33
import asyncio
4-
import functools
54
import logging
65

76
from fastapi import Depends
87

9-
from app import settings
10-
from app.core.exceptions import InvitationDoesNotExist, OptScaleAPIResponseError
8+
from app.core.exceptions import APIResponseError
119
from app.optscale_api.invitation_api import OptScaleInvitationAPI
1210
from app.optscale_api.orgs_api import OptScaleOrgAPI
1311
from app.optscale_api.users_api import OptScaleUserAPI
1412

1513
logger = logging.getLogger(__name__)
1614

1715

18-
def validate_invitation(): # noqa: E501
19-
def decorator(func):
20-
@functools.wraps(func)
21-
async def wrapper(*args, **kwargs):
22-
email = kwargs.get("email")
23-
invitation_api = OptScaleInvitationAPI()
24-
response = await invitation_api.get_list_of_invitations(email=email)
25-
no_invitations = {"invites": []}
26-
if response.get("data", {}) == no_invitations:
27-
# there is no invitation
28-
raise InvitationDoesNotExist("Invitation not found")
29-
return await func(*args, **kwargs)
30-
31-
return wrapper
32-
33-
return decorator
34-
35-
36-
@validate_invitation()
37-
async def register_invited_user_on_optscale(
38-
email: str,
39-
display_name: str,
40-
password: str,
41-
user_api: OptScaleUserAPI,
42-
) -> dict[str, str] | Exception:
43-
"""
44-
Registers invited users to OptScale, without
45-
verification.
46-
47-
:param user_api: an instance of OptScaleUserAPI
48-
:param email: The email of the given user to register
49-
:param display_name: The display name of the user
50-
:param password: The password of the user
51-
:return: dict[str, str] : User information.
52-
:raises: OptScaleAPIResponseError if any error occurs
53-
contacting the OptScale APIs
54-
"""
55-
try:
56-
response = await user_api.create_user(
57-
email=email,
58-
display_name=display_name,
59-
password=password,
60-
admin_api_key=settings.admin_token,
61-
verified=False,
62-
)
63-
logger.info(f"Invited User successfully registered: {response}")
64-
return response
65-
except OptScaleAPIResponseError as error:
66-
logger.error(f"An error {error} occurred registering the invited user {email}")
67-
raise error
68-
except InvitationDoesNotExist as error:
69-
logger.error(f"There is no invitation for this email {email}")
70-
raise error
71-
72-
7316
async def validate_user_delete(
7417
user_token: str,
7518
invitation_api: OptScaleInvitationAPI = Depends(),
@@ -117,7 +60,7 @@ async def remove_user(
11760
:param org_api: an instance of the OptScaleOrgAPI
11861
:param user_api: An instance of OptScaleUserAPI
11962
:return: True if the user was successfully deleted, False otherwise.
120-
:raises OptScaleAPIResponseError if any error occurs
63+
:raises APIResponseError if any error occurs
12164
contacting the OptScale APIs
12265
"""
12366
validate_delete = await validate_user_delete(
@@ -131,7 +74,7 @@ async def remove_user(
13174
)
13275
logger.info(f"The user {user_id} was successfully deleted")
13376
return True
134-
except OptScaleAPIResponseError:
77+
except APIResponseError:
13578
logger.error(f"Error deleting user:{user_id}")
13679
return False
13780
logger.info(f"The user {user_id} cannot be deleted.")

app/api/organizations/api.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from fastapi import APIRouter, Depends
24
from fastapi import status as http_status
35
from starlette.responses import JSONResponse
@@ -23,12 +25,13 @@
2325
path="",
2426
status_code=http_status.HTTP_200_OK,
2527
response_model=OptScaleOrganizationResponse,
26-
dependencies=[Depends(JWTBearer())],
28+
dependencies=[],
2729
)
2830
async def get_orgs(
2931
user_id: str,
3032
optscale_api: OptScaleOrgAPI = Depends(),
3133
auth_client: OptScaleAuth = Depends(get_auth_client),
34+
jwt_payload: dict = Depends(JWTBearer()),
3235
):
3336
"""
3437
Retrieve the organization data associated with a given user.
@@ -37,6 +40,7 @@ async def get_orgs(
3740
with the OptScale API.
3841
It returns the organization data as a JSON response.
3942
43+
:param jwt_payload: A dictionary that will contain the access token or an error
4044
:param user_id: The ID of the user whose organization data is to be retrieved.
4145
:param optscale_api: An instance of OptScaleOrgAPI for interacting with the organization API.
4246
Dependency injection via `Depends()`.
@@ -49,7 +53,7 @@ async def get_orgs(
4953
:raises:
5054
the optscale_api.get_user_org() may raise these exceptions
5155
52-
- OptScaleAPIResponseError: If an error occurs when communicating with the OptScale API.
56+
- APIResponseError: If an error occurs when communicating with the OptScale API.
5357
- UserAccessTokenError: If there is an issue obtaining the user's access token.
5458
- Exception: For any other unexpected errors.
5559
@@ -89,16 +93,18 @@ async def get_orgs(
8993
path="",
9094
status_code=http_status.HTTP_201_CREATED,
9195
response_model=OptScaleOrganization,
92-
dependencies=[Depends(JWTBearer())],
96+
dependencies=[],
9397
)
9498
async def create_orgs(
9599
data: CreateOrgData,
96100
org_api: OptScaleOrgAPI = Depends(),
97101
auth_client: OptScaleAuth = Depends(get_auth_client),
102+
jwt_payload: dict = Depends(JWTBearer()),
98103
):
99104
"""
100105
Create a new FinOPs organization.
101106
107+
:param jwt_payload: A dictionary that will contain the access token or an error
102108
:param data: The input data required to create an organization,including the user_id
103109
:param org_api: An instance of OptScaleOrgAPI for managing organization operations.
104110
Dependency injection via `Depends()`.
@@ -139,6 +145,7 @@ async def create_orgs(
139145
"""
140146

141147
try:
148+
# handle_jwt_dependency(jwt_payload)
142149
response = await org_api.create_user_org(
143150
org_name=data.org_name,
144151
user_id=data.user_id,

app/api/users/api.py

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,55 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
15
from fastapi import APIRouter, Depends
26
from fastapi import status as http_status
37
from starlette.responses import JSONResponse
48

59
from app import settings
610
from app.api.users.model import CreateUserData, CreateUserResponse
11+
from app.api.users.services.optscale_users_registration import (
12+
add_new_user,
13+
validate_email_and_add_invited_user,
14+
)
715
from app.core.auth_jwt_bearer import JWTBearer
8-
from app.core.exceptions import OptScaleAPIResponseError, format_error_response
16+
from app.core.exceptions import (
17+
APIResponseError,
18+
InvitationDoesNotExist,
19+
UserAccessTokenError,
20+
format_error_response,
21+
)
922
from app.optscale_api.users_api import OptScaleUserAPI
1023

24+
logger = logging.getLogger(__name__)
1125
router = APIRouter()
1226

1327

1428
@router.post(
1529
path="",
1630
status_code=http_status.HTTP_201_CREATED,
1731
response_model=CreateUserResponse,
18-
dependencies=[Depends(JWTBearer())],
32+
dependencies=[],
1933
)
20-
async def create_user(data: CreateUserData, user_api: OptScaleUserAPI = Depends()):
34+
async def create_user(
35+
data: CreateUserData,
36+
optscale_user_api: OptScaleUserAPI = Depends(),
37+
jwt_token: dict = Depends(JWTBearer(allow_unauthenticated=True)),
38+
):
2139
"""
22-
Create a FinOps user
23-
This endpoint allows the creation of a new user by interacting with the OptScale API.
40+
This endpoint registers users in OptScale.
41+
It can be consumed in two ways:
42+
1. If a JWT Token is provided, the user will be created and verified.
43+
2. IF no Authentication is provided, the given user will be created ONLY
44+
if an invitation exists with the provided email address. If an invitation exists,
45+
the user will be created but not auto-verified. The user has to perform
46+
the verification action.
47+
2448
It returns the created user's details.
2549
50+
:param jwt_token: a JWT token or None
2651
:param data: The input data required to create a user.
27-
:param user_api: An instance of OptScaleOrgAPI for managing organization operations.
52+
:param optscale_user_api: An instance of OptScaleOrgAPI for managing organization operations.
2853
Dependency injection via `Depends()`.
2954
3055
:return: A response model containing the details of the newly created user.
@@ -62,17 +87,32 @@ async def create_user(data: CreateUserData, user_api: OptScaleUserAPI = Depends(
6287
JWTBearer: Ensures that the request is authenticated using a valid JWT.
6388
"""
6489
try:
65-
response = await user_api.create_user(
66-
email=str(data.email),
67-
display_name=data.display_name,
68-
password=data.password,
69-
admin_api_key=settings.admin_token,
70-
verified=True,
71-
)
90+
if jwt_token is None:
91+
response = await validate_email_and_add_invited_user(
92+
email=str(data.email),
93+
display_name=data.display_name,
94+
password=data.password,
95+
admin_token=settings.admin_token,
96+
optscale_user_api=optscale_user_api,
97+
)
98+
logger.info(f"Invited User successfully registered: {response}")
99+
else:
100+
response = await add_new_user(
101+
email=str(data.email),
102+
display_name=data.display_name,
103+
password=data.password,
104+
admin_token=settings.admin_token,
105+
optscale_user_api=optscale_user_api,
106+
)
107+
logger.info(f"User successfully created: {response}")
72108
return JSONResponse(
73109
status_code=response.get("status_code", http_status.HTTP_201_CREATED),
74110
content=response.get("data", {}),
75111
)
76112

77-
except OptScaleAPIResponseError as error:
113+
except (
114+
APIResponseError,
115+
UserAccessTokenError,
116+
InvitationDoesNotExist,
117+
) as error:
78118
return format_error_response(error)

app/api/users/services/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)