|
@@ -1,4 +1,4 @@
|
|
-/**
|
|
|
|
|
|
+/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* distributed with this work for additional information
|
|
@@ -20,26 +20,36 @@ package org.apache.ambari.server.security.authorization;
|
|
|
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
|
|
|
|
|
+import javax.naming.NamingEnumeration;
|
|
import javax.naming.NamingException;
|
|
import javax.naming.NamingException;
|
|
import javax.naming.directory.Attributes;
|
|
import javax.naming.directory.Attributes;
|
|
|
|
+import javax.naming.directory.DirContext;
|
|
|
|
|
|
import org.apache.ambari.server.configuration.Configuration;
|
|
import org.apache.ambari.server.configuration.Configuration;
|
|
import org.apache.commons.lang.StringUtils;
|
|
import org.apache.commons.lang.StringUtils;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.ldap.core.AttributesMapper;
|
|
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.DirContextOperations;
|
|
|
|
+import org.springframework.ldap.core.DistinguishedName;
|
|
import org.springframework.ldap.core.LdapTemplate;
|
|
import org.springframework.ldap.core.LdapTemplate;
|
|
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
|
|
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.core.Authentication;
|
|
-import org.springframework.security.ldap.authentication.BindAuthenticator;
|
|
|
|
|
|
+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
|
|
* An authenticator which binds as a user and checks if user should get ambari
|
|
* admin authorities according to LDAP group membership
|
|
* admin authorities according to LDAP group membership
|
|
*/
|
|
*/
|
|
-public class AmbariLdapBindAuthenticator extends BindAuthenticator {
|
|
|
|
|
|
+public class AmbariLdapBindAuthenticator extends AbstractLdapAuthenticator {
|
|
private static final Logger LOG = LoggerFactory.getLogger(AmbariLdapBindAuthenticator.class);
|
|
private static final Logger LOG = LoggerFactory.getLogger(AmbariLdapBindAuthenticator.class);
|
|
|
|
|
|
private Configuration configuration;
|
|
private Configuration configuration;
|
|
@@ -55,9 +65,14 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator {
|
|
@Override
|
|
@Override
|
|
public DirContextOperations authenticate(Authentication authentication) {
|
|
public DirContextOperations authenticate(Authentication authentication) {
|
|
|
|
|
|
- DirContextOperations user = super.authenticate(authentication);
|
|
|
|
- LdapServerProperties ldapServerProperties =
|
|
|
|
- configuration.getLdapServerProperties();
|
|
|
|
|
|
+ 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())) {
|
|
if (StringUtils.isNotEmpty(ldapServerProperties.getAdminGroupMappingRules())) {
|
|
setAmbariAdminAttr(user, ldapServerProperties);
|
|
setAmbariAdminAttr(user, ldapServerProperties);
|
|
}
|
|
}
|
|
@@ -65,9 +80,13 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator {
|
|
// Users stored locally in ambari are matched against LDAP users by the ldap attribute configured to be used as user name.
|
|
// 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 )
|
|
// (e.g. uid, sAMAccount -> ambari user name )
|
|
String ldapUserName = user.getStringAttribute(ldapServerProperties.getUsernameAttribute());
|
|
String ldapUserName = user.getStringAttribute(ldapServerProperties.getUsernameAttribute());
|
|
- String loginName = authentication.getName(); // user login name the user has logged in
|
|
|
|
|
|
+ String loginName = authentication.getName(); // user login name the user has logged in
|
|
|
|
|
|
- if (!ldapUserName.equals(loginName)) {
|
|
|
|
|
|
+ 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
|
|
// 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
|
|
// 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.
|
|
// ambari db. In this case add the user login name as login alias for ambari user name.
|
|
@@ -76,11 +95,10 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator {
|
|
// If the ldap username needs to be processed (like converted to all lowercase characters),
|
|
// 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
|
|
// process it before setting it in the session via AuthorizationHelper#addLoginNameAlias
|
|
String processedLdapUserName;
|
|
String processedLdapUserName;
|
|
- if(ldapServerProperties.isForceUsernameToLowercase()) {
|
|
|
|
|
|
+ if (ldapServerProperties.isForceUsernameToLowercase()) {
|
|
processedLdapUserName = ldapUserName.toLowerCase();
|
|
processedLdapUserName = ldapUserName.toLowerCase();
|
|
LOG.info("Forcing ldap username to be lowercase characters: {} ==> {}", ldapUserName, processedLdapUserName);
|
|
LOG.info("Forcing ldap username to be lowercase characters: {} ==> {}", ldapUserName, processedLdapUserName);
|
|
- }
|
|
|
|
- else {
|
|
|
|
|
|
+ } else {
|
|
processedLdapUserName = ldapUserName;
|
|
processedLdapUserName = ldapUserName;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -91,10 +109,169 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator {
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
/**
|
|
- * Checks weather user is a member of ambari administrators group in LDAP. If
|
|
|
|
- * yes, sets user's ambari_admin attribute to true
|
|
|
|
- * @param user
|
|
|
|
- * @return
|
|
|
|
|
|
+ * 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;
|
|
|
|
+ DistinguishedName userDistinguishedName = new DistinguishedName(user.getDn());
|
|
|
|
+ DistinguishedName fullDn = new DistinguishedName(userDistinguishedName);
|
|
|
|
+ fullDn.prepend(baseLdapPathContextSource.getBaseLdapPath());
|
|
|
|
+
|
|
|
|
+ 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.getBaseLdapPath());
|
|
|
|
+ } 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) {
|
|
private DirContextOperations setAmbariAdminAttr(DirContextOperations user, LdapServerProperties ldapServerProperties) {
|
|
String baseDn = ldapServerProperties.getBaseDN().toLowerCase();
|
|
String baseDn = ldapServerProperties.getBaseDN().toLowerCase();
|
|
@@ -105,10 +282,10 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator {
|
|
|
|
|
|
//If groupBase is set incorrectly or isn't set - search in BaseDn
|
|
//If groupBase is set incorrectly or isn't set - search in BaseDn
|
|
int indexOfBaseDn = groupBase.indexOf(baseDn);
|
|
int indexOfBaseDn = groupBase.indexOf(baseDn);
|
|
- groupBase = indexOfBaseDn <= 0 ? "" : groupBase.substring(0,indexOfBaseDn - 1);
|
|
|
|
|
|
+ groupBase = indexOfBaseDn <= 0 ? "" : groupBase.substring(0, indexOfBaseDn - 1);
|
|
|
|
|
|
String memberValue = StringUtils.isNotEmpty(adminGroupMappingMemberAttr)
|
|
String memberValue = StringUtils.isNotEmpty(adminGroupMappingMemberAttr)
|
|
- ? user.getStringAttribute(adminGroupMappingMemberAttr) : user.getNameInNamespace();
|
|
|
|
|
|
+ ? user.getStringAttribute(adminGroupMappingMemberAttr) : user.getNameInNamespace();
|
|
LOG.debug("LDAP login - set '{}' as member attribute for adminGroupMappingRules", memberValue);
|
|
LOG.debug("LDAP login - set '{}' as member attribute for adminGroupMappingRules", memberValue);
|
|
|
|
|
|
String setAmbariAdminAttrFilter = resolveAmbariAdminAttrFilter(ldapServerProperties, memberValue);
|
|
String setAmbariAdminAttrFilter = resolveAmbariAdminAttrFilter(ldapServerProperties, memberValue);
|
|
@@ -125,8 +302,8 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator {
|
|
ldapTemplate.setIgnorePartialResultException(true);
|
|
ldapTemplate.setIgnorePartialResultException(true);
|
|
ldapTemplate.setIgnoreNameNotFoundException(true);
|
|
ldapTemplate.setIgnoreNameNotFoundException(true);
|
|
|
|
|
|
- List<String> ambariAdminGroups = ldapTemplate.search(
|
|
|
|
- groupBase, setAmbariAdminAttrFilter, attributesMapper);
|
|
|
|
|
|
+ @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,
|
|
//user has admin role granted, if user is a member of at least 1 group,
|
|
// which matches the rules in configuration
|
|
// which matches the rules in configuration
|
|
@@ -141,24 +318,24 @@ public class AmbariLdapBindAuthenticator extends BindAuthenticator {
|
|
String groupMembershipAttr = ldapServerProperties.getGroupMembershipAttr();
|
|
String groupMembershipAttr = ldapServerProperties.getGroupMembershipAttr();
|
|
String groupObjectClass = ldapServerProperties.getGroupObjectClass();
|
|
String groupObjectClass = ldapServerProperties.getGroupObjectClass();
|
|
String adminGroupMappingRules =
|
|
String adminGroupMappingRules =
|
|
- ldapServerProperties.getAdminGroupMappingRules();
|
|
|
|
|
|
+ ldapServerProperties.getAdminGroupMappingRules();
|
|
final String groupNamingAttribute =
|
|
final String groupNamingAttribute =
|
|
- ldapServerProperties.getGroupNamingAttr();
|
|
|
|
|
|
+ ldapServerProperties.getGroupNamingAttr();
|
|
String groupSearchFilter = ldapServerProperties.getGroupSearchFilter();
|
|
String groupSearchFilter = ldapServerProperties.getGroupSearchFilter();
|
|
|
|
|
|
String setAmbariAdminAttrFilter;
|
|
String setAmbariAdminAttrFilter;
|
|
if (StringUtils.isEmpty(groupSearchFilter)) {
|
|
if (StringUtils.isEmpty(groupSearchFilter)) {
|
|
String adminGroupMappingRegex = createAdminGroupMappingRegex(adminGroupMappingRules, groupNamingAttribute);
|
|
String adminGroupMappingRegex = createAdminGroupMappingRegex(adminGroupMappingRules, groupNamingAttribute);
|
|
setAmbariAdminAttrFilter = String.format("(&(%s=%s)(objectclass=%s)(|%s))",
|
|
setAmbariAdminAttrFilter = String.format("(&(%s=%s)(objectclass=%s)(|%s))",
|
|
- groupMembershipAttr,
|
|
|
|
- memberValue,
|
|
|
|
- groupObjectClass,
|
|
|
|
- adminGroupMappingRegex);
|
|
|
|
|
|
+ groupMembershipAttr,
|
|
|
|
+ memberValue,
|
|
|
|
+ groupObjectClass,
|
|
|
|
+ adminGroupMappingRegex);
|
|
} else {
|
|
} else {
|
|
setAmbariAdminAttrFilter = String.format("(&(%s=%s)%s)",
|
|
setAmbariAdminAttrFilter = String.format("(&(%s=%s)%s)",
|
|
- groupMembershipAttr,
|
|
|
|
- memberValue,
|
|
|
|
- groupSearchFilter);
|
|
|
|
|
|
+ groupMembershipAttr,
|
|
|
|
+ memberValue,
|
|
|
|
+ groupSearchFilter);
|
|
}
|
|
}
|
|
return setAmbariAdminAttrFilter;
|
|
return setAmbariAdminAttrFilter;
|
|
}
|
|
}
|