macula_realm_rights (macula v18.2.0)
View SourceThe realm's RBAC checks at the points that enforce them (macula#128, WP3 of the realm RBAC plan; threat model: macula-realm#97, "Threat model: RBAC for call, serve, publish and subscribe (WP0)").
A member presents its realm membership token once per link to its own station (admit/2): the station checks its size before verifying anything, verifies it under the realm's key with the link peer as its audience, and keeps the capabilities it grants until it stops authorizing. Each SUBSCRIBE and PUBLISH is then answered from those capabilities (subscribe/4, publish/4), with nothing verified again: one verification per token per link (T15). A caller checks that a provider's delegation names the procedure it serves (serve/4, T7).
Counting first
Every check runs in one of four modes per realm (mode/1): off checks nothing, counting counts what it would refuse and lets it through, members asks only whether the link holds a live membership of the realm, and refusing refuses what the capability check would. The default is counting, so a realm refuses only once its operator has seen its counters read zero for authorised traffic and set it to refusing. A realm in counting mode is open by design and must be shown as counting, never as protected (threat model 6.7).
Members mode is how a realm is closed on the wire (macula-realm#175, D1): a realm mints no pubsub/publish capability by design (macula#147), so refusing would refuse every legitimate PUBLISH and cannot close anything. membership/3 is a decision of its own, askable in every mode from counting up: in counting and refusing it allows and only counts what members mode would have done, under the membership check and by reason, so the close is measured before it is switched on (threat 6). subscribe/4, publish/4 and serve/5 defer to it in members mode.
Every decision in counting or refusing mode is counted by realm, check and outcome (counters/0): authorised, would_refuse (counting), refused (refusing) and exempt (a system topic, which names no realm and carries the mesh's own traffic, macula_topic:is_system_topic/1). A refusal names the capability and the resource it lacked.
What this does not promise
Publish is checked at the publisher's ingress station only: a station that colludes with a member can still inject readable events on a clear topic (threat model 4.1). Subscribe control on a clear topic is delivery control, not confidentiality (4.2): only a sealed group keeps a topic's content from those who may not read it.
A members-mode realm couples its own availability to its members' (macula-realm#175, threat 5, D4, accepted): if the realm cannot mint, or the trust list stops naming its key, every member of that realm is refused at every station within the token life, admins included, and the way back is an operator's mode change on the station, not anything the realm can do. Membership is also closed, not secret: a member's node id is readable from its endorsement records in the DHT.
SUBSCRIBE and PUBLISH are counted under membership only in members mode, where that is the whole decision: in counting and refusing they are counted under subscribe and publish as they always were, and a membership row moves only where the caller asks membership/3 itself — which is exactly what a station watching a realm before it closes it does, and why the two kinds of row must be read apart.
The counter table belongs to macula_peering_sup, which creates it with new_table/0, as macula_wire_tolerance's is.
Summary
Types
What a member presented on its link: its membership token and the proofs of its chain, each as it travelled.
A refusal names the check's capability, the resource it was asked for, and why.
The membership verdict a station keeps per link and realm, apart from the capabilities the token grants: the two are different questions, and in members mode only this one is asked.
What a members-mode decision counted when it was only counting (macula-realm#175, threat 6): a membership that would have been admitted, or would have been refused, counted apart from the capability decision's own no_token / not_granted, so an operator can see which members a close would have turned away.
What admit/2 keeps for a link: the capabilities the token grants, the second it stops authorizing, and the member the realm endorsed at its chain's root.
Why a frame did not come from a live member of the realm (macula-realm#175, threat 6): it presented no token, its token had expired, its token was minted for another node's link, or for another realm, or it did not verify at all (invalid, which keeps the refusal's own atom in the refusal's log line).
Functions
Verify a membership token a member presented on its link: checked for size before anything is verified, then under the realm key RealmKeyId with the link peer as its audience (threat model 6.1), so a token another node presents is refused not_the_audience. The rights it grants are kept until until, the earliest exp of the token and its proofs; the link's holder drops what it admitted on them then, unless a fresh token arrived (6.2).
The extension map a SUBSCRIBE or PUBLISH carries to present Presented to the link's station: one entry, not critical, so a station that predates it ignores it and the frame is handled as before.
Every decision counted since the table was created, keyed by realm (or other), check and outcome. A membership row also carries the outcome members mode only counts while it is counting (member, would_refuse) and the reason a frame would have been refused for (no_token, expired, not_the_audience, wrong_realm, invalid).
The name of the per-hop extension that carries a presented membership.
The most bytes a token and its proofs together may come to.
The most proofs admit/2 takes with a token.
The largest membership token admit/2 verifies, in bytes.
Whether a frame from a link holding Membership may act in Realm, in Mode (macula-realm#175, threat 1): the check a realm closes on. In members a live membership is the whole decision and no capability is read: a member is allowed, and anything else is refused naming realm/member and the reason, without reading a grant.
Why a frame from a link is not from a live member, from what admit/2 answered (macula-realm#175, WP3): a token another realm minted is wrong_realm — its issuer is not this realm's key (not_the_issuer), or a capability it grants names another realm (wrong_realm) — one minted for another node's link is not_the_audience, one past its exp is expired, and every other refusal is invalid: malformed, signature_invalid, not_yet_valid, realm_name_not_canonical (a malformed realm NAME in a capability says nothing about which realm the token belongs to) and the capability-side refusals. The refusal's own atom stays in the refusal's reason, and in the log line of whoever reported it.
The mode Realm runs its checks in: the realm_rights application environment of macula, a map from realm id to mode with an optional default, or counting when it names neither.
Create the counter table, owned by the calling process: macula_peering_sup, whose life is the run's.
The membership a received SUBSCRIBE or PUBLISH presents, read from its per-hop extension map as the frame decoder keeps it: none when it carries none, malformed when the entry is not a token and a list of proofs, each bytes. Nothing is verified here; admit/2 checks the size and verifies.
Whether the link may publish on Topic in Realm, in Mode, on the rights admit/2 kept for it: the exact bytes the station routes on, never a pattern.
Whether a provider may serve Procedure in Realm, in Mode, given what the caller or the station read from its delegation (macula_record:delegated_procedure/3): ok when the delegation names it, {error, Reason} when it does not or names no procedures at all. Counted like the other checks.
Whether a provider may serve Procedure in Realm, in Mode, given both what a caller or the station read from its delegation and the advertiser's membership of the realm (macula-realm#175, D3): in members an ADVERTISE needs both, so a member without the delegation is refused rpc/serve and a delegation without a live membership is refused realm/member. In every other mode the delegation decides as serve/4 does, and the membership is only counted (which is all counting and refusing do with it). Both decisions are counted, under serve and under membership.
Whether the link may subscribe to Pattern in Realm, in Mode, on the rights admit/2 kept for it (none when it presented no token, or its token stopped authorizing). A grant must cover every topic the pattern can match (threat model 4.4, macula_ucan:grants/4). allow lets the SUBSCRIBE through; {refuse, Refusal} refuses it not_authorized, naming the capability and the pattern.
Whether a token and its proofs fit the sizes admit/2 verifies: the token and each proof within max_token_bytes/0, at most max_proofs/0 proofs, and all within max_presented_bytes/0.
Types
-type check() :: subscribe | publish | serve | membership.
-type member_outcome() :: member | would_refuse.
What a member presented on its link: its membership token and the proofs of its chain, each as it travelled.
-type membership() :: {member, rights()} | {not_member, membership_reason()}.
A refusal names the check's capability, the resource it was asked for, and why.
-type membership_reason() :: no_token | expired | not_the_audience | wrong_realm | invalid.
The membership verdict a station keeps per link and realm, apart from the capabilities the token grants: the two are different questions, and in members mode only this one is asked.
-type mode() :: off | counting | members | refusing.
-type outcome() :: authorised | would_refuse | refused | exempt.
What a members-mode decision counted when it was only counting (macula-realm#175, threat 6): a membership that would have been admitted, or would have been refused, counted apart from the capability decision's own no_token / not_granted, so an operator can see which members a close would have turned away.
What admit/2 keeps for a link: the capabilities the token grants, the second it stops authorizing, and the member the realm endorsed at its chain's root.
Why a frame did not come from a live member of the realm (macula-realm#175, threat 6): it presented no token, its token had expired, its token was minted for another node's link, or for another realm, or it did not verify at all (invalid, which keeps the refusal's own atom in the refusal's log line).
Functions
-spec admit(presented(), #{peer := <<_:256>>, realm_key := <<_:256>>, profile := macula_crypto_profile:profile(), now := integer()}) -> {ok, rights()} | {error, too_large | macula_ucan:refusal()}.
Verify a membership token a member presented on its link: checked for size before anything is verified, then under the realm key RealmKeyId with the link peer as its audience (threat model 6.1), so a token another node presents is refused not_the_audience. The rights it grants are kept until until, the earliest exp of the token and its proofs; the link's holder drops what it admitted on them then, unless a fresh token arrived (6.2).
The extension map a SUBSCRIBE or PUBLISH carries to present Presented to the link's station: one entry, not critical, so a station that predates it ignores it and the frame is handled as before.
-spec counters() -> #{{<<_:256>> | other, check(), outcome() | member_outcome() | membership_reason()} => non_neg_integer()}.
Every decision counted since the table was created, keyed by realm (or other), check and outcome. A membership row also carries the outcome members mode only counts while it is counting (member, would_refuse) and the reason a frame would have been refused for (no_token, expired, not_the_audience, wrong_realm, invalid).
-spec extension_name() -> binary().
The name of the per-hop extension that carries a presented membership.
-spec max_presented_bytes() -> pos_integer().
The most bytes a token and its proofs together may come to.
-spec max_proofs() -> pos_integer().
The most proofs admit/2 takes with a token.
-spec max_token_bytes() -> pos_integer().
The largest membership token admit/2 verifies, in bytes.
-spec membership(mode(), <<_:256>>, membership()) -> allow | {refuse, refusal()}.
Whether a frame from a link holding Membership may act in Realm, in Mode (macula-realm#175, threat 1): the check a realm closes on. In members a live membership is the whole decision and no capability is read: a member is allowed, and anything else is refused naming realm/member and the reason, without reading a grant.
In counting and refusing it always allows, and counts what members mode would have decided as {Realm, membership, member | would_refuse} and, when it would have refused, the reason as its own outcome — the counter threat 6 is read on before a realm is closed, kept apart from the capability decision's no_token and not_granted. off does nothing.
Only the five reasons in membership_reason/0 are accepted in members: any other atom in not_member raises, rather than being put on the wire as a refusal whose reason a peer cannot name. A term that is neither {member, Rights} nor {not_member, Reason} does not match any clause, so it raises in every mode; in counting that is the first thing that happens, to the caller, rather than a counter nobody can explain.
-spec membership_reason(atom()) -> membership_reason().
Why a frame from a link is not from a live member, from what admit/2 answered (macula-realm#175, WP3): a token another realm minted is wrong_realm — its issuer is not this realm's key (not_the_issuer), or a capability it grants names another realm (wrong_realm) — one minted for another node's link is not_the_audience, one past its exp is expired, and every other refusal is invalid: malformed, signature_invalid, not_yet_valid, realm_name_not_canonical (a malformed realm NAME in a capability says nothing about which realm the token belongs to) and the capability-side refusals. The refusal's own atom stays in the refusal's reason, and in the log line of whoever reported it.
-spec mode(<<_:256>>) -> mode().
The mode Realm runs its checks in: the realm_rights application environment of macula, a map from realm id to mode with an optional default, or counting when it names neither.
-spec new_table() -> ok.
Create the counter table, owned by the calling process: macula_peering_sup, whose life is the run's.
-spec presented(macula_frame:frame()) -> {ok, presented()} | none | {error, malformed}.
The membership a received SUBSCRIBE or PUBLISH presents, read from its per-hop extension map as the frame decoder keeps it: none when it carries none, malformed when the entry is not a token and a list of proofs, each bytes. Nothing is verified here; admit/2 checks the size and verifies.
Whether the link may publish on Topic in Realm, in Mode, on the rights admit/2 kept for it: the exact bytes the station routes on, never a pattern.
Whether a provider may serve Procedure in Realm, in Mode, given what the caller or the station read from its delegation (macula_record:delegated_procedure/3): ok when the delegation names it, {error, Reason} when it does not or names no procedures at all. Counted like the other checks.
-spec serve(mode(), <<_:256>>, binary(), ok | {error, atom()}, membership()) -> allow | {refuse, refusal()}.
Whether a provider may serve Procedure in Realm, in Mode, given both what a caller or the station read from its delegation and the advertiser's membership of the realm (macula-realm#175, D3): in members an ADVERTISE needs both, so a member without the delegation is refused rpc/serve and a delegation without a live membership is refused realm/member. In every other mode the delegation decides as serve/4 does, and the membership is only counted (which is all counting and refusing do with it). Both decisions are counted, under serve and under membership.
Which refusal comes back when both fail: the delegation's. A provider with neither is told what it can fix in its own record first (rpc/serve), and what it lacks in the realm — its membership — is what it then finds on the next attempt. The membership decision is still taken and counted in that case, so the counters do not depend on which one was reported.
Whether the link may subscribe to Pattern in Realm, in Mode, on the rights admit/2 kept for it (none when it presented no token, or its token stopped authorizing). A grant must cover every topic the pattern can match (threat model 4.4, macula_ucan:grants/4). allow lets the SUBSCRIBE through; {refuse, Refusal} refuses it not_authorized, naming the capability and the pattern.
In members mode the rights map is read as a live membership and its until is NOT re-checked here: the caller keeps what admit/2 kept and drops it at until, as it already does on the capability path, and members mode relies on that. A caller that holds the map past until admits a member whose token ended — which is why the station, not this function, is the one that knows why a token ended (membership/3).
Whether a token and its proofs fit the sizes admit/2 verifies: the token and each proof within max_token_bytes/0, at most max_proofs/0 proofs, and all within max_presented_bytes/0.