reader/writer/manager grants cannot express what you
need — a reusable permission bundle assigned to many objects, a new inheritance
edge, or a relation for a resource kind that needs its own semantics.
Do not do this to add a new kind of governed object. Agents, skills, MCP servers
and clients are all resource:<uuid> and the kind lives in the database, so a new
kind needs no model change at all. And do not do this for runtime restrictions —
tool rules, budgets and approvals are policy rules, not graph relations.
Model changes are shared infrastructure. The same store backs workspace
membership and every resource grant, so a bad rollout breaks all authorization at
once. Make changes additive.
Prerequisites
- The
fgaCLI installed. Every step below depends on it. - Write access to
config/auth/openfga/in the repository. - A dev stack you can restart:
ACCESS_CONTROL_BACKEND=openfgawithACCESS_CONTROL_OPENFGA_AUTO_BOOTSTRAP=trueandACCESS_CONTROL_OPENFGA_AUTO_APPLY_MODEL=true, which is whatdocker-compose.dev.yamlsets. Start it withmake up-dev, notmake up— the latter runsdocker-compose.yaml, which names neither the setting nor OpenFGA, so the backend falls through to itsdisableddefault and none of the verification steps below will work. - Read the AgentArea model — you are editing exactly what that page describes.
Steps
Choose the shape
A role bundle is the usual answer, and it needs no model change at all. Create
a Reach for a new relation only when the graph itself must derive something it
cannot today — a second inheritance edge, or a bit that is not read, write or
manage.
role object carrying the bits, then bind it to a subject and one object with a
role_assignment. This is how you get “reviewer” or “operator” across many
projects without a relation per bundle:Edit the model
Add to Two conventions the existing model follows. Identity types are PascalCase
(
config/auth/openfga/model.fga. Keep changes additive: a new type, or a
new relation on an existing type. Changing or removing an existing relation
breaks live tuples that reference it.User, Agent, Workspace); governance types are lowercase (role,
role_assignment, project, resource). And permission bits are independent
— do not write can_write: writer or can_manage, because the model deliberately
has no roll-up.Add tests before you validate
Every new relation needs at least one positive and one negative assertion in
The negative assertion is the one that catches an accidental roll-up.
config/auth/openfga/model.fga.yaml. Add tuples to the shared tuples block and
a case to tests:Validate and test
validate catches DSL errors; test runs
every assertion in the fixture, including the ones that were already there — a
change that breaks existing inheritance shows up here.Generate the deployable model
The JSON is generated, never hand-edited:Both copies must move together. The compose stack mounts the
config/ copy at
/app/config/auth/openfga/authorization-model.json; the Helm chart packages the
charts/ copy as a ConfigMap mounted at
/etc/agentarea/openfga/authorization-model.json.Roll it out
Restart the API and the worker. Both bootstrap OpenFGA at startup: they find or
create the store named by Because OpenFGA versions models rather than replacing them, existing tuples keep
working against the previous version while the new one becomes current. That is
why additive changes are safe and destructive ones are not.
ACCESS_CONTROL_OPENFGA_STORE_NAME, compare the model
on disk against the models already in the store, write it if it is new, and use
the returned model id for the rest of the process lifetime.Verify
The CLI tests pass. This is the gate; run it before anything else:Troubleshooting
The new relation is not recognised after a restart
The new relation is not recognised after a restart
The bootstrap compares models by normalized content, ignoring ids. If your
edited model is byte-identical in structure to one already in the store, it
reuses that model id and writes nothing — which is correct. If it genuinely
differs and is still not picked up, check that
ACCESS_CONTROL_OPENFGA_AUTO_APPLY_MODEL=true and that
ACCESS_CONTROL_OPENFGA_MODEL_PATH points at the file you regenerated. With
auto-apply off, the process uses whatever
ACCESS_CONTROL_OPENFGA_AUTHORIZATION_MODEL_ID names and your file is
ignored.It works in compose and not in Kubernetes
It works in compose and not in Kubernetes
You regenerated
config/auth/openfga/authorization-model.json and forgot
the copy under charts/agentarea/files/openfga/ . They are two files and
nothing keeps them in sync automatically.Agents are denied where users are allowed
Agents are denied where users are allowed
Role bits are typed
[User:*, Agent:*] and both must be written per enabled
bit. A role object created with only User:* tuples silently denies every
agent assignee — a fail-closed footgun that only appears when a real agent
runs.A manager grant does not confer read
A manager grant does not confer read
Working as designed. The bits are independent; write all three when you mean
full access. See
grant access to a resource .
422 Unsupported relation for a resource grant when writing through the API
422 Unsupported relation for a resource grant when writing through the API
The relationship endpoint accepts a fixed set of relation names and maps
them onto
reader , writer or manager . A relation you added to the
model is not automatically writable through that endpoint — it needs a code
change, or write the tuple directly against OpenFGA during development.422 Group (subject_set) grants are managed via project/role
422 Group (subject_set) grants are managed via project/role
The relationship endpoint writes direct grants only. Bundled access is
exactly what
role and role_assignment are for; write those tuples
through OpenFGA or a migration script.Related
The AgentArea model
The model you are extending, and the invariants the writer enforces outside
the graph
Grant access to a resource
The direct grants that cover most needs
Authorization models
Why roles are objects here rather than a table