diff --git a/gcpdiag/queries/iam.py b/gcpdiag/queries/iam.py index 4fd14dd33..f3ae29ec4 100644 --- a/gcpdiag/queries/iam.py +++ b/gcpdiag/queries/iam.py @@ -156,6 +156,41 @@ def _get_iam_role(name: str, default_project_id: str) -> Role: return parent_roles[name] +def _get_group_members(group_email: str, + project_id: str, + processed_groups: Optional[set] = None) -> List[str]: + """Retrieve members of the IAM group, including nested groups.""" + + if processed_groups: + if group_email in processed_groups: + return [] + + else: + processed_groups = set() + + processed_groups.add(group_email) + + service = apis.get_api('cloudidentity', 'v1', project_id) + group_name = f'groups/{group_email}' + members = [] + + request = service.groups().memberships().list(parent=group_name) + while request: + response = request.execute() + for membership in response.get('memberships', []): + member_id = membership['memberKey']['id'] + members.append(member_id) + # Check if the member is a group and resolve its members recursively + if member_id.startswith('group:'): + nested_group_members = _get_group_members( + member_id.split(':')[1], project_id, processed_groups) + members.extend(nested_group_members) + request = service.groups().memberships().list_next( + previous_request=request, previous_response=response) + + return members + + class BaseIAMPolicy(models.Resource): """Common class for IAM policies""" @@ -219,6 +254,13 @@ def _expand_policy(self, resource_data: Dict[str, Any]) -> Dict[str, Any]: for member in binding['members']: member_policy = policy_by_member[member] member_policy.setdefault('roles', set()).add(binding['role']) + # Check if the member is a group and expand its members + if member.startswith('group:'): + group_members = _get_group_members( + member.split(':')[1], self.project_id) + for group_member in group_members: + group_member_policy = policy_by_member[group_member] + group_member_policy.setdefault('roles', set()).add(binding['role']) # Populate cache for IAM roles used in the policy # Unlike `has_role_permissions` this part will be executed inside