macula_authorization (macula v0.20.5)

View Source

Mesh Authorization Module for UCAN/DID-based access control.

This module implements decentralized authorization for Macula mesh operations using UCAN (User Controlled Authorization Networks) and DID (Decentralized Identifiers). All DID parsing and UCAN validation is implemented inline with no external dependencies.

Namespace Ownership Model

DIDs map to namespaces they own: - did:macula:io.macula.rgfaber owns io.macula.rgfaber.* - Parent DIDs can access child namespaces (hierarchical) - did:macula:io.macula can access everything in io.macula.*

Authorization Flow

1. Extract caller DID from connection/message 2. Parse topic/procedure to extract namespace 3. Check if caller owns namespace → Allow 4. If not owner, check for valid UCAN grant → Allow/Deny

Public Topics

Topics containing .public. segment are world-readable: - io.macula.rgfaber.public.announcements → Anyone can subscribe - Publishing still requires ownership or UCAN grant

Summary

Functions

Check if caller is authorized to announce/declare a procedure.

Check if a capability grants access to a resource for an operation.

Check if caller DID owns a namespace.

Check if caller is authorized to publish to a topic.

Check if caller is authorized to make an RPC call.

Check if caller is authorized to subscribe to a topic.

Check if caller is authorized to subscribe with UCAN support. Full version with UCAN token parameter.

Extract the identity portion from a DID.

Extract namespace from a topic or procedure.

Check if one namespace is an ancestor of another.

Check if a topic is public (contains .public. segment).

Resolve and validate a caller DID.

Validate a UCAN token for a specific operation.

Types

auth_error/0

-type auth_error() ::
          unauthorized | invalid_ucan | expired_ucan | revoked_ucan | insufficient_capability |
          invalid_did | namespace_mismatch.

auth_opts/0

-type auth_opts() :: #{atom() => term()}.

auth_result/0

-type auth_result() :: {ok, authorized} | {error, auth_error()}.

did/0

-type did() :: binary().

namespace/0

-type namespace() :: binary().

operation/0

-type operation() :: binary().

procedure/0

-type procedure() :: binary().

topic/0

-type topic() :: binary().

ucan_token/0

-type ucan_token() :: binary() | undefined.

Functions

check_announce(CallerDID, Procedure, Opts)

-spec check_announce(CallerDID :: did(), Procedure :: procedure(), Opts :: auth_opts()) -> auth_result().

Check if caller is authorized to announce/declare a procedure.

Announcing ALWAYS requires namespace ownership. UCAN grants cannot give announce rights (prevents namespace hijacking).

check_capability_match(Capability, Resource, Operation)

-spec check_capability_match(Capability :: map(), Resource :: binary(), Operation :: operation()) ->
                                boolean().

Check if a capability grants access to a resource for an operation.

Capability format: #{<<"with">> => Resource, <<"can">> => Operation}

Wildcards supported: - io.macula.rgfaber.* matches any resource in namespace - mesh:* matches any mesh operation

check_namespace_ownership(CallerDID, Namespace)

-spec check_namespace_ownership(CallerDID :: did(), Namespace :: namespace()) ->
                                   {ok, owner | ancestor} | {error, not_owner}.

Check if caller DID owns a namespace.

Returns: - {ok, owner} if DID identity matches namespace exactly - {ok, ancestor} if DID identity is parent of namespace - {error, not_owner} otherwise

check_publish(CallerDID, Topic, UcanToken, Opts)

-spec check_publish(CallerDID :: did(),
                    Topic :: topic(),
                    UcanToken :: ucan_token(),
                    Opts :: auth_opts()) ->
                       auth_result().

Check if caller is authorized to publish to a topic.

Publishing requires ownership or UCAN grant. Public topics do NOT grant publish rights.

check_rpc_call(CallerDID, Procedure, UcanToken, Opts)

-spec check_rpc_call(CallerDID :: did(),
                     Procedure :: procedure(),
                     UcanToken :: ucan_token(),
                     Opts :: auth_opts()) ->
                        auth_result().

Check if caller is authorized to make an RPC call.

Authorization succeeds if: 1. Caller owns the procedure's namespace, OR 2. Caller is ancestor of the namespace (parent access), OR 3. Caller has a valid UCAN with mesh:call capability

check_subscribe(CallerDID, Topic, Opts)

-spec check_subscribe(CallerDID :: did(), Topic :: topic(), Opts :: auth_opts()) -> auth_result().

Check if caller is authorized to subscribe to a topic.

Authorization succeeds if: 1. Topic is public (contains .public.), OR 2. Caller owns the namespace, OR 3. Caller is ancestor, OR 4. Caller has UCAN with mesh:subscribe capability

check_subscribe(CallerDID, Topic, UcanToken, Opts)

-spec check_subscribe(CallerDID :: did(),
                      Topic :: topic(),
                      UcanToken :: ucan_token(),
                      Opts :: auth_opts()) ->
                         auth_result().

Check if caller is authorized to subscribe with UCAN support. Full version with UCAN token parameter.

extract_identity_from_did(DID)

-spec extract_identity_from_did(DID :: did()) -> {ok, Identity :: binary()} | {error, invalid_did}.

Extract the identity portion from a DID.

did:macula:io.macula.rgfaberio.macula.rgfaber

Uses macula_did_cache for performance - repeated lookups for the same DID return cached results without re-parsing.

extract_namespace(TopicOrProcedure)

-spec extract_namespace(TopicOrProcedure :: binary()) -> namespace().

Extract namespace from a topic or procedure.

Namespace is the first 3 segments of a dotted path: - io.macula.rgfaber.place_orderio.macula.rgfaber - io.macula.public.eventsio.macula.public - Short topics return the topic itself as namespace.

is_ancestor_namespace(Parent, Child)

-spec is_ancestor_namespace(Parent :: namespace(), Child :: namespace()) -> boolean().

Check if one namespace is an ancestor of another.

io.macula is ancestor of io.macula.rgfaberio.macula.rgfaber is ancestor of io.macula.rgfaber.services

is_public_topic(Topic)

-spec is_public_topic(Topic :: topic()) -> boolean().

Check if a topic is public (contains .public. segment).

Public topics allow subscription without ownership or UCAN.

resolve_caller_did(CallerDID)

-spec resolve_caller_did(CallerDID :: did()) -> {ok, Components :: map()} | {error, invalid_did}.

Resolve and validate a caller DID.

Parses the DID and returns the parsed components if valid. Uses macula_did_cache for performance.

validate_ucan_for_operation(UcanToken, CallerDID, Resource, Operation)

-spec validate_ucan_for_operation(UcanToken :: ucan_token(),
                                  CallerDID :: did(),
                                  Resource :: binary(),
                                  Operation :: operation()) ->
                                     auth_result().

Validate a UCAN token for a specific operation.

Checks: 1. Token is well-formed and not expired 2. Audience matches caller DID 3. Token has required capability for operation 4. Token is not revoked