blob: 320995719ae66a2923f0b8328c4da1ce15b1c078 [file] [log] [blame]
// 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 "ldap-simple-bind.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 "common/names.h"
#include "util/os-util.h"
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.");
DECLARE_string(ldap_bind_dn);
using boost::algorithm::replace_all;
using namespace strings;
namespace impala {
Status LdapSimpleBind::ValidateFlags() {
RETURN_IF_ERROR(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"));
}
}
return Status::OK();
}
Status LdapSimpleBind::Init(const string& user_filter, const string& group_filter) {
RETURN_IF_ERROR(ImpalaLdap::Init(user_filter, 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") != string::npos) {
for (const string& group : group_filter_) {
group_base_dns_.push_back(
StringReplace(group_dn_pattern, "%s", group, /* replace_all */ false));
}
} else {
group_base_dns_.push_back(group_dn_pattern);
}
}
}
return Status::OK();
}
bool LdapSimpleBind::LdapCheckPass(const char* user, const char* pass, unsigned passlen) {
string user_dn = ConstructUserDN(user);
LDAP* ld;
VLOG(1) << "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 LdapSimpleBind::LdapCheckFilters(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 LdapSimpleBind::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_base_dns_) {
vector<string> dns = LdapSearchObject(ld, group_dn.c_str(), filter.c_str());
// Retrieve the value part of the first attribute in the provided relative DN.
for(const string& dn : dns) {
vector<string> attributes = Split(dn, delimiter::Limit(",", 1));
vector<string> short_name = Split(attributes[0], delimiter::Limit("=", 1));
if (short_name.size() > 1 && group_filter_.count(short_name[1]) == 1) {
return true;
}
}
}
return false;
}
string LdapSimpleBind::ConstructUserDN(const 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