Snodo.Authorization behaviour (snodo v0.2.0)

Copy Markdown View Source

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
end

Phases

: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

config()

@type config() :: nil | {module(), term()}

decision()

@type decision() :: :ok | {:refused, Snodo.Error.t()} | {:fault, Snodo.Error.t()}

phase()

@type phase() :: :discovery | :invocation

Callbacks

authorize(phase, t, t, term)

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