Repository navigation
Fix production OpenFGA model rollout #119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # 0017 — Pin OpenFGA Models To Product Releases | ||
|
|
||
| ## Status | ||
|
|
||
| Accepted on 2026-07-29 by explicit project-owner direction. | ||
|
|
||
| The repository-required independent Claude challenge was unavailable because | ||
| the configured account remained over quota. The project owner had already | ||
| directed the delivery loop to continue without that reviewer and then reported | ||
| the production authorization failure for repair. The proposal, strongest | ||
| counterargument, repository evidence, current official OpenFGA guidance, and | ||
| rollback test are recorded in the | ||
| [increment design](../increments/active/2026-07-29-openfga-model-rollout/design.md). | ||
|
|
||
| ## Context | ||
|
|
||
| OpenFGA authorization models are immutable. Writing one produces a new model | ||
| ID, while tuples remain in the store. Production OrgMemory requests explicitly | ||
| send one configured model ID so every binary uses a known policy version. | ||
|
|
||
| The initial deployment created one store and model, then persisted both IDs. | ||
| Subsequent deployments updated application images and the repository model but | ||
| never wrote another model version. Code could therefore start checking a | ||
| relation that did not exist in the pinned production model. This happened when | ||
| `can_manage_ai` shipped: authorization correctly failed closed, but valid | ||
| organization administrators lost access to the new AI settings endpoints. | ||
|
|
||
| ## Decision | ||
|
|
||
| The repository OpenFGA model is a versioned product-release input. | ||
|
|
||
| - First-store bootstrap persists the store ID, model ID, and SHA-256 of the | ||
| model bytes. | ||
| - A production deployment compares the release model digest with the pinned | ||
| digest. | ||
| - A missing or changed digest writes a new immutable model into the same store | ||
| before application containers are recreated. | ||
| - The deployment atomically persists the returned model ID and digest, and all | ||
| application calls remain explicitly pinned to that model ID. | ||
| - An unchanged model is a no-op and does not create another immutable version. | ||
| - Failed deployment rollback restores the previous images, model ID, and | ||
| digest. A newly written but unused model may remain in the store. | ||
|
|
||
| Tuple migration remains an explicit concern for model changes that add, rename, | ||
| or remove tuple-bearing relations. The deployment mechanism orders and pins the | ||
| model; it does not invent or rewrite tuples. | ||
|
|
||
| ## Strongest Counterargument | ||
|
|
||
| Omit `authorization_model_id` and let OpenFGA select the latest model. That | ||
| removes the configuration update and would have hidden this deployment bug. | ||
|
|
||
| This is rejected because a model write would then change authorization for | ||
| running replicas independently of their binary version. An accidental write | ||
| could affect production immediately, gradual rollout would be impossible, and | ||
| application rollback would not restore the prior policy. OpenFGA recommends | ||
| pinning a specific model ID in production. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Application and authorization policy rollback are one environment rollback. | ||
| - Legacy environments intentionally write one current model because they have | ||
| no stored digest. | ||
| - Identical product releases do not accumulate model versions. | ||
| - Model changes must keep the immediately previous binary/model combination | ||
| rollback-safe or explicitly use a staged migration. | ||
| - Deployment CI must test upgrade ordering, unchanged-model no-op, and rollback | ||
| of the model pin. | ||
|
|
||
| ## References | ||
|
|
||
| - [OpenFGA immutable authorization models](https://openfga.dev/docs/getting-started/immutable-models) | ||
| - [OpenFGA model migrations](https://openfga.dev/docs/modeling/migrating/migrating-models) | ||
| - [OpenFGA CLI model versions](https://openfga.dev/docs/getting-started/cli) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| # OpenFGA Model Rollout Repair | ||
|
|
||
| ## Problem | ||
|
|
||
| The production application pins every authorization request to | ||
| `ORGMEMORY_OPENFGA_AUTHORIZATION_MODEL_ID`, but the deployment lifecycle writes | ||
| that identifier only during first-store bootstrap. Later releases update the | ||
| repository-owned `model.fga` without writing a new immutable model version or | ||
| changing the pinned identifier. | ||
|
|
||
| This became user-visible when the multi-provider control plane added | ||
| `organization#can_manage_ai`. The web and API images deployed successfully, | ||
| while the production API continued checking an older model that did not contain | ||
| that relation. Organization administrators could enter the admin shell but the | ||
| Language Models and Index Settings requests failed closed. | ||
|
|
||
| Direct SSH evidence was unavailable during diagnosis because the ZM host timed | ||
| out from the current workstation. Repository and workflow evidence still proves | ||
| the lifecycle defect: | ||
|
|
||
| - `bootstrap-openfga.sh` creates a store and model only when both IDs are empty; | ||
| - `deploy.sh` requires and reuses the existing model ID without writing the | ||
| current `model.fga`; | ||
| - production successfully deployed the application commit containing | ||
| `can_manage_ai`. | ||
|
|
||
| ## Selected Design | ||
|
|
||
| Treat the authorization model as a versioned release input: | ||
|
|
||
| 1. Keep one durable OpenFGA store and all existing tuples. | ||
| 2. Compute SHA-256 over the repository model used by the release. | ||
| 3. If that digest differs from the digest pinned in the host environment, write | ||
| the model into the existing store with the official OpenFGA CLI. | ||
| 4. Parse and validate the returned `authorization_model_id`. | ||
| 5. Atomically persist the new model ID and digest before recreating API and | ||
| worker containers. | ||
| 6. Keep every application request explicitly pinned to that model ID. | ||
| 7. On a failed deployment, restore the prior environment and recreate the prior | ||
| image set with its prior model ID. The unused immutable model version may | ||
| remain in OpenFGA. | ||
|
|
||
| First-store bootstrap writes both the initial model ID and digest. Existing | ||
| installations have no digest, intentionally forcing one model write on the first | ||
| deployment containing this repair. | ||
|
|
||
| The official OpenFGA guidance says models are immutable, each write creates a | ||
| new version, production clients should pin a specific model ID, and adding a | ||
| relation requires writing the model before application code starts using it. | ||
|
|
||
| ## Strongest Counterargument | ||
|
|
||
| The application could stop sending an authorization model ID and let OpenFGA | ||
| use the latest version. That would make a newly written model visible without | ||
| updating application configuration. | ||
|
|
||
| This is rejected because "latest" disconnects a running binary from the policy | ||
| version it was tested against. A later or accidental model write could change | ||
| authorization for every replica immediately, and rollback of the application | ||
| would not restore its compatible policy. Explicit pinning is the safer | ||
| production contract. | ||
|
|
||
| Writing a model on every deployment is also rejected. OpenFGA models are | ||
| immutable and cannot be deleted, so identical releases would accumulate | ||
| unnecessary versions. The digest makes unchanged model delivery a no-op while | ||
| forcing legacy installations through one repair write. | ||
|
|
||
| ## Architecture Challenge | ||
|
|
||
| This changes the authorization deployment boundary and therefore requires an | ||
| independent challenge. The configured Claude reviewer remained unavailable due | ||
| to the previously reported quota limit. The project owner had already directed | ||
| this session to continue without the Claude discussion step and explicitly | ||
| asked for the production bug to be fixed. The counterargument above, repository | ||
| evidence, official OpenFGA lifecycle guidance, rollback behavior, and negative | ||
| tests are recorded here in place of that unavailable review. | ||
|
|
||
| ## Scope | ||
|
|
||
| - production Compose operations service for writing the repository model; | ||
| - first-store bootstrap model digest; | ||
| - production deployment model write, atomic pin, no-op, and rollback; | ||
| - deterministic shell regression coverage; | ||
| - deployment runbook, architecture, and authorization coverage updates. | ||
|
|
||
| No OpenFGA relation, tuple, application role, or browser authorization bypass is | ||
| changed by this repair. | ||
|
|
||
| ## Exit Gates | ||
|
|
||
| - OpenFGA model validation and store tests pass; | ||
| - production Compose interpolation and shellcheck pass; | ||
| - deterministic tests prove upgrade, unchanged-model no-op, and failed-canary | ||
| rollback to the prior model ID; | ||
| - documentation checks pass; | ||
| - PR CI passes, the PR merges, production deploys the immutable release, and an | ||
| authenticated administrator can load both affected screens. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # OpenFGA Model Rollout Repair Plan | ||
|
|
||
| - [x] Trace the UI denial through the API guard and production deployment model | ||
| pin. | ||
| - [x] Verify OpenFGA model-write and immutable-version behavior against current | ||
| official documentation. | ||
| - [x] Add a model-write operation to production Compose. | ||
| - [x] Persist the model digest during first-store bootstrap. | ||
| - [x] Write and atomically pin a changed model before API/worker recreation. | ||
| - [x] Preserve the previous model ID and digest across failed deployment | ||
| rollback. | ||
| - [x] Add deterministic upgrade, no-op, and rollback tests to deployment CI. | ||
| - [x] Reconcile architecture, deployment runbook, authorization spec/coverage, | ||
| and roadmap. | ||
| - [x] Run OpenFGA, deployment, documentation, and repository hygiene gates. | ||
| - [ ] Open the PR, resolve actionable review/CI findings, merge, deploy, and | ||
| verify the two administrator screens. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -187,6 +187,35 @@ services: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cap_drop: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - ALL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| openfga-model-write: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| image: openfga/cli:v0.7.19@sha256:2e0e250043ef480a9162623dbf1ff7a62a1a2cb96a79cb20577b144994ab114d | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| profiles: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - ops | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| command: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - model | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - write | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - --store-id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - ${ORGMEMORY_OPENFGA_STORE_ID:-} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - --file | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - /model/model.fga | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - --format | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - fga | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - --api-url | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - http://openfga:8080 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| depends_on: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| openfga-ready: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| condition: service_completed_successfully | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| networks: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - orgmemory-internal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| volumes: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - ../../integrations/authorization-openfga/src/main/openfga/model.fga:/model/model.fga:ro | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| restart: "no" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| read_only: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| security_opt: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - no-new-privileges:true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cap_drop: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - ALL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+190
to
+218
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Line 198 uses 🛠️ Proposed fix - --store-id
- - ${ORGMEMORY_OPENFGA_STORE_ID:-}
+ - ${ORGMEMORY_OPENFGA_STORE_ID:?Set ORGMEMORY_OPENFGA_STORE_ID}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| postgres-backup: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| image: ${ORGMEMORY_POSTGRES_IMAGE:?Set ORGMEMORY_POSTGRES_IMAGE} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| profiles: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
Model source for digest vs. model write can silently diverge.
deploy.sh/bootstrap-openfga.shcompute the release SHA-256 from an overridableORGMEMORY_OPENFGA_MODEL_FILE, but this compose service always mounts the hardcoded repo-relative path. If that override is ever used outside the test harness, the pinned digest would describe different bytes than what actually gets written into OpenFGA. Consider parameterizing this volume mount with the same variable (defaulting to the current hardcoded path) to keep the digest and the written model in sync, or document that the override is test-only.Also applies to: 210-211
🤖 Prompt for AI Agents