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. Returns nil on success.
Types ¶
type Config ¶
type Config struct {
Host string
Port 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 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.