Setup guide
Everything you must provision before this skill can run β the exact credentials, permissions, and where to click. Links go to the official consoles and docs, which stay current.
Create a Google Cloud project with a service account, enable the Workspace APIs you need, then have a Workspace super administrator authorize that service account's numeric OAuth client ID against an explicit scope list in the Admin console.
You need to already have
- Google Workspace or Cloud Identity tenant (consumer @gmail.com accounts cannot use domain-wide delegation)
- Super Administrator on the Workspace tenant to authorize domain-wide delegation in the Admin console
- Owner, or roles/serviceusage.serviceUsageAdmin plus roles/iam.serviceAccountAdmin, on the Google Cloud project
- A dedicated impersonation subject: an active, licensed Workspace user holding the relevant admin role (required for Admin SDK Directory and Reports calls)
- gcloud CLI installed and authenticated (gcloud auth login), or equivalent Cloud console access
- If the iam.disableServiceAccountKeyCreation org policy is enforced, a runtime principal with roles/iam.serviceAccountTokenCreator on the service account for keyless signJwt
How the pieces connect
Credentials this skill needs
Set these as environment variables. Never paste secrets into a chat or commit them.
| Variable | What it is | Where to get it |
|---|---|---|
GOOGLE_CLOUD_PROJECT | Google Cloud project ID format: lowercase project ID, 6-30 chars (e.g. acme-workspace-automation) - not the project number or display name | Google Cloud console > project picker (top bar) > the ID column, or `gcloud config get-value project` |
GWS_SERVICE_ACCOUNT_EMAIL | Service account email format: NAME@PROJECT_ID.iam.gserviceaccount.com | Google Cloud console > IAM & Admin > Service Accounts > the Email column |
GWS_SERVICE_ACCOUNT_CLIENT_ID | Service account OAuth client ID (uniqueId) format: 21-digit numeric string (e.g. 114857392016558472103) | Google Cloud console > IAM & Admin > Service Accounts > click the account > Details tab > expand Advanced settings > Client ID. CLI: gcloud iam service-accounts describe SA_EMAIL --format='value(uniqueId)' |
GWS_DELEGATED_SUBJECT | User email to impersonate format: [email protected] - active, non-suspended, has signed in at least once | Google Admin console > Directory > Users. For Admin SDK calls, assign this user an admin role under Account > Admin roles |
GWS_CUSTOMER_IDoptional | Workspace customer ID format: literal string my_customer, or a customer ID like C01abc23d | Google Admin console > Account > Account settings > Profile > Customer ID. Use the literal my_customer for the caller's own tenant |
GOOGLE_APPLICATION_CREDENTIALSsecretoptional | Path to service account JSON key (keyed mode only) format: filesystem path to a JSON key file containing private_key (PEM, PKCS#8), client_email, client_id | Google Cloud console > IAM & Admin > Service Accounts > click the account > Keys tab > Add key > Create new key > JSON. CLI: gcloud iam service-accounts keys create sa.json --iam-account=SA_EMAIL |
GWS_SCOPES | OAuth scopes to request format: space-delimited https://www.googleapis.com/auth/... URLs; must be a subset of the Admin console grant | Must match exactly what was entered in Google Admin console > Security > Access and data control > API controls > Manage Domain Wide Delegation > the client's OAuth Scopes field (comma-separated there, space-delimited in code) |
Permissions to grant
https://www.googleapis.com/auth/admin.directory.user.readonlyAPI scopeadmin consent requiredEnumerate and inspect users in the tenant (directory.users.list / users.get) to build inventories and verify the credential chain.
β³ Use this readonly scope instead of https://www.googleapis.com/auth/admin.directory.user, which also permits create, update, suspend and delete.
https://www.googleapis.com/auth/admin.directory.group.readonlyAPI scopeadmin consent requiredList groups and their settings for access-review and membership-mapping tasks.
β³ Drop entirely if the task never touches groups; use admin.directory.group only when writes are required.
https://www.googleapis.com/auth/admin.directory.group.member.readonlyAPI scopeadmin consent requiredRead group memberships (members.list) to resolve who is in which group.
β³ This is the narrowest scope that satisfies members.list; admin.directory.group.readonly also works but additionally exposes group metadata, so grant this one alone when only memberships are needed.
https://www.googleapis.com/auth/admin.directory.orgunit.readonlyAPI scopeadmin consent requiredRead the org unit tree so users can be attributed to the correct OU.
β³ Omit unless OU structure is actually needed; user records already carry orgUnitPath.
https://www.googleapis.com/auth/admin.directory.rolemanagement.readonlyAPI scopeadmin consent requiredList admin roles and role assignments to identify privileged accounts during a review.
β³ Highly sensitive; grant only for privilege-audit work and never pair it with the writable admin.directory.rolemanagement scope.
https://www.googleapis.com/auth/admin.reports.audit.readonlyAPI scopeadmin consent requiredPull login, admin, drive and token audit activity from the Reports API.
β³ For aggregate counts only, https://www.googleapis.com/auth/admin.reports.usage.readonly exposes far less per-event detail.
https://www.googleapis.com/auth/drive.readonlyAPI scopeadmin consent requiredEnumerate and read files as the impersonated user when the task inspects Drive content.
β³ Prefer https://www.googleapis.com/auth/drive.metadata.readonly when only file names, owners and sharing state are needed - it never exposes file contents.
https://www.googleapis.com/auth/gmail.readonlyAPI scopeadmin consent requiredList and read messages in an impersonated mailbox for e-discovery or migration checks.
β³ Use https://www.googleapis.com/auth/gmail.metadata for headers and labels without message bodies; never request https://mail.google.com/ unless full IMAP-equivalent access is genuinely required.
https://www.googleapis.com/auth/calendar.readonlyAPI scopeadmin consent requiredRead calendars and events for the impersonated user (events.list).
β³ https://www.googleapis.com/auth/calendar.events.readonly is narrower when only event data, not calendar metadata, is needed.
https://www.googleapis.com/auth/cloud-identity.groups.readonlyAPI scopeadmin consent requiredRead Cloud Identity groups and memberships, including dynamic and security groups the Directory API does not fully expose.
β³ Skip unless Cloud Identity group semantics are required; the Directory API covers ordinary Workspace groups.
roles/iam.serviceAccountTokenCreatorIAM roleLets the runtime principal call signJwt on the service account so a delegated token can be minted without ever creating a downloadable private key.
β³ Grant on the individual service account resource, not at project level, and only to the specific runtime principal.
roles/serviceusage.serviceUsageAdminIAM roleEnable admin.googleapis.com and the other Workspace APIs on the Cloud project.
β³ Needed only during setup by the human operator; revoke afterwards. Never grant it to the Workspace service account itself.
roles/iam.serviceAccountAdminIAM roleCreate the service account and, in keyed mode, manage its keys.
β³ Setup-time only for the human operator; roles/iam.serviceAccountKeyAdmin alone suffices if the account already exists.
Step by step
- 1
Select or create the Google Cloud project
Run `gcloud projects create ACME-WORKSPACE-AUTOMATION` (or pick an existing one) and `gcloud config set project ACME-WORKSPACE-AUTOMATION`. In the console: click the project picker in the top bar > New Project > enter Project name > Create. Record the Project ID, not the display name.
Open in the console / docs β - 2
Enable only the Workspace APIs the task needs
Run `gcloud services enable admin.googleapis.com` and add gmail.googleapis.com, drive.googleapis.com, calendar-json.googleapis.com, cloudidentity.googleapis.com, iamcredentials.googleapis.com as required. In the console: APIs & Services > Library > search 'Admin SDK API' > Enable. Confirm with `gcloud services list --enabled`.
Open in the console / docs β - 3
Create a dedicated service account
Run `gcloud iam service-accounts create wsp-reader --display-name="Workspace API reader"`. In the console: IAM & Admin > Service Accounts > + CREATE SERVICE ACCOUNT > fill Service account name > CREATE AND CONTINUE > skip the 'Grant this service account access to project' step entirely (Workspace authority does not come from Cloud IAM) > DONE.
Open in the console / docs β - 4
Copy the service account's numeric OAuth client ID
Run `gcloud iam service-accounts describe wsp-reader@PROJECT_ID.iam.gserviceaccount.com --format='value(uniqueId)'`. In the console: IAM & Admin > Service Accounts > click the account > Details tab > expand 'Advanced settings' > copy Client ID. It is a 21-digit number. Do NOT use the email address or the key ID here.
Open in the console / docs β - 5
Choose the credential mode: keyless signJwt or JSON key
Keyless (preferred): grant the runtime principal roles/iam.serviceAccountTokenCreator on the service account with `gcloud iam service-accounts add-iam-policy-binding SA_EMAIL --member='serviceAccount:RUNTIME_SA' --role='roles/iam.serviceAccountTokenCreator'`, and enable iamcredentials.googleapis.com. Keyed: IAM & Admin > Service Accounts > click the account > KEYS tab > ADD KEY > Create new key > JSON > CREATE, or `gcloud iam service-accounts keys create sa.json --iam-account=SA_EMAIL`.
Open in the console / docs β - 6
Authorize domain-wide delegation in the Admin console (the step everyone misses)
A super administrator opens the Google Admin console and goes to Menu > Security > Access and data control > API controls > Manage Domain Wide Delegation > Add new. Paste the 21-digit Client ID into the 'Client ID' field, paste the scopes into 'OAuth Scopes' as a comma-separated list with no spaces, then click AUTHORIZE. This step happens in admin.google.com, NOT in the Cloud console, and nothing in the Cloud console can substitute for it.
Open in the console / docs β - 7
Verify the grant recorded every scope
Still on Manage Domain Wide Delegation, click the new client ID row > View details and confirm every scope you pasted is listed. If any are missing, click EDIT, re-enter them and AUTHORIZE again. The client ID itself cannot be edited after creation - a wrong one must be deleted and re-added.
Open in the console / docs β - 8
Give the impersonation subject an admin role
In the Admin console: Account > Admin roles > choose a prebuilt role such as 'User Management Admin' or 'Services Admin', or CREATE NEW ROLE with only the privileges required, then Assign users and add the subject account. Delegation borrows the subject's privileges; it does not create them. Skip this only for pure Gmail/Drive/Calendar work where the subject is the data owner.
Open in the console / docs β - 9
Mint a delegated access token and confirm the scopes
In Python: `service_account.Credentials.from_service_account_file(path, scopes=SCOPES).with_subject(SUBJECT)` then `creds.refresh(Request())`. By hand: POST to https://oauth2.googleapis.com/token with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer and assertion=<signed JWT with iss/sub/scope/aud/iat/exp>. Then GET https://oauth2.googleapis.com/tokeninfo?access_token=... and diff the returned scope list against what you requested.
Open in the console / docs β - 10
Prove the chain with a real API call, then allow for propagation
Run the verify command below. A 200 with a users array means project, API enablement, service account, delegation grant and subject privileges are all correct. On failure, wait 5 minutes and retry once before changing anything - Admin console changes can take up to 24 hours to propagate, and Multi-party approval (if enabled) holds the grant pending a second super admin.
Open in the console / docs β
Check it worked
python -c "import os,json,urllib.request;from google.oauth2 import service_account;from google.auth.transport.requests import Request;c=service_account.Credentials.from_service_account_file(os.environ['GOOGLE_APPLICATION_CREDENTIALS'],scopes=['https://www.googleapis.com/auth/admin.directory.user.readonly']).with_subject(os.environ['GWS_DELEGATED_SUBJECT']);c.refresh(Request());r=urllib.request.urlopen(urllib.request.Request('https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&maxResults=1',headers={'Authorization':'Bearer '+c.token}));print(r.status, json.load(r).get('users',[{}])[0].get('primaryEmail'))"If you hit an error
unauthorized_client: Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested.Cause: The service account's numeric OAuth client ID has not been authorized in the Admin console, the wrong value (usually the service account email or key ID) was pasted into the Client ID field, or the grant is still pending Multi-party approval.
Fix: Re-copy uniqueId with `gcloud iam service-accounts describe SA_EMAIL --format='value(uniqueId)'` and add it at https://admin.google.com/ac/owl/domainwidedelegation. If a wrong client ID was saved, delete the row and re-add it - the client ID field cannot be edited. Wait up to 24 hours (usually under 5 minutes) for propagation.
access_denied: Requested client not authorized.Cause: The client ID is authorized but one or more of the scopes in the token request are absent from the Admin console grant.
Fix: Open Manage Domain Wide Delegation > the client row > View details, compare against the scopes your code requests, then EDIT and add the missing ones. The two lists must match byte-for-byte, including the https:// prefix.
403 Not Authorized to access this resource/apiCause: Delegation is configured, but the impersonated subject is not a Workspace administrator with the privilege the Admin SDK call needs.
Fix: Assign the subject an admin role in Admin console > Account > Admin roles (for example User Management Admin for directory reads), or impersonate an account that already holds one. Re-mint the token afterwards - existing tokens do not pick up new privileges.
invalid_grant: Invalid email or User IDCause: The sub / with_subject value is not a real active user in the tenant - it is suspended, deleted, in a different domain, or has never signed in and accepted the Terms of Service.
Fix: Verify the address in Admin console > Directory > Users, confirm status is Active, and have the account sign in once. Do not use a group address or an alias as the subject.
invalid_grant: Invalid JWT: Token must be a short-lived token (60 minutes) and in a reasonable timeframeCause: Clock skew on the signing host, or an exp claim more than 3600 seconds after iat.
Fix: Sync the host clock with NTP and set exp = iat + 3600 at most. Prefer the google-auth library or signJwt over hand-built JWTs so the claims are generated correctly.
403 PERMISSION_DENIED: Admin SDK API has not been used in project 123456789012 before or it is disabledCause: admin.googleapis.com is not enabled on the Cloud project that owns the service account, or enablement has not propagated yet.
Fix: Run `gcloud services enable admin.googleapis.com --project=PROJECT_ID`, confirm with `gcloud services list --enabled`, wait 1-2 minutes, and retry. Check you are looking at the project that owns the service account, not the one your CLI defaults to.
403 ACCESS_TOKEN_SCOPE_INSUFFICIENT: Request had insufficient authentication scopes.Cause: The access token was issued with fewer scopes than the call requires - typically because a scope was added in code but never added to the Admin console grant, so the token silently came back reduced.
Fix: Call https://oauth2.googleapis.com/tokeninfo?access_token=... and diff the returned scope string against what you requested. Add any missing scope to the Admin console grant, then mint a fresh token.
FAILED_PRECONDITION: Key creation is not allowed on this service account.Cause: The organization enforces the constraints/iam.disableServiceAccountKeyCreation org policy.
Fix: Do not request a policy exception. Switch to the keyless path: enable iamcredentials.googleapis.com, grant the runtime principal roles/iam.serviceAccountTokenCreator on the service account, and mint tokens via projects.serviceAccounts.signJwt.
404 Domain not found. / 400 Invalid Input: customerCause: customer=my_customer was used while impersonating a subject outside the tenant you meant to query, or a literal customer ID was mistyped.
Fix: Read the real customer ID from Admin console > Account > Account settings > Profile > Customer ID and pass it explicitly, and confirm the subject belongs to that tenant.
429 RESOURCE_EXHAUSTED: Quota exceeded for quota metric 'Queries' and limit 'Queries per minute per user'Cause: Admin SDK per-user and per-project QPS limits were exceeded, commonly by unthrottled parallel enumeration across many impersonated subjects.
Fix: Add exponential backoff with jitter, cap maxResults at 500, reuse tokens for their full hour, and spread work across subjects rather than hammering one. Request a quota increase in APIs & Services > Admin SDK API > Quotas if sustained volume is genuinely needed.
Official documentation: https://developers.google.com/workspace/guides/create-credentials β
API operations this skill uses (19)
Every call the skill makes, linked to its official reference page.
| Operation | Call | Permission | Ref |
|---|---|---|---|
| Enable a Workspace API on the project Turns on the Admin SDK (and any other required Workspace API) before the first call, avoiding the SERVICE_DISABLED failure. | CLI gcloud services enable admin.googleapis.com --project=$GOOGLE_CLOUD_PROJECT | roles/serviceusage.serviceUsageAdmin on the project | docs β |
| List enabled services Confirms enablement propagated and that no unnecessary APIs were switched on. | CLI gcloud services list --enabled --project=$GOOGLE_CLOUD_PROJECT | roles/serviceusage.serviceUsageViewer on the project | docs β |
| Create the service account Creates the identity that will be delegated authority over the Workspace tenant. | CLI gcloud iam service-accounts create wsp-reader --display-name="Workspace API reader" --project=$GOOGLE_CLOUD_PROJECT | roles/iam.serviceAccountAdmin on the project | docs β |
| Read the service account's numeric OAuth client ID Produces the exact 21-digit value that must be pasted into the Admin console Client ID field - the most commonly mistaken input in the whole flow. | CLI gcloud iam service-accounts describe $GWS_SERVICE_ACCOUNT_EMAIL --format='value(uniqueId)' | roles/iam.serviceAccountViewer on the service account | docs β |
| Create a JSON key (keyed mode only) Produces the private key used to sign delegation JWTs when keyless signing is unavailable; fails under the iam.disableServiceAccountKeyCreation org policy. | CLI gcloud iam service-accounts keys create sa.json --iam-account=$GWS_SERVICE_ACCOUNT_EMAIL | roles/iam.serviceAccountKeyAdmin on the service account | docs β |
| Sign a delegation JWT without a key Signs the assertion carrying the sub claim with the Google-managed key, so domain-wide delegation works with no private key on disk. | POST /v1/projects/-/serviceAccounts/{SA_EMAIL}:signJwt (host: iamcredentials.googleapis.com) | roles/iam.serviceAccountTokenCreator on the service account | docs β |
| Exchange the JWT for a delegated access token Turns the signed assertion into the bearer token that every subsequent Workspace call uses; this is where unauthorized_client and access_denied surface. | POST https://oauth2.googleapis.com/token (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=SIGNED_JWT) | Domain-wide delegation grant for the requested scopes | docs β |
| Inspect the granted scopes on a token Diffs granted against requested scopes so a silently reduced token is caught at setup time instead of as a later 403 ACCESS_TOKEN_SCOPE_INSUFFICIENT. | GET https://oauth2.googleapis.com/tokeninfo?access_token=ACCESS_TOKEN | none (introspects the presented token) | docs β |
| List users (primary verification call) A 200 here proves project, API enablement, delegation grant, scope list and subject privileges are all correct end to end. | GET /admin/directory/v1/users?customer=my_customer&maxResults=1 (host: admin.googleapis.com) | https://www.googleapis.com/auth/admin.directory.user.readonly | docs β |
| Get a single user Confirms the impersonation subject exists and is active before it is used as sub, and resolves individual accounts during troubleshooting. | GET /admin/directory/v1/users/{userKey}?projection=full (host: admin.googleapis.com) | https://www.googleapis.com/auth/admin.directory.user.readonly | docs β |
| List groups Validates the group scope grant and supplies group keys for membership queries. | GET /admin/directory/v1/groups?customer=my_customer (host: admin.googleapis.com) | https://www.googleapis.com/auth/admin.directory.group.readonly | docs β |
| List group members Resolves who belongs to each group; exercises the member scope independently, since a grant covering groups but not members is a classic partial-403. | GET /admin/directory/v1/groups/{groupKey}/members (host: admin.googleapis.com) | https://www.googleapis.com/auth/admin.directory.group.member.readonly | docs β |
| List organizational units Maps the OU tree so users can be attributed to the right part of the org during inventory work. | GET /admin/directory/v1/customer/my_customer/orgunits?type=all (host: admin.googleapis.com) | https://www.googleapis.com/auth/admin.directory.orgunit.readonly | docs β |
| List admin roles Identifies which roles exist so the impersonation subject can be given the narrowest role that still satisfies the task. | GET /admin/directory/v1/customer/my_customer/roles (host: admin.googleapis.com) | https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly | docs β |
| List login audit activities Verifies the Reports API grant independently of Directory, since the two scopes are authorized and fail separately. | GET /admin/reports/v1/activity/users/all/applications/login (host: admin.googleapis.com) | https://www.googleapis.com/auth/admin.reports.audit.readonly | docs β |
| List Drive files as the impersonated user Confirms per-user impersonation works for data APIs, where the subject must be the data owner rather than an admin. | GET /drive/v3/files?fields=files(id,name,owners,mimeType),nextPageToken (host: www.googleapis.com) | https://www.googleapis.com/auth/drive.readonly | docs β |
| List Gmail messages as the impersonated user Proves mailbox-level delegation for a specific subject; userId 'me' resolves to the impersonated account. | GET /gmail/v1/users/me/messages?maxResults=1 (host: gmail.googleapis.com) | https://www.googleapis.com/auth/gmail.readonly | docs β |
| List calendar events as the impersonated user Validates the Calendar scope grant against the subject's primary calendar. | GET /calendar/v3/calendars/primary/events?maxResults=1 (host: www.googleapis.com) | https://www.googleapis.com/auth/calendar.readonly | docs β |
| List Cloud Identity groups Covers security and dynamic groups that the Directory API does not fully represent, when the task needs complete group coverage. | GET /v1/groups?parent=customers/{CUSTOMER_ID} (host: cloudidentity.googleapis.com) | https://www.googleapis.com/auth/cloud-identity.groups.readonly | docs β |