ldap-upstream/internal/ldap/bleve_mapper.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
}