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:
- An empty password is refused before connecting (
:empty_password): LDAP treats it as an anonymous bind, which "succeeds". - Connect: LDAPS, or StartTLS, verifying the certificate chain (
depth: 3) and the hostname, with SNI, unlessverify_peer: false(:connect_failed,:tls_failed). Without either, only withallow_plaintext: true(:plaintext_refused). - 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). - Bind as that DN with the password (
:invalid_credentials). - Read the attributes, then the groups (
:group_search_failed). - 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
@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
@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
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.