Repository: getmoto/moto
Pull request #7179 (2024-01-03 04:55:58)

SSO Admin: list_account_assignments_for_principal
Implemented `sso-admin` `list_account_assignments_for_principal` [docs](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sso-admin/client/list_account_assignments_for_principal.html).

- [x] Feature is added
- [ ] The linter is happy
- [ ] Tests are added
- [ ] All tests pass, existing and new

Included the pagination implementation of this method and ensured proper testing.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7179?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`b23d94e`)](https://app.codecov.io/gh/getmoto/moto/commit/b23d94e27e8f2f36a232441dc0681f81d22380fe?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) 95.90% compared to head [(`00fc3ab`)](https://app.codecov.io/gh/getmoto/moto/pull/7179?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) 95.90%.
> Report is 4 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #7179      +/-   ##
==========================================
- Coverage   95.90%   95.90%   -0.01%     
==========================================
  Files         840      840              
  Lines       82488    82500      +12     
==========================================
+ Hits        79110    79120      +10     
- Misses       3378     3380       +2     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7179/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/7179/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `35.48% <25.00%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7179/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `95.84% <100.00%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/7179?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/identitystore/models.py b/moto/identitystore/models.py
index 167705355..8ec0d2613 100644
--- a/moto/identitystore/models.py
+++ b/moto/identitystore/models.py
@@ -82,6 +82,12 @@ class IdentityStoreBackend(BaseBackend):
             "limit_default": 100,
             "unique_attribute": "MembershipId",
         },
+        "list_group_memberships_for_member": {
+            "input_token": "next_token",
+            "limit_key": "max_results",
+            "limit_default": 100,
+            "unique_attribute": "MembershipId",
+        },
         "list_groups": {
             "input_token": "next_token",
             "limit_key": "max_results",
@@ -264,6 +270,19 @@ class IdentityStoreBackend(BaseBackend):
             if m["GroupId"] == group_id
         ]
 
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore
+    def list_group_memberships_for_member(
+        self, identity_store_id: str, member_id: Dict[str, str]
+    ) -> List[Any]:
+        identity_store = self.__get_identity_store(identity_store_id)
+        user_id = member_id["UserId"]
+
+        return [
+            m
+            for m in identity_store.group_memberships.values()
+            if m["MemberId"]["UserId"] == user_id
+        ]
+
     @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore
     def list_groups(
         self, identity_store_id: str, filters: List[Dict[str, str]]
diff --git a/moto/identitystore/responses.py b/moto/identitystore/responses.py
index a54dfd66e..6c76e040b 100644
--- a/moto/identitystore/responses.py
+++ b/moto/identitystore/responses.py
@@ -137,6 +137,25 @@ class IdentityStoreResponse(BaseResponse):
             dict(GroupMemberships=group_memberships, NextToken=next_token)
         )
 
+    def list_group_memberships_for_member(self) -> str:
+        identity_store_id = self._get_param("IdentityStoreId")
+        member_id = self._get_param("MemberId")
+        max_results = self._get_param("MaxResults")
+        next_token = self._get_param("NextToken")
+        (
+            group_memberships,
+            next_token,
+        ) = self.identitystore_backend.list_group_memberships_for_member(
+            identity_store_id=identity_store_id,
+            member_id=member_id,
+            max_results=max_results,
+            next_token=next_token,
+        )
+
+        return json.dumps(
+            dict(GroupMemberships=group_memberships, NextToken=next_token)
+        )
+
     def list_groups(self) -> str:
         identity_store_id = self._get_param("IdentityStoreId")
         max_results = self._get_param("MaxResults")
diff --git a/moto/ssoadmin/models.py b/moto/ssoadmin/models.py
index 3bb69e19a..4a5097525 100644
--- a/moto/ssoadmin/models.py
+++ b/moto/ssoadmin/models.py
@@ -180,6 +180,33 @@ class SSOAdminBackend(BaseBackend):
                 )
         return account_assignments
 
+    @paginate(PAGINATION_MODEL)  # type: ignore[misc]
+    def list_account_assignments_for_principal(
+        self,
+        filter_: Dict[str, Any],
+        instance_arn: str,
+        principal_id: str,
+        principal_type: str,
+    ) -> List[Dict[str, Any]]:
+        return [
+            {
+                "AccountId": account_assignment.target_id,
+                "PermissionSetArn": account_assignment.permission_set_arn,
+                "PrincipalId": account_assignment.principal_id,
+                "PrincipalType": account_assignment.principal_type,
+            }
+            for account_assignment in self.account_assignments
+            if all(
+                [
+                    filter_.get("AccountId", account_assignment.target_id)
+                    == account_assignment.target_id,
+                    principal_id == account_assignment.principal_id,
+                    principal_type == account_assignment.principal_type,
+                    instance_arn == account_assignment.instance_arn,
+                ]
+            )
+        ]
+
     def create_permission_set(
         self,
         name: str,
diff --git a/moto/ssoadmin/responses.py b/moto/ssoadmin/responses.py
index 8a684233e..e89ca0e89 100644
--- a/moto/ssoadmin/responses.py
+++ b/moto/ssoadmin/responses.py
@@ -69,6 +69,28 @@ class SSOAdminResponse(BaseResponse):
         )
         return json.dumps({"AccountAssignments": assignments})
 
+    def list_account_assignments_for_principal(self) -> str:
+        filter_ = self._get_param("Filter", {})
+        instance_arn = self._get_param("InstanceArn")
+        max_results = self._get_param("MaxResults")
+        next_token = self._get_param("NextToken")
+        principal_id = self._get_param("PrincipalId")
+        principal_type = self._get_param("PrincipalType")
+
+        (
+            assignments,
+            next_token,
+        ) = self.ssoadmin_backend.list_account_assignments_for_principal(
+            filter_=filter_,
+            instance_arn=instance_arn,
+            max_results=max_results,
+            next_token=next_token,
+            principal_id=principal_id,
+            principal_type=principal_type,
+        )
+
+        return json.dumps(dict(AccountAssignments=assignments, NextToken=next_token))
+
     def create_permission_set(self) -> str:
         name = self._get_param("Name")
         description = self._get_param("Description")
diff --git a/moto/ssoadmin/utils.py b/moto/ssoadmin/utils.py
index 1b684206c..45fc457f2 100644
--- a/moto/ssoadmin/utils.py
+++ b/moto/ssoadmin/utils.py
@@ -6,4 +6,16 @@ PAGINATION_MODEL = {
         "result_key": "PermissionSets",
         "unique_attribute": "permission_set_arn",
     },
+    "list_account_assignments_for_principal": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "result_key": "PermissionSets",
+        "unique_attribute": [
+            "AccountId",
+            "PermissionSetArn",
+            "PrincipalId",
+            "PrincipalType",
+        ],
+    },
 }


Test patch:
diff --git a/tests/test_identitystore/test_identitystore.py b/tests/test_identitystore/test_identitystore.py
index b5ec13b32..09d8b518f 100644
--- a/tests/test_identitystore/test_identitystore.py
+++ b/tests/test_identitystore/test_identitystore.py
@@ -560,6 +560,61 @@ def test_list_group_memberships():
             next_token = list_response["NextToken"]
 
 
+@mock_identitystore
+def test_list_group_memberships_for_member():
+    client = boto3.client("identitystore", region_name="us-east-2")
+    identity_store_id = get_identity_store_id()
+
+    start = 0
+    end = 5000
+    batch_size = 321
+    next_token = None
+    membership_ids = []
+
+    user_id = __create_and_verify_sparse_user(client, identity_store_id)["UserId"]
+    for i in range(end):
+        group_id = client.create_group(
+            IdentityStoreId=identity_store_id,
+            DisplayName=f"test_group_{i}",
+            Description="description",
+        )["GroupId"]
+        create_response = client.create_group_membership(
+            IdentityStoreId=identity_store_id,
+            GroupId=group_id,
+            MemberId={"UserId": user_id},
+        )
+        membership_ids.append((create_response["MembershipId"], user_id))
+
+    for iteration in range(start, end, batch_size):
+        last_iteration = end - iteration <= batch_size
+        expected_size = batch_size if not last_iteration else end - iteration
+        end_index = iteration + expected_size
+
+        if next_token is not None:
+            list_response = client.list_group_memberships_for_member(
+                IdentityStoreId=identity_store_id,
+                MemberId={"UserId": user_id},
+                MaxResults=batch_size,
+                NextToken=next_token,
+            )
+        else:
+            list_response = client.list_group_memberships_for_member(
+                IdentityStoreId=identity_store_id,
+                MemberId={"UserId": user_id},
+                MaxResults=batch_size,
+            )
+
+        assert len(list_response["GroupMemberships"]) == expected_size
+        __check_membership_list_values(
+            list_response["GroupMemberships"], membership_ids[iteration:end_index]
+        )
+        if last_iteration:
+            assert "NextToken" not in list_response
+        else:
+            assert "NextToken" in list_response
+            next_token = list_response["NextToken"]
+
+
 def __check_membership_list_values(members, expected):
     assert len(members) == len(expected)
     for i in range(len(expected)):
diff --git a/tests/test_ssoadmin/test_ssoadmin.py b/tests/test_ssoadmin/test_ssoadmin.py
index 1dddae081..ab1e88a17 100644
--- a/tests/test_ssoadmin/test_ssoadmin.py
+++ b/tests/test_ssoadmin/test_ssoadmin.py
@@ -9,6 +9,10 @@ from moto import mock_ssoadmin
 
 # See our Development Tips on writing tests for hints on how to write good tests:
 # http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
+DUMMY_PERMISSIONSET_ID = (
+    "arn:aws:sso:::permissionSet/ins-eeeeffffgggghhhh/ps-hhhhkkkkppppoooo"
+)
+DUMMY_INSTANCE_ARN = "arn:aws:sso:::instance/ins-aaaabbbbccccdddd"
 
 
 @mock_ssoadmin
@@ -186,6 +190,140 @@ def test_list_account_assignments():
     ]
 
 
+@mock_ssoadmin
+def test_list_account_assignments_for_principal():
+    client = boto3.client("sso-admin", region_name="us-west-2")
+
+    id_1 = str(uuid4())
+    id_2 = str(uuid4())
+
+    dummy_account_assignments = [
+        {
+            "InstanceArn": DUMMY_INSTANCE_ARN,
+            "TargetId": "111111111111",
+            "TargetType": "AWS_ACCOUNT",
+            "PermissionSetArn": DUMMY_PERMISSIONSET_ID,
+            "PrincipalType": "USER",
+            "PrincipalId": id_1,
+        },
+        {
+            "InstanceArn": DUMMY_INSTANCE_ARN,
+            "TargetId": "222222222222",
+            "TargetType": "AWS_ACCOUNT",
+            "PermissionSetArn": DUMMY_PERMISSIONSET_ID,
+            "PrincipalType": "USER",
+            "PrincipalId": id_2,
+        },
+        {
+            "InstanceArn": DUMMY_INSTANCE_ARN,
+            "TargetId": "333333333333",
+            "TargetType": "AWS_ACCOUNT",
+            "PermissionSetArn": DUMMY_PERMISSIONSET_ID,
+            "PrincipalType": "USER",
+            "PrincipalId": id_2,
+        },
+        {
+            "InstanceArn": DUMMY_INSTANCE_ARN,
+            "TargetId": "222222222222",
+            "TargetType": "AWS_ACCOUNT",
+            "PermissionSetArn": DUMMY_PERMISSIONSET_ID,
+            "PrincipalType": "GROUP",
+            "PrincipalId": id_2,
+        },
+    ]
+
+    # create the account assignments from above
+    for dummy_account_assignment in dummy_account_assignments:
+        client.create_account_assignment(**dummy_account_assignment)
+
+    # check user 1 assignments in all accounts
+    response = client.list_account_assignments_for_principal(
+        InstanceArn=DUMMY_INSTANCE_ARN, PrincipalId=id_1, PrincipalType="USER"
+    )
+    assert len(response["AccountAssignments"]) == 1
+    assert response["AccountAssignments"][0]["PrincipalId"] == id_1
+
+    # check user 2 in a single account
+    response = client.list_account_assignments_for_principal(
+        Filter={"AccountId": "222222222222"},
+        InstanceArn=DUMMY_INSTANCE_ARN,
+        PrincipalId=id_2,
+        PrincipalType="USER",
+    )
+    assert len(response["AccountAssignments"]) == 1
+    assert response["AccountAssignments"][0]["PrincipalId"] == id_2
+    assert response["AccountAssignments"][0]["AccountId"] == "222222222222"
+
+    # check group with id 2 is only returned
+    response = client.list_account_assignments_for_principal(
+        InstanceArn=DUMMY_INSTANCE_ARN,
+        PrincipalId=id_2,
+        PrincipalType="GROUP",
+    )
+    assert len(response["AccountAssignments"]) == 1
+    assert response["AccountAssignments"][0]["PrincipalId"] == id_2
+
+    # check empty response
+    response = client.list_account_assignments_for_principal(
+        InstanceArn=DUMMY_INSTANCE_ARN,
+        PrincipalId=str(uuid4()),
+        PrincipalType="USER",
+    )
+
+    assert len(response["AccountAssignments"]) == 0
+
+
+@mock_ssoadmin
+def test_list_account_assignments_for_principal_pagination():
+    client = boto3.client("sso-admin", region_name="us-east-2")
+
+    user_id = str(uuid4())
+
+    dummy_account_assignments = []
+    for x in range(3):
+        dummy_account_assignments.append(
+            {
+                "InstanceArn": DUMMY_INSTANCE_ARN,
+                "TargetId": str(x) * 12,
+                "TargetType": "AWS_ACCOUNT",
+                "PermissionSetArn": DUMMY_PERMISSIONSET_ID,
+                "PrincipalType": "USER",
+                "PrincipalId": user_id,
+            },
+        )
+
+    for dummy_account_assignment in dummy_account_assignments:
+        client.create_account_assignment(**dummy_account_assignment)
+
+    account_assignments = []
+
+    response = client.list_account_assignments_for_principal(
+        InstanceArn=DUMMY_INSTANCE_ARN,
+        PrincipalId=user_id,
+        PrincipalType="USER",
+        MaxResults=2,
+    )
+
+    assert len(response["AccountAssignments"]) == 2
+    account_assignments.extend(response["AccountAssignments"])
+    next_token = response["NextToken"]
+
+    response = client.list_account_assignments_for_principal(
+        InstanceArn=DUMMY_INSTANCE_ARN,
+        PrincipalId=user_id,
+        PrincipalType="USER",
+        MaxResults=2,
+        NextToken=next_token,
+    )
+
+    assert len(response["AccountAssignments"]) == 1
+    account_assignments.extend(response["AccountAssignments"])
+
+    assert set(
+        [account_assignment["AccountId"] for account_assignment in account_assignments]
+    ) == set(["000000000000", "111111111111", "222222222222"])
+
+
 @mock_ssoadmin
 def test_create_permission_set():
     client = boto3.client("sso-admin", region_name="ap-southeast-1")