diff --git a/docs/source/specs/openapi.json b/docs/source/specs/openapi.json index 10b565609..66c04dd8b 100644 --- a/docs/source/specs/openapi.json +++ b/docs/source/specs/openapi.json @@ -1273,6 +1273,18 @@ ] } }, + { + "in": "query", + "name": "service_account_client_ids", + "required": false, + "description": "By specifying a list of client IDs with this query parameter, RBAC will return an object with the specified client ID and it's matching boolean value to flag whether the client ID is present in the group or not. This query parameter cannot be used along with any other query parameter.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, { "in": "query", "name": "service_account_description", @@ -1304,6 +1316,9 @@ }, { "$ref": "#/components/schemas/ServiceAccountPagination" + }, + { + "$ref": "#/components/schemas/ServiceAccountInGroupResponse" } ] } @@ -3369,6 +3384,30 @@ } ] }, + "ServiceAccountInGroupResponse": { + "properties": { + "meta": { + "$ref": "#/components/schemas/PaginationMeta" + }, + "links": { + "description": "The links object for this particular response will be empty, since there is no pagination available for the query parameter", + "type": "object", + "example": {} + }, + "data": { + "description": "Object which indicates whether the given service account UUIDs in the query parameter are present in the specified group or not", + "type": "object", + "additionalProperties": { + "description": "The response is a map of the form \"UUID\": (true|false)", + "type": "boolean" + }, + "example": { + "dd946f24-cfda-11ee-acb6-7b2702ff4dc8": true, + "3e728bb0-b167-013c-c455-6aa2427b506c": false + } + } + } + }, "Group": { "required": [ "name" diff --git a/rbac/management/group/view.py b/rbac/management/group/view.py index 8c872cfa5..529efa2c9 100644 --- a/rbac/management/group/view.py +++ b/rbac/management/group/view.py @@ -17,7 +17,8 @@ """View for group management.""" import logging -from typing import Iterable +from typing import Iterable, Optional +from uuid import UUID import requests from django.conf import settings @@ -70,6 +71,7 @@ PRINCIPAL_USERNAME_KEY = "principal_username" VALID_ROLE_ORDER_FIELDS = list(RoleViewSet.ordering_fields) ROLE_DISCRIMINATOR_KEY = "role_discriminator" +SERVICE_ACCOUNT_CLIENT_IDS_KEY = "service_account_client_ids" SERVICE_ACCOUNT_DESCRIPTION_KEY = "service_account_description" SERVICE_ACCOUNT_NAME_KEY = "service_account_name" SERVICE_ACCOUNT_USERNAME_FORMAT = "service-account-{clientID}" @@ -500,7 +502,7 @@ def remove_principals(self, group, principals, account=None, org_id=None): return group @action(detail=True, methods=["get", "post", "delete"]) - def principals(self, request: Request, uuid=None): + def principals(self, request: Request, uuid: Optional[UUID] = None): """Get, add or remove principals from a group.""" """ @api {get} /api/v1/groups/:uuid/principals/ Get principals for a group @@ -653,6 +655,79 @@ def principals(self, request: Request, uuid=None): # ... and return it. response = Response(status=status.HTTP_200_OK, data=output.data) elif request.method == "GET": + # Check if the request comes with a bunch of service account client IDs that we need to check. Since this + # query parameter is incompatible with any other query parameter, we make the checks first. That way if any + # other query parameter was specified, we simply return early. + if SERVICE_ACCOUNT_CLIENT_IDS_KEY in request.query_params: + if len(request.query_params) > 1: + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={ + "errors": [ + { + "detail": f"The '{SERVICE_ACCOUNT_CLIENT_IDS_KEY}' parameter is incompatible with" + " any other query parameter. Please, use it alone", + "source": "groups", + "status": str(status.HTTP_400_BAD_REQUEST), + } + ] + }, + ) + + # Check that the specified query parameter is not empty. + service_account_client_ids_raw = request.query_params.get(SERVICE_ACCOUNT_CLIENT_IDS_KEY).strip() + if not service_account_client_ids_raw: + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={ + "errors": [ + { + "detail": "Not a single client ID was specified for the client IDs filter", + "source": "groups", + "status": str(status.HTTP_400_BAD_REQUEST), + } + ] + }, + ) + + # Turn the received and comma separated client IDs into a manageable set. + received_client_ids: set[str] = set(service_account_client_ids_raw.split(",")) + + # Validate that the provided strings are actually UUIDs. + for rci in received_client_ids: + try: + UUID(rci) + except ValueError: + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={ + "errors": [ + { + "detail": f"The specified client ID '{rci}' is not a valid UUID", + "source": "groups", + "status": str(status.HTTP_400_BAD_REQUEST), + } + ] + }, + ) + + # Generate the report of which of the tenant's service accounts are in a group, and which + # ones are available to be added to the given group. + it_service = ITService() + result: dict = it_service.generate_service_accounts_report_in_group( + group=group, client_ids=received_client_ids + ) + + # Prettify the output payload and return it. + return Response( + status=status.HTTP_200_OK, + data={ + "meta": {"count": len(result)}, + "links": {}, + "data": result, + }, + ) + # Get the "order_by" query parameter. all_valid_fields = VALID_PRINCIPAL_ORDER_FIELDS + ["-" + field for field in VALID_PRINCIPAL_ORDER_FIELDS] sort_order = None diff --git a/rbac/management/principal/it_service.py b/rbac/management/principal/it_service.py index 623988d40..18b6bfdf8 100644 --- a/rbac/management/principal/it_service.py +++ b/rbac/management/principal/it_service.py @@ -33,6 +33,7 @@ # Constants or global variables. LOGGER = logging.getLogger(__name__) +SERVICE_ACCOUNT_CLIENT_IDS_KEY = "service_account_client_ids" TYPE_SERVICE_ACCOUNT = "service-account" # IT path to fetch the service accounts. @@ -401,6 +402,22 @@ def extract_client_id_service_account_username(username: str) -> uuid.UUID: } ) + def generate_service_accounts_report_in_group(self, group: Group, client_ids: set[str]) -> dict[str, bool]: + """Check if the given service accounts are in the specified group.""" + # Fetch the service accounts from the group. + group_service_account_principals = ( + group.principals.values_list("service_account_id", flat=True) + .filter(type=TYPE_SERVICE_ACCOUNT) + .filter(service_account_id__in=client_ids) + ) + + # Mark the specified client IDs as "present or missing" from the result set. + result: dict[str, bool] = {} + for incoming_client_id in client_ids: + result[incoming_client_id] = incoming_client_id in group_service_account_principals + + return result + def _transform_incoming_payload(self, service_account_from_it_service: dict) -> dict: """Transform the incoming service account from IT into a dict which fits our response structure.""" service_account: dict = {} diff --git a/tests/management/group/test_view.py b/tests/management/group/test_view.py index 84606fbe9..519996cfc 100644 --- a/tests/management/group/test_view.py +++ b/tests/management/group/test_view.py @@ -16,14 +16,16 @@ # """Test the group viewset.""" import random +import uuid from unittest.mock import call, patch, ANY -from uuid import uuid4 +from uuid import uuid4, UUID from django.db import transaction from django.conf import settings from django.urls import reverse from django.test.utils import override_settings from rest_framework import status +from rest_framework.response import Response from rest_framework.test import APIClient from api.models import Tenant, User @@ -2129,6 +2131,283 @@ def test_get_group_service_account_invalid_limit_offset(self, mock_request): self.assertEqual(int(response.data.get("meta").get("count")), 3) self.assertEqual(len(response.data.get("data")), 3) + def test_get_group_principals_check_service_account_ids(self): + """Test that the endpoint for checking if service accounts are part of a group works as expected.""" + # Create a group and associate principals to it. + group = Group(name="it-service-group", platform_default=False, system=False, tenant=self.tenant) + group.save() + + # The user principals should not be retrieved in the results. + group.principals.add(Principal.objects.create(username="user-1", tenant=self.tenant)) + group.principals.add(Principal.objects.create(username="user-2", tenant=self.tenant)) + group.principals.add(Principal.objects.create(username="user-3", tenant=self.tenant)) + group.save() + + # Create some service accounts and add some of them to the group. + client_uuid_1 = uuid.uuid4() + client_uuid_2 = uuid.uuid4() + + sa_1 = Principal.objects.create( + username=f"service-account-{client_uuid_1}", + service_account_id=client_uuid_1, + type="service-account", + tenant=self.tenant, + ) + sa_2 = Principal.objects.create( + username=f"service-account-{client_uuid_2}", + service_account_id=client_uuid_2, + type="service-account", + tenant=self.tenant, + ) + + group.principals.add(sa_1) + group.principals.add(sa_2) + group.save() + + # Create a set with the service accounts that will go in the group. It will make it easier to make assertions + # below. + group_service_accounts_set = {str(sa_1.service_account_id), str(sa_2.service_account_id)} + + # Create more service accounts that should not show in the results, since they're not going to be specified in + # the "client_ids" parameter. + Principal.objects.create( + username=f"service-account-{uuid.uuid4()}", + service_account_id=uuid.uuid4(), + type="service-account", + tenant=self.tenant, + ) + Principal.objects.create( + username=f"service-account-{uuid.uuid4()}", + service_account_id=uuid.uuid4(), + type="service-account", + tenant=self.tenant, + ) + Principal.objects.create( + username=f"service-account-{uuid.uuid4()}", + service_account_id=uuid.uuid4(), + type="service-account", + tenant=self.tenant, + ) + + # Create the UUIDs to be specified in the request. + not_in_group = uuid.uuid4() + not_in_group_2 = uuid.uuid4() + not_in_group_3 = uuid.uuid4() + + # Also, create a set with the service accounts that will NOT go in the group to make it easier to assert that + # the results flag them as such. + service_accounts_not_in_group_set = { + str(not_in_group), + str(not_in_group_2), + str(not_in_group_3), + } + + # Create the query parameter. + service_accounts_client_ids = ( + f"{client_uuid_1},{client_uuid_2},{not_in_group},{not_in_group_2},{not_in_group_3}" + ) + + url = ( + f"{reverse('group-principals', kwargs={'uuid': group.uuid})}" + f"?service_account_client_ids={service_accounts_client_ids}" + ) + + # Call the endpoint under test. + client = APIClient() + response: Response = client.get(url, **self.headers) + + # Assert that we received a 200 response. + self.assertEqual( + status.HTTP_200_OK, + response.status_code, + "unexpected status code received", + ) + + # Assert that we received the correct results count. + self.assertEqual( + 5, + response.data.get("meta").get("count"), + "The results of five client IDs should have been returned, since those were the ones sent to the endpoint", + ) + + # Assert that the mixed matches are identified correctly. + for client_id, is_it_present_in_group in response.data.get("data").items(): + # If the value is "true" it should be present in the service accounts' result set from above. Else, it + # means that the specified client IDs were not part of the group, and that they should have been flagged + # as such. + if is_it_present_in_group: + self.assertEqual( + True, + client_id in group_service_accounts_set, + "a client ID which was not part of the group was incorrectly flagged as if it was", + ) + else: + self.assertEqual( + True, + client_id in service_accounts_not_in_group_set, + "a client ID which was part of the group was incorrectly flagged as if it wasn't", + ) + + def test_get_group_principals_check_service_account_ids_non_existent(self): + """Test that when checking non-existent service account client IDs from another group the endpoint flags them as not present.""" + + # Create the UUIDs to be specified in the request. + not_in_group = uuid.uuid4() + not_in_group_2 = uuid.uuid4() + not_in_group_3 = uuid.uuid4() + not_in_group_4 = uuid.uuid4() + not_in_group_5 = uuid.uuid4() + + # Also, create a set with the service accounts that will NOT go in the group to make it easier to assert that + # the results flag them as such. + service_accounts_not_in_group_set = { + str(not_in_group), + str(not_in_group_2), + str(not_in_group_3), + str(not_in_group_4), + str(not_in_group_5), + } + + # Create the query parameter. + service_accounts_client_ids = ( + f"{not_in_group},{not_in_group_2},{not_in_group_3},{not_in_group_4},{not_in_group_5}" + ) + + url = ( + f"{reverse('group-principals', kwargs={'uuid': self.group.uuid})}" + f"?service_account_client_ids={service_accounts_client_ids}" + ) + + # Call the endpoint under test. + client = APIClient() + response: Response = client.get(url, **self.headers) + + # Assert that we received a 200 response. + self.assertEqual( + status.HTTP_200_OK, + response.status_code, + "unexpected status code received", + ) + + # Assert that we received the correct results count. + self.assertEqual( + 5, + response.data.get("meta").get("count"), + "The results of five client IDs should have been returned, since those were the ones sent to the endpoint", + ) + + # Assert that the mixed matches are identified correctly. + for client_id, is_it_present_in_group in response.data.get("data").items(): + # If the value is "true" it should be present in the service accounts' result set from above. Else, it + # means that the specified client IDs were not part of the group, and that they should have been flagged + # as such. + if is_it_present_in_group: + self.fail( + "no existing service accounts were specified in the query parameter. Still, some were flagged as" + " present in the group" + ) + else: + self.assertEqual( + True, + client_id in service_accounts_not_in_group_set, + "a client ID which was part of the group was incorrectly flagged as if it wasn't", + ) + + def test_get_group_principals_check_service_account_ids_incompatible_query_parameters(self): + """Test that no other query parameter can be used along with the "service_account_ids" one.""" + # Use a few extra query parameter to test the behavior. Since we use a "len(query_params) > 1" condition it + # really does not matter which other query parameter we use for the test, but we are adding a bunch in case + # this changes in the future. + query_parameters_to_test: list[str] = [ + "order_by", + "principal_type", + "principal_username", + "service_account_name", + "username_only", + ] + + for query_parameter in query_parameters_to_test: + url = ( + f"{reverse('group-principals', kwargs={'uuid': self.group.uuid})}" + f"?service_account_client_ids={uuid.uuid4()}&{query_parameter}=abcde" + ) + client = APIClient() + response: Response = client.get(url, **self.headers) + + # Assert that we received a 400 response. + self.assertEqual( + status.HTTP_400_BAD_REQUEST, + response.status_code, + "unexpected status code received", + ) + + # Assert that the error message is the expected one. + self.assertEqual( + str(response.data.get("errors")[0].get("detail")), + "The 'service_account_client_ids' parameter is incompatible with any other query parameter." + " Please, use it alone", + ) + + def test_get_group_principals_check_service_account_ids_empty_client_ids(self): + """Test that an empty service account IDs query param returns a bad request response""" + url = f"{reverse('group-principals', kwargs={'uuid': self.group.uuid})}?service_account_client_ids=" + client = APIClient() + response: Response = client.get(url, **self.headers) + + # Assert that we received a 400 response. + self.assertEqual( + status.HTTP_400_BAD_REQUEST, + response.status_code, + "unexpected status code received", + ) + + # Assert that the error message is the expected one. + self.assertEqual( + str(response.data.get("errors")[0].get("detail")), + "Not a single client ID was specified for the client IDs filter", + "unexpected error message detail", + ) + + def test_get_group_principals_check_service_account_ids_blank_string(self): + """Test that a blank service account IDs query param returns a bad request response""" + url = f"{reverse('group-principals', kwargs={'uuid': self.group.uuid})}?service_account_client_ids= " + client = APIClient() + response: Response = client.get(url, **self.headers) + + # Assert that we received a 400 response. + self.assertEqual( + status.HTTP_400_BAD_REQUEST, + response.status_code, + "unexpected status code received", + ) + + # Assert that the error message is the expected one. + self.assertEqual( + str(response.data.get("errors")[0].get("detail")), + "Not a single client ID was specified for the client IDs filter", + "unexpected error message detail", + ) + + def test_get_group_principals_check_service_account_ids_invalid_uuid(self): + """Test that an invalid service account ID query param returns a bad request response""" + url = f"{reverse('group-principals', kwargs={'uuid': self.group.uuid})}?service_account_client_ids=abcde" + client = APIClient() + response: Response = client.get(url, **self.headers) + + # Assert that we received a 400 response. + self.assertEqual( + status.HTTP_400_BAD_REQUEST, + response.status_code, + "unexpected status code received", + ) + + # Assert that the error message is the expected one. + self.assertEqual( + str(response.data.get("errors")[0].get("detail")), + "The specified client ID 'abcde' is not a valid UUID", + "unexpected error message detail", + ) + class GroupViewNonAdminTests(IdentityRequest): """Test the group view for nonadmin user.""" diff --git a/tests/management/principal/test_it_service.py b/tests/management/principal/test_it_service.py index 1d1eadbbc..94c2889fa 100644 --- a/tests/management/principal/test_it_service.py +++ b/tests/management/principal/test_it_service.py @@ -19,6 +19,7 @@ from django.conf import settings +from management.models import Group, Principal from management.principal.it_service import ITService from rest_framework import serializers from tests.identity_request import IdentityRequest @@ -196,3 +197,286 @@ def test_extract_client_id_service_account_username(self) -> None: str(ve.detail.get("detail")), "unexpected error message when providing an invalid UUID as the client ID", ) + + def test_generate_service_accounts_report_in_group_zero_matches(self): + """Test that the function under test is able to flag service accounts as not present in a group""" + # Create a group for the principals. + group = Group(name="it-service-group", platform_default=False, system=False, tenant=self.tenant) + group.save() + + # Add the principal accounts to make sure that we are only working with service accounts. If we weren't, these + # principals below should give us unexpected results in our assertions. + group.principals.add(Principal.objects.create(username="user-1", tenant=self.tenant)) + group.principals.add(Principal.objects.create(username="user-2", tenant=self.tenant)) + group.principals.add(Principal.objects.create(username="user-3", tenant=self.tenant)) + + # Create three service accounts for the group. Since these will not be specified in the "client_ids" parameter + # of the function under test, they should not show up in the results. + sa_1 = Principal.objects.create( + username=f"service-account-{uuid.uuid4()}", + service_account_id=uuid.uuid4(), + type="service-account", + tenant=self.tenant, + ) + sa_2 = Principal.objects.create( + username=f"service-account-{uuid.uuid4()}", + service_account_id=uuid.uuid4(), + type="service-account", + tenant=self.tenant, + ) + sa_3 = Principal.objects.create( + username=f"service-account-{uuid.uuid4()}", + service_account_id=uuid.uuid4(), + type="service-account", + tenant=self.tenant, + ) + + # Add the service account principals. + group.principals.add(sa_1) + group.principals.add(sa_2) + group.principals.add(sa_3) + group.save() + + # Simulate that a few client IDs were specified in the request. + request_client_ids = set[str]() + request_client_ids.add(str(uuid.uuid4())) + request_client_ids.add(str(uuid.uuid4())) + request_client_ids.add(str(uuid.uuid4())) + + # Call the function under test. + result: dict[str, bool] = self.it_service.generate_service_accounts_report_in_group( + group=group, client_ids=request_client_ids + ) + # Assert that only the specified client IDs are present in the result. + self.assertEqual(3, len(result)) + + # Assert that all the service accounts were flagged as not present in the group. + for client_id, is_present_in_group in result.items(): + # Make sure the specified client IDs are in the set. + self.assertEqual( + True, + client_id in request_client_ids, + "expected to find the specified client ID from the request in the returning result", + ) + # Make sure they are all set to "false" since there shouldn't be any of those client IDs in the group. + self.assertEqual( + False, + is_present_in_group, + "the client ID should have not been found in the group, since the group had no service accounts", + ) + + def test_generate_service_accounts_report_in_group_mixed_results(self): + """Test that the function under test is able to correctly flag the sevice accounts when there are mixed results""" + # Create a group and associate principals to it. + group = Group(name="it-service-group", platform_default=False, system=False, tenant=self.tenant) + group.save() + + # Add the principal accounts to make sure that we are only working with service accounts. If we weren't, these + # principals below should give us unexpected results in our assertions. + group.principals.add(Principal.objects.create(username="user-1", tenant=self.tenant)) + group.principals.add(Principal.objects.create(username="user-2", tenant=self.tenant)) + group.principals.add(Principal.objects.create(username="user-3", tenant=self.tenant)) + group.save() + + client_uuid_1 = uuid.uuid4() + client_uuid_2 = uuid.uuid4() + sa_1 = Principal.objects.create( + username=f"service-account-{client_uuid_1}", + service_account_id=client_uuid_1, + type="service-account", + tenant=self.tenant, + ) + sa_2 = Principal.objects.create( + username=f"service-account-{client_uuid_2}", + service_account_id=client_uuid_2, + type="service-account", + tenant=self.tenant, + ) + + # Add the service accounts to the group. + group.principals.add(sa_1) + group.principals.add(sa_2) + group.save() + + # Create a set with the service accounts that will go in the group. It will make it easier to make assertions + # below. + group_service_accounts_set = {str(sa_1.service_account_id), str(sa_2.service_account_id)} + + # Create more service accounts that should not show in the results, since they're not going to be specified in + # the "client_ids" parameter. + sa_3 = Principal.objects.create( + username=f"service-account-{uuid.uuid4()}", + service_account_id=uuid.uuid4(), + type="service-account", + tenant=self.tenant, + ) + sa_4 = Principal.objects.create( + username=f"service-account-{uuid.uuid4()}", + service_account_id=uuid.uuid4(), + type="service-account", + tenant=self.tenant, + ) + sa_5 = Principal.objects.create( + username=f"service-account-{uuid.uuid4()}", + service_account_id=uuid.uuid4(), + type="service-account", + tenant=self.tenant, + ) + + # Add the service accounts that should not show up in the results. + group.principals.add(sa_3) + group.principals.add(sa_4) + group.principals.add(sa_5) + group.save() + + # Create the service accounts' client IDs that will be specified in the request. + not_in_group = uuid.uuid4() + not_in_group_2 = uuid.uuid4() + not_in_group_3 = uuid.uuid4() + + # Also, create a set with the service accounts that will NOT go in the group to make it easier to assert that + # the results flag them as such. + service_accounts_not_in_group_set = { + str(not_in_group), + str(not_in_group_2), + str(not_in_group_3), + } + + # Add all the UUIDs to a set to pass it to the function under test. + request_client_ids = set[str]() + request_client_ids.add(str(not_in_group)) + request_client_ids.add(str(not_in_group_2)) + request_client_ids.add(str(not_in_group_3)) + + # Specify the service accounts' UUIDs here too, because the function under test should flag them as present in + # the group. + request_client_ids.add(str(client_uuid_1)) + request_client_ids.add(str(client_uuid_2)) + + # Call the function under test. + result: dict[str, bool] = self.it_service.generate_service_accounts_report_in_group( + group=group, client_ids=request_client_ids + ) + + # Assert that all the specified client IDs are present in the result. + self.assertEqual(5, len(result)) + + # Assert that the mixed matches are identified correctly. + for client_id, is_it_present_in_group in result.items(): + # If the value is "true" it should be present in the service accounts' result set from above. Else, it + # means that the specified client IDs were not part of the group, and that they should have been flagged + # as such. + if is_it_present_in_group: + self.assertEqual( + True, + client_id in group_service_accounts_set, + "a client ID which was not part of the group was incorrectly flagged as if it was", + ) + else: + self.assertEqual( + True, + client_id in service_accounts_not_in_group_set, + "a client ID which was part of the group was incorrectly flagged as if it wasn't", + ) + + def test_generate_service_accounts_report_in_group_full_match(self): + """Test that the function under test is able to flag service accounts as all being present in the group.""" + # Create a group and associate principals to it. + group = Group(name="it-service-group", platform_default=False, system=False, tenant=self.tenant) + group.save() + + # Add the principal accounts to make sure that we are only working with service accounts. If we weren't, these + # principals below should give us unexpected results in our assertions. + group.principals.add(Principal.objects.create(username="user-1", tenant=self.tenant)) + group.principals.add(Principal.objects.create(username="user-2", tenant=self.tenant)) + group.principals.add(Principal.objects.create(username="user-3", tenant=self.tenant)) + + # Create the service accounts to be associated with the group. + client_uuid_1 = uuid.uuid4() + client_uuid_2 = uuid.uuid4() + client_uuid_3 = uuid.uuid4() + client_uuid_4 = uuid.uuid4() + client_uuid_5 = uuid.uuid4() + sa_1 = Principal.objects.create( + username=f"service-account-{client_uuid_1}", + service_account_id=client_uuid_1, + type="service-account", + tenant=self.tenant, + ) + sa_2 = Principal.objects.create( + username=f"service-account-{client_uuid_2}", + service_account_id=client_uuid_2, + type="service-account", + tenant=self.tenant, + ) + sa_3 = Principal.objects.create( + username=f"service-account-{client_uuid_3}", + service_account_id=client_uuid_3, + type="service-account", + tenant=self.tenant, + ) + sa_4 = Principal.objects.create( + username=f"service-account-{client_uuid_4}", + service_account_id=client_uuid_4, + type="service-account", + tenant=self.tenant, + ) + sa_5 = Principal.objects.create( + username=f"service-account-{client_uuid_5}", + service_account_id=client_uuid_5, + type="service-account", + tenant=self.tenant, + ) + + # Create a set with the service accounts that will go in the group. It will make it easier to make assertions + # below. + group_service_accounts_set = { + str(sa_1.service_account_id), + str(sa_2.service_account_id), + str(sa_3.service_account_id), + str(sa_4.service_account_id), + str(sa_5.service_account_id), + } + + # Add the service accounts to the group. + group.principals.add(sa_1) + group.principals.add(sa_2) + group.principals.add(sa_3) + group.principals.add(sa_4) + group.principals.add(sa_5) + group.save() + + # Simulate that a few client IDs were specified in the request. + request_client_ids = set[str]() + request_client_ids.add(str(client_uuid_1)) + request_client_ids.add(str(client_uuid_2)) + request_client_ids.add(str(client_uuid_3)) + request_client_ids.add(str(client_uuid_4)) + request_client_ids.add(str(client_uuid_5)) + + # Call the function under test. + result: dict[str, bool] = self.it_service.generate_service_accounts_report_in_group( + group=group, client_ids=request_client_ids + ) + + # Assert that all the specified client IDs are present in the result. + self.assertEqual(5, len(result)) + + # Assert that all the results are flagged as being part of the group. + for client_id, is_present_in_group in result.items(): + self.assertEqual( + True, + client_id in request_client_ids, + "expected to find the specified client ID from the request in the returning result", + ) + self.assertEqual( + True, + client_id in group_service_accounts_set, + "expected to find the client ID from the result set in the service accounts' group set", + ) + # Make sure they are all set to "true" since all the specified client IDs should be in the group. + self.assertEqual( + True, + is_present_in_group, + "the client ID should have been found in the group, since the group had all the service accounts added to it", + )