]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
LDAP library changes in preparation for LDAP sync (#4549)
authorNick Porter <nick@portercomputing.co.uk>
Mon, 6 Jun 2022 18:51:34 +0000 (19:51 +0100)
committerGitHub <noreply@github.com>
Mon, 6 Jun 2022 18:51:34 +0000 (14:51 -0400)
* Typo

* Retrieve namingContexts from LDAP directories when establishing their type

* Correct file name

* Define fr_ldap_attrs_check() - check if an LDAP attribute is in a list

* Define structures and enums for parsed LDAP filters

* Move common LDAP config items to a library header

* Move validation of LDAP server conf items to library

* Add OIDs for bit-wise LDAP match rules

* Define functions for parsing LDAP filters

* Add filter.c to ldap library sources

* Define functions for evaluating LDAP filters

src/lib/ldap/all.mk.in
src/lib/ldap/base.h
src/lib/ldap/conf.c [new file with mode: 0644]
src/lib/ldap/conf.h [new file with mode: 0644]
src/lib/ldap/directory.c
src/lib/ldap/filter.c [new file with mode: 0644]
src/lib/ldap/util.c
src/modules/rlm_ldap/rlm_ldap.c

index 22d057746c2e2c7aec2d6b2fa91f5cbcebdcc9ac..e7598721216a888d2905ea33b852e3b8f66bd226 100644 (file)
@@ -4,7 +4,7 @@ ifneq "$(TARGETNAME)" ""
 TARGET         := $(TARGETNAME)$(L)
 endif
 
-SOURCES                := base.c bind.c connection.c control.c directory.c edir.c map.c referral.c start_tls.c state.c util.c @SASL@
+SOURCES                := base.c bind.c conf.c connection.c control.c directory.c edir.c filter.c map.c referral.c start_tls.c state.c util.c @SASL@
 
 SRC_CFLAGS     := @mod_cflags@
 TGT_LDLIBS     := @mod_ldflags@
index 7608c9c83c0f45a92074ac9ea364d94108c92729..1fd0f78d19e8ce83c01c2baaff5032048179b104 100644 (file)
@@ -14,6 +14,7 @@
 #include <freeradius-devel/server/global_lib.h>
 #include <freeradius-devel/server/map.h>
 #include <freeradius-devel/server/trunk.h>
+#include <freeradius-devel/util/dlist.h>
 
 #define LDAP_DEPRECATED 0      /* Quiet warnings about LDAP_DEPRECATED not being defined */
 
@@ -113,6 +114,8 @@ ldap_create_session_tracking_control LDAP_P((
                                                                        //!< persistent search.
 #define LDAP_SERVER_SHOW_DELETED_OID   "1.2.840.113556.1.4.417"        //!< OID of Active Directory control which
                                                                        //!< enables searching for deleted objects.
+#define LDAP_MATCHING_RULE_BIT_AND     "1.2.840.113556.1.4.803"        //!< OID of bit-wise AND LDAP match rule
+#define LDAP_MATCHING_RULE_BIT_OR      "1.2.840.113556.1.4.804"        //!< OID of bit-wise OR LDAP match rule
 
 typedef enum {
        LDAP_EXT_UNSUPPORTED,                           //!< Unsupported extension.
@@ -200,7 +203,9 @@ typedef struct {
        bool                    cleartext_password;     //!< Whether the server will return the user's plaintext
                                                        ///< password.
 
-       fr_ldap_sync_type_t     sync_type;              //! <What kind of LDAP sync this directory supports.
+       fr_ldap_sync_type_t     sync_type;              //!< What kind of LDAP sync this directory supports.
+
+       char const              **naming_contexts;      //!< Databases served by this directory.
 } fr_ldap_directory_t;
 
 /** Connection configuration
@@ -495,6 +500,57 @@ typedef struct {
        fr_ldap_result_code_t   ret;            //!< Return code of bind operation.
 } fr_ldap_bind_auth_ctx_t;
 
+typedef struct ldap_filter_s ldap_filter_t;
+
+/** Types of parsed LDAP filter nodes
+ */
+typedef enum {
+       LDAP_FILTER_NODE                = 0,            //!< The filter node is an individual one
+                                                       //!< to be evaluated against an attribute.
+       LDAP_FILTER_GROUP                               //!< The filter node is a parent of a group
+                                                       //!< which will be combined using a logical operator.
+} ldap_filter_type_t;
+
+/** Logical operators for use in LDAP filters
+ */
+typedef enum {
+       LDAP_FILTER_LOGIC_AND           = 1,
+       LDAP_FILTER_LOGIC_OR,
+       LDAP_FILTER_LOGIC_NOT
+} ldap_filter_logic_t;
+
+/** Operators for use in LDAP filters
+ */
+typedef enum {
+       LDAP_FILTER_OP_UNSET            = 0,            //!< Attribute not set yet
+       LDAP_FILTER_OP_EQ,                              //!< Attribute equals value
+       LDAP_FILTER_OP_SUBSTR,                          //!< Attribute matches string with wildcards
+       LDAP_FILTER_OP_PRESENT,                         //!< Attribute present
+       LDAP_FILTER_OP_GE,                              //!< Attribute greater than or equal to value
+       LDAP_FILTER_OP_LE,                              //!< Attribute less than or equal to value
+       LDAP_FILTER_OP_BIT_AND,                         //!< Bitwise AND comparison
+       LDAP_FILTER_OP_BIT_OR                           //!< Bitwise OR comparison
+} ldap_filter_op_t;
+
+/** Structure to hold parsed details of LDAP filters
+ */
+struct ldap_filter_s {
+       fr_dlist_t              entry;                  //!< Entry in the list of filter nodes.
+       ldap_filter_type_t      filter_type;            //!< Type of this filter node.
+       char                    *orig;                  //!< Text representation of filter for debug messages,
+       union {
+               struct {
+                       ldap_filter_logic_t     logic_op;       //!< Logical operator for this group.
+                       fr_dlist_head_t         children;       //!< List of child nodes in this group.
+               };
+               struct {
+                       char                    *attr;          //!< Attribute for the filter node.
+                       ldap_filter_op_t        op;             //!< Operator to be used for comparison.
+                       fr_value_box_t          *value;         //!< Value to compare with.
+               };
+       };
+};
+
 /** Codes returned by fr_ldap internal functions
  *
  */
@@ -631,7 +687,7 @@ unlang_action_t fr_ldap_trunk_modify(rlm_rcode_t *p_result,
                                     bool is_async);
 
 /*
- *     ldap.c - Wrappers arounds OpenLDAP functions.
+ *     base.c - Wrappers arounds OpenLDAP functions.
  */
 void           fr_ldap_timeout_debug(request_t *request, fr_ldap_connection_t const *conn,
                                      fr_time_delta_t timeout, char const *prefix);
@@ -697,6 +753,7 @@ int         fr_ldap_control_add_session_tracking(fr_ldap_connection_t *conn, request_t
                               "objectClass", \
                               "orcldirectoryversion", \
                               "supportedControl", \
+                              "namingContexts", \
                               NULL }
 
 int            fr_ldap_directory_result_parse(fr_ldap_directory_t *directory, LDAP *handle,
@@ -809,6 +866,12 @@ uint8_t            *fr_ldap_berval_to_bin(TALLOC_CTX *ctx, struct berval const *in);
 
 int            fr_ldap_parse_url_extensions(LDAPControl **sss, size_t sss_len, char *extensions[]);
 
+int            fr_ldap_attrs_check(char const **attrs, char const *attr);
+
+int            fr_ldap_server_url_check(fr_ldap_config_t *handle_config, char const *server, CONF_SECTION const *cs);
+
+int            fr_ldap_server_config_check(fr_ldap_config_t *handle_config, char const *server, CONF_SECTION *cs);
+
 /*
  *     referral.c - Handle LDAP referrals
  */
@@ -817,3 +880,13 @@ fr_ldap_referral_t *fr_ldap_referral_alloc(TALLOC_CTX *ctx, request_t *request);
 int            fr_ldap_referral_follow(fr_ldap_thread_t *thread, request_t *request, fr_ldap_query_t *query);
 
 int            fr_ldap_referral_next(fr_ldap_thread_t *thread, request_t *request, fr_ldap_query_t *query);
+
+/*
+ *     filter.c - Basic filter parsing and filtering
+ */
+typedef int    (*filter_attr_check_t)(char const *attr, void *uctx);
+
+fr_slen_t      fr_ldap_filter_parse(TALLOC_CTX *ctx, fr_dlist_head_t **root, fr_sbuff_t *filter,
+               filter_attr_check_t attr_check, void *uctx);
+
+bool           fr_ldap_filter_eval(fr_dlist_head_t *root, fr_ldap_connection_t *conn, LDAPMessage *msg);
diff --git a/src/lib/ldap/conf.c b/src/lib/ldap/conf.c
new file mode 100644 (file)
index 0000000..7f86cde
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ *
+ * @file ldap/conf.c
+ * @brief Configuration parsing for LDAP server connections.
+ *
+ * @copyright 2022 The FreeRADIUS server project
+ */
+RCSID("$Id$")
+
+#include <freeradius-devel/ldap/base.h>
+#include <freeradius-devel/ldap/conf.h>
+
+CONF_PARSER const fr_ldap_sasl_mech_static[] = {
+       { FR_CONF_OFFSET("mech", FR_TYPE_STRING | FR_TYPE_NOT_EMPTY, fr_ldap_sasl_t, mech) },
+       { FR_CONF_OFFSET("proxy", FR_TYPE_STRING, fr_ldap_sasl_t, proxy) },
+       { FR_CONF_OFFSET("realm", FR_TYPE_STRING, fr_ldap_sasl_t, realm) },
+       CONF_PARSER_TERMINATOR
+};
+
+/*
+ *     TLS Configuration
+ */
+CONF_PARSER const fr_ldap_tls_config[] = {
+       /*
+        *      Deprecated attributes
+        */
+       { FR_CONF_OFFSET("ca_file", FR_TYPE_FILE_INPUT, fr_ldap_config_t, tls_ca_file) },
+
+       { FR_CONF_OFFSET("ca_path", FR_TYPE_FILE_INPUT, fr_ldap_config_t, tls_ca_path) },
+
+       { FR_CONF_OFFSET("certificate_file", FR_TYPE_FILE_INPUT, fr_ldap_config_t, tls_certificate_file) },
+
+       { FR_CONF_OFFSET("private_key_file", FR_TYPE_FILE_INPUT, fr_ldap_config_t, tls_private_key_file) },
+
+       /*
+        *      LDAP Specific TLS attributes
+        */
+       { FR_CONF_OFFSET("start_tls", FR_TYPE_BOOL, fr_ldap_config_t, start_tls), .dflt = "no" },
+
+       { FR_CONF_OFFSET("require_cert", FR_TYPE_STRING, fr_ldap_config_t, tls_require_cert_str) },
+
+       { FR_CONF_OFFSET("tls_min_version", FR_TYPE_STRING, fr_ldap_config_t, tls_min_version_str) },
+
+       CONF_PARSER_TERMINATOR
+};
+
+/*
+ *     Various options that don't belong in the main configuration.
+ *
+ *     Note that these overlap a bit with the connection pool code!
+ */
+CONF_PARSER const fr_ldap_option_config[] = {
+       /*
+        *      Pool config items
+        */
+       { FR_CONF_OFFSET("chase_referrals", FR_TYPE_BOOL, fr_ldap_config_t, chase_referrals) },
+
+       { FR_CONF_OFFSET("use_referral_credentials", FR_TYPE_BOOL, fr_ldap_config_t, use_referral_credentials), .dflt = "no" },
+
+       { FR_CONF_OFFSET("referral_depth", FR_TYPE_UINT16, fr_ldap_config_t, referral_depth), .dflt = "5" },
+
+       { FR_CONF_OFFSET("rebind", FR_TYPE_BOOL, fr_ldap_config_t, rebind) },
+
+       { FR_CONF_OFFSET("sasl_secprops", FR_TYPE_STRING, fr_ldap_config_t, sasl_secprops) },
+
+       /*
+        *      We use this config option to populate libldap's LDAP_OPT_NETWORK_TIMEOUT -
+        *      timeout on network activity - specifically libldap's initial call to "connect"
+        *      Must be non-zero for async connections to start correctly.
+        */
+       { FR_CONF_OFFSET("net_timeout", FR_TYPE_TIME_DELTA, fr_ldap_config_t, net_timeout), .dflt = "10" },
+
+       { FR_CONF_OFFSET("idle", FR_TYPE_TIME_DELTA, fr_ldap_config_t, keepalive_idle), .dflt = "60" },
+
+       { FR_CONF_OFFSET("probes", FR_TYPE_UINT32, fr_ldap_config_t, keepalive_probes), .dflt = "3" },
+
+       { FR_CONF_OFFSET("interval", FR_TYPE_TIME_DELTA, fr_ldap_config_t, keepalive_interval), .dflt = "30" },
+
+       { FR_CONF_OFFSET("dereference", FR_TYPE_STRING, fr_ldap_config_t, dereference_str) },
+
+       /* allow server unlimited time for search (server-side limit) */
+       { FR_CONF_OFFSET("srv_timelimit", FR_TYPE_TIME_DELTA, fr_ldap_config_t, srv_timelimit), .dflt = "20" },
+
+       /*
+        *      Instance config items
+        */
+       /* timeout for search results */
+       { FR_CONF_OFFSET("res_timeout", FR_TYPE_TIME_DELTA, fr_ldap_config_t, res_timeout), .dflt = "20" },
+
+       { FR_CONF_OFFSET("idle_timeout", FR_TYPE_TIME_DELTA, fr_ldap_config_t, idle_timeout), .dflt = "300" },
+
+       { FR_CONF_OFFSET("reconnection_delay", FR_TYPE_TIME_DELTA, fr_ldap_config_t, reconnection_delay), .dflt = "10" },
+
+       CONF_PARSER_TERMINATOR
+};
+
diff --git a/src/lib/ldap/conf.h b/src/lib/ldap/conf.h
new file mode 100644 (file)
index 0000000..3b63ef9
--- /dev/null
@@ -0,0 +1,24 @@
+#pragma once
+/**
+ * $Id$
+ * @file lib/ldap/conf.h
+ * @brief Configuration parsing for LDAP server connections.
+ *
+ * @copyright 2022 The FreeRADIUS Server Project.
+ */
+
+#include <freeradius-devel/ldap/base.h>
+
+extern CONF_PARSER const fr_ldap_sasl_mech_static[];
+extern CONF_PARSER const fr_ldap_tls_config[];
+extern CONF_PARSER const fr_ldap_option_config[];
+
+/*
+ *  Macro for including common LDAP configuration items
+ */
+#define FR_LDAP_COMMON_CONF(_conf) { FR_CONF_OFFSET("port", FR_TYPE_UINT16, _conf, handle_config.port) }, \
+       { FR_CONF_OFFSET("identity", FR_TYPE_STRING, _conf, handle_config.admin_identity) }, \
+       { FR_CONF_OFFSET("password", FR_TYPE_STRING | FR_TYPE_SECRET, _conf, handle_config.admin_password) }, \
+       { FR_CONF_OFFSET("sasl", FR_TYPE_SUBSECTION, _conf, handle_config.admin_sasl), .subcs = (void const *) fr_ldap_sasl_mech_static }, \
+       { FR_CONF_OFFSET("options", FR_TYPE_SUBSECTION, _conf, handle_config), .subcs = (void const *) fr_ldap_option_config }, \
+       { FR_CONF_OFFSET("tls", FR_TYPE_SUBSECTION, _conf, handle_config), .subcs = (void const *) fr_ldap_tls_config }
index d31de1d58b1bbfd30809a909e87c07120dc76ec3..4b41b1fca6c9d7389143f7dab6f7add08dc1cf4c 100644 (file)
@@ -209,6 +209,19 @@ found:
                WARN("No supportedControl returned by LDAP server");
        }
 
+       /*
+        *      Extract naming contexts
+        */
+       values = ldap_get_values_len(handle, entry, "namingContexts");
+       if (!values) return 0;
+
+       num = ldap_count_values_len(values);
+       directory->naming_contexts = talloc_array(directory, char const *, num);
+       for (i = 0; i < num; i++) {
+               directory->naming_contexts[i] = fr_ldap_berval_to_string(directory, values[i]);
+       }
+       ldap_value_free_len(values);
+
        return 0;
 }
 
diff --git a/src/lib/ldap/filter.c b/src/lib/ldap/filter.c
new file mode 100644 (file)
index 0000000..96ff71f
--- /dev/null
@@ -0,0 +1,585 @@
+/*
+ *   This program is is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or (at
+ *   your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program; if not, write to the Free Software
+ *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+/**
+ * $Id$
+ * @file lib/ldap/filter.c
+ * @brief Functions to handle basic LDAP filter parsing and filtering
+ *
+ * @copyright 2022 Network RADIUS SARL (legal@networkradius.com)
+ */
+
+#include <freeradius-devel/ldap/base.h>
+
+static fr_table_num_sorted_t const ldap_filter_op_table[] = {
+       { L("<="),      LDAP_FILTER_OP_LE       },
+       { L("="),       LDAP_FILTER_OP_EQ       },
+       { L(">="),      LDAP_FILTER_OP_GE       }
+};
+static size_t ldap_filter_op_table_len = NUM_ELEMENTS(ldap_filter_op_table);
+
+static bool const      fr_ldap_attr_allowed_chars[UINT8_MAX + 1] = {
+                               ['-'] = true,
+                               SBUFF_CHAR_CLASS_ALPHA_NUM
+};
+
+#define FILTER_ATTR_MAX_LEN 256
+#define FILTER_VALUE_MAX_LEN 256
+
+static fr_slen_t ldap_filter_parse_node(ldap_filter_t *node, fr_sbuff_t *sbuff, int depth,
+                                       filter_attr_check_t attr_check, void *uctx);
+
+/** Parse LDAP filter logic group
+ *
+ * @param[in,out] node         to populate with parsed filter.
+ * @param[in] sbuff            pointing to filter to parse.
+ * @param[in] depth            to indent debug output, indicating nesting of groups.
+ * @param[in] attr_check       callback to check if required attributes are in the query.
+ * @param[in] uctx             passed to attribute check callback.
+ * @return
+ *  - number of bytes parsed on success
+ *  - < 0 on error
+ */
+static fr_slen_t ldap_filter_parse_logic(ldap_filter_t *node, fr_sbuff_t *sbuff, int depth,
+                                        filter_attr_check_t attr_check, void *uctx)
+{
+       ldap_filter_t   *child_node;
+       fr_slen_t       ret = 0;
+       fr_slen_t       parsed = 0;
+
+       switch(*fr_sbuff_current(sbuff)) {
+       case '&':
+               node->logic_op = LDAP_FILTER_LOGIC_AND;
+               node->orig = talloc_typed_strdup(node, "&");
+               break;
+
+       case '|':
+               node->logic_op = LDAP_FILTER_LOGIC_OR;
+               node->orig = talloc_typed_strdup(node, "|");
+               break;
+
+       case '!':
+               node->logic_op = LDAP_FILTER_LOGIC_NOT;
+               node->orig = talloc_typed_strdup(node, "!");
+               break;
+       }
+       parsed += fr_sbuff_advance(sbuff, 1);
+
+       DEBUG3("%*sCreating LDAP filter group %s", depth, "", node->orig);
+       node->filter_type = LDAP_FILTER_GROUP;
+       fr_dlist_init(&node->children, ldap_filter_t, entry);
+       MEM(child_node = talloc_zero(node, ldap_filter_t));
+       fr_dlist_insert_head(&node->children, child_node);
+
+       depth += 2;
+       ret = ldap_filter_parse_node(child_node, sbuff, depth, attr_check, uctx);
+       if (ret < 0) return ret;
+       parsed += ret;
+
+       /*
+        *      Look for sibling nodes to the child just processed
+        */
+       while (fr_sbuff_is_char(sbuff, '(')) {
+               if (node->logic_op == LDAP_FILTER_LOGIC_NOT) {
+                       fr_strerror_const("'!' operator can only apply to one filter");
+                       return fr_sbuff_error(sbuff);
+               }
+               MEM(child_node = talloc_zero(node, ldap_filter_t));
+               fr_dlist_insert_tail(&node->children, child_node);
+               ret = ldap_filter_parse_node(child_node, sbuff, depth, attr_check, uctx);
+               if (ret < 0) return ret;
+               parsed += ret;
+       }
+
+       return parsed;
+}
+
+/** Parse individual LDAP filter
+ *
+ * @param[in,out] node         to populate with parsed filter.
+ * @param[in] sbuff            pointing to filter to parse.
+ * @param[in] depth            to indent debug output, indicating nesting of groups.
+ * @param[in] attr_check       callback to check if required attributes are in the query.
+ * @param[in] uctx             passed to attribute check callback.
+ * @return
+ *  - number of bytes parsed on success
+ *  - < 0 on error
+ */
+static fr_slen_t ldap_filter_parse_filter(ldap_filter_t *node, fr_sbuff_t *sbuff, int depth,
+                                         filter_attr_check_t attr_check, void *uctx)
+{
+       char                    attr_buffer[FILTER_ATTR_MAX_LEN], val_buffer[FILTER_VALUE_MAX_LEN];
+       fr_sbuff_t              attr_sbuff = FR_SBUFF_IN(attr_buffer, FILTER_ATTR_MAX_LEN);
+       fr_sbuff_t              val_sbuff = FR_SBUFF_IN(val_buffer, FILTER_VALUE_MAX_LEN);
+       size_t                  len;
+       ssize_t                 slen;
+       ldap_filter_op_t        op;
+       fr_sbuff_marker_t       marker;
+
+       fr_sbuff_marker(&marker, sbuff);
+
+       /*
+        *      Extract the attribute name, blanking the buffer first.
+        */
+       memset(attr_buffer, 0, FILTER_ATTR_MAX_LEN);
+       len = fr_sbuff_out_bstrncpy_allowed(&attr_sbuff, sbuff, FILTER_ATTR_MAX_LEN - 1, fr_ldap_attr_allowed_chars);
+       if (len == 0) {
+               fr_strerror_const("Missing attribute name");
+               return fr_sbuff_error(sbuff);
+       }
+
+       MEM(node->attr = talloc_zero_array(node, char, len+1));
+       memcpy(node->attr, attr_buffer, len);
+
+       /*
+        *      Check for the attribute needed for the filter using the
+        *      provided callback.
+        */
+       if (attr_check) attr_check(node->attr, uctx);
+
+       /*
+        *      If the attribute name is followed by ':' there is an
+        *      extended match rule.  We only support two of them.
+        */
+       if (fr_sbuff_next_if_char(sbuff, ':')) {
+               if (fr_sbuff_adv_past_str_literal(sbuff, LDAP_MATCHING_RULE_BIT_AND)) {
+                       node->op = LDAP_FILTER_OP_BIT_AND;
+                       goto found_op;
+               }
+               if (fr_sbuff_adv_past_str_literal(sbuff, LDAP_MATCHING_RULE_BIT_OR)) {
+                       node->op = LDAP_FILTER_OP_BIT_OR;
+                       goto found_op;
+               }
+
+               fr_strerror_const("Unsupported extended match rule");
+               return fr_sbuff_error(sbuff);
+
+       found_op:
+               if(!(fr_sbuff_next_if_char(sbuff, ':'))) {
+                       fr_strerror_const("Missing ':' after extended match rule");
+                       return fr_sbuff_error(sbuff);
+               }
+       }
+
+       fr_sbuff_out_by_longest_prefix(&slen, &op, ldap_filter_op_table, sbuff, 0);
+
+       switch(op) {
+       case LDAP_FILTER_OP_EQ:
+               if (node->op == LDAP_FILTER_OP_UNSET) node->op = op;
+               break;
+
+       case LDAP_FILTER_OP_LE:
+       case LDAP_FILTER_OP_GE:
+               node->op = op;
+               break;
+
+       default:
+               fr_strerror_const("Incorrect operator");
+               return fr_sbuff_error(sbuff);
+       }
+
+       if (((node->op == LDAP_FILTER_OP_BIT_AND) || (node->op == LDAP_FILTER_OP_BIT_OR)) &&
+           (op != LDAP_FILTER_OP_EQ)) {
+               fr_strerror_const("Extended match rule only valid with '=' operator");
+               return fr_sbuff_error(sbuff);
+       }
+
+       /*
+        *      Capture everything up to the next ')' as the value, blanking the buffer first.
+        */
+       memset(val_buffer, 0, FILTER_VALUE_MAX_LEN);
+       len = fr_sbuff_out_bstrncpy_until(&val_sbuff, sbuff, FILTER_VALUE_MAX_LEN - 1, &FR_SBUFF_TERM(")"), NULL);
+
+       if (len == 0) {
+               fr_strerror_const("Missing filter value");
+               return fr_sbuff_error(sbuff);
+       }
+
+       /*
+        *      An equality test with a value of '*' is a present test
+        */
+       if ((len == 1) && (*val_buffer == '*') && (node->op == LDAP_FILTER_OP_EQ)) node->op = LDAP_FILTER_OP_PRESENT;
+
+       /*
+        *      Equality tests with '*' in the value are substring matches
+        */
+       fr_sbuff_set_to_start(&val_sbuff);
+       if ((node->op == LDAP_FILTER_OP_EQ) && (fr_sbuff_adv_to_chr(&val_sbuff, SIZE_MAX, '*'))) {
+               node->op = LDAP_FILTER_OP_SUBSTR;
+       }
+
+       MEM(node->value = fr_value_box_alloc_null(node));
+
+       switch (node->op) {
+       case LDAP_FILTER_OP_EQ:
+       case LDAP_FILTER_OP_PRESENT:
+       case LDAP_FILTER_OP_SUBSTR:
+               if (fr_value_box_bstrndup(node, node->value, NULL, val_buffer, len, false) < 0) {
+                       fr_strerror_const("Failed parsing value for filter");
+                       return fr_sbuff_error(sbuff);
+               }
+               break;
+
+       /*
+        *      Since we don't have the LDAP schema, we make an assumption that <=, >= and
+        *      bitwise operators are going to be used with numeric attributes
+        */
+       case LDAP_FILTER_OP_GE:
+       case LDAP_FILTER_OP_LE:
+       case LDAP_FILTER_OP_BIT_AND:
+       case LDAP_FILTER_OP_BIT_OR:
+               if (fr_value_box_from_str(node, node->value, FR_TYPE_UINT32, NULL,
+                                         val_buffer, len, NULL, false) < 0) {
+                       fr_strerror_const("Failed parsing value for filter");
+                       return fr_sbuff_error(sbuff);
+               }
+               break;
+
+       /*
+        *      Operator should not be unset at the end of a filter
+        */
+       case LDAP_FILTER_OP_UNSET:
+               fr_assert(0);
+               break;
+       }
+
+       /*
+        *      Take a copy of the original filter for debug output
+        */
+       MEM(node->orig = talloc_zero_array(node, char, fr_sbuff_diff(sbuff, &marker) + 1));
+       memcpy(node->orig, fr_sbuff_current(&marker), fr_sbuff_diff(sbuff, &marker));
+       DEBUG3("%*sParsed LDAP filter (%s)", depth, "", node->orig);
+
+       return fr_sbuff_diff(sbuff, &marker);
+}
+
+/** Parse individual LDAP filter nodes
+ *
+ * A node can either be a group of nodes joined with a logical operator
+ * or an individual filter.
+ *
+ * @param[in,out] node         to populate with parsed filter.
+ * @param[in] sbuff            pointing to filter to parse.
+ * @param[in] depth            to indent debug output, indicating nesting of groups.
+ * @param[in] attr_check       callback to check if required attributes are in the query.
+ * @param[in] uctx             passed to attribute check callback.
+ * @return
+ *  - number of bytes parsed on success
+ *  - < 0 on error
+ */
+static fr_slen_t ldap_filter_parse_node(ldap_filter_t *node, fr_sbuff_t *sbuff, int depth,
+                                       filter_attr_check_t attr_check, void *uctx)
+{
+       fr_sbuff_marker_t       marker;
+       fr_slen_t               ret;
+       fr_slen_t               parsed = 0;
+
+       static bool const       logical_op_chars[UINT8_MAX +1] = {
+                                       ['!'] = true, ['&'] = true, ['|'] = true,
+                               };
+
+       if (!fr_sbuff_next_if_char(sbuff, '(')) {
+               fr_strerror_const("Missing '('");
+               return fr_sbuff_error(sbuff);
+       }
+
+       /*
+        *      Firstly, look for the characters which indicate the start of a group of filters
+        *      to be combined with a logical operator.
+        */
+       fr_sbuff_marker(&marker, sbuff);
+       if (fr_sbuff_adv_past_allowed(sbuff, 1, logical_op_chars, NULL)) {
+               fr_sbuff_set(sbuff, &marker);
+               ret = ldap_filter_parse_logic(node, sbuff, depth, attr_check, uctx);
+       } else {
+               ret = ldap_filter_parse_filter(node, sbuff, depth, attr_check, uctx);
+       }
+
+       if (ret < 0) return ret;
+       parsed += ret;
+
+       if (!fr_sbuff_next_if_char(sbuff, ')')) {
+               fr_strerror_const("Missing ')'");
+               return fr_sbuff_error(sbuff);
+       }
+       parsed ++;
+
+       /*
+        *      If we're at the very top level we should be at the end
+        *      of the buffer
+        */
+       if ((depth == 0) && (fr_sbuff_extend(sbuff))) {
+               fr_strerror_const("Extra characters at the end of LDAP filter");
+               return fr_sbuff_error(sbuff);
+       }
+
+       return parsed;
+}
+
+/** Parse an LDAP filter into its component nodes
+ *
+ * @param[in] ctx              to allocate nodes in.
+ * @param[in,out] root         where to allocate the root of the parsed filter.
+ * @param[in] filter           to parse.
+ * @param[in] attr_check       callback to check if required attributes are in the query.
+ * @param[in] uctx             passed to attribute check callback.
+ * @return
+ *     - number of bytes parsed on success
+ *     < 0 on failure
+ */
+fr_slen_t fr_ldap_filter_parse(TALLOC_CTX *ctx, fr_dlist_head_t **root, fr_sbuff_t *filter,
+                              filter_attr_check_t attr_check, void *uctx)
+{
+       ldap_filter_t   *node;
+       fr_slen_t       ret;
+
+       MEM(*root = talloc_zero(ctx, fr_dlist_head_t));
+       fr_dlist_init(*root, ldap_filter_t, entry);
+
+       MEM(node = talloc_zero(*root, ldap_filter_t));
+       fr_dlist_insert_head(*root, node);
+
+       ret = ldap_filter_parse_node(node, filter, 0, attr_check, uctx);
+       if (ret < 0) {
+               talloc_free(*root);
+               *root = NULL;
+               return ret;
+       }
+
+       return ret;
+}
+
+static bool ldap_filter_node_eval(ldap_filter_t *node, fr_ldap_connection_t *conn, LDAPMessage *msg, int depth);
+
+/** Evaluate a group of LDAP filters
+ *
+ * Groups have a logical operator of &, | or !
+ *
+ * @param[in] group    to evaluate.
+ * @param[in] conn     LDAP connection the message being filtered was returned on
+ * @param[in] msg      to filter
+ * @param[in] depth    to indent debug messages, reflecting group nesting
+ * @return true or false result of the group evaluation
+ */
+static bool ldap_filter_group_eval(ldap_filter_t *group, fr_ldap_connection_t *conn, LDAPMessage *msg, int depth)
+{
+       ldap_filter_t   *node = NULL;
+       bool            filter_state = false;
+
+       DEBUG3("%*sEvaluating LDAP filter group %s", depth, "", group->orig);
+       depth += 2;
+       while ((node = fr_dlist_next(&group->children, node))) {
+               switch (node->filter_type) {
+               case LDAP_FILTER_GROUP:
+                       filter_state = ldap_filter_group_eval(node, conn, msg, depth);
+                       break;
+               case LDAP_FILTER_NODE:
+                       filter_state = ldap_filter_node_eval(node, conn, msg, depth);
+                       break;
+               }
+
+               /*
+                *      Short circuit the group depending on the logical operator
+                *      and the return state of the last node
+                */
+               if (((group->logic_op == LDAP_FILTER_LOGIC_OR) && filter_state) ||
+                   ((group->logic_op == LDAP_FILTER_LOGIC_AND) && !filter_state)) {
+                       break;
+               }
+       }
+
+       filter_state = (group->logic_op == LDAP_FILTER_LOGIC_NOT ? !filter_state : filter_state);
+
+       depth -= 2;
+       DEBUG3("%*sLDAP filter group %s results in %s", depth, "", group->orig, (filter_state ? "TRUE" : "FALSE"));
+       return filter_state;
+}
+
+#define DEBUG_LDAP_ATTR_VAL if (DEBUG_ENABLED3) { \
+       fr_value_box_t  value_box; \
+       fr_ldap_berval_to_value_str_shallow(&value_box, values[i]); \
+       DEBUG3("%*s  Evaluating attribute \"%s\", value \"%pV\"", depth, "", node->attr, &value_box); \
+}
+
+/** Evaluate a single LDAP filter node
+ *
+ * @param[in] node     to evaluate.
+ * @param[in] conn     LDAP connection the message being filtered was returned on.
+ * @param[in] msg      to filter.
+ * @param[in] depth    to indent debug messages, reflecting group nesting.
+ * @return true or false result of the node evaluation.
+ */
+static bool ldap_filter_node_eval(ldap_filter_t *node, fr_ldap_connection_t *conn, LDAPMessage *msg, int depth)
+{
+       struct berval   **values;
+       int             count, i;
+       bool            filter_state = false;
+
+       switch (node->filter_type) {
+       case LDAP_FILTER_GROUP:
+               return ldap_filter_group_eval(node, conn, msg, depth);
+
+       case LDAP_FILTER_NODE:
+               DEBUG3("%*sEvaluating LDAP filter (%s)", depth, "", node->orig);
+               values = ldap_get_values_len(conn->handle, msg, node->attr);
+               count = ldap_count_values_len(values);
+
+               switch (node->op) {
+               case LDAP_FILTER_OP_PRESENT:
+                       filter_state = (count > 0 ? true : false);
+                       break;
+
+               case LDAP_FILTER_OP_EQ:
+                       for (i = 0; i < count; i++) {
+                               DEBUG_LDAP_ATTR_VAL
+                               if ((node->value->length == values[i]->bv_len) &&
+                                   (strncasecmp(values[i]->bv_val, node->value->vb_strvalue, values[i]->bv_len) == 0)) {
+                                       filter_state = true;
+                                       break;
+                               }
+                       }
+                       break;
+
+               /*
+                *      LDAP filters only use one wildcard character '*' for zero or more
+                *      character matches.
+                */
+               case LDAP_FILTER_OP_SUBSTR:
+               {
+                       char const      *v, *t, *v_end, *t_end;
+                       bool            skip;
+
+                       /*
+                        *      Point t_end at the final character of the filter value
+                        *      - not the NULL - so we can check for trailing '*'
+                        */
+                       t_end = node->value->vb_strvalue + node->value->length - 1;
+
+                       for (i = 0; i < count; i++) {
+                               DEBUG_LDAP_ATTR_VAL
+                               t = node->value->vb_strvalue;
+                               v = values[i]->bv_val;
+                               v_end = values[i]->bv_val + values[i]->bv_len - 1;
+                               skip = false;
+
+                               /*
+                                *      Walk the value (v) and test (t), comparing until
+                                *      there is a mis-match or the end of one is reached.
+                                */
+                               while ((v <= v_end) && (t <= t_end)) {
+                                       /*
+                                        *      If a wildcard is found in the test,
+                                        *      indicate that we can skip non-matching
+                                        *      characters in the value
+                                        */
+                                       if (*t == '*'){
+                                               skip = true;
+                                               t++;
+                                               continue;
+                                       }
+                                       if (skip) {
+                                               while ((tolower(*t) != tolower(*v)) && (v <= v_end)) v++;
+                                       }
+                                       if (tolower(*t) != tolower(*v)) break;
+                                       skip = false;
+                                       t++;
+                                       v++;
+                               }
+
+                               /*
+                                *      If we've got to the end of both the test and value,
+                                *      or we've used all of the test and the last character is '*'
+                                *      then we've matched the pattern.
+                                */
+                               if (((v > v_end) && (t > t_end)) || ((t >= t_end) && (*t_end == '*'))) {
+                                       filter_state = true;
+                                       break;
+                               }
+                       }
+               }
+                       break;
+
+               /*
+                *      For >=, <= and bitwise operators, we assume numeric values
+                */
+               case LDAP_FILTER_OP_GE:
+               case LDAP_FILTER_OP_LE:
+               case LDAP_FILTER_OP_BIT_AND:
+               case LDAP_FILTER_OP_BIT_OR:
+               {
+                       char            buffer[11];     /* Max uint32_t + 1 */
+                       uint32_t        value;
+                       for (i = 0; i < count; i++) {
+                               DEBUG_LDAP_ATTR_VAL
+                               /*
+                                *      String too long for max uint32
+                                */
+                               if (values[i]->bv_len > 10) continue;
+
+                               /*
+                                *      bv_val is not NULL terminated - so copy to a
+                                *      NULL terminated string before parsing.
+                                */
+                               memcpy(buffer, values[i]->bv_val, values[i]->bv_len);
+                               buffer[values[i]->bv_len] = '\0';
+
+                               value = (uint32_t)strtol(buffer, NULL, 10);
+                               switch (node->op) {
+                               case LDAP_FILTER_OP_GE:
+                                       if (value >= node->value->vb_uint32) filter_state = true;
+                                       break;
+                               case LDAP_FILTER_OP_LE:
+                                       if (value <= node->value->vb_uint32) filter_state = true;
+                                       break;
+                               case LDAP_FILTER_OP_BIT_AND:
+                                       if (value & node->value->vb_uint32) filter_state = true;
+                                       break;
+                               case LDAP_FILTER_OP_BIT_OR:
+                                       if (value | node->value->vb_uint32) filter_state = true;
+                                       break;
+                               default:
+                                       fr_assert(0);
+                                       break;
+                               }
+                               if (filter_state) break;
+                       }
+               }
+                       break;
+
+               default:
+                       fr_assert(0);
+                       break;
+
+               }
+
+               ldap_value_free_len(values);
+       }
+
+       DEBUG3("%*sLDAP filter returns %s", depth, "", (filter_state ? "TRUE" : "FALSE"));
+
+       return filter_state;
+}
+
+/** Evaluate an LDAP filter
+ *
+ * @param[in] root     of the LDAP filter to evaluate.
+ * @param[in] conn     LDAP connection the message being filtered was returned on.
+ * @param[in] msg      to filter.
+ * @return true or false result of the node evaluation.
+ */
+bool fr_ldap_filter_eval(fr_dlist_head_t *root, fr_ldap_connection_t *conn, LDAPMessage *msg) {
+       return ldap_filter_node_eval(fr_dlist_head(root), conn, msg, 0);
+}
index 05e3ac5ba838acc238b948d2539c5511563a143f..4ef649038a600502f6ee7e52522aa45d5133ccb1 100644 (file)
@@ -557,3 +557,169 @@ ssize_t fr_ldap_xlat_filter(request_t *request, char const **sub, size_t sublen,
 
        return len;
 }
+
+/** Check that a particular attribute is included in an attribute list
+ *
+ * @param[in] attrs    list to check
+ * @param[in] attr     to look for
+ * @return
+ *     - 1 if attr is in list
+ *     - 0 if attr is missing
+ *     - -1 if checks not possible
+ */
+int fr_ldap_attrs_check(char const **attrs, char const *attr)
+{
+       size_t          len, i;
+
+       if (!attr) return -1;
+
+       len = talloc_array_length(attrs);
+
+       for (i = 0; i < len; i++) {
+               if (!attrs[i]) continue;
+               if (strcasecmp(attrs[i], attr) == 0) return 1;
+               if (strcasecmp(attrs[i], "*") == 0) return 1;
+       }
+
+       return 0;
+}
+
+/** Check an LDAP server entry in URL format is valid
+ *
+ * @param[in,out] handle_config        LDAP handle config being built
+ * @param[in] server           string to parse
+ * @param[in] cs               in which the server is defined
+ * @return
+ *     - 0 for valid server definition
+ *     - -1 for invalid server definition
+ */
+int fr_ldap_server_url_check(fr_ldap_config_t *handle_config, char const *server, CONF_SECTION const *cs)
+{
+       LDAPURLDesc     *ldap_url;
+       bool            set_port_maybe = true;
+       int             default_port = LDAP_PORT;
+       char            *p, *url;
+       CONF_ITEM       *ci = (CONF_ITEM *)cf_pair_find(cs, "server");
+
+       if (ldap_url_parse(server, &ldap_url)) {
+               cf_log_err(ci, "Parsing LDAP URL \"%s\" failed", server);
+       ldap_url_error:
+               ldap_free_urldesc(ldap_url);
+               return -1;
+       }
+
+       if (ldap_url->lud_dn && (ldap_url->lud_dn[0] != '\0')) {
+               cf_log_err(ci, "Base DN cannot be specified via server URL");
+               goto ldap_url_error;
+       }
+
+       if (ldap_url->lud_attrs && ldap_url->lud_attrs[0]) {
+               cf_log_err(ci, "Attribute list cannot be speciried via server URL");
+               goto ldap_url_error;
+       }
+
+       /*
+        *      ldap_url_parse sets this to base by default.
+        */
+       if (ldap_url->lud_scope != LDAP_SCOPE_BASE) {
+               cf_log_err(ci, "Scope cannot be specified via server URL");
+               goto ldap_url_error;
+       }
+       ldap_url->lud_scope = -1;       /* Otherwise LDAP adds ?base */
+
+       /*
+        *      The public ldap_url_parse function sets the default
+        *      port, so we have to discover whether a port was
+        *      included ourselves.
+        */
+       if ((p = strchr(server, ']')) && (p[1] == ':')) {                       /* IPv6 */
+               set_port_maybe = false;
+       } else if ((p = strchr(server, ':')) && (strchr(p+1, ':') != NULL)) {   /* IPv4 */
+               set_port_maybe = false;
+       }
+
+       /*
+        *      Figure out the default port from the URL
+        */
+       if (ldap_url->lud_scheme) {
+               if (strcmp(ldap_url->lud_scheme, "ldaps") == 0) {
+                       if (handle_config->start_tls == true) {
+                               cf_log_err(ci, "ldap:s// scheme is not compatible with 'start_tls'");
+                               goto ldap_url_error;
+                       }
+               } else if (strcmp(ldap_url->lud_scheme, "ldapi") == 0) {
+                       set_port_maybe = false;
+               }
+       }
+
+       if (set_port_maybe) {
+               /*
+                *      URL port overrides configured port.
+                */
+               ldap_url->lud_port = handle_config->port;
+
+               /*
+                *      If there's no URL port, then set it to the default
+                *      this is so debugging messages show explicitly
+                *      the port we're connecting to.
+                */
+               if (!ldap_url->lud_port) ldap_url->lud_port = default_port;
+       }
+
+       url = ldap_url_desc2str(ldap_url);
+       if (!url) {
+               cf_log_err(ci, "Failed recombining URL components");
+               goto ldap_url_error;
+       }
+       handle_config->server = talloc_asprintf_append(handle_config->server, "%s ", url);
+
+       ldap_free_urldesc(ldap_url);
+       ldap_memfree(url);
+       return (0);
+}
+
+/** Check an LDAP server config in server:port format is valid
+ *
+ * @param[in,out] handle_config        LDAP handle config being built
+ * @param[in] server           string to parse
+ * @param[in] cs               in which the server is defined
+ * @return
+ *     - 0 for valid server definition
+ *     - -1 for invalid server definition
+ */
+int fr_ldap_server_config_check(fr_ldap_config_t *handle_config, char const *server, CONF_SECTION *cs)
+{
+       char    const *p;
+       char    *q;
+       int     port = 0;
+       size_t  len;
+
+       port = handle_config->port;
+
+       /*
+        *      We don't support URLs if the library didn't provide
+        *      URL parsing functions.
+        */
+       if (strchr(server, '/')) {
+               CONF_ITEM       *ci;
+       bad_server_fmt:
+               ci = (CONF_ITEM *)cf_pair_find(cs, "server");
+               cf_log_err(ci, "Invalid 'server' entry, must be in format <server>[:<port>] or "
+                              "an ldap URI (ldap|cldap|ldaps|ldapi)://<server>:<port>");
+               return -1;
+       }
+
+       p = strrchr(server, ':');
+       if (p) {
+               port = (int)strtol((p + 1), &q, 10);
+               if ((p == server) || ((p + 1) == q) || (*q != '\0')) goto bad_server_fmt;
+               len = p - server;
+       } else {
+               len = strlen(server);
+       }
+       if (port == 0) port = LDAP_PORT;
+
+       handle_config->server = talloc_asprintf_append(handle_config->server, "ldap://%.*s:%i ",
+                                                      (int)len, server, port);
+       return 0;
+}
index 63da9c444fde8c6774823e0e1e2451a983c19ebc..8d54885a301d92beed677760db2f32e49696288a 100644 (file)
@@ -35,6 +35,7 @@ USES_APPLE_DEPRECATED_API
 #include <freeradius-devel/util/uri.h>
 
 #include "rlm_ldap.h"
+#include <freeradius-devel/ldap/conf.h>
 
 #include <freeradius-devel/server/map_proc.h>
 #include <freeradius-devel/server/module_rlm.h>
@@ -46,41 +47,6 @@ static CONF_PARSER sasl_mech_dynamic[] = {
        CONF_PARSER_TERMINATOR
 };
 
-static CONF_PARSER sasl_mech_static[] = {
-       { FR_CONF_OFFSET("mech", FR_TYPE_STRING | FR_TYPE_NOT_EMPTY, fr_ldap_sasl_t, mech) },
-       { FR_CONF_OFFSET("proxy", FR_TYPE_STRING, fr_ldap_sasl_t, proxy) },
-       { FR_CONF_OFFSET("realm", FR_TYPE_STRING, fr_ldap_sasl_t, realm) },
-       CONF_PARSER_TERMINATOR
-};
-
-/*
- *     TLS Configuration
- */
-static CONF_PARSER tls_config[] = {
-       /*
-        *      Deprecated attributes
-        */
-       { FR_CONF_OFFSET("ca_file", FR_TYPE_FILE_INPUT, fr_ldap_config_t, tls_ca_file) },
-
-       { FR_CONF_OFFSET("ca_path", FR_TYPE_FILE_INPUT, fr_ldap_config_t, tls_ca_path) },
-
-       { FR_CONF_OFFSET("certificate_file", FR_TYPE_FILE_INPUT, fr_ldap_config_t, tls_certificate_file) },
-
-       { FR_CONF_OFFSET("private_key_file", FR_TYPE_FILE_INPUT, fr_ldap_config_t, tls_private_key_file) },
-
-       /*
-        *      LDAP Specific TLS attributes
-        */
-       { FR_CONF_OFFSET("start_tls", FR_TYPE_BOOL, fr_ldap_config_t, start_tls), .dflt = "no" },
-
-       { FR_CONF_OFFSET("require_cert", FR_TYPE_STRING, fr_ldap_config_t, tls_require_cert_str) },
-
-       { FR_CONF_OFFSET("tls_min_version", FR_TYPE_STRING, fr_ldap_config_t, tls_min_version_str) },
-
-       CONF_PARSER_TERMINATOR
-};
-
-
 static CONF_PARSER profile_config[] = {
        { FR_CONF_OFFSET("filter", FR_TYPE_TMPL, rlm_ldap_t, profile_filter), .dflt = "(&)", .quote = T_SINGLE_QUOTED_STRING }, //!< Correct filter for when the DN is known.
        { FR_CONF_OFFSET("attribute", FR_TYPE_STRING, rlm_ldap_t, profile_attr) },
@@ -132,68 +98,16 @@ static const CONF_PARSER acct_section_config[] = {
        CONF_PARSER_TERMINATOR
 };
 
-/*
- *     Various options that don't belong in the main configuration.
- *
- *     Note that these overlap a bit with the connection pool code!
- */
-static CONF_PARSER option_config[] = {
-       /*
-        *      Pool config items
-        */
-       { FR_CONF_OFFSET("chase_referrals", FR_TYPE_BOOL, rlm_ldap_t, handle_config.chase_referrals) },
-
-       { FR_CONF_OFFSET("use_referral_credentials", FR_TYPE_BOOL, rlm_ldap_t, handle_config.use_referral_credentials), .dflt = "no" },
-
-       { FR_CONF_OFFSET("referral_depth", FR_TYPE_UINT16, rlm_ldap_t, handle_config.referral_depth), .dflt = "5" },
-
-       { FR_CONF_OFFSET("rebind", FR_TYPE_BOOL, rlm_ldap_t, handle_config.rebind) },
-
-       { FR_CONF_OFFSET("sasl_secprops", FR_TYPE_STRING, rlm_ldap_t, handle_config.sasl_secprops) },
-
-       /*
-        *      We use this config option to populate libldap's LDAP_OPT_NETWORK_TIMEOUT -
-        *      timeout on network activity - specifically libldap's initial call to "connect"
-        *      Must be non-zero for async connections to start correctly.
-        */
-       { FR_CONF_OFFSET("net_timeout", FR_TYPE_TIME_DELTA, rlm_ldap_t, handle_config.net_timeout), .dflt = "10" },
-
-       { FR_CONF_OFFSET("idle", FR_TYPE_TIME_DELTA, rlm_ldap_t, handle_config.keepalive_idle), .dflt = "60" },
-
-       { FR_CONF_OFFSET("probes", FR_TYPE_UINT32, rlm_ldap_t, handle_config.keepalive_probes), .dflt = "3" },
-
-       { FR_CONF_OFFSET("interval", FR_TYPE_TIME_DELTA, rlm_ldap_t, handle_config.keepalive_interval), .dflt = "30" },
-
-       { FR_CONF_OFFSET("dereference", FR_TYPE_STRING, rlm_ldap_t, handle_config.dereference_str) },
-
-       /* allow server unlimited time for search (server-side limit) */
-       { FR_CONF_OFFSET("srv_timelimit", FR_TYPE_TIME_DELTA, rlm_ldap_t, handle_config.srv_timelimit), .dflt = "20" },
-
-       /*
-        *      Instance config items
-        */
-       /* timeout for search results */
-       { FR_CONF_OFFSET("res_timeout", FR_TYPE_TIME_DELTA, rlm_ldap_t, handle_config.res_timeout), .dflt = "20" },
-
-       { FR_CONF_OFFSET("idle_timeout", FR_TYPE_TIME_DELTA, rlm_ldap_t, handle_config.idle_timeout), .dflt = "300" },
-
-       { FR_CONF_OFFSET("reconnection_delay", FR_TYPE_TIME_DELTA, rlm_ldap_t, handle_config.reconnection_delay), .dflt = "10" },
-
-       CONF_PARSER_TERMINATOR
-};
-
 static const CONF_PARSER module_config[] = {
        /*
         *      Pool config items
         */
        { FR_CONF_OFFSET("server", FR_TYPE_STRING | FR_TYPE_MULTI, rlm_ldap_t, handle_config.server_str) },     /* Do not set to required */
 
-       { FR_CONF_OFFSET("port", FR_TYPE_UINT16, rlm_ldap_t, handle_config.port) },
-
-       { FR_CONF_OFFSET("identity", FR_TYPE_STRING, rlm_ldap_t, handle_config.admin_identity) },
-       { FR_CONF_OFFSET("password", FR_TYPE_STRING | FR_TYPE_SECRET, rlm_ldap_t, handle_config.admin_password) },
-
-       { FR_CONF_OFFSET("sasl", FR_TYPE_SUBSECTION, rlm_ldap_t, handle_config.admin_sasl), .subcs = (void const *) sasl_mech_static },
+       /*
+        *      Common LDAP conf parsers
+        */
+       FR_LDAP_COMMON_CONF(rlm_ldap_t),
 
        { FR_CONF_OFFSET("valuepair_attribute", FR_TYPE_STRING, rlm_ldap_t, valuepair_attr) },
 
@@ -218,10 +132,6 @@ static const CONF_PARSER module_config[] = {
 
        { FR_CONF_POINTER("profile", FR_TYPE_SUBSECTION, NULL), .subcs = (void const *) profile_config },
 
-       { FR_CONF_POINTER("options", FR_TYPE_SUBSECTION, NULL), .subcs = (void const *) option_config },
-
-       { FR_CONF_OFFSET("tls", FR_TYPE_SUBSECTION, rlm_ldap_t, handle_config), .subcs = (void const *) tls_config },
-
        { FR_CONF_OFFSET("pool", FR_TYPE_SUBSECTION, rlm_ldap_t, trunk_conf), .subcs = (void const *) fr_trunk_config },
 
        CONF_PARSER_TERMINATOR
@@ -1968,135 +1878,13 @@ static int mod_instantiate(module_inst_ctx_t const *mctx)
                 *      the server information in the format it needs.
                 */
                if (ldap_is_ldap_url(value)) {
-                       LDAPURLDesc     *ldap_url;
-                       bool            set_port_maybe = true;
-                       int             default_port = LDAP_PORT;
-                       char            *p;
-                       char            *url;
-
-                       if (ldap_url_parse(value, &ldap_url)){
-                               cf_log_err(conf, "Parsing LDAP URL \"%s\" failed", value);
-                       ldap_url_error:
-                               ldap_free_urldesc(ldap_url);
-                               return -1;
-                       }
-
-                       if (ldap_url->lud_dn && (ldap_url->lud_dn[0] != '\0')) {
-                               cf_log_err(conf, "Base DN cannot be specified via server URL");
-                               goto ldap_url_error;
-                       }
-
-                       if (ldap_url->lud_attrs && ldap_url->lud_attrs[0]) {
-                               cf_log_err(conf, "Attribute list cannot be specified via server URL");
-                               goto ldap_url_error;
-                       }
-
-                       /*
-                        *      ldap_url_parse sets this to base by default.
-                        */
-                       if (ldap_url->lud_scope != LDAP_SCOPE_BASE) {
-                               cf_log_err(conf, "Scope cannot be specified via server URL");
-                               goto ldap_url_error;
-                       }
-                       ldap_url->lud_scope = -1;       /* Otherwise LDAP adds ?base */
-
-                       /*
-                        *      The public ldap_url_parse function sets the default
-                        *      port, so we have to discover whether a port was
-                        *      included ourselves.
-                        */
-                       if ((p = strchr(value, ']')) && (p[1] == ':')) {                        /* IPv6 */
-                               set_port_maybe = false;
-                       } else if ((p = strchr(value, ':')) && (strchr(p + 1, ':') != NULL)) {  /* IPv4 */
-                               set_port_maybe = false;
-                       }
-
-                       /*
-                        *      Figure out the default port from the URL
-                        */
-                       if (ldap_url->lud_scheme) {
-                               if (strcmp(ldap_url->lud_scheme, "ldaps") == 0) {
-                                       if (inst->handle_config.start_tls == true) {
-                                               cf_log_err(conf, "ldaps:// scheme is not compatible with 'start_tls'");
-                                               goto ldap_url_error;
-                                       }
-                                       default_port = LDAPS_PORT;
-
-                               } else if (strcmp(ldap_url->lud_scheme, "ldapi") == 0) {
-                                       set_port_maybe = false; /* Unix socket, no port */
-                               }
-                       }
-
-                       if (set_port_maybe) {
-                               /*
-                                *      URL port overrides configured port.
-                                */
-                               ldap_url->lud_port = inst->handle_config.port;
-
-                               /*
-                                *      If there's no URL port, then set it to the default
-                                *      this is so debugging messages show explicitly
-                                *      the port we're connecting to.
-                                */
-                               if (!ldap_url->lud_port) ldap_url->lud_port = default_port;
-                       }
-
-                       url = ldap_url_desc2str(ldap_url);
-                       if (!url) {
-                               cf_log_err(conf, "Failed recombining URL components");
-                               goto ldap_url_error;
-                       }
-                       inst->handle_config.server = talloc_asprintf_append(inst->handle_config.server,
-                                                                           "%s ", url);
-                       free(url);
-
-                       /*
-                        *      @todo We could set a few other top level
-                        *      directives using the URL, like base_dn
-                        *      and scope.
-                        */
-                       ldap_free_urldesc(ldap_url);
-               /*
-                *      We need to construct an LDAP URI
-                */
+                       if (fr_ldap_server_url_check(&inst->handle_config, value, conf) < 0) return -1;
                } else
                /*
-                *      If it's not an URL, or we don't have the functions necessary
-                *      to break apart the URL and recombine it, then just treat
-                *      server as a hostname.
+                *      If it's not an URL, then just treat server as a hostname.
                 */
                {
-                       char    const *p;
-                       char    *q;
-                       int     port = 0;
-                       size_t  len;
-
-                       port = inst->handle_config.port;
-
-                       /*
-                        *      We don't support URLs if the library didn't provide
-                        *      URL parsing functions.
-                        */
-                       if (strchr(value, '/')) {
-                       bad_server_fmt:
-                               cf_log_err(conf, "Invalid 'server' entry, must be in format <server>[:<port>] or "
-                                             "an ldap URI (ldap|cldap|ldaps|ldapi)://<server>:<port>");
-                               return -1;
-                       }
-
-                       p = strrchr(value, ':');
-                       if (p) {
-                               port = (int)strtol((p + 1), &q, 10);
-                               if ((p == value) || ((p + 1) == q) || (*q != '\0')) goto bad_server_fmt;
-                               len = p - value;
-                       } else {
-                               len = strlen(value);
-                       }
-                       if (port == 0) port = LDAP_PORT;
-
-                       inst->handle_config.server = talloc_asprintf_append(inst->handle_config.server,
-                                                                           "ldap://%.*s:%i ",
-                                                                           (int) len, value, port);
+                       if (fr_ldap_server_config_check(&inst->handle_config, value, conf) < 0) return -1;
                }
        }