package main
import (
"errors"
"fmt"
"github.com/msteinert/pam"
"github.com/nmcclain/ldap"
"log"
"net"
"os"
"os/user"
"strings"
)
type Backend struct {
ldap.Binder
ldap.Searcher
ldap.Closer
logger *log.Logger
BaseDN string
PAMServiceName string
}
var logger = log.New(os.Stdout, "", log.LstdFlags)
func main() {
logger.Println("start")
l := ldap.NewServer()
l.EnforceLDAP = true
var handler = Backend{
PAMServiceName: "password-auth",
logger: logger,
BaseDN: "dc=example,dc=com",
}
l.BindFunc("", handler)
l.SearchFunc("", handler)
l.CloseFunc("", handler)
if err := l.ListenAndServe("0.0.0.0:10893"); err != nil {
logger.Fatalf("LDAP serve failed: %s", err.Error())
}
}
func (b Backend) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCode ldap.LDAPResultCode, err error) {
b.logger.Printf("Bind attempt addr=%s bindDN=%s", conn.RemoteAddr().String(), bindDN)
var username string
if username, err = b.getUserNameFromBindDN(bindDN); err != nil {
return ldap.LDAPResultInvalidCredentials, err
}
if err := PAMAuth(b.PAMServiceName, username, bindSimplePw); err != nil {
return ldap.LDAPResultInvalidCredentials, err
}
return ldap.LDAPResultSuccess, nil
}
func (b Backend) Search(bindDN string, req ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) {
b.logger.Printf("Search bindDN=%s baseDN=%s filter=%s addr=%s", bindDN, req.BaseDN, req.Filter, conn.RemoteAddr().String())
var username string
if username, err = b.getUserNameFromBindDN(bindDN); err != nil {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err
}
filterEntity, err := ldap.GetFilterObjectClass(req.Filter)
if err != nil {
b.logger.Printf("Search Error: error parsing filter: %s", req.Filter)
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
}
if filterEntity == "posixaccount" || filterEntity == "" {
var entry *ldap.Entry
if entry, err = b.makeSearchEntry(bindDN, username); err != nil {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err
}
return ldap.ServerSearchResult{[]*ldap.Entry{entry}, []string{}, []ldap.Control{}, ldap.LDAPResultSuccess}, nil
} else {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("filter entity could be: posixaccount")
}
return ldap.ServerSearchResult{make([]*ldap.Entry, 0), []string{}, []ldap.Control{}, ldap.LDAPResultSuccess}, nil
}
func (b Backend) Close(bindDN string, conn net.Conn) (err error) {
b.logger.Printf("Close addr=%s bindDN=%s", conn.RemoteAddr().String(), bindDN)
return nil
}
func PAMAuth(serviceName, userName, passwd string) error {
t, err := pam.StartFunc(serviceName, userName, func(s pam.Style, msg string) (string, error) {
switch s {
case pam.PromptEchoOff:
return passwd, nil
case pam.PromptEchoOn, pam.ErrorMsg, pam.TextInfo:
return "", nil
}
return "", errors.New("Unrecognized PAM message style")
})
if err != nil {
return err
}
if err = t.Authenticate(0); err != nil {
return err
}
return nil
}
func (b Backend) getUserNameFromBindDN(bindDN string) (username string, err error) {
if bindDN == "" {
return "", errors.New("bindDN not specified")
}
if !strings.HasSuffix(bindDN, ","+b.BaseDN) {
return "", errors.New("bindDN not matched")
}
rest := strings.TrimSuffix(bindDN, ","+b.BaseDN)
if rest == "" {
return "", errors.New("bindDN format error")
}
if strings.Contains(rest, ",") {
return "", errors.New("bindDN has too much entities")
}
if !strings.HasPrefix(rest, "uid=") {
return "", errors.New("bindDN contains no uid entry")
}
username = strings.TrimPrefix(rest, "uid=")
return username, nil
}
func (b Backend) makeSearchEntry(dn string, username string) (entry *ldap.Entry, err error) {
attrs := []*ldap.EntryAttribute{}
var u *user.User
if u, err = user.Lookup(username); err != nil {
return entry, err
}
attrs = append(attrs, &ldap.EntryAttribute{"objectClass", []string{"posixAccount"}})
attrs = append(attrs, &ldap.EntryAttribute{"cn", []string{username}})
attrs = append(attrs, &ldap.EntryAttribute{"uid", []string{username}})
attrs = append(attrs, &ldap.EntryAttribute{"uidNumber", []string{u.Uid}})
attrs = append(attrs, &ldap.EntryAttribute{"givenName", []string{u.Name}})
attrs = append(attrs, &ldap.EntryAttribute{"gidNumber", []string{u.Gid}})
attrs = append(attrs, &ldap.EntryAttribute{"homeDirectory", []string{u.HomeDir}})
entry = &ldap.Entry{dn, attrs}
return entry, nil
}