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 ¶
- Variables
- func ChangePassword(cfg Config, wb WritebackConfig, username, newPassword string) error
- func CreateGroup(cfg Config, wb WritebackConfig, in GroupInput) (string, error)
- func CreatePerson(cfg Config, wb WritebackConfig, in PersonInput) (string, error)
- func DNBelowBase(dn, base string) bool
- func DNInScope(dn, base string) bool
- func DeleteGroup(cfg Config, wb WritebackConfig, dn string) error
- func DeletePerson(cfg Config, wb WritebackConfig, dn string) error
- func EntryKind(objectClasses []string) string
- func LookupGroups(cfg Config, username string) (groups []string, found bool, err error)
- func NormalizeDN(dn string) string
- func TestConnection(cfg Config) error
- func UpdateGroup(cfg Config, wb WritebackConfig, in GroupInput) (string, error)
- func UpdatePerson(cfg Config, wb WritebackConfig, in PersonInput) (string, error)
- type Config
- type Entry
- type GroupInput
- type PersonInput
- type Record
- type ServerResult
- type UserInfo
- type WritebackConfig
Constants ¶
This section is empty.
Variables ¶
var ErrADReadOnly = errors.New("ldap: Active Directory accounts are managed with Active Directory's own tools")
ErrADReadOnly is returned when a directory write is attempted against an Active Directory server. Factum browses AD and can reset passwords through the existing write-back path, but account and group edits stay in AD's own tools.
var ErrWritebackMissing = errors.New("ldap: directory changes need ldap_writeback.bind_dn and ldap_writeback.bind_password in the config file")
ErrWritebackMissing is returned when a directory write is attempted without the elevated config-file identity.
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 CreateGroup ¶ added in v1.5.0
func CreateGroup(cfg Config, wb WritebackConfig, in GroupInput) (string, error)
CreateGroup adds a groupOfNames entry. Members must be DNs under the base.
func CreatePerson ¶ added in v1.5.0
func CreatePerson(cfg Config, wb WritebackConfig, in PersonInput) (string, error)
CreatePerson adds an inetOrgPerson under parent and sets its password.
func DNBelowBase ¶ added in v1.5.0
DNBelowBase reports whether dn is strictly under base.
func DNInScope ¶ added in v1.5.0
DNInScope reports whether dn is the base itself or an entry under it. Comparison follows NormalizeDN (case-insensitive, no RFC 4514 folding).
func DeleteGroup ¶ added in v1.5.0
func DeleteGroup(cfg Config, wb WritebackConfig, dn string) error
DeleteGroup deletes a group entry. The caller removes Factum rows that stored the DN.
func DeletePerson ¶ added in v1.5.0
func DeletePerson(cfg Config, wb WritebackConfig, dn string) error
DeletePerson removes the user from group member attributes, then deletes the entry. The local Factum shadow row is left for the caller.
func EntryKind ¶ added in v1.5.0
EntryKind classifies objectClass values for the directory tree. Group wins over person, and person wins over container.
func LookupGroups ¶ added in v1.2.0
LookupGroups searches for username with the service account and returns memberOf. It does not check a password. found is false when no entry matches. Callers that already proved the password another way (MS-CHAPv2 against Active Directory) use this for the device-role check.
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 search identity (anonymous when BindDN is blank) 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.
func UpdateGroup ¶ added in v1.5.0
func UpdateGroup(cfg Config, wb WritebackConfig, in GroupInput) (string, error)
UpdateGroup changes a groupOfNames or groupOfUniqueNames entry. A cn change renames a cn-RDN entry and returns the new DN so the caller can retarget role mappings. posixGroup and Active Directory groups are refused.
func UpdatePerson ¶ added in v1.5.0
func UpdatePerson(cfg Config, wb WritebackConfig, in PersonInput) (string, error)
UpdatePerson changes an inetOrgPerson's attributes. A uid change renames the entry when uid is the RDN, and group member attributes that pointed at the old DN are rewritten. Password, when set, is a separate password-modify after the attribute update.
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/BindPassword are the search identity. Both may be blank:
// that is an anonymous bind (RFC 4513 §5.1.1). A non-empty BindDN
// still requires a password - go-ldap (and bindService) refuse an
// empty password in that case, because some servers treat it as an
// unauthenticated bind that succeeds without checking anything.
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 ConfigFromServer ¶ added in v1.5.0
func ConfigFromServer(s *models.LdapServer) Config
ConfigFromServer builds a Config for one ldap_servers row. Host2 is unused: failover is the ordered list of rows, not a second host on the same config.
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
// Kind is "person", "group", "container", or "other". The directory
// tree uses it to decide which entries are leaves and which can be edited.
Kind string
}
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 search identity, same as TestConnection/Authenticate.
type GroupInput ¶ added in v1.5.0
GroupInput is a groupOfNames entry. Members are user DNs. A groupOfNames entry must have at least one member.
type PersonInput ¶ added in v1.5.0
type PersonInput struct {
DN string
ParentDN string
UID string
CN string
SN string
Mail string
DisplayName string
Mobile string
Password string
}
PersonInput is a new or updated inetOrgPerson. ParentDN is the container the entry is created under. DN is the existing entry on update. Password is required on create and optional on update; it is applied with the RFC 3062 password-modify operation so the server chooses the hash.
type Record ¶ added in v1.5.0
type Record struct {
DN string
RDN string
Kind string
ObjectClasses []string
UID string
CN string
SN string
Mail string
DisplayName string
Mobile string
Description string
Members []string
// MemberAttr is "member" or "uniqueMember" for a group Factum can edit.
// Empty for a group type that is only shown (posixGroup, AD group).
MemberAttr string
}
Record is one directory entry's editable attributes. Members is set for groups (member or uniqueMember). Person fields are set for people.
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 search identity (anonymous when BindDN is blank), 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.