Skip to content

Commit

Permalink
Fix/aws integration for client & identity store (#482)
Browse files Browse the repository at this point in the history
* feat: Add functions to convert snake_case to PascalCase

* fix: Refactor AWS client code to use PascalCase

* feat: handling of groups, memberships and user details

* feat: Add fixtures for Google API Python Client and AWS API
  • Loading branch information
gcharest authored Apr 25, 2024
1 parent 94183ee commit 0e1cbd0
Show file tree
Hide file tree
Showing 7 changed files with 716 additions and 98 deletions.
4 changes: 2 additions & 2 deletions app/integrations/aws/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import boto3 # type: ignore
from botocore.exceptions import BotoCoreError, ClientError # type: ignore
from dotenv import load_dotenv
from integrations.utils.api import convert_kwargs_to_camel_case
from integrations.utils.api import convert_kwargs_to_pascal_case

load_dotenv()

Expand Down Expand Up @@ -104,7 +104,7 @@ def execute_aws_api_call(service_name, method, paginated=False, **kwargs):
client = assume_role_client(service_name, role_arn)
kwargs.pop("role_arn", None)
if kwargs:
kwargs = convert_kwargs_to_camel_case(kwargs)
kwargs = convert_kwargs_to_pascal_case(kwargs)
api_method = getattr(client, method)
if paginated:
return paginator(client, method, **kwargs)
Expand Down
70 changes: 62 additions & 8 deletions app/integrations/aws/identity_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def create_user(email, first_name, family_name, **kwargs):
**kwargs: Additional keyword arguments for the API call.
Returns:
str: The user ID of the created user.
str: The unique ID of the user created.
"""
kwargs = resolve_identity_store_id(kwargs)
kwargs.update(
Expand Down Expand Up @@ -85,6 +85,23 @@ def get_user_id(user_name, **kwargs):
return response["UserId"] if response else False


@handle_aws_api_errors
def describe_user(user_id, **kwargs):
"""Retrieves the user details of the user
Args:
user_id (str): The user ID of the user.
**kwargs: Additional keyword arguments for the API call.
"""
kwargs = resolve_identity_store_id(kwargs)
kwargs.update({"UserId": user_id})
response = execute_aws_api_call("identitystore", "describe_user", **kwargs)
if not response:
return False
response.pop("ResponseMetadata", None)
return response


@handle_aws_api_errors
def list_users(**kwargs):
"""Retrieves all users from the AWS Identity Center (identitystore)"""
Expand Down Expand Up @@ -121,9 +138,10 @@ def get_group_id(group_name, **kwargs):
def list_groups(**kwargs):
"""Retrieves all groups from the AWS Identity Center (identitystore)"""
kwargs = resolve_identity_store_id(kwargs)
return execute_aws_api_call(
response = execute_aws_api_call(
"identitystore", "list_groups", paginated=True, keys=["Groups"], **kwargs
)
return response if response else []


@handle_aws_api_errors
Expand Down Expand Up @@ -165,24 +183,60 @@ def delete_group_membership(membership_id, **kwargs):
return True if response == {} else False


@handle_aws_api_errors
def get_group_membership_id(group_id, user_id, **kwargs):
"""Retrieves the group membership ID of the group membership
Args:
group_id (str): The group ID of the group.
user_id (str): The user ID of the user.
**kwargs: Additional keyword arguments for the API call.
"""
kwargs = resolve_identity_store_id(kwargs)
kwargs.update({"GroupId": group_id, "MemberId": {"UserId": user_id}})
response = execute_aws_api_call(
"identitystore", "get_group_membership_id", **kwargs
)
return response["MembershipId"] if response else False


@handle_aws_api_errors
def list_group_memberships(group_id, **kwargs):
"""Retrieves all group memberships from the AWS Identity Center (identitystore)"""
"""Retrieves all group memberships from the AWS Identity Center (identitystore)
Args:
group_id (str): The group ID of the group.
**kwargs: Additional keyword arguments for the API call.
Returns:
list: A list of group membership objects."""
kwargs = resolve_identity_store_id(kwargs)
return execute_aws_api_call(
response = execute_aws_api_call(
"identitystore",
"list_group_memberships",
["GroupMemberships"],
GroupId=group_id,
**kwargs,
)
return response["GroupMemberships"] if response else []


@handle_aws_api_errors
def list_groups_with_memberships():
"""Retrieves all groups with their members from the AWS Identity Center (identitystore)"""
groups = list_groups()
def list_groups_with_memberships(**kwargs):
"""Retrieves groups with their members from the AWS Identity Center (identitystore)
Args:
**kwargs: Additional keyword arguments for the API call. (passed to list_groups)
Returns:
list: A list of group objects with their members.
"""
members_details = kwargs.get("members_details", True)
kwargs.pop("members_details", None)
groups = list_groups(**kwargs)
for group in groups:
group["GroupMemberships"] = list_group_memberships(group["GroupId"])
if group["GroupMemberships"] and members_details:
for membership in group["GroupMemberships"]:
membership["MemberId"] = describe_user(membership["MemberId"]["UserId"])

return groups
36 changes: 36 additions & 0 deletions app/integrations/utils/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Utilities for API integrations."""
import re


def convert_string_to_camel_case(snake_str):
Expand Down Expand Up @@ -31,3 +32,38 @@ def convert_kwargs_to_camel_case(kwargs):
return [convert_kwargs_to_camel_case(i) for i in kwargs]
else:
return kwargs


def convert_string_to_pascal_case(snake_str):
"""Convert a snake_case string to PascalCase."""
if not isinstance(snake_str, str):
raise TypeError("Input must be a string")
# Convert camelCase to snake_case
snake_str = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", snake_str)
snake_str = re.sub("([a-z0-9])([A-Z])", r"\1_\2", snake_str).lower()

components = snake_str.split("_")
if len(components) == 1 and components[0] != "":
# return components[0]
return snake_str[0].upper() + snake_str[1:]
else:
return "".join(x.title() for x in components)


def convert_dict_to_pascale_case(dict):
"""Convert all keys in a dictionary from snake_case to PascalCase."""
new_dict = {}
for k, v in dict.items():
new_key = convert_string_to_pascal_case(k)
new_dict[new_key] = convert_kwargs_to_pascal_case(v)
return new_dict


def convert_kwargs_to_pascal_case(kwargs):
"""Convert all keys in a list of dictionaries from snake_case to PascalCase."""
if isinstance(kwargs, dict):
return convert_dict_to_pascale_case(kwargs)
elif isinstance(kwargs, list):
return [convert_kwargs_to_pascal_case(i) for i in kwargs]
else:
return kwargs
170 changes: 170 additions & 0 deletions app/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import pytest

# Google API Python Client


# Google Discovery Directory Resource
# Base fixtures
@pytest.fixture
def google_groups():
def _google_groups(n=3, prefix="", domain="test.com"):
return [
{
"id": f"{prefix}_google_group_id{i+1}",
"name": f"AWS-group{i+1}",
"email": f"{prefix}_aws-group{i+1}@{domain}",
}
for i in range(n)
]

return _google_groups


@pytest.fixture
def google_users():
def _google_users(n=3, prefix="", domain="test.com"):
users = []
for i in range(n):
user = {
"id": f"{prefix}_id_{i}",
"primaryEmail": f"{prefix}_email_{i}@{domain}",
"emails": [
{
"address": f"{prefix}_email_{i}@{domain}",
"primary": True,
"type": "work",
}
],
"suspended": False,
"name": {
"fullName": f"Given_name_{i} Family_name_{i}",
"familyName": f"Family_name_{i}",
"givenName": f"Given_name_{i}",
"displayName": f"Given_name_{i} Family_name_{i}",
},
}
users.append(user)
return users

return _google_users


@pytest.fixture
def google_group_members(google_users):
def _google_group_members(n=3, prefix="", domain="test.com"):
users = google_users(n, prefix, domain)
return [
{
"kind": "admin#directory#member",
"email": user["primaryEmail"],
"role": "MEMBER",
"type": "USER",
"status": "ACTIVE",
"id": user["id"],
}
for user in users
]

return _google_group_members


# Fixture with users
@pytest.fixture
def google_groups_w_users(google_groups, google_users):
def _google_groups_w_users(n_groups=1, n_users=3, prefix="", domain="test.com"):
groups = google_groups(n_groups, prefix, domain)
users = google_users(n_users, prefix, domain)
for group in groups:
group["members"] = users
return groups

return _google_groups_w_users


# AWS API fixtures


@pytest.fixture
def aws_users():
def _aws_users(n=3, prefix="", domain="test.com", store_id="d-123412341234"):
users = []
for i in range(n):
user = {
"UserName": f"{prefix}_email_{i}@{domain}",
"UserId": f"{prefix}_id_{i}",
"Name": {
"FamilyName": f"Family_name_{i}",
"GivenName": f"Given_name_{i}",
},
"DisplayName": f"Given_name_{i} Family_name_{i}",
"Emails": [
{
"Value": f"{prefix}_email_{i}@{domain}",
"Type": "work",
"Primary": True,
}
],
"IdentityStoreId": f"{store_id}",
}
users.append(user)
return users

return _aws_users


@pytest.fixture
def aws_groups():
def _aws_groups(n=3, prefix="", store_id="d-123412341234"):
return {
"Groups": [
{
"GroupId": f"{prefix}_aws-group_id{i+1}",
"DisplayName": f"AWS-group{i+1}",
"Description": f"A group to test resolving AWS-group{i+1} memberships",
"IdentityStoreId": f"{store_id}",
}
for i in range(n)
]
}

return _aws_groups


@pytest.fixture
def aws_groups_memberships():
def _aws_groups_memberships(n=3, prefix="", store_id="d-123412341234"):
return {
"GroupMemberships": [
{
"IdentityStoreId": f"{store_id}",
"MembershipId": f"{prefix}_membership_id_{i+1}",
"GroupId": f"{prefix}_aws-group_id{i+1}",
"MemberId": {
"UserId": f"{prefix}_id_{i}",
},
}
for i in range(n)
]
}

return _aws_groups_memberships


@pytest.fixture
def aws_groups_w_users(aws_groups, aws_users, aws_groups_memberships):
def _aws_groups_w_users(
n_groups=1, n_users=3, prefix="", domain="test.com", store_id="d-123412341234"
):
groups = aws_groups(n_groups, prefix, domain, store_id)["Groups"]
users = aws_users(n_users, prefix, domain, store_id)
memberships = aws_groups_memberships(n_groups, prefix, domain, store_id)[
"GroupMemberships"
]
for group, membership in zip(groups, memberships):
group.update(membership)
group["GroupMemberships"] = [
{**membership, "MemberId": user} for user in users
]
return groups

return _aws_groups_w_users
Loading

0 comments on commit 0e1cbd0

Please sign in to comment.