chldapasswd/ldap.go
Pierre-Olivier Mercier e6bca3ac8f
All checks were successful
continuous-integration/drone/push Build is passing
fix(ldap): split ambiguous error messages in SearchDN and GetEntry
Distinguish between "not found" and "multiple entries found" instead of
the generic "User does not exist or too many entries returned", making
it easier to diagnose issues in logs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 17:07:41 +07:00

189 lines
4.1 KiB
Go

package main
import (
"crypto/rand"
"crypto/tls"
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
"github.com/amoghe/go-crypt"
"github.com/go-ldap/ldap/v3"
)
type LDAP struct {
Host string
Port int
Starttls bool
Ssl bool
BaseDN string
ServiceDN string
ServicePassword string
}
type SMTPConfig struct {
MailHost string
MailPort int
MailUser string
MailPassword string
MailFrom string
}
func (l LDAP) Connect() (*LDAPConn, error) {
addr := fmt.Sprintf("%s:%d", l.Host, l.Port)
var opts []ldap.DialOpt
if l.Ssl {
opts = append(opts, ldap.DialWithTLSConfig(&tls.Config{ServerName: l.Host}))
}
scheme := "ldap"
if l.Ssl {
scheme = "ldaps"
}
c, err := ldap.DialURL(fmt.Sprintf("%s://%s", scheme, addr), opts...)
if err != nil {
return nil, fmt.Errorf("unable to establish %s connection to %s: %w", strings.ToUpper(scheme), addr, err)
}
if l.Starttls {
if err = c.StartTLS(&tls.Config{ServerName: l.Host}); err != nil {
c.Close()
return nil, fmt.Errorf("unable to StartTLS: %w", err)
}
}
return &LDAPConn{
LDAP: l,
connection: c,
}, nil
}
type LDAPConn struct {
LDAP
connection *ldap.Conn
}
func (l LDAPConn) Close() {
l.connection.Close()
}
func (l LDAPConn) ServiceBind() error {
return l.connection.Bind(l.ServiceDN, l.ServicePassword)
}
func (l LDAPConn) Bind(username string, password string) error {
return l.connection.Bind(username, password)
}
func (l LDAPConn) SearchDN(username string, person bool) (string, error) {
objectClass := "organizationalPerson"
if !person {
objectClass = "simpleSecurityObject"
}
searchRequest := ldap.NewSearchRequest(
l.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass=%s)(uid=%s))", ldap.EscapeFilter(objectClass), ldap.EscapeFilter(username)),
[]string{"dn"},
nil,
)
sr, err := l.connection.Search(searchRequest)
if err != nil {
return "", err
}
if len(sr.Entries) == 0 {
return "", fmt.Errorf("user %q not found", username)
}
if len(sr.Entries) > 1 {
return "", fmt.Errorf("multiple entries (%d) found for user %q", len(sr.Entries), username)
}
return sr.Entries[0].DN, nil
}
func (l LDAPConn) GetEntry(dn string) ([]*ldap.EntryAttribute, error) {
searchRequest := ldap.NewSearchRequest(
dn,
ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false,
"(objectClass=*)", []string{}, nil,
)
sr, err := l.connection.Search(searchRequest)
if err != nil {
return nil, err
}
if len(sr.Entries) == 0 {
return nil, fmt.Errorf("entry not found for DN %q", dn)
}
if len(sr.Entries) > 1 {
return nil, fmt.Errorf("multiple entries (%d) found for DN %q", len(sr.Entries), dn)
}
return sr.Entries[0].Attributes, nil
}
func genSalt() (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(b), nil
}
func (l LDAPConn) ChangePassword(dn string, rawpassword string) error {
salt, err := genSalt()
if err != nil {
return err
}
hashedpasswd, err := crypt.Crypt(rawpassword, "$6$"+salt+"$")
if err != nil {
return err
}
modify := ldap.NewModifyRequest(dn, nil)
modify.Replace("userPassword", []string{"{CRYPT}" + hashedpasswd})
modify.Replace("shadowLastChange", []string{strconv.FormatInt(time.Now().Unix()/86400, 10)})
return l.connection.Modify(modify)
}
func (l LDAPConn) AddMailAlias(dn string, alias string) error {
modify := ldap.NewModifyRequest(dn, nil)
modify.Add("mailAlias", []string{alias})
return l.connection.Modify(modify)
}
func (l LDAPConn) SearchMailAlias(address string) (int, error) {
searchRequest := ldap.NewSearchRequest(
l.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&(objectClass=*)(mailAlias=%s))", ldap.EscapeFilter(address)),
[]string{"dn"},
nil,
)
sr, err := l.connection.Search(searchRequest)
if err != nil {
return -1, err
}
return len(sr.Entries), nil
}
func (l LDAPConn) DelMailAlias(dn string, alias string) error {
modify := ldap.NewModifyRequest(dn, nil)
modify.Delete("mailAlias", []string{alias})
return l.connection.Modify(modify)
}