Documentation
¶
Overview ¶
Package ldapauth implements the search-and-bind flow used to authenticate a user against an LDAP/Active Directory server and to read the attributes (email, display name, group membership) needed for local auto-provisioning and role sync. It has no DB dependency beyond the Config it's handed - see web/auth.go for how it's wired into the local-first login flow, and models.Settings for where the connection config is stored.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ChangePassword ¶
func ChangePassword(cfg Config, wb WritebackConfig, username, newPassword string) error
ChangePassword looks up username's DN using cfg's ordinary search/bind service account (same lookup as Authenticate), then rebinds on a fresh connection as wb's elevated identity to perform the actual password write. The write itself is server-specific, driven by cfg.ServerType:
- "generic" (OpenLDAP-compatible): the RFC 3062 password-modify extended operation, which lets the server pick the correct password hash scheme itself.
- "ad": Active Directory doesn't implement RFC 3062. The password must be written directly to the unicodePwd attribute, UTF-16LE-encoded and quoted, which AD refuses outside an encrypted connection - so TLSMode == "none" is rejected up front with a clear error instead of an opaque failure from the wire.
func NormalizeDN ¶
NormalizeDN lowercases and trims a DN for case-insensitive comparison against LdapRoleMapping.GroupDN - AD/LDAP DN string comparison is case-insensitive per RFC 4514 for the attribute types in normal use here.
func TestConnection ¶
TestConnection dials, binds as the service account and issues a zero-result-tolerant search under BaseDN - used by the admin "Test Connection" button (via TestServers, one host at a time). Returns nil on success.
Types ¶
type Config ¶
type Config struct {
Host string
Port uint16
// Host2/Port2 are an optional second server for redundancy. connect()
// tries Host first and falls back to Host2 if dialing Host fails.
// Port2 of 0 means "same as Port".
Host2 string
Port2 uint16
TLSMode string // "none" | "starttls" | "ldaps"
SkipTLSVerify bool
// ServerType is "ad" | "generic" (OpenLDAP-compatible). Server-specific
// behavior that can't be expressed the same way on both (e.g. how a
// password change is written back) branches on this.
ServerType string
BindDN string
BindPassword string
BaseDN string
// UserFilter is a fmt.Sprintf template with one %s for the escaped
// username, e.g. "(sAMAccountName=%s)" for AD or "(uid=%s)" for
// OpenLDAP.
UserFilter string
// AttrUsername is the attribute holding the value used as the login
// username (the value UserFilter's %s is matched against) -
// "sAMAccountName" for AD, "uid" for OpenLDAP by default. Ordinary
// login already knows the username (it's what was typed into the
// form); this is only needed by LookupByEmail, which has to derive it
// from the directory instead.
AttrUsername string
AttrEmail string
AttrDisplayName string
AttrMobile string
// AttrGroups is the attribute holding group DNs on the user entry, e.g.
// "memberOf" for both AD and OpenLDAP (with the memberOf overlay).
AttrGroups string
}
Config is a projection of models.Settings' LDAP fields, kept separate so this package doesn't need to import gorm or know about the DB row shape.
func ConfigFromSettings ¶
ConfigFromSettings builds a Config from the DB-backed Settings row. Blank attribute fields fall back to their standard AD/OpenLDAP name rather than being left unrequested - an admin who just enables LDAP and fills in the connection details (without also typing out every attribute name) still gets Name/Email/Mobile/group-based roles populated correctly, instead of them silently staying empty (Name then falling back further, to the raw login username).
type Entry ¶
type Entry struct {
DN string
RDN string // display label - the value of the DN's leftmost attribute, e.g. "Admins"
ObjectClasses []string
IsGroup bool
}
Entry is one child node returned by Browse - enough for a tree-browser UI to render a label, know whether to offer it as a selectable group, and request its own children on expand (by DN).
func Browse ¶
Browse lists the immediate children of dn (or cfg.BaseDN if dn is blank) for a tree-browser UI - used so an admin setting up a group->role mapping can navigate the directory and click the group they mean instead of having to know/type its exact DN. Binds as the configured service account, same as TestConnection/Authenticate.
type ServerResult ¶ added in v1.0.2
ServerResult is one host's outcome from TestServers.
func TestServers ¶ added in v1.0.2
func TestServers(cfg Config) []ServerResult
TestServers dials+binds each configured server independently (no failover between them) so the admin "Test Connection" button can report whether redundancy actually works, not just that one of the two happened to answer. An empty Host yields an empty slice.
type UserInfo ¶
type UserInfo struct {
DN string
// Username is the value of Config.AttrUsername on the matched entry -
// only populated by LookupByEmail, which (unlike Authenticate) doesn't
// already know it from the caller's login input.
Username string
Email string
DisplayName string
Mobile string
Groups []string // raw group DNs from Config.AttrGroups
}
UserInfo is what a successful directory lookup yields for provisioning and role-sync purposes.
func Authenticate ¶
Authenticate performs the search+bind flow: bind as the configured service account, search BaseDN with UserFilter for username, then re-bind (on a fresh connection) as the single matched DN with password to verify credentials.
Return contract mirrors web.AuthenticateUser's existing split:
- (true, info, nil): credentials verified, info populated.
- (false, nil, nil): bad credentials - service bind ok, but either no user matched the filter or the user-DN bind was rejected (LDAPResultInvalidCredentials). Same shape as a wrong password.
- (false, nil, err): infra/config problem - dial failure, service-account bind failure, malformed filter, search error, ambiguous match (>1 entries - almost certainly a misconfigured UserFilter), or any bind failure that isn't LDAPResultInvalidCredentials. Callers should treat this as a real error (e.g. 500), not silently show "wrong password".
func LookupByEmail ¶
LookupByEmail searches for a directory entry whose AttrEmail attribute matches email, binding as the same read-only service account Authenticate/Browse use. Unlike Authenticate, it never verifies a password - a match only means "an account with this email exists in the directory," not that the caller is that person - so callers must not treat the result as an authenticated identity. Used by the forgot-password flow to auto-provision an LDAP user's local row before they've ever logged in (see web.provisionLDAPUserByEmail), the one case where a login attempt (and thus Authenticate, which already knows the username) hasn't happened yet.
Return contract mirrors Authenticate's:
- (info, nil): exactly one entry matched, info.Username populated from AttrUsername.
- (nil, nil): no entry matched, or Config.AttrEmail/AttrUsername is blank - not an error.
- (nil, err): infra/config problem, or an ambiguous match (>1 entries).
type WritebackConfig ¶
WritebackConfig is the elevated identity permitted to reset another user's password - deliberately separate from Config.BindDN/BindPassword (the read-only search/bind service account, DB-stored via models.Settings and returned in full by GET /api/admin/settings). Password write-back needs much stronger directory permissions than a search bind, so this comes from util.ConfigRoot.LdapWriteback (YAML config file only, never the DB) and is passed in by the caller, same as Config itself - this package still has no DB dependency of its own.