Skip to content

Commit

Permalink
Feat/sync single group (#603)
Browse files Browse the repository at this point in the history
* feat: improve logging of groups found

* feat: introduce ability to add args to groups sync slash command

* feat: update help text to match new commands available
  • Loading branch information
gcharest authored Aug 1, 2024
1 parent 9671a07 commit 2f58be0
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 19 deletions.
5 changes: 5 additions & 0 deletions app/integrations/aws/identity_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
9 changes: 7 additions & 2 deletions app/integrations/google_workspace/google_directory.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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):
Expand Down Expand Up @@ -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

Expand Down
19 changes: 14 additions & 5 deletions app/modules/aws/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,20 +22,25 @@
AWS_ADMIN_GROUPS = os.environ.get("AWS_ADMIN_GROUPS", "[email protected]").split(",")

help_text = """
\n `/aws user <operation> <user1> <user2> ...`
\n `/aws users <operation> <user1> <user2> ...`
\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 `<operation>`: `create` or/ou `delete`
\n `<user>`: email address or Slack username of the user | adresse courriel ou identifiant Slack de l'utilisateur
\n Usage: `/aws user create @username [email protected]`
\n `/aws groups <operation> <group1> <group2> ...`
\n - Manage AWS groups | Gérer les groupes AWS
\n `<operation>`: `sync`, `list`
\n `<group>`: 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
"""


Expand All @@ -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.
Expand Down
24 changes: 23 additions & 1 deletion app/modules/aws/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
10 changes: 8 additions & 2 deletions app/modules/aws/identity_center.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
37 changes: 36 additions & 1 deletion app/tests/modules/aws/test_aws_groups.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import patch, MagicMock, call
from unittest.mock import patch, MagicMock, call, ANY
from modules.aws import groups


Expand Down Expand Up @@ -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 = "[email protected]"
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(
"[email protected]", 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.")

Expand Down
30 changes: 22 additions & 8 deletions app/tests/modules/aws/test_sync_identity_center.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 2f58be0

Please sign in to comment.