iam-grants manages IAM bindings for service accounts owned by Z-Arch resources.
It is intended for IAM relationships that Z-Arch does not natively manage.
This extension is deployment-lifecycle automation. It is not app runtime logic.
- Uses only
project_context.gcloud(...)for IAM reads/writes. - Resolves principals from deployed resources at runtime (no service accounts in config).
- Fails closed when a principal service account cannot be resolved.
- Applies only unconditional bindings.
- Is idempotent for unconditional bindings.
- Rejects risky
customcommand shapes.
- No service-account creation.
- No fallback to naming conventions for service accounts.
- No conditional-binding creation or mutation.
- No gateway/auth/security policy mutation outside IAM bindings.
This extension executes on:
post_service_deploypost_job_deploypost_scheduler_deploy
Each hook evaluates the full configured binding set.
Development:
zarch ext install ./extensions/zarch_ext_iam_grants --editableIn Z-Arch MCP-driven workflows, install via install_extension after the extension
block exists in zarch.yaml.
Canonical location:
extensions.<extension_name>wheretype: "iam-grants"
Canonical shape:
extensions:
iam-grants:
type: "iam-grants"
required_roles: []
config:
continue_on_error: false
principal_bindings: []Backward-compatible shape (accepted by parser, not preferred for new configs):
extensions:
iam-grants:
type: "iam-grants"
continue_on_error: false
principal_bindings: []| Path | Type | Required | Default | Notes |
|---|---|---|---|---|
type |
string | yes | none | Must be "iam-grants" |
required_roles |
list[string] | no | [] |
Roles needed by the actor running the extension |
config |
object | recommended | none | Preferred wrapper for extension settings |
config.continue_on_error |
bool/string/int | no | false |
Boolean parser accepts common forms (true/false, 1/0, yes/no) |
config.principal_bindings |
list[object] | no | [] |
Empty list is valid (no-op) |
principal_bindings[i].principal.kind |
string | yes | none | One of service, job, scheduler |
principal_bindings[i].principal.id |
string | yes | none | Resource ID from services/jobs/scheduler name |
principal_bindings[i].grants |
list[object] | yes | none | Must be non-empty |
grants[j].role |
string | yes | none | IAM role name |
grants[j].target.kind |
string | yes | none | One of project, secret, run_service, run_job, topic, bucket, custom |
The extension resolves service accounts per principal kind using gcloud describe:
service:run services describe <id> --format=value(spec.template.spec.serviceAccountName)job:run jobs describe <id> --format=value(spec.template.spec.template.spec.serviceAccountName)scheduler:scheduler jobs describe <id> --format=value(httpTarget.oidcToken.serviceAccountEmail)
If resolution fails or returns empty, the grant fails.
No service-account email should be authored in config.
Required:
kind: project
Optional:
project_id
Required:
kind: secretid(Secret Manager secret ID)
Optional:
project_id
Required:
kind: run_serviceid(Cloud Run service name)
Optional:
project_idregion
Required:
kind: run_jobid(Cloud Run job name)
Optional:
project_idregion
Required:
kind: topicid(Pub/Sub topic name)
Optional:
project_id
Required:
kind: bucketname(orid; parser normalizes toname)
Optional:
project_id
Behavior:
- Bucket names are normalized to
gs://...when building commands.
Required:
kind: customget_policy_command(non-empty list of strings)add_binding_command(non-empty list of strings)
Optional:
project_id
Validation and normalization:
get_policy_commandmust includeget-iam-policy.add_binding_commandmust includeadd-iam-policy-binding.add_binding_commandmust not include--member,--role, or--condition.- Extension auto-appends if absent:
--projecton get/add commands--format=jsonon get command--memberand--roleon add command
For each hook execution:
- Parse and validate config.
- Resolve principal service account from live resource metadata.
- Build
get-iam-policyandadd-iam-policy-bindingcommands for each grant target. - Read IAM policy JSON.
- If exact unconditional binding exists: skip.
- If only conditional binding exists for same member+role: fail closed.
- Otherwise apply binding.
- Emit summary log:
applied,skipped,failed.
- Idempotency: policy is read before every add; existing unconditional binding is skipped.
- Dedupe in one hook run: key is
(member, role, target_identity). target_identityis scope-aware (project,region, target ID/name, and custom command shape), preventing false dedupe across scopes.
continue_on_error: false(default):- First failure raises immediately and stops processing.
continue_on_error: true:- Failure is logged, processing continues with remaining grants.
Set required_roles to cover all targeted IAM APIs. Typical mappings:
- Project bindings:
roles/resourcemanager.projectIamAdmin - Secret bindings:
roles/secretmanager.admin - Cloud Run service/job bindings:
roles/run.admin - Pub/Sub topic bindings:
roles/pubsub.admin - Storage bucket bindings:
roles/storage.admin - Custom targets: least-privileged admin role for that specific API/resource type
Use placeholder IDs and names when sharing publicly:
extensions:
iam-grants:
type: "iam-grants"
required_roles:
- "roles/resourcemanager.projectIamAdmin"
- "roles/secretmanager.admin"
- "roles/cloudkms.admin"
- "roles/artifactregistry.admin"
config:
continue_on_error: false
principal_bindings:
- principal:
kind: service
id: ingest-service
grants:
- role: roles/artifactregistry.reader
target:
kind: custom
get_policy_command: [artifacts, repositories, get-iam-policy, app-python-repo, --location, example-region1]
add_binding_command: [artifacts, repositories, add-iam-policy-binding, app-python-repo, --location, example-region1]
- role: roles/cloudsql.client
target: {kind: project}
- role: roles/cloudsql.instanceUser
target: {kind: project}
- role: roles/secretmanager.secretAccessor
target: {kind: secret, id: app-hmac-key}
- role: roles/cloudkms.cryptoKeyEncrypter
target:
kind: custom
get_policy_command: [kms, keys, get-iam-policy, app-dek-wrapper, --keyring, app-keyring, --location, example-region1]
add_binding_command: [kms, keys, add-iam-policy-binding, app-dek-wrapper, --keyring, app-keyring, --location, example-region1]- Never include service-account emails in config.
- Always set
type: "iam-grants". - Always place settings under
config. - Keep
continue_on_errorexplicit (falseunless intentional). - Keep each
principal_bindings[i].grantsminimal and resource-scoped. - Prefer resource-level targets (
secret,run_service,custom) over broadprojectgrants when possible. - For
customtargets, do not include--member,--role, or--condition. - Do not use conditional IAM through this extension.
- If one principal needs different scopes, use separate grants (or separate principal bindings) instead of broad roles.
- This extension mutates IAM policies; review config changes like security changes.
- Favor least privilege and resource scoping.
- Keep
continue_on_error: falsefor predictable fail-fast behavior in production. - Treat
customtarget commands as security-sensitive and review them carefully.
Unit tests:
extensions/zarch_ext_iam_grants/tests/test_extension.py
Integration-style, side-effect-free tests:
extensions/zarch_ext_iam_grants/tests/test_extension_integration.py- Uses in-memory fake
project_context.gcloud. - No real GCP calls, no project side effects.
Run:
.venv/bin/pytest -q extensions/zarch_ext_iam_grants/tests