/*
 * Policy.java
 *
 * Created on 2008-07-08 07:57
 */
package com.systemtier.jacc;

import com.sun.enterprise.security.auth.login.PasswordCredential;
import com.sun.enterprise.security.provider.PolicyWrapper;
import java.io.IOException;
import java.security.AccessController;
import java.security.Permission;
import java.security.ProtectionDomain;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Hashtable;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.security.auth.Subject;
import javax.sql.DataSource;

/**
 * Represents application server security policy. Only permissions instances of
 * <code>com.systemtier.jacc.BasicPermission</code> are checked here. Checking of other permissions are passed to
 * standard GlassFish policy implementation.
 *
 * @author Aleksandras Novikovas
 * @author System Tier
 * @version 1.0
 */
public class Policy
        extends PolicyWrapper {

    /**
     * Holds full class name. Mostly used for logging.
     */
    private static final String CLASS_NAME = Policy.class.getName ();

    /**
     * The logger object.
     */
    private static final Logger log = Logger.getLogger (CLASS_NAME);

    /**
     * Holds JDBC resource name to access security database.
     */
    public static String JDBC_RESOURCE_NAME = "jdbc/authPool";

    /**
     * Query used to test BasicPermission for the user.
     */
    // PostgreSql
    public static String PERMISSION_TEST_QUERY = "select exists (select 1 from sec.active_user_list au join sec.group_user gu on gu.user_name = au.user_name join sec.group_permission gp on gu.group_name = gp.group_name where au.user_name = ? and gp.permission_name = ?)";
    // MS SQL
//    public static String PERMISSION_TEST_QUERY = "select case when exists (select 1 from sec.active_user_list au join sec.group_user gu on gu.user_name = au.user_name join sec.group_permission gp on gu.group_name = gp.group_name where au.user_name = ? and gp.permission_name = ?) then 1 else 0 end";

    /**
     * Holds cache for tested permissions.
     */
    private Hashtable<String, Hashtable<Permission, Boolean>> cache;

    /**
     * Creates a new instance of Policy.
     */
    public Policy () {
        super ();
        cache = new Hashtable<String, Hashtable<Permission, Boolean>> ();
        Properties settings = new Properties ();
        try {
            if (log.isLoggable (Level.FINEST)) {
                log.finest ("Reading settings file.");
            }
            settings.load (getClass ().getClassLoader ().getResourceAsStream ("com/systemtier/jacc/settings.properties"));
            p = settings.getProperty ("JDBC_RESOURCE_NAME");
            if (p != null) {
                JDBC_RESOURCE_NAME = p;
                if (log.isLoggable (Level.FINEST)) {
                    log.finest ("JDBC_RESOURCE_NAME=" + JDBC_RESOURCE_NAME);
                }
            }
            p = settings.getProperty ("PERMISSION_TEST_QUERY");
            if (p != null) {
                PERMISSION_TEST_QUERY = p;
                if (log.isLoggable (Level.FINEST)) {
                    log.finest ("PERMISSION_TEST_QUERY=" + PERMISSION_TEST_QUERY);
                }
            }
        }
        catch (IOException ex) {
            if (log.isLoggable (Level.WARNING)) {
                log.log (Level.WARNING, "Settings for properties can not be openned. Using defaults.");
            }
        }
        if (log.isLoggable (Level.INFO)) {
            log.log (Level.INFO, CLASS_NAME + " instantiated.");
        }
    }

    @Override
    public void refresh () {
        if (log.isLoggable (Level.FINER)) {
            log.entering (CLASS_NAME, "refresh");
        }
        cache.clear ();
        super.refresh ();
        if (log.isLoggable (Level.FINER)) {
            log.exiting (CLASS_NAME, "refresh");
        }
    }

    /**
     * Evaluates the global policy for the permissions granted to the <code>ProtectionDomain</code> and tests whether
     * the permission is granted.<p/>
     * Only <code>com.systemtier.jacc.BasicPermission</code> instances are checked in this method. Checking of other
     * permissions are passed to standard GlassFish policy implementation.<p/>
     * Checking is done by querying database.<p/>
     *
     * @param domain <code>ProtectionDomain</code> to test.
     * @param permission <code>Permission</code> object to be tested for implication.
     *
     * @return <code>boolean</code> true if permission is a proper subset of a permission granted to this
     *          <code>ProtectionDomain</code>.
     */
    @Override
    public boolean implies (ProtectionDomain domain, Permission permission) {
        if (permission instanceof BasicPermission) {
            if (log.isLoggable (Level.FINER)) {
                log.entering (CLASS_NAME, "implies", new Object [] {permission});//domain, permission
            }
            if (log.isLoggable (Level.FINEST)) {
                log.finest ("Permission is instanceof BasicPermission.");
            }
            String userName = getCurrentUserName ();
            if (userName != null) {
                if (log.isLoggable (Level.FINEST)) {
                    log.finest ("Retrieving permission test result from cache.");
                }
                // Checking cache before doing actual test of permission
                Hashtable<Permission, Boolean> cachedSubjectPermissions = cache.get (userName);
                if (cachedSubjectPermissions == null) {
                    // Create permissions cache for subject.
                    cachedSubjectPermissions = new Hashtable<Permission, Boolean> ();
                    cache.put (userName, cachedSubjectPermissions);
                }
                // Retrieving cached permission test.
                Boolean cachedPermissionResult = cachedSubjectPermissions.get (permission);
                if (cachedPermissionResult == null) {
                    Context ctx = null;
                    DataSource ds = null;
                    Connection con = null;
                    PreparedStatement ps = null;
                    ResultSet rs = null;
                    try {
                        if (log.isLoggable (Level.FINEST)) {
                            log.finest ("Retrieving permission test result from database.");
                        }
                        ctx = new InitialContext ();
                        ds = (DataSource) ctx.lookup (JDBC_RESOURCE_NAME);
                        con = ds.getConnection ();
                        ps = con.prepareStatement (PERMISSION_TEST_QUERY);
                        ps.setString (1, userName);
                        ps.setString (2, permission.getName ());
                        if (log.isLoggable (Level.FINEST)) {
                            log.finest ("Executing database request to test permission.");
                        }
                        rs = ps.executeQuery ();
                        boolean authorized = false;
                        if (rs.next ()) authorized = rs.getBoolean (1);
                        if (log.isLoggable (Level.FINEST)) {
                            log.finest ("Caching permission test result.");
                        }
                        cachedSubjectPermissions.put (permission, new Boolean (authorized));
                        if (log.isLoggable (Level.FINER)) {
                            log.exiting (CLASS_NAME, "implies", authorized?Boolean.TRUE:Boolean.FALSE);
                        }
                        return authorized;
                    }
                    catch (Exception ex) {
                        log.log (Level.SEVERE, "Permission can not be tested.", ex);
                    }
                    finally {
                        if (rs != null) {
                            try {
                                rs.close ();
                            }
                            catch (Exception ex) {}
                        }
                        if (ps != null) {
                            try {
                                ps.close ();
                            }
                            catch (Exception ex) {}
                        }
                        if (con != null) {
                            try {
                                con.close ();
                            }
                            catch (Exception ex) {}
                        }
                        if (ctx != null) {
                            try {
                                ctx.close ();
                            }
                            catch (Exception ex) {}
                        }
                    }
                }
                else {
                    if (log.isLoggable (Level.FINEST)) {
                        log.finest ("Permission test result found in cache.");
                    }
                    if (log.isLoggable (Level.FINER)) {
                        log.exiting (CLASS_NAME, "implies", cachedPermissionResult);
                    }
                    return cachedPermissionResult.booleanValue ();
                }
            }
            if (log.isLoggable (Level.FINER)) {
                log.exiting (CLASS_NAME, "implies", Boolean.FALSE);
            }
            return false;
        }
        else {
            return super.implies (domain, permission);
        }
    }

    /**
     * Returns current subject's user name. Subject is taken from current calling context.
     * User name is extracted from first found private <code>PasswordCredential</code>.
     * Returns <code>null</code> if subject can not be retrieved or it does not contain private
     * <code>PasswordCredential</code> or user name in <code>PasswordCredential</code> is not set.
     *
     * @return <code>String</code> current user's name or null.
     */
    public static String getCurrentUserName () {
        if (log.isLoggable (Level.FINER)) {
            log.entering (CLASS_NAME, "getCurrentUserName");
        }
        String userName = null;
        Subject subject = Subject.getSubject (AccessController.getContext ());
        if (subject != null) {
            if (log.isLoggable (Level.FINEST)) {
                log.finest ("Got subject: " + subject.toString ());
            }
            Set<Object> privateCredentials = subject.getPrivateCredentials (Object.class);
            for (Object credential : privateCredentials) {
                if (credential instanceof PasswordCredential) {
                    if (log.isLoggable (Level.FINEST)) {
                        log.finest ("Got instance of PasswordCredential: " + credential.toString ());
                    }
                    userName = ((PasswordCredential) credential).getUser ();
                    break;
                }
            }
        }
        else {
            if (log.isLoggable (Level.FINEST)) {
                log.finest ("Subject not found in current context.");
            }
        }
        if ("".equals (userName)) userName = null;
        if (log.isLoggable (Level.FINER)) {
            log.exiting (CLASS_NAME, "getCurrentUserName", userName);
        }
        return userName;
    }
}