Permission graph
Grants open, permissions tighten phase by phase (Permissions). At load time, Core builds two graphs to prove these rules do not contradict each other: the profile graph says who can be whom, then the rights graph says what each may do, at each phase.
The profile graph
An identity can match several profiles, and its rights are the union of their grants. Checking
profile by profile is therefore not enough: Core must know which profiles can add up. It reads
this in the when conditions:
| relation | example | consequence |
|---|---|---|
| exclusive | anonyme (identity.authenticated == false) and premium (identity.authenticated and …) | never together |
| included | premium implies client when its condition contains client's | a premium customer also has the client's rights |
| cumulative | support and manager, two roles | rights are checked for the combination |
The graph's nodes are the combinations of profiles an identity can reach, plus the "no profile" case: an identity that matches no profile is allowed nothing.
Loading refuses, or reports:
| written | effect |
|---|---|
| a profile no identity can fill (its condition contradicts itself) | error: it would serve nobody |
| two profiles with the same condition | warning: one is enough |
| a door that admits identities no profile covers (a logged-in customer with no role, for example) | reported: they will be allowed nothing |
The rights graph
For each combination of profiles, each skill, each kind and each phase, Core computes the effective right: the combination's grants, intersected with that phase's permissions. For one combination, each phase must be included in the previous one:
grants ⊇
invoke⊇present⊇select
The submit is checked apart, because whoever chooses is not always whoever submits: HR reserves an
account, IT creates it. Those submitted_by names must hold the grant of every write operation
the selection can contain.
What breaks this chain is a contradiction, refused at load time:
| written | effect |
|---|---|
a permission given to a profile without the operation's grant | error: it would never apply |
| a kind a profile can choose without being able to see it | error: the model could add it by guessing its id |
a kind someone can choose, while none of those submitted_by names holds the grant of its write operation | error: the selection could never be submitted |
| a skill nobody can invoke, a kind nobody sees | error: dead config |
| a required task no profile can do | error: the playbook could never be submitted |
an automation whose named owner (owner) lacks the grants of the playbook it starts | error: it would fail every time |
| a rule that requires a proof no provider of the station can make | error: the authentication request would be impossible |
| a skill exposed on a portal whose provider cannot open its rule | warning: unavailable on that portal, with no request |
The matrix
The rights graph reads as a matrix, published with the config:
| combination | articles · the other items | articles · kind premium |
|---|---|---|
anonyme | invoke, present, select | invoke; removed at present |
premium | invoke, present, select | invoke, present, select |
| no profile | — | — |
The config's tests: can assert it:
tests:
- name: "a visitor does not see premium products"
given: {profile: anonyme}
when: {invoke: {skill: articles}}
expect: {never_kinds: [premium]}
What the graph does not decide
- A dynamic condition — an amount, a budget,
$params.team in identity.teams— is not decided at load time. The graph counts it as "maybe", never as "always". - A profile condition Core cannot compare (beyond
==,!=,in,and,or,noton identity fields) makes its profiles "cumulative" out of caution: the graph then checks the union.
Two keys
The graph proves the config is coherent, at load time. It does not replace the check at run
time: when Core calls the tenant's API, it checks the grants in the config again, and the
present filter applies to the real result. If the graph were wrong, execution would still refuse.