com.webapp.security.SecurityDataContext.java Source code

Java tutorial

Introduction

Here is the source code for com.webapp.security.SecurityDataContext.java

Source

/*
 * Copyright (c) 2014 Stephan D. Cote' - All rights reserved.
 * 
 * This program and the accompanying materials are made available under the 
 * terms of the MIT License which accompanies this distribution, and is 
 * available at http://creativecommons.org/licenses/MIT/
 *
 * Contributors:
 * Stephan D. Cote 
 * - Initial concept and initial implementation
 */
package com.webapp.security;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.sql.DataSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;

import coyote.commons.ByteUtil;
import coyote.commons.StringUtil;
import coyote.commons.security.CredentialSet;
import coyote.commons.security.GenericSecurityContext;
import coyote.commons.security.GenericSecurityPrincipal;
import coyote.commons.security.Login;
import coyote.commons.security.Permission;
import coyote.commons.security.Role;
import coyote.commons.security.SecurityContext;
import coyote.commons.security.Session;

/**
 * This is a security context backed by a data store.
 * 
 * <p>The main functionality is provided by the GenericContext superclass. This 
 * class has a few overrides which allow it to capture changes to the security
 * context data model and persist those changes to a database.<p>
 * 
 * <p>Other versions of this security context implement a distributed data 
 * store which allows many different servers in a data center to share security 
 * data such as sessions. This way it is possible for a user to login and 
 * establish as session on one server but still hit a different server on 
 * subsequent requests.</p> 
 */
public class SecurityDataContext extends GenericSecurityContext implements SecurityContext {

    private DataSource dataSource = null;
    private static final Log LOG = LogFactory.getLog(SecurityDataContext.class);
    private JdbcTemplate jdbcTemplate = null;

    private static final Role NO_ROLE = new Role("NONE", "NO ROLE");

    /**
    * @param name
    */
    public SecurityDataContext(String name) {
        super(name);
    }

    /**
    * @param source
    */
    public void setDataSource(DataSource source) {
        this.dataSource = source;
        jdbcTemplate = new JdbcTemplate(source);

    }

    /**
    * Initialize the context
    */
    public void init() throws Exception {
        // Get a listing of the tables in this data store
        Set<String> tableset = new HashSet<String>();
        DatabaseMetaData md = dataSource.getConnection().getMetaData();
        ResultSet rs = md.getTables(null, null, "%", null);
        while (rs.next()) {
            tableset.add(rs.getString(3));
        }

        if (!tableset.contains("SECURITY_LOGIN")) {
            LOG.info("Creating SECURITY_LOGIN table");

            // Create the table
            String createLoginTable = "CREATE TABLE SECURITY_LOGIN ( LOGIN BIGINT NOT NULL AUTO_INCREMENT, CONTEXT VARCHAR(64) NOT NULL, NAME VARCHAR(64) NOT NULL, PASSWORD VARCHAR(64) NOT NULL, PASSWORD_HINT VARCHAR(64), IS_SYSTEM BOOLEAN, IS_ENABLED BOOLEAN, IS_LOGGEDOUT BOOLEAN, REQUIRE_PASSWORD_CHANGE BOOLEAN, CURRENCY_UOM VARCHAR(64), LOCALE VARCHAR(64), TIMEZONE VARCHAR(64), DISABLED_DATETIME TIMESTAMP, PARTY BIGINT );";
            jdbcTemplate.execute(createLoginTable);

            // Create the primary key
            String createLoginkey = "ALTER TABLE SECURITY_LOGIN ADD CONSTRAINT pk_security_login PRIMARY KEY (CONTEXT,LOGIN);";
            jdbcTemplate.execute(createLoginkey);

            // Create the primary index
            String createLoginIndex = "CREATE UNIQUE INDEX IDX_SECURITY_LOGIN ON SECURITY_LOGIN(CONTEXT,NAME);";
            jdbcTemplate.execute(createLoginIndex);
        }

        if (!tableset.contains("SECURITY_ROLE")) {
            LOG.info("Creating SECURITY_ROLE table");
            String createRoleTable = "CREATE TABLE SECURITY_ROLE ( CONTEXT VARCHAR(64) NOT NULL, ROLE VARCHAR(64) NOT NULL, DESCRIPTION VARCHAR(255) NOT NULL );";
            jdbcTemplate.execute(createRoleTable);

            String createRolekey = "ALTER TABLE SECURITY_ROLE ADD CONSTRAINT PK_SECURITY_ROLE PRIMARY KEY (CONTEXT, ROLE);";
            jdbcTemplate.execute(createRolekey);
        }

        if (!tableset.contains("SECURITY_ROLE_PERMISSION")) {
            LOG.info("Creating SECURITY_ROLE_PERMISSION table");
            String createRolePermTable = " CREATE TABLE SECURITY_ROLE_PERMISSION ( CONTEXT VARCHAR(64) NOT NULL, ROLE VARCHAR(64) NOT NULL, TARGET VARCHAR(255) NOT NULL, PERMISSION BIGINT );";
            jdbcTemplate.execute(createRolePermTable);

            String createLoginkey = "ALTER TABLE SECURITY_ROLE_PERMISSION ADD CONSTRAINT PK_SECURITY_ROLE_PERMISSION PRIMARY KEY (CONTEXT, ROLE, TARGET);";
            jdbcTemplate.execute(createLoginkey);
        }

        if (!tableset.contains("SECURITY_LOGIN_ROLE")) {
            LOG.info("Creating SECURITY_LOGIN_ROLE table");
            String createLoginRoleTable = "CREATE TABLE SECURITY_LOGIN_ROLE ( CONTEXT VARCHAR(64) NOT NULL, LOGIN BIGINT NOT NULL, ROLE VARCHAR(64) NOT NULL, FROM_DATE TIMESTAMP, THRU_DATE TIMESTAMP );";
            jdbcTemplate.execute(createLoginRoleTable);

            String createLoginRolekey = "ALTER TABLE SECURITY_LOGIN_ROLE ADD CONSTRAINT PK_SECURITY_LOGIN_ROLE PRIMARY KEY (CONTEXT, LOGIN, ROLE);";
            jdbcTemplate.execute(createLoginRolekey);
        }

        if (!tableset.contains("SECURITY_LOGIN_PERMISSION")) {
            LOG.info("Creating SECURITY_LOGIN_PERMISSION table");
            String createLoginPermTable = "CREATE TABLE SECURITY_LOGIN_PERMISSION ( CONTEXT VARCHAR(64) NOT NULL, LOGIN BIGINT NOT NULL, TARGET VARCHAR(255) NOT NULL, PERMISSION BIGINT );";
            jdbcTemplate.execute(createLoginPermTable);

            String createLoginPermKey = "ALTER TABLE SECURITY_LOGIN_PERMISSION ADD CONSTRAINT pk_security_login_permission PRIMARY KEY (CONTEXT, LOGIN, TARGET);";
            jdbcTemplate.execute(createLoginPermKey);
        }

        if (!tableset.contains("SECURITY_LOGIN_REVOCATION")) {
            LOG.info("Creating SECURITY_LOGIN_REVOCATION table");
            String createLoginRevTable = "CREATE TABLE SECURITY_LOGIN_REVOCATION ( CONTEXT VARCHAR(64) NOT NULL, LOGIN BIGINT NOT NULL, TARGET VARCHAR(255) NOT NULL, PERMISSION BIGINT );";
            jdbcTemplate.execute(createLoginRevTable);

            String createLoginRevKey = "ALTER TABLE SECURITY_LOGIN_REVOCATION ADD CONSTRAINT pk_security_login_revocation PRIMARY KEY (CONTEXT, LOGIN, TARGET);";
            jdbcTemplate.execute(createLoginRevKey);
        }

        LOG.info("Security context initialized");
    }

    /**
     * @see coyote.commons.security.GenericSecurityContext#createSession(coyote.commons.security.Login)
     */
    @Override
    public Session createSession(Login login) {
        // TODO Auto-generated method stub
        return super.createSession(login);
    }

    /**
     * @see coyote.commons.security.GenericSecurityContext#createSession(java.lang.String, coyote.commons.security.Login)
     */
    @Override
    public Session createSession(String id, Login login) {
        // TODO Auto-generated method stub
        return super.createSession(id, login);
    }

    /**
     * @see coyote.commons.security.GenericSecurityContext#getLoginByName(java.lang.String)
     */
    @Override
    public Login getLoginByName(String loginName) {
        Login retval = null;

        // first check the cache
        retval = super.getLoginByName(loginName);

        // if not in the cache...check the data store
        if (retval == null) {
            retval = this.retrieveLogin(loginName);
        }

        // return what we found
        return retval;
    }

    /**
     * @see coyote.commons.security.GenericSecurityContext#getLoginBySession(java.lang.String)
     */
    @Override
    public Login getLoginBySession(String sessionId) {
        // TODO Auto-generated method stub
        return super.getLoginBySession(sessionId);
    }

    /**
    * @see coyote.commons.security.GenericContext#add(coyote.commons.security.Login)
    */
    @Override
    public void add(Login login) {

        // Make sure we have a login
        if (login != null) {

            // Make sure the login has a principal
            if (login.getPrincipal() != null) {
                CredentialSet credentials = login.getCredentials();

                // Make sure there are credentials associated with the login
                if (credentials != null) {

                    // If there is a login with these credentials, generate a warning 
                    if (credentials != null) {

                        // get the login name (the name of the principal)
                        String principalName = login.getPrincipal().getName();

                        // First we should see if there is already a login with this name 
                        Login existing = retrieveLogin(principalName);

                        if (existing != null) {
                            LOG.warn("A login with the name '" + principalName + "' already exists in the context '"
                                    + getName() + "' - login was not added");
                        } else {

                            // add the login to the database
                            String loginid = createLogin(login);

                            // if there is a login id, the database insert was successful
                            if (loginid != null) {
                                // add the login into the cache
                                super.add(login);
                            } else {
                                LOG.error("Could not add the login to the context");
                            }
                        }
                    }
                } else {
                    LOG.warn("Ignoring attempt to add login with no security principal");
                }
            } else {
                LOG.warn("Ignoring attempt to add login without credentials");
            }
        } else {
            LOG.warn("Ignoring attempt to add a null login reference");
        }

    }

    /**
    * @param login
    * 
    * @return
    */
    private String createLogin(final Login login) {

        final String insertSql = "insert into SECURITY_LOGIN (CONTEXT,NAME,PASSWORD,PARTY) values (?,?,?,?)";

        LOG.info("Inserting new login (" + login.getPrincipal().getName() + ") into " + getName());

        if (login.getCredentials() != null && login.getCredentials().contains(CredentialSet.PASSWORD)) {

            // Get the credentials
            byte[] value = login.getCredentials().getValue(CredentialSet.PASSWORD);

            // If there is a value for the password...
            if (value != null && value.length > 0) {

                // turn it into a hex string
                final String dbValue = ByteUtil.bytesToHex(value, null);

                KeyHolder keyHolder = new GeneratedKeyHolder();

                try {
                    jdbcTemplate.update(new PreparedStatementCreator() {
                        public PreparedStatement createPreparedStatement(Connection connection)
                                throws SQLException {

                            PreparedStatement ps = connection.prepareStatement(insertSql.toString(),
                                    Statement.RETURN_GENERATED_KEYS);
                            ps.setString(1, getName().toLowerCase()); //context
                            ps.setString(2, login.getPrincipal().getName().toLowerCase()); // name
                            ps.setString(3, dbValue); // password
                            ps.setLong(4, 0);// No party ID for now
                            return ps;
                        }
                    }, keyHolder);
                } catch (DataAccessException e) {
                    LOG.error("Could not create login: " + e.getMessage());
                    return null;
                }

                // Return the ID of the login
                return keyHolder.getKey().toString();

            } else {
                LOG.error("Empty password - login not added");
            }
        } else {
            LOG.error("No login credentials - login not added");
        }

        return null;
    }

    /**
    * @param principal 
    * @param credentials
    * 
    * @return
    */
    private Login retrieveLogin(String principal) {
        Login retval = null;

        if (principal != null) {

            // do the database lookup
            List<Login> work = new ArrayList<Login>();

            final String SQL = "SELECT * FROM SECURITY_LOGIN WHERE CONTEXT = ? AND NAME = ?";

            LOG.debug(SQL);

            work = jdbcTemplate.query(SQL, new Object[] { getName().toLowerCase(), principal.toLowerCase() },
                    new LoginMapper());

            if (work.size() > 0) {
                if (work.size() > 1) {
                    LOG.fatal("There are more than one security principals with the name of '" + principal
                            + "' in the '" + getName() + "' security context; returning the first record");
                }
                retval = work.get(0);
            }

            // populate the login with roles and permissions;
            retval = populateLogin(retval);
        }
        return retval;
    }

    /**
     * Populate the given login with roles and permissions.
     * 
     * <p>For most applications, it is enough to retrieve a login as all many 
     * components need is authentication. When the application wants to perform 
     * authorizations, however, there is a need to retrieve roles and permissions
     * and place them in the login for future authorization calls. This can be an 
     * expensive operation and consumes quite a bit of memory so it is best to 
     * only populate the logins with roles and permissions only when absolutely 
     * necessary.</p>
     * 
     * <p>If the given login is null, then a null will be returned.</p>
     * 
     * @param login
     * 
     * @return
     * 
     * @see #allows(Login, long, String)
     */
    private Login populateLogin(Login login) {
        Login retval = null;

        if (login != null) {
            retval = login;

            try {
                long loginId = Long.parseLong(retval.getId());

                // Get roles
                retval.addRoles(retrieveLoginRoles(loginId));

                // retrieve permissions for the login
                retval.addPermissions(retrieveLoginPermissions(loginId));

                // retrieve revocations for the login
                retval.revokePermissions(retrieveLoginRevocations(loginId));
            } catch (NumberFormatException e) {
                LOG.error("Login ID '" + retval.getId() + "' was not valid", e);
            }

            // Even if there are no roles or permissions, we should place a 'nothing' 
            // role object in there so we don't get called again
            if (!retval.hasRoles()) {
                retval.addRole(NO_ROLE);
            }
        }

        return retval;
    }

    /**
     * @see coyote.commons.security.GenericSecurityContext#allows(coyote.commons.security.Login, long, java.lang.String)
     */
    @Override
    public boolean allows(Login login, long actions, String target) {
        // we need to make sure the login is populated from the database first, see 
        // if it has any roles or permissions
        if (login.getRoles() == null || login.getRoles().size() == 0 || login.getPermissions() == null
                || login.getPermissions().size() == 0) {
            // populate it
            populateLogin(login);
        }
        return super.allows(login, actions, target);
    }

    private List<Permission> retrieveRolePermissions(String role) {
        List<Permission> retval = new ArrayList<Permission>();
        if (role != null) {
            final String SQL = "SELECT * FROM SECURITY_ROLE_PERMISSION WHERE CONTEXT = ? AND ROLE = ?";
            LOG.debug(SQL);
            retval = jdbcTemplate.query(SQL, new Object[] { getName().toLowerCase(), role.toLowerCase() },
                    new PermissionMapper());
        }
        return retval;
    }

    private List<Role> retrieveLoginRoles(long loginId) {
        List<Role> retval = new ArrayList<Role>();
        final String SQL = "SELECT * FROM SECURITY_LOGIN_ROLE WHERE CONTEXT = ? AND LOGIN = ?";
        LOG.debug(SQL);
        retval = jdbcTemplate.query(SQL, new Object[] { getName().toLowerCase(), loginId }, new RoleMapper());

        return retval;
    }

    private List<Permission> retrieveLoginPermissions(long loginId) {
        List<Permission> retval = new ArrayList<Permission>();
        final String SQL = "SELECT * FROM SECURITY_LOGIN_PERMISSION WHERE CONTEXT = ? AND LOGIN = ?";
        LOG.debug(SQL);
        retval = jdbcTemplate.query(SQL, new Object[] { getName().toLowerCase(), loginId }, new PermissionMapper());
        return retval;
    }

    private List<Permission> retrieveLoginRevocations(long loginId) {
        List<Permission> retval = new ArrayList<Permission>();
        final String SQL = "SELECT * FROM SECURITY_LOGIN_REVOCATION WHERE CONTEXT = ? AND LOGIN = ?";
        LOG.debug(SQL);
        retval = jdbcTemplate.query(SQL, new Object[] { getName().toLowerCase(), loginId }, new PermissionMapper());
        return retval;
    }

    /**
     * This is the standard login retrieval point for logins.
     * 
     * @see coyote.commons.security.GenericSecurityContext#getLogin(java.lang.String, coyote.commons.security.CredentialSet)
     */
    @Override
    public Login getLogin(String name, CredentialSet creds) {

        // Try the cache
        Login retval = super.getLogin(name, creds);
        ;

        // If not in cache...
        if (retval == null) {
            LOG.info("Did not find login '" + name + "' in cache, searching database");
            // ... try the database
            retval = retrieveLogin(name, creds);

            if (retval != null) {
                LOG.info("Found login in the database");
                // add it to the cache
                super.add(retval);
            } else {
                LOG.info("Did not find login '" + name + "' in database either");
            }
        } else {
            LOG.info("Found login in cache -> " + retval);
        }

        return retval;
    }

    /**
    * @param principal 
    * @param credentials
    * 
    * @return
    */
    private Login retrieveLogin(String principal, CredentialSet credentials) {

        Login retval = null;

        if (principal != null) {

            byte[] value = credentials.getValue(CredentialSet.PASSWORD);
            if (value != null) {
                String dbValue = ByteUtil.bytesToHex(value, null);

                // do the database lookup
                List<Login> work = new ArrayList<Login>();

                final String SQL = "SELECT * FROM SECURITY_LOGIN WHERE CONTEXT = ? AND NAME = ? AND PASSWORD = ?";

                LOG.debug(SQL);

                work = jdbcTemplate.query(SQL, new Object[] { getName(), principal, dbValue }, new LoginMapper());

                if (work.size() > 0) {
                    if (work.size() > 1) {
                        LOG.fatal("There are more than one security principals with the name of '" + principal
                                + "' in the '" + getName() + "' security context; returning the first record");
                    }
                    retval = work.get(0);
                }

                // populate the login with roles and permissions;
                retval = populateLogin(retval);

            } else {
                LOG.warn("No password found in credentials");
            }
        }
        return retval;
    }

    private void updateLogin(Login login) {

    }

    private void deleteLogin(Login login) {

    }

    /**
    * @see coyote.commons.security.GenericContext#add(coyote.commons.security.Role)
    */
    @Override
    public void add(Role role) {
        if (role != null) {
            if (role.getName() != null) {
                String name = role.getName();
                Role existing = getRole(name);

                if (existing != null) {
                    LOG.warn("A role with the name '" + name + "' already exists in the context '" + getName()
                            + "' - role was not added");
                } else {
                    if (createRole(role)) {
                        super.add(role);
                    } else {
                        LOG.error("Could not create role in context");
                    }

                }
            } else {
                LOG.warn("Ignoring attempt to add role without a name");
            }
        } else {
            LOG.warn("Ignoring attempt to add a null role reference");
        }
    }

    /**
     * @see coyote.commons.security.GenericSecurityContext#getRole(java.lang.String)
     */
    @Override
    public Role getRole(String name) {
        // first try the cache
        Role retval = super.getRole(name);

        // if not in the cache...
        if (retval == null) {
            // .. retrieve from the database
            retval = retrieveRole(name);

            // If we retrieved a role from the database
            if (retval != null) {
                // add it to the cache
                super.add(retval);
            }
        }

        // return what we found
        return retval;
    }

    /**
     * @param name
     * @return
     */
    private Role retrieveRole(String name) {

        Role retval = null;

        // do the database lookup
        List<Role> work = new ArrayList<Role>();

        final String SQL = "SELECT * FROM SECURITY_ROLE WHERE CONTEXT = ? AND ROLE = ?";

        LOG.debug(SQL);

        work = jdbcTemplate.query(SQL, new Object[] { getName().toLowerCase(), name.toLowerCase() },
                new RoleMapper());

        if (work.size() > 0) {
            if (work.size() > 1) {
                LOG.fatal("There are more than one security roles with the name of '" + name + "' in the '"
                        + getName() + "' security context; returning the first record");
            }
            retval = work.get(0);
        }
        return retval;
    }

    /**
     * @param role
     */
    private boolean createRole(final Role role) {
        if (role != null) {
            final String insertRoleSql = "insert into SECURITY_ROLE (CONTEXT, ROLE, DESCRIPTION) values (?,?,?)";
            final String insertRolePermSql = "insert into SECURITY_ROLE_PERMISSION (CONTEXT, ROLE, TARGET, PERMISSION) values (?,?,?,?)";

            LOG.info("Inserting new role (" + role.getName() + "-" + role.getDescription() + ") into " + getName()
                    + " context");

            // If there is a value for the password...
            if (role.getName() != null && role.getName().trim().length() > 0) {

                try {
                    jdbcTemplate.update(new PreparedStatementCreator() {
                        public PreparedStatement createPreparedStatement(Connection connection)
                                throws SQLException {

                            PreparedStatement ps = connection.prepareStatement(insertRoleSql.toString(),
                                    Statement.RETURN_GENERATED_KEYS);
                            ps.setString(1, getName().toLowerCase()); //context
                            ps.setString(2, role.getName().toLowerCase()); // name
                            ps.setString(3, role.getDescription()); // description
                            return ps;
                        }
                    });
                } catch (DataAccessException e) {
                    LOG.error("Could not create role: " + e.getMessage());
                    return false;
                }

                // now we have to insert any permissions in the role

                List<Permission> perms = role.getPermissions();
                for (final Permission perm : perms) {

                    try {
                        jdbcTemplate.update(new PreparedStatementCreator() {
                            public PreparedStatement createPreparedStatement(Connection connection)
                                    throws SQLException {

                                PreparedStatement ps = connection.prepareStatement(insertRolePermSql.toString(),
                                        Statement.RETURN_GENERATED_KEYS);
                                ps.setString(1, getName().toLowerCase()); //context
                                ps.setString(2, role.getName().toLowerCase()); // name
                                ps.setString(3, perm.getTarget()); // target of the permission
                                ps.setLong(4, perm.getAction());
                                return ps;
                            }
                        });
                    } catch (DataAccessException e) {
                        LOG.error("Could not record permission target '" + perm.getTarget() + "' for role '"
                                + role.getName() + "' : " + e.getMessage());
                        return false;
                    }

                }

                return true;
            } else {
                LOG.error("Empty role name - role not added");
            }
        } else {
            LOG.error("Null role cannot be added to context");
        }
        return false;
    }

    /**
     * Maps a login record to a simple Login record with a security principal and 
     * a credential set.
     */
    public final class LoginMapper implements RowMapper<Login> {

        @Override
        public Login mapRow(final ResultSet rs, final int rowNum) throws SQLException {
            final Login retval = new Login();

            retval.setId(rs.getString("LOGIN"));
            retval.setPrincipal(new GenericSecurityPrincipal(rs.getString("PARTY"), rs.getString("NAME")));
            String passwd = rs.getString("PASSWORD");
            if (StringUtil.isNotBlank(passwd)) {
                CredentialSet creds = new CredentialSet();
                creds.add(CredentialSet.PASSWORD, ByteUtil.hexToBytes(passwd));
                retval.setCredentials(creds);
            } else {
                LOG.warn("Retrieved a login (" + retval.getPrincipal().getName() + ") with no credentials");
            }

            return retval;
        }
    }

    /**
     * Map a resultset row into a permission
     */
    public final class PermissionMapper implements RowMapper<Permission> {

        @Override
        public Permission mapRow(final ResultSet rs, final int rowNum) throws SQLException {
            final Permission retval = new Permission(rs.getString("TARGET"), rs.getLong("PERMISSION"));
            return retval;
        }
    }

    /**
     * Map a resultset row to a Role object.
     */
    public final class RoleMapper implements RowMapper<Role> {

        @Override
        public Role mapRow(final ResultSet rs, final int rowNum) throws SQLException {
            final Role retval = new Role(rs.getString("ROLE"), rs.getString("DESCRIPTION"));
            return retval;
        }
    }

}