content: /*
   * 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 jakarta.servlet.FilterChain;
  import jakarta.servlet.ServletException;
  import jakarta.servlet.http.HttpServletRequest;
  import jakarta.servlet.http.HttpServletResponse;
  
  import org.apache.ambari.server.security.AmbariEntryPoint;
  import org.apache.ambari.server.utils.RequestUtils;
  import org.apache.commons.lang.StringUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.springframework.core.annotation.Order;
  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;
  import org.springframework.stereotype.Component;
  
  /**
   * 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
   */
  @Component
  @Order(3)
  public class AmbariBasicAuthenticationFilter extends BasicAuthenticationFilter implements AmbariAuthenticationFilter {
    private static final Logger LOG = LoggerFactory.getLogger(AmbariBasicAuthenticationFilter.class);
  
    private final AmbariAuthenticationEventHandler eventHandler;
  
    /**
     * Constructor.
     *
     * @param authenticationManager the Spring authentication manager
     * @param ambariEntryPoint      the Spring entry point
     * @param eventHandler          the authentication event handler
     */
    public AmbariBasicAuthenticationFilter(AuthenticationManager authenticationManager,
                                           AmbariEntryPoint ambariEntryPoint,
                                           AmbariAuthenticationEventHandler eventHandler) {
      super(authenticationManager, ambariEntryPoint);
  
      if (eventHandler == null) {
        throw new IllegalArgumentException("The AmbariAuthenticationEventHandler must not be null");
      }
  
      this.eventHandler = eventHandler;
    }
  
    /**
     * 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) {
  
      if (LOG.isDebugEnabled()) {
        RequestUtils.logRequestHeadersAndQueryParams(httpServletRequest, LOG);
      }
  
      String header = httpServletRequest.getHeader("Authorization");
      if ((header != null) && header.startsWith("Basic ")) {
        // If doAs is sent as a query parameter, ignore the basic auth header.
        // This logic is here to help deal with a potential issue when Knox is the trusted proxy and it
        // forwards the original request's Authorization header (for Basic Auth) when Kerberos authentication
        // is required.
        String doAsQueryParameterValue = RequestUtils.getQueryStringParameterValue(httpServletRequest, "doAs");
        if (StringUtils.isEmpty(doAsQueryParameterValue)) {
          return true;
        } else {
          LOG.warn("The 'doAs' query parameter was provided; however, the BasicAuth header is found. " +
              "Ignoring the BasicAuth header hoping to negotiate Kerberos authentication.");
          return false;
        }
      } else {
        return false;
      }
    }
  
    @Override
    public boolean shouldIncrementFailureCount() {
      return true;
    }
  
    /**
     * Checks whether the authentication information is filled. If it is not, then a login failed audit event is logged
     *
     * @param httpServletRequest  the request
     * @param httpServletResponse the response
     * @param filterChain         the Spring filter chain
     * @throws IOException
     * @throws ServletException
     */
    public void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws IOException, ServletException {
      if (eventHandler != null) {
        eventHandler.beforeAttemptAuthentication(this, httpServletRequest, httpServletResponse);
      }
  
      super.doFilterInternal(httpServletRequest, httpServletResponse, filterChain);
    }
  
    /**
     * 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 (eventHandler != null) {
        eventHandler.onSuccessfulAuthentication(this, servletRequest, servletResponse, authResult);
      }
    }
  
    /**
     * In the case of invalid username or password, the authentication fails and it is logged
     *
     * @param servletRequest  the request
     * @param servletResponse the response
     * @param authException   the exception, if any, causing the unsuccessful authentication attempt
     * @throws IOException
     */
    @Override
    protected void onUnsuccessfulAuthentication(HttpServletRequest servletRequest,
                                                HttpServletResponse servletResponse,
                                                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(), false, authException);
        }
  
        eventHandler.onUnsuccessfulAuthentication(this, servletRequest, servletResponse, cause);
      }
    }
  
    /**
     * 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);
      }
    }
  
  
  }
  