mirror of
https://github.com/netauth/ldap.git
synced 2026-11-03 11:43:44 -06:00
177 lines
5.7 KiB
Go
177 lines
5.7 KiB
Go
package ldap
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/ps78674/goldap/message"
|
|
)
|
|
|
|
// bleveWildcardValueRe matches the subset of characters that may appear
|
|
// in an unquoted Bleve wildcard term. NetAuth entity and group names
|
|
// consist of word characters, so anything outside this set can only
|
|
// come from a client trying to smuggle query syntax in.
|
|
var bleveWildcardValueRe = regexp.MustCompile(`^[\w.\-*]+$`)
|
|
|
|
// buildBleveQuery translates a decoded LDAP search filter into a Bleve
|
|
// query string for NetAuth. Only attribute equality, presence and the
|
|
// simple substring forms are understood; everything else is refused so
|
|
// that a client gets an error rather than wrong results.
|
|
func (s *server) buildBleveQuery(f message.Filter) (string, error) {
|
|
return s.bleveQuery(f, false)
|
|
}
|
|
|
|
// bleveQuery builds the query for a filter. The nested flag records
|
|
// whether this filter sits inside a boolean group, which determines
|
|
// whether a negation is meaningful.
|
|
func (s *server) bleveQuery(f message.Filter, nested bool) (string, error) {
|
|
s.l.Trace("Building search expression", "type", fmt.Sprintf("%T", f), "filter", fmt.Sprintf("%#v", f))
|
|
|
|
switch f := f.(type) {
|
|
case message.FilterEqualityMatch:
|
|
return mapToBleveStringQuery(string(f.AttributeDesc()), string(f.AssertionValue()))
|
|
case message.FilterPresent:
|
|
return mapToBleveStringQuery(string(f), "*")
|
|
case message.FilterSubstrings:
|
|
return s.bleveSubstringQuery(f)
|
|
case message.FilterNot:
|
|
// Bleve prohibits a clause with "-", but an expression made
|
|
// up entirely of prohibitions matches nothing at all. A
|
|
// top-level negation therefore has to be refused rather than
|
|
// quietly returning an empty result set.
|
|
if !nested {
|
|
s.l.Warn("Ignoring a top-level negated filter")
|
|
return "", errors.New("cannot negate the entire search")
|
|
}
|
|
e, err := s.bleveQuery(f.Filter, true)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if strings.HasPrefix(e, "-") {
|
|
// Double negation would leave Bleve with only
|
|
// prohibitions again.
|
|
return "", errors.New("cannot negate a negation")
|
|
}
|
|
if strings.HasPrefix(e, "(") {
|
|
return "-" + e, nil
|
|
}
|
|
return "-(" + e + ")", nil
|
|
case message.FilterAnd:
|
|
return s.bleveBooleanQuery(f, true)
|
|
case message.FilterOr:
|
|
return s.bleveBooleanQuery(f, false)
|
|
default:
|
|
s.l.Warn("Unsupported search filter", "filter", fmt.Sprintf("%#v", f))
|
|
return "", errors.New("unsupported search filter")
|
|
}
|
|
}
|
|
|
|
// bleveBooleanQuery maps an AND or OR filter onto a Bleve boolean
|
|
// expression. AND clauses are prefixed with + to make them required,
|
|
// while space separated clauses are already optional (OR) in Bleve.
|
|
// A group with more than one branch is parenthesized so it can be
|
|
// nested without changing its meaning.
|
|
func (s *server) bleveBooleanQuery(filters []message.Filter, and bool) (string, error) {
|
|
var expr []string
|
|
for _, subf := range filters {
|
|
e, err := s.bleveQuery(subf, true)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if and && !strings.HasPrefix(e, "-") {
|
|
// A prohibited clause must not also be marked required.
|
|
e = "+" + e
|
|
}
|
|
expr = append(expr, e)
|
|
}
|
|
if len(expr) == 0 {
|
|
return "", errors.New("empty boolean filter")
|
|
}
|
|
if !hasPositiveClause(expr) {
|
|
// A group made up only of prohibitions leaves Bleve with no
|
|
// clause to match on, which would silently return nothing.
|
|
return "", errors.New("filter contains no positive clause")
|
|
}
|
|
if len(expr) == 1 {
|
|
return expr[0], nil
|
|
}
|
|
return "(" + strings.Join(expr, " ") + ")", nil
|
|
}
|
|
|
|
// hasPositiveClause reports whether any of the compiled subexpressions
|
|
// can match on its own.
|
|
func hasPositiveClause(expr []string) bool {
|
|
for _, e := range expr {
|
|
if !strings.HasPrefix(e, "-") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// bleveSubstringQuery translates the substring forms clients actually
|
|
// send, such as "(uid=js*)" or "(uid=*smith)", into Bleve wildcard
|
|
// terms. Mixed inner wildcards like "(uid=a*b*)" are refused since a
|
|
// single Bleve wildcard cannot express them faithfully.
|
|
func (s *server) bleveSubstringQuery(f message.FilterSubstrings) (string, error) {
|
|
var initial, final string
|
|
var any []string
|
|
for _, sub := range f.Substrings() {
|
|
switch v := sub.(type) {
|
|
case message.SubstringInitial:
|
|
initial = string(v)
|
|
case message.SubstringFinal:
|
|
final = string(v)
|
|
case message.SubstringAny:
|
|
any = append(any, string(v))
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case len(any) == 0:
|
|
return mapToBleveStringQuery(string(f.Type_()), initial+"*"+final)
|
|
case len(any) == 1 && initial == "" && final == "":
|
|
// A single inner substring like "(uid=*smith)" is matched
|
|
// with wildcards on both ends.
|
|
return mapToBleveStringQuery(string(f.Type_()), "*"+any[0]+"*")
|
|
default:
|
|
s.l.Warn("Unsupported substring filter", "initial", initial, "final", final, "any", any)
|
|
return "", errors.New("unsupported substring filter")
|
|
}
|
|
}
|
|
|
|
// mapToBleveStringQuery builds a single Bleve term for the supported
|
|
// attributes. A bare "*" keeps its wildcard meaning for
|
|
// attribute-present filters; plain values are quoted so they are
|
|
// matched as one term and cannot introduce extra query clauses. Values
|
|
// containing a wildcard must stay unquoted for the wildcard to work,
|
|
// so those are restricted to the character set NetAuth names may
|
|
// contain.
|
|
func mapToBleveStringQuery(attr, val string) (string, error) {
|
|
var predicate string
|
|
|
|
switch attr {
|
|
case "uid":
|
|
predicate = "ID"
|
|
case "cn":
|
|
predicate = "Name"
|
|
default:
|
|
return "", errors.New("search attribute is unsupported")
|
|
}
|
|
|
|
if val == "*" {
|
|
return predicate + ":*", nil
|
|
}
|
|
if strings.Contains(val, "*") {
|
|
if !bleveWildcardValueRe.MatchString(val) {
|
|
return "", errors.New("search value contains unsupported characters")
|
|
}
|
|
return predicate + ":" + val, nil
|
|
}
|
|
|
|
escaped := strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(val)
|
|
return predicate + `:"` + escaped + `"`, nil
|
|
}
|