All checks were successful
continuous-integration/drone/push Build is passing
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>
189 lines
4.1 KiB
Go
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)
|
|
}
|