mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-02 02:59:00 -05:00
486 lines
13 KiB
Go
486 lines
13 KiB
Go
/*
|
|
* 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()
|
|
}
|