Macaw.Auth.Native.Source.Ldap (macaw_auth v0.4.1)

Copy Markdown View Source

A directory as a sign-in source, over OTP's :eldap (no external LDAP library). Ported from exio's, the copy with the security fixes, plus macaw's allow_plaintext: false. The configuration is Macaw.Auth.Native.Source.Ldap.Config's:

%{type: :ldap, name: "nas", host: "nas.example.test", port: 389,
  ssl: false, starttls: true, cacertfile: "/etc/ssl/nas-ca.pem",
  user_dn_template: "uid={username},cn=users,dc=example,dc=test",
  groups: %{base_dn: "cn=groups,dc=example,dc=test", member_attr: "memberUid"},
  required_group: "macaw", role_mapping: %{"macaw-admin" => "admin"},
  default_role: "viewer"}

The flow, each failure its own reason (explain/1), for the log and mix macaw_auth.ldap.check, never for the visitor:

  1. An empty password is refused before connecting (:empty_password): LDAP treats it as an anonymous bind, which "succeeds".
  2. Connect: LDAPS, or StartTLS, verifying the certificate chain (depth: 3) and the hostname, with SNI, unless verify_peer: false (:connect_failed, :tls_failed). Without either, only with allow_plaintext: true (:plaintext_refused).
  3. Find the user's DN: the template with the username RFC 4514-escaped (Macaw.Auth.Native.Source.Ldap.Escape), or a search whose filter is built with the username as data (Macaw.Auth.Native.Source.Ldap.Filter) (:search_failed, :user_not_found, :ambiguous_user).
  4. Bind as that DN with the password (:invalid_credentials).
  5. Read the attributes, then the groups (:group_search_failed).
  6. Apply required_group (:not_in_group) and derive the roles (:no_role); the directory's username must be a valid one (:invalid_username).

Every value sent to :eldap (DN, password, filter value) goes as its UTF-8 bytes, so a non-ASCII password binds as the directory stored it. Passwords are never logged; a directory that can't be reached or searched is logged as a warning, naming the source.

Whether it knows the user (knows?/2): a directory that wasn't reached, or found no such entry, doesn't, and the chain moves on. A direct-bind source can't tell a wrong password from a missing entry (both fail the bind), so its :invalid_credentials passes on too; in search mode the entry was found, so a failed bind is final.

Summary

Functions

Runs the sign-in flow for username without signing in, and says how each step went: [{step, :ok | :error, detail}], ending at the first failure. The steps: connect, TLS, find the user's DN (the search), bind, read attributes, username, groups, required group, roles. The password is used for the bind and appears nowhere in the result.

Whether groups include name, a CN or a DN, ignoring case.

Types

reason()

@type reason() ::
  :empty_password
  | :invalid_credentials
  | :plaintext_refused
  | :connect_failed
  | :tls_failed
  | :search_failed
  | :user_not_found
  | :ambiguous_user
  | :group_search_failed
  | :not_in_group
  | :no_role
  | :invalid_username

t()

@type t() :: %Macaw.Auth.Native.Source.Ldap{
  allow_plaintext: term(),
  bind_password: term(),
  cacertfile: term(),
  default_role: term(),
  display_name_attr: term(),
  email_attr: term(),
  groups: term(),
  host: term(),
  id_attr: term(),
  name: term(),
  port: term(),
  required_group: term(),
  role_mapping: term(),
  search: term(),
  ssl: term(),
  starttls: term(),
  timeout_ms: term(),
  user_dn_template: term(),
  username_attr: term(),
  verify_peer: term()
}

Functions

diagnose(source, username, password)

@spec diagnose(t(), String.t(), String.t() | nil) :: [
  {String.t(), :ok | :error, String.t()}
]

Runs the sign-in flow for username without signing in, and says how each step went: [{step, :ok | :error, detail}], ending at the first failure. The steps: connect, TLS, find the user's DN (the search), bind, read attributes, username, groups, required group, roles. The password is used for the bind and appears nowhere in the result.

member?(groups, name)

@spec member?([%{dn: String.t(), cn: String.t() | nil}], String.t()) :: boolean()

Whether groups include name, a CN or a DN, ignoring case.