openvpn-mgt/ldap.go

239 lines
6.0 KiB
Go

package main
import (
"crypto/tls"
"errors"
"fmt"
"log"
"net"
"strings"
"time"
"github.com/pyke369/golang-support/rcache"
"gopkg.in/ldap.v2"
)
type ldapConfig struct {
servers []string
baseDN string
bindCn string
bindPw string
searchFilter string
attributes []string
validGroups []string
mfaType string
certAuth string
ipMin net.IP
ipMax net.IP
upgradeFrom string
routes []string
}
func (l *ldapConfig) addIPRange(s string) error {
ips := strings.Split(s, "-")
if len(ips) != 2 {
return errors.New("invalid IPs")
}
for k, v := range []*net.IP{&(l.ipMin), &(l.ipMax)} {
if ip := net.ParseIP(strings.Trim(ips[k], " ")); ip == nil {
return errors.New(fmt.Sprintf("invalid IP '%s'", ips[k]))
} else {
*v = ip
}
}
return nil
}
// auth loop. Try all auth profiles from startProfile
// return the last possible profile and the mail if we found a mail like login
func (s *OpenVpnMgt) AuthLoop(startProfile, user, pass string, overridePwdCheck bool) (string, string, string) {
login := []string{user}
profile := startProfile
mail := ""
otpSalt := ""
re := rcache.Get("^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$")
for {
if mail == "" && re.MatchString(login[0]) {
mail = login[0]
}
n := profile
for k, ldap := range s.ldap {
if ldap.upgradeFrom != profile {
continue
}
err, userOk, passOk, attributes := ldap.Auth(login, pass)
if otpSalt == "" && len(attributes) > 1 && len(attributes[1]) > 0 {
otpSalt = attributes[1][0]
}
// if there is an error, try the other configurations
if err != nil {
log.Printf("user %s not validated as %s\n", user, k)
continue
}
// we did find a valid User
if userOk {
// the login for the new auth level is given by the current one
login = attributes[0]
if passOk && profile != "" {
// it's at least the second auth level, and we have a valid
// password on 2 different auth system. It's a dupplicate
// password, let's log it
log.Printf("User %s has a dupplicate password\n", user)
}
// we have either a positive auth ok a previous valid one
if passOk || profile != "" || overridePwdCheck {
profile = k
}
}
}
// no profile update this turn, no need to continue
if n == profile {
break
}
}
return profile, mail, otpSalt
}
// override the real DialTLS function
func myDialTLS(network, addr string, config *tls.Config) (*ldap.Conn, error) {
dc, err := net.DialTimeout(network, addr, 3*time.Second)
if err != nil {
return nil, ldap.NewError(ldap.ErrorNetwork, err)
}
c := tls.Client(dc, config)
if err = c.Handshake(); err != nil {
// Handshake error, close the established connection before we return an error
dc.Close()
return nil, ldap.NewError(ldap.ErrorNetwork, err)
}
conn := ldap.NewConn(c, true)
conn.Start()
return conn, nil
}
func (conf *ldapConfig) Auth(logins []string, pass string) (e error, userOk, passOk bool, attributes [][]string) {
// special case. This configuration is a filter on the previous one
if len(conf.servers) == 0 && len(conf.validGroups) > 0 {
if inArray(logins, conf.validGroups) {
return nil, true, false, [][]string{logins}
}
}
// no server ldap or multiple login should not happen here
if len(logins) != 1 || len(conf.servers) == 0 {
return nil, false, false, nil
}
attributes = [][]string{logins}
for _, s := range conf.servers {
// we force ldaps because we can
l, err := myDialTLS("tcp", s+":636", &tls.Config{ServerName: s})
if err != nil {
log.Println(err)
continue
}
defer l.Close()
// First bind with a read only user
if err = l.Bind(conf.bindCn, conf.bindPw); err != nil {
log.Println(err)
return err, false, false, nil
}
search := append(conf.attributes, "dn")
// search the user
searchRequest := ldap.NewSearchRequest(
conf.baseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf(conf.searchFilter, logins[0]),
search,
nil,
)
sr, err := l.Search(searchRequest)
if err != nil {
log.Println(err)
return err, false, false, nil
}
if len(sr.Entries) != 1 {
return errors.New("User does not exist or too many entries returned"), false, false, nil
}
// check the attributes requested in the search
// a valid account must be part of the correct group (per instance)
ret := [][]string{}
for _, needed := range conf.attributes {
ok := false
for _, attribute := range sr.Entries[0].Attributes {
if (*attribute).Name == needed {
ret = append(ret, attribute.Values)
ok = true
}
}
if !ok {
ret = append(ret, []string{})
}
}
// user must have both the first and second attributes
if len(ret[0]) == 0 {
log.Printf("User %s has no %s attribute", logins[0], conf.attributes[0])
return nil, false, false, nil
}
if len(ret[1]) == 0 {
log.Printf("User %s has no %s attribute", logins[0], conf.attributes[1])
return nil, false, false, nil
}
// check if the first attribute valus are in the validGroups list
if len(conf.validGroups) > 0 && !inArray(conf.validGroups, ret[0]) {
return nil, false, false, nil
}
// if there is no validGroups check, pass the first attribute values to the
// next level
if len(conf.validGroups) == 0 {
attributes = [][]string{ret[0]}
} else {
attributes = [][]string{ret[1]}
}
if len(ret) > 2 {
attributes = append(attributes, ret[2:]...)
}
log.Printf("User %s has a valid account on %s", logins[0], s)
userdn := sr.Entries[0].DN
// if the password is empty, stop here
if pass == "" {
return nil, true, false, attributes
}
// if there is an error, it's because the password is invalid
if err = l.Bind(userdn, pass); err != nil {
return nil, true, false, attributes
}
// everything is fine,
log.Printf("User %s has a valid password on %s", logins[0], s)
return nil, true, true, attributes
}
// if we are here, no server is responding, rejectif auth
log.Println("can't join any ldap server")
return
}