Repository: cartography-cncf/cartography (Python)

Issue:
AWS sso:ListAccountAssignmentsForPrincipal crashes on child accounts with AccessDeniedException
*Description*:
> What issue is being seen?  Describe what should be happening instead of the bug, for example: Cartography should not crash, the expected value isn't returned, the data schema is wrong, etc.

Follow up of https://github.com/cartography-cncf/cartography/issues/1415. Playing a bit of whack-a-mole here.


*To Reproduce*:
> Steps to reproduce the behavior.  Provide all data and inputs required to reproduce the issue.

Same as https://github.com/cartography-cncf/cartography/issues/1415 but use 0.97.0 instead.

*Logs*:
> If applicable, copy and paste your console log with the failing stack trace.

```
INFO:cartography.intel.aws.identitycenter:Loading 0 permission sets for instance arn:aws:sso:::instance/ssoins-ABC in region us-east-1
INFO:cartography.intel.aws.identitycenter:Loading 2 SSO users for identity store d-DEF in region us-east-1
INFO:cartography.intel.aws.identitycenter:Getting role assignments for 2 users
WARNING:cartography.intel.aws:Caught exception syncing account 1234. aws-best-effort-mode is on so we are continuing on to the next AWS account. All exceptions will be aggregated and re-logged at the end of the sync.
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/cartography/intel/aws/__init__.py", line 169, in _sync_multiple_accounts
    _sync_one_account(
  File "/usr/local/lib/python3.10/site-packages/cartography/intel/aws/__init__.py", line 64, in _sync_one_account
    RESOURCE_FUNCTIONS[func_name](**sync_args)
  File "/usr/local/lib/python3.10/site-packages/cartography/util.py", line 198, in timed
    return method(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/cartography/intel/aws/identitycenter.py", line 296, in sync_identity_center_instances
    role_assignments = get_role_assignments(
  File "/usr/local/lib/python3.10/site-packages/cartography/util.py", line 198, in timed
    return method(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/cartography/intel/aws/identitycenter.py", line 196, in get_role_assignments
    for page in paginator.paginate(InstanceArn=instance_arn, PrincipalId=user_id, PrincipalType='USER'):
  File "/usr/local/lib/python3.10/site-packages/botocore/paginate.py", line 269, in __iter__
    response = self._make_request(current_kwargs)
  File "/usr/local/lib/python3.10/site-packages/botocore/paginate.py", line 357, in _make_request
    return self._method(**current_kwargs)
  File "/usr/local/lib/python3.10/site-packages/botocore/client.py", line 569, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/usr/local/lib/python3.10/site-packages/botocore/client.py", line 1023, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.AccessDeniedException: An error occurred (AccessDeniedException) when calling the ListAccountAssignmentsForPrincipal operation: User: arn:aws:sts::1234:assumed-role/cartography-read-only/botocore-session-1735333435 is not authorized to perform: sso:ListAccountAssignmentsForPrincipal on resource: arn:aws:sso:::instance/ssoins-ABC because no resource-based policy allows the sso:ListAccountAssignmentsForPrincipal action
```

*Please complete the following information:*:
>  - Cartography release version or commit hash [e.g. 0.12.0 or 95e8e11913e2a44a4d4682506d8364a638ceac69]
0.97.0



Discussion:

Patch:
diff --git a/cartography/intel/aws/identitycenter.py b/cartography/intel/aws/identitycenter.py
index 651d55cfb2..022c686d4d 100644
--- a/cartography/intel/aws/identitycenter.py
+++ b/cartography/intel/aws/identitycenter.py
@@ -82,26 +82,6 @@ def get_permission_sets(boto3_session: boto3.session.Session, instance_arn: str,
     return permission_sets
 
 
-@timeit
-def get_permission_set_roles(
-    boto3_session: boto3.session.Session,
-    instance_arn: str,
-    permission_set_arn: str,
-    region: str,
-) -> List[Dict]:
-    """
-    Get all accounts associated with a given permission set
-    """
-    client = boto3_session.client('sso-admin', region_name=region)
-    accounts = []
-
-    paginator = client.get_paginator('list_accounts_for_provisioned_permission_set')
-    for page in paginator.paginate(InstanceArn=instance_arn, PermissionSetArn=permission_set_arn):
-        accounts.extend(page.get('AccountIds', []))
-
-    return accounts
-
-
 @timeit
 def load_permission_sets(
     neo4j_session: neo4j.Session,
@@ -128,6 +108,7 @@ def load_permission_sets(
 
 
 @timeit
+@aws_handle_regions
 def get_sso_users(
     boto3_session: boto3.session.Session,
     identity_store_id: str,
@@ -176,6 +157,7 @@ def load_sso_users(
 
 
 @timeit
+@aws_handle_regions
 def get_role_assignments(
     boto3_session: boto3.session.Session,
     users: List[Dict],
@@ -231,6 +213,7 @@ def load_role_assignments(
         )
 
 
+@timeit
 def cleanup(neo4j_session: neo4j.Session, common_job_parameters: Dict[str, Any]) -> None:
     GraphJob.from_node_schema(AWSIdentityCenterInstanceSchema(), common_job_parameters).run(neo4j_session)
     GraphJob.from_node_schema(AWSPermissionSetSchema(), common_job_parameters).run(neo4j_session)
diff --git a/setup.py b/setup.py
index 4ead049024..00bbf0f4e3 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,7 @@
 from setuptools import find_packages
 from setuptools import setup
 
-__version__ = '0.97.0'
+__version__ = '0.97.1'
 
 
 setup(


Test patch:
diff --git a/tests/unit/cartography/intel/aws/test_identitycenter.py b/tests/unit/cartography/intel/aws/test_identitycenter.py
index 69ea4fb1c6..b6e019d09f 100644
--- a/tests/unit/cartography/intel/aws/test_identitycenter.py
+++ b/tests/unit/cartography/intel/aws/test_identitycenter.py
@@ -3,6 +3,7 @@
 import botocore.exceptions
 
 from cartography.intel.aws.identitycenter import get_permission_sets
+from cartography.intel.aws.identitycenter import get_role_assignments
 
 
 def test_get_permission_sets_access_denied():
@@ -30,3 +31,36 @@ def test_get_permission_sets_access_denied():
     mock_session.client.assert_called_once_with('sso-admin', region_name='us-east-1')
     mock_client.get_paginator.assert_called_once_with('list_permission_sets')
     mock_paginator.paginate.assert_called_once_with(InstanceArn="arn:aws:sso:::instance/test")
+
+
+def test_get_role_assignments_access_denied():
+    # Ensure we gracefully handle access denied exceptions for identity center.
+    mock_session = MagicMock()
+    mock_client = MagicMock()
+    mock_paginator = MagicMock()
+    users = [{"UserId": "test-user-id"}]
+
+    # Arrange: Set up the mock chain
+    mock_session.client.return_value = mock_client
+    mock_client.get_paginator.return_value = mock_paginator
+
+    # Make paginate raise AccessDeniedException (simulate issue #1415)
+    mock_paginator.paginate.side_effect = botocore.exceptions.ClientError(
+        error_response={'Error': {'Code': 'AccessDeniedException', 'Message': 'Access Denied'}},
+        operation_name='ListAccountAssignmentsForPrincipal',
+    )
+
+    # Act: Call the function
+    result = get_role_assignments(mock_session, users, "arn:aws:sso:::instance/test", "us-east-1")
+
+    # Assert:Verify we got an empty list
+    assert result == []
+
+    # Verify our mocks were called as expected
+    mock_session.client.assert_called_once_with('sso-admin', region_name='us-east-1')
+    mock_client.get_paginator.assert_called_once_with('list_account_assignments_for_principal')
+    mock_paginator.paginate.assert_called_once_with(
+        InstanceArn="arn:aws:sso:::instance/test",
+        PrincipalId="test-user-id",
+        PrincipalType="USER",
+    )