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. Set refusal: :conceal in a keyword list or map of
options to make refused invocations answer like unknown components. The
policy still receives its original options and can record the refusal.
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. By default, a refusal is returned to the client as
the application's own Snodo.Error. With refusal: :conceal, the client
receives the same error as for an unknown component. In either case, the
policy has 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 by default, or replaced by the
corresponding unknown-component error with refusal: :conceal. A refusal
hides the component during :discovery.