بنقرة واحدة
render-permissions
// Render PERMISSIONS.md from @fgadoc annotations in the OpenFGA model. Use after any change to model.yaml to keep the human-readable permissions reference in sync.
// Render PERMISSIONS.md from @fgadoc annotations in the OpenFGA model. Use after any change to model.yaml to keep the human-readable permissions reference in sync.
| name | render-permissions |
| description | Render PERMISSIONS.md from @fgadoc annotations in the OpenFGA model. Use after any change to model.yaml to keep the human-readable permissions reference in sync. |
| license | MIT |
Read @fgadoc annotations from
charts/lfx-platform/templates/openfga/model.yaml and produce a
human-readable PERMISSIONS.md at the repo root.
model.yaml is a Helm template. The authorization model is the block
scalar under authorizationModel: |. Ignore everything outside that
block — Helm expressions ({{- if ... }}, {{- end }}, etc.) must
not be parsed or evaluated.[user:*] means "every user including anonymous". It produces an
Everyone column in the table — see Step 2.@fgadoc comments may appear between an @fgadoc annotation block
and the type/define line it annotates (e.g. descriptive prose already
in the file). Collect all consecutive # @fgadoc:* lines before a
type or define line as that entity's annotation block, skipping any
intervening non-@fgadoc comment lines.#### Permission Inheritance section lists only cross-type sources
— i.e. <rel> from <field> references where <field> resolves to a
different type. Same-type or <peer> inclusions are intentionally omitted
because they are already implicit in the ✅ / 🟡 columns of the table.## Object types must be preserved exactly if
PERMISSIONS.md already exists and has content there.<!-- generated-intro --> comment block at the top must be preserved
exactly if PERMISSIONS.md already exists.@fgadoc annotationsAnnotations are YAML-style comments placed immediately before the entity they describe:
# @fgadoc:alias Display Name — human-readable name for a type or relation
# @fgadoc:hide — suppress type (whole section) or relation (column)
# @fgadoc:jtbd Statement — one JTBD; multiple lines allowed per relation
Read charts/lfx-platform/templates/openfga/model.yaml. Extract the
authorizationModel: | block and parse it as plain text.
For each type <name> block, extract:
| Field | How to find it |
|---|---|
| Raw type name | type <name> |
| Display name | @fgadoc:alias in preceding annotation block; else raw name in Title Case with underscores replaced by spaces |
| Hidden? | @fgadoc:hide in preceding annotation block |
For each define <relation>: line, extract:
| Field | How to find it |
|---|---|
| Raw relation name | define <relation>: |
| Display name | @fgadoc:alias in preceding annotation block; else raw name in Title Case with underscores replaced by spaces |
| Hidden? | @fgadoc:hide in preceding annotation block |
| JTBD list | All @fgadoc:jtbd lines in preceding annotation block |
| Define expression | Everything after : on the define line |
| Direct user grant? | [user] appears literally (not [user:*]) in define expression |
| Public wildcard? | [user:*] appears in the define expression |
| Indirect-only? | Does NOT have [user] or [user:*], AND has at least one <rel> from <field> term where <field> resolves to a different type |
For each visible type (not hidden), determine two sets of visible columns:
Direct-grant columns — relations where [user] appears literally (not
[user:*]) in the define expression and the relation is not hidden.
Indirect-only columns — relations that are indirect-only (no [user] or
[user:*], has at least one cross-type <rel> from <field> term) and
are not hidden and would have at least one ✅ cell (i.e. their reachable
JTBD pool is non-empty — see Step 2c for how to compute this). These columns
represent roles that can only be assigned by granting access on a foreign
object, not directly on this object. Their header text is italicized in
the Markdown table (wrap the display name in *...*).
Additionally, if any relation in the type has [user:*] in its define
expression (even a hidden relation), include an Everyone column as the
rightmost column. The Everyone column header is always italicized (*Everyone*).
It is special: it uses the 🟡 marker instead of ✅, and it collects JTBDs
from all relations that contain [user:*].
Include ALL @fgadoc:jtbd statements across ALL relations of the type,
deduplicated. Do not filter out JTBDs whose relation has no visible
column — they still appear as rows (they may have a 🟡 in the Everyone
column).
Row ordering: Sort JTBD rows using the following priority rules, applied in order:
Base object first. JTBDs that describe viewing, reading, or accessing the object itself (the type; may be phrased as "details", "definition", or just the type name) come first. If viewing the base object is bundled with other operations in a single JTBD (e.g. "View a meeting & its attachments"), it still sorts first — especially when it is the JTBD that carries the 🟡 Everyone marker.
Settings next. JTBDs that refer to "settings" of the object come immediately after the base-object group.
Attributes in Read → Update → Delete order. For each logical group of related attributes or sub-resources (e.g. members, invites, links), sort Read operations before Update/Create before Delete. Group related attributes together so that Read/Update/Delete for the same thing are adjacent.
Child resource creation last. JTBDs that create child resources of another type come last. If creating a child of the same type is allowed, list that first among the child-creation group. Otherwise order child-creation JTBDs by the order their target types appear in model.yaml.
OpenFGA semantics primer: When relation B's define says or A (B
includes A), it means anyone who has relation A also satisfies relation B.
In other words, A ⊆ B — A is the more privileged role. A writer who is
included in auditor (auditor: ... or writer) automatically has auditor
access too, because writers are a subset of auditors.
Consequence for columns: A column represents a role. For direct-grant columns, a user is directly assigned to the role. For indirect-only columns, a user reaches the role via a foreign object. In both cases, the column should show ✅ for every action that role can perform — including actions inherited upward from any relation that includes this role.
For each (JTBD, column) pair:
For a direct-grant column (has [user]) or an indirect-only column:
Build the upward reachability set for the column's relation: starting
from that relation, find every other relation in the same type whose define
expression contains or <this-relation> (directly or transitively). Collect
the JTBD lists from the starting relation itself and every relation in
the upward reachability set. If any of those JTBD lists contains the target
JTBD, mark ✅.
Do not traverse downward (i.e., do not add JTBDs from relations listed
via or <peer> inside this column's own define — those are relations this
role subsumes, not roles that subsume this role).
Self-referential conditional fields (🟡):
A relation's define may contain <rel> from <field> terms where <field> is
typed as [<current_type>] — i.e. the field's declared type is the same type
currently being rendered. These are self-referential flag tuples set per-object
to enable conditional access for a particular role. The self-referential type
is the sole criterion; no other heuristic (naming pattern, presence of or
terms, etc.) is needed. Examples from v1_past_meeting:
define past_meeting_for_participant_recording_view: [v1_past_meeting]
define past_meeting_for_attendee_recording_view: [v1_past_meeting]
define past_meeting_for_host_recording_view: [v1_past_meeting]
define recording_viewer: [user:*] or organizer or auditor
or invitee from past_meeting_for_participant_recording_view
or attendee from past_meeting_for_attendee_recording_view
or host from past_meeting_for_host_recording_view
For each such <rel> from <field> term in the relation being computed:
<field>'s declared type (from define <field>: [<type>]) is
the same as the current type. If yes, it is a self-referential
conditional field — proceed with step 2.<rel> named in the expression is a direct-grant column on the same
type. Mark 🟡 for that column (and apply the upward reachability propagation
rule: all columns that include <rel> via or also get 🟡, unless they
already have ✅ from a different source).Worked example — v1_past_meeting#recording_viewer JTBD:
The JTBD "View past meeting recordings" is on recording_viewer. Its define:
[user:*] or organizer or auditor
or invitee from past_meeting_for_participant_recording_view
or attendee from past_meeting_for_attendee_recording_view
or host from past_meeting_for_host_recording_view
organizer is an indirect-only column. Upward set from organizer:
auditor says or organizer (implicitly via upward chain). Organizer gets
✅; auditor's upward set also yields ✅ for auditor.auditor is an indirect-only column. Gets ✅ directly.invitee from past_meeting_for_participant_recording_view:
past_meeting_for_participant_recording_view is declared [v1_past_meeting]
— same as the current type → self-referential conditional → Invitee gets 🟡.attendee from past_meeting_for_attendee_recording_view:
Attendee gets 🟡.host from past_meeting_for_host_recording_view:
Host gets 🟡.[user:*] → Everyone column gets 🟡.Result row: | View past meeting recordings | ✅ | ✅ | 🟡 | 🟡 | 🟡 | 🟡 |
(columns: Organizer, Auditor, Host, Invitee, Attendee, Everyone)
Cross-type fields — unconditional or halt:
If <field>'s declared type is a different type than the current one, it
is a cross-type link. There are two sub-cases:
Primary parent links (e.g. project, committee, meeting on
v1_past_meeting) — these are already handled by the standard upward
reachability algorithm in Step 2c. No special treatment needed here.
Any other cross-type field whose semantics are not covered by the upward reachability algorithm — halt and flag:
⚠ Unhandled cross-type field
<field>(type<other_type>) in<current_type>#<relation>. Manual review required before rendering.
Do not emit a blank cell, a 🟡, or a ✅ for that column. Leave the entire type's table unrendered and continue to the next type. This pattern has no current instances in the model; if one appears, the skill must be extended before it can be rendered correctly.
For the Everyone column ([user:*]):
For each relation R whose define contains [user:*], build R's own upward
reachability set using the same rule. Mark 🟡 if the JTBD appears in the
JTBD list of R itself or any relation in R's upward reachability set.
Worked example — project type:
Relations and their defines (simplified):
writer: [user] or owner or writer from parent
JTBDs: Create a vote, Manage key contacts, Create committees/meetings/lists,
Update project settings, Create & update a project
auditor: [user, team#member] or writer or auditor from parent
JTBDs: View project settings, View membership tiers,
View memberships & member companies, View membership key contacts
meeting_coordinator: [user]
JTBDs: (none)
viewer: [user:*] or auditor or auditor from parent
JTBDs: View a project, View project meeting count
Named-role columns: writer, auditor, meeting_coordinator.
Everyone column: yes (viewer has [user:*]).
Upward reachability:
writer: which relations say or writer? → auditor does. Which say or auditor? → viewer does (but viewer is not a named-role column). So writer's upward set = {auditor, viewer}.
Writer column JTBDs = writer's own ∪ auditor's own ∪ viewer's own = all JTBDs.
auditor: which relations say or auditor? → viewer does. Auditor's upward set = {viewer}.
Auditor column JTBDs = auditor's own ∪ viewer's own = auditor JTBDs + viewer JTBDs.
meeting_coordinator: nothing includes meeting_coordinator. Upward set = {}.
Meeting coordinator column JTBDs = (none) → all cells empty.
Everyone (viewer has [user:*]): viewer's upward set = {} (nothing includes viewer).
Everyone JTBDs = viewer's own only = {View a project, View project meeting count}.
Result table (JTBD rows ordered by semantic priority — base object first, then settings, then attributes, then child resource creation):
| Project Writer | Project Auditor (full read) | Project Meeting Coordinator | Everyone | |
|---|---|---|---|---|
| View a project | ✅ | ✅ | 🟡 | |
| View project meeting count | ✅ | ✅ | 🟡 | |
| View project membership key contacts | ✅ | ✅ | ||
| View project memberships & member companies | ✅ | ✅ | ||
| View project membership tiers | ✅ | ✅ | ||
| View project settings | ✅ | ✅ | ||
| Create a vote | ✅ | |||
| Manage project membership key contacts | ✅ | |||
| Create project committees, meetings & mailing lists | ✅ | |||
| Update project settings | ✅ | |||
| Create & update a project | ✅ |
Note: "View a project" and "View project meeting count" appear even though
they come from viewer which has no [user] grant — all JTBDs are always
shown as rows.
Note: write JTBDs ("Create a vote" etc.) do NOT appear in the Everyone
column because viewer does not include writer — the chain is
viewer → auditor → writer only when you are a privileged user, not when
you are anonymous. The upward reachability for viewer stops at viewer
itself (nothing includes viewer).
[user:*]If no relation in the type has [user:*] in its define expression, omit the
Everyone column entirely. The Everyone column is ALWAYS the rightmost.
For each visible type, for each direct-grant relation (has [user],
not hidden) and each indirect-only column, emit a bullet only when the
relation's own define expression contains one or more direct
<rel> from <field> terms where <field> resolves to a different type
(i.e. a field whose type annotation is not the current type).
Indirect-only columns always have at least one cross-type source by definition, so they will always produce a bullet. Their bullet uses the same format as direct-grant bullets — italicize the relation display name to match the italicized column header:
- ***<rel display name>***: inherited from <Source Type Display Name> <Relation Display Name>
Direct-grant bullet format (unchanged):
- **<rel display name>**: inherited from <Source Type Display Name> <Relation Display Name>
Rules:
or <peer> chains to discover cross-type sources that belong to
a peer relation. Each relation's bullet describes only what is written
directly in that relation's define.<rel> from parent) counts as cross-type when
parent holds the current type (i.e. it is a recursive parent link) —
mention it as "inherited from parent <Type Display Name>".[user:*] public-access — this is already
communicated by the Everyone column in the table.or <peer> inclusions — these are already
visible from the ✅ columns in the table.#### Permission Inheritance sub-section if no bullets
are generated for any relation in that type.Do not include verbatim OpenFGA syntax in the output. No backtick
expressions like `writer from project` or `or organizer` should
appear anywhere in PERMISSIONS.md. Describe inheritance in plain English
only (e.g. "inherited from Project Writer", "inherited from parent Project").
When multiple direct cross-type sources exist for one relation, list them on a single bullet separated by commas.
File structure:
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->
<!-- generated-intro
This file is generated automatically from
charts/lfx-platform/templates/openfga/model.yaml
by the render-permissions agent skill. Do not edit the sections below by hand.
Run .agents/skills/render-permissions/SKILL.md to regenerate after any model change.
-->
# LFX Self Service Platform Permissions
<intro — preserved if existing, else default below>
## Object types
### <Type display name>
| | <col1> | <col2> | ... | *Everyone* |
|---|---|---|---|---|
| <jtbd> | ✅ | | ✅ | 🟡 |
#### Permission Inheritance
- **<rel>**: inherited from ...
---
Use --- as a visual divider between type sections.
For types with no visible columns and no Everyone column (no direct [user]
or [user:*] grants at all), write a short prose paragraph explaining how
access is inherited, and omit the table and inheritance sub-section.
Table header row: The first cell of the header row is blank (no "Job to Be Done" text). Columns follow the ordering rule below, with Everyone always last.
Column ordering rule: Apply this sort across all columns:
member, participant, or subscriber — in file order among
themselvesDefault intro (use only if file is new or has no existing intro after
the <!-- generated-intro ... --> block):
This document describes the permissions model for the LFX Self Service
Platform. Each section below represents an object type that supports direct
role assignment.
## Legend
- "**Role Name**" column headings are assignable roles for this object type (may also be inherited; see lists below tables)
- "**_Italicized Role Name_**" headings are implicit or inherited roles (_not_ directly assignable on this object type)
- ✅ access is granted to this role to all objects of this type
- 🟡 access is conditional on per-object settings
Preserving the intro: The <!-- generated-intro ... --> comment block and
the H1 heading are always re-written. Everything between the H1 and the
## Object types heading is the intro and must be preserved if it already
exists.
After writing, re-read PERMISSIONS.md and confirm:
### headings matches the number of non-hidden types.<rel> from <field> terms where <field> is declared as [<current_type>] (self-referential) show 🟡 for the named <rel> column and its upward reachability set — not blank and not ✅.[<other_type>]) outside the standard upward reachability algorithm was silently rendered — if one was found, rendering halted and a ⚠ flag was emitted instead.[user:*]-only relation appears as a direct-grant or indirect-only column.[user:*] relation has an Everyone column (italicized header).[user:*] in its define does NOT get a public-access bullet (the Everyone column covers this).*Writer*, *Auditor*).[user], not hidden) and indirect-only columns, when they have direct cross-type <rel> from <field> terms in their own define — no peer-chain traversal.- ***Auditor***: inherited from ...); direct-grant bullets use bold name (e.g. - **Writer**: inherited from ...).[user:*] relation's own upward reachability set (not from privileged roles that the [user:*] relation happens to include downward).`writer from project`) appears anywhere in the file.<!-- generated-intro ... --> block is present at the top.# LFX Self Service Platform Permissions is present.## Object types heading is used (not ## Objects supporting role assignment or ## Entities).Report: types rendered, total columns (excluding Everyone), total JTBD rows.