Initial QSfera import
This commit is contained in:
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Config bundles server configuration settings.
|
||||
type Config struct {
|
||||
Logger logrus.FieldLogger
|
||||
|
||||
LDAPHandler string
|
||||
|
||||
LDAPListenAddr string
|
||||
LDAPSListenAddr string
|
||||
|
||||
TLSCertFile string
|
||||
TLSKeyFile string
|
||||
|
||||
LDAPBaseDN string
|
||||
LDAPAdminDN string
|
||||
|
||||
LDAPAllowLocalAnonymousBind bool
|
||||
|
||||
BoltDBFile string
|
||||
|
||||
LDIFMain string
|
||||
LDIFConfig string
|
||||
|
||||
LDIFDefaultCompany string
|
||||
LDIFDefaultMailDomain string
|
||||
LDIFTemplateExtraVars map[string]interface{}
|
||||
|
||||
Metrics prometheus.Registerer
|
||||
|
||||
OnReady func(*Server)
|
||||
}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapdn"
|
||||
"github.com/libregraph/idm/pkg/ldapentry"
|
||||
"github.com/libregraph/idm/pkg/ldappassword"
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/libregraph/idm/pkg/ldbbolt"
|
||||
"github.com/libregraph/idm/server/handler"
|
||||
)
|
||||
|
||||
type boltdbHandler struct {
|
||||
logger logrus.FieldLogger
|
||||
dbfile string
|
||||
baseDN string
|
||||
adminDN string
|
||||
allowLocalAnonymousBind bool
|
||||
ctx context.Context
|
||||
bdb *ldbbolt.LdbBolt
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
BaseDN string
|
||||
AdminDN string
|
||||
|
||||
AllowLocalAnonymousBind bool
|
||||
}
|
||||
|
||||
func NewBoltDBHandler(logger logrus.FieldLogger, fn string, options *Options) (handler.Handler, error) {
|
||||
if fn == "" {
|
||||
return nil, fmt.Errorf("file name is empty")
|
||||
}
|
||||
if options.BaseDN == "" {
|
||||
return nil, fmt.Errorf("base dn is empty")
|
||||
}
|
||||
|
||||
fn, err := filepath.Abs(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h := &boltdbHandler{
|
||||
logger: logger,
|
||||
dbfile: fn,
|
||||
|
||||
allowLocalAnonymousBind: options.AllowLocalAnonymousBind,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
if h.baseDN, err = ldapdn.ParseNormalize(options.BaseDN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h.adminDN, err = ldapdn.ParseNormalize(options.AdminDN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = h.setup()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) setup() error {
|
||||
bdb := &ldbbolt.LdbBolt{}
|
||||
|
||||
if err := bdb.Configure(h.logger, h.baseDN, h.dbfile, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := bdb.Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
h.bdb = bdb
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Add(boundDN string, req *ldap.AddRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "add",
|
||||
"bind_dn": boundDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
if !h.writeAllowed(boundDN) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
e := ldapentry.EntryFromAddRequest(req)
|
||||
|
||||
if err := h.bdb.EntryPut(e); err != nil {
|
||||
logger.WithError(err).WithField("entrydn", e.DN).Debugln("ldap add failed")
|
||||
if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
|
||||
return ldap.LDAPResultEntryAlreadyExists, nil
|
||||
}
|
||||
return ldap.LDAPResultUnwillingToPerform, err
|
||||
}
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "bind",
|
||||
"bind_dn": bindDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
// Handle anoymous bind
|
||||
if bindDN == "" {
|
||||
if !h.allowLocalAnonymousBind {
|
||||
logger.Debugln("ldap anonymous Bind disabled")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
} else if bindSimplePw == "" {
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
}
|
||||
|
||||
bindDN, err := ldapdn.ParseNormalize(bindDN)
|
||||
if err != nil {
|
||||
logger.WithError(err).Debugln("ldap bind request BindDN validation failed")
|
||||
return ldap.LDAPResultInvalidDNSyntax, nil
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(bindDN, h.baseDN) {
|
||||
logger.WithError(err).Debugln("ldap bind request BindDN outside of Database tree")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
// Disallow empty password
|
||||
if bindSimplePw == "" {
|
||||
logger.Debugf("BindDN without password")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
return h.validatePassword(logger, bindDN, bindSimplePw)
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) validatePassword(logger logrus.FieldLogger, bindDN, bindSimplePw string) (ldapserver.LDAPResultCode, error) {
|
||||
// Lookup Bind DN in database
|
||||
entries, err := h.bdb.Search(bindDN, ldap.ScopeBaseObject)
|
||||
if err != nil || len(entries) != 1 {
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
logger.Debugf("Entry '%s' does not exist", bindDN)
|
||||
}
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
userPassword := entries[0].GetEqualFoldAttributeValue("userPassword")
|
||||
match, err := ldappassword.Validate(bindSimplePw, userPassword)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
if match {
|
||||
logger.Debug("success")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Delete(boundDN string, req *ldap.DelRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "delete",
|
||||
"bind_dn": boundDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
if !h.writeAllowed(boundDN) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
logger.Debug("Calling boltdb delete")
|
||||
if err := h.bdb.EntryDelete(req.DN); err != nil {
|
||||
logger.WithError(err).WithField("entrydn", req.DN).Debugln("ldap delete failed")
|
||||
if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
|
||||
return ldap.LDAPResultEntryAlreadyExists, nil
|
||||
}
|
||||
return ldap.LDAPResultUnwillingToPerform, err
|
||||
}
|
||||
logger.Debug("delete succeeded")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Modify(boundDN string, req *ldap.ModifyRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "modify",
|
||||
"bind_dn": boundDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
"entrydn": req.DN,
|
||||
})
|
||||
|
||||
if !h.writeAllowed(boundDN) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
logger.Debug("Calling boltdb modify")
|
||||
if err := h.bdb.EntryModify(req); err != nil {
|
||||
logger.WithError(err).Debug("ldap modify failed")
|
||||
if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
|
||||
return ldap.LDAPResultEntryAlreadyExists, nil
|
||||
}
|
||||
ldapError, ok := err.(*ldap.Error)
|
||||
if !ok {
|
||||
return ldap.LDAPResultUnwillingToPerform, err
|
||||
}
|
||||
return ldapserver.LDAPResultCode(ldapError.ResultCode), ldapError.Err
|
||||
}
|
||||
logger.Debug("modify succeeded")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) ModifyDN(boundDN string, req *ldap.ModifyDNRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "modifyDN",
|
||||
"bind_dn": boundDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
"entrydn": req.DN,
|
||||
})
|
||||
|
||||
if !h.writeAllowed(boundDN) {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
logger.Debug("Calling boltdb modify DN")
|
||||
if err := h.bdb.EntryModifyDN(req); err != nil {
|
||||
logger.WithError(err).Debug("ldap modifyDN failed")
|
||||
if errors.Is(err, ldbbolt.ErrEntryAlreadyExists) {
|
||||
return ldap.LDAPResultEntryAlreadyExists, nil
|
||||
}
|
||||
ldapError, ok := err.(*ldap.Error)
|
||||
if !ok {
|
||||
return ldap.LDAPResultUnwillingToPerform, err
|
||||
}
|
||||
return ldapserver.LDAPResultCode(ldapError.ResultCode), ldapError.Err
|
||||
}
|
||||
logger.Debug("modify DN succeeded")
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) ModifyPasswordExop(boundDN string, req *ldap.PasswordModifyRequest, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "modpw_exop",
|
||||
"binddn": boundDN,
|
||||
"UserIdentity": req.UserIdentity,
|
||||
"OldPWPresent": req.OldPassword != "",
|
||||
"NewPwPresent": req.NewPassword != "",
|
||||
})
|
||||
|
||||
if boundDN == "" {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
if req.UserIdentity != "" {
|
||||
pwUserDN, err := ldapdn.ParseNormalize(req.UserIdentity)
|
||||
if err != nil {
|
||||
return ldap.LDAPResultOperationsError, errors.New("Error parsing DN in password modify extended operation.")
|
||||
}
|
||||
if boundDN != pwUserDN && boundDN != h.adminDN {
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Calling boltdb UpdatePassword")
|
||||
err := h.bdb.UpdatePassword(req)
|
||||
if err != nil {
|
||||
logger.Debugf("boltdb UpdatePassword returned '%s'", err)
|
||||
return ldap.LDAPResultOther, err
|
||||
}
|
||||
return ldap.LDAPResultSuccess, err
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Search(boundDN string, req *ldap.SearchRequest, conn net.Conn) (ldapserver.ServerSearchResult, error) {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"op": "search",
|
||||
"binddn": boundDN,
|
||||
"basedn": req.BaseDN,
|
||||
"filter": req.Filter,
|
||||
"attrs": req.Attributes,
|
||||
})
|
||||
|
||||
logger.Debug("Calling boltdb search")
|
||||
entries, _ := h.bdb.Search(req.BaseDN, req.Scope)
|
||||
logger.Debugf("boltdb search returned %d entries", len(entries))
|
||||
|
||||
return ldapserver.ServerSearchResult{
|
||||
Entries: entries,
|
||||
Referrals: []string{},
|
||||
Controls: []ldap.Control{},
|
||||
ResultCode: ldap.LDAPResultSuccess,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Close(boundDN string, conn net.Conn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) WithContext(ctx context.Context) handler.Handler {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
|
||||
h2 := new(boltdbHandler)
|
||||
*h2 = *h
|
||||
h2.ctx = ctx
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) Reload(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *boltdbHandler) writeAllowed(boundDN string) bool {
|
||||
if h.adminDN != "" && h.adminDN == boundDN {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
)
|
||||
|
||||
// Interface for handlers.
|
||||
type Handler interface {
|
||||
ldapserver.Adder
|
||||
ldapserver.Binder
|
||||
ldapserver.Deleter
|
||||
ldapserver.Modifier
|
||||
ldapserver.PasswordUpdater
|
||||
ldapserver.Renamer
|
||||
ldapserver.Searcher
|
||||
ldapserver.Closer
|
||||
|
||||
WithContext(context.Context) Handler
|
||||
Reload(context.Context) error
|
||||
}
|
||||
|
||||
// Interface for middlewares.
|
||||
type Middleware interface {
|
||||
WithHandler(next Handler) Handler
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldappassword"
|
||||
)
|
||||
|
||||
type ldifEntry struct {
|
||||
*ldap.Entry
|
||||
|
||||
UserPassword *ldap.EntryAttribute
|
||||
}
|
||||
|
||||
func (entry *ldifEntry) validatePassword(bindSimplePw string) error {
|
||||
match, err := ldappassword.Validate(bindSimplePw, entry.UserPassword.Values[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !match {
|
||||
return fmt.Errorf("password mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-asn1-ber/asn1-ber"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
)
|
||||
|
||||
func parseFilterToIndexFilter(filter string) ([][]string, error) {
|
||||
f, err := ldapserver.CompileFilter(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result [][]string
|
||||
matches, err := parseFilterMatchLeavesForIndex(f, nil, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse filter for index failed: %w", err)
|
||||
} else {
|
||||
withObjectClass := true
|
||||
if len(matches) > 1 {
|
||||
// NOTE(longsleep): In case of more than one filter, we remove the
|
||||
// index lookup for objectClass since pretty much everything has
|
||||
// an object class anyways and our index lookup does not support
|
||||
// "and" lookups. Thus this gains a lot of efficiency.
|
||||
for _, f := range matches {
|
||||
if !strings.EqualFold("objectClass", f[1]) {
|
||||
withObjectClass = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, f := range matches {
|
||||
if !withObjectClass && strings.EqualFold("objectClass", f[1]) {
|
||||
continue
|
||||
}
|
||||
result = append(result, f[1:])
|
||||
}
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func parseFilterMatchLeavesForIndex(f *ber.Packet, parent [][]string, level string) ([][]string, error) {
|
||||
var err error
|
||||
|
||||
if parent == nil {
|
||||
parent = make([][]string, 0)
|
||||
}
|
||||
|
||||
switch f.Tag {
|
||||
case ldapserver.FilterEqualityMatch:
|
||||
if len(f.Children) != 2 {
|
||||
return nil, errors.New("unsupported number of children in equality match filter")
|
||||
}
|
||||
attribute := f.Children[0].Value.(string)
|
||||
value := f.Children[1].Value.(string)
|
||||
parent = append(parent, []string{level, attribute, "eq", value})
|
||||
|
||||
case ldapserver.FilterPresent:
|
||||
if len(f.Children) != 0 {
|
||||
return nil, errors.New("unsupported number of children in presence match filter")
|
||||
}
|
||||
attribute := f.Data.String()
|
||||
parent = append(parent, []string{level, attribute, "pres", ""})
|
||||
|
||||
case ldapserver.FilterSubstrings:
|
||||
if len(f.Children) != 2 {
|
||||
return nil, errors.New("unsupported number of children in substrings filter")
|
||||
}
|
||||
attribute := f.Children[0].Value.(string)
|
||||
if len(f.Children[1].Children) != 1 {
|
||||
return nil, errors.New("unsupported number of children in substrings filter")
|
||||
}
|
||||
value := f.Children[1].Children[0].Value.(string)
|
||||
switch f.Children[1].Children[0].Tag {
|
||||
case ldapserver.FilterSubstringsInitial, ldapserver.FilterSubstringsAny, ldapserver.FilterSubstringsFinal:
|
||||
parent = append(parent, []string{level, attribute, "sub", value, strconv.FormatInt(int64(f.Children[1].Children[0].Tag), 10)})
|
||||
}
|
||||
|
||||
case ldapserver.FilterAnd:
|
||||
for idx, child := range f.Children {
|
||||
parent, err = parseFilterMatchLeavesForIndex(child, parent, fmt.Sprintf("%s.&%d", level, idx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case ldapserver.FilterOr:
|
||||
for idx, child := range f.Children {
|
||||
parent, err = parseFilterMatchLeavesForIndex(child, parent, fmt.Sprintf("%s.|%d", level, idx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
case ldapserver.FilterNot:
|
||||
// Ignored for now.
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
return parent, nil
|
||||
}
|
||||
+512
@@ -0,0 +1,512 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/go-ldap/ldif"
|
||||
"github.com/longsleep/rndm"
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapdn"
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/libregraph/idm/server/handler"
|
||||
)
|
||||
|
||||
type ldifHandler struct {
|
||||
logger logrus.FieldLogger
|
||||
fn string
|
||||
options *Options
|
||||
|
||||
baseDN string
|
||||
adminDN string
|
||||
allowLocalAnonymousBind bool
|
||||
|
||||
ctx context.Context
|
||||
|
||||
current atomic.Value
|
||||
|
||||
activeSearchPagings cmap.ConcurrentMap
|
||||
}
|
||||
|
||||
var _ handler.Handler = (*ldifHandler)(nil) // Verify that *ldifHandler implements handler.Handler.
|
||||
|
||||
func NewLDIFHandler(logger logrus.FieldLogger, fn string, options *Options) (handler.Handler, error) {
|
||||
if fn == "" {
|
||||
return nil, fmt.Errorf("file name is empty")
|
||||
}
|
||||
if options.BaseDN == "" {
|
||||
return nil, fmt.Errorf("base dn is empty")
|
||||
}
|
||||
|
||||
fn, err := filepath.Abs(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger = logger.WithField("fn", fn)
|
||||
|
||||
h := &ldifHandler{
|
||||
logger: logger,
|
||||
fn: fn,
|
||||
options: options,
|
||||
|
||||
allowLocalAnonymousBind: options.AllowLocalAnonymousBind,
|
||||
|
||||
ctx: context.Background(),
|
||||
|
||||
activeSearchPagings: cmap.New(),
|
||||
}
|
||||
if h.baseDN, err = ldapdn.ParseNormalize(options.BaseDN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h.adminDN, err = ldapdn.ParseNormalize(options.AdminDN); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = h.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *ldifHandler) open() error {
|
||||
if !strings.EqualFold(h.options.BaseDN, h.baseDN) {
|
||||
return fmt.Errorf("mismatched BaseDN")
|
||||
}
|
||||
|
||||
info, err := os.Stat(h.fn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open LDIF: %w", err)
|
||||
}
|
||||
|
||||
var l *ldif.LDIF
|
||||
index := newIndexMapRegister()
|
||||
|
||||
if info.IsDir() {
|
||||
h.logger.Debugln("loading LDIF files from folder")
|
||||
h.options.templateBasePath = h.fn
|
||||
var parseErrors []error
|
||||
l, parseErrors, err = parseLDIFDirectory(h.fn, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(parseErrors) > 0 {
|
||||
for _, parseErr := range parseErrors {
|
||||
h.logger.WithError(parseErr).Errorln("LDIF error")
|
||||
}
|
||||
return fmt.Errorf("error in LDIF files")
|
||||
}
|
||||
} else {
|
||||
h.logger.Debugln("loading LDIF")
|
||||
h.options.templateBasePath = filepath.Dir(h.fn)
|
||||
l, err = parseLDIFFile(h.fn, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
t, err := treeFromLDIF(l, index, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store parsed data as memory value.
|
||||
value := &ldifMemoryValue{
|
||||
l: l,
|
||||
t: t,
|
||||
|
||||
index: index,
|
||||
}
|
||||
h.current.Store(value)
|
||||
|
||||
h.logger.WithFields(logrus.Fields{
|
||||
"version": l.Version,
|
||||
"entries_count": len(l.Entries),
|
||||
"tree_length": t.Len(),
|
||||
"base_dn": h.options.BaseDN,
|
||||
"indexes": len(index),
|
||||
}).Debugln("loaded LDIF")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ldifHandler) load() *ldifMemoryValue {
|
||||
value := h.current.Load()
|
||||
return value.(*ldifMemoryValue)
|
||||
}
|
||||
|
||||
func (h *ldifHandler) WithContext(ctx context.Context) handler.Handler {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
|
||||
h2 := new(ldifHandler)
|
||||
*h2 = *h
|
||||
h2.ctx = ctx
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Reload(ctx context.Context) error {
|
||||
return h.open()
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Add(_ string, _ *ldap.AddRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"bind_dn": bindDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
if err := h.validateBindDN(bindDN, conn); err != nil {
|
||||
logger.WithError(err).Debugln("ldap bind request BindDN validation failed")
|
||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||
}
|
||||
|
||||
if bindSimplePw == "" {
|
||||
logger.Debugf("ldap anonymous bind request")
|
||||
if bindDN == "" {
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
} else {
|
||||
return ldap.LDAPResultUnwillingToPerform, nil
|
||||
}
|
||||
} else {
|
||||
logger.Debugf("ldap bind request")
|
||||
}
|
||||
|
||||
current := h.load()
|
||||
|
||||
entryRecord, found := current.t.Get([]byte(bindDN))
|
||||
if !found {
|
||||
err := fmt.Errorf("user not found")
|
||||
logger.WithError(err).Debugf("ldap bind error")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
entry := entryRecord.(*ldifEntry)
|
||||
|
||||
if err := entry.validatePassword(bindSimplePw); err != nil {
|
||||
logger.WithError(err).Debugf("ldap bind credentials error")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Delete(_ string, _ *ldap.DelRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Modify(_ string, _ *ldap.ModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) ModifyDN(_ string, _ *ldap.ModifyDNRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) ModifyPasswordExop(_ string, _ *ldap.PasswordModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Search(bindDN string, searchReq *ldap.SearchRequest, conn net.Conn) (ldapserver.ServerSearchResult, error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
searchBaseDN := strings.ToLower(searchReq.BaseDN)
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"bind_dn": bindDN,
|
||||
"search_base_dn": searchBaseDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
"controls": searchReq.Controls,
|
||||
"size_limit": searchReq.SizeLimit,
|
||||
})
|
||||
|
||||
logger.Debugf("ldap search request for %s", searchReq.Filter)
|
||||
|
||||
if err := h.validateBindDN(bindDN, conn); err != nil {
|
||||
logger.WithError(err).Debugln("ldap search request BindDN validation failed")
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: ldap.LDAPResultInsufficientAccessRights,
|
||||
}, err
|
||||
}
|
||||
|
||||
indexFilter, _ := parseFilterToIndexFilter(searchReq.Filter)
|
||||
|
||||
if !strings.HasSuffix(searchBaseDN, h.baseDN) {
|
||||
err := fmt.Errorf("ldap search BaseDN is not in our BaseDN %s", h.baseDN)
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: ldap.LDAPResultInsufficientAccessRights,
|
||||
}, err
|
||||
}
|
||||
|
||||
doneControls := []ldap.Control{}
|
||||
var pagingControl *ldap.ControlPaging
|
||||
var pagingCookie []byte
|
||||
if paging := ldap.FindControl(searchReq.Controls, ldap.ControlTypePaging); paging != nil {
|
||||
pagingControl = paging.(*ldap.ControlPaging)
|
||||
if searchReq.SizeLimit > 0 && pagingControl.PagingSize >= uint32(searchReq.SizeLimit) {
|
||||
pagingControl = nil
|
||||
} else {
|
||||
pagingCookie = pagingControl.Cookie
|
||||
}
|
||||
}
|
||||
|
||||
pumpCh, resultCode := func() (<-chan *ldifEntry, ldapserver.LDAPResultCode) {
|
||||
var pumpCh chan *ldifEntry
|
||||
var start = true
|
||||
if pagingControl != nil {
|
||||
if len(pagingCookie) == 0 {
|
||||
pagingCookie = []byte(base64.RawStdEncoding.EncodeToString(rndm.GenerateRandomBytes(8)))
|
||||
pagingControl.Cookie = pagingCookie
|
||||
pumpCh = make(chan *ldifEntry)
|
||||
h.activeSearchPagings.Set(string(pagingControl.Cookie), pumpCh)
|
||||
logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("ldap search paging pump start")
|
||||
} else {
|
||||
pumpChRecord, ok := h.activeSearchPagings.Get(string(pagingControl.Cookie))
|
||||
if !ok {
|
||||
return nil, ldap.LDAPResultUnwillingToPerform
|
||||
}
|
||||
if pagingControl.PagingSize > 0 {
|
||||
logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("ldap search paging pump continue")
|
||||
pumpCh = pumpChRecord.(chan *ldifEntry)
|
||||
start = false
|
||||
} else {
|
||||
// No paging size with cookie, means abandon.
|
||||
start = false
|
||||
logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("search paging pump abandon")
|
||||
// TODO(longsleep): Cancel paging pump context.
|
||||
h.activeSearchPagings.Remove(string(pagingControl.Cookie))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pumpCh = make(chan *ldifEntry)
|
||||
}
|
||||
if start {
|
||||
current := h.load()
|
||||
go h.searchEntriesPump(h.ctx, current, pumpCh, searchReq, pagingControl, indexFilter)
|
||||
}
|
||||
|
||||
return pumpCh, ldap.LDAPResultSuccess
|
||||
}()
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
err := fmt.Errorf("search unable to perform: %d", resultCode)
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: resultCode,
|
||||
}, err
|
||||
}
|
||||
|
||||
filterPacket, err := ldapserver.CompileFilter(searchReq.Filter)
|
||||
if err != nil {
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: ldap.LDAPResultOperationsError,
|
||||
}, err
|
||||
}
|
||||
|
||||
var entryRecord *ldifEntry
|
||||
var entries []*ldap.Entry
|
||||
var entry *ldap.Entry
|
||||
var count uint32
|
||||
var keep bool
|
||||
results:
|
||||
for {
|
||||
select {
|
||||
case entryRecord = <-pumpCh:
|
||||
if entryRecord == nil {
|
||||
// All done, set cookie to empty.
|
||||
pagingCookie = []byte{}
|
||||
break results
|
||||
|
||||
} else {
|
||||
entry = entryRecord.Entry
|
||||
|
||||
// Apply filter.
|
||||
keep, resultCode = ldapserver.ServerApplyFilter(filterPacket, entry)
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: resultCode,
|
||||
}, errors.New("search filter apply error")
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter scope.
|
||||
keep, resultCode = ldapserver.ServerFilterScope(searchReq.BaseDN, searchReq.Scope, entry)
|
||||
if resultCode != ldap.LDAPResultSuccess {
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: resultCode,
|
||||
}, errors.New("search scope apply error")
|
||||
}
|
||||
if !keep {
|
||||
continue
|
||||
}
|
||||
|
||||
// Make a copy, before filtering attributes.
|
||||
e := &ldap.Entry{
|
||||
DN: entry.DN,
|
||||
Attributes: make([]*ldap.EntryAttribute, len(entry.Attributes)),
|
||||
}
|
||||
copy(e.Attributes, entry.Attributes)
|
||||
|
||||
// Filter attributes from entry.
|
||||
resultCode, err = ldapserver.ServerFilterAttributes(searchReq.Attributes, e)
|
||||
if err != nil {
|
||||
return ldapserver.ServerSearchResult{
|
||||
ResultCode: resultCode,
|
||||
}, err
|
||||
}
|
||||
|
||||
// Append entry as result.
|
||||
entries = append(entries, e)
|
||||
|
||||
// Count and more.
|
||||
count++
|
||||
if pagingControl != nil {
|
||||
if count >= pagingControl.PagingSize {
|
||||
break results
|
||||
}
|
||||
}
|
||||
if searchReq.SizeLimit > 0 && count >= uint32(searchReq.SizeLimit) {
|
||||
// TODO(longsleep): handle total sizelimit for paging.
|
||||
break results
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pagingControl != nil {
|
||||
doneControls = append(doneControls, &ldap.ControlPaging{
|
||||
PagingSize: 0,
|
||||
Cookie: pagingCookie,
|
||||
})
|
||||
}
|
||||
|
||||
return ldapserver.ServerSearchResult{
|
||||
Entries: entries,
|
||||
Referrals: []string{},
|
||||
Controls: doneControls,
|
||||
ResultCode: ldap.LDAPResultSuccess,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ldifHandler) searchEntriesPump(ctx context.Context, current *ldifMemoryValue, pumpCh chan<- *ldifEntry, searchReq *ldap.SearchRequest, pagingControl *ldap.ControlPaging, indexFilter [][]string) {
|
||||
defer func() {
|
||||
if pagingControl != nil {
|
||||
h.activeSearchPagings.Remove(string(pagingControl.Cookie))
|
||||
close(pumpCh)
|
||||
h.logger.WithField("paging_cookie", string(pagingControl.Cookie)).Debugln("ldap search paging pump ended")
|
||||
} else {
|
||||
close(pumpCh)
|
||||
}
|
||||
}()
|
||||
|
||||
pump := func(entryRecord *ldifEntry) bool {
|
||||
select {
|
||||
case pumpCh <- entryRecord:
|
||||
case <-ctx.Done():
|
||||
if pagingControl != nil {
|
||||
h.logger.WithField("paging_cookie", string(pagingControl.Cookie)).Warnln("ldap search paging pump context done")
|
||||
} else {
|
||||
h.logger.Warnln("ldap search pump context done")
|
||||
}
|
||||
return false
|
||||
case <-time.After(1 * time.Minute):
|
||||
if pagingControl != nil {
|
||||
h.logger.WithField("paging_cookie", string(pagingControl.Cookie)).Warnln("ldap search paging pump timeout")
|
||||
} else {
|
||||
h.logger.Warnln("ldap search pump timeout")
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
searchBaseDN := strings.ToLower(searchReq.BaseDN)
|
||||
|
||||
load := true
|
||||
if len(indexFilter) > 0 {
|
||||
// Get entries with help of index.
|
||||
load = false
|
||||
var results []*[]*ldifEntry
|
||||
for _, f := range indexFilter {
|
||||
indexed, found := current.index.Load(f[0], f[1], f[2:]...)
|
||||
if !found {
|
||||
load = true
|
||||
break
|
||||
}
|
||||
results = append(results, &indexed)
|
||||
}
|
||||
if !load {
|
||||
cache := make(map[*ldifEntry]struct{})
|
||||
for _, indexed := range results {
|
||||
for _, entryRecord := range *indexed {
|
||||
if _, cached := cache[entryRecord]; cached {
|
||||
// Prevent duplicates.
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(entryRecord.DN, searchBaseDN) {
|
||||
if ok := pump(entryRecord); !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
cache[entryRecord] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if load {
|
||||
// Walk through all entries (this is slow).
|
||||
h.logger.WithField("filter", searchReq.Filter).Warnln("ldap search filter does not match any index, using slow walk")
|
||||
current.t.WalkSuffix([]byte(searchBaseDN), func(key []byte, entryRecord interface{}) bool {
|
||||
if ok := pump(entryRecord.(*ldifEntry)); !ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ldifHandler) validateBindDN(bindDN string, conn net.Conn) error {
|
||||
if bindDN == "" {
|
||||
if h.allowLocalAnonymousBind {
|
||||
host, _, _ := net.SplitHostPort(conn.RemoteAddr().String())
|
||||
if net.ParseIP(host).IsLoopback() {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("anonymous BindDN rejected")
|
||||
}
|
||||
return fmt.Errorf("anonymous BindDN not allowed")
|
||||
}
|
||||
|
||||
if strings.HasSuffix(bindDN, h.baseDN) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("the BindDN is not in our BaseDN: %s", h.baseDN)
|
||||
}
|
||||
|
||||
func (h *ldifHandler) Close(bindDN string, conn net.Conn) error {
|
||||
h.logger.WithFields(logrus.Fields{
|
||||
"bind_dn": bindDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
}).Debugln("ldap close")
|
||||
|
||||
return nil
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/armon/go-radix"
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/spacewander/go-suffix-tree"
|
||||
)
|
||||
|
||||
var indexAttributes = map[string]string{
|
||||
"entryCSN": "eq",
|
||||
"entryUUID": "eq",
|
||||
"objectClass": "eq",
|
||||
"cn": "pres,eq,sub",
|
||||
"gidNumber": "eq",
|
||||
"mail": "pres,eq,sub",
|
||||
"memberUid": "eq",
|
||||
"ou": "eq",
|
||||
"uid": "eq",
|
||||
"uidNumber": "eq",
|
||||
"uniqueMember": "eq",
|
||||
|
||||
"sn": "pres,eq,sub",
|
||||
"givenName": "pres,eq,sub",
|
||||
|
||||
"mailAlternateAddress": "eq",
|
||||
|
||||
// Additional indexes for attributes usually used with AD.
|
||||
"objectGUID": "eq",
|
||||
"objectSID": "eq",
|
||||
"otherMailbox": "eq",
|
||||
"samAccountName": "eq",
|
||||
}
|
||||
|
||||
func AddIndexAttribute(name string, indices string) {
|
||||
indexAttributes[name] = indices
|
||||
}
|
||||
|
||||
func RemoveIndexAttribute(name string) {
|
||||
delete(indexAttributes, name)
|
||||
}
|
||||
|
||||
type Index interface {
|
||||
Add(name, op string, values []string, entry *ldifEntry) bool
|
||||
Load(name, op string, params ...string) ([]*ldifEntry, bool)
|
||||
}
|
||||
|
||||
type indexMap map[string][]*ldifEntry
|
||||
|
||||
func newIndexMap() indexMap {
|
||||
return make(indexMap)
|
||||
}
|
||||
|
||||
func (im indexMap) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
for _, value := range values {
|
||||
value = strings.ToLower(value)
|
||||
im[value] = append(im[value], entry)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (im indexMap) Load(name, op string, value ...string) ([]*ldifEntry, bool) {
|
||||
entries := im[strings.ToLower(value[0])]
|
||||
return entries, true
|
||||
}
|
||||
|
||||
type indexSuffixTree struct {
|
||||
t *suffix.Tree
|
||||
}
|
||||
|
||||
func newIndexSuffixTree() *indexSuffixTree {
|
||||
return &indexSuffixTree{
|
||||
t: suffix.NewTree(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ist indexSuffixTree) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
for _, value := range values {
|
||||
sfx := []byte(value)
|
||||
var entries []*ldifEntry
|
||||
if v, ok := ist.t.Get(sfx); ok {
|
||||
entries = v.([]*ldifEntry)
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
ist.t.Insert(sfx, entries)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ist indexSuffixTree) Load(name, op string, value ...string) ([]*ldifEntry, bool) {
|
||||
var entries []*ldifEntry
|
||||
sfx := []byte(value[0])
|
||||
ist.t.WalkSuffix(sfx, func(key []byte, value interface{}) bool {
|
||||
entries = append(entries, value.([]*ldifEntry)...)
|
||||
return false
|
||||
})
|
||||
return entries, true
|
||||
}
|
||||
|
||||
type indexRadixTree struct {
|
||||
t *radix.Tree
|
||||
}
|
||||
|
||||
func newIndexRadixTree() *indexRadixTree {
|
||||
return &indexRadixTree{
|
||||
t: radix.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (irt *indexRadixTree) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
for _, value := range values {
|
||||
pfx := value
|
||||
var entries []*ldifEntry
|
||||
if v, ok := irt.t.Get(pfx); ok {
|
||||
entries = v.([]*ldifEntry)
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
irt.t.Insert(pfx, entries)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (irt *indexRadixTree) Load(name, op string, value ...string) ([]*ldifEntry, bool) {
|
||||
var entries []*ldifEntry
|
||||
pfx := value[0]
|
||||
irt.t.WalkPrefix(pfx, func(key string, value interface{}) bool {
|
||||
entries = append(entries, value.([]*ldifEntry)...)
|
||||
return false
|
||||
})
|
||||
return entries, true
|
||||
}
|
||||
|
||||
type indexSubTree struct {
|
||||
pres indexMap
|
||||
irt *indexRadixTree
|
||||
ist *indexSuffixTree
|
||||
}
|
||||
|
||||
func newIndexSubTree() *indexSubTree {
|
||||
return &indexSubTree{
|
||||
pres: newIndexMap(),
|
||||
irt: newIndexRadixTree(),
|
||||
ist: newIndexSuffixTree(),
|
||||
}
|
||||
}
|
||||
|
||||
func (idx *indexSubTree) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
ok0 := idx.pres.Add(name, op, []string{""}, entry)
|
||||
ok1 := idx.irt.Add(name, op, values, entry)
|
||||
ok2 := idx.ist.Add(name, op, values, entry)
|
||||
return ok0 || ok1 || ok2
|
||||
}
|
||||
|
||||
func (idx *indexSubTree) Load(name, op string, params ...string) ([]*ldifEntry, bool) {
|
||||
if len(params) != 2 {
|
||||
// Require one value and sub tag.
|
||||
return nil, false
|
||||
}
|
||||
tag, err := strconv.ParseInt(params[1], 10, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case ldapserver.FilterSubstringsAny:
|
||||
// TODO(longsleep): Find a suitable way for full text search substring
|
||||
// matching, for example with Knuth-Morris-Pratt algorithm. Currently
|
||||
// we just do a presence match.
|
||||
return idx.pres.Load(name, op, "")
|
||||
case ldapserver.FilterSubstringsInitial:
|
||||
return idx.irt.Load(name, op, params[0])
|
||||
case ldapserver.FilterSubstringsFinal:
|
||||
return idx.ist.Load(name, op, params[0])
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
type indexMapRegister map[string]Index
|
||||
|
||||
func newIndexMapRegister() indexMapRegister {
|
||||
imr := make(indexMapRegister)
|
||||
for name, ops := range indexAttributes {
|
||||
for _, op := range strings.Split(ops, ",") {
|
||||
switch op {
|
||||
case "sub":
|
||||
imr[imr.getKey(name, op)] = newIndexSubTree()
|
||||
case "pres":
|
||||
imr[imr.getKey(name, op)] = newIndexMap()
|
||||
case "eq":
|
||||
imr[imr.getKey(name, op)] = newIndexMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
return imr
|
||||
}
|
||||
|
||||
func (imr indexMapRegister) getKey(name, op string) string {
|
||||
return strings.ToLower(name) + "," + op
|
||||
}
|
||||
|
||||
func (imr indexMapRegister) Add(name, op string, values []string, entry *ldifEntry) bool {
|
||||
index, ok := imr[imr.getKey(name, op)]
|
||||
if !ok {
|
||||
// No matching index, refuse to add.
|
||||
return false
|
||||
}
|
||||
return index.Add(name, op, values, entry)
|
||||
}
|
||||
|
||||
func (imr indexMapRegister) Load(name, op string, params ...string) ([]*ldifEntry, bool) {
|
||||
index, ok := imr[imr.getKey(name, op)]
|
||||
if !ok {
|
||||
// No such index.
|
||||
return nil, false
|
||||
}
|
||||
return index.Load(name, op, params...)
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/go-ldap/ldif"
|
||||
"github.com/spacewander/go-suffix-tree"
|
||||
)
|
||||
|
||||
// parseLDIFFile opens the named file for reading and parses it as LDIF.
|
||||
func parseLDIFFile(fn string, options *Options) (*ldif.LDIF, error) {
|
||||
f, err := os.Open(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var r io.Reader
|
||||
|
||||
if options.TemplateEngineDisabled {
|
||||
r = f
|
||||
} else {
|
||||
r, err = parseLDIFTemplate(f, options, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return parseLDIF(r, options)
|
||||
}
|
||||
|
||||
// parseLDIFDirectory opens all ldif files in the given path in sorted order,
|
||||
// cats them all together and parses the result as LDIF.
|
||||
func parseLDIFDirectory(pn string, options *Options) (*ldif.LDIF, []error, error) {
|
||||
matches, err := filepath.Glob(filepath.Join(pn, "*.ldif"))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sort.Slice(matches, func(i, j int) bool {
|
||||
return matches[i] < matches[j]
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
var matchErrors []error
|
||||
for _, match := range matches {
|
||||
err = func() error {
|
||||
f, openErr := os.Open(match)
|
||||
if openErr != nil {
|
||||
matchErrors = append(matchErrors, fmt.Errorf("file read error: %w", openErr))
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
if options.TemplateEngineDisabled {
|
||||
_, copyErr := io.Copy(&buf, f)
|
||||
if copyErr != nil {
|
||||
return fmt.Errorf("file read error: %w", copyErr)
|
||||
}
|
||||
} else {
|
||||
p, parseErr := parseLDIFTemplate(f, options, nil)
|
||||
if parseErr != nil {
|
||||
matchErrors = append(matchErrors, fmt.Errorf("parse error in %s: %w", match, parseErr))
|
||||
return nil
|
||||
}
|
||||
_, copyErr := io.Copy(&buf, p)
|
||||
if copyErr != nil {
|
||||
return fmt.Errorf("template read error: %w", copyErr)
|
||||
}
|
||||
}
|
||||
buf.WriteString("\n\n")
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, matchErrors, err
|
||||
}
|
||||
}
|
||||
|
||||
l, err := parseLDIF(&buf, options)
|
||||
return l, matchErrors, err
|
||||
}
|
||||
|
||||
// parseLDIFTemplate exectues the provided text template and then parses the
|
||||
// result as LDIF.
|
||||
func parseLDIFTemplate(r io.Reader, options *Options, m map[string]interface{}) (io.Reader, error) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
var text []string
|
||||
for scanner.Scan() {
|
||||
t := scanner.Text()
|
||||
if t != "" && t[0] == '#' {
|
||||
// Ignore commented lines.
|
||||
continue
|
||||
}
|
||||
text = append(text, scanner.Text())
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
m = make(map[string]interface{})
|
||||
}
|
||||
tpl, err := template.New("tpl").Funcs(TemplateFuncs(m, options)).Parse(strings.Join(text, "\n"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse LDIF template: %w", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tpl.Execute(&buf, m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to process LDIF template: %w", err)
|
||||
}
|
||||
|
||||
if options.TemplateDebug {
|
||||
fmt.Println("---\n", buf.String(), "\n----")
|
||||
}
|
||||
|
||||
return &buf, nil
|
||||
}
|
||||
|
||||
func parseLDIF(r io.Reader, options *Options) (*ldif.LDIF, error) {
|
||||
l := &ldif.LDIF{}
|
||||
err := ldif.Unmarshal(r, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// treeFromLDIF makes a tree out of the provided LDIF and if index is not nil,
|
||||
// also indexes each entry in the provided index.
|
||||
func treeFromLDIF(l *ldif.LDIF, index Index, options *Options) (*suffix.Tree, error) {
|
||||
t := suffix.NewTree()
|
||||
|
||||
// NOTE(longsleep): Create in memory tree records from LDIF data.
|
||||
var entry *ldap.Entry
|
||||
for _, entryRecord := range l.Entries {
|
||||
if entryRecord == nil || entryRecord.Entry == nil {
|
||||
// NOTE(longsleep): We don't use l.AllEntries as "nil" records can happen.
|
||||
continue
|
||||
}
|
||||
entry = entryRecord.Entry
|
||||
e := &ldifEntry{
|
||||
Entry: &ldap.Entry{
|
||||
DN: strings.ToLower(entry.DN),
|
||||
},
|
||||
}
|
||||
for _, a := range entry.Attributes {
|
||||
switch strings.ToLower(a.Name) {
|
||||
case "userpassword":
|
||||
// Don't include the password in the normal attributes.
|
||||
e.UserPassword = &ldap.EntryAttribute{
|
||||
Name: a.Name,
|
||||
Values: a.Values,
|
||||
}
|
||||
default:
|
||||
// Append it.
|
||||
e.Entry.Attributes = append(e.Entry.Attributes, &ldap.EntryAttribute{
|
||||
Name: a.Name,
|
||||
Values: a.Values,
|
||||
})
|
||||
}
|
||||
if index != nil {
|
||||
// Index equalityMatch.
|
||||
index.Add(a.Name, "eq", a.Values, e)
|
||||
// Index present.
|
||||
index.Add(a.Name, "pres", []string{""}, e)
|
||||
// Index substrings.
|
||||
index.Add(a.Name, "sub", a.Values, e)
|
||||
}
|
||||
}
|
||||
v, ok := t.Insert([]byte(e.DN), e)
|
||||
if !ok || v != nil {
|
||||
return nil, fmt.Errorf("duplicate dn value: %s", e.DN)
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"github.com/go-ldap/ldif"
|
||||
"github.com/spacewander/go-suffix-tree"
|
||||
)
|
||||
|
||||
type ldifMemoryValue struct {
|
||||
l *ldif.LDIF
|
||||
t *suffix.Tree
|
||||
|
||||
index Index
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/libregraph/idm/server/handler"
|
||||
)
|
||||
|
||||
type ldifMiddleware struct {
|
||||
logger logrus.FieldLogger
|
||||
fn string
|
||||
options *Options
|
||||
|
||||
baseDN string
|
||||
|
||||
current atomic.Value
|
||||
|
||||
next handler.Handler
|
||||
}
|
||||
|
||||
var _ handler.Handler = (*ldifMiddleware)(nil) // Verify that *configHandler implements handler.Handler.
|
||||
|
||||
func NewLDIFMiddleware(logger logrus.FieldLogger, fn string, options *Options) (handler.Middleware, error) {
|
||||
if fn == "" {
|
||||
return nil, fmt.Errorf("file name is empty")
|
||||
}
|
||||
if options.BaseDN == "" {
|
||||
return nil, fmt.Errorf("base dn is empty")
|
||||
}
|
||||
|
||||
fn, err := filepath.Abs(fn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger = logger.WithField("fn", fn)
|
||||
|
||||
h := &ldifMiddleware{
|
||||
logger: logger,
|
||||
fn: fn,
|
||||
options: options,
|
||||
|
||||
baseDN: strings.ToLower(options.BaseDN),
|
||||
}
|
||||
|
||||
err = h.open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) open() error {
|
||||
if !strings.EqualFold(h.options.BaseDN, h.baseDN) {
|
||||
return fmt.Errorf("mismatched BaseDN")
|
||||
}
|
||||
|
||||
h.logger.Debugln("loading LDIF")
|
||||
l, err := parseLDIFFile(h.fn, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t, err := treeFromLDIF(l, nil, h.options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store parsed data as memory value.
|
||||
value := &ldifMemoryValue{
|
||||
t: t,
|
||||
}
|
||||
h.current.Store(value)
|
||||
|
||||
h.logger.WithFields(logrus.Fields{
|
||||
"version": l.Version,
|
||||
"entries_count": len(l.Entries),
|
||||
"tree_length": t.Len(),
|
||||
"base_dn": h.options.BaseDN,
|
||||
}).Debugln("loaded LDIF")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) load() *ldifMemoryValue {
|
||||
value := h.current.Load()
|
||||
return value.(*ldifMemoryValue)
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) WithHandler(next handler.Handler) handler.Handler {
|
||||
h.next = next
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) WithContext(ctx context.Context) handler.Handler {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
|
||||
h2 := new(ldifMiddleware)
|
||||
*h2 = *h
|
||||
h2.next = h.next.WithContext(ctx)
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Reload(ctx context.Context) error {
|
||||
err := h.open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.next.Reload(ctx)
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Add(_ string, _ *ldap.AddRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Bind(bindDN, bindSimplePw string, conn net.Conn) (resultCode ldapserver.LDAPResultCode, err error) {
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
|
||||
if bindSimplePw == "" { // Empty password means anonymous bind.
|
||||
return h.next.Bind(bindDN, bindSimplePw, conn)
|
||||
}
|
||||
|
||||
current := h.load()
|
||||
|
||||
entryRecord, found := current.t.Get([]byte(bindDN))
|
||||
if found {
|
||||
logger := h.logger.WithFields(logrus.Fields{
|
||||
"bind_dn": bindDN,
|
||||
"remote_addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
if !strings.HasSuffix(bindDN, h.baseDN) {
|
||||
err := fmt.Errorf("the BindDN is not in our BaseDN %s", h.baseDN)
|
||||
logger.WithError(err).Infoln("ldap bind error")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
if err := entryRecord.(*ldifEntry).validatePassword(bindSimplePw); err != nil {
|
||||
logger.WithError(err).Infoln("bind error")
|
||||
return ldap.LDAPResultInvalidCredentials, nil
|
||||
}
|
||||
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
}
|
||||
|
||||
return h.next.Bind(bindDN, bindSimplePw, conn)
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Delete(_ string, _ *ldap.DelRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Modify(_ string, _ *ldap.ModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) ModifyDN(_ string, _ *ldap.ModifyDNRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) ModifyPasswordExop(_ string, _ *ldap.PasswordModifyRequest, _ net.Conn) (ldapserver.LDAPResultCode, error) {
|
||||
return ldap.LDAPResultUnwillingToPerform, errors.New("unsupported operation")
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Search(bindDN string, searchReq *ldap.SearchRequest, conn net.Conn) (result ldapserver.ServerSearchResult, err error) {
|
||||
return h.next.Search(bindDN, searchReq, conn)
|
||||
}
|
||||
|
||||
func (h *ldifMiddleware) Close(bindDN string, conn net.Conn) error {
|
||||
return h.next.Close(bindDN, conn)
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
type Options struct {
|
||||
BaseDN string
|
||||
AdminDN string
|
||||
AllowLocalAnonymousBind bool
|
||||
|
||||
DefaultCompany string
|
||||
DefaultMailDomain string
|
||||
|
||||
TemplateExtraVars map[string]interface{}
|
||||
TemplateEngineDisabled bool
|
||||
TemplateDebug bool
|
||||
|
||||
templateBasePath string
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package ldif
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/libregraph/idm"
|
||||
)
|
||||
|
||||
const formatAsFileSizeLimit int64 = 1024 * 1024
|
||||
|
||||
func TemplateFuncs(m map[string]interface{}, options *Options) template.FuncMap {
|
||||
defaults := map[string]interface{}{
|
||||
"Company": "Default",
|
||||
"BaseDN": idm.DefaultLDAPBaseDN,
|
||||
"MailDomain": idm.DefaultMailDomain,
|
||||
}
|
||||
for k, v := range defaults {
|
||||
if _, ok := m[k]; !ok {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
if options != nil {
|
||||
if options.BaseDN != "" {
|
||||
m["BaseDN"] = options.BaseDN
|
||||
}
|
||||
if options.DefaultCompany != "" {
|
||||
m["Company"] = options.DefaultCompany
|
||||
}
|
||||
if options.DefaultMailDomain != "" {
|
||||
m["MailDomain"] = options.DefaultMailDomain
|
||||
}
|
||||
for k, v := range options.TemplateExtraVars {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
autoIncrement := uint64(1000)
|
||||
if v, ok := m["AutoIncrementMin"]; ok {
|
||||
autoIncrement = v.(uint64)
|
||||
}
|
||||
|
||||
basePath := options.templateBasePath
|
||||
|
||||
return template.FuncMap{
|
||||
"WithCompany": func(value string) string {
|
||||
m["Company"] = value
|
||||
return ""
|
||||
},
|
||||
"WithBaseDN": func(value string) string {
|
||||
m["BaseDN"] = value
|
||||
return ""
|
||||
},
|
||||
"WithMailDomain": func(value string) string {
|
||||
m["MailDomain"] = value
|
||||
return ""
|
||||
},
|
||||
"AutoIncrement": func(values ...uint64) uint64 {
|
||||
if len(values) > 0 {
|
||||
autoIncrement = values[0]
|
||||
} else {
|
||||
autoIncrement++
|
||||
}
|
||||
return autoIncrement
|
||||
},
|
||||
"formatAsBase64": func(s string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(s))
|
||||
},
|
||||
"formatAsFileBase64": func(fn string) (string, error) {
|
||||
if basePath == "" {
|
||||
return "", fmt.Errorf("LDIF template fromFile failed, no base path")
|
||||
}
|
||||
fn = filepath.Clean(fn)
|
||||
if !filepath.IsAbs(fn) {
|
||||
fn = filepath.Join(basePath, fn)
|
||||
}
|
||||
fn, err := filepath.Abs(fn)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// NOTE(longsleep): Poor man base path check, should work well enough on Linux.
|
||||
// See https://github.com/golang/go/issues/18358 for details.
|
||||
if !strings.HasPrefix(fn, strings.TrimRight(basePath, "/")+"/") {
|
||||
return "", fmt.Errorf("LDIF template formatAsFile %s outside of %s is not allowed", fn, basePath)
|
||||
}
|
||||
|
||||
f, err := os.Open(fn)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("LDIF template formatAsFile open failed with error: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
reader := io.LimitReader(f, formatAsFileSizeLimit+1)
|
||||
|
||||
var buf bytes.Buffer
|
||||
encoder := base64.NewEncoder(base64.StdEncoding, &buf)
|
||||
n, err := io.Copy(encoder, reader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("LDIF template formatAsFile error: %w", err)
|
||||
}
|
||||
if n > formatAsFileSizeLimit {
|
||||
return "", fmt.Errorf("LDIF template formatAsFile size limit exceeded: %s", fn)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
},
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
metricsSubsystemLDAPServer = "ldapserver"
|
||||
)
|
||||
|
||||
// MustRegister registers all rtm metrics with the provided registerer and
|
||||
// panics upon the first registration that causes an error.
|
||||
func MustRegister(reg prometheus.Registerer, cs ...prometheus.Collector) {
|
||||
reg.MustRegister(cs...)
|
||||
}
|
||||
|
||||
type ldapServerCollector struct {
|
||||
stats *ldapserver.Stats
|
||||
|
||||
connsTotalDesc *prometheus.Desc
|
||||
connsCurrentDesc *prometheus.Desc
|
||||
connsMaxDesc *prometheus.Desc
|
||||
|
||||
bindsDesc *prometheus.Desc
|
||||
unbindsDesc *prometheus.Desc
|
||||
searchesDsc *prometheus.Desc
|
||||
}
|
||||
|
||||
func NewLDAPServerCollector(s *ldapserver.Server) prometheus.Collector {
|
||||
return &ldapServerCollector{
|
||||
stats: s.Stats,
|
||||
|
||||
connsTotalDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", metricsSubsystemLDAPServer, "connections_total"),
|
||||
"Total number of incoming LDAP connections",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
connsCurrentDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", metricsSubsystemLDAPServer, "connections_current"),
|
||||
"Current number of concurrent established incoming LDAP connections",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
connsMaxDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", metricsSubsystemLDAPServer, "connections_max"),
|
||||
"Maximum number of concurrent established incoming LDAP connections",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
bindsDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", metricsSubsystemLDAPServer, "binds_total"),
|
||||
"Total number of incoming LDAP bind requests",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
unbindsDesc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", metricsSubsystemLDAPServer, "unbinds_total"),
|
||||
"Total number of incoming LDAP unbind requests",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
searchesDsc: prometheus.NewDesc(
|
||||
prometheus.BuildFQName("", metricsSubsystemLDAPServer, "searches_total"),
|
||||
"Total number of incoming LDAP search requests",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Describe is implemented with DescribeByCollect. That's possible because the
|
||||
// Collect method will always return the same two metrics with the same two
|
||||
// descriptors.
|
||||
func (lc *ldapServerCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
prometheus.DescribeByCollect(lc, ch)
|
||||
}
|
||||
|
||||
// Collect first gathers the associated managers collectors managers data. Then
|
||||
// it creates constant metrics based on the returned data.
|
||||
func (lc *ldapServerCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
stats := lc.stats.Clone()
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
lc.connsTotalDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.Conns),
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
lc.connsCurrentDesc,
|
||||
prometheus.GaugeValue,
|
||||
float64(stats.ConnsCurrent),
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
lc.connsMaxDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.ConnsMax),
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
lc.bindsDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.Binds),
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
lc.unbindsDesc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.Unbinds),
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
lc.searchesDsc,
|
||||
prometheus.CounterValue,
|
||||
float64(stats.Searches),
|
||||
)
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/bombsimon/logrusr/v3"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapserver"
|
||||
"github.com/libregraph/idm/server/handler"
|
||||
"github.com/libregraph/idm/server/handler/boltdb"
|
||||
"github.com/libregraph/idm/server/handler/ldif"
|
||||
)
|
||||
|
||||
const DefaultGeneratedPasswordLength = 16
|
||||
|
||||
// Server is our server implementation.
|
||||
type Server struct {
|
||||
config *Config
|
||||
|
||||
logger logrus.FieldLogger
|
||||
|
||||
LDAPServer *ldapserver.Server
|
||||
LDAPHandler handler.Handler
|
||||
}
|
||||
|
||||
// NewServer constructs a server from the provided parameters.
|
||||
func NewServer(c *Config) (*Server, error) {
|
||||
s := &Server{
|
||||
config: c,
|
||||
|
||||
logger: c.Logger,
|
||||
}
|
||||
|
||||
s.LDAPServer = ldapserver.NewServer()
|
||||
s.LDAPServer.EnforceLDAP = false
|
||||
s.LDAPServer.GeneratedPasswordLength = DefaultGeneratedPasswordLength
|
||||
ldapserver.Logger(logrusr.New(c.Logger))
|
||||
|
||||
var err error
|
||||
switch c.LDAPHandler {
|
||||
case "ldif":
|
||||
ldifHandlerOptions := &ldif.Options{
|
||||
BaseDN: s.config.LDAPBaseDN,
|
||||
AdminDN: s.config.LDAPAdminDN,
|
||||
AllowLocalAnonymousBind: s.config.LDAPAllowLocalAnonymousBind,
|
||||
|
||||
DefaultCompany: s.config.LDIFDefaultCompany,
|
||||
DefaultMailDomain: s.config.LDIFDefaultMailDomain,
|
||||
TemplateExtraVars: s.config.LDIFTemplateExtraVars,
|
||||
|
||||
TemplateDebug: os.Getenv("KIDM_TEMPLATE_DEBUG") != "",
|
||||
}
|
||||
|
||||
s.LDAPHandler, err = ldif.NewLDIFHandler(s.logger, s.config.LDIFMain, ldifHandlerOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create LDIF source handler: %w", err)
|
||||
}
|
||||
if s.config.LDIFConfig != "" {
|
||||
middleware, middlewareErr := ldif.NewLDIFMiddleware(s.logger, s.config.LDIFConfig, ldifHandlerOptions)
|
||||
if middlewareErr != nil {
|
||||
return nil, fmt.Errorf("failed to create LDIF config handler: %w", middlewareErr)
|
||||
}
|
||||
s.LDAPHandler = middleware.WithHandler(s.LDAPHandler)
|
||||
}
|
||||
case "boltdb":
|
||||
boltOptions := &boltdb.Options{
|
||||
BaseDN: s.config.LDAPBaseDN,
|
||||
AdminDN: s.config.LDAPAdminDN,
|
||||
|
||||
AllowLocalAnonymousBind: s.config.LDAPAllowLocalAnonymousBind,
|
||||
}
|
||||
s.LDAPHandler, err = boltdb.NewBoltDBHandler(s.logger, s.config.BoltDBFile, boltOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create BoltDB handler: %w", err)
|
||||
}
|
||||
|
||||
// FIXME Let the frontend (LDAPServer) handle filtering and attribute list until we added backend support
|
||||
s.LDAPServer.EnforceLDAP = true
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown LDAPHandler: '%s'", c.LDAPHandler)
|
||||
}
|
||||
|
||||
if c.Metrics != nil {
|
||||
s.LDAPServer.SetStats(true)
|
||||
MustRegister(c.Metrics, NewLDAPServerCollector(s.LDAPServer))
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Serve starts all the accociated servers resources and listeners and blocks
|
||||
// forever until signals or error occurs.
|
||||
func (s *Server) Serve(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
serveCtx, serveCtxCancel := context.WithCancel(ctx)
|
||||
defer serveCtxCancel()
|
||||
|
||||
logger := s.logger
|
||||
|
||||
errCh := make(chan error, 2)
|
||||
exitCh := make(chan struct{}, 1)
|
||||
signalCh := make(chan os.Signal, 1)
|
||||
readyCh := make(chan struct{}, 1)
|
||||
triggerCh := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-serveCtx.Done():
|
||||
return
|
||||
case <-readyCh:
|
||||
}
|
||||
logger.WithFields(logrus.Fields{}).Infoln("ready")
|
||||
}()
|
||||
|
||||
var serversWg sync.WaitGroup
|
||||
|
||||
// NOTE(rhafer): since v3.4.3 the ldap package allows to set a custom logger.
|
||||
// Set that to use to our logger.
|
||||
loggerWriter := logger.WithField("scope", "ldap").WriterLevel(logrus.DebugLevel)
|
||||
defer loggerWriter.Close()
|
||||
ldap.Logger(log.New(loggerWriter, "", 0))
|
||||
|
||||
ldapHandler := s.LDAPHandler.WithContext(serveCtx)
|
||||
s.LDAPServer.AddFunc("", ldapHandler)
|
||||
s.LDAPServer.BindFunc("", ldapHandler)
|
||||
s.LDAPServer.DeleteFunc("", ldapHandler)
|
||||
s.LDAPServer.ModifyFunc("", ldapHandler)
|
||||
s.LDAPServer.ModifyDNFunc("", ldapHandler)
|
||||
s.LDAPServer.PasswordExOpFunc("", ldapHandler)
|
||||
s.LDAPServer.SearchFunc("", ldapHandler)
|
||||
s.LDAPServer.CloseFunc("", ldapHandler)
|
||||
|
||||
serversWg.Add(1)
|
||||
go func() {
|
||||
defer serversWg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-triggerCh:
|
||||
reloadErr := ldapHandler.Reload(serveCtx)
|
||||
if reloadErr != nil {
|
||||
logger.Debugln("reload error: %w", reloadErr)
|
||||
} else {
|
||||
logger.Debugln("reload complete")
|
||||
}
|
||||
case <-serveCtx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if s.config.LDAPListenAddr != "" {
|
||||
serversWg.Add(1)
|
||||
go func() {
|
||||
defer serversWg.Done()
|
||||
logger.WithField("listen_addr", s.config.LDAPListenAddr).Infoln("starting LDAP listener")
|
||||
serveErr := s.LDAPServer.ListenAndServe(s.config.LDAPListenAddr)
|
||||
if serveErr != nil {
|
||||
errCh <- serveErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
if s.config.LDAPSListenAddr != "" {
|
||||
serversWg.Add(1)
|
||||
go func() {
|
||||
defer serversWg.Done()
|
||||
logger.WithField("listen_addr_tls", s.config.LDAPSListenAddr).Infoln("starting LDAPS listener")
|
||||
serveErr := s.LDAPServer.ListenAndServeTLS(s.config.LDAPSListenAddr, s.config.TLSCertFile, s.config.TLSKeyFile)
|
||||
if serveErr != nil {
|
||||
errCh <- serveErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
serversWg.Wait()
|
||||
logger.Debugln("server listeners stopped")
|
||||
close(exitCh)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
close(readyCh) // TODO(longsleep): Implement real ready.
|
||||
if s.config.OnReady != nil {
|
||||
go s.config.OnReady(s)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for exit or error, with support for HUP to reload
|
||||
err = func() error {
|
||||
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
for {
|
||||
select {
|
||||
case errFromChannel := <-errCh:
|
||||
return errFromChannel
|
||||
case reason := <-signalCh:
|
||||
if reason == syscall.SIGHUP {
|
||||
logger.Infoln("reload signal received")
|
||||
select {
|
||||
case triggerCh <- true:
|
||||
default:
|
||||
}
|
||||
continue
|
||||
}
|
||||
logger.WithField("signal", reason).Warnln("received signal")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Shutdown, server will stop to accept new connections, requires Go 1.8+.
|
||||
logger.Infoln("clean server shutdown start")
|
||||
_, shutdownCtxCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
go func() {
|
||||
close(s.LDAPServer.Quit)
|
||||
}()
|
||||
|
||||
// Cancel our own context,
|
||||
serveCtxCancel()
|
||||
func() {
|
||||
for {
|
||||
select {
|
||||
case <-exitCh:
|
||||
logger.Infoln("clean server shutdown complete, exiting")
|
||||
return
|
||||
default:
|
||||
// Services have not quit yet.
|
||||
logger.Info("waiting for services to exit")
|
||||
}
|
||||
select {
|
||||
case reason := <-signalCh:
|
||||
logger.WithField("signal", reason).Warn("received signal")
|
||||
return
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}()
|
||||
shutdownCtxCancel() // Prevents leak.
|
||||
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user