---
id: w4/m4/t003
title: "Authorization model in git + idempotent store/model apply script"
worker: worker4
status: done
estimate: 40m
depends_on: [w4/m4/t002]
---

## Objective

Check the authorization model into git and make applying it idempotent and deploy-driven: one store (`bex`), one model file (FGA DSL), applied by a script that creates the store if absent and writes the model only when it changed — the same out-of-band, never-in-Argo pattern as `scripts/auth-secrets.sh` / `auth-bootstrap-client.sh`.

## Context

- First model (single default tenant, matching what the control plane can express today):
  - types: `tenant` (relations: `admin`, `member`; `member` implied by `admin`), `app`, `database`, `api_key` — each with `owner: tenant` and permissions derived from it (`can_view` ← member, `can_manage` ← admin, `can_mint_keys` ← admin on tenant).
  - subjects are the strings `api.IdentityFrom` yields: Hydra `client_id` / Kratos identity id — model them as `user` (OpenFGA's untyped subject is fine for now).
- Store/model ids are runtime state, not git: the script resolves the store by name and stores nothing; bex-api discovers them the same way (t004) or reads them from the Secret the script updates.
- OpenFGA API: `POST /stores` (idempotent-by-name via list+match), `POST /stores/{id}/authorization-models` (append-only — write only when the DSL changed; compare against the latest model).
- CI: a deploy.yml step after "seed bootstrap client", using the preshared key from GH secrets; local: same script against a port-forward (`HYDRA_ADMIN_URL`-style `OPENFGA_URL` override).

## Steps

1. Add `deploy/gitops/authz/model.fga` (DSL above, commented).
2. Add `scripts/authz-model.sh`: resolve/create store `bex`, diff latest model vs file (transform via `openfga` CLI if available, else the JSON API), write when changed; `DRY_RUN=1` prints intent; never prints the preshared key.
3. Add the deploy.yml step (env: preshared key; waits for the openfga rollout like the hydra seeding step does).
4. Run on the mock cluster: first run creates, second run no-ops.

## Files

- `deploy/gitops/authz/model.fga` (new)
- `scripts/authz-model.sh` (new)
- `.github/workflows/deploy.yml`

## Acceptance criteria

- [ ] Two consecutive runs: first creates store+model, second exits 0 reporting no change (verified on mock).
- [ ] Model file round-trips: what the script wrote is what a fresh `GET authorization-models` returns (modulo ids).

## Out of scope

- Enforcement in bex-api (t004); per-tenant tuples beyond the schema (t005 seeds; real tenants arrive with w1/m2).
