| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| #include "util/ldap-util.h" |
| |
| #include <ldap.h> |
| #include <boost/algorithm/string.hpp> |
| #include <gflags/gflags.h> |
| #include <gutil/strings/split.h> |
| #include <gutil/strings/util.h> |
| |
| #include "common/logging.h" |
| #include "kudu/util/flag_tags.h" |
| #include "util/os-util.h" |
| |
| #include "common/names.h" |
| |
| DEFINE_string(ldap_uri, "", "The URI of the LDAP server to authenticate users against"); |
| DEFINE_bool(ldap_tls, false, "If true, use the secure TLS protocol to connect to the LDAP" |
| " server"); |
| |
| DEFINE_bool(ldap_passwords_in_clear_ok, false, "If set, will allow LDAP passwords " |
| "to be sent in the clear (without TLS/SSL) over the network. This option should not " |
| "be used in production environments" ); |
| DEFINE_bool(ldap_allow_anonymous_binds, false, "(Advanced) If true, LDAP authentication " |
| "with a blank password (an 'anonymous bind') is allowed by Impala."); |
| |
| DEFINE_string(ldap_domain, "", "If set, Impala will try to bind to LDAP with a name of " |
| "the form <userid>@<ldap_domain>"); |
| DEFINE_string(ldap_baseDN, "", "If set, Impala will try to bind to LDAP with a name of " |
| "the form uid=<userid>,<ldap_baseDN>"); |
| DEFINE_string(ldap_bind_pattern, "", "If set, Impala will try to bind to LDAP with a name" |
| " of <ldap_bind_pattern>, but where the string #UID is replaced by the user ID. Use" |
| " to control the bind name precisely; do not set --ldap_domain or --ldap_baseDN with" |
| " this option"); |
| |
| DEFINE_string(ldap_group_dn_pattern, "", "Colon separated list of patterns for the " |
| "'distinguished name' used to search for groups in the directory. Each pattern may " |
| "contain a '%s' which will be substituted with each group name from " |
| "--ldap_group_filter when doing group searches."); |
| DEFINE_string(ldap_group_membership_key, "member", |
| "The LDAP attribute on group entries that indicates its members."); |
| DEFINE_string(ldap_group_class_key, "groupOfNames", |
| "The LDAP objectClass each of the groups in --ldap_group_filter implements in LDAP."); |
| |
| DEFINE_string(ldap_bind_dn, "", |
| "Distinguished name of the user to bind as when doing user or group searches. Only " |
| "required if user or group filters are being used and the LDAP server is not " |
| "configured to allow anonymous searches."); |
| DEFINE_string(ldap_bind_password_cmd, "", |
| "A Unix command whose output returns the password to use with --ldap_bind_dn. The " |
| "output of the command will be truncated to 1024 bytes and trimmed of trailing " |
| "whitespace."); |
| TAG_FLAG(ldap_bind_password_cmd, sensitive); |
| |
| DECLARE_string(ldap_ca_certificate); |
| DECLARE_string(ldap_user_filter); |
| DECLARE_string(ldap_group_filter); |
| DECLARE_string(principal); |
| DECLARE_bool(skip_external_kerberos_auth); |
| |
| using boost::algorithm::replace_all; |
| using namespace strings; |
| |
| namespace impala { |
| |
| // Required prefixes for ldap URIs: |
| static const string LDAP_URI_PREFIX = "ldap://"; |
| static const string LDAPS_URI_PREFIX = "ldaps://"; |
| |
| Status ImpalaLdap::ValidateFlags() { |
| const string excl_msg = "--$0 and --$1 are mutually exclusive " |
| "and should not be set together"; |
| |
| if (!FLAGS_ldap_domain.empty()) { |
| if (!FLAGS_ldap_baseDN.empty()) { |
| return Status(Substitute(excl_msg, "ldap_domain", "ldap_baseDN")); |
| } |
| if (!FLAGS_ldap_bind_pattern.empty()) { |
| return Status(Substitute(excl_msg, "ldap_domain", "ldap_bind_pattern")); |
| } |
| } else if (!FLAGS_ldap_baseDN.empty()) { |
| if (!FLAGS_ldap_bind_pattern.empty()) { |
| return Status(Substitute(excl_msg, "ldap_baseDN", "ldap_bind_pattern")); |
| } |
| } |
| |
| if (FLAGS_ldap_uri.empty()) { |
| return Status("--ldap_uri must be supplied when --ldap_enable_auth is set"); |
| } |
| |
| if ((FLAGS_ldap_uri.find(LDAP_URI_PREFIX) != 0) |
| && (FLAGS_ldap_uri.find(LDAPS_URI_PREFIX) != 0)) { |
| return Status(Substitute( |
| "--ldap_uri must start with either $0 or $1", LDAP_URI_PREFIX, LDAPS_URI_PREFIX)); |
| } |
| |
| LOG(INFO) << "Using LDAP authentication with server " << FLAGS_ldap_uri; |
| |
| if (!FLAGS_ldap_tls && (FLAGS_ldap_uri.find(LDAPS_URI_PREFIX) != 0)) { |
| if (FLAGS_ldap_passwords_in_clear_ok) { |
| LOG(WARNING) << "LDAP authentication is being used, but without TLS. " |
| << "ALL PASSWORDS WILL GO OVER THE NETWORK IN THE CLEAR."; |
| } else { |
| return Status("LDAP authentication specified, but without TLS. " |
| "Passwords would go over the network in the clear. " |
| "Enable TLS with --ldap_tls or use an ldaps:// URI. " |
| "To override this is non-production environments, " |
| "specify --ldap_passwords_in_clear_ok"); |
| } |
| } else if (FLAGS_ldap_ca_certificate.empty()) { |
| LOG(WARNING) << "LDAP authentication is being used with TLS, but without " |
| << "an --ldap_ca_certificate file, the identity of the LDAP " |
| << "server cannot be verified. Network communication (and " |
| << "hence passwords) could be intercepted by a " |
| << "man-in-the-middle attack"; |
| } |
| |
| if ((!FLAGS_ldap_user_filter.empty() || !FLAGS_ldap_group_filter.empty()) |
| && (!FLAGS_principal.empty() && !FLAGS_skip_external_kerberos_auth)) { |
| return Status("LDAP user and group filters may not be used if Kerberos auth is " |
| "turned on for external connections."); |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status ImpalaLdap::Init(const std::string& user_filter, const std::string& group_filter) { |
| if (!user_filter.empty()) { |
| user_filter_ = Split(user_filter, ","); |
| } |
| |
| if (!group_filter.empty()) { |
| if (FLAGS_ldap_group_dn_pattern.empty()) { |
| return Status("In order to apply an LDAP group filter, --ldap_group_dn_pattern " |
| "must be specified."); |
| } |
| group_filter_ = Split(group_filter, ","); |
| vector<string> group_dns = Split(FLAGS_ldap_group_dn_pattern, ":"); |
| |
| // Build the list of DNs to search for groups by iterating through the |
| // DN patterns and replacing the optional '%s' with each group name, if present. |
| for (const string& group_dn_pattern : group_dns) { |
| if (group_dn_pattern.find("%s") != std::string::npos) { |
| for (const string& group : group_filter_) { |
| group_filter_dns_.push_back( |
| StringReplace(group_dn_pattern, "%s", group, /* replace_all */ false)); |
| } |
| } else { |
| group_filter_dns_.push_back(group_dn_pattern); |
| } |
| } |
| } |
| |
| if (!FLAGS_ldap_bind_password_cmd.empty()) { |
| if (!RunShellProcess( |
| FLAGS_ldap_bind_password_cmd, &bind_password_, true, {"JAVA_TOOL_OPTIONS"})) { |
| return Status( |
| Substitute("ldap_bind_password_cmd failed with output: '$0'", bind_password_)); |
| } |
| } |
| |
| return Status::OK(); |
| } |
| |
| bool ImpalaLdap::LdapCheckPass(const char* user, const char* pass, unsigned passlen) { |
| if (passlen == 0 && !FLAGS_ldap_allow_anonymous_binds) { |
| // Disable anonymous binds. |
| return false; |
| } |
| |
| string user_dn = ConstructUserDN(user); |
| LDAP* ld; |
| VLOG_QUERY << "Trying simple LDAP bind for: " << user_dn; |
| bool success = Bind(user_dn, pass, passlen, &ld); |
| if (success) { |
| ldap_unbind_ext(ld, nullptr, nullptr); |
| } |
| VLOG(2) << "LDAP bind successful"; |
| return success; |
| } |
| |
| bool ImpalaLdap::Bind( |
| const std::string& user_dn, const char* pass, unsigned passlen, LDAP** ld) { |
| int rc = ldap_initialize(ld, FLAGS_ldap_uri.c_str()); |
| if (rc != LDAP_SUCCESS) { |
| LOG(WARNING) << "Could not initialize connection with LDAP server (" << FLAGS_ldap_uri |
| << "). Error: " << ldap_err2string(rc); |
| return false; |
| } |
| |
| // Force the LDAP version to 3 to make sure TLS is supported. |
| int ldap_ver = 3; |
| ldap_set_option(*ld, LDAP_OPT_PROTOCOL_VERSION, &ldap_ver); |
| |
| // If -ldap_tls is turned on, and the URI is ldap://, issue a STARTTLS operation. |
| // Note that we'll ignore -ldap_tls when using ldaps:// because we've already |
| // got a secure connection (and the LDAP server will reject the STARTTLS). |
| if (FLAGS_ldap_tls && (FLAGS_ldap_uri.find(LDAP_URI_PREFIX) == 0)) { |
| int tls_rc = ldap_start_tls_s(*ld, nullptr, nullptr); |
| if (tls_rc != LDAP_SUCCESS) { |
| LOG(WARNING) << "Could not start TLS secure connection to LDAP server (" |
| << FLAGS_ldap_uri << "). Error: " << ldap_err2string(tls_rc); |
| ldap_unbind_ext(*ld, nullptr, nullptr); |
| return false; |
| } |
| VLOG(2) << "Started TLS connection with LDAP server: " << FLAGS_ldap_uri; |
| } |
| |
| // Map the password into a credentials structure |
| struct berval cred; |
| cred.bv_val = const_cast<char*>(pass); |
| cred.bv_len = passlen; |
| |
| rc = ldap_sasl_bind_s( |
| *ld, user_dn.c_str(), LDAP_SASL_SIMPLE, &cred, nullptr, nullptr, nullptr); |
| // Free ld |
| if (rc != LDAP_SUCCESS) { |
| LOG(WARNING) << "LDAP authentication failure for " << user_dn << " : " |
| << ldap_err2string(rc); |
| ldap_unbind_ext(*ld, nullptr, nullptr); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool ImpalaLdap::LdapCheckFilters(std::string username) { |
| if (user_filter_.empty() && group_filter_.empty()) return true; |
| |
| VLOG(2) << "Checking LDAP filters for " << username; |
| if (username.empty()) { |
| LOG(WARNING) << "Failed to check LDAP filters: username empty."; |
| return false; |
| } |
| |
| LDAP* ld; |
| bool success = |
| Bind(FLAGS_ldap_bind_dn, bind_password_.c_str(), bind_password_.size(), &ld); |
| if (!success) return false; |
| |
| if (!user_filter_.empty() && user_filter_.count(username) != 1) { |
| LOG(WARNING) << "LDAP authentication failure for " << username << ". Bind was " |
| << "successful but user is not in the authorized user list."; |
| ldap_unbind_ext(ld, nullptr, nullptr); |
| return false; |
| } |
| |
| if (!group_filter_.empty()) { |
| string filter_user_dn = ConstructUserDN(username); |
| if (!CheckGroupMembership(ld, filter_user_dn)) { |
| LOG(WARNING) << "LDAP authentication failure for " << username << ". Bind was " |
| << "successful but user is not in any of the required groups."; |
| ldap_unbind_ext(ld, nullptr, nullptr); |
| return false; |
| } |
| } |
| ldap_unbind_ext(ld, nullptr, nullptr); |
| VLOG(2) << "LDAP filter check for " << username << " was successful."; |
| return true; |
| } |
| |
| bool ImpalaLdap::CheckGroupMembership(LDAP* ld, const string& user_dn) { |
| // Construct a filter that will search for LDAP entries that represent groups |
| // (determined by having the group class key) and that contain the user trying to |
| // authenticate (determined by having a membership entry matching the user). |
| string filter = Substitute("(&(objectClass=$0)($1=$2))", FLAGS_ldap_group_class_key, |
| FLAGS_ldap_group_membership_key, user_dn); |
| VLOG(2) << "Searching for groups with filter: " << filter; |
| |
| for (const string& group_dn : group_filter_dns_) { |
| LDAPMessage* result; |
| // Search through LDAP starting at a base of 'group_dn' and including the entire |
| // subtree below it while applying 'filter'. This should return a list of all group |
| // entries encountered in the search that have the given user as a member. |
| int rc = ldap_search_ext_s(ld, group_dn.c_str(), LDAP_SCOPE_SUBTREE, filter.c_str(), |
| nullptr, false, nullptr, nullptr, nullptr, LDAP_MAXINT, &result); |
| if (rc != LDAP_SUCCESS) { |
| LOG(WARNING) << "LDAP search failed for " << filter << " with DN=" << group_dn |
| << ": " << ldap_err2string(rc); |
| ldap_msgfree(result); |
| continue; |
| } |
| |
| for (LDAPMessage* msg = ldap_first_message(ld, result); msg != nullptr; |
| msg = ldap_next_message(ld, msg)) { |
| int msg_type = ldap_msgtype(msg); |
| switch (msg_type) { |
| case LDAP_RES_SEARCH_ENTRY: |
| char* dn; |
| if ((dn = ldap_get_dn(ld, msg)) != nullptr) { |
| string short_name = GetShortName(dn); |
| if (group_filter_.count(short_name) == 1) { |
| ldap_memfree(dn); |
| ldap_msgfree(result); |
| return true; |
| } |
| ldap_memfree(dn); |
| } else { |
| LOG(WARNING) << "LDAP search error for " << filter << " with DN=" << group_dn |
| << ": Was not able to get DN from search result."; |
| } |
| break; |
| case LDAP_RES_SEARCH_REFERENCE: { |
| LOG(WARNING) << "LDAP search error for " << filter << " with DN=" << group_dn |
| << ": Following of referrals not supported, ignoring."; |
| char** referrals; |
| int parse_rc = ldap_parse_reference(ld, msg, &referrals, nullptr, 0); |
| if (parse_rc != LDAP_SUCCESS) { |
| LOG(WARNING) << "Was unable to parse LDAP search reference result: " |
| << ldap_err2string(parse_rc); |
| break; |
| } |
| |
| if (referrals != nullptr) { |
| for (int i = 0; referrals[i] != nullptr; ++i) { |
| LOG(WARNING) << "Got search reference: " << referrals[i]; |
| } |
| ber_memvfree((void**)referrals); |
| } |
| break; |
| } |
| case LDAP_RES_SEARCH_RESULT: |
| // Indicates the end of the messages in the result. Nothing to do. |
| break; |
| } |
| } |
| ldap_msgfree(result); |
| } |
| |
| return false; |
| } |
| |
| string ImpalaLdap::GetShortName(const string& rdn) { |
| vector<string> attributes = Split(rdn, delimiter::Limit(",", 1)); |
| vector<string> value = Split(attributes[0], delimiter::Limit("=", 1)); |
| return value[1]; |
| } |
| |
| string ImpalaLdap::ConstructUserDN(const std::string& user) { |
| string user_dn = user; |
| if (!FLAGS_ldap_domain.empty()) { |
| // Append @domain if there isn't already an @ in the user string. |
| if (user_dn.find("@") == string::npos) { |
| user_dn = Substitute("$0@$1", user_dn, FLAGS_ldap_domain); |
| } |
| } else if (!FLAGS_ldap_baseDN.empty()) { |
| user_dn = Substitute("uid=$0,$1", user_dn, FLAGS_ldap_baseDN); |
| } else if (!FLAGS_ldap_bind_pattern.empty()) { |
| user_dn = FLAGS_ldap_bind_pattern; |
| replace_all(user_dn, "#UID", user); |
| } |
| return user_dn; |
| } |
| |
| } // namespace impala |