ldapauth

package
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

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

func NormalizeDN(dn string) string

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

func TestConnection(cfg Config) error

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

func ConfigFromSettings(s *models.Settings) Config

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

func Browse(cfg Config, dn string) ([]Entry, error)

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

type ServerResult struct {
	Host  string
	Port  uint16
	Error error // nil = success
}

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

func Authenticate(cfg Config, username, password string) (bool, *UserInfo, error)

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

func LookupByEmail(cfg Config, email string) (*UserInfo, error)

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

type WritebackConfig struct {
	BindDN   string
	Password string
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL