# GCP IAM Primitive Role in Use

> Primitive roles (roles/owner, roles/editor, roles/viewer) predate GCP's granular IAM and grant project-wide power; Editor alone can modify nearly every resource in the project. ZopNight raises high-severity RC-1250 whenever a discovered role row carries the primitive_role=true label, and the finding closes once no binding uses the 3 roles.

Source: https://zop.dev/integrations/gcp/recommendations/gcp-iam-primitive-role-in-use
Updated: 2026-08-19

---

## Leftovers from before IAM was granular

Owner, Editor, and Viewer are the three "basic" (historically, primitive) roles GCP shipped
before per-service IAM existed. They are not scoped to a service, a resource, or a purpose.
They are scoped to everything. Editor grants modify access across nearly every API in the
project: a CI service account bound as Editor "because deploys kept failing" can also read
every Cloud SQL export, rewrite firewall rules, and delete buckets it has never touched.
Owner adds IAM administration on top, which turns any compromise into full, self-escalating
project control. Viewer is the mildest of the three and still hands out read access to
every dataset, secret-adjacent config, and log line in the project at once.

## Why the fix is replacement, not narrowing

Unlike a project-level `roles/storage.admin` grant, which can be re-scoped to a bucket,
a primitive role has no smaller version of itself. Remediation is always substitution:
work out what the member actually does, then bind the matching predefined roles
(`roles/cloudsql.client`, `roles/run.developer`, `roles/storage.objectViewer`, ...) or a
custom role, and remove the primitive binding. The workable sequence for a live system:

1. Pull 30–90 days of Cloud Audit Logs for the member to see which APIs it really calls.
2. Grant the narrow roles alongside the primitive one.
3. Watch for permission-denied errors, then drop Owner/Editor/Viewer last.

## Enumerate the bindings in one command

```bash
gcloud projects get-iam-policy PROJECT_ID \
  --flatten="bindings[].members" \
  --filter="bindings.role:(roles/owner OR roles/editor OR roles/viewer)" \
  --format="table(bindings.role, bindings.members)"
```

## Where ZopNight's signal comes from

The discoverer walks the project IAM policy and emits one synthetic `iam-role` row per
unique role it finds, labeling primitive ones with `primitive_role=true`; the rule fires
purely on that label and abstains for any row without it. Multi-project organizations get
one row (and one finding) per project per role, so a fleet-wide Editor habit surfaces as
a countable list rather than a single vague warning. Severity is high with $0 attached:
nothing about the finding saves money, and no automation edits your policy. Once the last
Owner/Editor/Viewer binding is gone, the next discovery cycle stops emitting the label and
the finding closes on its own.
