macula_realm_rights (macula v17.1.1)

View Source

The 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 three modes per realm (mode/1): off checks nothing, counting counts what it would refuse and lets it through, and refusing refuses it. 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).

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.

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.

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.

A refusal names the check's capability, the resource it was asked for, and why.

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.

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.

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 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

check/0

-type check() :: subscribe | publish | serve.

mode/0

-type mode() :: off | counting | refusing.

outcome/0

-type outcome() :: authorised | would_refuse | refused | exempt.

What a member presented on its link: its membership token and the proofs of its chain, each as it travelled.

presented/0

-type presented() :: #{token := binary(), proofs := [binary()]}.

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.

refusal/0

-type refusal() :: #{can := binary(), resource := binary(), reason := atom()}.

rights/0

-type rights() :: #{caps := [map()], until := integer(), member := <<_:256>>}.

A refusal names the check's capability, the resource it was asked for, and why.

Functions

admit(Presented, Context)

-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).

carrying(_)

-spec carrying(presented()) -> #{binary() => #{critical := 0, value := map()}}.

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.

counters()

-spec counters() -> #{{<<_:256>> | other, check(), outcome()} => non_neg_integer()}.

Every decision counted since the table was created, keyed by realm (or other), check and outcome.

extension_name()

-spec extension_name() -> binary().

The name of the per-hop extension that carries a presented membership.

max_presented_bytes()

-spec max_presented_bytes() -> pos_integer().

The most bytes a token and its proofs together may come to.

max_proofs()

-spec max_proofs() -> pos_integer().

The most proofs admit/2 takes with a token.

max_token_bytes()

-spec max_token_bytes() -> pos_integer().

The largest membership token admit/2 verifies, in bytes.

mode(Realm)

-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.

new_table()

-spec new_table() -> ok.

Create the counter table, owned by the calling process: macula_peering_sup, whose life is the run's.

presented(Frame)

-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.

publish(Mode, Realm, Rights, Topic)

-spec publish(mode(), <<_:256>>, rights() | none, binary()) -> allow | {refuse, refusal()}.

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.

serve(Mode, Realm, Procedure, Verdict)

-spec serve(mode(), <<_:256>>, binary(), ok | {error, atom()}) -> allow | {refuse, refusal()}.

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.

subscribe(Mode, Realm, Rights, Pattern)

-spec subscribe(mode(), <<_:256>>, rights() | none, binary()) -> allow | {refuse, refusal()}.

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.

within_size(_)

-spec within_size(presented()) -> boolean().

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.