diff --git a/.env.template b/.env.template index 9d76e4e6..d5bfb21c 100644 --- a/.env.template +++ b/.env.template @@ -13,3 +13,10 @@ export DATABASE_MASTER_KEY= # export COSMOS_DATABASE_URI=AccountEndpoint=;AccountKey= ## Also specify the database name export DATABASE_NAME= + +## For Azure Users interaction +export MS_AUTHORITY= +export MS_CLIENT_ID= +export MS_SCOPE= +export MS_SECRET= +export MS_ENDPOINT= diff --git a/requirements/time_tracker_api/prod.txt b/requirements/time_tracker_api/prod.txt index 344bc4bb..ea5818a4 100644 --- a/requirements/time_tracker_api/prod.txt +++ b/requirements/time_tracker_api/prod.txt @@ -34,3 +34,6 @@ flask-cors==3.0.8 #JWT PyJWT==1.7.1 + +#Azure +msal==1.3.0 diff --git a/time_tracker_api/time_entries/time_entries_model.py b/time_tracker_api/time_entries/time_entries_model.py index 20cb6f5b..1d4c0eac 100644 --- a/time_tracker_api/time_entries/time_entries_model.py +++ b/time_tracker_api/time_entries/time_entries_model.py @@ -24,13 +24,14 @@ from utils.extend_model import ( add_project_name_to_time_entries, add_activity_name_to_time_entries, -) -from utils import worked_time -from utils.worked_time import str_to_datetime -from utils.extend_model import ( create_in_condition, create_custom_query_from_str, + add_user_email_to_time_entries, ) +from utils import worked_time +from utils.worked_time import str_to_datetime + +from utils.azure_users import AzureConnection from time_tracker_api.projects.projects_model import ProjectCosmosDBModel from time_tracker_api.projects import projects_model from time_tracker_api.database import CRUDDao, APICosmosDBDao @@ -177,6 +178,9 @@ def find_all( activity_dao = activities_model.create_dao() activities = activity_dao.get_all() add_activity_name_to_time_entries(time_entries, activities) + + users = AzureConnection().users() + add_user_email_to_time_entries(time_entries, users) return time_entries def on_create(self, new_item_data: dict, event_context: EventContext): diff --git a/time_tracker_api/time_entries/time_entries_namespace.py b/time_tracker_api/time_entries/time_entries_namespace.py index aae7e6ff..7227fea3 100644 --- a/time_tracker_api/time_entries/time_entries_namespace.py +++ b/time_tracker_api/time_entries/time_entries_namespace.py @@ -129,6 +129,13 @@ description='Name of the activity associated with the time-entry', example=faker.word(['development', 'QA']), ), + 'owner_email': fields.String( + required=True, + title="Owner's Email", + max_length=50, + description='Email of the user that owns the time-entry', + example=faker.email(), + ), } time_entry_response_fields.update(common_fields) diff --git a/utils/azure_users.py b/utils/azure_users.py new file mode 100644 index 00000000..b31f0f0e --- /dev/null +++ b/utils/azure_users.py @@ -0,0 +1,79 @@ +import msal +import os +import requests +from typing import List + + +class MSConfig: + def check_variables_are_defined(): + auth_variables = [ + 'MS_CLIENT_ID', + 'MS_AUTHORITY', + 'MS_SECRET', + 'MS_SCOPE', + 'MS_ENDPOINT', + ] + for var in auth_variables: + if var not in os.environ: + raise EnvironmentError( + "{} is not defined in the environment".format(var) + ) + + check_variables_are_defined() + CLIENT_ID = os.environ.get('MS_CLIENT_ID') + AUTHORITY = os.environ.get('MS_AUTHORITY') + SECRET = os.environ.get('MS_SECRET') + SCOPE = os.environ.get('MS_SCOPE') + ENDPOINT = os.environ.get('MS_ENDPOINT') + + +class BearerAuth(requests.auth.AuthBase): + def __init__(self, access_token): + self.access_token = access_token + + def __call__(self, r): + r.headers["Authorization"] = f'Bearer {self.access_token}' + return r + + +class AzureUser: + def __init__(self, id, name, email): + self.id = id + self.name = name + self.email = email + + +class AzureConnection: + def __init__(self, config=MSConfig): + self.client = msal.ConfidentialClientApplication( + config.CLIENT_ID, + authority=config.AUTHORITY, + client_credential=config.SECRET, + ) + self.config = config + self.access_token = self.get_token() + + def get_token(self): + response = self.client.acquire_token_for_client( + scopes=self.config.SCOPE + ) + if "access_token" in response: + return response['access_token'] + else: + error_info = f"{response['error']} {response['error_description']}" + raise ValueError(error_info) + + def users(self) -> List[AzureUser]: + def to_azure_user(item) -> AzureUser: + there_is_email = len(item['otherMails']) > 0 + id = item['objectId'] + name = item['displayName'] + email = item['otherMails'][0] if there_is_email else '' + return AzureUser(id, name, email) + + endpoint = f"{self.config.ENDPOINT}/users?api-version=1.6&$select=displayName,otherMails,objectId" + response = requests.get(endpoint, auth=BearerAuth(self.access_token)) + + assert 200 == response.status_code + assert 'value' in response.json() + return [to_azure_user(item) for item in response.json()['value']] diff --git a/utils/extend_model.py b/utils/extend_model.py index 356f133c..5c72e26f 100644 --- a/utils/extend_model.py +++ b/utils/extend_model.py @@ -38,6 +38,13 @@ def add_activity_name_to_time_entries(time_entries, activities): setattr(time_entry, 'activity_name', activity.name) +def add_user_email_to_time_entries(time_entries, users): + for time_entry in time_entries: + for user in users: + if time_entry.owner_id == user.id: + setattr(time_entry, 'owner_email', user.email) + + def create_in_condition( data_object: list, attr_to_filter: str = "", first_attr: str = "c.id" ):