feat(googleworkspace): add directory check for CIS 1.1.3 - super admin only admin roles (#10488)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
lydiavilchez
2026-04-08 12:05:15 +02:00
committed by GitHub
parent 1d43885230
commit 72e8f09c07
12 changed files with 930 additions and 18 deletions

View File

@@ -17,6 +17,7 @@ Prowler requests the following read-only OAuth 2.0 scopes from the Google Worksp
| `https://www.googleapis.com/auth/admin.directory.user.readonly` | Read access to user accounts and their admin status |
| `https://www.googleapis.com/auth/admin.directory.domain.readonly` | Read access to domain information |
| `https://www.googleapis.com/auth/admin.directory.customer.readonly` | Read access to customer information (Customer ID) |
| `https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly` | Read access to admin roles and role assignments |
<Warning>
The delegated user must be a **super administrator** in your Google Workspace organization. Using a non-admin account will result in permission errors when accessing the Admin SDK.
@@ -73,7 +74,7 @@ This JSON key grants access to your Google Workspace organization. Never commit
6. In the **OAuth scopes** field, enter the following scopes as a comma-separated list:
```
https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/admin.directory.domain.readonly,https://www.googleapis.com/auth/admin.directory.customer.readonly
https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/admin.directory.domain.readonly,https://www.googleapis.com/auth/admin.directory.customer.readonly,https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly
```
7. Click **Authorize**
@@ -114,7 +115,7 @@ The delegated user must be provided via the `GOOGLEWORKSPACE_DELEGATED_USER` env
- **Use environment variables** — Never hardcode credentials in scripts or commands
- **Use a dedicated Service Account** — Create one specifically for Prowler, separate from other integrations
- **Use read-only scopes** — Prowler only requires the three read-only scopes listed above
- **Use read-only scopes** — Prowler only requires the read-only scopes listed above
- **Restrict key access** — Set file permissions to `600` on the JSON key file
- **Rotate keys regularly** — Delete and regenerate the JSON key periodically
- **Use a least-privilege super admin** — Consider using a dedicated super admin account for Prowler's delegated user rather than a personal admin account
@@ -151,7 +152,7 @@ python3 -c "import json; json.load(open('/path/to/key.json'))" && echo "Valid JS
The Service Account cannot impersonate the delegated user. This usually means Domain-Wide Delegation has not been configured, or the OAuth scopes are incorrect. Verify:
- The Service Account Client ID is correctly entered in the Admin Console
- All three required OAuth scopes are included
- All required OAuth scopes are included
- The delegated user is a super administrator
### Permission Denied on Admin SDK Calls
@@ -159,5 +160,5 @@ The Service Account cannot impersonate the delegated user. This usually means Do
If Prowler connects but returns empty results or permission errors for specific API calls:
- Confirm Domain-Wide Delegation is fully propagated (wait a few minutes after setup)
- Verify all three scopes are authorized in the Admin Console
- Verify all scopes are authorized in the Admin Console
- Ensure the delegated user is an active super administrator

View File

@@ -78,7 +78,7 @@ The Service Account JSON is the full content of the key file downloaded when cre
![Check Connection](/images/providers/googleworkspace-check-connection.png)
<Note>
If the connection test fails, verify that Domain-Wide Delegation is properly configured and that all three OAuth scopes are authorized. It may take a few minutes for delegation changes to propagate. See the [Troubleshooting](/user-guide/providers/googleworkspace/authentication#troubleshooting) section for common errors.
If the connection test fails, verify that Domain-Wide Delegation is properly configured and that all required OAuth scopes are authorized. It may take a few minutes for delegation changes to propagate. See the [Troubleshooting](/user-guide/providers/googleworkspace/authentication#troubleshooting) section for common errors.
</Note>
### Step 5: Launch the Scan

View File

@@ -11,6 +11,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `glue_etl_jobs_no_secrets_in_arguments` check for plaintext secrets in AWS Glue ETL job arguments [(#10368)](https://github.com/prowler-cloud/prowler/pull/10368)
- `awslambda_function_no_dead_letter_queue`, `awslambda_function_using_cross_account_layers`, and `awslambda_function_env_vars_not_encrypted_with_cmk` checks for AWS Lambda [(#10381)](https://github.com/prowler-cloud/prowler/pull/10381)
- `entra_conditional_access_policy_mdm_compliant_device_required` check for M365 provider [(#10220)](https://github.com/prowler-cloud/prowler/pull/10220)
- `directory_super_admin_only_admin_roles` check for Google Workspace provider [(#10488)](https://github.com/prowler-cloud/prowler/pull/10488)
- `ec2_securitygroup_allow_ingress_from_internet_to_any_port_from_ip` check for AWS provider using `ipaddress.is_global` for accurate public IP detection [(#10335)](https://github.com/prowler-cloud/prowler/pull/10335)
- `entra_conditional_access_policy_block_o365_elevated_insider_risk` check for M365 provider [(#10232)](https://github.com/prowler-cloud/prowler/pull/10232)
- `--resource-group` and `--list-resource-groups` CLI flags to filter checks by resource group across all providers [(#10479)](https://github.com/prowler-cloud/prowler/pull/10479)

View File

@@ -54,7 +54,9 @@
{
"Id": "1.1.3",
"Description": "Ensure super admin accounts are used only for super admin activities",
"Checks": [],
"Checks": [
"directory_super_admin_only_admin_roles"
],
"Attributes": [
{
"Section": "1 Directory",

View File

@@ -64,6 +64,7 @@ class GoogleworkspaceProvider(Provider):
"https://www.googleapis.com/auth/admin.directory.user.readonly",
"https://www.googleapis.com/auth/admin.directory.domain.readonly",
"https://www.googleapis.com/auth/admin.directory.customer.readonly",
"https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly",
]
def __init__(

View File

@@ -8,23 +8,21 @@ class Directory(GoogleWorkspaceService):
def __init__(self, provider):
super().__init__(provider)
self._service = self._build_service("admin", "directory_v1")
self.users = self._list_users()
self._roles = self._list_roles()
self._populate_role_assignments()
def _list_users(self):
logger.info("Directory - Listing Users...")
users = {}
try:
# Build the Admin SDK Directory service
service = self._build_service("admin", "directory_v1")
if not service:
if not self._service:
logger.error("Failed to build Directory service")
return users
# Fetch users using the Directory API
# Reference: https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/list
request = service.users().list(
request = self._service.users().list(
customer=self.provider.identity.customer_id,
maxResults=500, # Max allowed by API
orderBy="email",
@@ -38,14 +36,11 @@ class Directory(GoogleWorkspaceService):
user = User(
id=user_data.get("id"),
email=user_data.get("primaryEmail"),
is_admin=user_data.get("isAdmin", False),
)
users[user.id] = user
logger.debug(
f"Processed user: {user.email} (Admin: {user.is_admin})"
)
logger.debug(f"Processed user: {user.email}")
request = service.users().list_next(request, response)
request = self._service.users().list_next(request, response)
except Exception as error:
self._handle_api_error(
@@ -62,9 +57,108 @@ class Directory(GoogleWorkspaceService):
return users
def _list_roles(self):
logger.info("Directory - Listing Roles...")
roles = {}
try:
if not self._service:
return roles
request = self._service.roles().list(
customer=self.provider.identity.customer_id,
)
while request is not None:
try:
response = request.execute()
for role_data in response.get("items", []):
role_id = str(role_data.get("roleId", ""))
role_name = role_data.get("roleName", "")
if role_id and role_name:
roles[role_id] = Role(
id=role_id,
name=role_name,
description=role_data.get("roleDescription", ""),
is_super_admin_role=role_data.get(
"isSuperAdminRole", False
),
)
request = self._service.roles().list_next(request, response)
except Exception as error:
self._handle_api_error(
error,
"listing roles",
self.provider.identity.customer_id,
)
break
logger.info(f"Found {len(roles)} roles in the domain")
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return roles
def _populate_role_assignments(self):
logger.info("Directory - Fetching Role Assignments...")
if not self._service:
return
try:
request = self._service.roleAssignments().list(
customer=self.provider.identity.customer_id,
)
while request is not None:
try:
response = request.execute()
for assignment in response.get("items", []):
user_id = str(assignment.get("assignedTo", ""))
role_id = str(assignment.get("roleId", ""))
user = self.users.get(user_id)
role = self._roles.get(role_id)
if user and role:
user.role_assignments.append(role)
if role.is_super_admin_role:
user.is_admin = True
request = self._service.roleAssignments().list_next(
request, response
)
except Exception as error:
self._handle_api_error(
error,
"listing role assignments",
self.provider.identity.customer_id,
)
break
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
class Role(BaseModel):
id: str
name: str
description: str = ""
is_super_admin_role: bool = False
class User(BaseModel):
id: str
email: str
is_admin: bool = False
role_assignments: list[Role] = []

View File

@@ -0,0 +1,39 @@
{
"Provider": "googleworkspace",
"CheckID": "directory_super_admin_only_admin_roles",
"CheckTitle": "All super admin accounts are used only for super admin activities",
"CheckType": [],
"ServiceName": "directory",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "NotDefined",
"ResourceGroup": "IAM",
"Description": "Super admin accounts do not also hold **additional admin roles** such as Groups Admin, User Management Admin, etc. Each super administrator has a separate, non-admin account for daily activities, following the **principle of least privilege**.",
"Risk": "A super admin account that also holds additional admin roles increases the **attack surface** for phishing and credential theft. Compromising a single dual-role account grants full administrative access, bypassing **separation of duties** and enabling unauthorized changes to users, billing, and security settings.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://knowledge.workspace.google.com/admin/users/prebuilt-administrator-roles",
"https://support.google.com/a/answer/9011373"
],
"Remediation": {
"Code": {
"CLI": "",
"NativeIaC": "",
"Other": "1. Sign in to the Google **Admin console** at https://admin.google.com\n2. Navigate to **Directory** > **Users**\n3. Click on the super admin user who also has additional admin roles\n4. Click **Admin roles and privileges**\n5. Remove the additional admin roles from the super admin account\n6. Create a separate account for daily admin tasks",
"Terraform": ""
},
"Recommendation": {
"Text": "Apply the principle of separation of duties by maintaining dedicated super admin accounts exclusively for privileged tasks. Daily administrative activities should be performed from separate accounts with only the delegated roles required.",
"Url": "https://hub.prowler.com/check/directory_super_admin_only_admin_roles"
}
},
"Categories": [
"identity-access"
],
"DependsOn": [],
"RelatedTo": [
"directory_super_admin_count"
],
"Notes": ""
}

View File

@@ -0,0 +1,60 @@
from typing import List
from prowler.lib.check.models import Check, CheckReportGoogleWorkspace
from prowler.providers.googleworkspace.services.directory.directory_client import (
directory_client,
)
class directory_super_admin_only_admin_roles(Check):
"""Check that super admin accounts are used only for super admin activities
This check verifies that no super admin user has additional admin roles assigned
beyond the Super Admin role. Super admins should have separate accounts for daily
activities to follow least privilege.
"""
def execute(self) -> List[CheckReportGoogleWorkspace]:
findings = []
if directory_client.users:
dual_role_admins = {}
for user in directory_client.users.values():
if user.is_admin:
extra_roles = [
r.description or r.name
for r in user.role_assignments
if not r.is_super_admin_role
]
if extra_roles:
dual_role_admins[user.email] = extra_roles
report = CheckReportGoogleWorkspace(
metadata=self.metadata(),
resource=directory_client.provider.identity,
resource_name=directory_client.provider.identity.domain,
resource_id=directory_client.provider.identity.customer_id,
customer_id=directory_client.provider.identity.customer_id,
location="global",
)
if dual_role_admins:
details = ", ".join(
f"{email} ({', '.join(roles)})"
for email, roles in dual_role_admins.items()
)
report.status = "FAIL"
report.status_extended = (
f"Super admin accounts also holding additional admin roles: {details}. "
f"Super admin accounts should be used only for super admin activities."
)
else:
report.status = "PASS"
report.status_extended = (
f"All super admin accounts in domain {directory_client.provider.identity.domain} "
f"are used only for super admin activities."
)
findings.append(report)
return findings

View File

@@ -43,6 +43,39 @@ USER_3 = {
}
# Role data for Directory API role tests
SUPER_ADMIN_ROLE_ID = "13801188331880449"
SEED_ADMIN_ROLE_ID = "13801188331880451"
GROUPS_ADMIN_ROLE_ID = "13801188331880450"
ROLE_SUPER_ADMIN = {
"roleId": SUPER_ADMIN_ROLE_ID,
"roleName": "Super Admin",
"roleDescription": "Super Admin",
"isSystemRole": True,
"isSuperAdminRole": True,
}
# Google automatically assigns _SEED_ADMIN_ROLE to the first account that
# created the domain. It is a super-admin-capable system role with a
# different name, so it must also be excluded when counting "extra" roles.
ROLE_SEED_ADMIN = {
"roleId": SEED_ADMIN_ROLE_ID,
"roleName": "_SEED_ADMIN_ROLE",
"roleDescription": "Super Admin",
"isSystemRole": True,
"isSuperAdminRole": True,
}
ROLE_GROUPS_ADMIN = {
"roleId": GROUPS_ADMIN_ROLE_ID,
"roleName": "_GROUPS_ADMIN_ROLE",
"roleDescription": "Groups Administrator",
"isSystemRole": True,
"isSuperAdminRole": False,
}
def set_mocked_googleworkspace_provider(
identity: GoogleWorkspaceIdentityInfo = GoogleWorkspaceIdentityInfo(
domain=DOMAIN,

View File

@@ -1,6 +1,12 @@
from unittest.mock import MagicMock, patch
from tests.providers.googleworkspace.googleworkspace_fixtures import (
GROUPS_ADMIN_ROLE_ID,
ROLE_GROUPS_ADMIN,
ROLE_SEED_ADMIN,
ROLE_SUPER_ADMIN,
SEED_ADMIN_ROLE_ID,
SUPER_ADMIN_ROLE_ID,
USER_1,
USER_2,
USER_3,
@@ -25,6 +31,24 @@ class TestDirectoryService:
mock_service.users().list.return_value = mock_users_list
mock_service.users().list_next.return_value = None
# Mock roles response
mock_roles_list = MagicMock()
mock_roles_list.execute.return_value = {
"items": [ROLE_SUPER_ADMIN, ROLE_GROUPS_ADMIN]
}
mock_service.roles().list.return_value = mock_roles_list
mock_service.roles().list_next.return_value = None
mock_ra = MagicMock()
mock_ra.execute.return_value = {
"items": [
{"assignedTo": "user1-id", "roleId": SUPER_ADMIN_ROLE_ID},
{"assignedTo": "user2-id", "roleId": SUPER_ADMIN_ROLE_ID},
]
}
mock_service.roleAssignments().list.return_value = mock_ra
mock_service.roleAssignments().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
@@ -67,6 +91,17 @@ class TestDirectoryService:
mock_service.users().list.return_value = mock_users_list
mock_service.users().list_next.return_value = None
# Mock roles response
mock_roles_list = MagicMock()
mock_roles_list.execute.return_value = {"items": []}
mock_service.roles().list.return_value = mock_roles_list
mock_service.roles().list_next.return_value = None
mock_ra = MagicMock()
mock_ra.execute.return_value = {"items": []}
mock_service.roleAssignments().list.return_value = mock_ra
mock_service.roleAssignments().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
@@ -97,6 +132,16 @@ class TestDirectoryService:
mock_service = MagicMock()
mock_service.users().list.side_effect = Exception("API Error")
mock_roles_list = MagicMock()
mock_roles_list.execute.return_value = {"items": []}
mock_service.roles().list.return_value = mock_roles_list
mock_service.roles().list_next.return_value = None
mock_ra = MagicMock()
mock_ra.execute.return_value = {"items": []}
mock_service.roleAssignments().list.return_value = mock_ra
mock_service.roleAssignments().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
@@ -130,3 +175,193 @@ class TestDirectoryService:
assert user.id == "test-id"
assert user.email == "test@test-company.com"
assert user.is_admin is True
assert user.role_assignments == []
def test_directory_list_roles(self):
"""Test that _list_roles correctly builds a roleId-to-roleName mapping"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
# Mock empty users
mock_users_list = MagicMock()
mock_users_list.execute.return_value = {"users": []}
mock_service.users().list.return_value = mock_users_list
mock_service.users().list_next.return_value = None
# Mock roles response
mock_roles_list = MagicMock()
mock_roles_list.execute.return_value = {
"items": [ROLE_SUPER_ADMIN, ROLE_GROUPS_ADMIN]
}
mock_service.roles().list.return_value = mock_roles_list
mock_service.roles().list_next.return_value = None
mock_ra = MagicMock()
mock_ra.execute.return_value = {"items": []}
mock_service.roleAssignments().list.return_value = mock_ra
mock_service.roleAssignments().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.directory.directory_service import (
Directory,
)
directory = Directory(mock_provider)
super_admin_role = directory._roles[SUPER_ADMIN_ROLE_ID]
assert super_admin_role.name == "Super Admin"
assert super_admin_role.description == "Super Admin"
assert super_admin_role.is_super_admin_role is True
groups_admin_role = directory._roles[GROUPS_ADMIN_ROLE_ID]
assert groups_admin_role.name == "_GROUPS_ADMIN_ROLE"
assert groups_admin_role.description == "Groups Administrator"
assert groups_admin_role.is_super_admin_role is False
def test_directory_role_assignments_populated(self):
"""Test that role assignments are fetched and resolved for super admins"""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
# Mock users - one super admin
mock_users_list = MagicMock()
mock_users_list.execute.return_value = {"users": [USER_1]}
mock_service.users().list.return_value = mock_users_list
mock_service.users().list_next.return_value = None
# Mock roles
mock_roles_list = MagicMock()
mock_roles_list.execute.return_value = {
"items": [ROLE_SUPER_ADMIN, ROLE_GROUPS_ADMIN]
}
mock_service.roles().list.return_value = mock_roles_list
mock_service.roles().list_next.return_value = None
mock_ra = MagicMock()
mock_ra.execute.return_value = {
"items": [
{"assignedTo": "user1-id", "roleId": SUPER_ADMIN_ROLE_ID},
{"assignedTo": "user1-id", "roleId": GROUPS_ADMIN_ROLE_ID},
]
}
mock_service.roleAssignments().list.return_value = mock_ra
mock_service.roleAssignments().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.directory.directory_service import (
Directory,
)
directory = Directory(mock_provider)
user = directory.users["user1-id"]
role_names = [r.name for r in user.role_assignments]
role_descriptions = [r.description for r in user.role_assignments]
assert "Super Admin" in role_names
assert "_GROUPS_ADMIN_ROLE" in role_names
assert "Groups Administrator" in role_descriptions
assert len(user.role_assignments) == 2
assert user.is_admin is True
def test_directory_second_super_admin_detected_via_role_assignments(self):
"""Regression: a second super admin whose users.list().isAdmin still
reads False (e.g. API propagation lag, or only holding
_SEED_ADMIN_ROLE) must still be recognised as a super admin through
the Role Assignments API, AND any extra non-super-admin roles they
hold must be surfaced on their User object."""
mock_provider = set_mocked_googleworkspace_provider()
mock_provider.audit_config = {}
mock_provider.fixer_config = {}
mock_session = MagicMock()
mock_session.credentials = MagicMock()
mock_provider.session = mock_session
mock_service = MagicMock()
stale_user_1 = {
"id": "user1-id",
"primaryEmail": "admin1@test-company.com",
"isAdmin": False,
}
stale_user_2 = {
"id": "user2-id",
"primaryEmail": "admin2@test-company.com",
"isAdmin": False,
}
mock_users_list = MagicMock()
mock_users_list.execute.return_value = {"users": [stale_user_1, stale_user_2]}
mock_service.users().list.return_value = mock_users_list
mock_service.users().list_next.return_value = None
mock_roles_list = MagicMock()
mock_roles_list.execute.return_value = {
"items": [ROLE_SUPER_ADMIN, ROLE_SEED_ADMIN, ROLE_GROUPS_ADMIN]
}
mock_service.roles().list.return_value = mock_roles_list
mock_service.roles().list_next.return_value = None
mock_ra = MagicMock()
mock_ra.execute.return_value = {
"items": [
{"assignedTo": "user1-id", "roleId": SEED_ADMIN_ROLE_ID},
{"assignedTo": "user2-id", "roleId": SUPER_ADMIN_ROLE_ID},
{"assignedTo": "user2-id", "roleId": GROUPS_ADMIN_ROLE_ID},
]
}
mock_service.roleAssignments().list.return_value = mock_ra
mock_service.roleAssignments().list_next.return_value = None
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_service.GoogleWorkspaceService._build_service",
return_value=mock_service,
),
):
from prowler.providers.googleworkspace.services.directory.directory_service import (
Directory,
)
directory = Directory(mock_provider)
user1 = directory.users["user1-id"]
user2 = directory.users["user2-id"]
assert user1.is_admin is True
assert user2.is_admin is True
assert [r.name for r in user1.role_assignments] == ["_SEED_ADMIN_ROLE"]
user2_role_names = {r.name for r in user2.role_assignments}
assert user2_role_names == {"Super Admin", "_GROUPS_ADMIN_ROLE"}

View File

@@ -0,0 +1,446 @@
from unittest.mock import patch
from prowler.providers.googleworkspace.services.directory.directory_service import (
Role,
User,
)
from tests.providers.googleworkspace.googleworkspace_fixtures import (
CUSTOMER_ID,
DOMAIN,
set_mocked_googleworkspace_provider,
)
SUPER_ADMIN_ROLE = Role(
id="13801188331880449",
name="Super Admin",
description="Super Admin",
is_super_admin_role=True,
)
SEED_ADMIN_ROLE = Role(
id="13801188331880451",
name="_SEED_ADMIN_ROLE",
description="Super Admin",
is_super_admin_role=True,
)
GROUPS_ADMIN_ROLE = Role(
id="13801188331880450",
name="_GROUPS_ADMIN_ROLE",
description="Groups Administrator",
is_super_admin_role=False,
)
USER_MANAGEMENT_ADMIN_ROLE = Role(
id="13801188331880452",
name="_USER_MANAGEMENT_ADMIN_ROLE",
description="User Management Administrator",
is_super_admin_role=False,
)
CUSTOM_ROLE_NO_DESCRIPTION = Role(
id="13801188331880453",
name="custom-helpdesk-role",
description="",
is_super_admin_role=False,
)
class TestDirectorySuperAdminOnlyAdminRoles:
def test_pass_super_admins_only_super_admin_role(self):
"""Test PASS when super admins have only the Super Admin role"""
users = {
"admin1-id": User(
id="admin1-id",
email="admin1@test-company.com",
is_admin=True,
role_assignments=[SUPER_ADMIN_ROLE],
),
"admin2-id": User(
id="admin2-id",
email="admin2@test-company.com",
is_admin=True,
role_assignments=[SUPER_ADMIN_ROLE],
),
"user1-id": User(
id="user1-id",
email="user@test-company.com",
is_admin=False,
role_assignments=[],
),
}
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles.directory_client"
) as mock_directory_client,
):
from prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles import (
directory_super_admin_only_admin_roles,
)
mock_directory_client.users = users
mock_directory_client.provider = mock_provider
check = directory_super_admin_only_admin_roles()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "used only for super admin activities" in findings[0].status_extended
assert findings[0].resource_name == DOMAIN
assert findings[0].customer_id == CUSTOMER_ID
def test_pass_super_admin_with_seed_admin_role(self):
"""Test PASS when a super admin only holds _SEED_ADMIN_ROLE.
_SEED_ADMIN_ROLE is auto-assigned by Google to the original domain
creator and has isSuperAdminRole=True, so it must not count as an
"extra" role.
"""
users = {
"admin1-id": User(
id="admin1-id",
email="playground@prowler.cloud",
is_admin=True,
role_assignments=[SEED_ADMIN_ROLE],
),
}
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles.directory_client"
) as mock_directory_client,
):
from prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles import (
directory_super_admin_only_admin_roles,
)
mock_directory_client.users = users
mock_directory_client.provider = mock_provider
check = directory_super_admin_only_admin_roles()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "_SEED_ADMIN_ROLE" not in findings[0].status_extended
def test_pass_super_admin_with_both_super_admin_and_seed_admin(self):
"""Test PASS when admin holds both Super Admin and _SEED_ADMIN_ROLE"""
users = {
"admin1-id": User(
id="admin1-id",
email="playground@prowler.cloud",
is_admin=True,
role_assignments=[SUPER_ADMIN_ROLE, SEED_ADMIN_ROLE],
),
}
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles.directory_client"
) as mock_directory_client,
):
from prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles import (
directory_super_admin_only_admin_roles,
)
mock_directory_client.users = users
mock_directory_client.provider = mock_provider
check = directory_super_admin_only_admin_roles()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
def test_fail_super_admin_with_additional_roles(self):
"""Test FAIL when a super admin also has additional admin roles"""
users = {
"admin1-id": User(
id="admin1-id",
email="admin1@test-company.com",
is_admin=True,
role_assignments=[SUPER_ADMIN_ROLE, GROUPS_ADMIN_ROLE],
),
"user1-id": User(
id="user1-id",
email="user@test-company.com",
is_admin=False,
role_assignments=[],
),
}
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles.directory_client"
) as mock_directory_client,
):
from prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles import (
directory_super_admin_only_admin_roles,
)
mock_directory_client.users = users
mock_directory_client.provider = mock_provider
check = directory_super_admin_only_admin_roles()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "admin1@test-company.com" in findings[0].status_extended
assert "Groups Administrator" in findings[0].status_extended
assert "_GROUPS_ADMIN_ROLE" not in findings[0].status_extended
assert "used only for super admin activities" in findings[0].status_extended
assert findings[0].resource_name == DOMAIN
assert findings[0].customer_id == CUSTOMER_ID
def test_fail_seed_admin_with_additional_roles(self):
"""Test FAIL when a _SEED_ADMIN_ROLE holder also has extra roles"""
users = {
"admin1-id": User(
id="admin1-id",
email="playground@prowler.cloud",
is_admin=True,
role_assignments=[SEED_ADMIN_ROLE, GROUPS_ADMIN_ROLE],
),
}
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles.directory_client"
) as mock_directory_client,
):
from prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles import (
directory_super_admin_only_admin_roles,
)
mock_directory_client.users = users
mock_directory_client.provider = mock_provider
check = directory_super_admin_only_admin_roles()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "playground@prowler.cloud" in findings[0].status_extended
assert "Groups Administrator" in findings[0].status_extended
assert "_GROUPS_ADMIN_ROLE" not in findings[0].status_extended
assert "_SEED_ADMIN_ROLE" not in findings[0].status_extended
def test_fail_multiple_super_admins_with_extra_roles(self):
"""Test FAIL lists all super admins that have additional roles"""
users = {
"admin1-id": User(
id="admin1-id",
email="admin1@test-company.com",
is_admin=True,
role_assignments=[SUPER_ADMIN_ROLE, GROUPS_ADMIN_ROLE],
),
"admin2-id": User(
id="admin2-id",
email="admin2@test-company.com",
is_admin=True,
role_assignments=[SUPER_ADMIN_ROLE, USER_MANAGEMENT_ADMIN_ROLE],
),
"admin3-id": User(
id="admin3-id",
email="admin3@test-company.com",
is_admin=True,
role_assignments=[SUPER_ADMIN_ROLE],
),
}
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles.directory_client"
) as mock_directory_client,
):
from prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles import (
directory_super_admin_only_admin_roles,
)
mock_directory_client.users = users
mock_directory_client.provider = mock_provider
check = directory_super_admin_only_admin_roles()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "admin1@test-company.com" in findings[0].status_extended
assert "admin2@test-company.com" in findings[0].status_extended
assert "admin3@test-company.com" not in findings[0].status_extended
def test_no_findings_when_no_users(self):
"""Test no findings when there are no users"""
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles.directory_client"
) as mock_directory_client,
):
from prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles import (
directory_super_admin_only_admin_roles,
)
mock_directory_client.users = {}
mock_directory_client.provider = mock_provider
check = directory_super_admin_only_admin_roles()
findings = check.execute()
assert len(findings) == 0
def test_non_super_admin_with_roles_not_flagged(self):
"""Test that users who are not super admins are ignored even if they have roles"""
users = {
"admin1-id": User(
id="admin1-id",
email="admin1@test-company.com",
is_admin=True,
role_assignments=[SUPER_ADMIN_ROLE],
),
"delegated1-id": User(
id="delegated1-id",
email="delegated@test-company.com",
is_admin=False,
role_assignments=[GROUPS_ADMIN_ROLE],
),
}
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles.directory_client"
) as mock_directory_client,
):
from prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles import (
directory_super_admin_only_admin_roles,
)
mock_directory_client.users = users
mock_directory_client.provider = mock_provider
check = directory_super_admin_only_admin_roles()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
assert "delegated@test-company.com" not in findings[0].status_extended
def test_pass_super_admin_with_empty_role_assignments(self):
"""Test PASS when super admin has no role assignments (edge case)"""
users = {
"admin1-id": User(
id="admin1-id",
email="admin1@test-company.com",
is_admin=True,
role_assignments=[],
),
}
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles.directory_client"
) as mock_directory_client,
):
from prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles import (
directory_super_admin_only_admin_roles,
)
mock_directory_client.users = users
mock_directory_client.provider = mock_provider
check = directory_super_admin_only_admin_roles()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "PASS"
def test_fail_custom_role_without_description_falls_back_to_name(self):
"""A custom role with an empty description should be displayed
using its name as a fall-back, so the FAIL message is never blank
for users that genuinely hold extra roles."""
users = {
"admin1-id": User(
id="admin1-id",
email="admin1@test-company.com",
is_admin=True,
role_assignments=[SUPER_ADMIN_ROLE, CUSTOM_ROLE_NO_DESCRIPTION],
),
}
mock_provider = set_mocked_googleworkspace_provider()
with (
patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=mock_provider,
),
patch(
"prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles.directory_client"
) as mock_directory_client,
):
from prowler.providers.googleworkspace.services.directory.directory_super_admin_only_admin_roles.directory_super_admin_only_admin_roles import (
directory_super_admin_only_admin_roles,
)
mock_directory_client.users = users
mock_directory_client.provider = mock_provider
check = directory_super_admin_only_admin_roles()
findings = check.execute()
assert len(findings) == 1
assert findings[0].status == "FAIL"
assert "custom-helpdesk-role" in findings[0].status_extended