Initial QSfera import
This commit is contained in:
+94
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
+16
@@ -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")
|
||||
}
|
||||
+133
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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