Optional application-owned authorization at the component boundary.
The application decides whether a request context may discover or invoke a
registered component. snodo applies that decision inside the router, below
every transport and protocol dialect, and before argument validation or any
application callback. The library supplies no identity, role, token, or
policy vocabulary and never interprets the returned error.
Configure a runtime with authorization: MyApp.Policy or
authorization: {MyApp.Policy, options}; the second element is passed back
unchanged on every call. An unconfigured runtime performs no extra work.
defmodule MyApp.Policy do
@behaviour Snodo.Authorization
alias Snodo.Authorization.Component
@impl true
def authorize(phase, %Component{} = component, context, _options) do
if allowed?(context.auth, component) do
:ok
else
if phase == :invocation, do: MyApp.Audit.refused(context.auth, component)
{:error, Snodo.Error.authorization(-32_003, "Not authorized")}
end
end
endPhases
:discovery covers tools/list, prompts/list, resources/list, and
resources/templates/list. A refusal removes the component from that
response, so the client never learns the name exists.
:invocation covers tools/call, prompts/get, resources/read, and
completion/complete. A refusal is returned to the client as the
application's own Snodo.Error, which keeps a boundary violation distinct from
an unknown name and gives the policy the one place to record an audit event.
Snodo.Context.request_method names the exact operation being refused.
The policy runs once per listed component during discovery and once per invocation, so keep it allocation-light: pass a precomputed catalog through the options rather than querying a store inside the callback.
Only the enforcement seam lives here. Server capability advertisement stays
catalog-wide, because it describes the server rather than one request.
Application-owned subscription sources and negotiated extension routes
receive the same Snodo.Context and own their policy, because neither
dispatches through the router catalog.
A policy that raises, exits, throws, or returns something else is a fault rather than a decision: the operation fails with an internal error in both phases instead of silently emptying a catalog.
Summary
Callbacks
Decides whether context may discover or invoke component.
Types
@type decision() :: :ok | {:refused, Snodo.Error.t()} | {:fault, Snodo.Error.t()}
@type phase() :: :discovery | :invocation
Callbacks
@callback authorize(phase(), Snodo.Authorization.Component.t(), Snodo.Context.t(), term()) :: :ok | {:error, Snodo.Error.t()}
Decides whether context may discover or invoke component.
Return :ok to allow, or {:error, %Snodo.Error{}} to refuse. The error is
returned verbatim during :invocation and only hides the component during
:discovery.