AMBARI-21216. Add support for consecutive login failure accounting (rlevas)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/f760516c Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/f760516c Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/f760516c Branch: refs/heads/branch-feature-AMBARI-20859 Commit: f760516c24478b19d4e579cb67702d9d43251eaa Parents: ac5008d Author: Robert Levas <[email protected]> Authored: Thu Jun 22 13:29:51 2017 -0400 Committer: Robert Levas <[email protected]> Committed: Thu Jun 22 13:29:51 2017 -0400 ---------------------------------------------------------------------- .../server/audit/event/LoginAuditEvent.java | 20 ++ .../ambari/server/controller/AmbariServer.java | 2 + .../ambari/server/orm/entities/UserEntity.java | 32 ++++ .../AmbariAuthenticationEventHandler.java | 64 +++++++ .../AmbariAuthenticationEventHandlerImpl.java | 152 +++++++++++++++ .../AmbariAuthenticationException.java | 43 +++++ .../AmbariBasicAuthenticationFilter.java | 92 ++++------ .../AmbariJWTAuthenticationFilter.java | 71 +++---- ...lidUsernamePasswordCombinationException.java | 32 ++++ .../authentication/UserNotFoundException.java | 15 +- .../AmbariKerberosAuthenticationFilter.java | 70 +++---- .../AmbariLdapAuthenticationProvider.java | 7 +- .../AmbariLdapAuthoritiesPopulator.java | 3 +- .../authorization/AmbariLocalUserProvider.java | 9 +- ...lidUsernamePasswordCombinationException.java | 34 ---- .../server/security/authorization/Users.java | 167 ++++++++++++++++- .../AmbariInternalAuthenticationProvider.java | 2 +- .../jwt/JwtAuthenticationFilter.java | 2 +- .../main/resources/Ambari-DDL-Derby-CREATE.sql | 1 + .../main/resources/Ambari-DDL-MySQL-CREATE.sql | 1 + .../main/resources/Ambari-DDL-Oracle-CREATE.sql | 1 + .../resources/Ambari-DDL-Postgres-CREATE.sql | 1 + .../resources/Ambari-DDL-SQLAnywhere-CREATE.sql | 1 + .../resources/Ambari-DDL-SQLServer-CREATE.sql | 1 + .../webapp/WEB-INF/spring-security.xml | 9 +- .../server/audit/LoginAuditEventTest.java | 36 +++- .../AmbariBasicAuthenticationFilterTest.java | 136 ++++++++------ .../AmbariJWTAuthenticationFilterTest.java | 160 +++++++++------- .../AmbariKerberosAuthenticationFilterTest.java | 183 ++++++++++++++----- ...ariAuthorizationProviderDisableUserTest.java | 1 + ...uthenticationProviderForDNWithSpaceTest.java | 1 + .../AmbariLdapAuthenticationProviderTest.java | 1 + .../AmbariLocalUserProviderTest.java | 1 + 33 files changed, 977 insertions(+), 374 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/audit/event/LoginAuditEvent.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/audit/event/LoginAuditEvent.java b/ambari-server/src/main/java/org/apache/ambari/server/audit/event/LoginAuditEvent.java index 9583b84..9be216a 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/audit/event/LoginAuditEvent.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/audit/event/LoginAuditEvent.java @@ -51,6 +51,11 @@ public class LoginAuditEvent extends AbstractUserAuditEvent { private String reasonOfFailure; /** + * Number of consecutive failed authentication attempts since the last successful attempt + */ + private Integer consecutiveFailures; + + /** * {@inheritDoc} */ @Override @@ -73,6 +78,9 @@ public class LoginAuditEvent extends AbstractUserAuditEvent { if (reasonOfFailure != null) { builder.append("), Reason(") .append(reasonOfFailure); + + builder.append("), Consecutive failures(") + .append((consecutiveFailures == null) ? "UNKNOWN USER" : String.valueOf(consecutiveFailures)); } builder.append(")"); } @@ -95,6 +103,18 @@ public class LoginAuditEvent extends AbstractUserAuditEvent { } /** + * Set the number of consecutive authentication failures since the last successful authentication + * attempt + * + * @param consecutiveFailures the number of consecutive authentication failures + * @return this builder + */ + public LoginAuditEventBuilder withConsecutiveFailures(Integer consecutiveFailures) { + this.consecutiveFailures = consecutiveFailures; + return this; + } + + /** * {@inheritDoc} */ @Override http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java index 01920f8..8173655 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java @@ -98,6 +98,7 @@ import org.apache.ambari.server.security.AmbariServerSecurityHeaderFilter; import org.apache.ambari.server.security.AmbariViewsSecurityHeaderFilter; import org.apache.ambari.server.security.CertificateManager; import org.apache.ambari.server.security.SecurityFilter; +import org.apache.ambari.server.security.authentication.AmbariAuthenticationEventHandlerImpl; import org.apache.ambari.server.security.authorization.AmbariLdapAuthenticationProvider; import org.apache.ambari.server.security.authorization.AmbariLocalUserProvider; import org.apache.ambari.server.security.authorization.AmbariPamAuthenticationProvider; @@ -327,6 +328,7 @@ public class AmbariServer { factory.registerSingleton("guiceInjector", injector); factory.registerSingleton("ambariConfiguration", injector.getInstance(Configuration.class)); + factory.registerSingleton("ambariAuthenticationEventHandler", injector.getInstance(AmbariAuthenticationEventHandlerImpl.class)); factory.registerSingleton("ambariUsers", injector.getInstance(Users.class)); factory.registerSingleton("passwordEncoder", injector.getInstance(PasswordEncoder.class)); http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/UserEntity.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/UserEntity.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/UserEntity.java index 66e9003..c679fff 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/UserEntity.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/UserEntity.java @@ -42,6 +42,7 @@ import javax.persistence.TableGenerator; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.UniqueConstraint; +import javax.persistence.Version; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; @@ -85,6 +86,10 @@ public class UserEntity { @Column(name = "local_username") private String localUsername; + @Version + @Column(name = "version") + private Long version; + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) private Set<MemberEntity> memberEntities = new HashSet<>(); @@ -214,6 +219,31 @@ public class UserEntity { this.createTime = createTime; } + /** + * Returns the version number of the relevant data stored in the database. + * <p> + * This is used to help ensure that collisions updatin the relevant data in the database are + * handled properly via Optimistic locking. + * + * @return a version number + */ + public Long getVersion() { + return version; + } + + /** + * Sets the version number of the relevant data stored in the database. + * <p> + * This is used to help ensure that collisions updatin the relevant data in the database are + * handled properly via Optimistic locking. It is recommended that this value is <b>not</b> + * manually updated, else issues may occur when persisting the data. + * + * @param version a version number + */ + public void setVersion(Long version) { + this.version = version; + } + public Set<MemberEntity> getMemberEntities() { return memberEntities; } @@ -297,6 +327,7 @@ public class UserEntity { equalsBuilder.append(consecutiveFailures, that.consecutiveFailures); equalsBuilder.append(active, that.active); equalsBuilder.append(createTime, that.createTime); + equalsBuilder.append(version, that.version); return equalsBuilder.isEquals(); } } @@ -311,6 +342,7 @@ public class UserEntity { hashCodeBuilder.append(consecutiveFailures); hashCodeBuilder.append(active); hashCodeBuilder.append(createTime); + hashCodeBuilder.append(version); return hashCodeBuilder.toHashCode(); } } http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandler.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandler.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandler.java new file mode 100644 index 0000000..037fc13 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandler.java @@ -0,0 +1,64 @@ +/* + * 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.authentication; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; + +/** + * AmbariAuthenticationEventHandler is an interface to be implemented by classes used to track Ambari + * user authentication attempts. + */ +public interface AmbariAuthenticationEventHandler { + /** + * The event callback called when a successful authentication attempt has occurred. + * + * @param filter the Authentication filer used for authentication + * @param servletRequest the request + * @param servletResponse the response + * @param result the authentication result + */ + void onSuccessfulAuthentication(AmbariAuthenticationFilter filter, HttpServletRequest servletRequest, + HttpServletResponse servletResponse, Authentication result); + + /** + * The event callback called when a failed authentication attempt has occurred. + * + * @param filter the Authentication filer used for authentication + * @param servletRequest the request + * @param servletResponse the response + * @param cause the exception used to declare the cause for the failure + */ + void onUnsuccessfulAuthentication(AmbariAuthenticationFilter filter, HttpServletRequest servletRequest, + HttpServletResponse servletResponse, AmbariAuthenticationException cause); + + /** + * The event callback called just before an authentication attempt. + * + * @param filter the Authentication filer used for authentication + * @param servletRequest the request + * @param servletResponse the response + */ + void beforeAttemptAuthentication(AmbariAuthenticationFilter filter, ServletRequest servletRequest, + ServletResponse servletResponse); +} http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandlerImpl.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandlerImpl.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandlerImpl.java new file mode 100644 index 0000000..3a5a66b --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationEventHandlerImpl.java @@ -0,0 +1,152 @@ +/* + * 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.authentication; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.ambari.server.audit.AuditLogger; +import org.apache.ambari.server.audit.event.AuditEvent; +import org.apache.ambari.server.audit.event.LoginAuditEvent; +import org.apache.ambari.server.security.authorization.AuthorizationHelper; +import org.apache.ambari.server.security.authorization.PermissionHelper; +import org.apache.ambari.server.security.authorization.Users; +import org.apache.ambari.server.utils.RequestUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +/** + * AmbariAuthenticationEventHandlerImpl is the default {@link AmbariAuthenticationEventHandler} + * implementation. + * <p> + * This implementation tracks authentication attempts using the Ambari {@link AuditLogger} and + * ensures that the relevant user's consecutive authentication failure count is properly tracked. + * <p> + * Upon an authentication failure, the user's consecutive authentication failure count is incremented + * by <code>1</code> and upon a successful authentication, the user's consecutive authentication failure count + * is reset to <code>0</code>. + */ +@Singleton +public class AmbariAuthenticationEventHandlerImpl implements AmbariAuthenticationEventHandler { + private static final Logger LOG = LoggerFactory.getLogger(AmbariAuthenticationEventHandlerImpl.class); + /** + * Audit logger + */ + @Inject + private AuditLogger auditLogger; + + /** + * PermissionHelper to help create audit entries + */ + @Inject + private PermissionHelper permissionHelper; + + @Inject + private Users users; + + @Override + public void onSuccessfulAuthentication(AmbariAuthenticationFilter filter, HttpServletRequest servletRequest, HttpServletResponse servletResponse, Authentication result) { + String username = (result == null) ? null : result.getName(); + + // Using the Ambari audit logger, log this event (if enabled) + if (auditLogger.isEnabled()) { + AuditEvent loginSucceededAuditEvent = LoginAuditEvent.builder() + .withRemoteIp(RequestUtils.getRemoteAddress(servletRequest)) + .withUserName(username) + .withTimestamp(System.currentTimeMillis()) + .withRoles(permissionHelper.getPermissionLabels(result)) + .build(); + auditLogger.log(loginSucceededAuditEvent); + } + + // Reset the user's consecutive authentication failure count to 0. + if (!StringUtils.isEmpty(username)) { + LOG.debug("Successfully authenticated {}", username); + users.clearConsecutiveAuthenticationFailures(username); + } else { + LOG.warn("Successfully authenticated an unknown user"); + } + } + + @Override + public void onUnsuccessfulAuthentication(AmbariAuthenticationFilter filter, HttpServletRequest servletRequest, HttpServletResponse servletResponse, AmbariAuthenticationException cause) { + String username; + String message; + String logMessage; + Integer consecutiveFailures = null; + + if (cause == null) { + username = null; + message = "Unknown cause"; + } else { + username = cause.getUsername(); + message = cause.getLocalizedMessage(); + } + + // Increment the user's consecutive authentication failure count. + if (!StringUtils.isEmpty(username)) { + consecutiveFailures = users.incrementConsecutiveAuthenticationFailures(username); + logMessage = String.format("Failed to authenticate %s (attempt #%d): %s", username, consecutiveFailures, message); + } else { + logMessage = String.format("Failed to authenticate an unknown user: %s", message); + } + + if (LOG.isDebugEnabled()) { + LOG.debug(logMessage, cause); + } else { + LOG.info(logMessage); + } + + // Using the Ambari audit logger, log this event (if enabled) + if (auditLogger.isEnabled()) { + AuditEvent loginFailedAuditEvent = LoginAuditEvent.builder() + .withRemoteIp(RequestUtils.getRemoteAddress(servletRequest)) + .withTimestamp(System.currentTimeMillis()) + .withReasonOfFailure("Invalid username/password combination") + .withConsecutiveFailures(consecutiveFailures) + .withUserName(username) + .build(); + auditLogger.log(loginFailedAuditEvent); + } + } + + @Override + public void beforeAttemptAuthentication(AmbariAuthenticationFilter filter, ServletRequest servletRequest, ServletResponse servletResponse) { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + + // Using the Ambari audit logger, log this event (if enabled) + if (auditLogger.isEnabled() && filter.shouldApply(httpServletRequest) && (AuthorizationHelper.getAuthenticatedName() == null)) { + AuditEvent loginFailedAuditEvent = LoginAuditEvent.builder() + .withRemoteIp(RequestUtils.getRemoteAddress(httpServletRequest)) + .withTimestamp(System.currentTimeMillis()) + .withReasonOfFailure("Authentication required") + .withUserName(null) + .build(); + auditLogger.log(loginFailedAuditEvent); + } + + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationException.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationException.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationException.java new file mode 100644 index 0000000..fb18b9c --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariAuthenticationException.java @@ -0,0 +1,43 @@ +/* + * 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.authentication; + +import org.springframework.security.core.AuthenticationException; + +/** + * AmbariAuthenticationException is an AuthenticationException implementation to be thrown + * when the user fails to authenticate with Ambari. + */ +public class AmbariAuthenticationException extends AuthenticationException { + private final String username; + + public AmbariAuthenticationException(String username, String message) { + super(message); + this.username = username; + } + + public AmbariAuthenticationException(String username, String message, Throwable throwable) { + super(message, throwable); + this.username = username; + } + + public String getUsername() { + return username; + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariBasicAuthenticationFilter.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariBasicAuthenticationFilter.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariBasicAuthenticationFilter.java index ac3e15f..3667012 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariBasicAuthenticationFilter.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariBasicAuthenticationFilter.java @@ -26,13 +26,7 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.ambari.server.audit.AuditLogger; -import org.apache.ambari.server.audit.event.AuditEvent; -import org.apache.ambari.server.audit.event.LoginAuditEvent; import org.apache.ambari.server.security.AmbariEntryPoint; -import org.apache.ambari.server.security.authorization.AuthorizationHelper; -import org.apache.ambari.server.security.authorization.PermissionHelper; -import org.apache.ambari.server.utils.RequestUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AuthenticationManager; @@ -53,31 +47,25 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi public class AmbariBasicAuthenticationFilter extends BasicAuthenticationFilter implements AmbariAuthenticationFilter { private static final Logger LOG = LoggerFactory.getLogger(AmbariBasicAuthenticationFilter.class); - /** - * Audit logger - */ - private AuditLogger auditLogger; - - /** - * PermissionHelper to help create audit entries - */ - private PermissionHelper permissionHelper; + private final AmbariAuthenticationEventHandler eventHandler; /** * Constructor. * - * @param authenticationManager the Spring authencation manager + * @param authenticationManager the Spring authentication manager * @param ambariEntryPoint the Spring entry point - * @param auditLogger an Audit Logger - * @param permissionHelper a permission helper + * @param eventHandler the authentication event handler */ public AmbariBasicAuthenticationFilter(AuthenticationManager authenticationManager, AmbariEntryPoint ambariEntryPoint, - AuditLogger auditLogger, - PermissionHelper permissionHelper) { + AmbariAuthenticationEventHandler eventHandler) { super(authenticationManager, ambariEntryPoint); - this.auditLogger = auditLogger; - this.permissionHelper = permissionHelper; + + if(eventHandler == null) { + throw new IllegalArgumentException("The AmbariAuthenticationEventHandler must not be null"); + } + + this.eventHandler = eventHandler; } /** @@ -115,16 +103,9 @@ public class AmbariBasicAuthenticationFilter extends BasicAuthenticationFilter i */ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { - HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; - - if (auditLogger.isEnabled() && shouldApply(httpServletRequest) && (AuthorizationHelper.getAuthenticatedName() == null)) { - AuditEvent loginFailedAuditEvent = LoginAuditEvent.builder() - .withRemoteIp(RequestUtils.getRemoteAddress(httpServletRequest)) - .withTimestamp(System.currentTimeMillis()) - .withReasonOfFailure("Authentication required") - .withUserName(null) - .build(); - auditLogger.log(loginFailedAuditEvent); + + if (eventHandler != null) { + eventHandler.beforeAttemptAuthentication(this, servletRequest, servletResponse); } super.doFilter(servletRequest, servletResponse, chain); @@ -142,14 +123,9 @@ public class AmbariBasicAuthenticationFilter extends BasicAuthenticationFilter i protected void onSuccessfulAuthentication(HttpServletRequest servletRequest, HttpServletResponse servletResponse, Authentication authResult) throws IOException { - if (auditLogger.isEnabled()) { - AuditEvent loginSucceededAuditEvent = LoginAuditEvent.builder() - .withRemoteIp(RequestUtils.getRemoteAddress(servletRequest)) - .withUserName(authResult.getName()) - .withTimestamp(System.currentTimeMillis()) - .withRoles(permissionHelper.getPermissionLabels(authResult)) - .build(); - auditLogger.log(loginSucceededAuditEvent); + + if (eventHandler != null) { + eventHandler.onSuccessfulAuthentication(this, servletRequest, servletResponse, authResult); } } @@ -158,28 +134,30 @@ public class AmbariBasicAuthenticationFilter extends BasicAuthenticationFilter i * * @param servletRequest the request * @param servletResponse the response - * @param authExecption the exception, if any, causing the unsuccessful authentication attempt + * @param authException the exception, if any, causing the unsuccessful authentication attempt * @throws IOException */ @Override protected void onUnsuccessfulAuthentication(HttpServletRequest servletRequest, HttpServletResponse servletResponse, - AuthenticationException authExecption) throws IOException { - String header = servletRequest.getHeader("Authorization"); - String username = null; - try { - username = getUsernameFromAuth(header, getCredentialsCharset(servletRequest)); - } catch (Exception e) { - LOG.warn("Error occurred during decoding authorization header.", e); - } - if (auditLogger.isEnabled()) { - AuditEvent loginFailedAuditEvent = LoginAuditEvent.builder() - .withRemoteIp(RequestUtils.getRemoteAddress(servletRequest)) - .withTimestamp(System.currentTimeMillis()) - .withReasonOfFailure("Invalid username/password combination") - .withUserName(username) - .build(); - auditLogger.log(loginFailedAuditEvent); + AuthenticationException authException) throws IOException { + if (eventHandler != null) { + AmbariAuthenticationException cause; + if (authException instanceof AmbariAuthenticationException) { + cause = (AmbariAuthenticationException) authException; + } else { + String header = servletRequest.getHeader("Authorization"); + String username = null; + try { + username = getUsernameFromAuth(header, getCredentialsCharset(servletRequest)); + } catch (Exception e) { + LOG.warn("Error occurred during decoding authorization header.", e); + } + + cause = new AmbariAuthenticationException(username, authException.getMessage(), authException); + } + + eventHandler.onUnsuccessfulAuthentication(this, servletRequest, servletResponse, cause); } } http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariJWTAuthenticationFilter.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariJWTAuthenticationFilter.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariJWTAuthenticationFilter.java index fca8b29..3d35578 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariJWTAuthenticationFilter.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/AmbariJWTAuthenticationFilter.java @@ -27,15 +27,9 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.ambari.server.audit.AuditLogger; -import org.apache.ambari.server.audit.event.AuditEvent; -import org.apache.ambari.server.audit.event.LoginAuditEvent; import org.apache.ambari.server.configuration.Configuration; -import org.apache.ambari.server.security.authorization.AuthorizationHelper; -import org.apache.ambari.server.security.authorization.PermissionHelper; import org.apache.ambari.server.security.authorization.Users; import org.apache.ambari.server.security.authorization.jwt.JwtAuthenticationFilter; -import org.apache.ambari.server.utils.RequestUtils; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; @@ -51,14 +45,9 @@ import org.springframework.security.web.AuthenticationEntryPoint; public class AmbariJWTAuthenticationFilter extends JwtAuthenticationFilter implements AmbariAuthenticationFilter { /** - * Audit logger + * Ambari authentication event handler */ - private AuditLogger auditLogger; - - /** - * PermissionHelper to help create audit entries - */ - private PermissionHelper permissionHelper; + private final AmbariAuthenticationEventHandler eventHandler; /** @@ -67,17 +56,19 @@ public class AmbariJWTAuthenticationFilter extends JwtAuthenticationFilter imple * @param ambariEntryPoint the Spring entry point * @param configuration the Ambari configuration * @param users the Ambari users object - * @param auditLogger an Audit Logger - * @param permissionHelper a permission helper + * @param eventHandler the Ambari authentication event handler */ public AmbariJWTAuthenticationFilter(AuthenticationEntryPoint ambariEntryPoint, Configuration configuration, Users users, - AuditLogger auditLogger, - PermissionHelper permissionHelper) { + AmbariAuthenticationEventHandler eventHandler) { super(configuration, ambariEntryPoint, users); - this.auditLogger = auditLogger; - this.permissionHelper = permissionHelper; + + if(eventHandler == null) { + throw new IllegalArgumentException("The AmbariAuthenticationEventHandler must not be null"); + } + + this.eventHandler = eventHandler; } /** @@ -91,16 +82,9 @@ public class AmbariJWTAuthenticationFilter extends JwtAuthenticationFilter imple */ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { - HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; - - if (auditLogger.isEnabled() && shouldApply(httpServletRequest) && (AuthorizationHelper.getAuthenticatedName() == null)) { - AuditEvent loginFailedAuditEvent = LoginAuditEvent.builder() - .withRemoteIp(RequestUtils.getRemoteAddress(httpServletRequest)) - .withTimestamp(System.currentTimeMillis()) - .withReasonOfFailure("Authentication required") - .withUserName(null) - .build(); - auditLogger.log(loginFailedAuditEvent); + + if (eventHandler != null) { + eventHandler.beforeAttemptAuthentication(this, servletRequest, servletResponse); } super.doFilter(servletRequest, servletResponse, chain); @@ -108,32 +92,23 @@ public class AmbariJWTAuthenticationFilter extends JwtAuthenticationFilter imple @Override protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException { - if (auditLogger.isEnabled()) { - AuditEvent loginSucceededAuditEvent = LoginAuditEvent.builder() - .withRemoteIp(RequestUtils.getRemoteAddress(request)) - .withUserName(authResult.getName()) - .withTimestamp(System.currentTimeMillis()) - .withRoles(permissionHelper.getPermissionLabels(authResult)) - .build(); - auditLogger.log(loginSucceededAuditEvent); + if (eventHandler != null) { + eventHandler.onSuccessfulAuthentication(this, request, response, authResult); } } @Override protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { - if (auditLogger.isEnabled()) { - String username = null; - if (authException instanceof UserNotFoundException) { - username = ((UserNotFoundException) authException).getUsername(); + if (eventHandler != null) { + AmbariAuthenticationException cause; + + if (authException instanceof AmbariAuthenticationException) { + cause = (AmbariAuthenticationException) authException; + } else { + cause = new AmbariAuthenticationException(null, authException.getMessage(), authException); } - AuditEvent loginFailedAuditEvent = LoginAuditEvent.builder() - .withRemoteIp(RequestUtils.getRemoteAddress(request)) - .withTimestamp(System.currentTimeMillis()) - .withReasonOfFailure(authException.getLocalizedMessage()) - .withUserName(username) - .build(); - auditLogger.log(loginFailedAuditEvent); + eventHandler.onUnsuccessfulAuthentication(this, request, response, cause); } } } http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/InvalidUsernamePasswordCombinationException.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/InvalidUsernamePasswordCombinationException.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/InvalidUsernamePasswordCombinationException.java new file mode 100644 index 0000000..cb1babd --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/InvalidUsernamePasswordCombinationException.java @@ -0,0 +1,32 @@ +/* + * 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.authentication; + +public class InvalidUsernamePasswordCombinationException extends AmbariAuthenticationException { + + public static final String MESSAGE = "Unable to sign in. Invalid username/password combination."; + + public InvalidUsernamePasswordCombinationException(String username) { + super(username, MESSAGE); + } + + public InvalidUsernamePasswordCombinationException(String username, Throwable t) { + super(username, MESSAGE, t); + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/UserNotFoundException.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/UserNotFoundException.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/UserNotFoundException.java index f6c4bcf..0f2fbb6 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/UserNotFoundException.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/UserNotFoundException.java @@ -18,26 +18,17 @@ package org.apache.ambari.server.security.authentication; -import org.springframework.security.core.AuthenticationException; - /** * AuthenticationUserNotFoundException is an AuthenticationException implementation to be thrown * when the user specified in an authentication attempt is not found in the Ambari user database. */ -public class UserNotFoundException extends AuthenticationException { - private final String username; +public class UserNotFoundException extends AmbariAuthenticationException { public UserNotFoundException(String username, String message) { - super(message); - this.username = username; + super(username, message); } public UserNotFoundException(String username, String message, Throwable throwable) { - super(message, throwable); - this.username = username; - } - - public String getUsername() { - return username; + super(username, message, throwable); } } http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/kerberos/AmbariKerberosAuthenticationFilter.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/kerberos/AmbariKerberosAuthenticationFilter.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/kerberos/AmbariKerberosAuthenticationFilter.java index 1b001ec..23fa171 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/kerberos/AmbariKerberosAuthenticationFilter.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authentication/kerberos/AmbariKerberosAuthenticationFilter.java @@ -28,13 +28,10 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.ambari.server.audit.AuditLogger; -import org.apache.ambari.server.audit.event.AuditEvent; -import org.apache.ambari.server.audit.event.LoginAuditEvent; import org.apache.ambari.server.configuration.Configuration; +import org.apache.ambari.server.security.authentication.AmbariAuthenticationEventHandler; +import org.apache.ambari.server.security.authentication.AmbariAuthenticationException; import org.apache.ambari.server.security.authentication.AmbariAuthenticationFilter; -import org.apache.ambari.server.security.authorization.AuthorizationHelper; -import org.apache.ambari.server.security.authorization.PermissionHelper; -import org.apache.ambari.server.utils.RequestUtils; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -52,9 +49,9 @@ import org.springframework.security.web.authentication.AuthenticationSuccessHand public class AmbariKerberosAuthenticationFilter extends SpnegoAuthenticationProcessingFilter implements AmbariAuthenticationFilter { /** - * Audit logger + * Ambari authentication event handler */ - private final AuditLogger auditLogger; + private final AmbariAuthenticationEventHandler eventHandler; /** * A Boolean value indicating whether Kerberos authentication is enabled or not. @@ -70,30 +67,37 @@ public class AmbariKerberosAuthenticationFilter extends SpnegoAuthenticationProc * @param authenticationManager the Spring authentication manager * @param entryPoint the Spring entry point * @param configuration the Ambari configuration data - * @param auditLogger an audit logger - * @param permissionHelper a permission helper to aid in audit logging + * @param eventHandler the Ambari authentication event handler */ - public AmbariKerberosAuthenticationFilter(AuthenticationManager authenticationManager, final AuthenticationEntryPoint entryPoint, Configuration configuration, final AuditLogger auditLogger, final PermissionHelper permissionHelper) { + public AmbariKerberosAuthenticationFilter(AuthenticationManager authenticationManager, + final AuthenticationEntryPoint entryPoint, + Configuration configuration, + final AmbariAuthenticationEventHandler eventHandler) { AmbariKerberosAuthenticationProperties kerberosAuthenticationProperties = (configuration == null) ? null : configuration.getKerberosAuthenticationProperties(); kerberosAuthenticationEnabled = (kerberosAuthenticationProperties != null) && kerberosAuthenticationProperties.isKerberosAuthenticationEnabled(); - this.auditLogger = auditLogger; + if(eventHandler == null) { + throw new IllegalArgumentException("The AmbariAuthenticationEventHandler must not be null"); + } + + this.eventHandler = eventHandler; setAuthenticationManager(authenticationManager); setFailureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { - if (auditLogger.isEnabled()) { - AuditEvent loginFailedAuditEvent = LoginAuditEvent.builder() - .withRemoteIp(RequestUtils.getRemoteAddress(httpServletRequest)) - .withTimestamp(System.currentTimeMillis()) - .withReasonOfFailure(e.getLocalizedMessage()) - .build(); - auditLogger.log(loginFailedAuditEvent); + if (eventHandler != null) { + AmbariAuthenticationException cause; + if (e instanceof AmbariAuthenticationException) { + cause = (AmbariAuthenticationException) e; + } else { + cause = new AmbariAuthenticationException(null, e.getLocalizedMessage(), e); + } + eventHandler.onUnsuccessfulAuthentication(AmbariKerberosAuthenticationFilter.this, httpServletRequest, httpServletResponse, cause); } entryPoint.commence(httpServletRequest, httpServletResponse, e); @@ -103,14 +107,8 @@ public class AmbariKerberosAuthenticationFilter extends SpnegoAuthenticationProc setSuccessHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { - if (auditLogger.isEnabled()) { - AuditEvent loginSucceededAuditEvent = LoginAuditEvent.builder() - .withRemoteIp(RequestUtils.getRemoteAddress(httpServletRequest)) - .withUserName(authentication.getName()) - .withTimestamp(System.currentTimeMillis()) - .withRoles(permissionHelper.getPermissionLabels(authentication)) - .build(); - auditLogger.log(loginSucceededAuditEvent); + if (eventHandler != null) { + eventHandler.onSuccessfulAuthentication(AmbariKerberosAuthenticationFilter.this, httpServletRequest, httpServletResponse, authentication); } } }); @@ -152,22 +150,10 @@ public class AmbariKerberosAuthenticationFilter extends SpnegoAuthenticationProc */ @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; - - if (shouldApply(httpServletRequest)) { - if (auditLogger.isEnabled() && (AuthorizationHelper.getAuthenticatedName() == null)) { - AuditEvent loginFailedAuditEvent = LoginAuditEvent.builder() - .withRemoteIp(RequestUtils.getRemoteAddress(httpServletRequest)) - .withTimestamp(System.currentTimeMillis()) - .withReasonOfFailure("Authentication required") - .withUserName(null) - .build(); - auditLogger.log(loginFailedAuditEvent); - } - - super.doFilter(servletRequest, servletResponse, filterChain); - } else { - filterChain.doFilter(servletRequest, servletResponse); + if (eventHandler != null) { + eventHandler.beforeAttemptAuthentication(AmbariKerberosAuthenticationFilter.this, servletRequest, servletResponse); } + + super.doFilter(servletRequest, servletResponse, filterChain); } } http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java index 6137b68..caff735 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProvider.java @@ -24,6 +24,7 @@ import org.apache.ambari.server.orm.dao.UserDAO; import org.apache.ambari.server.orm.entities.UserAuthenticationEntity; import org.apache.ambari.server.orm.entities.UserEntity; import org.apache.ambari.server.security.ClientSecurityType; +import org.apache.ambari.server.security.authentication.InvalidUsernamePasswordCombinationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -93,7 +94,7 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider "connecting to LDAP server) are invalid.", e); } } - throw new InvalidUsernamePasswordCombinationException(e); + throw new InvalidUsernamePasswordCombinationException(username, e); } catch (IncorrectResultSizeDataAccessException multipleUsersFound) { String message = configuration.isLdapAlternateUserSearchEnabled() ? String.format("Login Failed: Please append your domain to your username and try again. Example: %s@domain", username) : @@ -204,7 +205,7 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider // lookup is case insensitive, so no need for string comparison if (userEntity == null) { LOG.info("user not found ('{}')", userName); - throw new InvalidUsernamePasswordCombinationException(); + throw new InvalidUsernamePasswordCombinationException(userName); } if (!userEntity.getActive()) { @@ -221,7 +222,7 @@ public class AmbariLdapAuthenticationProvider implements AuthenticationProvider LOG.debug("Failed to find LDAP authentication entry for {})", userName); } - throw new InvalidUsernamePasswordCombinationException(); + throw new InvalidUsernamePasswordCombinationException(userName); } } http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthoritiesPopulator.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthoritiesPopulator.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthoritiesPopulator.java index 5c482a1..4331f59 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthoritiesPopulator.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthoritiesPopulator.java @@ -25,6 +25,7 @@ import org.apache.ambari.server.orm.dao.PrivilegeDAO; import org.apache.ambari.server.orm.dao.UserDAO; import org.apache.ambari.server.orm.entities.PrivilegeEntity; import org.apache.ambari.server.orm.entities.UserEntity; +import org.apache.ambari.server.security.authentication.InvalidUsernamePasswordCombinationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ldap.core.DirContextOperations; @@ -71,7 +72,7 @@ public class AmbariLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator return Collections.emptyList(); } if(!user.getActive()){ - throw new InvalidUsernamePasswordCombinationException(); + throw new InvalidUsernamePasswordCombinationException(username); } Collection<PrivilegeEntity> privilegeEntities = users.getUserPrivileges(user); http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProvider.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProvider.java index 517efe4..2c8bf12 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProvider.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/AmbariLocalUserProvider.java @@ -22,6 +22,7 @@ import java.util.List; import org.apache.ambari.server.orm.dao.UserDAO; import org.apache.ambari.server.orm.entities.UserAuthenticationEntity; import org.apache.ambari.server.orm.entities.UserEntity; +import org.apache.ambari.server.security.authentication.InvalidUsernamePasswordCombinationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -66,17 +67,17 @@ public class AmbariLocalUserProvider extends AbstractUserDetailsAuthenticationPr if (userEntity == null) { LOG.info("user not found"); - throw new InvalidUsernamePasswordCombinationException(); + throw new InvalidUsernamePasswordCombinationException(userName); } if (!userEntity.getActive()) { LOG.debug("User account is disabled"); - throw new InvalidUsernamePasswordCombinationException(); + throw new InvalidUsernamePasswordCombinationException(userName); } if (authentication.getCredentials() == null) { LOG.debug("Authentication failed: no credentials provided"); - throw new InvalidUsernamePasswordCombinationException(); + throw new InvalidUsernamePasswordCombinationException(userName); } List<UserAuthenticationEntity> authenticationEntities = userEntity.getAuthenticationEntities(); @@ -98,7 +99,7 @@ public class AmbariLocalUserProvider extends AbstractUserDetailsAuthenticationPr // The user was not authenticated, fail LOG.debug("Authentication failed: password does not match stored value"); - throw new InvalidUsernamePasswordCombinationException(); + throw new InvalidUsernamePasswordCombinationException(userName); } @Override http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/InvalidUsernamePasswordCombinationException.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/InvalidUsernamePasswordCombinationException.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/InvalidUsernamePasswordCombinationException.java deleted file mode 100644 index db82381..0000000 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/InvalidUsernamePasswordCombinationException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 org.springframework.security.core.AuthenticationException; - -public class InvalidUsernamePasswordCombinationException extends AuthenticationException { - - public static final String MESSAGE = "Unable to sign in. Invalid username/password combination."; - - public InvalidUsernamePasswordCombinationException() { - super(MESSAGE); - } - - public InvalidUsernamePasswordCombinationException(Throwable t) { - super(MESSAGE, t); - } -} http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java index 35eb255..de12a16 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Set; import javax.persistence.EntityManager; +import javax.persistence.OptimisticLockException; import org.apache.ambari.server.AmbariException; import org.apache.ambari.server.hooks.HookContextFactory; @@ -71,6 +72,11 @@ public class Users { private static final Logger LOG = LoggerFactory.getLogger(Users.class); + /** + * The maximum number of retries when handling OptimisticLockExceptions + */ + private static final int MAX_RETRIES = 10; + @Inject private Provider<EntityManager> entityManagerProvider; @@ -238,9 +244,17 @@ public class Users { * @param active true if active; false if not active * @throws AmbariException if user does not exist */ - public synchronized void setUserActive(UserEntity userEntity, boolean active) throws AmbariException { - userEntity.setActive(active); - userDAO.merge(userEntity); + public synchronized void setUserActive(UserEntity userEntity, final boolean active) throws AmbariException { + if(userEntity != null) { + Command command = new Command() { + @Override + public void perform(UserEntity userEntity) { + userEntity.setActive(active); + } + }; + + safelyUpdateUserEntity(userEntity, command, MAX_RETRIES); + } } /** @@ -1252,13 +1266,148 @@ public class Users { } /** + * Increments the named user's consecutive authentication failure count by <code>1</code>. + * <p> + * This operation is safe when concurrent authentication attempts by the same username are made + * due to {@link UserEntity#version} and optimistic locking. + * + * @param username the user's username + * @return the updated number of consecutive authentication failures; or null if the user does not exist + */ + public Integer incrementConsecutiveAuthenticationFailures(String username) { + return incrementConsecutiveAuthenticationFailures(getUserEntity(username)); + } + + /** + * Increments the named user's consecutive authentication failure count by <code>1</code>. + * <p> + * This operation is safe when concurrent authentication attempts by the same username are made + * due to {@link UserEntity#version} and optimistic locking. + * + * @param userEntity the user + * @return the updated number of consecutive authentication failures; or null if the user does not exist + */ + public Integer incrementConsecutiveAuthenticationFailures(UserEntity userEntity) { + if (userEntity != null) { + Command command = new Command() { + @Override + public void perform(UserEntity userEntity) { + userEntity.incrementConsecutiveFailures(); + } + }; + + userEntity = safelyUpdateUserEntity(userEntity, command, MAX_RETRIES); + } + + return (userEntity == null) ? null : userEntity.getConsecutiveFailures(); + } + + /** + * Resets the named user's consecutive authentication failure count to <code>0</code>. + * <p> + * This operation is safe when concurrent authentication attempts by the same username are made + * due to {@link UserEntity#version} and optimistic locking. + * + * @param username the user's username + */ + public void clearConsecutiveAuthenticationFailures(String username) { + clearConsecutiveAuthenticationFailures(getUserEntity(username)); + } + + /** + * Resets the named user's consecutive authentication failure count to <code>0</code>. + * <p> + * This operation is safe when concurrent authentication attempts by the same username are made + * due to {@link UserEntity#version} and optimistic locking. + * + * @param userEntity the user + */ + public void clearConsecutiveAuthenticationFailures(UserEntity userEntity) { + if (userEntity != null) { + if (userEntity.getConsecutiveFailures() != 0) { + Command command = new Command() { + @Override + public void perform(UserEntity userEntity) { + userEntity.setConsecutiveFailures(0); + } + }; + + safelyUpdateUserEntity(userEntity, command, MAX_RETRIES); + } + } + } + + /*** + * Attempts to update the specified {@link UserEntity} while handling {@link OptimisticLockException}s + * by obtaining the latest version of the {@link UserEntity} and retrying the operation. + * + * If the maximum number of retries is exceeded, then the operation will fail by rethrowing the last + * exception encountered. + * + * + * @param userEntity the user entity + * @param command a command to perform on the user entity object that changes it state thus needing + * to be persisted + */ + private UserEntity safelyUpdateUserEntity(UserEntity userEntity, Command command, int maxRetries) { + int retriesLeft = maxRetries; + + do { + try { + command.perform(userEntity); + userDAO.merge(userEntity); + + // The merge was a success, break out of this loop and return + return userEntity; + } catch (Throwable t) { + Throwable cause = t; + + do { + if (cause instanceof OptimisticLockException) { + // An OptimisticLockException was caught, refresh the entity and retry. + Integer userID = userEntity.getUserId(); + + // Find the userEntity record to make sure the object is managed by JPA. The passed-in + // object may be detached, therefore calling reset on it will fail. + userEntity = userDAO.findByPK(userID); + + if (userEntity == null) { + LOG.warn("Failed to find user with user id of {}. The user may have been removed. Aborting.", userID); + return null; // return since this user is no longer available. + } + + retriesLeft--; + + // The the number of attempts has been exhausted, re-throw the exception + if (retriesLeft == 0) { + LOG.error("Failed to update the user's ({}) consecutive failures value due to an OptimisticLockException. Aborting.", + userEntity.getUserName()); + throw t; + } else { + LOG.warn("Failed to update the user's ({}) consecutive failures value due to an OptimisticLockException. {} retries left, retrying...", + userEntity.getUserName(), retriesLeft); + } + + break; + } else { + // Get the cause to see if it is an OptimisticLockException + cause = cause.getCause(); + } + } while ((cause != null) && (cause != t)); // We are out of causes + } + } while (retriesLeft > 0); // We are out of retries + + return userEntity; + } + + /** * Validator is an interface to be implemented by authentication type specific validators to ensure * new user authentication records meet the specific requirements for the relative authentication * type. */ private interface Validator { /** - * Valudate the authentication type specific key meets the requirments for the relative user + * Validate the authentication type specific key meets the requirements for the relative user * authentication type. * * @param userEntity the user @@ -1267,4 +1416,14 @@ public class Users { */ void validate(UserEntity userEntity, String key) throws AmbariException; } + + /** + * Command is an interface used to perform operations on a {@link UserEntity} while safely updating + * a {@link UserEntity} object. + * + * @see #safelyUpdateUserEntity(UserEntity, Command, int) + */ + private interface Command { + void perform(UserEntity userEntity); + } } http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/internal/AmbariInternalAuthenticationProvider.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/internal/AmbariInternalAuthenticationProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/internal/AmbariInternalAuthenticationProvider.java index c57bdf1..89b6333 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/internal/AmbariInternalAuthenticationProvider.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/internal/AmbariInternalAuthenticationProvider.java @@ -18,7 +18,7 @@ package org.apache.ambari.server.security.authorization.internal; -import org.apache.ambari.server.security.authorization.InvalidUsernamePasswordCombinationException; +import org.apache.ambari.server.security.authentication.InvalidUsernamePasswordCombinationException; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/jwt/JwtAuthenticationFilter.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/jwt/JwtAuthenticationFilter.java b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/jwt/JwtAuthenticationFilter.java index 3c3a446..f42df6c 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/jwt/JwtAuthenticationFilter.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/jwt/JwtAuthenticationFilter.java @@ -73,7 +73,7 @@ public class JwtAuthenticationFilter implements AmbariAuthenticationFilter { private List<String> audiences = null; private String cookieName = "hadoop-jwt"; - private boolean ignoreFailure = true; + private boolean ignoreFailure = false; private AuthenticationEntryPoint entryPoint; private Users users; http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/resources/Ambari-DDL-Derby-CREATE.sql ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/resources/Ambari-DDL-Derby-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-Derby-CREATE.sql index 0c86591..78f9aec 100644 --- a/ambari-server/src/main/resources/Ambari-DDL-Derby-CREATE.sql +++ b/ambari-server/src/main/resources/Ambari-DDL-Derby-CREATE.sql @@ -266,6 +266,7 @@ CREATE TABLE users ( display_name VARCHAR(255) NOT NULL, local_username VARCHAR(255) NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + version BIGINT NOT NULL DEFAULT 0, CONSTRAINT PK_users PRIMARY KEY (user_id), CONSTRAINT FK_users_principal_id FOREIGN KEY (principal_id) REFERENCES adminprincipal(principal_id), CONSTRAINT UNQ_users_0 UNIQUE (user_name)); http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql index 83b1f48..d3a3650 100644 --- a/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql +++ b/ambari-server/src/main/resources/Ambari-DDL-MySQL-CREATE.sql @@ -286,6 +286,7 @@ CREATE TABLE users ( display_name VARCHAR(255) NOT NULL, local_username VARCHAR(255) NOT NULL, create_time TIMESTAMP DEFAULT NOW(), + version BIGINT NOT NULL DEFAULT 0, CONSTRAINT PK_users PRIMARY KEY (user_id), CONSTRAINT FK_users_principal_id FOREIGN KEY (principal_id) REFERENCES adminprincipal(principal_id), CONSTRAINT UNQ_users_0 UNIQUE (user_name)); http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql index 215c01d..a27bc88 100644 --- a/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql +++ b/ambari-server/src/main/resources/Ambari-DDL-Oracle-CREATE.sql @@ -266,6 +266,7 @@ CREATE TABLE users ( display_name VARCHAR2(255) NOT NULL, local_username VARCHAR2(255) NOT NULL, create_time TIMESTAMP NULL, + version NUMBER(19) DEFAULT 0 NOT NULL, CONSTRAINT PK_users PRIMARY KEY (user_id), CONSTRAINT FK_users_principal_id FOREIGN KEY (principal_id) REFERENCES adminprincipal(principal_id), CONSTRAINT UNQ_users_0 UNIQUE (user_name)); http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/resources/Ambari-DDL-Postgres-CREATE.sql ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/resources/Ambari-DDL-Postgres-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-Postgres-CREATE.sql index 40ba709..e56cb04 100644 --- a/ambari-server/src/main/resources/Ambari-DDL-Postgres-CREATE.sql +++ b/ambari-server/src/main/resources/Ambari-DDL-Postgres-CREATE.sql @@ -265,6 +265,7 @@ CREATE TABLE users ( display_name VARCHAR(255) NOT NULL, local_username VARCHAR(255) NOT NULL, create_time TIMESTAMP DEFAULT NOW(), + version BIGINT DEFAULT 0 NOT NULL, CONSTRAINT PK_users PRIMARY KEY (user_id), CONSTRAINT FK_users_principal_id FOREIGN KEY (principal_id) REFERENCES adminprincipal(principal_id), CONSTRAINT UNQ_users_0 UNIQUE (user_name)); http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/resources/Ambari-DDL-SQLAnywhere-CREATE.sql ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/resources/Ambari-DDL-SQLAnywhere-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-SQLAnywhere-CREATE.sql index 35951f1..a1758e3 100644 --- a/ambari-server/src/main/resources/Ambari-DDL-SQLAnywhere-CREATE.sql +++ b/ambari-server/src/main/resources/Ambari-DDL-SQLAnywhere-CREATE.sql @@ -263,6 +263,7 @@ CREATE TABLE users ( display_name VARCHAR(255) NOT NULL, local_username VARCHAR(255) NOT NULL, create_time TIMESTAMP DEFAULT NOW(), + version NUMERIC(19) NOT NULL DEFAULT 0, CONSTRAINT PK_users PRIMARY KEY (user_id), CONSTRAINT FK_users_principal_id FOREIGN KEY (principal_id) REFERENCES adminprincipal(principal_id), CONSTRAINT UNQ_users_0 UNIQUE (user_name)); http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/resources/Ambari-DDL-SQLServer-CREATE.sql ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/resources/Ambari-DDL-SQLServer-CREATE.sql b/ambari-server/src/main/resources/Ambari-DDL-SQLServer-CREATE.sql index b7244ab..e794866 100644 --- a/ambari-server/src/main/resources/Ambari-DDL-SQLServer-CREATE.sql +++ b/ambari-server/src/main/resources/Ambari-DDL-SQLServer-CREATE.sql @@ -269,6 +269,7 @@ CREATE TABLE users ( display_name VARCHAR(255) NOT NULL, local_username VARCHAR(255) NOT NULL, create_time DATETIME DEFAULT GETDATE(), + version BIGINT NOT NULL DEFAULT 0, CONSTRAINT PK_users PRIMARY KEY (user_id), CONSTRAINT FK_users_principal_id FOREIGN KEY (principal_id) REFERENCES adminprincipal(principal_id), CONSTRAINT UNQ_users_0 UNIQUE (user_name)); http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/main/resources/webapp/WEB-INF/spring-security.xml ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/resources/webapp/WEB-INF/spring-security.xml b/ambari-server/src/main/resources/webapp/WEB-INF/spring-security.xml index bdbf0de..6650f67 100644 --- a/ambari-server/src/main/resources/webapp/WEB-INF/spring-security.xml +++ b/ambari-server/src/main/resources/webapp/WEB-INF/spring-security.xml @@ -55,24 +55,21 @@ <beans:bean id="ambariBasicAuthenticationFilter" class="org.apache.ambari.server.security.authentication.AmbariBasicAuthenticationFilter"> <beans:constructor-arg ref="authenticationManager"/> <beans:constructor-arg ref="ambariEntryPoint"/> - <beans:constructor-arg ref="auditLogger"/> - <beans:constructor-arg ref="permissionHelper"/> + <beans:constructor-arg ref="ambariAuthenticationEventHandler"/> </beans:bean> <beans:bean id="ambariJwtAuthenticationFilter" class="org.apache.ambari.server.security.authentication.AmbariJWTAuthenticationFilter"> <beans:constructor-arg ref="ambariEntryPoint"/> <beans:constructor-arg ref="ambariConfiguration"/> <beans:constructor-arg ref="ambariUsers"/> - <beans:constructor-arg ref="auditLogger"/> - <beans:constructor-arg ref="permissionHelper"/> + <beans:constructor-arg ref="ambariAuthenticationEventHandler"/> </beans:bean> <beans:bean id="ambariKerberosAuthenticationFilter" class="org.apache.ambari.server.security.authentication.kerberos.AmbariKerberosAuthenticationFilter"> <beans:constructor-arg ref="authenticationManager"/> <beans:constructor-arg ref="ambariEntryPoint"/> <beans:constructor-arg ref="ambariConfiguration"/> - <beans:constructor-arg ref="auditLogger"/> - <beans:constructor-arg ref="permissionHelper"/> + <beans:constructor-arg ref="ambariAuthenticationEventHandler"/> </beans:bean> <beans:bean id="ambariAuthorizationFilter" class="org.apache.ambari.server.security.authorization.AmbariAuthorizationFilter"> http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/test/java/org/apache/ambari/server/audit/LoginAuditEventTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/audit/LoginAuditEventTest.java b/ambari-server/src/test/java/org/apache/ambari/server/audit/LoginAuditEventTest.java index ac79967..2cff97e 100644 --- a/ambari-server/src/test/java/org/apache/ambari/server/audit/LoginAuditEventTest.java +++ b/ambari-server/src/test/java/org/apache/ambari/server/audit/LoginAuditEventTest.java @@ -69,6 +69,7 @@ public class LoginAuditEventTest { String testUserName = "USER1"; String testRemoteIp = "127.0.0.1"; String reason = "Bad credentials"; + Integer consecutiveFailures = 1; Map<String, List<String>> roles = new HashMap<>(); roles.put("a", Arrays.asList("r1", "r2", "r3")); @@ -79,6 +80,7 @@ public class LoginAuditEventTest { .withUserName(testUserName) .withRoles(roles) .withReasonOfFailure(reason) + .withConsecutiveFailures(consecutiveFailures) .build(); // When @@ -87,11 +89,41 @@ public class LoginAuditEventTest { String roleMessage = System.lineSeparator() + " a: r1, r2, r3" + System.lineSeparator(); // Then - String expectedAuditMessage = String.format("User(%s), RemoteIp(%s), Operation(User login), Roles(%s), Status(Failed), Reason(%s)", - testUserName, testRemoteIp, roleMessage, reason); + String expectedAuditMessage = String.format("User(%s), RemoteIp(%s), Operation(User login), Roles(%s), Status(Failed), Reason(%s), Consecutive failures(%d)", + testUserName, testRemoteIp, roleMessage, reason, consecutiveFailures); assertThat(actualAuditMessage, equalTo(expectedAuditMessage)); + } + + @Test + public void testFailedAuditMessageUnknownUser() throws Exception { + // Given + String testUserName = "USER1"; + String testRemoteIp = "127.0.0.1"; + String reason = "Bad credentials"; + + Map<String, List<String>> roles = new HashMap<>(); + roles.put("a", Arrays.asList("r1", "r2", "r3")); + + LoginAuditEvent evnt = LoginAuditEvent.builder() + .withTimestamp(System.currentTimeMillis()) + .withRemoteIp(testRemoteIp) + .withUserName(testUserName) + .withRoles(roles) + .withReasonOfFailure(reason) + .withConsecutiveFailures(null) + .build(); + + // When + String actualAuditMessage = evnt.getAuditMessage(); + + String roleMessage = System.lineSeparator() + " a: r1, r2, r3" + System.lineSeparator(); + // Then + String expectedAuditMessage = String.format("User(%s), RemoteIp(%s), Operation(User login), Roles(%s), Status(Failed), Reason(%s), Consecutive failures(UNKNOWN USER)", + testUserName, testRemoteIp, roleMessage, reason); + + assertThat(actualAuditMessage, equalTo(expectedAuditMessage)); } @Test http://git-wip-us.apache.org/repos/asf/ambari/blob/f760516c/ambari-server/src/test/java/org/apache/ambari/server/security/authentication/AmbariBasicAuthenticationFilterTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/security/authentication/AmbariBasicAuthenticationFilterTest.java b/ambari-server/src/test/java/org/apache/ambari/server/security/authentication/AmbariBasicAuthenticationFilterTest.java index 18c4cce..ed4c383 100644 --- a/ambari-server/src/test/java/org/apache/ambari/server/security/authentication/AmbariBasicAuthenticationFilterTest.java +++ b/ambari-server/src/test/java/org/apache/ambari/server/security/authentication/AmbariBasicAuthenticationFilterTest.java @@ -18,110 +18,134 @@ package org.apache.ambari.server.security.authentication; import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.capture; +import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.getCurrentArguments; +import static org.easymock.EasyMock.newCapture; import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; -import org.apache.ambari.server.audit.AuditLogger; -import org.apache.ambari.server.audit.event.AuditEvent; import org.apache.ambari.server.security.AmbariEntryPoint; -import org.apache.ambari.server.security.authorization.PermissionHelper; +import org.easymock.Capture; +import org.easymock.CaptureType; import org.easymock.EasyMockSupport; +import org.easymock.IAnswer; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.crypto.codec.Base64; public class AmbariBasicAuthenticationFilterTest extends EasyMockSupport { - private AmbariBasicAuthenticationFilter underTest; - - private AuditLogger mockedAuditLogger; - - private PermissionHelper permissionHelper; + private AmbariAuthenticationEventHandler eventHandler; private AmbariEntryPoint entryPoint; + private AuthenticationManager authenticationManager; + @Before public void setUp() { - mockedAuditLogger = createMock(AuditLogger.class); - permissionHelper = createMock(PermissionHelper.class); + SecurityContextHolder.getContext().setAuthentication(null); + + eventHandler = createMock(AmbariAuthenticationEventHandler.class); entryPoint = createMock(AmbariEntryPoint.class); - underTest = new AmbariBasicAuthenticationFilter(null, entryPoint, mockedAuditLogger, permissionHelper); + authenticationManager = createMock(AuthenticationManager.class); + } + + @Test (expected = IllegalArgumentException.class) + public void ensureNonNullEventHandler() { + new AmbariBasicAuthenticationFilter(authenticationManager, entryPoint, null); } @Test - public void testDoFilter() throws IOException, ServletException { - SecurityContextHolder.getContext().setAuthentication(null); + public void testDoFilterSuccessful() throws IOException, ServletException { + Capture<? extends AmbariAuthenticationFilter> captureFilter = newCapture(CaptureType.ALL); + // GIVEN HttpServletRequest request = createMock(HttpServletRequest.class); HttpServletResponse response = createMock(HttpServletResponse.class); + HttpSession session = createMock(HttpSession.class); FilterChain filterChain = createMock(FilterChain.class); - expect(request.getHeader("Authorization")).andReturn("Basic ").andReturn(null); - expect(request.getHeader("X-Forwarded-For")).andReturn("1.2.3.4").anyTimes(); - expect(mockedAuditLogger.isEnabled()).andReturn(true).anyTimes(); - mockedAuditLogger.log(anyObject(AuditEvent.class)); - expectLastCall().times(1); + + expect(request.getHeader("Authorization")).andReturn("Basic YWRtaW46YWRtaW4=").once(); + expect(request.getRemoteAddr()).andReturn("1.2.3.4").once(); + expect(request.getSession(false)).andReturn(session).once(); + expect(session.getId()).andReturn("sessionID").once(); + expect(authenticationManager.authenticate(anyObject(Authentication.class))) + .andAnswer(new IAnswer<Authentication>() { + @Override + public Authentication answer() throws Throwable { + return (Authentication) getCurrentArguments()[0]; + } + }) + .anyTimes(); + + eventHandler.beforeAttemptAuthentication(capture(captureFilter), eq(request), eq(response)); + expectLastCall().once(); + eventHandler.onSuccessfulAuthentication(capture(captureFilter), eq(request), eq(response), anyObject(Authentication.class)); + expectLastCall().once(); + filterChain.doFilter(request, response); - expectLastCall(); - replayAll(); - // WHEN - underTest.doFilter(request, response, filterChain); - // THEN - verifyAll(); - } + expectLastCall().once(); - @Test - public void testOnSuccessfulAuthentication() throws IOException, ServletException { - // GIVEN - HttpServletRequest request = createMock(HttpServletRequest.class); - HttpServletResponse response = createMock(HttpServletResponse.class); - Authentication authentication = createMock(Authentication.class); - - Map<String, List<String>> roles = new HashMap<>(); - roles.put("a", Arrays.asList("r1", "r2", "r3")); - expect(permissionHelper.getPermissionLabels(authentication)) - .andReturn(roles); - expect(request.getHeader("X-Forwarded-For")).andReturn("1.2.3.4"); - expect(authentication.getName()).andReturn("admin"); - expect(mockedAuditLogger.isEnabled()).andReturn(true); - mockedAuditLogger.log(anyObject(AuditEvent.class)); - expectLastCall().times(1); replayAll(); // WHEN - underTest.onSuccessfulAuthentication(request, response, authentication); + AmbariAuthenticationFilter filter = new AmbariBasicAuthenticationFilter(authenticationManager, entryPoint, eventHandler); + filter.doFilter(request, response, filterChain); // THEN verifyAll(); + + List<? extends AmbariAuthenticationFilter> capturedFilters = captureFilter.getValues(); + for (AmbariAuthenticationFilter capturedFiltered : capturedFilters) { + Assert.assertSame(filter, capturedFiltered); + } } @Test - public void testOnUnsuccessfulAuthentication() throws IOException, ServletException { + public void testDoFilterUnsuccessful() throws IOException, ServletException { + Capture<? extends AmbariAuthenticationFilter> captureFilter = newCapture(CaptureType.ALL); + // GIVEN HttpServletRequest request = createMock(HttpServletRequest.class); HttpServletResponse response = createMock(HttpServletResponse.class); - AuthenticationException authEx = createMock(AuthenticationException.class); - expect(request.getHeader("X-Forwarded-For")).andReturn("1.2.3.4"); - expect(request.getHeader("Authorization")).andReturn( - "Basic " + new String(Base64.encode("admin:admin".getBytes("UTF-8")))); - expect(mockedAuditLogger.isEnabled()).andReturn(true); - mockedAuditLogger.log(anyObject(AuditEvent.class)); - expectLastCall().times(1); + HttpSession session = createMock(HttpSession.class); + FilterChain filterChain = createMock(FilterChain.class); + + expect(request.getHeader("Authorization")).andReturn("Basic YWRtaW46YWRtaW4=").once(); + expect(request.getRemoteAddr()).andReturn("1.2.3.4").once(); + expect(request.getSession(false)).andReturn(session).once(); + expect(session.getId()).andReturn("sessionID").once(); + expect(authenticationManager.authenticate(anyObject(Authentication.class))).andThrow(new InvalidUsernamePasswordCombinationException("user")).once(); + + eventHandler.beforeAttemptAuthentication(capture(captureFilter), eq(request), eq(response)); + expectLastCall().once(); + eventHandler.onUnsuccessfulAuthentication(capture(captureFilter), eq(request), eq(response), anyObject(AmbariAuthenticationException.class)); + expectLastCall().once(); + + entryPoint.commence(eq(request), eq(response), anyObject(AmbariAuthenticationException.class)); + expectLastCall().once(); + replayAll(); // WHEN - underTest.onUnsuccessfulAuthentication(request, response, authEx); + AmbariAuthenticationFilter filter = new AmbariBasicAuthenticationFilter(authenticationManager, entryPoint, eventHandler); + filter.doFilter(request, response, filterChain); // THEN verifyAll(); + + List<? extends AmbariAuthenticationFilter> capturedFilters = captureFilter.getValues(); + for (AmbariAuthenticationFilter capturedFiltered : capturedFilters) { + Assert.assertSame(filter, capturedFiltered); + } } }
