From a4575a3e750720ac2dd310d81e072a27cf50098f Mon Sep 17 00:00:00 2001 From: Guillaume Charest <1690085+gcharest@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:39:13 +0000 Subject: [PATCH 1/3] feat: improve logging of groups found --- app/integrations/aws/identity_store.py | 5 +++++ app/integrations/google_workspace/google_directory.py | 9 +++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/integrations/aws/identity_store.py b/app/integrations/aws/identity_store.py index 47e6303e..f6d288f6 100644 --- a/app/integrations/aws/identity_store.py +++ b/app/integrations/aws/identity_store.py @@ -296,6 +296,10 @@ def list_groups_with_memberships( """Retrieves groups with their members from the AWS Identity Center (identitystore) Args: + group_members (bool): Include group members in the response. Default is True. + members_details (bool): Include the details of the members. Default is True. + include_empty_groups (bool): Include groups without members. Default is True. + groups_filters (list): A list of filters to apply to the groups. Default is None. **kwargs: Additional keyword arguments for the API call. (passed to list_groups) Returns: @@ -309,6 +313,7 @@ def list_groups_with_memberships( if groups_filters is not None: for groups_filter in groups_filters: groups = filters.filter_by_condition(groups, groups_filter) + logger.info(f"Founds {len(groups)} groups in AWS Identity Store.") if not group_members: return groups diff --git a/app/integrations/google_workspace/google_directory.py b/app/integrations/google_workspace/google_directory.py index 11e2010a..dcfd3359 100644 --- a/app/integrations/google_workspace/google_directory.py +++ b/app/integrations/google_workspace/google_directory.py @@ -1,5 +1,6 @@ """Google Directory module to interact with the Google Workspace Directory API.""" +from logging import getLogger from integrations.google_workspace.google_service import ( handle_google_api_errors, execute_google_api_call, @@ -9,6 +10,8 @@ from integrations.utils.api import convert_string_to_camel_case from utils import filters +logger = getLogger(__name__) + @handle_google_api_errors def get_user(user_key, delegated_user_email=None): @@ -181,8 +184,10 @@ def list_groups_with_members( if not groups: return [] - for filter in groups_filters: - groups = filters.filter_by_condition(groups, filter) + if groups_filters is not None: + for groups_filter in groups_filters: + groups = filters.filter_by_condition(groups, groups_filter) + logger.info(f"Found {len(groups)} groups.") if not group_members: return groups From 9b2b802543f5443980a2993b3a8ab7c4f2fe1d5e Mon Sep 17 00:00:00 2001 From: Guillaume Charest <1690085+gcharest@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:40:26 +0000 Subject: [PATCH 2/3] feat: introduce ability to add args to groups sync slash command --- app/modules/aws/groups.py | 24 +++++++++++- app/modules/aws/identity_center.py | 10 ++++- app/tests/modules/aws/test_aws_groups.py | 37 ++++++++++++++++++- .../modules/aws/test_sync_identity_center.py | 30 +++++++++++---- 4 files changed, 89 insertions(+), 12 deletions(-) diff --git a/app/modules/aws/groups.py b/app/modules/aws/groups.py index ec41fb8d..48134fe3 100644 --- a/app/modules/aws/groups.py +++ b/app/modules/aws/groups.py @@ -37,15 +37,37 @@ def command_handler(client, body, respond, args, logger): def request_groups_sync(client, body, respond, args, logger): - """Sync groups from AWS Identity Center.""" + """Sync groups from AWS Identity Center. + + If additional arguments are provided, they will be used to filter the groups to sync. + + Args: + client (Slack WebClient): The Slack client. + body (dict): The request body. + respond (function): The function to respond to the request. + args (list[str]): The list of arguments. + logger (Logger): The logger. + """ requestor_email = slack_users.get_user_email_from_body(client, body) if permissions.is_user_member_of_groups(requestor_email, AWS_ADMIN_GROUPS): + pre_processing_filters = ( + [ + lambda group, arg=arg: arg.lower() + in group.get("DisplayName", "").lower() + or arg.lower() in group.get("name", "").lower() + for arg in args + ] + if args + else [] + ) logger.info("Synchronizing AWS Identity Center Groups.") respond("AWS Groups Memberships Synchronization Initiated.") identity_center.synchronize( + enable_users_sync=False, enable_user_create=False, enable_membership_create=True, enable_membership_delete=True, + pre_processing_filters=pre_processing_filters, ) else: logger.error(f"User {requestor_email} does not have permission to sync groups.") diff --git a/app/modules/aws/identity_center.py b/app/modules/aws/identity_center.py index 44740d8f..382795ef 100644 --- a/app/modules/aws/identity_center.py +++ b/app/modules/aws/identity_center.py @@ -28,20 +28,26 @@ def synchronize(**kwargs): enable_membership_create = kwargs.pop("enable_membership_create", True) enable_membership_delete = kwargs.pop("enable_membership_delete", False) query = kwargs.pop("query", "email:aws-*") + pre_processing_filters = kwargs.pop("pre_processing_filters", []) users_sync_status = None groups_sync_status = None source_groups_filters = [lambda group: "AWS-" in group["name"]] source_groups = groups.get_groups_from_integration( - "google_groups", query=query, post_processing_filters=source_groups_filters + "google_groups", + query=query, + pre_processing_filters=pre_processing_filters, + post_processing_filters=source_groups_filters, ) source_users = filters.get_unique_nested_dicts(source_groups, "members") logger.info( f"synchronize:Found {len(source_groups)} Groups and {len(source_users)} Users from Source" ) - target_groups = groups.get_groups_from_integration("aws_identity_center") + target_groups = groups.get_groups_from_integration( + "aws_identity_center", pre_processing_filters=pre_processing_filters + ) target_users = identity_store.list_users() logger.info( diff --git a/app/tests/modules/aws/test_aws_groups.py b/app/tests/modules/aws/test_aws_groups.py index 56a02595..d3ce36cc 100644 --- a/app/tests/modules/aws/test_aws_groups.py +++ b/app/tests/modules/aws/test_aws_groups.py @@ -1,4 +1,4 @@ -from unittest.mock import patch, MagicMock, call +from unittest.mock import patch, MagicMock, call, ANY from modules.aws import groups @@ -86,9 +86,44 @@ def test_request_groups_sync_synchronizes_groups( ) logger.info.assert_called_once_with("Synchronizing AWS Identity Center Groups.") mock_identity_center.synchronize.assert_called_once_with( + enable_users_sync=False, enable_user_create=False, enable_membership_create=True, enable_membership_delete=True, + pre_processing_filters=[], + ) + respond.assert_called_once_with("AWS Groups Memberships Synchronization Initiated.") + + +@patch("modules.aws.groups.slack_users") +@patch("modules.aws.groups.permissions") +@patch("modules.aws.groups.identity_center") +def test_request_groups_sync_synchronizes_groups_with_args( + mock_identity_center, mock_permissions, mock_slack_users +): + client = MagicMock() + body = MagicMock() + respond = MagicMock() + args = ["group1", "group2"] + logger = MagicMock() + + mock_slack_users.get_user_email_from_body.return_value = "admin.user@test.com" + mock_permissions.is_user_member_of_groups.return_value = True + mock_identity_center.synchronize.return_value = None + + groups.request_groups_sync(client, body, respond, args, logger) + + mock_slack_users.get_user_email_from_body.assert_called_once_with(client, body) + mock_permissions.is_user_member_of_groups.assert_called_once_with( + "admin.user@test.com", groups.AWS_ADMIN_GROUPS + ) + logger.info.assert_called_once_with("Synchronizing AWS Identity Center Groups.") + mock_identity_center.synchronize.assert_called_once_with( + enable_users_sync=False, + enable_user_create=False, + enable_membership_create=True, + enable_membership_delete=True, + pre_processing_filters=ANY, ) respond.assert_called_once_with("AWS Groups Memberships Synchronization Initiated.") diff --git a/app/tests/modules/aws/test_sync_identity_center.py b/app/tests/modules/aws/test_sync_identity_center.py index af65be27..47985353 100644 --- a/app/tests/modules/aws/test_sync_identity_center.py +++ b/app/tests/modules/aws/test_sync_identity_center.py @@ -217,8 +217,13 @@ def test_synchronize_sync_users_and_groups_with_defaults( assert mock_groups.get_groups_from_integration.call_count == 2 assert mock_groups.get_groups_from_integration.call_args_list == [ - call("google_groups", query="email:aws-*", post_processing_filters=ANY), - call("aws_identity_center"), + call( + "google_groups", + query="email:aws-*", + pre_processing_filters=[], + post_processing_filters=ANY, + ), + call("aws_identity_center", pre_processing_filters=[]), ] assert mock_identity_store.list_users.call_count == 2 @@ -284,9 +289,12 @@ def test_synchronize_sync_skip_users_if_false( assert mock_groups.get_groups_from_integration.call_count == 2 google_groups_call = call( - "google_groups", query="email:aws-*", post_processing_filters=ANY + "google_groups", + query="email:aws-*", + pre_processing_filters=[], + post_processing_filters=ANY, ) - aws_identity_center_call = call("aws_identity_center") + aws_identity_center_call = call("aws_identity_center", pre_processing_filters=[]) assert mock_groups.get_groups_from_integration.call_args_list == [ google_groups_call, aws_identity_center_call, @@ -351,9 +359,12 @@ def test_synchronize_sync_skip_groups_false_if_false( assert mock_groups.get_groups_from_integration.call_count == 2 google_groups_call = call( - "google_groups", query="email:aws-*", post_processing_filters=ANY + "google_groups", + query="email:aws-*", + pre_processing_filters=[], + post_processing_filters=ANY, ) - aws_identity_center_call = call("aws_identity_center") + aws_identity_center_call = call("aws_identity_center", pre_processing_filters=[]) assert mock_groups.get_groups_from_integration.call_args_list == [ google_groups_call, aws_identity_center_call, @@ -419,9 +430,12 @@ def test_synchronize_sync_skip_users_and_groups_if_false( assert mock_groups.get_groups_from_integration.call_count == 2 google_groups_call = call( - "google_groups", query="email:aws-*", post_processing_filters=ANY + "google_groups", + query="email:aws-*", + pre_processing_filters=[], + post_processing_filters=ANY, ) - aws_identity_center_call = call("aws_identity_center") + aws_identity_center_call = call("aws_identity_center", pre_processing_filters=[]) assert mock_groups.get_groups_from_integration.call_args_list == [ google_groups_call, aws_identity_center_call, From 58d665ff3c54f5420b0e934ab3560ca955ce6f3a Mon Sep 17 00:00:00 2001 From: Guillaume Charest <1690085+gcharest@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:49:06 +0000 Subject: [PATCH 3/3] feat: update help text to match new commands available --- app/modules/aws/aws.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/modules/aws/aws.py b/app/modules/aws/aws.py index 4c4288fa..07a2d58d 100644 --- a/app/modules/aws/aws.py +++ b/app/modules/aws/aws.py @@ -9,7 +9,9 @@ """ import os -from slack_bolt import App +from slack_bolt import App, Ack, Respond +from slack_sdk.web import WebClient +from logging import Logger from integrations.aws.organizations import get_account_id_by_name from integrations.aws import identity_store @@ -20,20 +22,25 @@ AWS_ADMIN_GROUPS = os.environ.get("AWS_ADMIN_GROUPS", "sre-ifs@cds-snc.ca").split(",") help_text = """ -\n `/aws user ...` +\n `/aws users ...` \n - Provision or deprovision AWS users | Provisionner ou déprovisionner des utilisateurs AWS \n Supports multiple users for a single operation | Supporte plusieurs utilisateurs pour l'opération \n ``: `create` or/ou `delete` \n ``: email address or Slack username of the user | adresse courriel ou identifiant Slack de l'utilisateur \n Usage: `/aws user create @username user.name@email.com` +\n `/aws groups ...` +\n - Manage AWS groups | Gérer les groupes AWS +\n ``: `sync`, `list` +\n ``: name of the group | nom du groupe (sync only) +\n Usage: `/aws groups sync`, `/aws groups sync group-name` or/ou `/aws groups list` \n `/aws help | aide` \n - Show this help text | montre le dialogue d'aide \n \n (currently disabled) -\n `/aws access` -\n - starts the process to access an AWS account | débute le processus pour accéder à un compte AWS \n `/aws health` \n - Query the health of an AWS account | Demander l'état de santé d'un compte AWS +\n `/aws access` +\n - starts the process to access an AWS account | débute le processus pour accéder à un compte AWS """ @@ -48,7 +55,9 @@ def register(bot: App) -> None: bot.view("aws_health_view")(aws_account_health.health_view_handler) -def aws_command(ack, command, logger, respond, client, body) -> None: +def aws_command( + ack: Ack, command, logger: Logger, respond: Respond, client: WebClient, body +) -> None: """AWS command handler. This function handles the `/aws` command by parsing the command text and executing the appropriate action.