/* * 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 java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; 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.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; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.codec.Base64; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; /** * AmbariBasicAuthenticationFilter extends a {@link BasicAuthenticationFilter} to allow for auditing * of authentication attempts * <p> * This authentication filter is expected to be used withing an {@link AmbariDelegatingAuthenticationFilter}. * * @see AmbariDelegatingAuthenticationFilter */ 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; /** * Constructor. * * @param authenticationManager the Spring authencation manager * @param ambariEntryPoint the Spring entry point * @param auditLogger an Audit Logger * @param permissionHelper a permission helper */ public AmbariBasicAuthenticationFilter(AuthenticationManager authenticationManager, AmbariEntryPoint ambariEntryPoint, AuditLogger auditLogger, PermissionHelper permissionHelper) { super(authenticationManager, ambariEntryPoint); this.auditLogger = auditLogger; this.permissionHelper = permissionHelper; } /** * Tests to see if this {@link AmbariBasicAuthenticationFilter} should be applied in the authentication * filter chain. * <p> * <code>true</code> will be returned if the HTTP request contains the basic authentication header; * otherwise <code>false</code> will be returned. * <p> * The basic authentication header is named "Authorization" and the value begins with the string * "Basic" following by the encoded username and password information. * <p> * For example: * <code> * Authorization: Basic YWRtaW46YWRtaW4= * </code> * * @param httpServletRequest the HttpServletRequest the HTTP service request * @return <code>true</code> if the HTTP request contains the basic authentication header; otherwise <code>false</code> */ @Override public boolean shouldApply(HttpServletRequest httpServletRequest) { String header = httpServletRequest.getHeader("Authorization"); return (header != null) && header.startsWith("Basic "); } /** * Checks whether the authentication information is filled. If it is not, then a login failed audit event is logged * * @param servletRequest the request * @param servletResponse the response * @param chain the Spring filter chain * @throws IOException * @throws ServletException */ @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); } super.doFilter(servletRequest, servletResponse, chain); } /** * If the authentication was successful, then an audit event is logged about the success * * @param servletRequest the request * @param servletResponse the response * @param authResult the Authentication result * @throws IOException */ @Override 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); } } /** * In the case of invalid username or password, the authentication fails and it is logged * * @param servletRequest the request * @param servletResponse the response * @param authExecption 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); } } /** * Helper function to decode Authorization header * * @param authenticationValue the authentication value to parse * @param charSet the character set of the authentication value * @return the username parsed from the authentication header value * @throws IOException */ private String getUsernameFromAuth(String authenticationValue, String charSet) throws IOException { byte[] base64Token = authenticationValue.substring(6).getBytes("UTF-8"); byte[] decoded; try { decoded = Base64.decode(base64Token); } catch (IllegalArgumentException ex) { throw new BadCredentialsException("Failed to decode basic authentication token"); } String token = new String(decoded, charSet); int delimiter = token.indexOf(":"); if (delimiter == -1) { throw new BadCredentialsException("Invalid basic authentication token"); } else { return token.substring(0, delimiter); } } }