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

feat: #1469 Enable email notification for granting delegated admin #1529

Merged
merged 10 commits into from
Aug 14, 2024
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: 1 addition & 1 deletion client-code-gen/admin-management-openapi.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
*/


// May contain unused imports in some cases
// @ts-ignore
import { FamApplicationBase } from './fam-application-base';
// May contain unused imports in some cases
// @ts-ignore
import { FamForestClientBase } from './fam-forest-client-base';
Expand Down Expand Up @@ -50,5 +53,11 @@ export interface FamRoleWithClientDto {
* @memberof FamRoleWithClientDto
*/
'parent_role'?: FamRoleBase | null;
/**
*
* @type {FamApplicationBase}
* @memberof FamRoleWithClientDto
*/
'application': FamApplicationBase;
}

1 change: 1 addition & 0 deletions infrastructure/server/fam_admin_management_api.tf
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ resource "aws_lambda_function" "fam_admin_management_api_function" {

IDIM_PROXY_BASE_URL_PROD = "${var.idim_proxy_api_base_url_prod}"
IDIM_PROXY_API_KEY = "${var.idim_proxy_api_api_key}"
GC_NOTIFY_EMAIL_API_KEY = "${var.gc_notify_email_api_key}"
TARGET_ENV = "${var.target_env}"
}

Expand Down
65 changes: 65 additions & 0 deletions server/admin_management/api/app/integration/gc_notify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import logging

import requests
from api.app.schemas import GCNotifyGrantDelegatedAdminEmailParam
from api.config import config

LOGGER = logging.getLogger(__name__)

GC_NOTIFY_EMAIL_BASE_URL = "https://api.notification.canada.ca"
GC_NOTIFY_GRANT_DELEGATED_ADMIN_EMAIL_TEMPLATE_ID = "9abff613-e507-4562-aae0-008317dfe3b9"
# Template id for granting application admin, we will use this later
GC_NOTIFY_GRANT_APP_ADMIN_EMAIL_TEMPLATE_ID = "230bca59-4906-40b2-8f2b-2f6186a98663"
ianliuwk1019 marked this conversation as resolved.
Show resolved Hide resolved


class GCNotifyEmailService:
"""
The class is used for sending email
"""

TIMEOUT = (5, 10) # Timeout (connect, read) in seconds.

def __init__(self):
self.API_KEY = config.get_gc_notify_email_api_key()
self.email_base_url = GC_NOTIFY_EMAIL_BASE_URL
self.headers = {
"Content-Type": "application/json",
"Authorization": "ApiKey-v1 " + self.API_KEY,
}

self.session = requests.Session()
self.session.headers.update(self.headers)

def send_delegated_admin_granted_email(self, params: GCNotifyGrantDelegatedAdminEmailParam):
"""
Send email notification for new delegated admin
"""
# GC Notify does not have sufficient conditional rendering, cannot send None to variable, and does not support
# 'variable' within coditional text. Easier to do this in code.
contact_message = (
f"Please contact your administrator {params.application_team_contact_email} if you have any questions."
if params.application_team_contact_email is not None
else "Please contact your administrator if you have any questions."
)

email_params = {
"email_address": params.send_to_email_address,
"template_id": GC_NOTIFY_GRANT_DELEGATED_ADMIN_EMAIL_TEMPLATE_ID,
"personalisation": {
"first_name": params.first_name,
"last_name": params.last_name,
"application_name": params.application_name,
"role_list_string": params.role_list_string,
"contact_message": contact_message
},
}
gc_notify_email_send_url = f"{self.email_base_url}/v2/notifications/email"

r = self.session.post(
gc_notify_email_send_url, timeout=self.TIMEOUT, json=email_params
)
r.raise_for_status() # There is a general error handler, see: requests_http_error_handler
send_email_result = r.json()

LOGGER.debug(f"Send Email result: {send_email_result}")
return send_email_result
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,24 @@ def create_access_control_privilege_many(
)
audit_event_log.application = audit_event_log.role.application

response = access_control_privilege_service.create_access_control_privilege_many(
access_control_privilege_request, requester.cognito_user_id, target_user
response = (
access_control_privilege_service.create_access_control_privilege_many(
access_control_privilege_request, requester.cognito_user_id, target_user
)
)
# get target user from database, so for existing user, we can get the cognito user id
audit_event_log.target_user = user_service.get_user_by_domain_and_guid(
access_control_privilege_request.user_type_code,
access_control_privilege_request.user_guid,
)

return response
# Send email notification if required
if access_control_privilege_request.requires_send_user_email:
access_control_privilege_service.send_email_notification(
target_user, response
)

return response

except Exception as e:
audit_event_log.event_outcome = AuditEventOutcome.FAIL
Expand Down
16 changes: 15 additions & 1 deletion server/admin_management/api/app/schemas.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
from typing import List, Optional, Union

from pydantic import BaseModel, ConfigDict, Field, StringConstraints
from pydantic import BaseModel, ConfigDict, Field, StringConstraints, EmailStr
from typing_extensions import Annotated

from . import constants as famConstants
Expand Down Expand Up @@ -160,6 +160,7 @@ class FamRoleWithClientDto(BaseModel):
role_name: Annotated[str, StringConstraints(max_length=100)]
client_number: Optional[FamForestClientBase] = None
parent_role: Optional[FamRoleBase] = None
application: FamApplicationBase

model_config = ConfigDict(from_attributes=True)

Expand Down Expand Up @@ -316,6 +317,7 @@ class IdimProxyIdirInfo(BaseModel):
guid: Optional[Annotated[str, StringConstraints(max_length=32)]] = None
firstName: Optional[Annotated[str, StringConstraints(max_length=20)]] = None
lastName: Optional[Annotated[str, StringConstraints(max_length=20)]] = None
email: Optional[Annotated[str, StringConstraints(max_length=250)]] = None


class IdimProxyBceidInfo(BaseModel):
Expand All @@ -326,3 +328,15 @@ class IdimProxyBceidInfo(BaseModel):
businessLegalName: Optional[Annotated[str, StringConstraints(max_length=60)]] = None
firstName: Optional[Annotated[str, StringConstraints(max_length=20)]] = None
lastName: Optional[Annotated[str, StringConstraints(max_length=20)]] = None
email: Optional[Annotated[str, StringConstraints(max_length=250)]] = None


# ------------------------------------- GC Notify Integraion ---------------------------------------- #
class GCNotifyGrantDelegatedAdminEmailParam(BaseModel):
send_to_email_address: EmailStr
application_name: Annotated[str, StringConstraints(max_length=100)]
first_name: Annotated[str, StringConstraints(max_length=20)]
last_name: Annotated[str, StringConstraints(max_length=20)]
role_list_string: Annotated[str, StringConstraints(max_length=200)]
application_team_contact_email: Optional[EmailStr] = None

Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@

from api.app import constants as famConstants
from api.app import schemas
from api.app.integration.forest_client_integration import \
ForestClientIntegrationService
from api.app.repositories.access_control_privilege_repository import \
AccessControlPrivilegeRepository
from api.app.integration.forest_client_integration import ForestClientIntegrationService
from api.app.integration.gc_notify import GCNotifyEmailService
from api.app.repositories.access_control_privilege_repository import (
AccessControlPrivilegeRepository,
)
from api.app.services import utils_service
from api.app.services.role_service import RoleService
from api.app.services.user_service import UserService
from api.app.services.validator.forest_client_validator import (
forest_client_active, forest_client_number_exists,
get_forest_client_status)
forest_client_active,
forest_client_number_exists,
get_forest_client_status,
)
from api.app.utils import utils
from sqlalchemy.orm import Session

Expand Down Expand Up @@ -100,7 +103,9 @@ def create_access_control_privilege_many(
)

forest_client_integration_service = ForestClientIntegrationService(
utils_service.use_api_instance_by_app_env(fam_role.application.app_environment)
utils_service.use_api_instance_by_app_env(
fam_role.application.app_environment
)
)
for forest_client_number in request.forest_client_numbers:
# validate the forest client number
Expand Down Expand Up @@ -138,7 +143,6 @@ def create_access_control_privilege_many(
fam_user.user_id, child_role.role_id, requester
)
create_return_list.append(handle_create_return)

else:
handle_create_return = self.grant_privilege(
fam_user.user_id, fam_role.role_id, requester
Expand Down Expand Up @@ -209,3 +213,44 @@ def grant_privilege(
)

return access_control_privilege_return

def send_email_notification(
self,
target_user: schemas.TargetUser,
access_control_priviliege_response: List[
schemas.FamAccessControlPrivilegeCreateResponse
],
):
try:
ianliuwk1019 marked this conversation as resolved.
Show resolved Hide resolved
granted_roles = ", ".join(
item.detail.role.role_name
for item in filter(
lambda res: res.status_code == HTTPStatus.OK,
access_control_priviliege_response,
)
)
ianliuwk1019 marked this conversation as resolved.
Show resolved Hide resolved

if granted_roles == "": # no role is granted
return

gc_notify_email_service = GCNotifyEmailService()
email_response = gc_notify_email_service.send_delegated_admin_granted_email(
schemas.GCNotifyGrantDelegatedAdminEmailParam(
**{
"send_to_email_address": target_user.email,
"application_name": access_control_priviliege_response[
0
].detail.role.application.application_description,
"first_name": target_user.first_name,
"last_name": target_user.last_name,
"role_list_string": granted_roles,
}
)
)
LOGGER.debug(f"Email is sent to {target_user.email}: {email_response}")
return email_response
except Exception as e:
LOGGER.debug(
f"Failure sending email to the new delegated admin {target_user.email}."
)
LOGGER.debug(f"Failure reason : {e}.")
6 changes: 5 additions & 1 deletion server/admin_management/api/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,11 @@ def get_idim_proxy_api_baseurl(api_instance_env: ApiInstanceEnv):
return idim_proxy_api_baseurl



def get_idim_proxy_api_key():
idim_proxy_api_key = get_env_var("IDIM_PROXY_API_KEY")
return idim_proxy_api_key


def get_gc_notify_email_api_key():
gc_notify_email_api_key = get_env_var("GC_NOTIFY_EMAIL_API_KEY")
return gc_notify_email_api_key
3 changes: 2 additions & 1 deletion server/admin_management/local-dev.env
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ COGNITO_USER_POOL_ID=ca-central-1_p8X8GdjKW
COGNITO_CLIENT_ID=6jfveou69mgford233or30hmta
COGNITO_USER_POOL_DOMAIN=dev-fam-user-pool-domain
FC_API_TOKEN_TEST=thisisasecret
IDIM_PROXY_API_KEY=thisisasecret
IDIM_PROXY_API_KEY=thisisasecret
GC_NOTIFY_EMAIL_API_KEY=thisisasecret
1 change: 1 addition & 0 deletions server/admin_management/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ psycopg2-binary==2.9.9
httpx==0.27.0
pydantic==2.5.3
pydantic_core==2.14.6
email-validator==2.1.1
requests==2.31.0
cryptography==42.0.7
pyjwt==2.8.0
Loading