/* * 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.ambari.server.security.authorization; import java.util.List; import javax.naming.Name; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attributes; import javax.naming.directory.DirContext; import org.apache.ambari.server.configuration.Configuration; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ldap.core.AttributesMapper; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.DirContextAdapter; import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.support.BaseLdapPathContextSource; import org.springframework.ldap.support.LdapUtils; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.ldap.authentication.AbstractLdapAuthenticator; import org.springframework.security.ldap.search.LdapUserSearch; /** * An authenticator which binds as a user and checks if user should get ambari * admin authorities according to LDAP group membership */ public class AmbariLdapBindAuthenticator extends AbstractLdapAuthenticator { private static final Logger LOG = LoggerFactory.getLogger(AmbariLdapBindAuthenticator.class); private Configuration configuration; private static final String AMBARI_ADMIN_LDAP_ATTRIBUTE_KEY = "ambari_admin"; public AmbariLdapBindAuthenticator(BaseLdapPathContextSource contextSource, Configuration configuration) { super(contextSource); this.configuration = configuration; } @Override public DirContextOperations authenticate(Authentication authentication) { if (!(authentication instanceof UsernamePasswordAuthenticationToken)) { LOG.info("Unexpected authentication token type encountered ({}) - failing authentication.", authentication.getClass().getName()); throw new BadCredentialsException("Unexpected authentication token type encountered."); } DirContextOperations user = authenticate((UsernamePasswordAuthenticationToken) authentication); LdapServerProperties ldapServerProperties = configuration.getLdapServerProperties(); if (StringUtils.isNotEmpty(ldapServerProperties.getAdminGroupMappingRules())) { setAmbariAdminAttr(user, ldapServerProperties); } // Users stored locally in ambari are matched against LDAP users by the ldap attribute configured to be used as user name. // (e.g. uid, sAMAccount -> ambari user name ) String ldapUserName = user.getStringAttribute(ldapServerProperties.getUsernameAttribute()); String loginName = authentication.getName(); // user login name the user has logged in if (ldapUserName == null) { LOG.warn("The user data does not contain a value for {}.", ldapServerProperties.getUsernameAttribute()); } else if (ldapUserName.isEmpty()) { LOG.warn("The user data contains an empty value for {}.", ldapServerProperties.getUsernameAttribute()); } else if (!ldapUserName.equals(loginName)) { // if authenticated user name is different from ldap user name than user has logged in // with a login name that is different (e.g. user principal name) from the ambari user name stored in // ambari db. In this case add the user login name as login alias for ambari user name. LOG.info("User with {}='{}' logged in with login alias '{}'", ldapServerProperties.getUsernameAttribute(), ldapUserName, loginName); // If the ldap username needs to be processed (like converted to all lowercase characters), // process it before setting it in the session via AuthorizationHelper#addLoginNameAlias String processedLdapUserName; if (ldapServerProperties.isForceUsernameToLowercase()) { processedLdapUserName = ldapUserName.toLowerCase(); LOG.info("Forcing ldap username to be lowercase characters: {} ==> {}", ldapUserName, processedLdapUserName); } else { processedLdapUserName = ldapUserName; } AuthorizationHelper.addLoginNameAlias(processedLdapUserName, loginName); } return user; } /** * Authenticates a user with a configured LDAP server using the user's username and password. * <p> * To authenticate a user: * <ol> * <li> * The LDAP server is queried for the relevant user object where the * supplied username matches the configured LDAP attribute that represents the user's username * <ul><li>Example: (&(uid=user1)(objectClass=posixAccount))</li></ul> * </li> * <li> * If found, the distinguished name (DN) of the user object is obtained from returned data and then * used, along with the supplied password to perform an LDAP bind (see {@link #bind(DirContextOperations, String)}) * </li> * </ol> * <p> * Failure to authenticate will result in a {@link BadCredentialsException} to be thrown. * * @param authentication the credentials to use for authentication * @return the authenticated user details * @see #bind(DirContextOperations, String) */ private DirContextOperations authenticate(UsernamePasswordAuthenticationToken authentication) { DirContextOperations user = null; String username = authentication.getName(); Object credentials = authentication.getCredentials(); String password = (credentials instanceof String) ? (String) credentials : null; if (StringUtils.isEmpty(username)) { LOG.debug("Empty username encountered - failing authentication."); throw new BadCredentialsException("Empty username encountered."); } LOG.debug("Authenticating {}", username); if (StringUtils.isEmpty(password)) { LOG.debug("Empty password encountered - failing authentication."); throw new BadCredentialsException("Empty password encountered."); } LdapUserSearch userSearch = getUserSearch(); if (userSearch == null) { LOG.debug("The user search facility has not been set - failing authentication."); throw new BadCredentialsException("The user search facility has not been set."); } else { if (LOG.isTraceEnabled()) { LOG.trace("Searching for user with username {}: {}", username, userSearch.toString()); } // Find the user data where the supplied username matches the value of the configured LDAP // attribute for the user's username. If a user is found, use the DN fro the returned data // and the supplied password to attempt authentication. DirContextOperations userFromSearch = userSearch.searchForUser(username); if (userFromSearch == null) { LOG.debug("LDAP user object not found for {}", username); } else { LOG.debug("Found LDAP user for {}: {}", username, userFromSearch.getDn()); user = bind(userFromSearch, password); // If trace enabled, log the user's LDAP attributes. if (LOG.isTraceEnabled()) { Attributes attributes = user.getAttributes(); if (attributes != null) { StringBuilder builder = new StringBuilder(); NamingEnumeration<String> ids = attributes.getIDs(); try { while (ids.hasMore()) { String id = ids.next(); builder.append("\n\t"); builder.append(attributes.get(id)); } } catch (NamingException e) { // Ignore this... } LOG.trace("User Attributes: {}", builder); } else { LOG.trace("User Attributes: not available"); } } } } // If a user was not authenticated, thrown a BadCredentialsException, else return the user data if (user == null) { LOG.debug("Invalid credentials for {} - failing authentication.", username); throw new BadCredentialsException("Invalid credentials."); } else { LOG.debug("Successfully authenticated {}", username); } return user; } /** * Attempt to authenticate a user with the configured LDAP server by performing an LDAP bind. * <p> * Using the distinguished name provided in the supplied user data and the supplied password, * attempt to authenticate with the configured LDAP server. If authentication is successful, use the * attributes from the supplied user data rather than the attributes associated with the bound context * because some scenarios result in missing data within the bound context due to LDAP server implementations. * <p> * If authentication is not successful, throw a {@link BadCredentialsException}. * * @param user the user data containing the relevant DN and associated attributes * @param password the password * @return the authenticated user details * @throws BadCredentialsException if authentication fails */ private DirContextOperations bind(DirContextOperations user, String password) { ContextSource contextSource = getContextSource(); if (contextSource == null) { String message = "Missing ContextSource - failing authentication."; LOG.debug(message); throw new InternalAuthenticationServiceException(message); } if (!(contextSource instanceof BaseLdapPathContextSource)) { String message = String.format("Unexpected ContextSource type (%s) - failing authentication.", contextSource.getClass().getName()); LOG.debug(message); throw new InternalAuthenticationServiceException(message); } BaseLdapPathContextSource baseLdapPathContextSource = (BaseLdapPathContextSource) contextSource; Name userDistinguishedName = user.getDn(); Name fullDn = AmbariLdapUtils.getFullDn(userDistinguishedName, baseLdapPathContextSource.getBaseLdapName()); LOG.debug("Attempting to bind as {}", fullDn); DirContext dirContext = null; try { // Perform the authentication. The result is not used because it is expected that the supplied // user data has all of the attributes for the authenticated user. If authentication fails, it // expected that the supplied user data will be destroyed or orphaned. dirContext = baseLdapPathContextSource.getContext(fullDn.toString(), password); // Build a new DirContextAdapter using the attributes from the passed in user details since it // is expected these details will be more complete of querying for them from the bound context. // Some LDAP server implementations will no return all attributes to the bound context due to // the filter being used in the query. return new DirContextAdapter(user.getAttributes(), userDistinguishedName, baseLdapPathContextSource.getBaseLdapName()); } catch (org.springframework.ldap.AuthenticationException e) { String message = String.format("Failed to bind as %s - %s", user.getDn().toString(), e.getMessage()); if (LOG.isTraceEnabled()) { LOG.trace(message, e); } else if (LOG.isDebugEnabled()) { LOG.debug(message); } throw new BadCredentialsException("The username or password is incorrect."); } finally { LdapUtils.closeContext(dirContext); } } /** * Checks weather user is a member of ambari administrators group in LDAP. If * yes, sets user's ambari_admin attribute to true * * @param user the user details * @return the updated user details */ private DirContextOperations setAmbariAdminAttr(DirContextOperations user, LdapServerProperties ldapServerProperties) { String baseDn = ldapServerProperties.getBaseDN().toLowerCase(); String groupBase = ldapServerProperties.getGroupBase().toLowerCase(); final String groupNamingAttribute = ldapServerProperties.getGroupNamingAttr(); final String adminGroupMappingMemberAttr = ldapServerProperties.getAdminGroupMappingMemberAttr(); //If groupBase is set incorrectly or isn't set - search in BaseDn int indexOfBaseDn = groupBase.indexOf(baseDn); groupBase = indexOfBaseDn <= 0 ? "" : groupBase.substring(0, indexOfBaseDn - 1); String memberValue = StringUtils.isNotEmpty(adminGroupMappingMemberAttr) ? user.getStringAttribute(adminGroupMappingMemberAttr) : user.getNameInNamespace(); LOG.debug("LDAP login - set '{}' as member attribute for adminGroupMappingRules", memberValue); String setAmbariAdminAttrFilter = resolveAmbariAdminAttrFilter(ldapServerProperties, memberValue); LOG.debug("LDAP login - set admin attr filter: {}", setAmbariAdminAttrFilter); AttributesMapper attributesMapper = new AttributesMapper() { public Object mapFromAttributes(Attributes attrs) throws NamingException { return attrs.get(groupNamingAttribute).get(); } }; LdapTemplate ldapTemplate = new LdapTemplate((getContextSource())); ldapTemplate.setIgnorePartialResultException(true); ldapTemplate.setIgnoreNameNotFoundException(true); @SuppressWarnings("unchecked") List<String> ambariAdminGroups = ldapTemplate.search(groupBase, setAmbariAdminAttrFilter, attributesMapper); //user has admin role granted, if user is a member of at least 1 group, // which matches the rules in configuration if (ambariAdminGroups.size() > 0) { user.setAttributeValue(AMBARI_ADMIN_LDAP_ATTRIBUTE_KEY, true); } return user; } private String resolveAmbariAdminAttrFilter(LdapServerProperties ldapServerProperties, String memberValue) { String groupMembershipAttr = ldapServerProperties.getGroupMembershipAttr(); String groupObjectClass = ldapServerProperties.getGroupObjectClass(); String adminGroupMappingRules = ldapServerProperties.getAdminGroupMappingRules(); final String groupNamingAttribute = ldapServerProperties.getGroupNamingAttr(); String groupSearchFilter = ldapServerProperties.getGroupSearchFilter(); String setAmbariAdminAttrFilter; if (StringUtils.isEmpty(groupSearchFilter)) { String adminGroupMappingRegex = createAdminGroupMappingRegex(adminGroupMappingRules, groupNamingAttribute); setAmbariAdminAttrFilter = String.format("(&(%s=%s)(objectclass=%s)(|%s))", groupMembershipAttr, memberValue, groupObjectClass, adminGroupMappingRegex); } else { setAmbariAdminAttrFilter = String.format("(&(%s=%s)%s)", groupMembershipAttr, memberValue, groupSearchFilter); } return setAmbariAdminAttrFilter; } private String createAdminGroupMappingRegex(String adminGroupMappingRules, String groupNamingAttribute) { String[] adminGroupMappingRegexs = adminGroupMappingRules.split(","); StringBuilder builder = new StringBuilder(""); for (String adminGroupMappingRegex : adminGroupMappingRegexs) { builder.append(String.format("(%s=%s)", groupNamingAttribute, adminGroupMappingRegex)); } return builder.toString(); } }