package org.infinispan.security;

import java.security.AccessControlContext;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.acl.Group;
import java.util.Stack;

import javax.security.auth.Subject;

import sun.reflect.Reflection;

/**
 * Security. A simple class to implement caller privileges without a security manager and a
 * much faster implementations of the {@link Subject#doAs(Subject, PrivilegedAction)} and
 * {@link Subject#doAs(Subject, PrivilegedExceptionAction)} when interaction with the
 * {@link AccessControlContext} is not needed.
 *
 * N.B. this uses the caller's {@link Package}, this can easily be subverted by placing the
 * calling code within the org.infinispan hierarchy. However for most purposes this is ok.
 *
 * @author Tristan Tarrant
 * @since 7.0
 */
@SuppressWarnings({ "restriction", "deprecation" })
public final class Security {
   private static final boolean hasGetCallerClass;
   private static final int callerOffset;
   private static final LocalSecurityManager SECURITY_MANAGER;

   static {
      boolean result = false;
      int offset = 0;
      try {
         result = Reflection.getCallerClass(1) == Security.class || Reflection.getCallerClass(2) == Security.class;
         offset = Reflection.getCallerClass(1) == Reflection.class ? 2 : 1;
      } catch (Throwable ignored) {
      }
      hasGetCallerClass = result;
      callerOffset = offset;
      if (!hasGetCallerClass) {
         SECURITY_MANAGER = new LocalSecurityManager();
      } else {
         SECURITY_MANAGER = null;
      }
   }

   private static class LocalSecurityManager extends SecurityManager {
      public Class<?>[] getClasses() {
          return this.getClassContext();
      }
  }

   private static final ThreadLocal<Boolean> PRIVILEGED = new ThreadLocal<Boolean>() {
      @Override
      protected Boolean initialValue() {
         return Boolean.FALSE;
      }
   };

   private static final ThreadLocal<Stack<Subject>> SUBJECT = new ThreadLocal<Stack<Subject>>();

   private static boolean isTrustedClass(Class<?> klass) {
      // TODO: implement a better way
      return klass.getPackage().getName().startsWith("org.infinispan.");
   }

   public static <T> T doPrivileged(PrivilegedAction<T> action) {
      if (!isPrivileged() && isTrustedClass(getCallerClass(2))) {
         try {
            PRIVILEGED.set(true);
            return action.run();
         } finally {
            PRIVILEGED.remove();
         }
      } else {
         return action.run();
      }
   }

   public static <T> T doPrivileged(PrivilegedExceptionAction<T> action) throws PrivilegedActionException {
      if (!isPrivileged() && isTrustedClass(getCallerClass(2))) {
         try {
            PRIVILEGED.set(true);
            return action.run();
         } catch (Exception e) {
            throw new PrivilegedActionException(e);
         } finally {
            PRIVILEGED.remove();
         }
      } else {
         try {
            return action.run();
         } catch (Exception e) {
            throw new PrivilegedActionException(e);
         }
      }
   }

   /**
    * A "lightweight" implementation of {@link Subject#doAs(Subject, PrivilegedAction)} which uses a ThreadLocal
    * {@link Subject} instead of modifying the current {@link AccessControlContext}.
    *
    * @see Subject#doAs(Subject, PrivilegedAction)
    */
   public static <T> T doAs(final Subject subject, final java.security.PrivilegedAction<T> action) {
      Stack<Subject> stack = SUBJECT.get();
      if (stack == null) {
         stack = new Stack<Subject>();
         SUBJECT.set(stack);
      }
      stack.push(subject);
      try {
         return action.run();
      } finally {
         stack.pop();
         if (stack.isEmpty()) {
            SUBJECT.remove();
         }
      }
   }

   /**
    * A "lightweight" implementation of {@link Subject#doAs(Subject, PrivilegedExceptionAction)} which uses a ThreadLocal
    * {@link Subject} instead of modifying the current {@link AccessControlContext}.
    *
    * @see Subject#doAs(Subject, PrivilegedExceptionAction)
    */
   public static <T> T doAs(final Subject subject,
         final java.security.PrivilegedExceptionAction<T> action)
         throws java.security.PrivilegedActionException {
      Stack<Subject> stack = SUBJECT.get();
      if (stack == null) {
         stack = new Stack<Subject>();
         SUBJECT.set(stack);
      }
      stack.push(subject);
      try {
         return action.run();
      } catch (Exception e) {
         throw new PrivilegedActionException(e);
      } finally {
         stack.pop();
         if (stack.isEmpty()) {
            SUBJECT.remove();
         }
      }
   }


   public static void checkPermission(CachePermission permission) throws AccessControlException {
      if (!isPrivileged()) {
         throw new AccessControlException("Call from unprivileged code", permission);
      }
   }

   public static boolean isPrivileged() {
      return PRIVILEGED.get();
   }

   /**
    * If using {@link Security#doAs(Subject, PrivilegedAction)} or
    * {@link Security#doAs(Subject, PrivilegedExceptionAction)}, returns the {@link Subject} associated with the current thread
    * otherwise it returns the {@link Subject} associated with the current {@link AccessControlContext}
    */
   public static Subject getSubject() {
      if (SUBJECT.get() != null) {
         return SUBJECT.get().peek();
      } else {
         AccessControlContext acc = AccessController.getContext();
         if (System.getSecurityManager() == null) {
            return Subject.getSubject(acc);
         } else {
            return AccessController.doPrivileged((PrivilegedAction<Subject>) () -> Subject.getSubject(acc));
         }
      }
   }

   /**
    * Returns the first principal of a subject which is not of type {@link java.security.acl.Group}
    */
   public static Principal getSubjectUserPrincipal(Subject s) {
      if (s != null) {
         for (Principal p : s.getPrincipals()) {
            if (!(p instanceof Group)) {
               return p;
            }
         }
      }
      return null;
   }

   static Class<?> getCallerClass(int n) {
      if (hasGetCallerClass) {
         return Reflection.getCallerClass(n + callerOffset);
      } else {
         return SECURITY_MANAGER.getClasses()[n + callerOffset];
      }
   }
}
