# googlecloud toolpass authenticates as a service account and asks the Policy Troubleshooter API (`POST https://policytroubleshooter.googleapis.com/v3/iam:troubleshoot`) whether a principal holds an IAM permission on a full resource name. Google evaluates the allow policies and the deny policies of the resource and every ancestor (project, folder, organization), expanding group membership, and answers `CAN_ACCESS`, `CANNOT_ACCESS`, `UNKNOWN_CONDITIONAL` or `UNKNOWN_INFO`. One check is one call. Nothing is written and no user credential is ever used. ## Credential 1. Create a service account in a project where the **Policy Troubleshooter API** is enabled. 2. Grant it `roles/iam.securityReviewer` on the organization, folder or project you want to answer for (`scope`). The role is read-only: `*.getIamPolicy` on every resource type plus the deny policy reads. The troubleshooter can only explain policies the caller can read, so a role granted too low in the hierarchy makes every check `UNKNOWN_INFO` (unknown). 3. Either create a key for the service account and store the JSON in a file (`auth_mode: key`), or run toolpass on GCE/GKE as that service account and use `auth_mode: keyless`: toolpass then takes the runtime identity's token from the metadata server and no key exists. 4. If the credential's own project cannot be billed for the API (the API is disabled there, or the service account belongs to another organization), set `quota_project` to a project where the API is enabled; toolpass sends it as `X-Goog-User-Project`, and the service account needs `serviceusage.services.use` on that project. `toolpass probe` warns when the role is missing under `scope` and always reminds that the troubleshooter discloses which permissions other principals hold: that is what toolpass is for, but the service account should be held by toolpass alone. Signing with a service-account key (`auth_mode: key`) needs `cryptography`: install `toolpass[crypto]` (the Docker image has it). ## Connection ```yaml - id: gcp-acme integration: googlecloud scope: organization:123456789012 # or folder:, project: credential: file:/secrets/gcp-toolpass.json # service-account key JSON (auth_mode key) auth_mode: key # or keyless quota_project: acme-toolpass # optional googleworkspace_connection: gws-acme # optional, see Identity ``` | Key | Meaning | |---|---| | `scope` | `organization:`, `folder:` or `project:`: where the service account holds `roles/iam.securityReviewer`. The probe asks the troubleshooter about this resource. Checks are not restricted to it | | `credential` | service-account key JSON (`file:`); `client_email`, `private_key` and `private_key_id` are read from it (the token endpoint is `token_url`). Required in `auth_mode: key` | | `auth_mode` | `key` (default) signs a JWT with the key; `keyless` uses the GCE/GKE runtime identity | | `quota_project` | sent as `X-Goog-User-Project` on every call | | `googleworkspace_connection` | id of a `googleworkspace` connection used to resolve the user first | | `token_url` | default `https://oauth2.googleapis.com/token` | | `api_url` | default `https://policytroubleshooter.googleapis.com` | | `metadata_url` | default `http://metadata.google.internal`, `keyless` only | Production leaves the three URLs at their defaults; they exist so tests can point at a fake. In `key` mode the token is a JWT bearer exchange: RS256 with `kid` = `private_key_id`, `iss` = `client_email`, `scope` = `https://www.googleapis.com/auth/cloud-platform`, `aud` = `token_url`, no `sub`, one hour lifetime; the token is cached and refreshed five minutes before it expires. In `keyless` mode the metadata server's token is used as is. A 401 from the API invalidates the token and retries the call once. ### Identity The principal is an email address. Without `googleworkspace_connection` the caller's email, lowercased, is sent as is: the troubleshooter answers for that address whether or not a Google Account exists, so an unknown user is `deny` ("no allow policy grants ..."), never `user_not_found`. With `googleworkspace_connection` the Workspace Directory is consulted first, as that connection does: an alias becomes the primary address (which is the principal sent to Google), an email with no account is `user_not_found`, and a suspended or archived account is denied every action without asking Google. Groups sent by the caller are ignored: the troubleshooter expands Google Groups itself. ## Resources | Resource | Full resource name | |---|---| | `project:` | `//cloudresourcemanager.googleapis.com/projects/` | | `folder:` | `//cloudresourcemanager.googleapis.com/folders/` | | `organization:` | `//cloudresourcemanager.googleapis.com/organizations/` | | `bucket:` | `//storage.googleapis.com/projects/_/buckets/` | | `object:/` | `//storage.googleapis.com/projects/_/buckets//objects/` | | `dataset:/` | `//bigquery.googleapis.com/projects/

/datasets/` | | `table://` | `//bigquery.googleapis.com/projects/

/datasets//tables/` | | `secret:/` | `//secretmanager.googleapis.com/projects/

/secrets/` | | `serviceaccount:@.iam.gserviceaccount.com` | `//iam.googleapis.com/projects//serviceAccounts/` | | `instance://` | `//compute.googleapis.com/projects/

/zones//instances/` | | `service://` | `//run.googleapis.com/projects/

/locations//services/` (Cloud Run) | | `cluster://` | `//container.googleapis.com/projects/

/locations//clusters/` (GKE) | | `name:` | verbatim; must start with `//.googleapis.com/` | Every piece is validated against a strict shape (project ids or numbers, bucket names, numbers, resource names) before it is placed in the request; anything else is `invalid_request`. An object name may contain any character but control characters, since it only travels inside the JSON body, except `.` or `..` segments. `` is a project id or a project number everywhere. Google-managed service accounts (`...@developer.gserviceaccount.com`) are addressed through `name:`. ## Actions | Action | IAM permission | Resources | |---|---|---| | `raw:` | exactly that, in the v1 (`storage.objects.delete`) or v2 (`iam.googleapis.com/roles.create`) format | any | | `project.view` | `resourcemanager.projects.get` | project | | `iam.set` | `.setIamPolicy` of the resource type | project, folder, organization, bucket, secret, serviceaccount, table | | `storage.read` / `storage.write` / `storage.delete` | `storage.objects.get` / `create` / `delete` | bucket, object | | `storage.list` | `storage.objects.list` | bucket | | `bucket.delete` | `storage.buckets.delete` | bucket | | `bigquery.read` / `bigquery.write` / `bigquery.delete` | `bigquery.tables.getData` / `updateData` / `delete` | dataset, table | | `secret.read` | `secretmanager.versions.access` | secret | | `serviceaccount.actas` | `iam.serviceAccounts.actAs` | serviceaccount | | `compute.start` / `compute.stop` / `compute.delete` | `compute.instances.start` / `stop` / `delete` | instance | | `run.deploy` | `run.services.update` | service | | `gke.access` | `container.clusters.get` | cluster | Every action also accepts a `project:`, `folder:`, `organization:` or `name:` resource, since a permission is meaningful at any level of the hierarchy: `storage.delete` on `project:acme-prod` asks whether the user may delete objects anywhere in the project's buckets (as far as IAM is concerned). `iam.set` needs a typed resource to pick the permission; on `name:` use `raw:..setIamPolicy`. ## Decisions | Troubleshooter says | toolpass answers | |---|---| | `overallAccessState: CAN_ACCESS` | allow | | `CANNOT_ACCESS` with `denyPolicyExplanation.denyAccessState: DENY_ACCESS_STATE_DENIED` | deny ("a deny policy denies ...") | | `CANNOT_ACCESS` otherwise | deny ("no allow policy grants ...") | | `UNKNOWN_CONDITIONAL` (a binding or deny rule has a condition Google could not evaluate without a request context) | unknown (`unsupported`) | | `UNKNOWN_INFO` (toolpass cannot read one of the policies, or the resource does not exist) | unknown (`resource_not_visible`) | | any other or missing state, unreadable body | unknown (`upstream_error`) | | suspended / archived account (with `googleworkspace_connection`) | deny | | no Workspace account (with `googleworkspace_connection`) | deny (`user_not_found`) | | Workspace record without `suspended` or `archived` | unknown (`unsupported`) | | HTTP 400 (bad permission name, bad resource name, principal is not a Google Account or service account) | unknown (`invalid_request`) | | HTTP 403 with a rate-limit reason (`RATE_LIMIT_EXCEEDED`, `quotaExceeded`, status `RESOURCE_EXHAUSTED`), HTTP 429 | unknown (`upstream_rate_limited`) | | HTTP 403 otherwise (API disabled, role missing, quota project refused), HTTP 401 after one retry, token endpoint `invalid_grant` | unknown (`credential_rejected`) | | HTTP 404 | unknown (`resource_not_visible`) | | 5xx, timeout | unknown (`upstream_error` / `upstream_timeout`) | Error bodies are read whole so the reason is found even after a long message; only the status and reason tokens are used. Google error messages and the explained policies are never copied into a decision text. ## Probe `toolpass probe` mints a token, then asks the troubleshooter whether the service account itself may `resourcemanager..get` the `scope` resource. `CAN_ACCESS` or `CANNOT_ACCESS` proves the API is enabled and the policies under `scope` are readable; `UNKNOWN_INFO` warns that `roles/iam.securityReviewer` is missing there. Without `googleworkspace_connection` it warns that an unknown email is answered as deny rather than `user_not_found`. ## Requests a toolpass tool makes The integration's route table (`toolpass routes googlecloud`) maps the REST requests a toolpass tool sends to Google Cloud to the IAM permission each needs on the resource in its path, so the toolkit asks the troubleshooter about the session's user before a request inside `as_user()` leaves. A request uses the named action when one asks exactly that permission (`storage.read`, `secret.read`, `compute.start`, `gke.access`, `iam.set`), and `raw:` otherwise. | Service (host) | Routed | |---|---| | Cloud Storage (`storage.googleapis.com`, JSON API) | buckets, objects (also downloads from `/download/storage/v1/...`, the client's and an object's `mediaLink`), IAM policies and ACLs (`setIamPolicy`), uploads that cannot overwrite (`ifGenerationMatch=0`, under `/upload/` or `/resumable/upload/`) | | BigQuery (`bigquery.googleapis.com`) | datasets, tables (a new table's schema without column-level security), table data (`tabledata.list`, `insertAll`), table IAM policies | | Secret Manager (`secretmanager.googleapis.com`) | secrets, versions (`:access` is `secret.read`), IAM policies | | IAM (`iam.googleapis.com`), IAM Credentials (`iamcredentials.googleapis.com`) | service accounts, their keys and IAM policies, custom roles, tokens and signing | | Compute Engine (`compute.googleapis.com`) | instances: get, list, start, stop, reset, suspend, resume, labels, delete, IAM policy; operations | | Cloud Run (`run.googleapis.com`, v2) | services: get, list, delete, IAM policy | | GKE (`container.googleapis.com`) | clusters: get (credentials), list, delete; operations | | Resource Manager (`cloudresourcemanager.googleapis.com`) | projects, folders, organizations: get, IAM policies; project create, update, delete | `setIamPolicy` and ACL changes send out: they share the resource. Refused, because the request needs a second permission or a second resource that one check cannot cover: an upload that may overwrite an object or sets its ACL, copies, rewrites and composes, BigQuery jobs and queries (SQL names its own tables), views and external tables, `insertAll` with `templateSuffix`, dataset access lists, `deleteContents`, a table schema that sets `policyTags` or `dataPolicies` (column-level security needs `bigquery.tables.setCategory`) and any schema change on an existing table (a column sent without its policy tags loses them), batch requests, Cloud Run deploys and Compute changes that run code as a service account (`iam.serviceAccounts.actAs`), instance creation, token minting through `delegates`, project moves, metadata fields that need a permission of their own (object retention, bucket IP filters), object names that contain `?` (a toolpass resource ends its id there, so the object cannot be named), and any read on these hosts the table does not know. `X-HTTP-Method-Override` and `useAdminAccess` are refused. Query parameters are read in every spelling Google accepts, camelCase or snake_case (`delete_contents`, `predefined_acl`, `if_generation_match`) and `$`-prefixed for system parameters; one given twice, in one spelling or two, is refused, as are an unknown `$` system parameter, a `$ct` Content-Type override on a request with a body, `upload_protocol` and the `X-Goog-Upload-*` headers. The same services under `www.googleapis.com` and `content.googleapis.com` (their JSON, upload, resumable upload, media download and batch paths) are refused: address the service's own host, the client libraries' default. The Policy Troubleshooter itself is not routed: a tool does not ask it. Not claimed, so not checked: the XML API, virtual-hosted buckets (`.storage.googleapis.com`), regional endpoints and every other Google Cloud service. ## What it cannot see The troubleshooter evaluates IAM allow and deny policies of the resource and its ancestors. It does not evaluate: - IAM Conditions: a conditional binding answers `UNKNOWN_CONDITIONAL` (unknown), never allow; - Cloud Storage legacy ACLs on buckets without uniform bucket-level access, BigQuery dataset access entries that are not IAM bindings, and other product-specific grants outside IAM; - VPC Service Controls perimeters, organization policy constraints, principal access boundary policies, and Access Context Manager levels; - principals that are not a Google Account or a service account: Workforce Identity Federation users, Workload Identity Federation identities, `allUsers` and `allAuthenticatedUsers`; - whether the user can actually sign in (a suspended account is only caught with `googleworkspace_connection`); - resources that do not exist: the answer is `UNKNOWN_INFO`, not deny. What the request then returns is the agent's view, not the user's. A BigQuery `tabledata.list` (`bigquery.read`) is checked as `bigquery.tables.getData` on the table; BigQuery applies the row access policies and column policy tags (and data masking) of the identity that sends the request, the agent's, so the user can be shown rows and columns their own fine-grained access would hide. ## Unverified Marked `# UNVERIFIED:` in the code: - Whether `roles/iam.securityReviewer` is enough for the troubleshooter to read every allow and deny policy under `scope`, or whether a `policytroubleshooter.*` permission is needed as well; the probe reports `UNKNOWN_INFO` if the role is not enough. - Group expansion: the troubleshooter expands Google Groups only when the caller can see the membership; for groups it cannot read the state is `UNKNOWN_INFO`, which toolpass reports as `resource_not_visible` like an unreadable policy. - A principal that is not a Google Account or a service account: assumed to answer `CANNOT_ACCESS` or HTTP 400 rather than some other state. ## Test Unit tests run against one fake server that serves the token endpoint, the metadata server and `/v3/iam:troubleshoot`, verifying the JWT assertion, the bearer, the quota header and every request against the API's discovery document (`google-policytroubleshooter` in `test/specs/fetch.sh`). There is no live test; after configuring, run `toolpass probe` and one check for a user you know is allowed. `tests/integrations/googlecloud/test_routes.py` maps requests to every routed service, checks that each resource parses into the full resource name the troubleshooter is asked about, and covers the refusals above.