From: Nick Porter Date: Mon, 6 Jun 2022 18:51:34 +0000 (+0100) Subject: LDAP library changes in preparation for LDAP sync (#4549) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=913178609f6982664c25bb6e8805d39aa6453069;p=thirdparty%2Ffreeradius-server.git LDAP library changes in preparation for LDAP sync (#4549) * 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 --- diff --git a/src/lib/ldap/all.mk.in b/src/lib/ldap/all.mk.in index 22d057746c2..e7598721216 100644 --- a/src/lib/ldap/all.mk.in +++ b/src/lib/ldap/all.mk.in @@ -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@ diff --git a/src/lib/ldap/base.h b/src/lib/ldap/base.h index 7608c9c83c0..1fd0f78d19e 100644 --- a/src/lib/ldap/base.h +++ b/src/lib/ldap/base.h @@ -14,6 +14,7 @@ #include #include #include +#include #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; //! +#include + +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 index 00000000000..3b63ef9b976 --- /dev/null +++ b/src/lib/ldap/conf.h @@ -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 + +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 } diff --git a/src/lib/ldap/directory.c b/src/lib/ldap/directory.c index d31de1d58b1..4b41b1fca6c 100644 --- a/src/lib/ldap/directory.c +++ b/src/lib/ldap/directory.c @@ -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 index 00000000000..96ff71fec10 --- /dev/null +++ b/src/lib/ldap/filter.c @@ -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 + +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); +} diff --git a/src/lib/ldap/util.c b/src/lib/ldap/util.c index 05e3ac5ba83..4ef649038a6 100644 --- a/src/lib/ldap/util.c +++ b/src/lib/ldap/util.c @@ -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 [:] or " + "an ldap URI (ldap|cldap|ldaps|ldapi)://:"); + 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; +} diff --git a/src/modules/rlm_ldap/rlm_ldap.c b/src/modules/rlm_ldap/rlm_ldap.c index 63da9c444fd..8d54885a301 100644 --- a/src/modules/rlm_ldap/rlm_ldap.c +++ b/src/modules/rlm_ldap/rlm_ldap.c @@ -35,6 +35,7 @@ USES_APPLE_DEPRECATED_API #include #include "rlm_ldap.h" +#include #include #include @@ -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 [:] or " - "an ldap URI (ldap|cldap|ldaps|ldapi)://:"); - 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; } }