func ValidateLDAPQuery(query api.LDAPQuery) ValidationResults { validationResults := ValidationResults{} if _, err := ldap.ParseDN(query.BaseDN); err != nil { validationResults.AddErrors(fielderrors.NewFieldInvalid("baseDN", query.BaseDN, fmt.Sprintf("invalid base DN for search: %v", err))) } if len(query.Scope) > 0 { if _, err := ldaputil.DetermineLDAPScope(query.Scope); err != nil { validationResults.AddErrors(fielderrors.NewFieldInvalid("scope", query.Scope, "invalid LDAP search scope")) } } if len(query.DerefAliases) > 0 { if _, err := ldaputil.DetermineDerefAliasesBehavior(query.DerefAliases); err != nil { validationResults.AddErrors(fielderrors.NewFieldInvalid("derefAliases", query.DerefAliases, "LDAP alias dereferencing instruction invalid")) } } if query.TimeLimit < 0 { validationResults.AddErrors(fielderrors.NewFieldInvalid("timeout", query.TimeLimit, "timeout must be equal to or greater than zero")) } if _, err := ldap.CompileFilter(query.Filter); err != nil { validationResults.AddErrors(fielderrors.NewFieldInvalid("filter", query.Filter, fmt.Sprintf("invalid query filter: %v", err))) } return validationResults }
// DetermineLDAPFilter determines the LDAP search filter. Filter is a valid LDAP filter // Default to "(objectClass=*)" per RFC func DetermineLDAPFilter(filter string) (string, error) { if len(filter) == 0 { return defaultFilter, nil } if _, err := ldap.CompileFilter(filter); err != nil { return "", fmt.Errorf("invalid filter: %v", err) } return filter, nil }
// ParseURL parsed the given ldapURL as an RFC 2255 URL // The syntax of the URL is ldap://host:port/basedn?attribute?scope?filter func ParseURL(ldapURL string) (LDAPURL, error) { // Must be a valid URL to start parsedURL, err := url.Parse(ldapURL) if err != nil { return LDAPURL{}, err } opts := LDAPURL{} // Set scheme (default to ldap) opts.Scheme = Scheme(parsedURL.Scheme) switch opts.Scheme { case SchemeLDAP, SchemeLDAPS: // ok default: return LDAPURL{}, fmt.Errorf("invalid scheme %q", parsedURL.Scheme) } // Set host (default to localhost to match mod_auth_ldap) opts.Host = parsedURL.Host if len(opts.Host) == 0 { opts.Host = defaultHost } // Add port if needed if _, _, err := net.SplitHostPort(opts.Host); err != nil { switch opts.Scheme { case SchemeLDAPS: opts.Host = net.JoinHostPort(opts.Host, strconv.Itoa(defaultLDAPSPort)) case SchemeLDAP: opts.Host = net.JoinHostPort(opts.Host, strconv.Itoa(defaultLDAPPort)) default: return LDAPURL{}, fmt.Errorf("no default port for scheme %q", opts.Scheme) } } // Set base dn (default to "") // url.Parse() already percent-decodes the path opts.BaseDN = strings.TrimLeft(parsedURL.Path, "/") // Split query // All sections are optional // attribute?scope?filter?extensions var attributes, scope, filter, extensions string parts := strings.Split(parsedURL.RawQuery, "?") switch len(parts) { case 4: extensions = parts[3] fallthrough case 3: if v, err := url.QueryUnescape(parts[2]); err != nil { return LDAPURL{}, err } else { filter = v } fallthrough case 2: if v, err := url.QueryUnescape(parts[1]); err != nil { return LDAPURL{}, err } else { scope = v } fallthrough case 1: if v, err := url.QueryUnescape(parts[0]); err != nil { return LDAPURL{}, err } else { attributes = v } case 0: // no-op default: return LDAPURL{}, fmt.Errorf("too many query options %q", parsedURL.RawQuery) } // Attributes contains comma-separated attributes // Set query attribute to first attribute // Default to uid to match mod_auth_ldap opts.QueryAttribute = strings.Split(attributes, ",")[0] if len(opts.QueryAttribute) == 0 { opts.QueryAttribute = defaultQueryAttribute } // Scope is one of "sub", "one", or "base" // Default to "sub" to match mod_auth_ldap switch scope { case "", scopeWholeSubtreeString: opts.Scope = ScopeWholeSubtree case scopeSingleLevelString: opts.Scope = ScopeSingleLevel case scopeBaseObjectString: opts.Scope = ScopeBaseObject default: return LDAPURL{}, fmt.Errorf("invalid scope %q", scope) } // Filter is a valid LDAP filter // Default to "(objectClass=*)" per RFC opts.Filter = filter if len(opts.Filter) == 0 { opts.Filter = defaultFilter } if _, err := ldap.CompileFilter(opts.Filter); err != nil { return LDAPURL{}, fmt.Errorf("invalid filter: %v", err) } // Extensions are in "name=value,name2=value2" form // Critical extensions are prefixed with a ! // Optional extensions are ignored, per RFC // Fail if there are any critical extensions, since we don't support any if len(extensions) > 0 { for _, extension := range strings.Split(extensions, ",") { exttype := strings.SplitN(extension, "=", 2)[0] if strings.HasPrefix(exttype, criticalExtensionPrefix) { return LDAPURL{}, fmt.Errorf("unsupported critical extension %s", extension) } } } return opts, nil }