Ectomancer.Authorization (Ectomancer v1.7.0)

Copy Markdown View Source

Authorization system for Ectomancer tools.

Supports three authorization strategies:

  1. Inline function - Simple authorization with a function

    authorize fn actor, action ->
      actor.role == :admin or action in [:list, :get]
    end
  2. Policy module - Complex authorization with a policy module

    authorize with: MyApp.Policies.UserPolicy
  3. No authorization - Public access

    authorize :none

Cascade Authorization

Authorization can be defined at three levels (from broadest to specific):

  1. Global (in use Ectomancer)
  2. Schema (in expose/2)
  3. Action (in expose/2 with action-specific rules)

When multiple levels are defined, they cascade:

  • All levels must pass for authorization to succeed
  • If any level fails, the tool returns an unauthorized error

Summary

Functions

Generates an AST quote for the authorize macro call.

Checks authorization for a tool execution.

Checks if authorization is configured.

Parses authorization configuration into a standard format.

Parses an authorization handler into a canonical form.

Like parse_handler/1 but also accepts bare atoms as policy module names. Used for global authorization configs where MyPolicyModule is a valid form.

Functions

authorize_to_ast(module)

Generates an AST quote for the authorize macro call.

check(actor, action, opts)

@spec check(any(), atom(), keyword()) ::
  :ok | {:error, String.t()} | {:ok, :scoped, (term() -> term())}

Checks authorization for a tool execution.

Parameters

  • actor - The current actor (from frame.assigns[:ectomancer_actor])
  • action - The action being performed (e.g., :list, :get, :create)
  • opts - Authorization options
    • :handler - The authorization handler (function, module, or :none)
    • :schema - The schema module (for context)
    • :parent_auth - Parent authorization config for cascading

Returns

  • :ok - Authorization passed
  • {:ok, :scoped, scope_fn} - Authorization passed with row-level scope
  • {:error, reason} - Authorization failed with reason

The scope_fn is a function that takes an Ecto query and returns a scoped query:

fn query ->
  from(u in query, where: u.org_id == ^actor.org_id)
end

When a scope is returned, it is automatically applied to all CRUD queries (list, get, update, destroy) for that tool call.

enabled?(opts)

@spec enabled?(keyword()) :: boolean()

Checks if authorization is configured.

parse_authorization_config(action_rules)

@spec parse_authorization_config(nil | :none | :public | function() | keyword()) ::
  nil | %{global: term(), actions: map()}

Parses authorization configuration into a standard format.

Returns nil for no authorization, or a map with :global handler and :actions for per-action handlers.

Forms

  • nil, :none, :publicnil
  • function or module → %{global: handler, actions: %{}}
  • keyword list with per-action rules → %{global: handler, actions: %{action: handler}}

Example

# Global admin-only auth, but list_queues is public:
expose_oban_jobs authorize: [
  all: fn actor, _ -> actor.role == :admin end,
  list_queues: :public
]

parse_handler(handler)

@spec parse_handler(any()) :: term() | nil

Parses an authorization handler into a canonical form.

Returns the handler value (function, module atom, or AST tuple), or nil for no auth. Raises ArgumentError for unknown atoms (strict parsing, used for per-tool auth).

parse_handler_for_global(module)

Like parse_handler/1 but also accepts bare atoms as policy module names. Used for global authorization configs where MyPolicyModule is a valid form.