|
@@ -0,0 +1,321 @@
|
|
|
+/**
|
|
|
+ * 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.
|
|
|
+ */
|
|
|
+package org.apache.hadoop.security;
|
|
|
+
|
|
|
+import java.io.FileReader;
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.Reader;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Hashtable;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+import javax.naming.Context;
|
|
|
+import javax.naming.NamingEnumeration;
|
|
|
+import javax.naming.NamingException;
|
|
|
+import javax.naming.directory.Attribute;
|
|
|
+import javax.naming.directory.DirContext;
|
|
|
+import javax.naming.directory.InitialDirContext;
|
|
|
+import javax.naming.directory.SearchControls;
|
|
|
+import javax.naming.directory.SearchResult;
|
|
|
+
|
|
|
+import org.apache.commons.logging.Log;
|
|
|
+import org.apache.commons.logging.LogFactory;
|
|
|
+import org.apache.hadoop.classification.InterfaceAudience;
|
|
|
+import org.apache.hadoop.classification.InterfaceStability;
|
|
|
+import org.apache.hadoop.conf.Configurable;
|
|
|
+import org.apache.hadoop.conf.Configuration;
|
|
|
+
|
|
|
+/**
|
|
|
+ * An implementation of {@link GroupMappingServiceProvider} which
|
|
|
+ * connects directly to an LDAP server for determining group membership.
|
|
|
+ *
|
|
|
+ * This provider should be used only if it is necessary to map users to
|
|
|
+ * groups that reside exclusively in an Active Directory or LDAP installation.
|
|
|
+ * The common case for a Hadoop installation will be that LDAP users and groups
|
|
|
+ * materialized on the Unix servers, and for an installation like that,
|
|
|
+ * ShellBasedUnixGroupsMapping is preferred. However, in cases where
|
|
|
+ * those users and groups aren't materialized in Unix, but need to be used for
|
|
|
+ * access control, this class may be used to communicate directly with the LDAP
|
|
|
+ * server.
|
|
|
+ *
|
|
|
+ * It is important to note that resolving group mappings will incur network
|
|
|
+ * traffic, and may cause degraded performance, although user-group mappings
|
|
|
+ * will be cached via the infrastructure provided by {@link Groups}.
|
|
|
+ *
|
|
|
+ * This implementation does not support configurable search limits. If a filter
|
|
|
+ * is used for searching users or groups which returns more results than are
|
|
|
+ * allowed by the server, an exception will be thrown.
|
|
|
+ *
|
|
|
+ * The implementation also does not attempt to resolve group hierarchies. In
|
|
|
+ * order to be considered a member of a group, the user must be an explicit
|
|
|
+ * member in LDAP.
|
|
|
+ */
|
|
|
+@InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
|
|
|
+@InterfaceStability.Evolving
|
|
|
+public class LdapGroupsMapping
|
|
|
+ implements GroupMappingServiceProvider, Configurable {
|
|
|
+
|
|
|
+ public static final String LDAP_CONFIG_PREFIX = "hadoop.security.group.mapping.ldap";
|
|
|
+
|
|
|
+ /*
|
|
|
+ * URL of the LDAP server
|
|
|
+ */
|
|
|
+ public static final String LDAP_URL_KEY = LDAP_CONFIG_PREFIX + ".url";
|
|
|
+ public static final String LDAP_URL_DEFAULT = "";
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Should SSL be used to connect to the server
|
|
|
+ */
|
|
|
+ public static final String LDAP_USE_SSL_KEY = LDAP_CONFIG_PREFIX + ".ssl";
|
|
|
+ public static final Boolean LDAP_USE_SSL_DEFAULT = false;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * File path to the location of the SSL keystore to use
|
|
|
+ */
|
|
|
+ public static final String LDAP_KEYSTORE_KEY = LDAP_CONFIG_PREFIX + ".ssl.keystore";
|
|
|
+ public static final String LDAP_KEYSTORE_DEFAULT = "";
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Password for the keystore
|
|
|
+ */
|
|
|
+ public static final String LDAP_KEYSTORE_PASSWORD_KEY = LDAP_CONFIG_PREFIX + ".ssl.keystore.password";
|
|
|
+ public static final String LDAP_KEYSTORE_PASSWORD_DEFAULT = "";
|
|
|
+
|
|
|
+ public static final String LDAP_KEYSTORE_PASSWORD_FILE_KEY = LDAP_KEYSTORE_PASSWORD_KEY + ".file";
|
|
|
+ public static final String LDAP_KEYSTORE_PASSWORD_FILE_DEFAULT = "";
|
|
|
+
|
|
|
+ /*
|
|
|
+ * User to bind to the LDAP server with
|
|
|
+ */
|
|
|
+ public static final String BIND_USER_KEY = LDAP_CONFIG_PREFIX + ".bind.user";
|
|
|
+ public static final String BIND_USER_DEFAULT = "";
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Password for the bind user
|
|
|
+ */
|
|
|
+ public static final String BIND_PASSWORD_KEY = LDAP_CONFIG_PREFIX + ".bind.password";
|
|
|
+ public static final String BIND_PASSWORD_DEFAULT = "";
|
|
|
+
|
|
|
+ public static final String BIND_PASSWORD_FILE_KEY = BIND_PASSWORD_KEY + ".file";
|
|
|
+ public static final String BIND_PASSWORD_FILE_DEFAULT = "";
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Base distinguished name to use for searches
|
|
|
+ */
|
|
|
+ public static final String BASE_DN_KEY = LDAP_CONFIG_PREFIX + ".base";
|
|
|
+ public static final String BASE_DN_DEFAULT = "";
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Any additional filters to apply when searching for users
|
|
|
+ */
|
|
|
+ public static final String USER_SEARCH_FILTER_KEY = LDAP_CONFIG_PREFIX + ".search.filter.user";
|
|
|
+ public static final String USER_SEARCH_FILTER_DEFAULT = "(&(objectClass=user)(sAMAccountName={0}))";
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Any additional filters to apply when finding relevant groups
|
|
|
+ */
|
|
|
+ public static final String GROUP_SEARCH_FILTER_KEY = LDAP_CONFIG_PREFIX + ".search.filter.group";
|
|
|
+ public static final String GROUP_SEARCH_FILTER_DEFAULT = "(objectClass=group)";
|
|
|
+
|
|
|
+ /*
|
|
|
+ * LDAP attribute to use for determining group membership
|
|
|
+ */
|
|
|
+ public static final String GROUP_MEMBERSHIP_ATTR_KEY = LDAP_CONFIG_PREFIX + ".search.attr.member";
|
|
|
+ public static final String GROUP_MEMBERSHIP_ATTR_DEFAULT = "member";
|
|
|
+
|
|
|
+ /*
|
|
|
+ * LDAP attribute to use for identifying a group's name
|
|
|
+ */
|
|
|
+ public static final String GROUP_NAME_ATTR_KEY = LDAP_CONFIG_PREFIX + ".search.attr.group.name";
|
|
|
+ public static final String GROUP_NAME_ATTR_DEFAULT = "cn";
|
|
|
+
|
|
|
+ private static final Log LOG = LogFactory.getLog(LdapGroupsMapping.class);
|
|
|
+
|
|
|
+ private static final SearchControls SEARCH_CONTROLS = new SearchControls();
|
|
|
+ static {
|
|
|
+ SEARCH_CONTROLS.setSearchScope(SearchControls.SUBTREE_SCOPE);
|
|
|
+ }
|
|
|
+
|
|
|
+ private DirContext ctx;
|
|
|
+ private Configuration conf;
|
|
|
+
|
|
|
+ private String ldapUrl;
|
|
|
+ private boolean useSsl;
|
|
|
+ private String keystore;
|
|
|
+ private String keystorePass;
|
|
|
+ private String bindUser;
|
|
|
+ private String bindPassword;
|
|
|
+ private String baseDN;
|
|
|
+ private String groupSearchFilter;
|
|
|
+ private String userSearchFilter;
|
|
|
+ private String groupMemberAttr;
|
|
|
+ private String groupNameAttr;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Returns list of groups for a user.
|
|
|
+ *
|
|
|
+ * The LdapCtx which underlies the DirContext object is not thread-safe, so
|
|
|
+ * we need to block around this whole method. The caching infrastructure will
|
|
|
+ * ensure that performance stays in an acceptable range.
|
|
|
+ *
|
|
|
+ * @param user get groups for this user
|
|
|
+ * @return list of groups for a given user
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public synchronized List<String> getGroups(String user) throws IOException {
|
|
|
+ List<String> groups = new ArrayList<String>();
|
|
|
+
|
|
|
+ try {
|
|
|
+ DirContext ctx = getDirContext();
|
|
|
+
|
|
|
+ // Search for the user. We'll only ever need to look at the first result
|
|
|
+ NamingEnumeration<SearchResult> results = ctx.search(baseDN,
|
|
|
+ userSearchFilter,
|
|
|
+ new Object[]{user},
|
|
|
+ SEARCH_CONTROLS);
|
|
|
+ if (results.hasMoreElements()) {
|
|
|
+ SearchResult result = results.nextElement();
|
|
|
+ String userDn = result.getNameInNamespace();
|
|
|
+
|
|
|
+ NamingEnumeration<SearchResult> groupResults =
|
|
|
+ ctx.search(baseDN,
|
|
|
+ "(&" + groupSearchFilter + "(" + groupMemberAttr + "={0}))",
|
|
|
+ new Object[]{userDn},
|
|
|
+ SEARCH_CONTROLS);
|
|
|
+ while (groupResults.hasMoreElements()) {
|
|
|
+ SearchResult groupResult = groupResults.nextElement();
|
|
|
+ Attribute groupName = groupResult.getAttributes().get(groupNameAttr);
|
|
|
+ groups.add(groupName.get().toString());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (NamingException e) {
|
|
|
+ LOG.warn("Exception trying to get groups for user " + user, e);
|
|
|
+ return new ArrayList<String>();
|
|
|
+ }
|
|
|
+
|
|
|
+ return groups;
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("deprecation")
|
|
|
+ DirContext getDirContext() throws NamingException {
|
|
|
+ if (ctx == null) {
|
|
|
+ // Set up the initial environment for LDAP connectivity
|
|
|
+ Hashtable<String, String> env = new Hashtable<String, String>();
|
|
|
+ env.put(Context.INITIAL_CONTEXT_FACTORY,
|
|
|
+ com.sun.jndi.ldap.LdapCtxFactory.class.getName());
|
|
|
+ env.put(Context.PROVIDER_URL, ldapUrl);
|
|
|
+ env.put(Context.SECURITY_AUTHENTICATION, "simple");
|
|
|
+
|
|
|
+ // Set up SSL security, if necessary
|
|
|
+ if (useSsl) {
|
|
|
+ env.put(Context.SECURITY_PROTOCOL, "ssl");
|
|
|
+ System.setProperty("javax.net.ssl.keyStore", keystore);
|
|
|
+ System.setProperty("javax.net.ssl.keyStorePassword", keystorePass);
|
|
|
+ }
|
|
|
+
|
|
|
+ env.put(Context.SECURITY_PRINCIPAL, bindUser);
|
|
|
+ env.put(Context.SECURITY_CREDENTIALS, bindPassword);
|
|
|
+
|
|
|
+ ctx = new InitialDirContext(env);
|
|
|
+ }
|
|
|
+
|
|
|
+ return ctx;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Caches groups, no need to do that for this provider
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void cacheGroupsRefresh() throws IOException {
|
|
|
+ // does nothing in this provider of user to groups mapping
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Adds groups to cache, no need to do that for this provider
|
|
|
+ *
|
|
|
+ * @param groups unused
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void cacheGroupsAdd(List<String> groups) throws IOException {
|
|
|
+ // does nothing in this provider of user to groups mapping
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public synchronized Configuration getConf() {
|
|
|
+ return conf;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public synchronized void setConf(Configuration conf) {
|
|
|
+ ldapUrl = conf.get(LDAP_URL_KEY, LDAP_URL_DEFAULT);
|
|
|
+ if (ldapUrl == null || ldapUrl.isEmpty()) {
|
|
|
+ throw new RuntimeException("LDAP URL is not configured");
|
|
|
+ }
|
|
|
+
|
|
|
+ useSsl = conf.getBoolean(LDAP_USE_SSL_KEY, LDAP_USE_SSL_DEFAULT);
|
|
|
+ keystore = conf.get(LDAP_KEYSTORE_KEY, LDAP_KEYSTORE_DEFAULT);
|
|
|
+
|
|
|
+ keystorePass =
|
|
|
+ conf.get(LDAP_KEYSTORE_PASSWORD_KEY, LDAP_KEYSTORE_PASSWORD_DEFAULT);
|
|
|
+ if (keystorePass.isEmpty()) {
|
|
|
+ keystorePass = extractPassword(
|
|
|
+ conf.get(LDAP_KEYSTORE_PASSWORD_KEY, LDAP_KEYSTORE_PASSWORD_DEFAULT));
|
|
|
+ }
|
|
|
+
|
|
|
+ bindUser = conf.get(BIND_USER_KEY, BIND_USER_DEFAULT);
|
|
|
+ bindPassword = conf.get(BIND_PASSWORD_KEY, BIND_PASSWORD_DEFAULT);
|
|
|
+ if (bindPassword.isEmpty()) {
|
|
|
+ bindPassword = extractPassword(
|
|
|
+ conf.get(BIND_PASSWORD_FILE_KEY, BIND_PASSWORD_FILE_DEFAULT));
|
|
|
+ }
|
|
|
+
|
|
|
+ baseDN = conf.get(BASE_DN_KEY, BASE_DN_DEFAULT);
|
|
|
+ groupSearchFilter =
|
|
|
+ conf.get(GROUP_SEARCH_FILTER_KEY, GROUP_SEARCH_FILTER_DEFAULT);
|
|
|
+ userSearchFilter =
|
|
|
+ conf.get(USER_SEARCH_FILTER_KEY, USER_SEARCH_FILTER_DEFAULT);
|
|
|
+ groupMemberAttr =
|
|
|
+ conf.get(GROUP_MEMBERSHIP_ATTR_KEY, GROUP_MEMBERSHIP_ATTR_DEFAULT);
|
|
|
+ groupNameAttr =
|
|
|
+ conf.get(GROUP_NAME_ATTR_KEY, GROUP_NAME_ATTR_DEFAULT);
|
|
|
+
|
|
|
+ this.conf = conf;
|
|
|
+ }
|
|
|
+
|
|
|
+ String extractPassword(String pwFile) {
|
|
|
+ if (pwFile.isEmpty()) {
|
|
|
+ // If there is no password file defined, we'll assume that we should do
|
|
|
+ // an anonymous bind
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ StringBuilder password = new StringBuilder();
|
|
|
+ Reader reader = new FileReader(pwFile);
|
|
|
+ int c = reader.read();
|
|
|
+ while (c > -1) {
|
|
|
+ password.append((char)c);
|
|
|
+ c = reader.read();
|
|
|
+ }
|
|
|
+ reader.close();
|
|
|
+ return password.toString();
|
|
|
+ } catch (IOException ex) {
|
|
|
+ throw new RuntimeException("Could not read password file: " + pwFile);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|