Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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
View File
@@ -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
View File
@@ -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
},
}
}