package org.wikiwebserver.core;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.security.Permission;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TimeZone;
import java.util.Vector;

/**
 * The SecurityMan is used to limit access to system resources.
 * 
 * You must set a security manager(in the WikiWebServer class) to ensure that
 * the server can not be compromised.  Without a security manager all files 
 * on the filing system could be read and modified by anyone!
 * 
 * It is the task of this class to limit access to files and other resources.
 * 
 * By default this class will only permit read access to files within;
 * the class path, the library path and JAVA_HOME.
 * 
 * Write access is only permitted to files within the writable locations
 * specified when constructing a new instance of a security manager.
 * 
 * A second list of locations can be used to specify specific locations
 * within the writable locations that must NOT be written to.
 *  
 * Lists of locations must be separated by File.pathSeparator.
 * 
 * @author Michael Gardiner
 */
public class SecurityMan extends SecurityManager {
	
    private static final boolean DEBUG_FILE_ACCESS = 
    	ConfigManager.getBoolean("security.debug-file-access");   	
    
    private static Vector<File> readableLocations = new Vector<File>();
    private static Vector<File> writableLocations = new Vector<File>();
    private static Vector<File> notWritableLocations = new Vector<File>();  
    
    private static String superPassword = null;      
    private static File wikiRoot, mapRoot, logRoot;
    
    private static final List<String> logHeadings = Arrays.asList(new String[] {
    		"Time","Thread ID","Thread Privileges","Action","Details","Result" 
    });       
    
    public static Privilege getCodePrivilege() {
        return getCodePrivilege(Thread.currentThread(), true);
    }
    
    public static Privilege getCodePrivilege(Thread thread, boolean wareHouseIsSuperAdmin) {
        StackTraceElement[] trace = thread.getStackTrace();
        
        if (trace == null || trace.length == 0) {
            // Unknown or can not be safely determined
            return Privilege.GUEST;
        }
        
        // Construct list of class names in stack trace
        StringBuilder builder = new StringBuilder(100);
        for (StackTraceElement ste : trace) {
            builder.append("^");
            builder.append(ste.getClassName());
        }
        String classList = builder.toString();
      
        if (wareHouseIsSuperAdmin) {
            // The gateway to the unprotected world for restricted classes
            if (classList.contains("^org.wikiwebserver.core.WareHouse")) {
                return Privilege.SUPER_ADMIN;
            }
        }
        
        // WikiMap has extra privileges
        if (classList.contains("^org.wikiwebserver.core.WikiMap")) {
            return Privilege.SUPER_ADMIN;
        }         
        // Many classes and libraries require extra privileges, they should
        // placed in the util package. (Axis calls need to go via utils)
        if (classList.contains("^org.wikiwebserver.util.")) {
            return Privilege.SUPER_ADMIN;
        }          
        if (classList.contains("^org.wikiwebserver.core.CustomClassLoader")) {
            return Privilege.ADMIN;
        }          
        
        // Barely restricted
        else if (classList.contains("^org.wikiwebserver.http.")) {
            return Privilege.ADMIN;
        } 
        else if (classList.contains("^page.config")) {
            return Privilege.ADMIN;
        }
        // Slightly restricted
        else if (classList.contains("^page.")) {
            return Privilege.MODERATOR;
        }                  
        
        
        // Trusted classes                
        else if (classList.contains("^java.")) {
            return Privilege.SUPER_ADMIN;
        }   
        else if (classList.contains("^javax.")) {
            return Privilege.SUPER_ADMIN;
        }   
        else if (classList.contains("^sun.")) {
            return Privilege.SUPER_ADMIN;
        }   
        else if (classList.contains("^org.wikiwebserver.core.")) {
            return Privilege.SUPER_ADMIN;
        }           
        else if (classList.contains("^org.tanukisoftware.wrapper.")) {
            return Privilege.SUPER_ADMIN;
        }
        
        // Severely restricted
        else if (classList.contains("^user.")) {
            return Privilege.USER;
        }          
        
        // Unknown code base
        return Privilege.GUEST;
    }
   
    /**
     * Constructs a new SecurityManager which restricts access
     * to system resources.
     * To specify multiple writable locations use File.pathSeparator
     * 
     * @param allow The location(s) where files can be modified.
     * @param deny The location(s) within allowed locations that can NOT be modified.
     * @param spw The super password required to perform special actions. 
     * @throws IOException If a specified location can not be resolved to the filing system.
     */
    public SecurityMan(String allow, String deny, String spw) {
        
        // Ensure the required Privilege class is available and linked
        Privilege.class.getName();
        
        // Everything in temp can be read and written 
        String tempPathString = System.getProperty("java.io.tmpdir");
        addAllPaths(tempPathString, readableLocations);
        addAllPaths(tempPathString, writableLocations);
        
        // Everything on the classpath can be read
        String classPathString = System.getProperty("java.class.path");
        addAllPaths(classPathString, readableLocations);
        // Everything in libraries can be read
        String javaLibraryString = System.getProperty("java.library.path");
        addAllPaths(javaLibraryString, readableLocations);
        // Everything in java home can be read
        String homeString = System.getProperty("java.home");
        addAllPaths(homeString, readableLocations);
        
        // Everything in allow can be read AND written
        addAllPaths(allow, readableLocations);
        addAllPaths(allow, writableLocations);
        
        addAllPaths(deny, notWritableLocations);
        
        superPassword = spw;
        wikiRoot = realFile(".");
        mapRoot = realFile(WareHouse.MAP_ROOT);
        logRoot = realFile(WareHouse.LOG_ROOT);
    }
    
    public static void checkSuperPassword(String spw) {
        if ((spw == null) || !spw.equals(superPassword)) {
            // Delay failed password requests
            logPrivileged("PASSWORD_CHECK", "Verify super password", "INCORRECT");
            try { Thread.sleep(200); } catch (Exception ex) {}
            throw new SecurityException("Permission denied. Incorrect super password.");  
        }
        logPrivileged("PASSWORD_CHECK", "Verify super password", "CORRECT");
    }
    
    public void checkAccept(String host, int port) {}
    
    public void checkAccess(Thread t) {
        Privilege codePrivilege = getCodePrivilege();
        if (codePrivilege.isBelow(Privilege.MODERATOR)) {
            logPrivileged("THREAD_ACCESS", "Request to access thread properties", "DENIED");
            throw new SecurityException(codePrivilege.getLabel() + 
                                        " code is not permitted access to threads.");
        }
    }
    
    public void checkAccess(ThreadGroup g) {
        Privilege codePrivilege = getCodePrivilege();
        if (codePrivilege.isBelow(Privilege.MODERATOR)) {
            logPrivileged("THREAD_ACCESS", "Request to access thread group properties", "DENIED");
            throw new SecurityException(codePrivilege.getLabel() + 
                                        " code is not permitted access to threads.");
        }
    }
    
    public void checkAwtEventQueueAccess() {
        Privilege codePrivilege = getCodePrivilege();
        if (codePrivilege.isBelow(Privilege.SUPER_ADMIN)) {
            logPrivileged("AWT_ACCESS", "Request to access AWT Events", "DENIED");
            throw new SecurityException(codePrivilege.getLabel() + 
                                        " code is not permitted access to AWT.");
        }
    }
    
    public void checkConnect(String host, int port) {
        Privilege codePrivilege = getCodePrivilege();
        if (codePrivilege.isBelow(Privilege.PREMIUM_USER)) {
            logPrivileged("NETWORK_CONNECT", "Request to access network", "DENIED");
            throw new SecurityException(codePrivilege.getLabel() +
                                        " code is not permitted network access.");
        }
        logPrivileged("NETWORK_CONNECT", host + ":" + port, "PERMITTED");
    }
    
    public void checkConnect(String host, int port, Object context) {
        checkConnect(host, port);
    }
    
    public void checkCreateClassLoader() {
        Privilege codePrivilege = getCodePrivilege();
        if (codePrivilege.isBelow(Privilege.MODERATOR)) {
            logPrivileged("CREATE_CLASS_LOADER", "Request to create class loader", "DENIED");
            throw new SecurityException(codePrivilege.getLabel() + 
                                        " code is not permitted to create classloader.");
        }
    }
    
    public void checkExec(String cmd) {
        Privilege codePrivilege = getCodePrivilege();
        if (codePrivilege.isBelow(Privilege.SUPER_ADMIN)) {        
            logPrivileged("EXEC", cmd, "DENIED");
            throw new SecurityException(codePrivilege.getLabel() + 
                                        " code is not permitted to execute " + cmd); 
        }
        logPrivileged("EXEC", cmd, "PERMITTED");
    }
    
    public void checkExit(int status) {
        Privilege codePrivilege = getCodePrivilege();
        if (codePrivilege.isBelow(Privilege.SUPER_ADMIN)) {        
            logPrivileged("EXIT", "Request to exit virtual machine", "DENIED");
            throw new SecurityException(codePrivilege.getLabel() + 
                                        " code is not permitted to exit virtual machine");
        }
        logPrivileged("EXIT", "Request to exit virtual machine", "PERMITTED");
    }
    
    public void checkLink(String lib) {
        if (!realFile(lib).exists()) return;
        try {
            checkWrite(lib);
        }
        catch (SecurityException ex) {
            // This occurs if the lib location is NOT writable
            // (Locations which are not writable are deemed safe
            // IF they can be read.)
            checkRead(lib);
            logPrivileged("LINK", lib, "PERMITTED");
            return;
        }
        logPrivileged("LINK", lib, "DENIED");
        throw new SecurityException("Native code not permitted for " + lib);
    }
    
    public void checkListen(int port) {
        Privilege codePrivilege = getCodePrivilege();
        if (codePrivilege.isBelow(Privilege.SUPER_ADMIN)) {
            logPrivileged("SERVER_SOCKET_ACCESS", String.valueOf(port), "DENIED");
            throw new SecurityException(codePrivilege.getLabel() + 
                                        " code is not permitted to open server sockets.");
        }
        logPrivileged("SERVER_SOCKET_ACCESS", String.valueOf(port), "PERMITTED");
    }
    
    public void checkMemberAccess(Class<?> cl, int which) {
        Privilege codePrivilege = getCodePrivilege();
        if (which == java.lang.reflect.Member.DECLARED) {
            if (codePrivilege.isBelow(Privilege.ADMIN)) {
                if (!cl.getName().startsWith("com.sun") && !cl.getName().startsWith("sun")) {
                    logPrivileged("MEMBER_ACCESS", cl.getName(), "DENIED");
                    throw new SecurityException(codePrivilege.getLabel() + 
                            " code is not permitted member access for class " + cl);
                }
            }
        }
        if (which == java.lang.reflect.Member.PUBLIC) {
            if (codePrivilege.isBelow(Privilege.MODERATOR)) {
                if (!cl.getName().startsWith("com.sun") && !cl.getName().startsWith("sun")) {
                    logPrivileged("MEMBER_ACCESS", cl.getName(), "DENIED");
                    throw new SecurityException(codePrivilege.getLabel() + 
                            " code is not permitted member access for class " + cl);
                }
            }
        }        
    }
    
    public void checkMulticast(InetAddress maddr) {
        Privilege codePrivilege = getCodePrivilege();
        if (codePrivilege.isBelow(Privilege.SUPER_ADMIN)) {
            logPrivileged("MULTICAST_ACCESS", "Request to perform network multicasting", "DENIED");
            throw new SecurityException(codePrivilege.getLabel() + 
                      " code is not permitted multicasting for class " + maddr);
        }
    }
    
    public void checkPackageAccess(String pkg) {}
    public void checkPackageDefinition(String pkg) {}
    
    public void checkPermission(Permission perm) {
        // Do not allow write operations in sand box
        // eg setProperty
        if (perm.getActions().contains("write")) {
            Privilege codePrivilege = getCodePrivilege();
            if (codePrivilege.isBelow(Privilege.MODERATOR)) {       
                logPrivileged("PERMISSION", "Request to write system data", "DENIED");
                throw new SecurityException(codePrivilege.getLabel() +
                                            " code is not permitted to modify " + perm.toString());
            }
        }         
    }
   
    public void checkPermission(Permission perm, Object context) {
        checkPermission(perm);
    }
    
    public void checkPrintJobAccess() {
        Privilege codePrivilege = getCodePrivilege();
        if (codePrivilege.isBelow(Privilege.SUPER_ADMIN)) {
            logPrivileged("PRINT_JOB_ACCESS", "Request to access printer", "DENIED");
            throw new SecurityException(codePrivilege.getLabel() + 
                                        " code is not permitted access to print jobs");
        }        
    }
  
    //public void checkPropertiesAccess() { }
    //public void checkPropertyAccess(String key) { }
    //public void checkRead(FileDescriptor fd) {}
    //public void checkRead(String file, Object context) {}
    //public void checkSecurityAccess(String target) {}
    
    public void checkSetFactory() {
        Privilege codePrivilege = getCodePrivilege();
        if (codePrivilege.isBelow(Privilege.SUPER_ADMIN)) {
            logPrivileged("FACTORY_ACCESS", "Request to install new factory", "DENIED");
            throw new SecurityException(codePrivilege.getLabel() + 
                                        " code is not permitted access to factory");
        } 
    }
    
    public void checkSystemClipboardAccess() {
        Privilege codePrivilege = getCodePrivilege();
        if (codePrivilege.isBelow(Privilege.SUPER_ADMIN)) {
            logPrivileged("CLIPBOARD_ACCESS", "Request to access clipboard", "DENIED");
            throw new SecurityException(codePrivilege.getLabel() +
                                        " code is not permitted access to clipboard");
        } 
    }
    //public void checkWrite(FileDescriptor fd) {}
    
    public void checkRead(String fileString) {
        checkRead(fileString, getCodePrivilege());
    }
    
    public void checkRead(String fileString, Privilege privilege) {
    	debugFileAccess("Reading from file", fileString);
        if (fileString == null) return;
        
        File test = realFile(fileString);
        
        // Prevent access to /etc/ (just in case / is marked readable)
        if (test.toString().startsWith("/etc/")) {
            logPrivileged("FILE_READ", test.toString(), "DENIED");
            throw new SecurityException("Read not permitted for " + fileString + 
                                        " (inside /etc/)");
        }        
        
        if (privilege.isBelow(Privilege.MODERATOR)) {
            if (!test.toString().startsWith(wikiRoot.toString())) {
                logPrivileged("FILE_READ", test.toString(), "DENIED");
                throw new SecurityException(privilege.getLabel() +
                          " code is not permitted to read " + fileString + " (above wiki root)");
            }
        }
        
        if (test.toString().startsWith(mapRoot.toString())) {      
            checkMapAccess(test);
            return;
        }
        
        for (File file : readableLocations) {
            if (test.toString().startsWith(file.toString())) {
                //logPrivileged("FILE_READ", test.toString(), "PERMITTED");
                return;
            }
        }
        
        logPrivileged("FILE_READ", test.toString(), "DENIED");
        throw new SecurityException(privilege.getLabel() + 
                                    " code is not permitted to read " + fileString);
    }    
    
    public void checkDelete(String fileString) {
        checkWrite(fileString);
    }    
    
    public void checkWrite(String fileString) {
        checkWrite(fileString, getCodePrivilege());
    }

    public void checkWrite(String fileString, Privilege privilege) {
        debugFileAccess("Modification to file", fileString);
        File test = realFile(fileString);
        
        // Not writable locations override all writable locations
        for (File file : notWritableLocations) {
            if (test.toString().startsWith(file.toString())) {
                logPrivileged("FILE_WRITE", test.toString(), "DENIED");
                throw new SecurityException(privilege.getLabel() + 
                          " code is not permitted to write to " + fileString + " (blacklisted)");
            }
        }

        if (test.toString().startsWith(mapRoot.toString())) {      
            checkMapAccess(test);
            return;
        }
        
        if (test.toString().startsWith(logRoot.toString())) {      
            checkLogAccess(test);
            return;
        }        
        
        if (privilege.isBelow(Privilege.USER)) {
            logPrivileged("FILE_WRITE", test.toString(), "DENIED");
            throw new SecurityException(privilege.getLabel() +
                    " code is not permitted to write to " + fileString);
        }  
        else if (privilege == Privilege.USER || privilege == Privilege.PREMIUM_USER) {
            // Users can write to user root
            File userClassLocation = realFile(WareHouse.USER_ROOT);
            if (test.toString().startsWith(userClassLocation.toString())) {
                logPrivileged("FILE_WRITE", test.toString(), "PERMITTED");
                return;
            }
        }              
        else { 
            // Only allow writing to writable locations
            for (File file : writableLocations) {
                if (test.toString().startsWith(file.toString())) {
                    logPrivileged("FILE_WRITE", test.toString(), "PERMITTED");
                    return;
                } 
            }
        }
        
        logPrivileged("FILE_WRITE", test.toString(), "DENIED");
        throw new SecurityException(privilege.getLabel() + 
                                    " code is not permitted to write to " + fileString);        
    } 
    
    
    private void addAllPaths(String pathList, Vector<File> target) {
        String[] fileStrings = pathList.split(File.pathSeparator);
        for (String fileString : fileStrings) {
            target.add(realFile(fileString));
        }        
    }
      
    private static File realFile(String fileString) {
        return realFile(new File(fileString));
    }
    
    private static File realFile(File file) {
        //return file.getAbsoluteFile();
      
        try {
            debugFileAccess("Canonical lookup: ", file.getPath());
            return file.getCanonicalFile();
        } catch (IOException ex) {
            throw new SecurityException("Invalid path: " + file.getPath());
        }
    }
    
    public static File getUserClassLocation() {
        return getClassLocation(WareHouse.USER_PACKAGE_PREFIX);
    }     
    
    public static File getClassLocation(String pkg) {

        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
        for (int i = trace.length-1; i >= 0; i--) {
            String className = trace[i].getClassName();
            if (className.startsWith(pkg)) {
                int idx = className.lastIndexOf('.');
                String classLocation = className.substring(0, idx).replace('.', '/');
                return new File(classLocation);
            }
        }
        throw new SecurityException("Unknown location");
    }     
    
    
    private void checkMapAccess(File test) {
        if (test == null) return;
        
        // Super admin can access store files
        if (getCodePrivilege() == Privilege.SUPER_ADMIN) {
            return;
        }
        
        if (test.getName().endsWith(".map")) {
            logPrivileged("MAP_ACCESS", test.toString(), "DENIED");
            throw new SecurityException("Not permitted to access map file.");
        }
    }
    
    private void checkLogAccess(File test) {
        if (test == null) return;
        
        // Super admin can access log files
        if (getCodePrivilege() == Privilege.SUPER_ADMIN) {
            return;
        }
        
        if (test.getName().endsWith(".log.csv")) {
            logPrivileged("LOG_ACCESS", test.toString(), "DENIED");
            throw new SecurityException("Not permitted to access log files.");
        }
    }    
    
    private static void logPrivileged(String action, String details, String result) {

        SimpleDateFormat formatter = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'");
        formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
        String time = formatter.format(System.currentTimeMillis());
        
        Thread thread = Thread.currentThread();
        String threadID = thread.getName();
        String privileges = getCodePrivilege().getLabel();
        
        List<String> columns = new ArrayList<String>();
        columns.add(time);
        columns.add(threadID);
        columns.add(privileges);
        columns.add(action);
        columns.add(details);
        columns.add(result);
        
        WareHouse.logData("security", logHeadings, columns);
    }     
    
    private static void debugFileAccess(String msg, String path) {
        if (DEBUG_FILE_ACCESS) System.out.println(msg + ": " + path);
    }    
}

