diff options
Diffstat (limited to 'src/modules/rlm_ldap/rlm_ldap.c')
-rw-r--r-- | src/modules/rlm_ldap/rlm_ldap.c | 1977 |
1 files changed, 1977 insertions, 0 deletions
diff --git a/src/modules/rlm_ldap/rlm_ldap.c b/src/modules/rlm_ldap/rlm_ldap.c new file mode 100644 index 0000000..33e5a88 --- /dev/null +++ b/src/modules/rlm_ldap/rlm_ldap.c @@ -0,0 +1,1977 @@ +/* + * 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 rlm_ldap.c + * @brief LDAP authorization and authentication module. + * + * @author Arran Cudbard-Bell <a.cudbardb@freeradius.org> + * @author Alan DeKok <aland@freeradius.org> + * + * @copyright 2012,2015 Arran Cudbard-Bell <a.cudbardb@freeradius.org> + * @copyright 2013,2015 Network RADIUS SARL <info@networkradius.com> + * @copyright 2012 Alan DeKok <aland@freeradius.org> + * @copyright 1999-2013 The FreeRADIUS Server Project. + */ +RCSID("$Id$") + +#include <freeradius-devel/rad_assert.h> + +#include <stdarg.h> +#include <ctype.h> + +#include "ldap.h" + +/* + * Scopes + */ +FR_NAME_NUMBER const ldap_scope[] = { + { "sub", LDAP_SCOPE_SUB }, + { "one", LDAP_SCOPE_ONE }, + { "base", LDAP_SCOPE_BASE }, +#ifdef LDAP_SCOPE_CHILDREN + { "children", LDAP_SCOPE_CHILDREN }, +#endif + { NULL , -1 } +}; + +#ifdef LDAP_OPT_X_TLS_NEVER +FR_NAME_NUMBER const ldap_tls_require_cert[] = { + { "never", LDAP_OPT_X_TLS_NEVER }, + { "demand", LDAP_OPT_X_TLS_DEMAND }, + { "allow", LDAP_OPT_X_TLS_ALLOW }, + { "try", LDAP_OPT_X_TLS_TRY }, + { "hard", LDAP_OPT_X_TLS_HARD }, /* oh yes, just like that */ + + { NULL , -1 } +}; +#endif + +static FR_NAME_NUMBER const ldap_dereference[] = { + { "never", LDAP_DEREF_NEVER }, + { "searching", LDAP_DEREF_SEARCHING }, + { "finding", LDAP_DEREF_FINDING }, + { "always", LDAP_DEREF_ALWAYS }, + + { NULL , -1 } +}; + +static CONF_PARSER sasl_mech_dynamic[] = { + { "mech", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_TMPL | PW_TYPE_NOT_EMPTY, ldap_sasl_dynamic, mech), NULL }, + { "proxy", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_TMPL, ldap_sasl_dynamic, proxy), NULL }, + { "realm", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_TMPL, ldap_sasl_dynamic, realm), NULL }, + CONF_PARSER_TERMINATOR +}; + +static CONF_PARSER sasl_mech_static[] = { + { "mech", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_NOT_EMPTY, ldap_sasl, mech), NULL }, + { "proxy", FR_CONF_OFFSET(PW_TYPE_STRING, ldap_sasl, proxy), NULL }, + { "realm", FR_CONF_OFFSET(PW_TYPE_STRING, ldap_sasl, realm), NULL }, + CONF_PARSER_TERMINATOR +}; + +/* + * TLS Configuration + */ +static CONF_PARSER tls_config[] = { + /* + * Deprecated attributes + */ + { "cacertfile", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT | PW_TYPE_DEPRECATED, rlm_ldap_t, tls_ca_file), NULL }, + { "ca_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_ldap_t, tls_ca_file), NULL }, + + { "cacertdir", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT | PW_TYPE_DEPRECATED, rlm_ldap_t, tls_ca_path), NULL }, + { "ca_path", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_ldap_t, tls_ca_path), NULL }, + + { "certfile", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT | PW_TYPE_DEPRECATED, rlm_ldap_t, tls_certificate_file), NULL }, + { "certificate_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_ldap_t, tls_certificate_file), NULL }, + + { "keyfile", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT | PW_TYPE_DEPRECATED, rlm_ldap_t, tls_private_key_file), NULL }, // OK if it changes on HUP + { "private_key_file", FR_CONF_OFFSET(PW_TYPE_FILE_INPUT, rlm_ldap_t, tls_private_key_file), NULL }, // OK if it changes on HUP + + { "randfile", FR_CONF_OFFSET(PW_TYPE_FILE_EXISTS | PW_TYPE_DEPRECATED, rlm_ldap_t, tls_random_file), NULL }, + { "random_file", FR_CONF_OFFSET(PW_TYPE_FILE_EXISTS, rlm_ldap_t, tls_random_file), NULL }, + +#ifdef LDAP_OPT_X_TLS_PROTOCOL_MIN + { "tls_min_version", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, tls_min_version_str), NULL }, +#endif + + /* + * LDAP Specific TLS attributes + */ + { "start_tls", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_ldap_t, start_tls), "no" }, + { "require_cert", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, tls_require_cert_str), NULL }, + CONF_PARSER_TERMINATOR +}; + + +static CONF_PARSER profile_config[] = { + { "filter", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_TMPL, rlm_ldap_t, profile_filter), "(&)" }, //!< Correct filter for when the DN is known. + { "attribute", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, profile_attr), NULL }, + { "default", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_TMPL, rlm_ldap_t, default_profile), NULL }, + CONF_PARSER_TERMINATOR +}; + +/* + * User configuration + */ +static CONF_PARSER user_config[] = { + { "filter", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_TMPL, rlm_ldap_t, userobj_filter), NULL }, + { "scope", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, userobj_scope_str), "sub" }, + { "base_dn", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_TMPL, rlm_ldap_t, userobj_base_dn), "" }, + { "sort_by", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, userobj_sort_by), NULL }, + + { "access_attribute", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, userobj_access_attr), NULL }, + { "access_positive", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_ldap_t, access_positive), "yes" }, + + /* Should be deprecated */ + { "sasl", FR_CONF_OFFSET(PW_TYPE_SUBSECTION, rlm_ldap_t, user_sasl), (void const *) sasl_mech_dynamic }, + CONF_PARSER_TERMINATOR +}; + +/* + * Group configuration + */ +static CONF_PARSER group_config[] = { + { "filter", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, groupobj_filter), NULL }, + { "scope", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, groupobj_scope_str), "sub" }, + { "base_dn", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_TMPL, rlm_ldap_t, groupobj_base_dn), "" }, + + { "name_attribute", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, groupobj_name_attr), "cn" }, + { "membership_attribute", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, userobj_membership_attr), NULL }, + { "membership_filter", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, rlm_ldap_t, groupobj_membership_filter), NULL }, + { "cacheable_name", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_ldap_t, cacheable_group_name), "no" }, + { "cacheable_dn", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_ldap_t, cacheable_group_dn), "no" }, + { "cache_attribute", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, cache_attribute), NULL }, + { "allow_dangling_group_ref", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_ldap_t, allow_dangling_group_refs), "no" }, + CONF_PARSER_TERMINATOR +}; + +static CONF_PARSER client_config[] = { + { "filter", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, clientobj_filter), NULL }, + { "scope", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, clientobj_scope_str), "sub" }, + { "base_dn", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, clientobj_base_dn), "" }, + CONF_PARSER_TERMINATOR +}; + +/* + * Reference for accounting updates + */ +static const CONF_PARSER acct_section_config[] = { + { "reference", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_XLAT, ldap_acct_section_t, reference), "." }, + 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[] = { + /* + * Debugging flags to the server + */ + { "ldap_debug", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_ldap_t, ldap_debug), "0x0000" }, + + { "dereference", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, dereference_str), NULL }, + + { "chase_referrals", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_ldap_t, chase_referrals), NULL }, + + { "rebind", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_ldap_t, rebind), NULL }, + + { "sasl_secprops", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, sasl_secprops), NULL }, + +#ifdef LDAP_OPT_NETWORK_TIMEOUT + /* timeout on network activity */ + { "net_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_ldap_t, net_timeout), "10" }, +#endif + + /* timeout for search results */ + { "res_timeout", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_ldap_t, res_timeout), "20" }, + + /* allow server unlimited time for search (server-side limit) */ + { "srv_timelimit", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_ldap_t, srv_timelimit), "20" }, + +#ifdef LDAP_OPT_X_KEEPALIVE_IDLE + { "idle", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_ldap_t, keepalive_idle), "60" }, +#endif +#ifdef LDAP_OPT_X_KEEPALIVE_PROBES + { "probes", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_ldap_t, keepalive_probes), "3" }, +#endif +#ifdef LDAP_OPT_X_KEEPALIVE_INTERVAL + { "interval", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_ldap_t, keepalive_interval), "30" }, +#endif + CONF_PARSER_TERMINATOR +}; + + +static const CONF_PARSER module_config[] = { + { "server", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_MULTI, rlm_ldap_t, config_server), NULL }, /* Do not set to required */ + { "port", FR_CONF_OFFSET(PW_TYPE_SHORT, rlm_ldap_t, port), NULL }, + + { "identity", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, admin_identity), NULL }, + { "password", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, rlm_ldap_t, admin_password), NULL }, + + { "sasl", FR_CONF_OFFSET(PW_TYPE_SUBSECTION, rlm_ldap_t, admin_sasl), (void const *) sasl_mech_static }, + + { "valuepair_attribute", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, valuepair_attr), NULL }, + + { "user_dn", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_ldap_t, user_dn), NULL }, + +#ifdef WITH_EDIR + /* support for eDirectory Universal Password */ + { "edir", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_ldap_t, edir), NULL }, /* NULL defaults to "no" */ + + /* + * Attempt to bind with the cleartext password we got from eDirectory + * Universal password for additional authorization checks. + */ + { "edir_autz", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_ldap_t, edir_autz), NULL }, /* NULL defaults to "no" */ +#endif + + { "read_clients", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_ldap_t, do_clients), NULL }, /* NULL defaults to "no" */ + + { "user", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) user_config }, + + { "group", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) group_config }, + + { "client", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) client_config }, + + { "profile", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) profile_config }, + + { "options", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) option_config }, + + { "tls", FR_CONF_POINTER(PW_TYPE_SUBSECTION, NULL), (void const *) tls_config }, + CONF_PARSER_TERMINATOR +}; + +static ssize_t ldapquote_xlat(UNUSED void *instance, REQUEST *request, char const *fmt, char *out, size_t freespace) +{ + return rlm_ldap_escape_func(request, out, freespace, fmt, NULL); +} + +/** Expand an LDAP URL into a query, and return a string result from that query. + * + */ +static ssize_t ldap_xlat(void *instance, REQUEST *request, char const *fmt, char *out, size_t freespace) +{ + ldap_rcode_t status; + size_t len = 0; + rlm_ldap_t *inst = instance; + + LDAPURLDesc *ldap_url; + LDAPMessage *result = NULL; + LDAPMessage *entry = NULL; + + struct berval **values; + + ldap_handle_t *conn; + int ldap_errno; + + char const *url; + char const **attrs; + + url = fmt; + + if (!ldap_is_ldap_url(url)) { + REDEBUG("String passed does not look like an LDAP URL"); + return -1; + } + + if (ldap_url_parse(url, &ldap_url)){ + REDEBUG("Parsing LDAP URL failed"); + return -1; + } + + /* + * Nothing, empty string, "*" string, or got 2 things, die. + */ + if (!ldap_url->lud_attrs || !ldap_url->lud_attrs[0] || + !*ldap_url->lud_attrs[0] || + (strcmp(ldap_url->lud_attrs[0], "*") == 0) || + ldap_url->lud_attrs[1]) { + REDEBUG("Bad attributes list in LDAP URL. URL must specify exactly one attribute to retrieve"); + + goto free_urldesc; + } + + conn = mod_conn_get(inst, request); + if (!conn) goto free_urldesc; + + memcpy(&attrs, &ldap_url->lud_attrs, sizeof(attrs)); + + status = rlm_ldap_search(&result, inst, request, &conn, ldap_url->lud_dn, ldap_url->lud_scope, + ldap_url->lud_filter, attrs, NULL, NULL); + switch (status) { + case LDAP_PROC_SUCCESS: + break; + + default: + goto free_socket; + } + + rad_assert(conn); + rad_assert(result); + + entry = ldap_first_entry(conn->handle, result); + if (!entry) { + ldap_get_option(conn->handle, LDAP_OPT_RESULT_CODE, &ldap_errno); + REDEBUG("Failed retrieving entry: %s", ldap_err2string(ldap_errno)); + len = -1; + goto free_result; + } + + values = ldap_get_values_len(conn->handle, entry, ldap_url->lud_attrs[0]); + if (!values) { + RDEBUG("No \"%s\" attributes found in specified object", ldap_url->lud_attrs[0]); + goto free_result; + } + + if (values[0]->bv_len >= freespace) goto free_values; + + memcpy(out, values[0]->bv_val, values[0]->bv_len + 1); /* +1 as strlcpy expects buffer size */ + len = values[0]->bv_len; + +free_values: + ldap_value_free_len(values); +free_result: + ldap_msgfree(result); +free_socket: + mod_conn_release(inst, conn); +free_urldesc: + ldap_free_urldesc(ldap_url); + + return len; +} + +/** Perform LDAP-Group comparison checking + * + * Attempts to match users to groups using a variety of methods. + * + * @param instance of the rlm_ldap module. + * @param request Current request. + * @param thing Unknown. + * @param check Which group to check for user membership. + * @param check_pairs Unknown. + * @param reply_pairs Unknown. + * @return + * - 1 on failure (or if the user is not a member). + * - 0 on success. + */ +static int rlm_ldap_groupcmp(void *instance, REQUEST *request, UNUSED VALUE_PAIR *thing, VALUE_PAIR *check, + UNUSED VALUE_PAIR *check_pairs, UNUSED VALUE_PAIR **reply_pairs) +{ + rlm_ldap_t *inst = instance; + rlm_rcode_t rcode; + + bool found = false; + bool check_is_dn; + + ldap_handle_t *conn = NULL; + char const *user_dn; + + rad_assert(inst->groupobj_base_dn); + + RDEBUG("Searching for user in group \"%s\"", check->vp_strvalue); + + if (check->vp_length == 0) { + REDEBUG("Cannot do comparison (group name is empty)"); + return 1; + } + + /* + * Check if we can do cached membership verification + */ + check_is_dn = rlm_ldap_is_dn(check->vp_strvalue, check->vp_length); + if (check_is_dn) { + char *norm; + + MEM(norm = talloc_memdup(check, check->vp_strvalue, talloc_array_length(check->vp_strvalue))); + rlm_ldap_normalise_dn(norm, check->vp_strvalue); + fr_pair_value_strsteal(check, norm); + } + if ((check_is_dn && inst->cacheable_group_dn) || (!check_is_dn && inst->cacheable_group_name)) { + switch (rlm_ldap_check_cached(inst, request, check)) { + case RLM_MODULE_NOTFOUND: + found = false; + goto finish; + + case RLM_MODULE_OK: + found = true; + goto finish; + /* + * Fallback to dynamic search on failure + */ + case RLM_MODULE_FAIL: + case RLM_MODULE_INVALID: + default: + break; + } + } + + conn = mod_conn_get(inst, request); + if (!conn) return 1; + + /* + * This is used in the default membership filter. + */ + user_dn = rlm_ldap_find_user(inst, request, &conn, NULL, false, NULL, &rcode); + if (!user_dn) { + mod_conn_release(inst, conn); + return 1; + } + + rad_assert(conn); + + /* + * Check groupobj user membership + */ + if (inst->groupobj_membership_filter) { + switch (rlm_ldap_check_groupobj_dynamic(inst, request, &conn, check)) { + case RLM_MODULE_NOTFOUND: + break; + + case RLM_MODULE_OK: + found = true; + + default: + goto finish; + } + } + + rad_assert(conn); + + /* + * Check userobj group membership + */ + if (inst->userobj_membership_attr) { + switch (rlm_ldap_check_userobj_dynamic(inst, request, &conn, user_dn, check)) { + case RLM_MODULE_NOTFOUND: + break; + + case RLM_MODULE_OK: + found = true; + + default: + goto finish; + } + } + + rad_assert(conn); + +finish: + if (conn) mod_conn_release(inst, conn); + + if (!found) { + RDEBUG("User is not a member of \"%s\"", check->vp_strvalue); + + return 1; + } + + return 0; +} + +/** Detach from the LDAP server and cleanup internal state. + * + */ +static int mod_detach(void *instance) +{ + rlm_ldap_t *inst = instance; + + fr_connection_pool_free(inst->pool); + + if (inst->user_map) { + talloc_free(inst->user_map); + } + + /* + * Keeping the dummy ld around for the lifetime + * of the module should always work, + * irrespective of what changes happen in libldap. + */ + if (inst->handle) { +#ifdef HAVE_LDAP_UNBIND_EXT_S + ldap_unbind_ext_s(inst->handle, NULL, NULL); +#else + ldap_unbind_s(inst->handle); +#endif + } + +#ifdef HAVE_LDAP_CREATE_SORT_CONTROL + if (inst->userobj_sort_ctrl) ldap_control_free(inst->userobj_sort_ctrl); +#endif + + return 0; +} + +/** Parse an accounting sub section. + * + * Allocate a new ldap_acct_section_t and write the config data into it. + * + * @param[in] inst rlm_ldap configuration. + * @param[in] parent of the config section. + * @param[out] config to write the sub section parameters to. + * @param[in] comp The section name were parsing the config for. + * @return + * - 0 on success. + * - < 0 on failure. + */ +static int parse_sub_section(rlm_ldap_t *inst, CONF_SECTION *parent, ldap_acct_section_t **config, + rlm_components_t comp) +{ + CONF_SECTION *cs; + + char const *name = section_type_value[comp].section; + + cs = cf_section_sub_find(parent, name); + if (!cs) { + DEBUG2("rlm_ldap (%s): Couldn't find configuration for %s, will return NOOP for calls " + "from this section", inst->name, name); + + return 0; + } + + *config = talloc_zero(inst, ldap_acct_section_t); + if (cf_section_parse(cs, *config, acct_section_config) < 0) { + LDAP_ERR("Failed parsing configuration for section %s", name); + + return -1; + } + + (*config)->cs = cs; + + return 0; +} + +/** Bootstrap the module + * + * Define attributes. + * + * @param conf to parse. + * @param instance configuration data. + * @return + * - 0 on success. + * - < 0 on failure. + */ +static int mod_bootstrap(CONF_SECTION *conf, void *instance) +{ + rlm_ldap_t *inst = instance; + + inst->name = cf_section_name2(conf); + if (!inst->name) { + inst->name = cf_section_name1(conf); + } + + /* + * Group comparison checks. + */ + if (cf_section_name2(conf)) { + char buffer[256]; + + snprintf(buffer, sizeof(buffer), "%s-LDAP-Group", inst->name); + + if (paircompare_register_byname(buffer, dict_attrbyvalue(PW_USER_NAME, 0), false, rlm_ldap_groupcmp, inst) < 0) { + LDAP_ERR("Error registering group comparison: %s", fr_strerror()); + goto error; + } + + inst->group_da = dict_attrbyname(buffer); + + /* + * We're the default instance + */ + } else { + if (paircompare_register_byname("LDAP-Group", dict_attrbyvalue(PW_USER_NAME, 0), + false, rlm_ldap_groupcmp, inst) < 0) { + LDAP_ERR("Error registering group comparison: %s", fr_strerror()); + goto error; + } + + inst->group_da = dict_attrbyname("LDAP-Group"); + } + + /* + * Setup the cache attribute + */ + if (inst->cache_attribute) { + ATTR_FLAGS flags; + + memset(&flags, 0, sizeof(flags)); + if (dict_addattr(inst->cache_attribute, -1, 0, PW_TYPE_STRING, flags) < 0) { + LDAP_ERR("Error creating cache attribute: %s", fr_strerror()); + error: + return -1; + + } + inst->cache_da = dict_attrbyname(inst->cache_attribute); + } else { + inst->cache_da = inst->group_da; /* Default to the group_da */ + } + + if (!inst->user_dn || !*inst->user_dn) { + inst->user_dn = talloc_strdup(inst, "LDAP-UserDn"); + } + + /* + * Check or create the LDAP-UserDn attribute. + */ + if (inst->user_dn) { + ATTR_FLAGS flags; + + memset(&flags, 0, sizeof(flags)); + if (dict_addattr(inst->user_dn, -1, 0, PW_TYPE_STRING, flags) < 0) { + LDAP_ERR("Error creating %s attribute: %s", inst->user_dn, fr_strerror()); + return -1; + } + inst->user_dn_da = dict_attrbyname(inst->user_dn); + } + + xlat_register(inst->name, ldap_xlat, rlm_ldap_escape_func, inst); + xlat_register("ldapquote", ldapquote_xlat, NULL, inst); + + return 0; +} + + +/** Instantiate the module + * + * Creates a new instance of the module reading parameters from a configuration section. + * + * @param conf to parse. + * @param instance configuration data. + * @return + * - 0 on success. + * - < 0 on failure. + */ +static int mod_instantiate(CONF_SECTION *conf, void *instance) +{ + static bool version_done; + + CONF_PAIR *cp; + CONF_ITEM *ci; + + CONF_SECTION *options, *update; + rlm_ldap_t *inst = instance; + + inst->cs = conf; + + options = cf_section_sub_find(conf, "options"); + if (!options || !cf_pair_find(options, "chase_referrals")) { + inst->chase_referrals_unset = true; /* use OpenLDAP defaults */ + } + + /* + * Only needs to be done once, prevents races in environment + * initialisation within libldap. + * + * See: https://github.com/arr2036/ldapperf/issues/2 + */ +#ifdef HAVE_LDAP_INITIALIZE + ldap_initialize(&inst->handle, ""); +#else + inst->handle = ldap_init("", 0); +#endif + + /* + * Get version info from the LDAP API. + */ + if (!version_done) { + static LDAPAPIInfo info = { .ldapai_info_version = LDAP_API_INFO_VERSION }; /* static to quiet valgrind about this being uninitialised */ + int ldap_errno; + + version_done = true; + + ldap_errno = ldap_get_option(NULL, LDAP_OPT_API_INFO, &info); + if (ldap_errno == LDAP_OPT_SUCCESS) { + int i; + + /* + * Don't generate warnings if the compile type vendor name + * is found within the link time vendor name. + * + * This allows the server to be built against OpenLDAP but + * run with Symas OpenLDAP. + */ + if (strcasestr(info.ldapai_vendor_name, LDAP_VENDOR_NAME) == NULL) { + WARN("rlm_ldap: libldap vendor changed since the server was built"); + WARN("rlm_ldap: linked: %s, built: %s", info.ldapai_vendor_name, LDAP_VENDOR_NAME); + } + + if (info.ldapai_vendor_version < LDAP_VENDOR_VERSION) { + WARN("rlm_ldap: libldap older than the version the server was built against"); + WARN("rlm_ldap: linked: %i, built: %i", + info.ldapai_vendor_version, LDAP_VENDOR_VERSION); + } + + INFO("rlm_ldap: libldap vendor: %s, version: %i", info.ldapai_vendor_name, + info.ldapai_vendor_version); + + if (info.ldapai_extensions != NULL ) { + for ( i = 0; info.ldapai_extensions[i] != NULL; i++) { + ldap_memfree(info.ldapai_extensions[i]); + } + ldap_memfree(info.ldapai_extensions); + } + ldap_memfree(info.ldapai_vendor_name); + } else { + DEBUG("rlm_ldap: Falling back to build time libldap version info. Query for LDAP_OPT_API_INFO " + "returned: %i", ldap_errno); + INFO("rlm_ldap: libldap vendor: %s, version: %i.%i.%i", LDAP_VENDOR_NAME, + LDAP_VENDOR_VERSION_MAJOR, LDAP_VENDOR_VERSION_MINOR, LDAP_VENDOR_VERSION_PATCH); + } + } + + /* + * If the configuration parameters can't be parsed, then fail. + */ + if ((parse_sub_section(inst, conf, &inst->accounting, MOD_ACCOUNTING) < 0) || + (parse_sub_section(inst, conf, &inst->postauth, MOD_POST_AUTH) < 0)) { + cf_log_err_cs(conf, "Failed parsing configuration"); + + goto error; + } + + /* + * Sanity checks for cacheable groups code. + */ + if (inst->cacheable_group_name && inst->groupobj_membership_filter) { + if (!inst->groupobj_name_attr) { + cf_log_err_cs(conf, "Configuration item 'group.name_attribute' must be set if cacheable " + "group names are enabled"); + + goto error; + } + } + + /* + * If we have a *pair* as opposed to a *section* + * then the module is referencing another ldap module's + * connection pool. + */ + if (!cf_pair_find(conf, "pool")) { + if (!inst->config_server) { + cf_log_err_cs(conf, "Configuration item 'server' must have a value"); + goto error; + } + } + +#ifndef WITH_SASL + if (inst->user_sasl.mech) { + cf_log_err_cs(conf, "Configuration item 'user.sasl.mech' not supported. " + "Linked libldap does not provide ldap_sasl_bind function"); + goto error; + } + + if (inst->admin_sasl.mech) { + cf_log_err_cs(conf, "Configuration item 'sasl.mech' not supported. " + "Linked libldap does not provide ldap_sasl_interactive_bind function"); + goto error; + } +#endif + +#ifndef HAVE_LDAP_CREATE_SORT_CONTROL + if (inst->userobj_sort_by) { + cf_log_err_cs(conf, "Configuration item 'sort_by' not supported. " + "Linked libldap does not provide ldap_create_sort_control function"); + goto error; + } +#endif + + /* + * For backwards compatibility hack up the first 'server' + * CONF_ITEM into chunks, and add them back into the config. + * + * @fixme this should be removed at some point. + */ + if (inst->config_server) { + char const *value; + char const *p; + char const *q; + char *buff; + + bool done = false; + bool first = true; + + cp = cf_pair_find(conf, "server"); + if (!cp) { + cf_log_err_cs(conf, "Configuration item 'server' must have a value"); + return -1; + } + + value = cf_pair_value(cp); + + p = value; + q = p; + while (!done) { + switch (*q) { + case '\0': + done = true; + if (p == value) break; /* string contained no separators */ + + /* FALL-THROUGH */ + + case ',': + case ';': + case ' ': + while (isspace((int) *p)) p++; + if (p == q) continue; + + buff = talloc_array(inst, char, (q - p) + 1); + strlcpy(buff, p, talloc_array_length(buff)); + p = ++q; + + if (first) { + WARN("Listing multiple LDAP servers in the 'server' configuration item " + "is deprecated and will be removed in a future release. " + "Use multiple 'server' configuration items instead"); + WARN("- server = '%s'", value); + } + WARN("+ server = '%s'", buff); + + /* + * For the first instance of server we find, just replace + * the existing "server" config item. + */ + if (first) { + cf_pair_replace(conf, cp, buff); + first = false; + continue; + } + + /* + * For subsequent instances we need to add new conf pairs. + */ + cp = cf_pair_alloc(conf, "server", buff, T_OP_EQ, T_BARE_WORD, T_SINGLE_QUOTED_STRING); + if (!cp) return -1; + + ci = cf_pair_to_item(cp); + cf_item_add(conf, ci); + + break; + + default: + q++; + continue; + } + } + } + + /* + * Now iterate over all the 'server' config items + */ + if (!inst->server) inst->server = talloc_strdup(inst, ""); + for (cp = cf_pair_find(conf, "server"); + cp; + cp = cf_pair_find_next(conf, cp, "server")) { + char const *value; + + value = cf_pair_value(cp); + +#ifdef LDAP_CAN_PARSE_URLS + /* + * Split original server value out into URI, server and port + * so whatever initialization function we use later will have + * 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; + + if (ldap_url_parse(value, &ldap_url)){ + cf_log_err_cs(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_cs(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_cs(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_cs(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; + } + + /* We allow extensions */ + +# ifdef HAVE_LDAP_INITIALIZE + { + char *url; + + /* + * Figure out the default port from the URL + */ + if (ldap_url->lud_scheme) { + if (strcmp(ldap_url->lud_scheme, "ldaps") == 0) { + if (inst->start_tls == true) { + cf_log_err_cs(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->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_cs(conf, "Failed recombining URL components"); + goto ldap_url_error; + } + inst->server = talloc_asprintf_append(inst->server, "%s ", url); + free(url); + } +# else + /* + * No LDAP initialize function. Can't specify a scheme. + */ + if (ldap_url->lud_scheme && + ((strcmp(ldap_url->lud_scheme, "ldaps") == 0) || + (strcmp(ldap_url->lud_scheme, "ldapi") == 0) || + (strcmp(ldap_url->lud_scheme, "cldap") == 0))) { + cf_log_err_cs(conf, "%s is not supported by linked libldap", + ldap_url->lud_scheme); + return -1; + } + + /* + * URL port over-rides the configured + * port. But if there's no configured + * port, we use the hard-coded default. + */ + if (set_port_maybe) { + ldap_url->lud_port = inst->port; + if (!ldap_url->lud_port) ldap_url->lud_port = default_port; + } + + inst->server = talloc_asprintf_append(inst->server, "%s:%i ", + ldap_url->lud_host ? ldap_url->lud_host : "localhost", + ldap_url->lud_port); +# endif + /* + * @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 + */ + } else +#endif /* HAVE_LDAP_URL_PARSE && HAVE_LDAP_IS_LDAP_URL && LDAP_URL_DESC2STR */ + /* + * 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. + */ + { +#ifdef HAVE_LDAP_INITIALIZE + char const *p; + char *q; + int port = 0; + size_t len; + + port = inst->port; + + /* + * We don't support URLs if the library didn't provide + * URL parsing functions. + */ + if (strchr(value, '/')) { + bad_server_fmt: +#ifdef LDAP_CAN_PARSE_URLS + cf_log_err_cp(cp, "Invalid server value, must be in format <server>[:<port>] or " + "an ldap URI (ldap|cldap|ldaps|ldapi)://<server>:<port>"); +#else + cf_log_err_cp(cp, "Invalid server value, must be in format <server>[:<port>]"); +#endif + 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->server = talloc_asprintf_append(inst->server, "ldap://%.*s:%i ", (int) len, value, port); +#else + /* + * ldap_init takes port, which can be overridden by :port so + * we don't need to do any parsing here. + */ + inst->server = talloc_asprintf_append(inst->server, "%s ", value); +#endif + } + } + if (inst->server) inst->server[talloc_array_length(inst->server) - 2] = '\0'; + + DEBUG4("LDAP server string: %s", inst->server); + +#ifdef LDAP_OPT_X_TLS_NEVER + /* + * Workaround for servers which support LDAPS but not START TLS + */ + if (inst->port == LDAPS_PORT || inst->tls_mode) { + inst->tls_mode = LDAP_OPT_X_TLS_HARD; + } else { + inst->tls_mode = 0; + } +#endif + + /* + * Convert dereference strings to enumerated constants + */ + if (inst->dereference_str) { + inst->dereference = fr_str2int(ldap_dereference, inst->dereference_str, -1); + if (inst->dereference < 0) { + cf_log_err_cs(conf, "Invalid 'dereference' value \"%s\", expected 'never', 'searching', " + "'finding' or 'always'", inst->dereference_str); + goto error; + } + } + +#if LDAP_SET_REBIND_PROC_ARGS != 3 + /* + * The 2-argument rebind doesn't take an instance variable. Our rebind function needs the instance + * variable for the username, password, etc. + */ + if (inst->rebind == true) { + cf_log_err_cs(conf, "Cannot use 'rebind' configuration item as this version of libldap " + "does not support the API that we need"); + + goto error; + } +#endif + + /* + * Convert scope strings to enumerated constants + */ + inst->userobj_scope = fr_str2int(ldap_scope, inst->userobj_scope_str, -1); + if (inst->userobj_scope < 0) { + cf_log_err_cs(conf, "Invalid 'user.scope' value \"%s\", expected 'sub', 'one'" +#ifdef LDAP_SCOPE_CHILDREN + ", 'base' or 'children'" +#else + " or 'base'" +#endif + , inst->userobj_scope_str); + goto error; + } + + inst->groupobj_scope = fr_str2int(ldap_scope, inst->groupobj_scope_str, -1); + if (inst->groupobj_scope < 0) { + cf_log_err_cs(conf, "Invalid 'group.scope' value \"%s\", expected 'sub', 'one'" +#ifdef LDAP_SCOPE_CHILDREN + ", 'base' or 'children'" +#else + " or 'base'" +#endif + , inst->groupobj_scope_str); + goto error; + } + + inst->clientobj_scope = fr_str2int(ldap_scope, inst->clientobj_scope_str, -1); + if (inst->clientobj_scope < 0) { + cf_log_err_cs(conf, "Invalid 'client.scope' value \"%s\", expected 'sub', 'one'" +#ifdef LDAP_SCOPE_CHILDREN + ", 'base' or 'children'" +#else + " or 'base'" +#endif + , inst->clientobj_scope_str); + goto error; + } + +#ifdef HAVE_LDAP_CREATE_SORT_CONTROL + /* + * Build the server side sort control for user objects + */ + if (inst->userobj_sort_by) { + LDAPSortKey **keys; + int ret; + char *p; + + memcpy(&p, &inst->userobj_sort_by, sizeof(p)); + + ret = ldap_create_sort_keylist(&keys, p); + if (ret != LDAP_SUCCESS) { + cf_log_err_cs(conf, "Invalid user.sort_by value \"%s\": %s", + inst->userobj_sort_by, ldap_err2string(ret)); + goto error; + } + + /* + * Always set the control as critical, if it's not needed + * the user can comment it out... + */ + ret = ldap_create_sort_control(inst->handle, keys, 1, &inst->userobj_sort_ctrl); + ldap_free_sort_keylist(keys); + if (ret != LDAP_SUCCESS) { + LDAP_ERR("Failed creating server sort control: %s", ldap_err2string(ret)); + goto error; + } + } +#endif + + if (inst->tls_require_cert_str) { +#ifdef LDAP_OPT_X_TLS_NEVER + /* + * Convert cert strictness to enumerated constants + */ + inst->tls_require_cert = fr_str2int(ldap_tls_require_cert, inst->tls_require_cert_str, -1); + if (inst->tls_require_cert < 0) { + cf_log_err_cs(conf, "Invalid 'tls.require_cert' value \"%s\", expected 'never', " + "'demand', 'allow', 'try' or 'hard'", inst->tls_require_cert_str); + goto error; + } +#else + cf_log_err_cs(conf, "Modifying 'tls.require_cert' is not supported by current " + "version of libldap. Please upgrade or substitute current libldap and " + "rebuild this module"); + + goto error; +#endif + } + + if (inst->tls_min_version_str) { +#ifdef LDAP_OPT_X_TLS_PROTOCOL_MIN + if (strcmp(inst->tls_min_version_str, "1.2") == 0) { + inst->tls_min_version = LDAP_OPT_X_TLS_PROTOCOL_TLS1_2; + + } else if (strcmp(inst->tls_min_version_str, "1.1") == 0) { + inst->tls_min_version = LDAP_OPT_X_TLS_PROTOCOL_TLS1_1; + + } else if (strcmp(inst->tls_min_version_str, "1.0") == 0) { + inst->tls_min_version = LDAP_OPT_X_TLS_PROTOCOL_TLS1_0; + + } else { + cf_log_err_cs(conf, "Invalid 'tls.tls_min_version' value \"%s\"", inst->tls_min_version_str); + goto error; + } +#else + cf_log_err_cs(conf, "This version of libldap does not support tls.tls_min_version." + " Please upgrade or substitute current libldap and " + "rebuild this module"); + goto error; + +#endif + } + + /* + * Build the attribute map + */ + update = cf_section_sub_find(inst->cs, "update"); + if (update && (map_afrom_cs(&inst->user_map, update, + PAIR_LIST_REPLY, PAIR_LIST_REQUEST, rlm_ldap_map_verify, inst, + LDAP_MAX_ATTRMAP) < 0)) { + return -1; + } + + /* + * Set global options + */ + if (rlm_ldap_global_init(inst) < 0) goto error; + + /* + * Initialize the socket pool. + */ + inst->pool = fr_connection_pool_module_init(inst->cs, inst, mod_conn_create, NULL, NULL); + if (!inst->pool) goto error; + + /* + * Bulk load dynamic clients. + */ + if (inst->do_clients) { + CONF_SECTION *cs, *map, *tmpl; + + cs = cf_section_sub_find(inst->cs, "client"); + if (!cs) { + cf_log_err_cs(conf, "Told to load clients but no client section found"); + goto error; + } + + map = cf_section_sub_find(cs, "attribute"); + if (!map) { + cf_log_err_cs(cs, "Told to load clients but no attribute section found"); + goto error; + } + + tmpl = cf_section_sub_find(cs, "template"); + + if (rlm_ldap_client_load(inst, tmpl, map) < 0) { + cf_log_err_cs(cs, "Error loading clients"); + + return -1; + } + } + + return 0; + +error: + return -1; +} + +static rlm_rcode_t mod_authenticate(void *instance, REQUEST *request) CC_HINT(nonnull); +static rlm_rcode_t CC_HINT(nonnull) mod_authenticate(void *instance, REQUEST *request) +{ + rlm_rcode_t rcode; + ldap_rcode_t status; + char const *dn; + rlm_ldap_t *inst = instance; + ldap_handle_t *conn; + + char sasl_mech_buff[LDAP_MAX_DN_STR_LEN]; + char sasl_proxy_buff[LDAP_MAX_DN_STR_LEN]; + char sasl_realm_buff[LDAP_MAX_DN_STR_LEN]; + ldap_sasl sasl; + + /* + * Ensure that we're being passed a plain-text password, and not + * anything else. + */ + + if (!request->username) { + REDEBUG("Attribute \"User-Name\" is required for authentication"); + + return RLM_MODULE_INVALID; + } + + if (!request->password || + (request->password->da->attr != PW_USER_PASSWORD)) { + RWDEBUG("You have set \"Auth-Type := LDAP\" somewhere"); + RWDEBUG("*********************************************"); + RWDEBUG("* THAT CONFIGURATION IS WRONG. DELETE IT. "); + RWDEBUG("* YOU ARE PREVENTING THE SERVER FROM WORKING"); + RWDEBUG("*********************************************"); + + REDEBUG("Attribute \"User-Password\" is required for authentication"); + + return RLM_MODULE_INVALID; + } + + if (request->password->vp_length == 0) { + REDEBUG("Empty password supplied"); + + return RLM_MODULE_INVALID; + } + + conn = mod_conn_get(inst, request); + if (!conn) return RLM_MODULE_FAIL; + + /* + * Expand dynamic SASL fields + */ + if (conn->inst->user_sasl.mech) { + memset(&sasl, 0, sizeof(sasl)); + + if (tmpl_expand(&sasl.mech, sasl_mech_buff, sizeof(sasl_mech_buff), request, + conn->inst->user_sasl.mech, rlm_ldap_escape_func, inst) < 0) { + REDEBUG("Failed expanding user.sasl.mech: %s", fr_strerror()); + rcode = RLM_MODULE_FAIL; + goto finish; + } + + if (conn->inst->user_sasl.proxy) { + if (tmpl_expand(&sasl.proxy, sasl_proxy_buff, sizeof(sasl_proxy_buff), request, + conn->inst->user_sasl.proxy, rlm_ldap_escape_func, inst) < 0) { + REDEBUG("Failed expanding user.sasl.proxy: %s", fr_strerror()); + rcode = RLM_MODULE_FAIL; + goto finish; + } + } + + if (conn->inst->user_sasl.realm) { + if (tmpl_expand(&sasl.realm, sasl_realm_buff, sizeof(sasl_realm_buff), request, + conn->inst->user_sasl.realm, rlm_ldap_escape_func, inst) < 0) { + REDEBUG("Failed expanding user.sasl.realm: %s", fr_strerror()); + rcode = RLM_MODULE_FAIL; + goto finish; + } + } + } + + RDEBUG("Login attempt by \"%s\"", request->username->vp_strvalue); + + /* + * Get the DN by doing a search. + */ + dn = rlm_ldap_find_user(inst, request, &conn, NULL, false, NULL, &rcode); + if (!dn) { + mod_conn_release(inst, conn); + + return rcode; + } + conn->rebound = true; + status = rlm_ldap_bind(inst, request, &conn, dn, request->password->vp_strvalue, + conn->inst->user_sasl.mech ? &sasl : NULL, true); + switch (status) { + case LDAP_PROC_SUCCESS: + rcode = RLM_MODULE_OK; + RDEBUG("Bind as user \"%s\" was successful", dn); + break; + + case LDAP_PROC_NOT_PERMITTED: + rcode = RLM_MODULE_USERLOCK; + break; + + case LDAP_PROC_REJECT: + rcode = RLM_MODULE_REJECT; + break; + + case LDAP_PROC_BAD_DN: + rcode = RLM_MODULE_INVALID; + break; + + case LDAP_PROC_NO_RESULT: + rcode = RLM_MODULE_NOTFOUND; + break; + + default: + rcode = RLM_MODULE_FAIL; + break; + }; + +finish: + mod_conn_release(inst, conn); + + return rcode; +} + +/** Search for and apply an LDAP profile + * + * LDAP profiles are mapped using the same attribute map as user objects, they're used to add common sets of attributes + * to the request. + * + * @param[in] inst rlm_ldap configuration. + * @param[in] request Current request. + * @param[in,out] pconn to use. May change as this function calls functions which auto re-connect. + * @param[in] dn of profile object to apply. + * @param[in] expanded Structure containing a list of xlat expanded attribute names and mapping information. + * @return One of the RLM_MODULE_* values. + */ +static rlm_rcode_t rlm_ldap_map_profile(rlm_ldap_t const *inst, REQUEST *request, ldap_handle_t **pconn, + char const *dn, rlm_ldap_map_exp_t const *expanded) +{ + rlm_rcode_t rcode = RLM_MODULE_OK; + ldap_rcode_t status; + LDAPMessage *result = NULL, *entry = NULL; + int ldap_errno; + LDAP *handle = (*pconn)->handle; + char const *filter; + char filter_buff[LDAP_MAX_FILTER_STR_LEN]; + + rad_assert(inst->profile_filter); /* We always have a default filter set */ + + if (!dn || !*dn) return RLM_MODULE_OK; + + if (tmpl_expand(&filter, filter_buff, sizeof(filter_buff), request, + inst->profile_filter, rlm_ldap_escape_func, NULL) < 0) { + REDEBUG("Failed creating profile filter"); + + return RLM_MODULE_INVALID; + } + + status = rlm_ldap_search(&result, inst, request, pconn, dn, + LDAP_SCOPE_BASE, filter, expanded->attrs, NULL, NULL); + switch (status) { + case LDAP_PROC_SUCCESS: + break; + + case LDAP_PROC_BAD_DN: + case LDAP_PROC_NO_RESULT: + RDEBUG("Profile object \"%s\" not found", dn); + return RLM_MODULE_NOTFOUND; + + default: + return RLM_MODULE_FAIL; + } + + rad_assert(*pconn); + rad_assert(result); + + entry = ldap_first_entry(handle, result); + if (!entry) { + ldap_get_option(handle, LDAP_OPT_RESULT_CODE, &ldap_errno); + REDEBUG("Failed retrieving entry: %s", ldap_err2string(ldap_errno)); + + rcode = RLM_MODULE_NOTFOUND; + + goto free_result; + } + + RDEBUG("Processing profile attributes"); + if (rlm_ldap_map_do(inst, request, handle, expanded, entry) > 0) rcode = RLM_MODULE_UPDATED; + +free_result: + ldap_msgfree(result); + + return rcode; +} + +static rlm_rcode_t mod_authorize(void *instance, REQUEST *request) CC_HINT(nonnull); +static rlm_rcode_t mod_authorize(void *instance, REQUEST *request) +{ + rlm_rcode_t rcode = RLM_MODULE_OK; + ldap_rcode_t status; + int ldap_errno; + int i; + rlm_ldap_t *inst = instance; + struct berval **values; + VALUE_PAIR *vp; + ldap_handle_t *conn; + LDAPMessage *result, *entry; + char const *dn = NULL; + rlm_ldap_map_exp_t expanded; /* faster than mallocing every time */ + + /* + * Don't be tempted to add a check for request->username + * or request->password here. rlm_ldap.authorize can be used for + * many things besides searching for users. + */ + + if (rlm_ldap_map_expand(&expanded, request, inst->user_map) < 0) return RLM_MODULE_FAIL; + + conn = mod_conn_get(inst, request); + if (!conn) return RLM_MODULE_FAIL; + + /* + * Add any additional attributes we need for checking access, memberships, and profiles + */ + if (inst->userobj_access_attr) { + expanded.attrs[expanded.count++] = inst->userobj_access_attr; + } + + if (inst->userobj_membership_attr && (inst->cacheable_group_dn || inst->cacheable_group_name)) { + expanded.attrs[expanded.count++] = inst->userobj_membership_attr; + } + + if (inst->profile_attr) { + expanded.attrs[expanded.count++] = inst->profile_attr; + } + + if (inst->valuepair_attr) { + expanded.attrs[expanded.count++] = inst->valuepair_attr; + } + + expanded.attrs[expanded.count] = NULL; + + dn = rlm_ldap_find_user(inst, request, &conn, expanded.attrs, true, &result, &rcode); + if (!dn) { + goto finish; + } + + entry = ldap_first_entry(conn->handle, result); + if (!entry) { + ldap_get_option(conn->handle, LDAP_OPT_RESULT_CODE, &ldap_errno); + REDEBUG("Failed retrieving entry: %s", ldap_err2string(ldap_errno)); + + goto finish; + } + + /* + * Check for access. + */ + if (inst->userobj_access_attr) { + rcode = rlm_ldap_check_access(inst, request, conn, entry); + if (rcode != RLM_MODULE_OK) { + goto finish; + } + } + + /* + * Check if we need to cache group memberships + */ + if (inst->cacheable_group_dn || inst->cacheable_group_name) { + if (inst->userobj_membership_attr) { + rcode = rlm_ldap_cacheable_userobj(inst, request, &conn, entry, inst->userobj_membership_attr); + if (rcode != RLM_MODULE_OK) { + goto finish; + } + } + + rcode = rlm_ldap_cacheable_groupobj(inst, request, &conn); + if (rcode != RLM_MODULE_OK) { + goto finish; + } + } + +#ifdef WITH_EDIR + /* + * We already have a Cleartext-Password. Skip edir. + */ + if (fr_pair_find_by_num(request->config, PW_CLEARTEXT_PASSWORD, 0, TAG_ANY)) { + goto skip_edir; + } + + /* + * Retrieve Universal Password if we use eDirectory + */ + if (inst->edir) { + int res = 0; + char password[256]; + size_t pass_size = sizeof(password); + + /* + * Retrive universal password + */ + res = nmasldap_get_password(conn->handle, dn, password, &pass_size); + if (res != 0) { + REDEBUG("Failed to retrieve eDirectory password: (%i) %s", res, edir_errstr(res)); + rcode = RLM_MODULE_FAIL; + + goto finish; + } + + /* + * Add Cleartext-Password attribute to the request + */ + vp = radius_pair_create(request, &request->config, PW_CLEARTEXT_PASSWORD, 0); + fr_pair_value_strcpy(vp, password); + vp->vp_length = pass_size; + + if (RDEBUG_ENABLED3) { + RDEBUG3("Added eDirectory password. control:%s += '%s'", vp->da->name, vp->vp_strvalue); + } else { + RDEBUG2("Added eDirectory password"); + } + + if (inst->edir_autz) { + RDEBUG2("Binding as user for eDirectory authorization checks"); + /* + * Bind as the user + */ + conn->rebound = true; + status = rlm_ldap_bind(inst, request, &conn, dn, vp->vp_strvalue, NULL, true); + switch (status) { + case LDAP_PROC_SUCCESS: + rcode = RLM_MODULE_OK; + RDEBUG("Bind as user '%s' was successful", dn); + break; + + case LDAP_PROC_NOT_PERMITTED: + rcode = RLM_MODULE_USERLOCK; + goto finish; + + case LDAP_PROC_REJECT: + rcode = RLM_MODULE_REJECT; + goto finish; + + case LDAP_PROC_BAD_DN: + rcode = RLM_MODULE_INVALID; + goto finish; + + case LDAP_PROC_NO_RESULT: + rcode = RLM_MODULE_NOTFOUND; + goto finish; + + default: + rcode = RLM_MODULE_FAIL; + goto finish; + }; + } + } + +skip_edir: +#endif + + /* + * Apply ONE user profile, or a default user profile. + */ + if (inst->default_profile) { + char const *profile; + char profile_buff[1024]; + + if (tmpl_expand(&profile, profile_buff, sizeof(profile_buff), + request, inst->default_profile, NULL, NULL) < 0) { + REDEBUG("Failed creating default profile string"); + + rcode = RLM_MODULE_INVALID; + goto finish; + } + + switch (rlm_ldap_map_profile(inst, request, &conn, profile, &expanded)) { + case RLM_MODULE_INVALID: + rcode = RLM_MODULE_INVALID; + goto finish; + + case RLM_MODULE_FAIL: + rcode = RLM_MODULE_FAIL; + goto finish; + + case RLM_MODULE_UPDATED: + rcode = RLM_MODULE_UPDATED; + /* FALL-THROUGH */ + default: + break; + } + } + + /* + * Apply a SET of user profiles. + */ + if (inst->profile_attr) { + values = ldap_get_values_len(conn->handle, entry, inst->profile_attr); + if (values != NULL) { + for (i = 0; values[i] != NULL; i++) { + rlm_rcode_t ret; + char *value; + + value = rlm_ldap_berval_to_string(request, values[i]); + ret = rlm_ldap_map_profile(inst, request, &conn, value, &expanded); + talloc_free(value); + if (ret == RLM_MODULE_FAIL) { + ldap_value_free_len(values); + rcode = ret; + goto finish; + } + + } + ldap_value_free_len(values); + } + } + + if (inst->user_map || inst->valuepair_attr) { + RDEBUG("Processing user attributes"); + if (rlm_ldap_map_do(inst, request, conn->handle, &expanded, entry) > 0) rcode = RLM_MODULE_UPDATED; + rlm_ldap_check_reply(inst, request); + } + +finish: + talloc_free(expanded.ctx); + if (result) ldap_msgfree(result); + mod_conn_release(inst, conn); + + return rcode; +} + +/** Modify user's object in LDAP + * + * Process a modifcation map to update a user object in the LDAP directory. + * + * @param inst rlm_ldap instance. + * @param request Current request. + * @param section that holds the map to process. + * @return one of the RLM_MODULE_* values. + */ +static rlm_rcode_t user_modify(rlm_ldap_t *inst, REQUEST *request, ldap_acct_section_t *section) +{ + rlm_rcode_t rcode = RLM_MODULE_OK; + ldap_rcode_t status; + + ldap_handle_t *conn = NULL; + + LDAPMod *mod_p[LDAP_MAX_ATTRMAP + 1], mod_s[LDAP_MAX_ATTRMAP]; + LDAPMod **modify = mod_p; + + char *passed[LDAP_MAX_ATTRMAP * 2]; + int i, total = 0, last_pass = 0; + + char *expanded[LDAP_MAX_ATTRMAP]; + int last_exp = 0; + + char const *attr; + char const *value; + + char const *dn; + /* + * Build our set of modifications using the update sections in + * the config. + */ + CONF_ITEM *ci; + CONF_PAIR *cp; + CONF_SECTION *cs; + FR_TOKEN op; + char path[MAX_STRING_LEN]; + + char *p = path; + + rad_assert(section); + + /* + * Locate the update section were going to be using + */ + if (section->reference[0] != '.') { + *p++ = '.'; + } + + if (radius_xlat(p, (sizeof(path) - (p - path)) - 1, request, section->reference, NULL, NULL) < 0) { + goto error; + } + + ci = cf_reference_item(NULL, section->cs, path); + if (!ci) { + goto error; + } + + if (!cf_item_is_section(ci)){ + REDEBUG("Reference must resolve to a section"); + + goto error; + } + + cs = cf_section_sub_find(cf_item_to_section(ci), "update"); + if (!cs) { + REDEBUG("Section must contain 'update' subsection"); + + goto error; + } + + /* + * Iterate over all the pairs, building our mods array + */ + for (ci = cf_item_find_next(cs, NULL); ci != NULL; ci = cf_item_find_next(cs, ci)) { + bool do_xlat = false; + + if (total == LDAP_MAX_ATTRMAP) { + REDEBUG("Modify map size exceeded"); + + goto error; + } + + if (!cf_item_is_pair(ci)) { + REDEBUG("Entry is not in \"ldap-attribute = value\" format"); + + goto error; + } + + /* + * Retrieve all the information we need about the pair + */ + cp = cf_item_to_pair(ci); + value = cf_pair_value(cp); + attr = cf_pair_attr(cp); + op = cf_pair_operator(cp); + + if (!value || (*value == '\0')) { + RDEBUG("Empty value string, skipping attribute \"%s\"", attr); + + continue; + } + + switch (cf_pair_value_type(cp)) { + case T_BARE_WORD: + case T_SINGLE_QUOTED_STRING: + break; + + case T_BACK_QUOTED_STRING: + case T_DOUBLE_QUOTED_STRING: + do_xlat = true; + break; + + default: + rad_assert(0); + goto error; + } + + if (op == T_OP_CMP_FALSE) { + passed[last_pass] = NULL; + } else if (do_xlat) { + char *exp = NULL; + + if (radius_axlat(&exp, request, value, NULL, NULL) <= 0) { + RDEBUG("Skipping attribute \"%s\"", attr); + + talloc_free(exp); + + continue; + } + + expanded[last_exp++] = exp; + passed[last_pass] = exp; + /* + * Static strings + */ + } else { + memcpy(&(passed[last_pass]), &value, sizeof(passed[last_pass])); + } + + passed[last_pass + 1] = NULL; + + mod_s[total].mod_values = &(passed[last_pass]); + + last_pass += 2; + + switch (op) { + /* + * T_OP_EQ is *NOT* supported, it is impossible to + * support because of the lack of transactions in LDAP + */ + case T_OP_ADD: + mod_s[total].mod_op = LDAP_MOD_ADD; + break; + + case T_OP_SET: + mod_s[total].mod_op = LDAP_MOD_REPLACE; + break; + + case T_OP_SUB: + case T_OP_CMP_FALSE: + mod_s[total].mod_op = LDAP_MOD_DELETE; + break; + +#ifdef LDAP_MOD_INCREMENT + case T_OP_INCRM: + mod_s[total].mod_op = LDAP_MOD_INCREMENT; + break; +#endif + default: + REDEBUG("Operator '%s' is not supported for LDAP modify operations", + fr_int2str(fr_tokens, op, "<INVALID>")); + + goto error; + } + + /* + * Now we know the value is ok, copy the pointers into + * the ldapmod struct. + */ + memcpy(&(mod_s[total].mod_type), &attr, sizeof(mod_s[total].mod_type)); + + mod_p[total] = &(mod_s[total]); + total++; + } + + if (total == 0) { + rcode = RLM_MODULE_NOOP; + goto release; + } + + mod_p[total] = NULL; + + conn = mod_conn_get(inst, request); + if (!conn) return RLM_MODULE_FAIL; + + + dn = rlm_ldap_find_user(inst, request, &conn, NULL, false, NULL, &rcode); + if (!dn || (rcode != RLM_MODULE_OK)) { + goto error; + } + + status = rlm_ldap_modify(inst, request, &conn, dn, modify); + switch (status) { + case LDAP_PROC_SUCCESS: + break; + + case LDAP_PROC_REJECT: + case LDAP_PROC_BAD_DN: + rcode = RLM_MODULE_INVALID; + break; + + default: + rcode = RLM_MODULE_FAIL; + break; + }; + + release: + error: + /* + * Free up any buffers we allocated for xlat expansion + */ + for (i = 0; i < last_exp; i++) { + talloc_free(expanded[i]); + } + + mod_conn_release(inst, conn); + + return rcode; +} + +static rlm_rcode_t mod_accounting(void *instance, REQUEST *request) CC_HINT(nonnull); +static rlm_rcode_t mod_accounting(void *instance, REQUEST *request) +{ + rlm_ldap_t *inst = instance; + + if (inst->accounting) return user_modify(inst, request, inst->accounting); + + return RLM_MODULE_NOOP; +} + +static rlm_rcode_t mod_post_auth(void *instance, REQUEST *request) CC_HINT(nonnull); +static rlm_rcode_t CC_HINT(nonnull) mod_post_auth(void *instance, REQUEST *request) +{ + rlm_ldap_t *inst = instance; + + if (inst->postauth) { + return user_modify(inst, request, inst->postauth); + } + + return RLM_MODULE_NOOP; +} + + +/* globally exported name */ +extern module_t rlm_ldap; +module_t rlm_ldap = { + .magic = RLM_MODULE_INIT, + .name = "ldap", + .inst_size = sizeof(rlm_ldap_t), + .config = module_config, + .bootstrap = mod_bootstrap, + .instantiate = mod_instantiate, + .detach = mod_detach, + .methods = { + [MOD_AUTHENTICATE] = mod_authenticate, + [MOD_AUTHORIZE] = mod_authorize, + [MOD_ACCOUNTING] = mod_accounting, + [MOD_POST_AUTH] = mod_post_auth + }, +}; |