summaryrefslogblamecommitdiffstats
path: root/pamldapd.go
blob: af1d36adde7585e64de43f875f352586b89da053 (plain) (tree)














































































































































                                                                                                                                                            
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
}