/*
 * 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.knox.gateway.filter;

import org.apache.knox.gateway.audit.api.Action;
import org.apache.knox.gateway.audit.api.ActionOutcome;
import org.apache.knox.gateway.audit.api.AuditServiceFactory;
import org.apache.knox.gateway.audit.api.Auditor;
import org.apache.knox.gateway.audit.api.ResourceType;
import org.apache.knox.gateway.audit.log4j.audit.AuditConstants;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.security.GroupPrincipal;
import org.apache.knox.gateway.security.ImpersonatedPrincipal;
import org.apache.knox.gateway.security.PrimaryPrincipal;

import javax.security.auth.Subject;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.AccessController;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

public class AclsAuthorizationFilter implements Filter {
  private static AclsAuthorizationMessages log = MessagesFactory.get( AclsAuthorizationMessages.class );
  private static Auditor auditor = AuditServiceFactory.getAuditService().getAuditor( AuditConstants.DEFAULT_AUDITOR_NAME,
          AuditConstants.KNOX_SERVICE_NAME, AuditConstants.KNOX_COMPONENT_NAME );

  private String resourceRole;
  private String aclProcessingMode;
  private AclParser parser = new AclParser();
  private List<String> adminGroups = new ArrayList<>();
  private List<String> adminUsers = new ArrayList<>();

  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    String adminGroups = filterConfig.getInitParameter("knox.admin.groups");
    if (adminGroups != null) {
      parseAdminGroupConfig(adminGroups);
    }

    String adminUsers = filterConfig.getInitParameter("knox.admin.users");
    if (adminUsers != null) {
      parseAdminUserConfig(adminUsers);
    }

    resourceRole = getInitParameter(filterConfig, "resource.role");
    log.initializingForResourceRole(resourceRole);
    aclProcessingMode = getInitParameter(filterConfig, resourceRole + ".acl.mode");
    if (aclProcessingMode == null) {
      aclProcessingMode = getInitParameter(filterConfig, "acl.mode");
      if (aclProcessingMode == null) {
        aclProcessingMode = "AND";
      }
    }
    log.aclProcessingMode(aclProcessingMode);
    String acls = getInitParameter(filterConfig, resourceRole + ".acl");
    parser.parseAcls(resourceRole, acls);
  }

  private String getInitParameter(FilterConfig filterConfig, String paramName) {
    return filterConfig.getInitParameter(paramName.toLowerCase(Locale.ROOT));
  }

  private void parseAdminGroupConfig(String groups) {
    Collections.addAll(adminGroups, groups.split(","));
  }

  private void parseAdminUserConfig(String users) {
    Collections.addAll(adminUsers, users.split(","));
  }

  @Override
  public void destroy() {
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response,
                       FilterChain chain) throws IOException, ServletException {
    boolean accessGranted = enforceAclAuthorizationPolicy(request, response, chain);
    log.accessGranted(accessGranted);
    String sourceUrl = (String)request.getAttribute( AbstractGatewayFilter.SOURCE_REQUEST_CONTEXT_URL_ATTRIBUTE_NAME );
    if (accessGranted) {
      auditor.audit( Action.AUTHORIZATION, sourceUrl, ResourceType.URI, ActionOutcome.SUCCESS );
      chain.doFilter(request, response);
    }
    else {
      auditor.audit( Action.AUTHORIZATION, sourceUrl, ResourceType.URI, ActionOutcome.FAILURE );
      sendForbidden((HttpServletResponse) response);
    }
  }

  protected boolean enforceAclAuthorizationPolicy(ServletRequest request,
      ServletResponse response, FilterChain chain) {
    HttpServletRequest req = (HttpServletRequest) request;

    // before enforcing acls check whether there are no acls defined
    // which would mean that there are no restrictions
    if (parser.users.isEmpty() && parser.groups.isEmpty() && parser.ipv.getIPAddresses().isEmpty()) {
      return true;
    }

    boolean userAccess;
    boolean groupAccess = false;
    boolean ipAddrAccess;

    Subject subject = Subject.getSubject(AccessController.getContext());
    Principal primaryPrincipal = (Principal)subject.getPrincipals(PrimaryPrincipal.class).toArray()[0];
    log.primaryPrincipal(primaryPrincipal.getName());
    Object[] impersonations = subject.getPrincipals(ImpersonatedPrincipal.class).toArray();
    if (impersonations.length > 0) {
      log.impersonatedPrincipal(((Principal)impersonations[0]).getName());
      userAccess = checkUserAcls((Principal)impersonations[0]);
      log.impersonatedPrincipalHasAccess(userAccess);
    }
    else {
      userAccess = checkUserAcls(primaryPrincipal);
      log.primaryPrincipalHasAccess(userAccess);
    }
    Object[] groups = subject.getPrincipals(GroupPrincipal.class).toArray();
    if (groups.length > 0) {
      groupAccess = checkGroupAcls(groups);
      log.groupPrincipalHasAccess(groupAccess);
    }
    else {
      // if we have no groups in the subject then make
      // it true if there is an anyGroup acl
      // for AND mode and acls like *;*;127.0.0.* we need to
      // make it pass
      if (parser.anyGroup && "AND".equals(aclProcessingMode)) {
        groupAccess = true;
      }
    }
    log.remoteIPAddress(req.getRemoteAddr());
    ipAddrAccess = checkRemoteIpAcls(req.getRemoteAddr());
    log.remoteIPAddressHasAccess(ipAddrAccess);

    if ("OR".equals(aclProcessingMode)) {
      // need to interpret '*' as excluded for OR semantics
      // to make sense and not grant access to everyone by mistake.
      // exclusion in OR is equivalent to denied
      // so, let's set each one that contains '*' to false.
      if (parser.anyUser) {
        userAccess = false;
      }
      if (parser.anyGroup) {
        groupAccess = false;
      }
      if (parser.ipv.allowsAnyIP()) {
        ipAddrAccess = false;
      }

      return (userAccess || groupAccess || ipAddrAccess);
    }
    else if ("AND".equals(aclProcessingMode)) {
      return (userAccess && groupAccess && ipAddrAccess);
    }
    return false;
  }

  private boolean checkRemoteIpAcls(String remoteAddr) {
    boolean allowed;
    if (remoteAddr == null) {
      return false;
    }
    allowed = parser.ipv.validateIpAddress(remoteAddr);
    return allowed;
  }

  boolean checkUserAcls(Principal user) {
    boolean allowed = false;
    if (user == null) {
      return false;
    }
    if (parser.anyUser) {
      allowed = true;
    }
    else {
      if (parser.users.contains(user.getName())) {
        allowed = true;
      }
      else if (parser.users.contains("KNOX_ADMIN_USERS") &&
          adminUsers.contains(user.getName())) {
        allowed = true;
      }
    }
    return allowed;
  }

  boolean checkGroupAcls(Object[] userGroups) {
    boolean allowed;
    if (userGroups == null) {
      return false;
    }
    if (parser.anyGroup) {
      allowed = true;
    }
    else {
      allowed = hasAllowedPrincipal(parser.groups, userGroups);
      if (!allowed && parser.groups.contains("KNOX_ADMIN_GROUPS")) {
        allowed = hasAllowedPrincipal(adminGroups, userGroups);
      }
    }
    return allowed;
  }

  private boolean hasAllowedPrincipal(List<String> allowed, Object[] userGroups) {
    boolean rc = false;
    for (Object userGroup : userGroups) {
      if (allowed.contains(((Principal) userGroup).getName())) {
        rc = true;
        break;
      }
    }
    return rc;
  }

  private void sendForbidden(HttpServletResponse res) {
    sendErrorCode(res, 403);
  }

  private void sendErrorCode(HttpServletResponse res, int code) {
    try {
      res.sendError(code);
    } catch (IOException e) {
      // TODO: log appropriately
      e.printStackTrace();
    }
  }
}
