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) }