/*
 * 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.activemq.artemis.spi.core.security;

import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.security.cert.X509Certificate;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.security.Principal;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration;
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection;
import org.apache.activemq.artemis.core.security.CheckType;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.spi.core.security.jaas.JaasCallbackHandler;
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal;
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
import org.apache.activemq.artemis.utils.CertificateUtil;
import org.jboss.logging.Logger;

/**
 * This implementation delegates to the JAAS security interfaces.
 *
 * The {@link Subject} returned by the login context is expecting to have a set of {@link RolePrincipal} for each
 * role of the user.
 */
public class ActiveMQJAASSecurityManager implements ActiveMQSecurityManager3 {

   private static final Logger logger = Logger.getLogger(ActiveMQJAASSecurityManager.class);

   private static final String WILDCARD = "*";

   private String configurationName;
   private String certificateConfigurationName;
   private SecurityConfiguration configuration;
   private SecurityConfiguration certificateConfiguration;
   private String rolePrincipalClass = "org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal";

   public ActiveMQJAASSecurityManager() {
   }

   public ActiveMQJAASSecurityManager(String configurationName) {
      this.configurationName = configurationName;
   }

   public ActiveMQJAASSecurityManager(String configurationName, String certificateConfigurationName) {
      this.configurationName = configurationName;
      this.certificateConfigurationName = certificateConfigurationName;
   }

   public ActiveMQJAASSecurityManager(String configurationName, SecurityConfiguration configuration) {
      this.configurationName = configurationName;
      this.configuration = configuration;
   }

   public ActiveMQJAASSecurityManager(String configurationName,
                                      String certificateConfigurationName,
                                      SecurityConfiguration configuration,
                                      SecurityConfiguration certificateConfiguration) {
      this.configurationName = configurationName;
      this.configuration = configuration;
      this.certificateConfigurationName = certificateConfigurationName;
      this.certificateConfiguration = certificateConfiguration;
   }

   @Override
   public boolean validateUser(String user, String password) {
      throw new UnsupportedOperationException("Invoke validateUser(String, String, X509Certificate[]) instead");
   }

   @Override
   public String validateUser(final String user, final String password, X509Certificate[] certificates) {
      try {
         return getUserFromSubject(getAuthenticatedSubject(user, password, certificates));
      } catch (LoginException e) {
         if (logger.isDebugEnabled()) {
            logger.debug("Couldn't validate user", e);
         }
         return null;
      }
   }

   public String getUserFromSubject(Subject subject) {
      String validatedUser = "";
      Set<UserPrincipal> users = subject.getPrincipals(UserPrincipal.class);

      // should only ever be 1 UserPrincipal
      for (UserPrincipal userPrincipal : users) {
         validatedUser = userPrincipal.getName();
      }
      return validatedUser;
   }

   @Override
   public boolean validateUserAndRole(String user, String password, Set<Role> roles, CheckType checkType) {
      throw new UnsupportedOperationException("Invoke validateUserAndRole(String, String, Set<Role>, CheckType, String, RemotingConnection) instead");
   }

   @Override
   public String validateUserAndRole(final String user,
                                     final String password,
                                     final Set<Role> roles,
                                     final CheckType checkType,
                                     final String address,
                                     final RemotingConnection connection) {
      X509Certificate[] certificates = null;
      if (connection != null && connection.getTransportConnection() instanceof NettyConnection) {
         certificates = CertificateUtil.getCertsFromChannel(((NettyConnection) connection.getTransportConnection()).getChannel());
      }
      Subject localSubject;
      try {
         localSubject = getAuthenticatedSubject(user, password, certificates);
      } catch (LoginException e) {
         if (logger.isDebugEnabled()) {
            logger.debug("Couldn't validate user", e);
         }
         return null;
      }

      boolean authorized = false;

      if (localSubject != null) {
         Set<RolePrincipal> rolesWithPermission = getPrincipalsInRole(checkType, roles);

         // Check the caller's roles
         Set<Principal> rolesForSubject = new HashSet<>();
         try {
            rolesForSubject.addAll(localSubject.getPrincipals(Class.forName(rolePrincipalClass).asSubclass(Principal.class)));
         } catch (Exception e) {
            logger.info("Can't find roles for the subject", e);
         }
         if (rolesForSubject.size() > 0 && rolesWithPermission.size() > 0) {
            Iterator<Principal> rolesForSubjectIter = rolesForSubject.iterator();
            while (!authorized && rolesForSubjectIter.hasNext()) {
               Iterator<RolePrincipal> rolesWithPermissionIter = rolesWithPermission.iterator();
               Principal subjectRole = rolesForSubjectIter.next();
               while (!authorized && rolesWithPermissionIter.hasNext()) {
                  Principal roleWithPermission = rolesWithPermissionIter.next();
                  authorized = subjectRole.equals(roleWithPermission);
               }
            }
         }

         if (logger.isTraceEnabled()) {
            logger.trace("user " + (authorized ? " is " : " is NOT ") + "authorized");
         }
      }

      if (authorized) {
         return getUserFromSubject(localSubject);
      } else {
         return null;
      }
   }

   private Subject getAuthenticatedSubject(final String user,
                                           final String password,
                                           final X509Certificate[] certificates) throws LoginException {
      LoginContext lc;
      if (certificateConfigurationName != null && certificateConfigurationName.length() > 0 && certificates != null) {
         lc = new LoginContext(certificateConfigurationName, null, new JaasCallbackHandler(user, password, certificates), certificateConfiguration);
      } else {
         lc = new LoginContext(configurationName, null, new JaasCallbackHandler(user, password, certificates), configuration);
      }
      lc.login();
      return lc.getSubject();
   }

   private Set<RolePrincipal> getPrincipalsInRole(final CheckType checkType, final Set<Role> roles) {
      Set principals = new HashSet<>();
      for (Role role : roles) {
         if (checkType.hasRole(role)) {
            try {
               principals.add(createGroupPrincipal(role.getName(), rolePrincipalClass));
            } catch (Exception e) {
               logger.info("Can't add role principal", e);
            }
         }
      }
      return principals;
   }

   public void setConfigurationName(final String configurationName) {
      this.configurationName = configurationName;
   }

   public void setConfiguration(SecurityConfiguration configuration) {
      this.configuration = configuration;
   }

   public void setCertificateConfigurationName(final String certificateConfigurationName) {
      this.certificateConfigurationName = certificateConfigurationName;
   }

   public void setCertificateConfiguration(SecurityConfiguration certificateConfiguration) {
      this.certificateConfiguration = certificateConfiguration;
   }

   public SecurityConfiguration getConfiguration() {
      if (configuration == null) {
         configuration = new SecurityConfiguration();
      }

      return configuration;
   }

   public SecurityConfiguration getCertificateConfiguration() {
      if (certificateConfiguration == null) {
         certificateConfiguration = new SecurityConfiguration();
      }

      return certificateConfiguration;
   }

   public String getRolePrincipalClass() {
      return rolePrincipalClass;
   }

   public void setRolePrincipalClass(String rolePrincipalClass) {
      this.rolePrincipalClass = rolePrincipalClass;
   }

   public static Object createGroupPrincipal(String name, String groupClass) throws Exception {
      if (WILDCARD.equals(name)) {
         // simple match all group principal - match any name and class
         return new Principal() {
            @Override
            public String getName() {
               return WILDCARD;
            }

            @Override
            public boolean equals(Object other) {
               return true;
            }

            @Override
            public int hashCode() {
               return WILDCARD.hashCode();
            }
         };
      }
      Object[] param = new Object[]{name};

      Class<?> cls = Class.forName(groupClass);

      Constructor<?>[] constructors = cls.getConstructors();
      int i;
      Object instance;
      for (i = 0; i < constructors.length; i++) {
         Class<?>[] paramTypes = constructors[i].getParameterTypes();
         if (paramTypes.length != 0 && paramTypes[0].equals(String.class)) {
            break;
         }
      }
      if (i < constructors.length) {
         instance = constructors[i].newInstance(param);
      } else {
         instance = cls.newInstance();
         Method[] methods = cls.getMethods();
         i = 0;
         for (i = 0; i < methods.length; i++) {
            Class<?>[] paramTypes = methods[i].getParameterTypes();
            if (paramTypes.length != 0 && methods[i].getName().equals("setName") && paramTypes[0].equals(String.class)) {
               break;
            }
         }

         if (i < methods.length) {
            methods[i].invoke(instance, param);
         } else {
            throw new NoSuchMethodException();
         }
      }

      return instance;
   }

}
