├── enum-admin-user-roles.py ├── readme.md └── .gitignore /enum-admin-user-roles.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import csv 3 | from google.oauth2 import service_account 4 | from googleapiclient.discovery import build 5 | 6 | SCOPES = [ 7 | 'https://www.googleapis.com/auth/admin.directory.user.readonly', 8 | 'https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly', 9 | ] 10 | 11 | 12 | SERVICE_ACCOUNT_FILE = "service-account.json.cred" # Update this path 13 | DELEGATED_ADMIN = "LOL YOU FORGOT TO CHANGE ME" # Update to your super admin email 14 | 15 | 16 | def get_service(): 17 | credentials = service_account.Credentials.from_service_account_file( 18 | SERVICE_ACCOUNT_FILE, scopes=SCOPES) 19 | return build('admin', 'directory_v1', credentials=credentials.with_subject(DELEGATED_ADMIN)) 20 | 21 | def get_roles(service): 22 | roles = {} 23 | request = service.roles().list(customer='my_customer') 24 | while request: 25 | response = request.execute() 26 | for role in response.get('items', []): 27 | roles[role['roleId']] = role['roleName'] 28 | request = service.roles().list_next(request, response) 29 | return roles 30 | 31 | def get_users(service): 32 | users = [] 33 | page_token = None 34 | while True: 35 | response = service.users().list(customer='my_customer', maxResults=500, pageToken=page_token).execute() 36 | users.extend(response.get('users', [])) 37 | page_token = response.get('nextPageToken') 38 | if not page_token: 39 | break 40 | return users 41 | 42 | def get_user_roles(service, email): 43 | result = service.roleAssignments().list(customer='my_customer', userKey=email).execute() 44 | return set(role['roleId'] for role in result.get('items', [])) 45 | 46 | def write_csv(filename, headers, rows): 47 | with open(filename, 'w', newline='') as f: 48 | writer = csv.writer(f) 49 | writer.writerow(headers) 50 | writer.writerows(rows) 51 | 52 | def print_summary(users, roles_map, service): 53 | print("Admin Users and their Roles:\n") 54 | for user in users: 55 | email = user['primaryEmail'] 56 | name = user.get('name', {}).get('fullName', '') 57 | assigned_roles = get_user_roles(service, email) 58 | assigned_names = [roles_map.get(role_id, role_id) for role_id in assigned_roles] 59 | if assigned_names: 60 | print(f"{name} ({email})\n Roles: {', '.join(assigned_names)}\n") 61 | 62 | def main(): 63 | parser = argparse.ArgumentParser(description='Enumerate Google Workspace admins and roles') 64 | parser.add_argument('--csv', action='store_true', help='Export to CSV file') 65 | args = parser.parse_args() 66 | 67 | service = get_service() 68 | roles_map = get_roles(service) 69 | users = get_users(service) 70 | 71 | # Print summary format to console always 72 | print_summary(users, roles_map, service) 73 | 74 | # If CSV flag is requested, output spreadsheet-compatible format 75 | if args.csv: 76 | headers = ['Full Name', 'Email', 'Admin Roles'] + list(roles_map.values()) 77 | rows = [] 78 | for user in users: 79 | email = user['primaryEmail'] 80 | name = user.get('name', {}).get('fullName', '') 81 | assigned_roles = get_user_roles(service, email) 82 | assigned_names = [roles_map.get(role_id, role_id) for role_id in assigned_roles] 83 | assigned_col = ";".join(assigned_names) 84 | role_cols = ['TRUE' if role_id in assigned_roles else 'FALSE' for role_id in roles_map.keys()] 85 | rows.append([name, email, assigned_col] + role_cols) 86 | write_csv('admins.csv', headers, rows) 87 | print('Exported to admins.csv') 88 | 89 | if __name__ == '__main__': 90 | main() 91 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Google Workspace Admin Role Enumerator 2 | 3 | This Python script enumerates all users in your Google Workspace domain who have assigned admin roles. It displays a summary in the console and can export spreadsheet-ready CSV data for detailed filtering. 4 | 5 | ## Features 6 | 7 | - Lists all users with assigned admin roles in Google Workspace. 8 | - Console output shows user’s full name, email, and their assigned admin roles. 9 | - Optionally exports a CSV with each admin role as its own TRUE/FALSE column. 10 | - Uses service account authentication and supports domain-wide delegation. 11 | 12 | ## Prerequisites 13 | 14 | 1. **Google Cloud Project** 15 | Enable the [Admin SDK Directory API](https://console.cloud.google.com/apis/library/admin.googleapis.com) for your project. 16 | 17 | 2. **Service Account** 18 | Create a service account and download the JSON key. 19 | 20 | 3. **Domain-Wide Delegation** 21 | - Enable domain-wide delegation for the service account. 22 | - Grant the following OAuth scopes in your Workspace Admin Console: 23 | - `https://www.googleapis.com/auth/admin.directory.user.readonly` 24 | - `https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly` 25 | 26 | 4. **Super Admin Email** 27 | Identify a Google Workspace super admin email (must have admin API privilege). 28 | 29 | ## Python & pip Dependencies 30 | 31 | This script requires **Python 3.7 or newer** and the following pip packages: 32 | 33 | ``` 34 | pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib 35 | ``` 36 | 37 | ### Dependency List (as of 2025) 38 | 39 | - `google-api-python-client` (v2.x or later) 40 | - `google-auth-httplib2` 41 | - `google-auth-oauthlib` 42 | 43 | These packages are available on PyPI and can be installed globally or in a virtual environment. 44 | 45 | If you prefer a requirements file, save this as `requirements.txt`: 46 | 47 | ``` 48 | google-api-python-client>=2.0.0 49 | google-auth-httplib2 50 | google-auth-oauthlib 51 | ``` 52 | 53 | Then install with: 54 | 55 | ``` 56 | pip install -r requirements.txt 57 | ``` 58 | 59 | **Python 3.7 or higher is required.** 60 | Check your installed version with: 61 | 62 | ``` 63 | python --version 64 | ``` 65 | 66 | ## Setup 67 | 68 | Edit your script with these lines: 69 | 70 | ``` 71 | SERVICE_ACCOUNT_FILE = 'path/to/service_account.json.cred' 72 | DELEGATED_ADMIN = 'admin@example.com' 73 | ``` 74 | 75 | ## Usage 76 | 77 | Print a readable summary to the console: 78 | 79 | ``` 80 | python enum-admin-user-roles.py 81 | ``` 82 | 83 | Print the summary and export a CSV: 84 | 85 | ``` 86 | python enum-admin-user-roles.py --csv 87 | ``` 88 | 89 | ## Example Output 90 | 91 | **Console:** 92 | ``` 93 | Admin Users in Google Workspace: 94 | 95 | Jane Doe (jane.doe@example.com): Super Admin, Groups Admin 96 | John Smith (john.smith@example.com): User Management Admin 97 | ... 98 | ``` 99 | 100 | **CSV (`admins.csv`):** 101 | 102 | | Full Name | Email | Admin Roles | Super Admin | Groups Admin | User Management Admin | ... | 103 | |-------------|------------------------|-----------------------------|-------------|--------------|----------------------|-----| 104 | | Jane Doe | jane.doe@example.com | Super Admin;Groups Admin | TRUE | TRUE | FALSE | ... | 105 | | John Smith | john.smith@example.com | User Management Admin | FALSE | FALSE | TRUE | ... | 106 | 107 | - The "Admin Roles" column is semicolon-separated. 108 | - Each admin role is a TRUE/FALSE column for spreadsheet filtering. 109 | 110 | ## References 111 | 112 | - [Google Admin SDK Directory API Python Quickstart](https://developers.google.com/workspace/admin/directory/v1/quickstart/python) 113 | - [Directory API: roles.list](https://developers.google.com/workspace/admin/directory/reference/rest/v1/roles/list) 114 | - [Directory API: roleAssignments.list](https://developers.google.com/workspace/admin/directory/reference/rest/v1/roleAssignments/list) 115 | 116 | --- 117 | 118 | ``` 119 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[codz] 4 | *$py.class 5 | 6 | # Credential json files 7 | *.cred 8 | 9 | # Output files 10 | *.csv 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py.cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | cover/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | .pybuilder/ 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | # For a library or package, you might want to ignore these files since the code is 93 | # intended to run in multiple environments; otherwise, check them in: 94 | # .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # UV 104 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | #uv.lock 108 | 109 | # poetry 110 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 111 | # This is especially recommended for binary packages to ensure reproducibility, and is more 112 | # commonly ignored for libraries. 113 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 114 | #poetry.lock 115 | #poetry.toml 116 | 117 | # pdm 118 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 119 | # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. 120 | # https://pdm-project.org/en/latest/usage/project/#working-with-version-control 121 | #pdm.lock 122 | #pdm.toml 123 | .pdm-python 124 | .pdm-build/ 125 | 126 | # pixi 127 | # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. 128 | #pixi.lock 129 | # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one 130 | # in the .venv directory. It is recommended not to include this directory in version control. 131 | .pixi 132 | 133 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 134 | __pypackages__/ 135 | 136 | # Celery stuff 137 | celerybeat-schedule 138 | celerybeat.pid 139 | 140 | # SageMath parsed files 141 | *.sage.py 142 | 143 | # Environments 144 | .env 145 | .envrc 146 | .venv 147 | env/ 148 | venv/ 149 | ENV/ 150 | env.bak/ 151 | venv.bak/ 152 | 153 | # Spyder project settings 154 | .spyderproject 155 | .spyproject 156 | 157 | # Rope project settings 158 | .ropeproject 159 | 160 | # mkdocs documentation 161 | /site 162 | 163 | # mypy 164 | .mypy_cache/ 165 | .dmypy.json 166 | dmypy.json 167 | 168 | # Pyre type checker 169 | .pyre/ 170 | 171 | # pytype static type analyzer 172 | .pytype/ 173 | 174 | # Cython debug symbols 175 | cython_debug/ 176 | 177 | # PyCharm 178 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 179 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 180 | # and can be added to the global gitignore or merged into this file. For a more nuclear 181 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 182 | #.idea/ 183 | 184 | # Abstra 185 | # Abstra is an AI-powered process automation framework. 186 | # Ignore directories containing user credentials, local state, and settings. 187 | # Learn more at https://abstra.io/docs 188 | .abstra/ 189 | 190 | # Visual Studio Code 191 | # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 192 | # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore 193 | # and can be added to the global gitignore or merged into this file. However, if you prefer, 194 | # you could uncomment the following to ignore the entire vscode folder 195 | # .vscode/ 196 | 197 | # Ruff stuff: 198 | .ruff_cache/ 199 | 200 | # PyPI configuration file 201 | .pypirc 202 | 203 | # Cursor 204 | # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to 205 | # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data 206 | # refer to https://docs.cursor.com/context/ignore-files 207 | .cursorignore 208 | .cursorindexingignore 209 | 210 | # Marimo 211 | marimo/_static/ 212 | marimo/_lsp/ 213 | __marimo__/ 214 | --------------------------------------------------------------------------------