> ## Documentation Index
> Fetch the complete documentation index at: https://ctrlrun.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# An IAM agent that can grant read but never admin

> An access-request agent grants roles: read and viewer roles run on their own, anything else waits for a human.

An access-request agent grants roles from tickets. Read-only roles are routine, write roles
need a person, and `admin` is never granted by an agent. The hazard is the one this recipe
shows last: a human approves `reader` and the agent, re-planning or told to by a ticket's
text, tries to execute `admin` with that approval.

## The policy

```yaml runnable theme={null}
schema: ctrlrun.policy/v2

actions:
  iam.grant_role:
    effect: "grant:{principal}:{role}"
    resource: "project:{project}"
    rules:
      - when: { role_eq: admin }
        decision: deny
      - when: { role_in: [reader, viewer] }
        decision: allow
      - decision: approve
  iam.revoke_role:
    effect: "revoke:{principal}:{role}"
    decision: allow
```

## The code

```python runnable file=main.py theme={null}
from pathlib import Path

import ctrlrun
from ctrlrun import Control, Policy, SQLiteStateStore

HERE = Path(__file__).resolve().parent
STATE = HERE / ".ctrlrun"
STATE.mkdir(exist_ok=True)
for name in ("state.db", "state.db-wal", "state.db-shm"):
    (STATE / name).unlink(missing_ok=True)

bindings: list[tuple[str, str, str]] = []


def bind(project: str, principal: str, role: str) -> str:
    bindings.append((project, principal, role))
    return "bound"


store = SQLiteStateStore(STATE / "state.db")
control = Control(Policy.from_file(HERE / "ctrlrun.yaml"), store)


@ctrlrun.protect(
    "iam.grant_role",
    effect="grant:{principal}:{role}",
    resource="project:{project}",
    control=control,
)
def grant(project: str, principal: str, role: str) -> str:
    return bind(project, principal, role)


with ctrlrun.context(agent="access-agent"):
    print(
        "reader on billing:", grant(project="billing", principal="ana@example.com", role="reader")
    )

    try:
        grant(project="billing", principal="ana@example.com", role="editor")
    except ctrlrun.ApprovalRequired as pending:
        print("editor on billing: a human decides:", pending.request_id)
        request_id = pending.request_id
    else:
        raise SystemExit("editor was granted without a human")

    store.grant_approval(request_id, "human:security")
    with ctrlrun.with_approval(request_id):
        try:
            grant(project="billing", principal="ana@example.com", role="admin")
        except ctrlrun.ActionDenied:
            print("admin with the editor approval: refused; admin is never an agent action")
        else:
            raise SystemExit("admin was granted on an approval for editor")
        print(
            "editor with the editor approval:",
            grant(project="billing", principal="ana@example.com", role="editor"),
        )

print("bindings:", bindings)
```

## What the agent sees

```text theme={null}
reader on billing: bound
editor on billing: a human decides: apr_…
admin with the editor approval: refused; admin is never an agent action
editor with the editor approval: bound
bindings: [('billing', 'ana@example.com', 'reader'), ('billing', 'ana@example.com', 'editor')]
```

`admin` is refused by the policy before the approval is even considered. Had the policy allowed
it with approval, the approval for `editor` would still not have matched: it is bound to the
hash of `editor`, and `admin` hashes differently.

## The receipt

```bash runnable theme={null}
ctrlrun receipts --last 4
```

## When an AMBIGUOUS appears

An IAM call that timed out may have bound the role. Read the binding back from the provider,
then `ctrlrun resolve grant:ana@example.com:editor --committed` or `--failed`. Granting a role
twice is usually idempotent at the provider, but the receipt should say what happened, not what
was assumed.

## Next

* [A credential-rotation agent](/cookbook/credential-rotation-agent).
* [Approval binding](/concepts/approval-binding) · [Get started](/get-started/quickstart) · [Why](/why).


## Related topics

- [A credential-rotation agent](/cookbook/credential-rotation-agent.md)
- [Cookbook](/cookbook/index.md)
- [Architecture](/ARCHITECTURE.md)
- [Authority and delegation](/authority.md)
- [ctrlrun verify](/verify.md)
