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
+94
View File
@@ -0,0 +1,94 @@
package ldapdn
import (
"bytes"
"encoding/hex"
"github.com/go-ldap/ldap/v3"
"golang.org/x/text/cases"
)
// Normalize takes an ldap.DN struct and turns it into a "normalized" DN string
// by cases folding all RDN (attributetypes and values). Note: This currently
// handles all attributes as caseIgnoreStrings ignoring the Syntax the Attribute
// Type might have assigned.
func Normalize(dn *ldap.DN) string {
var nDN string
caseFold := cases.Fold()
for r, rdn := range dn.RDNs {
// FIXME to really normalize multivalued RDNs we'd need
// to normalize the order of Attributes here as well
for a, ava := range rdn.Attributes {
if a > 0 {
// This is a multivalued RDN.
nDN += "+"
} else if r > 0 {
nDN += ","
}
nDN = nDN + caseFold.String(ava.Type) + "=" + encodeRDNValue(caseFold.String(ava.Value))
}
}
return nDN
}
// encodeRDNValue applies the DN escaping rules (RFC4514) to the supplied
// string (the value part of an RDN). Returns the escaped string.
// Note: This function is taken from https://github.com/go-ldap/ldap/pull/104
func encodeRDNValue(rDNValue string) string {
encodedBuf := bytes.Buffer{}
escapeChar := func(c byte) {
encodedBuf.WriteByte('\\')
encodedBuf.WriteByte(c)
}
escapeHex := func(c byte) {
encodedBuf.WriteByte('\\')
encodedBuf.WriteString(hex.EncodeToString([]byte{c}))
}
for i := 0; i < len(rDNValue); i++ {
char := rDNValue[i]
if i == 0 && char == ' ' || char == '#' {
// Special case leading space or number sign.
escapeChar(char)
continue
}
if i == len(rDNValue)-1 && char == ' ' {
// Special case trailing space.
escapeChar(char)
continue
}
switch char {
case '"', '+', ',', ';', '<', '>', '\\':
// Each of these special characters must be escaped.
escapeChar(char)
continue
}
if char < ' ' || char > '~' {
// All special character escapes are handled first
// above. All bytes less than ASCII SPACE and all bytes
// greater than ASCII TILDE must be hex-escaped.
escapeHex(char)
continue
}
// Any other character does not require escaping.
encodedBuf.WriteByte(char)
}
return encodedBuf.String()
}
// ParseNormalize normalizes the passed LDAP DN string by first parsing it (using ldap.ParseDN)
// and then casefolding all RDN using ldapdn.Normalize(). ParseNormalize will return an error
// when parsing the DN fails.
func ParseNormalize(dn string) (string, error) {
parsed, err := ldap.ParseDN(dn)
if err != nil {
return "", err
}
return Normalize(parsed), nil
}
+144
View File
@@ -0,0 +1,144 @@
package ldapentry
import (
"errors"
"github.com/go-ldap/ldap/v3"
"golang.org/x/text/cases"
)
func ApplyModify(old *ldap.Entry, mod *ldap.ModifyRequest) (newEntry *ldap.Entry, err error) {
oldDN, err := ldap.ParseDN(old.DN)
if err != nil {
return nil, err
}
rdn := oldDN.RDNs[0]
modDN, err := ldap.ParseDN(mod.DN)
if err != nil {
return nil, err
}
// This shouldn't happen if we ge here (TM)
if !oldDN.EqualFold(modDN) {
return nil, ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("DNs do not match"))
}
casefold := cases.Fold()
newEntry = ldap.NewEntry(old.DN, map[string][]string{})
newEntry.Attributes = old.Attributes
for _, c := range mod.Changes {
nType := casefold.String(c.Modification.Type)
switch c.Operation {
case ldap.AddAttribute:
newValues := entryApplyModAdd(newEntry.GetEqualFoldAttributeValues(nType), c.Modification.Vals)
newEntry.Attributes = entryReplaceValues(newEntry.Attributes, c.Modification.Type, newValues)
case ldap.ReplaceAttribute:
// Modifies on RDN attributes need special care to make sure that the rdn Value is not removed
for _, rdnAttr := range rdn.Attributes {
if nType == casefold.String(rdnAttr.Type) {
rdnPresent := false
nRdnVal := casefold.String(rdnAttr.Value)
for _, newVal := range c.Modification.Vals {
if nRdnVal == casefold.String(newVal) {
rdnPresent = true
break
}
}
if !rdnPresent {
return nil, ldap.NewError(ldap.LDAPResultNotAllowedOnRDN, errors.New(""))
}
}
}
newEntry.Attributes = entryReplaceValues(newEntry.Attributes, c.Modification.Type, c.Modification.Vals)
case ldap.DeleteAttribute:
for _, rdnAttr := range rdn.Attributes {
// Modifies on RDN attributes need special care
if nType == casefold.String(rdnAttr.Type) {
if len(c.Modification.Vals) == 0 {
return nil, ldap.NewError(ldap.LDAPResultNotAllowedOnRDN, errors.New(""))
}
nRdnVal := casefold.String(rdnAttr.Value)
for _, delVal := range c.Modification.Vals {
if nRdnVal == casefold.String(delVal) {
return nil, ldap.NewError(ldap.LDAPResultNotAllowedOnRDN, errors.New(""))
}
}
}
}
newValues := entryApplyModDelete(old.GetEqualFoldAttributeValues(nType), c.Modification.Vals)
newEntry.Attributes = entryReplaceValues(newEntry.Attributes, c.Modification.Type, newValues)
}
}
return newEntry, nil
}
func entryReplaceValues(ea []*ldap.EntryAttribute, attrType string, newValues []string) (updatedAttrs []*ldap.EntryAttribute) {
casefold := cases.Fold()
nType := casefold.String(attrType)
updated := false
for _, attr := range ea {
if casefold.String(attr.Name) == nType {
updated = true
if len(newValues) == 0 {
continue
}
updatedAttrs = append(updatedAttrs, ldap.NewEntryAttribute(attr.Name, newValues))
} else {
updatedAttrs = append(updatedAttrs, attr)
}
}
if !updated {
if len(newValues) != 0 {
updatedAttrs = append(updatedAttrs, ldap.NewEntryAttribute(attrType, newValues))
}
}
return updatedAttrs
}
func entryApplyModAdd(curVals, addVals []string) (newVals []string) {
newVals = curVals
casefold := cases.Fold()
for _, newVal := range addVals {
present := false
for _, val := range curVals {
if casefold.String(newVal) == casefold.String(val) {
present = true
break
}
}
if !present {
newVals = append(newVals, newVal)
}
}
return newVals
}
func entryApplyModDelete(curVals, delVals []string) (newVals []string) {
casefold := cases.Fold()
if len(delVals) == 0 {
return []string{}
}
for _, curVal := range curVals {
nCurVal := casefold.String(curVal)
keep := true
for _, del := range delVals {
if nCurVal == casefold.String(del) {
keep = false
break
}
}
if keep {
newVals = append(newVals, curVal)
}
}
return newVals
}
func EntryFromAddRequest(add *ldap.AddRequest) *ldap.Entry {
attrs := map[string][]string{}
for _, a := range add.Attributes {
attrs[a.Type] = a.Vals
}
return ldap.NewEntry(add.DN, attrs)
}
+16
View File
@@ -0,0 +1,16 @@
//go:build !disable_crypt && (linux || freebsd || netbsd)
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2021 The LibreGraph Authors.
*/
package ldappassword
import (
gocrypt "github.com/amoghe/go-crypt"
)
func crypt(pass, salt string) (string, error) {
return gocrypt.Crypt(pass, salt)
}
@@ -0,0 +1,16 @@
//go:build disable_crypt || darwin || windows
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2021 The LibreGraph Authors.
*/
package ldappassword
import (
"errors"
)
func crypt(pass, salt string) (string, error) {
return "", errors.New("CRYPT unsupported on this platform")
}
@@ -0,0 +1,133 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2021 The LibreGraph Authors.
*/
package ldappassword
import (
"crypto/rand"
"crypto/sha1" //nolint,gosec
"crypto/subtle"
"encoding/base64"
"fmt"
"math/big"
"strings"
"github.com/alexedwards/argon2id"
"github.com/trustelem/zxcvbn"
)
var Argon2DefaultParams = argon2id.DefaultParams
func Validate(password string, hash string) (bool, error) {
algorithm := ""
if hash[0] == '{' {
algorithmEnd := strings.Index(hash[0:], "}")
if algorithmEnd >= 1 {
algorithm = hash[0 : algorithmEnd+1]
hash = hash[algorithmEnd+1:]
}
}
hashBytes := []byte(hash)
var passwordBytes []byte
switch strings.ToUpper(algorithm) {
case "":
// No password scheme, direct comparison.
passwordBytes = []byte(password)
case "{ARGON2}":
// Follows the format used by the Argon2 reference C implementation and looks like this:
// $argon2id$v=19$m=65536,t=3,p=2$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
match, err := argon2id.ComparePasswordAndHash(password, hash)
if err != nil {
return false, fmt.Errorf("argon2 error: %w", err)
}
if !match {
return false, fmt.Errorf("invalid credentials")
}
return true, nil
case "{CRYPT}":
// By default the salt is a two character string.
salt := hash[:2]
if hash[0] == '$' {
// In the glibc2 version, salt format for additional encryption
// $id$salt$encrypted.
hashParts := strings.SplitN(hash, "$", 4)
if len(hashParts) == 4 {
salt = strings.Join(hashParts[:4], "$")
}
}
encrypted, err := crypt(password, salt)
if err != nil {
return false, fmt.Errorf("crypt error: %w", err)
}
passwordBytes = []byte(encrypted)
case "{SSHA}":
// BASE64(SHA-1(clear_text + salt) + salt)
// The salt is 4 bytes long.
decodedBytes, err := base64.StdEncoding.DecodeString(hash)
if err != nil {
return false, fmt.Errorf("ssha error: %w", err)
}
salt := decodedBytes[len(decodedBytes)-4:]
h := sha1.New() //nolint,gosec
h.Write([]byte(password))
h.Write(salt)
passwordBytes = h.Sum(nil)
passwordBytes = append(passwordBytes, salt...)
hashBytes = decodedBytes
default:
return false, fmt.Errorf("unsupported password algorithm: %s", algorithm)
}
if subtle.ConstantTimeCompare(hashBytes, passwordBytes) != 1 {
return false, fmt.Errorf("invalid credentials")
}
return true, nil
}
func Hash(password string, algorithm string) (string, error) {
var result string
switch algorithm {
case "", "{CLEARTEXT}":
result = password
case "{ARGON2}":
hash, hashErr := argon2id.CreateHash(password, Argon2DefaultParams)
if hashErr != nil {
return "", fmt.Errorf("password hash error: %w", hashErr)
}
result = "{ARGON2}" + hash
default:
return "", fmt.Errorf("password hash alg not supported: %s", algorithm)
}
return result, nil
}
func EstimatePasswordStrength(password string, userInputs []string) int {
result := zxcvbn.PasswordStrength(password, userInputs)
return result.Score
}
func GenerateRandomPassword(length int) (string, error) {
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-=+!@#$%^&*."
ret := make([]byte, length)
for i := 0; i < length; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
if err != nil {
return "", err
}
ret[i] = chars[num.Int64()]
}
return string(ret), nil
}
+28
View File
@@ -0,0 +1,28 @@
Copyright (c) 2012 The Go Authors. All rights reserved.
Copyright (c) 2021 The LibreGraph Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+21
View File
@@ -0,0 +1,21 @@
# LDAP server library for Golang
This library provides LDAP server v3 functionality for the GO programming
language.
The server implementation is based on github.com/nmcclain/ldap and is enhanced
so it can be used together with github.com/go-ldap/ldap/v3.
From the server perspective, all of RFC4510 is implemented except:
4.5.1.3. SearchRequest.derefAliases
4.5.1.5. SearchRequest.timeLimit
4.5.1.6. SearchRequest.typesOnly
4.14. StartTLS Operation
The purpose of this library is not a general LDAP server implementation but to
provide enough of an LDAP server for Kopano compatible identity management.
## License
See `LICENSE.txt` for licensing information of this module.
+127
View File
@@ -0,0 +1,127 @@
package ldapserver
import (
"errors"
"fmt"
"net"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/go-ldap/ldap/v3"
)
func HandleAddRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) error {
if boundDN == "" {
return ldap.NewError(ldap.LDAPResultInsufficientAccessRights, errors.New("anonymous Write denied"))
}
addReq, err := parseAddRequest(req)
if err != nil {
return err
}
fnNames := []string{}
for k := range server.AddFns {
fnNames = append(fnNames, k)
}
fn := routeFunc(addReq.DN, fnNames)
var adder Adder
if adder = server.AddFns[fn]; adder == nil {
if fn == "" {
err = fmt.Errorf("no suitable handler found for dn: '%s'", addReq.DN)
} else {
err = fmt.Errorf("handler '%s' does not support add", fn)
}
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
}
code, err := adder.Add(boundDN, addReq, conn)
return ldap.NewError(uint16(code), err)
}
func parseAddRequest(req *ber.Packet) (*ldap.AddRequest, error) {
addReq := ldap.AddRequest{}
// LDAP Add request have 2 Elements (DN and AttributeList)
if len(req.Children) != 2 {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid add request"))
}
dn, ok := req.Children[0].Value.(string)
if !ok {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding entry DN"))
}
_, err := ldap.ParseDN(dn)
if err != nil {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
}
addReq.DN = dn
al, err := parseAttributeList(req.Children[1])
if err != nil {
return nil, err
}
addReq.Attributes = al
return &addReq, nil
}
func parseAttributeList(req *ber.Packet) ([]ldap.Attribute, error) {
ldapAttrs := []ldap.Attribute{}
if req.ClassType != ber.ClassUniversal || req.TagType != ber.TypeConstructed || req.Tag != ber.TagSequence {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute List"))
}
for _, a := range req.Children {
attr, err := parseAttribute(a, false)
if err != nil {
return nil, err
}
ldapAttrs = append(ldapAttrs, *attr)
}
return ldapAttrs, nil
}
func parseAttribute(attr *ber.Packet, partial bool) (*ldap.Attribute, error) {
var la ldap.Attribute
var ok bool
var err error
// Partial attributes, might just contain a type and allow the Value to be absent
if partial && (len(attr.Children) < 1 || len(attr.Children) > 2) {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding partial Attribute"))
} else if !partial && len(attr.Children) != 2 {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute"))
}
ad := attr.Children[0]
if ad.ClassType != ber.ClassUniversal || ad.TagType != ber.TypePrimitive || ad.Tag != ber.TagOctetString {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute Description"))
}
la.Type, ok = ad.Value.(string)
if !ok {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute Description"))
}
// We can return here if this is a Partial Attribute without values
if partial && len(attr.Children) == 1 {
return &la, nil
}
if la.Vals, err = parseAttributeValues(attr.Children[1]); err != nil {
return nil, err
}
return &la, nil
}
func parseAttributeValues(values *ber.Packet) ([]string, error) {
var strVals []string
if values.ClassType != ber.ClassUniversal || values.TagType != ber.TypeConstructed || values.Tag != ber.TagSet {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute Values"))
}
for _, value := range values.Children {
strVal, ok := value.Value.(string)
if !ok {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Attribute Value"))
}
strVals = append(strVals, strVal)
}
return strVals, nil
}
+79
View File
@@ -0,0 +1,79 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Copyright 2021 The LibreGraph Authors.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ldapserver
import (
"net"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/go-ldap/ldap/v3"
)
func HandleBindRequest(req *ber.Packet, fns map[string]Binder, conn net.Conn) (resultCode LDAPResultCode) {
defer func() {
if r := recover(); r != nil {
resultCode = ldap.LDAPResultOperationsError
}
}()
// we only support ldapv3
ldapVersion, ok := req.Children[0].Value.(int64)
if !ok {
return ldap.LDAPResultProtocolError
}
if ldapVersion != 3 {
logger.V(1).Info("Unsupported LDAP version", "version", ldapVersion)
return ldap.LDAPResultInappropriateAuthentication
}
// auth types
bindDN, ok := req.Children[1].Value.(string)
if !ok {
return ldap.LDAPResultProtocolError
}
bindAuth := req.Children[2]
switch bindAuth.Tag {
default:
logger.V(1).Info("Unknown LDAP authentication method", "tag", bindAuth.Tag)
return ldap.LDAPResultInappropriateAuthentication
case LDAPBindAuthSimple:
if len(req.Children) == 3 {
fnNames := []string{}
for k := range fns {
fnNames = append(fnNames, k)
}
fn := routeFunc(bindDN, fnNames)
resultCode, err := fns[fn].Bind(bindDN, bindAuth.Data.String(), conn)
if err != nil {
logger.Error(err, "BindFn Error")
return ldap.LDAPResultOperationsError
}
return resultCode
} else {
logger.V(1).Info("Simple bind request has wrong # children. len(req.Children) != 3")
return ldap.LDAPResultInappropriateAuthentication
}
case LDAPBindAuthSASL:
logger.V(1).Info("SASL authentication is not supported")
return ldap.LDAPResultInappropriateAuthentication
}
return ldap.LDAPResultOperationsError
}
func encodeBindResponse(messageID int64, ldapResultCode LDAPResultCode) *ber.Packet {
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
bindReponse := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ldap.ApplicationBindResponse, nil, "Bind Response")
bindReponse.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: "))
bindReponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
bindReponse.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "errorMessage: "))
responsePacket.AppendChild(bindReponse)
// ber.PrintPacket(responsePacket)
return responsePacket
}
+54
View File
@@ -0,0 +1,54 @@
package ldapserver
import (
"errors"
"fmt"
"net"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/go-ldap/ldap/v3"
)
func HandleDeleteRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) error {
if boundDN == "" {
return ldap.NewError(ldap.LDAPResultInsufficientAccessRights, errors.New("anonymous Write denied"))
}
delReq, err := parseDeleteRequest(req)
if err != nil {
return err
}
fnNames := []string{}
for k := range server.DeleteFns {
fnNames = append(fnNames, k)
}
fn := routeFunc(delReq.DN, fnNames)
var del Deleter
if del = server.DeleteFns[fn]; del == nil {
if fn == "" {
err = fmt.Errorf("no suitable handler found for dn: '%s'", delReq.DN)
} else {
err = fmt.Errorf("handler '%s' does not support add", fn)
}
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
}
code, err := del.Delete(boundDN, delReq, conn)
return ldap.NewError(uint16(code), err)
}
func parseDeleteRequest(req *ber.Packet) (*ldap.DelRequest, error) {
delReq := ldap.DelRequest{}
// LDAP Delete requests contain just the DN (no Sequence, or set)
// i.e. they have no childre
if len(req.Children) != 0 {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid delete request"))
}
dn := req.Data.String()
_, err := ldap.ParseDN(dn)
if err != nil {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
}
delReq.DN = dn
return &delReq, nil
}
+90
View File
@@ -0,0 +1,90 @@
package ldapserver
import (
"errors"
"net"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/go-ldap/ldap/v3"
)
type ExopHandler func(req *ber.Packet, boundDN string, server *Server, conn net.Conn) (*ber.Packet, error)
type ExtendedRequest struct {
OID string
Body *ber.Packet
}
var exopRegistry = map[string]ExopHandler{}
func RegisterExtendedOperation(oid string, handler ExopHandler) {
exopRegistry[oid] = handler
}
func HandleExtendedRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) (*ber.Packet, error) {
extReq, err := parseExtendedRequest(req)
if err != nil {
logger.V(1).Info("parsing extened request failed", "error", err)
return nil, err
}
if handler, ok := exopRegistry[extReq.OID]; ok {
innerBer, err := handler(extReq.Body, boundDN, server, conn)
var resCode LDAPResultCode = ldap.LDAPResultSuccess
msg := ""
if err != nil {
if lerr, ok := err.(*ldap.Error); ok {
msg = lerr.Err.Error()
resCode = LDAPResultCode(lerr.ResultCode)
} else {
msg = err.Error()
resCode = ldap.LDAPResultOther
}
}
return encodeExtendedResponse(resCode, msg, extReq.OID, innerBer), nil
} else {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Unsupported Extented Operations"))
}
return nil, err
}
func parseExtendedRequest(req *ber.Packet) (*ExtendedRequest, error) {
// RFC 4511 Extended Operation:
// ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
// requestName [0] LDAPOID,
// requestValue [1] OCTET STRING OPTIONAL }
extReq := ExtendedRequest{}
if len(req.Children) < 1 || len(req.Children) > 2 {
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid extented request"))
}
if req.Children[0].Identifier.ClassType != ber.ClassContext || req.Children[0].Identifier.Tag != 0 {
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("error decoding extented request OID"))
}
extReq.OID = req.Children[0].Data.String()
if len(req.Children) == 2 {
if req.Children[1].Identifier.ClassType != ber.ClassContext || req.Children[1].Identifier.Tag != 1 {
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("error decoding extented request OID"))
}
extReq.Body = req.Children[1]
}
logger.V(1).Info("Extended Request", "oid", extReq.OID)
return &extReq, nil
}
func encodeExtendedResponse(rescode LDAPResultCode, msg, oid string, responseValue *ber.Packet) *ber.Packet {
respBer := ber.Encode(ber.ClassApplication, ber.TypeConstructed,
ber.Tag(ldap.ApplicationExtendedResponse), nil,
ldap.ApplicationMap[ldap.ApplicationExtendedResponse])
respBer.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(rescode), "resultCode: "))
respBer.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
respBer.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, msg, "errorMessage: "))
respBer.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 10, oid, "responseName"))
if responseValue != nil {
encValue := ber.Encode(ber.ClassContext, ber.TypePrimitive, 11, nil, "responseValue")
encValue.AppendChild(responseValue)
respBer.AppendChild(encValue)
}
return respBer
}
+193
View File
@@ -0,0 +1,193 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Copyright 2021 The LibreGraph Authors.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ldapserver
import (
"strings"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/go-ldap/ldap/v3"
"golang.org/x/text/cases"
)
const (
FilterAnd = ldap.FilterAnd
FilterOr = ldap.FilterOr
FilterNot = ldap.FilterNot
FilterEqualityMatch = ldap.FilterEqualityMatch
FilterSubstrings = ldap.FilterSubstrings
FilterGreaterOrEqual = ldap.FilterGreaterOrEqual
FilterLessOrEqual = ldap.FilterLessOrEqual
FilterPresent = ldap.FilterPresent
FilterApproxMatch = ldap.FilterApproxMatch
FilterExtensibleMatch = ldap.FilterExtensibleMatch
)
var (
FilterMap = ldap.FilterMap
casefold = cases.Fold()
)
const (
FilterSubstringsInitial = ldap.FilterSubstringsInitial
FilterSubstringsAny = ldap.FilterSubstringsAny
FilterSubstringsFinal = ldap.FilterSubstringsFinal
)
func CompileFilter(filter string) (*ber.Packet, error) {
return ldap.CompileFilter(filter)
}
func DecompileFilter(packet *ber.Packet) (ret string, err error) {
return ldap.DecompileFilter(packet)
}
func ServerApplyFilter(f *ber.Packet, entry *ldap.Entry) (bool, LDAPResultCode) {
switch FilterMap[uint64(f.Tag)] {
default:
//log.Fatalf("Unknown LDAP filter code: %d", f.Tag)
return false, ldap.LDAPResultOperationsError
case "Equality Match":
if len(f.Children) != 2 {
return false, ldap.LDAPResultOperationsError
}
attribute := f.Children[0].Value.(string)
value := f.Children[1].Value.(string)
for _, a := range entry.Attributes {
if strings.EqualFold(a.Name, attribute) {
for _, v := range a.Values {
if strings.EqualFold(v, value) {
return true, ldap.LDAPResultSuccess
}
}
}
}
case "Present":
for _, a := range entry.Attributes {
if strings.EqualFold(a.Name, f.Data.String()) {
return true, ldap.LDAPResultSuccess
}
}
case "And":
for _, child := range f.Children {
ok, exitCode := ServerApplyFilter(child, entry)
if exitCode != ldap.LDAPResultSuccess {
return false, exitCode
}
if !ok {
return false, ldap.LDAPResultSuccess
}
}
return true, ldap.LDAPResultSuccess
case "Or":
anyOk := false
for _, child := range f.Children {
ok, exitCode := ServerApplyFilter(child, entry)
if exitCode != ldap.LDAPResultSuccess {
return false, exitCode
} else if ok {
anyOk = true
}
}
if anyOk {
return true, ldap.LDAPResultSuccess
}
case "Not":
if len(f.Children) != 1 {
return false, ldap.LDAPResultOperationsError
}
ok, exitCode := ServerApplyFilter(f.Children[0], entry)
if exitCode != ldap.LDAPResultSuccess {
return false, exitCode
} else if !ok {
return true, ldap.LDAPResultSuccess
}
case "Substrings":
if len(f.Children) != 2 {
return false, ldap.LDAPResultOperationsError
}
attribute := f.Children[0].Value.(string)
bytes := f.Children[1].Children[0].Data.Bytes()
value := casefold.String(string(bytes))
for _, a := range entry.Attributes {
if strings.EqualFold(a.Name, attribute) {
for _, v := range a.Values {
v = casefold.String(v)
switch f.Children[1].Children[0].Tag {
case FilterSubstringsInitial:
if strings.HasPrefix(v, value) {
return true, ldap.LDAPResultSuccess
}
case FilterSubstringsAny:
if strings.Contains(v, value) {
return true, ldap.LDAPResultSuccess
}
case FilterSubstringsFinal:
if strings.HasSuffix(v, value) {
return true, ldap.LDAPResultSuccess
}
}
}
}
}
case "FilterGreaterOrEqual": // TODO
return false, ldap.LDAPResultOperationsError
case "FilterLessOrEqual": // TODO
return false, ldap.LDAPResultOperationsError
case "FilterApproxMatch": // TODO
return false, ldap.LDAPResultOperationsError
case "FilterExtensibleMatch": // TODO
return false, ldap.LDAPResultOperationsError
}
return false, ldap.LDAPResultSuccess
}
func ServerFilterScope(baseDN string, scope int, entry *ldap.Entry) (bool, LDAPResultCode) {
// constrained search scope
parsedBaseDn, err := ldap.ParseDN(baseDN)
if err != nil {
return false, ldap.LDAPResultOperationsError
}
parsedDn, err := ldap.ParseDN(entry.DN)
if err != nil {
return false, ldap.LDAPResultOperationsError
}
switch scope {
case ldap.ScopeWholeSubtree: // The scope is constrained to the entry named by baseObject and to all its subordinates.
case ldap.ScopeBaseObject: // The scope is constrained to the entry named by baseObject.
if !parsedDn.EqualFold(parsedBaseDn) {
return false, ldap.LDAPResultSuccess
}
case ldap.ScopeSingleLevel: // The scope is constrained to the immediate subordinates of the entry named by baseObject.
parts := strings.Split(entry.DN, ",")
if len(parts) < 2 && !parsedDn.EqualFold(parsedBaseDn) {
return false, ldap.LDAPResultSuccess
}
subDn := strings.Join(parts[1:], ",")
parsedSubDn, err := ldap.ParseDN(subDn)
if err != nil {
return false, ldap.LDAPResultOperationsError
}
if !parsedSubDn.EqualFold(parsedBaseDn) {
return false, ldap.LDAPResultSuccess
}
}
return true, ldap.LDAPResultSuccess
}
func ServerFilterAttributes(attributes []string, entry *ldap.Entry) (LDAPResultCode, error) {
// attributes
if len(attributes) > 1 || (len(attributes) == 1 && len(attributes[0]) > 0) {
_, err := filterAttributes(entry, attributes)
if err != nil {
return ldap.LDAPResultOperationsError, err
}
}
return ldap.LDAPResultSuccess, nil
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Copyright 2021 The LibreGraph Authors.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ldapserver
type LDAPResultCode uint8
const (
LDAPBindAuthSimple = 0
LDAPBindAuthSASL = 3
)
+107
View File
@@ -0,0 +1,107 @@
package ldapserver
import (
"errors"
"fmt"
"net"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/go-ldap/ldap/v3"
)
func HandleModifyRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) error {
if boundDN == "" {
return ldap.NewError(ldap.LDAPResultInsufficientAccessRights, errors.New("anonymous Write denied"))
}
modReq, err := parseModifyRequest(req)
if err != nil {
return err
}
logger.V(1).Info("Parsed Modification", "request", dumpModRequest(modReq))
fnNames := []string{}
for k := range server.ModifyFns {
fnNames = append(fnNames, k)
}
fn := routeFunc(modReq.DN, fnNames)
var modifier Modifier
if modifier = server.ModifyFns[fn]; modifier == nil {
if fn == "" {
err = fmt.Errorf("no suitable handler found for dn: '%s'", modReq.DN)
} else {
err = fmt.Errorf("handler '%s' does not support modify", fn)
}
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
}
code, err := modifier.Modify(boundDN, modReq, conn)
return ldap.NewError(uint16(code), err)
}
func dumpModRequest(mr *ldap.ModifyRequest) string {
str := fmt.Sprintf("dn: %s\n", mr.DN)
for _, change := range mr.Changes {
str += fmt.Sprintf("op: %d\n attr: %s values: %v\n", change.Operation, change.Modification.Type, change.Modification.Vals)
}
return str
}
func parseModifyRequest(req *ber.Packet) (*ldap.ModifyRequest, error) {
modReq := ldap.ModifyRequest{}
// LDAP Modify requests have 2 Elements (DN and AttributeList)
if len(req.Children) != 2 {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid modify request"))
}
dn, ok := req.Children[0].Value.(string)
if !ok {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding entry DN"))
}
_, err := ldap.ParseDN(dn)
if err != nil {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
}
modReq.DN = dn
ml, err := parseModList(req.Children[1])
if err != nil {
return nil, err
}
modReq.Changes = ml
return &modReq, nil
}
func parseModList(req *ber.Packet) ([]ldap.Change, error) {
var changes []ldap.Change
if req.ClassType != ber.ClassUniversal || req.TagType != ber.TypeConstructed || req.Tag != ber.TagSequence {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding Changes List"))
}
for _, c := range req.Children {
var change ldap.Change
switch c.Children[0].Data.Bytes()[0] {
default:
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid change operation"))
case ldap.AddAttribute:
change.Operation = ldap.AddAttribute
logger.V(1).Info("op=add")
case ldap.ReplaceAttribute:
change.Operation = ldap.ReplaceAttribute
logger.V(1).Info("op=replace")
case ldap.DeleteAttribute:
change.Operation = ldap.DeleteAttribute
logger.V(1).Info("op=delete")
}
attr, err := parseAttribute(c.Children[1], true)
if err != nil {
return nil, err
}
change.Modification = ldap.PartialAttribute{Type: attr.Type, Vals: attr.Vals}
changes = append(changes, change)
}
return changes, nil
}
+83
View File
@@ -0,0 +1,83 @@
package ldapserver
import (
"errors"
"fmt"
"net"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/go-ldap/ldap/v3"
)
func HandleModifyDNRequest(req *ber.Packet, boundDN string, server *Server, conn net.Conn) error {
if boundDN == "" {
return ldap.NewError(ldap.LDAPResultInsufficientAccessRights, errors.New("anonymous Write denied"))
}
modDNReq, err := parseModifyDNRequest(req)
if err != nil {
return err
}
fnNames := []string{}
for k := range server.ModifyDNFns {
fnNames = append(fnNames, k)
}
fn := routeFunc(modDNReq.DN, fnNames)
var rename Renamer
if rename = server.ModifyDNFns[fn]; rename == nil {
if fn == "" {
err = fmt.Errorf("no suitable handler found for dn: '%s'", modDNReq.DN)
} else {
err = fmt.Errorf("handler '%s' does not support rename", fn)
}
return ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
}
code, err := rename.ModifyDN(boundDN, modDNReq, conn)
return ldap.NewError(uint16(code), err)
return ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid ModifyDN request"))
}
func parseModifyDNRequest(req *ber.Packet) (*ldap.ModifyDNRequest, error) {
modDNReq := ldap.ModifyDNRequest{}
// LDAP ModifyDN requests have up to 4 Elements (DN, newRDN, deleteOld flag and new superior)
if len(req.Children) < 3 || len(req.Children) > 4 {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("invalid ModifyDN request"))
}
dn, ok := req.Children[0].Value.(string)
if !ok {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding entry DN"))
}
_, err := ldap.ParseDN(dn)
if err != nil {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
}
modDNReq.DN = dn
newRDN, ok := req.Children[1].Value.(string)
if !ok {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding entry new RDN"))
}
_, err = ldap.ParseDN(newRDN)
if err != nil {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, err)
}
modDNReq.NewRDN = newRDN
removeOld, ok := req.Children[2].Value.(bool)
if !ok {
return nil, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("error decoding 'deleteOldRDN` flag"))
}
modDNReq.DeleteOldRDN = removeOld
// moving to a new subtree is not yet supported
if len(req.Children) == 4 {
return nil, ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("moving to 'newSuperior' is not implemented"))
}
return &modDNReq, nil
}
+119
View File
@@ -0,0 +1,119 @@
package ldapserver
import (
"errors"
"fmt"
"net"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/go-ldap/ldap/v3"
"github.com/libregraph/idm/pkg/ldappassword"
)
const pwmodOID = "1.3.6.1.4.1.4203.1.11.1"
const (
TagReqIdentity = 0
TagReqOldPW = 1
TagReqNewPW = 2
TagRespGenPW = 0
)
func init() {
RegisterExtendedOperation(pwmodOID, HandlePasswordModifyExOp)
}
func HandlePasswordModifyExOp(req *ber.Packet, boundDN string, server *Server, conn net.Conn) (*ber.Packet, error) {
var passwordGenerated bool
logger.V(1).Info("HandlePasswordModifyExOp")
if boundDN == "" {
return nil, ldap.NewError(ldap.LDAPResultUnwillingToPerform, errors.New("authentication required"))
}
pwReq, err := parsePasswordModifyExop(req)
if err != nil {
return nil, err
}
// If `UserIdentity` is empty, this is a request to update the bound user's own password
if pwReq.UserIdentity == "" {
pwReq.UserIdentity = boundDN
}
if pwReq.NewPassword == "" {
// New password empty means, we're requested to generate a new password
var err error
if pwReq.NewPassword, err = ldappassword.GenerateRandomPassword(server.GeneratedPasswordLength); err != nil {
logger.Error(err, "Failed to generate new password")
return nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("Failed to generate new Password"))
}
passwordGenerated = true
}
pwReq.NewPassword, err = ldappassword.Hash(pwReq.NewPassword, "{ARGON2}")
if err != nil {
return nil, ldap.NewError(ldap.LDAPResultOperationsError, err)
}
logger.V(1).Info("Modify password extended operation", "dn", pwReq.UserIdentity)
fnNames := []string{}
for k := range server.PasswordExOpFns {
fnNames = append(fnNames, k)
}
fn := routeFunc(pwReq.UserIdentity, fnNames)
var pwUpdatefn PasswordUpdater
if pwUpdatefn = server.PasswordExOpFns[fn]; pwUpdatefn == nil {
if fn == "" {
err = fmt.Errorf("no suitable handler found for dn: '%s'", pwReq.UserIdentity)
} else {
err = fmt.Errorf("handler '%s' does not support add", fn)
}
return nil, ldap.NewError(ldap.LDAPResultUnwillingToPerform, err)
}
code, err := pwUpdatefn.ModifyPasswordExop(boundDN, pwReq, conn)
if code != ldap.LDAPResultSuccess {
return nil, ldap.NewError(uint16(code), err)
}
var response *ber.Packet
if passwordGenerated {
response = ber.NewSequence("PasswdModifyResponseValue")
response.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, pwReq.NewPassword, "genPasswd"))
}
return response, nil
}
func parsePasswordModifyExop(req *ber.Packet) (*ldap.PasswordModifyRequest, error) {
pwReq := ldap.PasswordModifyRequest{}
// An absent (or empty) body of the request is valid. Translates into: "generate a new password for
// for the current user"
if req == nil {
return &pwReq, nil
}
inner := ber.DecodePacket(req.Data.Bytes())
if inner == nil {
return &pwReq, nil
}
if len(inner.Children) > 3 {
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid request"))
}
for _, kid := range inner.Children {
if kid.ClassType != ber.ClassContext {
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid request"))
}
switch kid.Tag {
default:
return nil, ldap.NewError(ldap.LDAPResultDecodingError, errors.New("invalid request"))
case TagReqIdentity:
pwReq.UserIdentity = kid.Data.String()
case TagReqOldPW:
pwReq.OldPassword = kid.Data.String()
case TagReqNewPW:
pwReq.NewPassword = kid.Data.String()
}
}
return &pwReq, nil
}
+225
View File
@@ -0,0 +1,225 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Copyright 2021 The LibreGraph Authors.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ldapserver
import (
"errors"
"net"
"strings"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/go-ldap/ldap/v3"
)
func HandleSearchRequest(req *ber.Packet, controls *[]ldap.Control, messageID int64, boundDN string, server *Server, conn net.Conn) (doneControls *[]ldap.Control, resultErr error) {
searchReq, err := parseSearchRequest(boundDN, req, controls)
if err != nil {
return nil, ldap.NewError(ldap.LDAPResultOperationsError, err)
}
var filterPacket *ber.Packet
if server.EnforceLDAP {
filterPacket, err = ldap.CompileFilter(searchReq.Filter)
if err != nil {
return nil, ldap.NewError(ldap.LDAPResultOperationsError, err)
}
}
fnNames := []string{}
for k := range server.SearchFns {
fnNames = append(fnNames, k)
}
fn := routeFunc(searchReq.BaseDN, fnNames)
searchResp, err := server.SearchFns[fn].Search(boundDN, searchReq, conn)
if err != nil {
return &searchResp.Controls, ldap.NewError(uint16(searchResp.ResultCode), err)
}
if server.EnforceLDAP {
if searchReq.DerefAliases != ldap.NeverDerefAliases { // [-a {never|always|search|find}
// TODO: Server DerefAliases not supported: RFC4511 4.5.1.3
}
if searchReq.TimeLimit > 0 {
// TODO: Server TimeLimit not implemented
}
}
i := 0
for _, entry := range searchResp.Entries {
if server.EnforceLDAP {
// filter
keep, resultCode := ServerApplyFilter(filterPacket, entry)
if resultCode != ldap.LDAPResultSuccess {
return &searchResp.Controls, ldap.NewError(uint16(resultCode), errors.New("ServerApplyFilter error"))
}
if !keep {
continue
}
keep, resultCode = ServerFilterScope(searchReq.BaseDN, searchReq.Scope, entry)
if resultCode != ldap.LDAPResultSuccess {
return &searchResp.Controls, ldap.NewError(uint16(resultCode), errors.New("ServerApplyScope error"))
}
if !keep {
continue
}
resultCode, err = ServerFilterAttributes(searchReq.Attributes, entry)
if err != nil {
return &searchResp.Controls, ldap.NewError(uint16(resultCode), err)
}
// size limit
if searchReq.SizeLimit > 0 && i >= searchReq.SizeLimit {
resultErr = ldap.NewError(
ldap.LDAPResultSizeLimitExceeded,
errors.New(ldap.LDAPResultCodeMap[ldap.LDAPResultSizeLimitExceeded]),
)
break
}
i++
}
// respond
responsePacket := encodeSearchResponse(messageID, searchReq, entry)
if err = sendPacket(conn, responsePacket); err != nil {
return &searchResp.Controls, ldap.NewError(ldap.LDAPResultOperationsError, err)
}
}
return &searchResp.Controls, resultErr
}
func parseSearchRequest(boundDN string, req *ber.Packet, controls *[]ldap.Control) (*ldap.SearchRequest, error) {
if len(req.Children) != 8 {
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("Bad search request"))
}
// Parse the request.
baseObject, ok := req.Children[0].Value.(string)
if !ok {
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
}
s, ok := req.Children[1].Value.(int64)
if !ok {
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
}
scope := int(s)
d, ok := req.Children[2].Value.(int64)
if !ok {
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
}
derefAliases := int(d)
s, ok = req.Children[3].Value.(int64)
if !ok {
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
}
sizeLimit := int(s)
t, ok := req.Children[4].Value.(int64)
if !ok {
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
}
timeLimit := int(t)
typesOnly := false
if req.Children[5].Value != nil {
typesOnly, ok = req.Children[5].Value.(bool)
if !ok {
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
}
}
filter, err := DecompileFilter(req.Children[6])
if err != nil {
return &ldap.SearchRequest{}, err
}
attributes := []string{}
for _, attr := range req.Children[7].Children {
a, ok := attr.Value.(string)
if !ok {
return &ldap.SearchRequest{}, ldap.NewError(ldap.LDAPResultProtocolError, errors.New("Bad search request"))
}
attributes = append(attributes, a)
}
searchReq := &ldap.SearchRequest{
BaseDN: baseObject,
Scope: scope,
DerefAliases: derefAliases,
SizeLimit: sizeLimit,
TimeLimit: timeLimit,
TypesOnly: typesOnly,
Filter: filter,
Attributes: attributes,
Controls: *controls,
}
return searchReq, nil
}
func filterAttributes(entry *ldap.Entry, attributes []string) (*ldap.Entry, error) {
// Only return requested attributes.
newAttributes := []*ldap.EntryAttribute{}
for _, attr := range entry.Attributes {
for _, requested := range attributes {
if requested == "*" || strings.EqualFold(attr.Name, requested) {
newAttributes = append(newAttributes, attr)
}
}
}
entry.Attributes = newAttributes
return entry, nil
}
func encodeSearchResponse(messageID int64, req *ldap.SearchRequest, res *ldap.Entry) *ber.Packet {
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
searchEntry := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ldap.ApplicationSearchResultEntry, nil, "Search Result Entry")
searchEntry.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, res.DN, "Object Name"))
attrs := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes: ")
for _, attribute := range res.Attributes {
attrs.AppendChild(encodeSearchAttribute(attribute.Name, attribute.Values))
}
searchEntry.AppendChild(attrs)
responsePacket.AppendChild(searchEntry)
return responsePacket
}
func encodeSearchAttribute(name string, values []string) *ber.Packet {
packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute")
packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, name, "Attribute Name"))
valuesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "Attribute Values: ")
for _, value := range values {
valuesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Attribute Value"))
}
packet.AppendChild(valuesPacket)
return packet
}
func encodeSearchDone(messageID int64, ldapResultCode LDAPResultCode, doneControls *[]ldap.Control) *ber.Packet {
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
donePacket := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ldap.ApplicationSearchResultDone, nil, "Search result done")
donePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: "))
donePacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
donePacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "errorMessage: "))
responsePacket.AppendChild(donePacket)
if doneControls != nil {
contextPacket := ber.Encode(ber.ClassContext, ber.TypeConstructed, ber.TagEOC, nil, "Controls: ")
for _, control := range *doneControls {
contextPacket.AppendChild(control.Encode())
}
responsePacket.AppendChild(contextPacket)
}
// ber.PrintPacket(responsePacket)
return responsePacket
}
+491
View File
@@ -0,0 +1,491 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Copyright 2021 The LibreGraph Authors.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ldapserver
import (
"crypto/tls"
"errors"
"io"
"log"
"net"
"strings"
ber "github.com/go-asn1-ber/asn1-ber"
"github.com/go-ldap/ldap/v3"
"github.com/go-logr/logr"
"github.com/go-logr/stdr"
"github.com/libregraph/idm/pkg/ldapdn"
)
type Adder interface {
Add(boundDN string, req *ldap.AddRequest, conn net.Conn) (LDAPResultCode, error)
}
type Binder interface {
Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error)
}
type Deleter interface {
Delete(boundDN string, req *ldap.DelRequest, conn net.Conn) (LDAPResultCode, error)
}
type Modifier interface {
Modify(boundDN string, req *ldap.ModifyRequest, conn net.Conn) (LDAPResultCode, error)
}
type PasswordUpdater interface {
ModifyPasswordExop(boundDN string, req *ldap.PasswordModifyRequest, conn net.Conn) (LDAPResultCode, error)
}
type Renamer interface {
ModifyDN(boundDN string, req *ldap.ModifyDNRequest, conn net.Conn) (LDAPResultCode, error)
}
type Searcher interface {
Search(boundDN string, req *ldap.SearchRequest, conn net.Conn) (ServerSearchResult, error)
}
type Closer interface {
Close(boundDN string, conn net.Conn) error
}
var logger logr.Logger = stdr.New(log.Default())
type Server struct {
AddFns map[string]Adder
BindFns map[string]Binder
DeleteFns map[string]Deleter
ModifyFns map[string]Modifier
ModifyDNFns map[string]Renamer
PasswordExOpFns map[string]PasswordUpdater
SearchFns map[string]Searcher
CloseFns map[string]Closer
Quit chan bool
EnforceLDAP bool
GeneratedPasswordLength int
Stats *Stats
}
type ServerSearchResult struct {
Entries []*ldap.Entry
Referrals []string
Controls []ldap.Control
ResultCode LDAPResultCode
}
func NewServer() *Server {
s := new(Server)
s.Quit = make(chan bool)
d := defaultHandler{}
s.AddFns = make(map[string]Adder)
s.BindFns = make(map[string]Binder)
s.DeleteFns = make(map[string]Deleter)
s.ModifyFns = make(map[string]Modifier)
s.ModifyDNFns = make(map[string]Renamer)
s.PasswordExOpFns = make(map[string]PasswordUpdater)
s.SearchFns = make(map[string]Searcher)
s.CloseFns = make(map[string]Closer)
s.BindFunc("", d)
s.SearchFunc("", d)
s.CloseFunc("", d)
s.GeneratedPasswordLength = 16
s.Stats = nil
return s
}
func Logger(l logr.Logger) {
logger = l
}
func (server *Server) AddFunc(baseDN string, f Adder) {
server.AddFns[baseDN] = f
}
func (server *Server) BindFunc(baseDN string, f Binder) {
server.BindFns[baseDN] = f
}
func (server *Server) DeleteFunc(baseDN string, f Deleter) {
server.DeleteFns[baseDN] = f
}
func (server *Server) ModifyFunc(baseDN string, f Modifier) {
server.ModifyFns[baseDN] = f
}
func (server *Server) ModifyDNFunc(baseDN string, f Renamer) {
server.ModifyDNFns[baseDN] = f
}
func (server *Server) PasswordExOpFunc(baseDN string, f PasswordUpdater) {
server.PasswordExOpFns[baseDN] = f
}
func (server *Server) SearchFunc(baseDN string, f Searcher) {
server.SearchFns[baseDN] = f
}
func (server *Server) CloseFunc(baseDN string, f Closer) {
server.CloseFns[baseDN] = f
}
func (server *Server) QuitChannel(quit chan bool) {
server.Quit = quit
}
func (server *Server) ListenAndServeTLS(listenString string, certFile string, keyFile string) error {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return err
}
tlsConfig := tls.Config{Certificates: []tls.Certificate{cert}}
tlsConfig.ServerName = "localhost"
ln, err := tls.Listen("tcp", listenString, &tlsConfig)
if err != nil {
return err
}
err = server.Serve(ln)
if err != nil {
return err
}
return nil
}
func (server *Server) SetStats(enable bool) {
if enable {
server.Stats = &Stats{}
} else {
server.Stats = nil
}
}
func (server *Server) GetStats() Stats {
return *server.Stats.Clone()
}
func (server *Server) ListenAndServe(listenString string) error {
ln, err := net.Listen("tcp", listenString)
if err != nil {
return err
}
err = server.Serve(ln)
if err != nil {
return err
}
return nil
}
func (server *Server) Serve(ln net.Listener) error {
newConn := make(chan net.Conn)
go func() {
for {
conn, err := ln.Accept()
if err != nil {
if !strings.HasSuffix(err.Error(), "use of closed network connection") {
logger.Error(err, "Error accepting network connection")
}
break
}
logger.V(1).Info("New Connection", "addr", ln.Addr())
newConn <- conn
}
}()
listener:
for {
select {
case c := <-newConn:
server.Stats.countConns(1)
go server.handleConnection(c)
case <-server.Quit:
ln.Close()
break listener
}
}
return nil
}
func (server *Server) handleConnection(conn net.Conn) {
boundDN := "" // "" == anonymous
handler:
for {
// Read incoming LDAP packet.
packet, err := ber.ReadPacket(conn)
if err == io.EOF || err == io.ErrUnexpectedEOF { // Client closed connection.
break
} else if err != nil {
logger.Error(err, "handleConnection ber.ReadPacket")
break
}
// Sanity check this packet.
if len(packet.Children) < 2 {
logger.V(1).Info("len(packet.Children) < 2")
break
}
// Check the message ID and ClassType.
messageID, ok := packet.Children[0].Value.(int64)
if !ok {
logger.V(1).Info("malformed messageID")
break
}
req := packet.Children[1]
if req.ClassType != ber.ClassApplication {
logger.V(1).Info("req.ClassType != ber.ClassApplication")
break
}
// Handle controls if present.
controls := []ldap.Control{}
if len(packet.Children) > 2 {
for _, child := range packet.Children[2].Children {
c, err := ldap.DecodeControl(child)
if err != nil {
logger.Error(err, "handleConnection decode control")
continue
}
controls = append(controls, c)
}
}
// log.Printf("DEBUG: handling operation: %s [%d]", ldap.ApplicationMap[uint8(req.Tag)], req.Tag)
// ber.PrintPacket(packet) // DEBUG
// Dispatch the LDAP operation.
switch req.Tag { // LDAP op code.
default:
op, ok := ldap.ApplicationMap[uint8(req.Tag)]
if !ok {
op = "unknown"
}
logger.V(1).Info("Unhandled operation", "type", op, "tag", req.Tag)
break handler
case ldap.ApplicationAddRequest:
server.Stats.countAdds(1)
resultCode := uint16(ldap.LDAPResultSuccess)
resultMsg := ""
if err = HandleAddRequest(req, boundDN, server, conn); err != nil {
var lErr *ldap.Error
if errors.As(err, &lErr) {
resultCode = lErr.ResultCode
if lErr.Err != nil {
resultMsg = lErr.Err.Error()
}
} else {
resultCode = ldap.LDAPResultOperationsError
resultMsg = err.Error()
}
}
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationAddResponse, LDAPResultCode(resultCode), resultMsg)
if err = sendPacket(conn, responsePacket); err != nil {
logger.Error(err, "sendPacket error")
break handler
}
case ldap.ApplicationBindRequest:
server.Stats.countBinds(1)
ldapResultCode := HandleBindRequest(req, server.BindFns, conn)
if ldapResultCode == ldap.LDAPResultSuccess {
boundDN, ok = req.Children[1].Value.(string)
if !ok {
logger.V(1).Info("Malformed Bind DN")
break handler
}
if boundDN, err = ldapdn.ParseNormalize(boundDN); err != nil {
logger.V(1).Info("Error normalizing Bind DN", "error", err.Error())
break handler
}
}
responsePacket := encodeBindResponse(messageID, ldapResultCode)
if err = sendPacket(conn, responsePacket); err != nil {
logger.Error(err, "sendPacket error")
break handler
}
case ldap.ApplicationCompareRequest:
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationCompareRequest, ldap.LDAPResultOperationsError, "Unsupported operation: compare")
if err = sendPacket(conn, responsePacket); err != nil {
logger.Error(err, "sendPacket error")
}
logger.V(1).Info("Unhandled operation", "type", ldap.ApplicationMap[uint8(req.Tag)], "tag", req.Tag)
break handler
case ldap.ApplicationDelRequest:
server.Stats.countDeletes(1)
resultCode := uint16(ldap.LDAPResultSuccess)
resultMsg := ""
if err = HandleDeleteRequest(req, boundDN, server, conn); err != nil {
var lErr *ldap.Error
if errors.As(err, &lErr) {
resultCode = lErr.ResultCode
if lErr.Err != nil {
resultMsg = lErr.Err.Error()
}
} else {
resultCode = ldap.LDAPResultOperationsError
resultMsg = err.Error()
}
}
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationDelResponse, LDAPResultCode(resultCode), resultMsg)
if err = sendPacket(conn, responsePacket); err != nil {
logger.Error(err, "sendPacket error")
break handler
}
case ldap.ApplicationExtendedRequest:
resultCode := uint16(ldap.LDAPResultSuccess)
resultMsg := ""
var innerBer, responsePacket *ber.Packet
if innerBer, err = HandleExtendedRequest(req, boundDN, server, conn); err != nil {
resultCode = ldap.LDAPResultOperationsError
resultMsg = err.Error()
responsePacket = encodeLDAPResponse(messageID, ldap.ApplicationExtendedResponse, LDAPResultCode(resultCode), resultMsg)
} else {
responsePacket = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
responsePacket.AppendChild(innerBer)
}
if err = sendPacket(conn, responsePacket); err != nil {
logger.Error(err, "sendPacket error")
break handler
}
case ldap.ApplicationModifyDNRequest:
server.Stats.countModifyDNs(1)
resultCode := uint16(ldap.LDAPResultSuccess)
resultMsg := ""
if err = HandleModifyDNRequest(req, boundDN, server, conn); err != nil {
var lErr *ldap.Error
if errors.As(err, &lErr) {
resultCode = lErr.ResultCode
if lErr.Err != nil {
resultMsg = lErr.Err.Error()
}
} else {
resultCode = ldap.LDAPResultOperationsError
resultMsg = err.Error()
}
}
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationModifyDNResponse, LDAPResultCode(resultCode), resultMsg)
if err = sendPacket(conn, responsePacket); err != nil {
logger.Error(err, "sendPacket error")
break handler
}
case ldap.ApplicationModifyRequest:
server.Stats.countModifies(1)
resultCode := uint16(ldap.LDAPResultSuccess)
resultMsg := ""
if err = HandleModifyRequest(req, boundDN, server, conn); err != nil {
var lErr *ldap.Error
if errors.As(err, &lErr) {
resultCode = lErr.ResultCode
if lErr.Err != nil {
resultMsg = lErr.Err.Error()
}
} else {
resultCode = ldap.LDAPResultOperationsError
resultMsg = err.Error()
}
}
responsePacket := encodeLDAPResponse(messageID, ldap.ApplicationModifyResponse, LDAPResultCode(resultCode), resultMsg)
if err = sendPacket(conn, responsePacket); err != nil {
logger.Error(err, "sendPacket error")
break handler
}
case ldap.ApplicationSearchRequest:
server.Stats.countSearches(1)
if doneControls, err := HandleSearchRequest(req, &controls, messageID, boundDN, server, conn); err != nil {
// TODO: make this more testable/better err handling - stop using log, stop using breaks?
logger.V(1).Info("handleSearchRequest", "error", err.Error())
e := err.(*ldap.Error)
if err = sendPacket(conn, encodeSearchDone(messageID, LDAPResultCode(e.ResultCode), doneControls)); err != nil {
logger.Error(err, "sendPacket error")
break handler
}
break handler
} else {
if err = sendPacket(conn, encodeSearchDone(messageID, ldap.LDAPResultSuccess, doneControls)); err != nil {
logger.Error(err, "sendPacket error")
break handler
}
}
case ldap.ApplicationUnbindRequest:
server.Stats.countUnbinds(1)
break handler // Simply disconnect.
}
}
for _, c := range server.CloseFns {
c.Close(boundDN, conn)
}
conn.Close()
server.Stats.countConnsClose(1)
}
func sendPacket(conn net.Conn, packet *ber.Packet) error {
_, err := conn.Write(packet.Bytes())
if err != nil {
logger.Error(err, "Error Sending Message")
return err
}
return nil
}
func routeFunc(dn string, funcNames []string) string {
bestPick := ""
for _, fn := range funcNames {
if strings.HasSuffix(dn, fn) {
l := len(strings.Split(bestPick, ","))
if bestPick == "" {
l = 0
}
if len(strings.Split(fn, ",")) > l {
bestPick = fn
}
}
}
return bestPick
}
func encodeLDAPResponse(messageID int64, responseType uint8, ldapResultCode LDAPResultCode, message string) *ber.Packet {
responsePacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Response")
responsePacket.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "Message ID"))
response := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ber.Tag(responseType), nil, ldap.ApplicationMap[responseType])
response.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ldapResultCode), "resultCode: "))
response.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, "", "matchedDN: "))
response.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, message, "errorMessage: "))
responsePacket.AppendChild(response)
return responsePacket
}
type defaultHandler struct {
}
func (h defaultHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (LDAPResultCode, error) {
return ldap.LDAPResultInvalidCredentials, nil
}
func (h defaultHandler) Search(boundDN string, req *ldap.SearchRequest, conn net.Conn) (ServerSearchResult, error) {
return ServerSearchResult{make([]*ldap.Entry, 0), []string{}, []ldap.Control{}, ldap.LDAPResultSuccess}, nil
}
func (h defaultHandler) Close(boundDN string, conn net.Conn) error {
conn.Close()
return nil
}
+119
View File
@@ -0,0 +1,119 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Copyright 2021 The LibreGraph Authors.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ldapserver
import (
"sync"
)
type Stats struct {
Conns uint64
ConnsCurrent uint64
ConnsMax uint64
Adds uint64
Binds uint64
Deletes uint64
ModifyDNs uint64
Modifies uint64
Unbinds uint64
Searches uint64
statsMutex sync.RWMutex
}
func (stats *Stats) countConns(delta uint64) {
if stats != nil {
stats.statsMutex.Lock()
stats.Conns += delta
stats.ConnsCurrent += delta
if stats.ConnsCurrent > stats.ConnsMax {
stats.ConnsMax = stats.ConnsCurrent
}
stats.statsMutex.Unlock()
}
}
func (stats *Stats) countConnsClose(delta uint64) {
if stats != nil {
stats.statsMutex.Lock()
stats.ConnsCurrent -= delta
stats.statsMutex.Unlock()
}
}
func (stats *Stats) countAdds(delta uint64) {
if stats != nil {
stats.statsMutex.Lock()
stats.Adds += delta
stats.statsMutex.Lock()
}
}
func (stats *Stats) countBinds(delta uint64) {
if stats != nil {
stats.statsMutex.Lock()
stats.Binds += delta
stats.statsMutex.Unlock()
}
}
func (stats *Stats) countDeletes(delta uint64) {
if stats != nil {
stats.statsMutex.Lock()
stats.Deletes += delta
stats.statsMutex.Unlock()
}
}
func (stats *Stats) countModifyDNs(delta uint64) {
if stats != nil {
stats.statsMutex.Lock()
stats.ModifyDNs += delta
stats.statsMutex.Unlock()
}
}
func (stats *Stats) countModifies(delta uint64) {
if stats != nil {
stats.statsMutex.Lock()
stats.Modifies += delta
stats.statsMutex.Unlock()
}
}
func (stats *Stats) countUnbinds(delta uint64) {
if stats != nil {
stats.statsMutex.Lock()
stats.Unbinds += delta
stats.statsMutex.Unlock()
}
}
func (stats *Stats) countSearches(delta uint64) {
if stats != nil {
stats.statsMutex.Lock()
stats.Searches += delta
stats.statsMutex.Unlock()
}
}
func (stats *Stats) Clone() *Stats {
var s2 *Stats
if stats != nil {
s2 = &Stats{}
stats.statsMutex.RLock()
s2.Conns = stats.Conns
s2.ConnsCurrent = stats.ConnsCurrent
s2.Adds = stats.Adds
s2.Binds = stats.Binds
s2.Deletes = stats.Deletes
s2.ModifyDNs = stats.ModifyDNs
s2.Modifies = stats.Modifies
s2.Unbinds = stats.Unbinds
s2.Searches = stats.Searches
stats.statsMutex.RUnlock()
}
return s2
}
+485
View File
@@ -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()
}