Initial QSfera import
This commit is contained in:
+485
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Copyright 2021 The LibreGraph Authors.
|
||||
*/
|
||||
|
||||
// Package ldbbolt provides the lower-level Database functions for managing LDAP Entries
|
||||
// in a BoltDB database. Some implementation details:
|
||||
//
|
||||
// # The database is currently separated in these three buckets
|
||||
//
|
||||
// - id2entry: This bucket contains the GOB encoded ldap.Entry instances keyed
|
||||
// by a unique 64bit ID
|
||||
//
|
||||
// - dn2id: This bucket is used as an index to lookup the ID of an entry by its DN. The DN
|
||||
// is used in an normalized (case-folded) form here.
|
||||
//
|
||||
// - id2children: This bucket uses the entry-ids as and index and the values contain a list
|
||||
// of the entry ids of its direct childdren
|
||||
//
|
||||
// Additional buckets will likely be added in the future to create efficient search indexes
|
||||
package ldbbolt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
||||
"github.com/libregraph/idm/pkg/ldapdn"
|
||||
"github.com/libregraph/idm/pkg/ldapentry"
|
||||
"github.com/libregraph/idm/pkg/ldappassword"
|
||||
)
|
||||
|
||||
type LdbBolt struct {
|
||||
logger logrus.FieldLogger
|
||||
db *bolt.DB
|
||||
options *bolt.Options
|
||||
base string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrEntryAlreadyExists = errors.New("entry already exists")
|
||||
ErrEntryNotFound = errors.New("entry does not exist")
|
||||
ErrNonLeafEntry = errors.New("entry is not a leaf entry")
|
||||
)
|
||||
|
||||
func (bdb *LdbBolt) Configure(logger logrus.FieldLogger, baseDN, dbfile string, options *bolt.Options) error {
|
||||
bdb.logger = logger
|
||||
logger = logger.WithField("db", dbfile)
|
||||
logger.Debug("Open boltdb")
|
||||
db, err := bolt.Open(dbfile, 0o600, options)
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error opening database")
|
||||
return err
|
||||
}
|
||||
bdb.db = db
|
||||
bdb.options = options
|
||||
bdb.base, _ = ldapdn.ParseNormalize(baseDN)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initialize() opens the Database file and create the required buckets if they do not
|
||||
// exist yet. After calling initialize the database is ready to process transactions
|
||||
func (bdb *LdbBolt) Initialize() error {
|
||||
var err error
|
||||
logger := bdb.logger.WithField("db", bdb.db.Path())
|
||||
if bdb.options == nil || !bdb.options.ReadOnly {
|
||||
logger.Debug("Adding default buckets")
|
||||
err = bdb.db.Update(func(tx *bolt.Tx) error {
|
||||
_, err = tx.CreateBucketIfNotExists([]byte("dn2id"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create bucket 'dn2id': %w", err)
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte("id2children"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create bucket 'dn2id': %w", err)
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte("id2entry"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create bucket 'id2entry': %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("Error creating default buckets")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Performs basic LDAP searches, using the dn2id and id2children buckets to generate
|
||||
// a list of Result entries. Currently this does strip of the non-request attribute
|
||||
// Neither does it support LDAP filters. For now we rely on the frontent (LDAPServer)
|
||||
// to both.
|
||||
func (bdb *LdbBolt) Search(base string, scope int) ([]*ldap.Entry, error) {
|
||||
entries := []*ldap.Entry{}
|
||||
nDN, err := ldapdn.ParseNormalize(base)
|
||||
if err != nil {
|
||||
return entries, err
|
||||
}
|
||||
|
||||
err = bdb.db.View(func(tx *bolt.Tx) error {
|
||||
entryID := bdb.getIDByDN(tx, nDN)
|
||||
var entryIDs []uint64
|
||||
if entryID == 0 {
|
||||
return fmt.Errorf("not found")
|
||||
}
|
||||
switch scope {
|
||||
case ldap.ScopeBaseObject:
|
||||
entryIDs = append(entryIDs, entryID)
|
||||
case ldap.ScopeSingleLevel:
|
||||
entryIDs = bdb.getChildrenIDs(tx, entryID)
|
||||
case ldap.ScopeWholeSubtree:
|
||||
entryIDs = append(entryIDs, entryID)
|
||||
entryIDs = append(entryIDs, bdb.getSubtreeIDs(tx, entryID)...)
|
||||
}
|
||||
for _, id := range entryIDs {
|
||||
entry, err := bdb.getEntryByID(tx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return entries, err
|
||||
}
|
||||
|
||||
func idToBytes(id uint64) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(b, id)
|
||||
return b
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) getChildrenIDs(tx *bolt.Tx, parent uint64) []uint64 {
|
||||
id2Children := tx.Bucket([]byte("id2children"))
|
||||
children := id2Children.Get(idToBytes(parent))
|
||||
r := bytes.NewReader(children)
|
||||
ids := make([]uint64, len(children)/8)
|
||||
if err := binary.Read(r, binary.LittleEndian, &ids); err != nil {
|
||||
bdb.logger.Error(err)
|
||||
}
|
||||
// This logging it too verbose even for the "debug" level. Leaving
|
||||
// it here commented out as it can be helpful during development.
|
||||
// bdb.logger.WithFields(logrus.Fields{
|
||||
// "parentid": parent,
|
||||
// "children": ids,
|
||||
// }).Debug("getChildrenIDs")
|
||||
return ids
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) getSubtreeIDs(tx *bolt.Tx, root uint64) []uint64 {
|
||||
var res []uint64
|
||||
children := bdb.getChildrenIDs(tx, root)
|
||||
res = append(res, children...)
|
||||
for _, child := range children {
|
||||
res = append(res, bdb.getSubtreeIDs(tx, child)...)
|
||||
}
|
||||
// This logging it too verbose even for the "debug" level. Leaving
|
||||
// it here commented out as it can be helpful during development.
|
||||
// bdb.logger.WithFields(logrus.Fields{
|
||||
// "rootid": root,
|
||||
// "subtree": res,
|
||||
// }).Debug("getSubtreeIDs")
|
||||
return res
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) EntryPut(e *ldap.Entry) error {
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
if err := enc.Encode(e); err != nil {
|
||||
fmt.Printf("%v\n", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
dn, _ := ldap.ParseDN(e.DN)
|
||||
parentDN := &ldap.DN{
|
||||
RDNs: dn.RDNs[1:],
|
||||
}
|
||||
nDN := ldapdn.Normalize(dn)
|
||||
|
||||
if !strings.HasSuffix(nDN, bdb.base) {
|
||||
return fmt.Errorf("'%s' is not a descendant of '%s'", e.DN, bdb.base)
|
||||
}
|
||||
|
||||
nParentDN := ldapdn.Normalize(parentDN)
|
||||
err := bdb.db.Update(func(tx *bolt.Tx) error {
|
||||
id2entry := tx.Bucket([]byte("id2entry"))
|
||||
id := bdb.getIDByDN(tx, nDN)
|
||||
if id != 0 {
|
||||
return ErrEntryAlreadyExists
|
||||
}
|
||||
var err error
|
||||
if id, err = id2entry.NextSequence(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := id2entry.Put(idToBytes(id), buf.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
if nDN != bdb.base {
|
||||
if err := bdb.addID2Children(tx, nParentDN, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
dn2id := tx.Bucket([]byte("dn2id"))
|
||||
if err := dn2id.Put([]byte(nDN), idToBytes(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) EntryDelete(dn string) error {
|
||||
parsed, err := ldap.ParseDN(dn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pparentDN := &ldap.DN{
|
||||
RDNs: parsed.RDNs[1:],
|
||||
}
|
||||
pdn := ldapdn.Normalize(pparentDN)
|
||||
|
||||
ndn := ldapdn.Normalize(parsed)
|
||||
err = bdb.db.Update(func(tx *bolt.Tx) error {
|
||||
// Does this entry even exist?
|
||||
entryID := bdb.getIDByDN(tx, ndn)
|
||||
if entryID == 0 {
|
||||
return ErrEntryNotFound
|
||||
}
|
||||
|
||||
// Refuse to delete if the entry has childs
|
||||
id2Children := tx.Bucket([]byte("id2children"))
|
||||
children := id2Children.Get(idToBytes(entryID))
|
||||
if len(children) != 0 {
|
||||
return ErrNonLeafEntry
|
||||
}
|
||||
|
||||
// Update id2children bucket (remove entryid from parent)
|
||||
parentid := bdb.getIDByDN(tx, pdn)
|
||||
if parentid == 0 {
|
||||
return ErrEntryNotFound
|
||||
}
|
||||
children = id2Children.Get(idToBytes(parentid))
|
||||
r := bytes.NewReader(children)
|
||||
var newids []byte
|
||||
idBytes := make([]byte, 8)
|
||||
for _, err = io.ReadFull(r, idBytes); err == nil; _, err = io.ReadFull(r, idBytes) {
|
||||
if entryID != binary.LittleEndian.Uint64(idBytes) {
|
||||
newids = append(newids, idBytes...)
|
||||
}
|
||||
}
|
||||
if err = id2Children.Put(idToBytes(parentid), newids); err != nil {
|
||||
return fmt.Errorf("error updating id2Children index for %d: %w", parentid, err)
|
||||
}
|
||||
|
||||
// Remove entry from dn2id bucket
|
||||
dn2id := tx.Bucket([]byte("dn2id"))
|
||||
err = dn2id.Delete([]byte(ndn))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id2entry := tx.Bucket([]byte("id2entry"))
|
||||
err = id2entry.Delete(idToBytes(entryID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) EntryModify(req *ldap.ModifyRequest) error {
|
||||
ndn, err := ldapdn.ParseNormalize(req.DN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = bdb.db.Update(func(tx *bolt.Tx) error {
|
||||
oldEntry, id, innerErr := bdb.getEntryByDN(tx, ndn)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
return bdb.entryModifyWithTxn(tx, id, oldEntry, req)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) entryModifyWithTxn(tx *bolt.Tx, id uint64, entry *ldap.Entry, req *ldap.ModifyRequest) error {
|
||||
newEntry, innerErr := ldapentry.ApplyModify(entry, req)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
if innerErr := enc.Encode(newEntry); innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
id2entry := tx.Bucket([]byte("id2entry"))
|
||||
if innerErr := id2entry.Put(idToBytes(id), buf.Bytes()); innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) EntryModifyDN(req *ldap.ModifyDNRequest) error {
|
||||
olddn, err := ldap.ParseDN(req.DN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newrdn, err := ldap.ParseDN(req.NewRDN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var newDN ldap.DN
|
||||
|
||||
newDN.RDNs = []*ldap.RelativeDN{newrdn.RDNs[0]}
|
||||
newDN.RDNs = append(newDN.RDNs, olddn.RDNs[1:]...)
|
||||
|
||||
err = bdb.db.Update(func(tx *bolt.Tx) error {
|
||||
flatNewDN := ldapdn.Normalize(&newDN)
|
||||
flatOldDN := ldapdn.Normalize(olddn)
|
||||
|
||||
// error out if there is an entry with the new name already
|
||||
if id := bdb.getIDByDN(tx, flatNewDN); id != 0 {
|
||||
return ErrEntryAlreadyExists
|
||||
}
|
||||
|
||||
entry, id, innerErr := bdb.getEntryByDN(tx, flatOldDN)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
|
||||
// only allow renaming leaf entries
|
||||
childIds := bdb.getChildrenIDs(tx, id)
|
||||
if len(childIds) > 0 {
|
||||
return ErrNonLeafEntry
|
||||
}
|
||||
|
||||
entry.DN = flatNewDN
|
||||
|
||||
modReq := ldap.ModifyRequest{
|
||||
DN: entry.DN,
|
||||
}
|
||||
|
||||
// create modify operation for the change attribute values
|
||||
if req.DeleteOldRDN {
|
||||
oldRDN := olddn.RDNs[0]
|
||||
for _, ava := range oldRDN.Attributes {
|
||||
modReq.Delete(ava.Type, []string{ava.Value})
|
||||
}
|
||||
}
|
||||
for _, ava := range newrdn.RDNs[0].Attributes {
|
||||
modReq.Add(ava.Type, []string{ava.Value})
|
||||
}
|
||||
innerErr = bdb.entryModifyWithTxn(tx, id, entry, &modReq)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
|
||||
// update the dn2id index
|
||||
dn2id := tx.Bucket([]byte("dn2id"))
|
||||
if err := dn2id.Put([]byte(flatNewDN), idToBytes(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dn2id.Delete([]byte(flatOldDN)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) UpdatePassword(req *ldap.PasswordModifyRequest) error {
|
||||
ndn, err := ldapdn.ParseNormalize(req.UserIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bdb.db.Update(func(tx *bolt.Tx) error {
|
||||
userEntry, id, innerErr := bdb.getEntryByDN(tx, ndn)
|
||||
if innerErr != nil {
|
||||
return innerErr
|
||||
}
|
||||
// Note: the password check we perform here is more or less unneeded.
|
||||
// If the request got here it's either issued by the admin (which does
|
||||
// not need the old password to reset a users password) or a user trying
|
||||
// to update its own password. In which case the password is already verified
|
||||
// as we only allow authenticated users to issue this request. Still, if
|
||||
// the request contains an old password we verify it and error out if it
|
||||
// doesn't match.
|
||||
if req.OldPassword != "" {
|
||||
userPassword := userEntry.GetEqualFoldAttributeValue("userPassword")
|
||||
match, err := ldappassword.Validate(req.OldPassword, userPassword)
|
||||
if err != nil {
|
||||
bdb.logger.Error(err)
|
||||
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("Failed to validate old Password"))
|
||||
}
|
||||
if !match {
|
||||
bdb.logger.Debug("Old password does not match")
|
||||
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("Failed to validate old Password"))
|
||||
}
|
||||
}
|
||||
|
||||
mod := ldap.ModifyRequest{}
|
||||
mod.DN = req.UserIdentity
|
||||
mod.Replace("userPassword", []string{req.NewPassword})
|
||||
innerErr = bdb.entryModifyWithTxn(tx, id, userEntry, &mod)
|
||||
if innerErr != nil {
|
||||
bdb.logger.Debugf("Failed to update password for '%s': '%s'", ndn, err)
|
||||
return ldap.NewError(ldap.LDAPResultOperationsError, errors.New("Failed to update Password"))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) addID2Children(tx *bolt.Tx, nParentDN string, newChildID uint64) error {
|
||||
bdb.logger.Debugf("AddID2Children '%s' id '%d'", nParentDN, newChildID)
|
||||
parentID := bdb.getIDByDN(tx, nParentDN)
|
||||
if parentID == 0 {
|
||||
return fmt.Errorf("parent not found '%s'", nParentDN)
|
||||
}
|
||||
|
||||
bdb.logger.Debugf("Parent ID: %v", parentID)
|
||||
|
||||
id2Children := tx.Bucket([]byte("id2children"))
|
||||
// FIXME add sanity check here if ID is already present
|
||||
children := id2Children.Get(idToBytes(parentID))
|
||||
children = append(children, idToBytes(newChildID)...)
|
||||
if err := id2Children.Put(idToBytes(parentID), children); err != nil {
|
||||
return fmt.Errorf("error updating id2Children index for %d: %w", parentID, err)
|
||||
}
|
||||
|
||||
bdb.logger.Debugf("AddID2Children '%d' id '%v'", parentID, children)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) getIDByDN(tx *bolt.Tx, nDN string) uint64 {
|
||||
dn2id := tx.Bucket([]byte("dn2id"))
|
||||
if dn2id == nil {
|
||||
bdb.logger.Debugf("Bucket 'dn2id' does not exist")
|
||||
return 0
|
||||
}
|
||||
id := dn2id.Get([]byte(nDN))
|
||||
if id == nil {
|
||||
bdb.logger.Debugf("DN: '%s' not found", nDN)
|
||||
return 0
|
||||
}
|
||||
return binary.LittleEndian.Uint64(id)
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) getEntryByID(tx *bolt.Tx, id uint64) (entry *ldap.Entry, err error) {
|
||||
id2entry := tx.Bucket([]byte("id2entry"))
|
||||
entrybytes := id2entry.Get(idToBytes(id))
|
||||
buf := bytes.NewBuffer(entrybytes)
|
||||
dec := gob.NewDecoder(buf)
|
||||
if err := dec.Decode(&entry); err != nil {
|
||||
return nil, fmt.Errorf("error decoding entry id: %d, %w", id, err)
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) getEntryByDN(tx *bolt.Tx, ndn string) (entry *ldap.Entry, id uint64, err error) {
|
||||
id = bdb.getIDByDN(tx, ndn)
|
||||
if id == 0 {
|
||||
return nil, id, ErrEntryNotFound
|
||||
}
|
||||
entry, err = bdb.getEntryByID(tx, id)
|
||||
return entry, id, err
|
||||
}
|
||||
|
||||
func (bdb *LdbBolt) Close() {
|
||||
bdb.db.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user