/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.ambari.server.security.authorization;

import java.io.IOException;
import java.security.Principal;
import java.util.EnumSet;
import java.util.regex.Pattern;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.apache.ambari.server.audit.AuditLogger;
import org.apache.ambari.server.audit.event.AccessUnauthorizedAuditEvent;
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.orm.entities.PermissionEntity;
import org.apache.ambari.server.orm.entities.PrivilegeEntity;
import org.apache.ambari.server.security.AmbariEntryPoint;
import org.apache.ambari.server.security.authorization.internal.InternalAuthenticationToken;
import org.apache.ambari.server.utils.RequestUtils;
import org.apache.ambari.server.view.ViewRegistry;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Component
public class AmbariAuthorizationFilter implements Filter {
  private static final String REALM_PARAM = "realm";
  private static final String DEFAULT_REALM = "AuthFilter";

  private static final String INTERNAL_TOKEN_HEADER = "X-Internal-Token";

  private static final Pattern STACK_ADVISOR_REGEX = Pattern.compile("/api/v[0-9]+/stacks/[^/]+/versions/[^/]+/(validations|recommendations).*");

  public static final String API_VERSION_PREFIX = "/api/v[0-9]+";
  public static final String VIEWS_CONTEXT_PATH_PREFIX = "/views/";

  private static final String VIEWS_CONTEXT_PATH_PATTERN = VIEWS_CONTEXT_PATH_PREFIX + "([^/]+)/([^/]+)/([^/]+)(.*)";
  private static final String VIEWS_CONTEXT_ALL_PATTERN = VIEWS_CONTEXT_PATH_PREFIX + ".*";
  private static final String API_USERS_ALL_PATTERN = API_VERSION_PREFIX + "/users.*";
  private static final String API_PRIVILEGES_ALL_PATTERN = API_VERSION_PREFIX + "/privileges.*";
  private static final String API_GROUPS_ALL_PATTERN = API_VERSION_PREFIX + "/groups.*";
  private static final String API_CLUSTERS_PATTERN = API_VERSION_PREFIX + "/clusters/(\\w+/?)?";
  private static final String API_WIDGET_LAYOUTS_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/widget_layouts.*?";
  private static final String API_WIDGET_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/widgets.*";
  private static final String API_CLUSTERS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters.*";
  private static final String API_VIEWS_ALL_PATTERN = API_VERSION_PREFIX + "/views.*";
  private static final String API_AUTH_PATTERN = API_VERSION_PREFIX + "/auth";
  private static final String API_PERSIST_ALL_PATTERN = API_VERSION_PREFIX + "/persist.*";
  private static final String API_LDAP_SYNC_EVENTS_ALL_PATTERN = API_VERSION_PREFIX + "/ldap_sync_events.*";
  private static final String API_CREDENTIALS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/credentials.*";
  private static final String API_CREDENTIALS_AMBARI_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/credentials/ambari\\..*";
  private static final String API_CLUSTER_REQUESTS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/requests.*";
  private static final String API_CLUSTER_SERVICES_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/services.*";
  private static final String API_CLUSTER_ALERT_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/alert.*";
  private static final String API_CLUSTER_HOSTS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/hosts.*";
  private static final String API_CLUSTER_CONFIGURATIONS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/configurations.*";
  private static final String API_CLUSTER_COMPONENTS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/components.*";
  private static final String API_CLUSTER_HOST_COMPONENTS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/host_components.*";
  private static final String API_CLUSTER_CONFIG_GROUPS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/config_groups.*";
  private static final String API_STACK_VERSIONS_PATTERN = API_VERSION_PREFIX + "/stacks/.*?/versions/.*";
  private static final String API_HOSTS_ALL_PATTERN = API_VERSION_PREFIX + "/hosts.*";
  private static final String API_ALERT_TARGETS_ALL_PATTERN = API_VERSION_PREFIX + "/alert_targets.*";
  private static final String API_BOOTSTRAP_PATTERN_ALL = API_VERSION_PREFIX + "/bootstrap.*";
  private static final String API_REQUESTS_ALL_PATTERN = API_VERSION_PREFIX + "/requests.*";
  private static final String API_CLUSTERS_UPGRADES_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/upgrades.*";

  protected static final String LOGIN_REDIRECT_BASE = "/#/login?targetURI=";

  /**
   * The Ambari authentication entry point
   */
  private final AmbariEntryPoint entryPoint;

  /**
   * Access to Ambari configuration data
   */
  private final Configuration configuration;

  /**
   * Access to user information
   */
  private final Users users;

  /**
   * The audit logger
   */
  private final AuditLogger auditLogger;

  /**
   * A Permission Helper used to provided inforamtion for the Audit Logger
   */
  private final PermissionHelper permissionHelper;

  /**
   * The realm to use for the basic http auth
   */
  private String realm;

  /**
   * Constructor.
   *
   * @param entryPoint       the authentication entrypoint
   * @param configuration    the Ambari configuration
   * @param users            Ambari user access
   * @param auditLogger      the Audit logger
   * @param permissionHelper the permission helper
   */
  public AmbariAuthorizationFilter(AmbariEntryPoint entryPoint,
                                   Configuration configuration,
                                   Users users,
                                   AuditLogger auditLogger,
                                   PermissionHelper permissionHelper) {
    this.entryPoint = entryPoint;
    this.configuration = configuration;
    this.users = users;
    this.auditLogger = auditLogger;
    this.permissionHelper = permissionHelper;
  }

  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    realm = getParameterValue(filterConfig, REALM_PARAM, DEFAULT_REALM);
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletResponse httpResponse = (HttpServletResponse) response;

    String requestURI = httpRequest.getRequestURI();

    SecurityContext context = getSecurityContext();

    Authentication authentication = context.getAuthentication();

    AuditEvent auditEvent = null;

    //  If no explicit authenticated user is set, set it to the default user (if one is specified)
    if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
      Authentication defaultAuthentication = getDefaultAuthentication();
      if (defaultAuthentication != null) {
        context.setAuthentication(defaultAuthentication);
        authentication = defaultAuthentication;
      }
    }
    if (authentication == null || authentication instanceof AnonymousAuthenticationToken ||
        !authentication.isAuthenticated()) {
      String token = httpRequest.getHeader(INTERNAL_TOKEN_HEADER);
      if (token != null) {
        InternalAuthenticationToken internalAuthenticationToken = new InternalAuthenticationToken(token);
        context.setAuthentication(internalAuthenticationToken);
        if(auditLogger.isEnabled()) {
          LoginAuditEvent loginAuditEvent = LoginAuditEvent.builder()
            .withUserName(internalAuthenticationToken.getName())
            .withProxyUserName(AuthorizationHelper.getProxyUserName(internalAuthenticationToken))
            .withRemoteIp(RequestUtils.getRemoteAddress(httpRequest))
            .withRoles(permissionHelper.getPermissionLabels(authentication))
            .withTimestamp(System.currentTimeMillis()).build();
          auditLogger.log(loginAuditEvent);
        }
      } else {
        // for view access, we should redirect to the Ambari login
        if (requestURI.matches(VIEWS_CONTEXT_ALL_PATTERN)) {
          String queryString = httpRequest.getQueryString();
          String requestedURL = queryString == null ? requestURI : (requestURI + '?' + queryString);
          String redirectURL = httpResponse.encodeRedirectURL(LOGIN_REDIRECT_BASE + requestedURL);
          httpResponse.sendRedirect(redirectURL);
        } else {
          entryPoint.commence(httpRequest, httpResponse, new AuthenticationCredentialsNotFoundException("Missing authentication token"));
        }
        return;
      }
    } else if (!authorizationPerformedInternally(requestURI)) {
      boolean authorized = false;

      if (requestURI.matches(API_BOOTSTRAP_PATTERN_ALL)) {
        authorized = AuthorizationHelper.isAuthorized(authentication,
            ResourceType.CLUSTER,
            null,
            EnumSet.of(RoleAuthorization.HOST_ADD_DELETE_HOSTS));
      }
      else {
        for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
          if (grantedAuthority instanceof AmbariGrantedAuthority) {

            AmbariGrantedAuthority ambariGrantedAuthority = (AmbariGrantedAuthority) grantedAuthority;

            PrivilegeEntity privilegeEntity = ambariGrantedAuthority.getPrivilegeEntity();
            Integer permissionId = privilegeEntity.getPermission().getId();

            // admin has full access
            if (permissionId.equals(PermissionEntity.AMBARI_ADMINISTRATOR_PERMISSION)) {
              authorized = true;
              break;
            }

            // clusters require permission
            if (!"GET".equalsIgnoreCase(httpRequest.getMethod()) && requestURI.matches(API_CREDENTIALS_AMBARI_PATTERN)) {
              // Only the administrator can operate on credentials where the alias starts with "ambari."
              if (permissionId.equals(PermissionEntity.AMBARI_ADMINISTRATOR_PERMISSION)) {
                authorized = true;
                break;
              }
            } else if (requestURI.matches(API_CLUSTERS_ALL_PATTERN)) {
              if (permissionId.equals(PermissionEntity.CLUSTER_USER_PERMISSION) ||
                  permissionId.equals(PermissionEntity.CLUSTER_ADMINISTRATOR_PERMISSION)) {
                authorized = true;
                break;
              }
            } else if (STACK_ADVISOR_REGEX.matcher(requestURI).matches()) {
              //TODO permissions model doesn't manage stacks api, but we need access to stack advisor to save configs
              if (permissionId.equals(PermissionEntity.CLUSTER_USER_PERMISSION) ||
                  permissionId.equals(PermissionEntity.CLUSTER_ADMINISTRATOR_PERMISSION)) {
                authorized = true;
                break;
              }
            } else if (requestURI.matches(API_VIEWS_ALL_PATTERN)) {
              // views require permission
              if (permissionId.equals(PermissionEntity.VIEW_USER_PERMISSION)) {
                authorized = true;
                break;
              }
            } else if (requestURI.matches(API_AUTH_PATTERN) && "POST".equalsIgnoreCase(httpRequest.getMethod())) {
              // all are able to login
              authorized = true;
              break;
            }
          }
        }

        // Allow all GETs that are not LDAP sync events...
        authorized = authorized || (httpRequest.getMethod().equals("GET") && !requestURI.matches(API_LDAP_SYNC_EVENTS_ALL_PATTERN));
      }

      if (!authorized) {
        if(auditLogger.isEnabled()) {
          auditEvent = AccessUnauthorizedAuditEvent.builder()
            .withHttpMethodName(httpRequest.getMethod())
            .withRemoteIp(RequestUtils.getRemoteAddress(httpRequest))
            .withResourcePath(httpRequest.getRequestURI())
            .withUserName(AuthorizationHelper.getAuthenticatedName())
            .withProxyUserName(AuthorizationHelper.getProxyUserName())
            .withTimestamp(System.currentTimeMillis())
            .build();
          auditLogger.log(auditEvent);
        }

        httpResponse.setHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
        httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "You do not have permissions to access this resource.");
        httpResponse.flushBuffer();
        return;
      }
    }
    if (AuthorizationHelper.getAuthenticatedName() != null) {
      httpResponse.setHeader("User", AuthorizationHelper.getAuthenticatedName());
      if (auditLogger.isEnabled() && httpResponse.getStatus() == HttpServletResponse.SC_FORBIDDEN) {
        auditEvent = AccessUnauthorizedAuditEvent.builder()
          .withHttpMethodName(httpRequest.getMethod())
          .withRemoteIp(RequestUtils.getRemoteAddress(httpRequest))
          .withResourcePath(httpRequest.getRequestURI())
          .withUserName(AuthorizationHelper.getAuthenticatedName())
          .withProxyUserName(AuthorizationHelper.getProxyUserName())
          .withTimestamp(System.currentTimeMillis())
          .build();
        auditLogger.log(auditEvent);
      }
    }
    chain.doFilter(request, response);
  }

  /**
   * Creates the default Authentication if a default user is configured
   *
   * @return an Authentication representing the default user
   */
  private Authentication getDefaultAuthentication() {
    Authentication defaultUser = null;

    if ((configuration != null) && (users != null)) {
      String username = configuration.getDefaultApiAuthenticatedUser();

      if (!StringUtils.isEmpty(username)) {
        final User user = users.getUser(username);

        if (user != null) {
          Principal principal = new Principal() {
            @Override
            public String getName() {
              return user.getUserName();
            }
          };

          defaultUser = new UsernamePasswordAuthenticationToken(principal, null,
              users.getUserAuthorities(user.getUserName()));
        }
      }
    }

    return defaultUser;
  }

  /**
   * Tests the URI to determine if authorization checks are performed internally or should be
   * performed in the filter.
   *
   * @param requestURI the request uri
   * @return true if handled internally; otherwise false
   */
  private boolean authorizationPerformedInternally(String requestURI) {
    return requestURI.matches(API_USERS_ALL_PATTERN) ||
        requestURI.matches(API_REQUESTS_ALL_PATTERN) ||
        requestURI.matches(API_GROUPS_ALL_PATTERN) ||
        requestURI.matches(API_CREDENTIALS_ALL_PATTERN) ||
        requestURI.matches(API_PRIVILEGES_ALL_PATTERN) ||
        requestURI.matches(API_CLUSTER_REQUESTS_ALL_PATTERN) ||
        requestURI.matches(API_CLUSTER_SERVICES_ALL_PATTERN) ||
        requestURI.matches(API_CLUSTER_ALERT_ALL_PATTERN) ||
        requestURI.matches(API_CLUSTERS_PATTERN) ||
        requestURI.matches(API_STACK_VERSIONS_PATTERN) ||
        requestURI.matches(API_VIEWS_ALL_PATTERN) ||
        requestURI.matches(VIEWS_CONTEXT_PATH_PATTERN) ||
        requestURI.matches(API_WIDGET_LAYOUTS_PATTERN) ||
        requestURI.matches(API_WIDGET_PATTERN) ||
        requestURI.matches(API_CLUSTER_HOSTS_ALL_PATTERN) ||
        requestURI.matches(API_CLUSTER_CONFIGURATIONS_ALL_PATTERN) ||
        requestURI.matches(API_CLUSTER_COMPONENTS_ALL_PATTERN) ||
        requestURI.matches(API_CLUSTER_HOST_COMPONENTS_ALL_PATTERN) ||
        requestURI.matches(API_CLUSTER_CONFIG_GROUPS_ALL_PATTERN) ||
        requestURI.matches(API_HOSTS_ALL_PATTERN) ||
        requestURI.matches(API_ALERT_TARGETS_ALL_PATTERN) ||
        requestURI.matches(API_PERSIST_ALL_PATTERN) ||
        requestURI.matches(API_CLUSTERS_UPGRADES_PATTERN);
  }

  @Override
  public void destroy() {
    // do nothing
  }


  // ----- helper methods ----------------------------------------------------

  /**
   * Get the parameter value from the given servlet filter configuration.
   *
   * @param filterConfig  the servlet configuration
   * @param parameterName the parameter name
   * @param defaultValue  the default value
   * @return the parameter value or the default value if not set
   */
  private static String getParameterValue(
      FilterConfig filterConfig, String parameterName, String defaultValue) {

    String value = filterConfig.getInitParameter(parameterName);
    if (value == null || value.length() == 0) {
      value = filterConfig.getServletContext().getInitParameter(parameterName);
    }
    return value == null || value.length() == 0 ? defaultValue : value;
  }

  SecurityContext getSecurityContext() {
    return SecurityContextHolder.getContext();
  }

  ViewRegistry getViewRegistry() {
    return ViewRegistry.getInstance();
  }
}
