package ldap import ( "errors" "fmt" "regexp" "strings" "github.com/vjeantet/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 }