summaryrefslogtreecommitdiffstats
path: root/pamldapd.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--pamldapd.go143
1 files changed, 143 insertions, 0 deletions
diff --git a/pamldapd.go b/pamldapd.go
new file mode 100644
index 0000000..af1d36a
--- /dev/null
+++ b/pamldapd.go
@@ -0,0 +1,143 @@
+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
+}