Relyra.Provider behaviour (relyra v1.1.0)

Copy Markdown View Source

Provider preset registry for known SAML IdPs.

Presets carry safe defaults, label translations from our internal field names to the IdP's admin UI vocabulary, and known-footgun checks. They exist so that:

  • adopters get fail-closed defaults that match each IdP's quirks (e.g. Microsoft Entra's NameID :persistent requirement);
  • admin-channel error hints can speak the IdP's language (e.g. "Audience URI" for Okta, "Identifier (Entity ID)" for Entra);
  • footgun checks surface known cross-IdP gotchas before they become production incidents.

Shape

Each preset is a module implementing Relyra.Provider callbacks. They are pure data — no GenServers, no compile-time magic. The module-per- provider shape (Assent's default_config/1 idiom) plays naturally with runtime config:

config :my_app, :saml_connection,
  provider_preset: :okta,
  sp_entity_id: "https://my.app/sso/metadata",
  ...

Public API

Relyra.Provider.apply_defaults(:okta, user_keyword)
Relyra.Provider.translate_label(:okta, :sp_entity_id)
Relyra.Provider.check_footguns(:okta, %Relyra.Connection{...})
Relyra.Provider.guide_url(:okta)

Adding a preset

Create a module that @behaviour Relyra.Provider and register its id in @presets below. Keep default_config/0 strictly safer-than-spec — presets exist to narrow the trust surface, never to widen it.

Summary

Types

Footgun definition. check.(conn) returns :ok or {:warn, reason}. Severity guides whether the warning is logged or raised in strict-config validation.

Per-field translation entry.

Internal field name we expose to adopters.

Functions

Merge preset defaults under a user-supplied keyword list. User-supplied keys win — presets only fill in what the adopter omitted.

Run every footgun check defined by the preset against a resolved connection. Returns a list of results in declaration order; the caller decides whether to log warnings, raise errors, or aggregate.

Display name suitable for log lines and admin UIs.

Resolve a preset id to its implementing module. Raises if unknown so typos fail loudly at config time, not at runtime during a login flow.

Bootstrap a preset from an IdP metadata URL.

Public guide URL for the preset.

Build an admin-facing hint string suitable for Relyra.Error details. Returns nil when no preset is bound to the connection (adopters without a preset get clean errors with no hints injected).

Return the full label entry (label + section + hint) for a field. Use this when building admin error hints so the operator gets the IdP's section name and a one-liner hint, not just the label.

List supported preset ids.

Translate one of our internal field names to the IdP's admin UI label. Falls back to the underscored key as a string when the preset has no entry, so missing translations degrade gracefully.

Types

footgun()

@type footgun() :: %{
  id: atom(),
  severity: :error | :warning,
  message: String.t(),
  check: (Relyra.Connection.t() -> :ok | {:warn, String.t()})
}

Footgun definition. check.(conn) returns :ok or {:warn, reason}. Severity guides whether the warning is logged or raised in strict-config validation.

label_entry()

@type label_entry() :: %{
  :idp_label => String.t(),
  optional(:idp_section) => String.t() | nil,
  optional(:hint) => String.t() | nil
}

Per-field translation entry.

label_key()

@type label_key() ::
  :sp_entity_id
  | :acs_url
  | :idp_entity_id
  | :idp_sso_url
  | :idp_certificate
  | :name_id_format
  | :signing_algorithm
  | :digest_algorithm

Internal field name we expose to adopters.

label_map()

@type label_map() :: %{required(label_key()) => label_entry()}

Callbacks

default_config()

@callback default_config() :: keyword()

display_name()

@callback display_name() :: String.t()

footguns()

@callback footguns() :: [footgun()]

guide_url()

@callback guide_url() :: String.t()

id()

@callback id() :: atom()

labels()

@callback labels() :: label_map()

Functions

apply_defaults(preset_id, user_config)

@spec apply_defaults(
  atom(),
  keyword()
) :: keyword()

Merge preset defaults under a user-supplied keyword list. User-supplied keys win — presets only fill in what the adopter omitted.

Returns a keyword list suitable for building a Relyra.Connection.t().

check_footguns(preset_id, conn)

@spec check_footguns(atom(), Relyra.Connection.t()) :: [
  :ok | {:warn, atom(), String.t()} | {:check_failed, atom(), term()}
]

Run every footgun check defined by the preset against a resolved connection. Returns a list of results in declaration order; the caller decides whether to log warnings, raise errors, or aggregate.

Footguns whose check raises are caught and reported as {:check_failed, id, reason} so a buggy preset never breaks the consume pipeline.

display_name(preset_id)

@spec display_name(atom()) :: String.t()

Display name suitable for log lines and admin UIs.

fetch!(id)

@spec fetch!(atom()) :: module()

Resolve a preset id to its implementing module. Raises if unknown so typos fail loudly at config time, not at runtime during a login flow.

from_metadata_url(preset_id, metadata_url)

@spec from_metadata_url(atom(), String.t()) :: keyword()

Bootstrap a preset from an IdP metadata URL.

The URL is stored as :idp_metadata_url and the preset defaults are still applied underneath user overrides.

guide_url(preset_id)

@spec guide_url(atom()) :: String.t()

Public guide URL for the preset.

hint_for(connection, field)

@spec hint_for(Relyra.Connection.t() | nil, label_key()) :: String.t() | nil

Build an admin-facing hint string suitable for Relyra.Error details. Returns nil when no preset is bound to the connection (adopters without a preset get clean errors with no hints injected).

label_entry(preset_id, field)

@spec label_entry(atom(), label_key()) :: label_entry() | nil

Return the full label entry (label + section + hint) for a field. Use this when building admin error hints so the operator gets the IdP's section name and a one-liner hint, not just the label.

list()

@spec list() :: [atom()]

List supported preset ids.

translate_label(preset_id, field)

@spec translate_label(atom(), label_key()) :: String.t()

Translate one of our internal field names to the IdP's admin UI label. Falls back to the underscored key as a string when the preset has no entry, so missing translations degrade gracefully.