/**
* Copyright (C) 2012 KRM Associates, Inc. healtheme@krminc.com
*
* Licensed 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.
*/
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package com.krminc.phr.security;
import com.sun.appserv.security.AppservRealm;
import com.sun.enterprise.security.auth.realm.User;
import com.sun.enterprise.security.auth.realm.BadRealmException;
import com.sun.enterprise.security.auth.realm.NoSuchUserException;
import com.sun.enterprise.security.auth.realm.NoSuchRealmException;
import java.util.Enumeration;
import java.util.Vector;
import java.util.Properties;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.logging.Logger;
import java.util.logging.Level;
import com.sun.logging.LogDomains;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import javax.naming.InitialContext;
/**
*
* @author cmccall
*/
public class PHRRealm extends AppservRealm {
//the following property variables are allocated in the following fashion:
// private static PROPERTY => default value of PROPERTY
// private static PROPERTY_PARAM => string used to specific PROPERTY in domain.xml
// private propertyValue => variable containing version of PROPERTY for use in code (whether default or specific in domain.xml)
//auth type property variables
private static String AUTH_TYPE = "phrcustomauth";
private static String AUTH_TYPE_PARAM = "auth-type";
private String authType = null;
//failed attempts property variables
private static String FAILED_ATTEMPTS = "5";
private static String FAILED_ATTEMPTS_PARAM = "failed-attempts";
private Integer failedAttempts = null;
//locked role name property variables
private static String LOCKED_ROLE = "ROLE_LOCKED";
private static String LOCKED_ROLE_PARAM = "locked-role";
private String lockedRole = null;
//reset role name property variables
private static String RESET_ROLE = "ROLE_RESET";
private static String RESET_ROLE_PARAM = "reset-role";
private String resetRole = null;
//jdbc resource name property variables
private static String JDBC_RESOURCE = "jdbc/phr";
private static String JDBC_RESOURCE_PARAM = "jdbc-resource";
private String jdbcResource = null;
//logging level property variables
private static String LOG_LEVEL = "INFO";
private static String LOG_LEVEL_PARAM = "logging-level";
private Level logLevel = null;
// end properties variables
private Connection conn = null;
private com.sun.appserv.jdbc.DataSource ds = null;
private static Logger _logger = null;
static {
_logger = Logger.getLogger(LogDomains.SECURITY_LOGGER);
}
/*
* This method is invoked during server startup when the realm is
* initially loaded.
* The props argument contains the properties defined
* for this realm in domain.xml.
* The realm can do any initialization it needs in this method.
* If the method returns without throwing an exception,
* J2EE Application Server assumes the realm is ready
* to service authentication requests.
* If an exception is thrown, the realm eis disabled,
* check the server.log for messages.
*/
public void init(Properties props)
throws BadRealmException, NoSuchRealmException{
super.init(props);
/*
* Set the jaas context, otherwise server doesn't indentify the login module.
* jaas-context is the property specified in domain.xml and
* is the name corresponding to LoginModule
* config/login.conf
*/
String jaasCtx = props.getProperty(AppservRealm.JAAS_CONTEXT_PARAM);
this.setProperty(AppservRealm.JAAS_CONTEXT_PARAM, jaasCtx);
/*
* Get any other interested properties from configuration file - domain.xml
*
*/
String authTypeProp = props.getProperty(AUTH_TYPE_PARAM);
this.authType = (authTypeProp != null) ? authTypeProp : AUTH_TYPE;
String failedAttemptsProp = props.getProperty(FAILED_ATTEMPTS_PARAM);
this.failedAttempts = Integer.valueOf((failedAttemptsProp != null) ? failedAttemptsProp : FAILED_ATTEMPTS);
String lockedRoleProp = props.getProperty(LOCKED_ROLE_PARAM);
this.lockedRole = (lockedRoleProp != null) ? lockedRoleProp : LOCKED_ROLE;
String resetRoleProp = props.getProperty(RESET_ROLE_PARAM);
this.resetRole = (resetRoleProp != null) ? resetRoleProp : RESET_ROLE;
String jdbcResourceProp = props.getProperty(JDBC_RESOURCE_PARAM);
this.jdbcResource = (jdbcResourceProp != null) ? jdbcResourceProp : JDBC_RESOURCE;
String logLevelParam = props.getProperty(LOG_LEVEL_PARAM);
logLevelParam = (logLevelParam != null) ? logLevelParam : LOG_LEVEL;
if (logLevelParam.equalsIgnoreCase("INFO")) {
this.logLevel = Level.INFO;
} else if (logLevelParam.equalsIgnoreCase("SEVERE")) {
this.logLevel = Level.SEVERE;
} else if (logLevelParam.equalsIgnoreCase("WARNING")) {
this.logLevel = Level.WARNING;
} else if (logLevelParam.equalsIgnoreCase("OFF")) {
this.logLevel = Level.OFF;
} else if (logLevelParam.equalsIgnoreCase("FINEST")) {
this.logLevel = Level.FINEST;
}
log("Initialized PHR Custom Realm");
}
private boolean createDS() {
if (ds != null) return true;
try {
InitialContext ctx = new InitialContext();
if (ctx == null) {
log("JNDI problem, Cannot get Initial Context.");
return false;
}
ds = (com.sun.appserv.jdbc.DataSource)ctx.lookup(jdbcResource);
if (ds == null) {
log("Unable to lookup datasource.");
return false;
}
}
catch (Exception e){
log("Exception encountered in database connection initialization.");
log(e.getMessage());
return false;
}
return true;
}
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException("Not supported");
}
/**
* Return a short description supported authentication by this realm.
*
* @return Description of the kind of authentication that is directly
* supported by this realm.
*/
public String getAuthType(){
return this.authType;
}
public String getLockedRole() {
return this.lockedRole;
}
public String getResetRole(){
return this.resetRole;
}
public String getJdbcResource(){
return this.jdbcResource;
}
public Integer getFailedAttempts(){
return this.failedAttempts;
}
public Level getLoggingLevel(){
return this.logLevel;
}
/**
* Returns names of all the users in this particular realm.
*
* @return enumeration of user names
*
*/
public Enumeration getUserNames() throws BadRealmException
{
String query = "SELECT username FROM user_users";
ResultSet rs = null;
Vector usernames = new Vector();
PreparedStatement st = null;
try {
createDS();
conn = ds.getNonTxConnection();
st = conn.prepareStatement(query);
rs = st.executeQuery();
}
catch (Exception e){
log("Error getting usernames from database");
log(e.getMessage());
rs = null;
}
finally {
try {
conn.close();
}
catch(Exception e) {
log(e.getMessage());
}
conn = null;
}
if (rs != null) {
try {
rs.beforeFirst();
while (rs.next()) {
usernames.add(rs.getString(1));
}
}
catch (Exception e){
log("Error getting usernames from resultset");
log(e.getMessage());
}
finally {
try {
st.close();
rs.close();
}
catch(Exception e) {
log(e.getMessage());
}
}
}
return usernames.elements();
}
/**
* Returns the information recorded about a particular named user.
*
* This method always throws a BadRealmException since this method
* is not supported in this context.
*
* @exception BadRealmException
*
*/
public User getUser(String name)
throws NoSuchUserException, BadRealmException
{
throw new BadRealmException("Not supported");
}
/**
* Returns names of all the groups in this particular realm.
*
* @return enumeration of group names (strings)
*
*/
public Enumeration getGroupNames()
throws BadRealmException
{
//check role cache before querying
String query = "SELECT UNIQUE role FROM user_roles";
ResultSet rs = null;
Vector roles = new Vector();
PreparedStatement st = null;
try {
createDS();
conn = ds.getNonTxConnection();
st = conn.prepareStatement(query);
rs = st.executeQuery();
}
catch (Exception e){
log("Error getting roles from database");
log(e.getMessage());
rs = null;
}
finally {
try {
conn.close();
}
catch(Exception e) {
log(e.getMessage());
}
conn = null;
}
if (rs != null) {
try {
rs.beforeFirst();
while (rs.next()) {
roles.add(rs.getString(1));
}
}
catch (Exception e){
log("Error getting roles from resultset");
log(e.getMessage());
}
finally {
try {
st.close();
rs.close();
}
catch(Exception e) {
log(e.getMessage());
}
}
}
return roles.elements();
}
/**
* Returns enumeration of groups that a particular user belongs to.
*
*@exception NoSuchUserException
*/
public Enumeration getGroupNames(String user)
throws NoSuchUserException
{
//check user cache before querying?
String query = "SELECT DISTINCT role FROM user_roles WHERE username = ?";
ResultSet rs = null;
Vector roles = new Vector();
PreparedStatement st = null;
try {
createDS();
conn = ds.getNonTxConnection();
st = conn.prepareStatement(query);
st.setString(1, user);
rs = st.executeQuery();
}
catch (Exception e){
log("Error getting roles from database");
log(e.getMessage());
rs = null;
}
finally {
try {
conn.close();
}
catch(Exception e) {
log(e.getMessage());
}
conn = null;
}
if (rs != null) {
try {
rs.beforeFirst();
while (rs.next()) {
roles.add(rs.getString(1));
}
}
catch (Exception e){
log("Error getting roles from resultset");
log(e.getMessage());
}
finally {
try {
st.close();
rs.close();
}
catch(Exception e) {
log(e.getMessage());
}
}
} else {
throw new NoSuchUserException("User not available.");
}
return roles.elements();
}
/**
* Refreshes the realm data so that new users/groups are visible.
*
*/
public void refresh() throws BadRealmException
{
super.refresh();
}
/**
* Checks the authentication of a user and returns the groups it belongs to.
*
* @return groups that this particular user belongs to
*/
public String[] authenticateUser(String user, String password)
{
String query = "SELECT password, requires_reset, is_locked_out, active, failed_password_attempts FROM user_users WHERE username = ?";
ResultSet rs = null;
String passwordHash = new String();
boolean requiresReset = false;
boolean lockedOut = false;
boolean active = false;
int failedAttemptsVal = 0;
PreparedStatement st = null;
try {
createDS();
conn = ds.getNonTxConnection();
st = conn.prepareStatement(query);
st.setString(1, user);
rs = st.executeQuery();
}
catch (Exception e){
log("Error getting password from database");
log(e.getMessage());
rs = null;
}
finally {
try {
conn.close();
}
catch(Exception e) {
log(e.getMessage());
}
conn = null;
}
if (rs != null) {
try {
if (rs.next()) {
passwordHash = rs.getString(1);
requiresReset = rs.getBoolean(2);
lockedOut = rs.getBoolean(3);
active = rs.getBoolean(4);
failedAttemptsVal = rs.getInt(5);
}
}
catch (Exception e){
log("Error getting password from resultset");
log(e.getMessage());
}
finally {
try {
st.close();
rs.close();
}
catch(Exception e) {
log(e.getMessage());
}
}
}
//inactive users have no roles, no login attempt monitoring
if (!active) return null;
//locked users have a locked role, which is filtered by the Login class as needed
if (lockedOut){
incrementFailedUpdate(user);
String[] lock = {lockedRole};
return lock;
}
if (!passwordHash.isEmpty()) {
if (passwordHash.equals(DigestUtils.sha512Hex(password))) {
//password is correct
//find and return groups
String[] retArr = null;
try {
Enumeration userGroups = getGroupNames(user);
ArrayList retGroups = new ArrayList();
//populate with groups from db
while (userGroups.hasMoreElements()){
retGroups.add(userGroups.nextElement().toString());
}
//force password reset if needed by adding role
if (requiresReset) {
retGroups.add(resetRole);
}
//formulate returnable collection
Object[] arr = retGroups.toArray();
retArr = new String[arr.length];
for (int i=0; i<arr.length; i++){
retArr[i] = arr[i].toString();
}
}
catch (NoSuchUserException e){
log("Exception encountered looking up password");
}
catch (Exception e){
log("Group lookup error: " + e.getClass() + ":" + e.getMessage());
}
//reset bad login info, if needed, now that we're successful
// this will only happen for valid logins and pw reset logins, not locks or inactive accts
if (failedAttemptsVal > 0 ) doSuccessfulUpdate(user);
return retArr;
} else {
log("Passwords do not match");
try {
InvalidPasswordAttempt(user);
}
catch (NoSuchUserException e) {
//not an issue here
}
}
} else {
log("Unable to find user password");
}
return null;
}
private void InvalidPasswordAttempt (String username) throws NoSuchUserException {
String query = "SELECT failed_password_attempts, failed_password_window_start FROM user_users WHERE username = ?";
ResultSet rs = null;
PreparedStatement st = null;
Timestamp windowStart = null;
int failedAttemptsVal = 0;
try {
createDS();
conn = ds.getNonTxConnection();
st = conn.prepareStatement(query);
st.setString(1, username);
rs = st.executeQuery();
}
catch (Exception e){
log("Error getting roles from database");
log(e.getMessage());
rs = null;
}
finally {
try {
conn.close();
}
catch(Exception e) {
log(e.getMessage());
}
conn = null;
}
if (rs != null) {
try {
rs.beforeFirst();
while (rs.next()) {
failedAttemptsVal = rs.getInt(1);
windowStart = rs.getTimestamp(2);
}
}
catch (Exception e){
log("Error getting invalid attempt values from resultset");
log(e.getMessage());
}
finally {
try {
st.close();
rs.close();
}
catch(Exception e) {
log(e.getMessage());
}
}
} else {
throw new NoSuchUserException("User not available.");
}
//take values and decide whether to lock account, increment failed attempts, or start new failure window
if (windowStart != null) {
//check if user has more than X previously existing failed logins
if (Integer.valueOf(failedAttemptsVal).compareTo(failedAttempts) >= 0) {
//lock out
doFailedUpdate(username,null,failedAttemptsVal+1, true);
} else {
//dont lock account, just increment failed attempts
doFailedUpdate(username, windowStart, failedAttemptsVal+1, false);
}
} else {
//windowStart is null, set to now and set failed attempts to 1
GregorianCalendar tempCal = new GregorianCalendar();
windowStart = new java.sql.Timestamp(tempCal.getTimeInMillis());
failedAttemptsVal = 1;
doFailedUpdate(username,windowStart,failedAttemptsVal, false);
}
}
//reset password attempts and password window
private void doSuccessfulUpdate(String username) {
String query = "UPDATE user_users SET failed_password_attempts = ? , failed_password_window_start = ? WHERE username = ?";
PreparedStatement st = null;
try {
createDS();
//TX for UPDATE
conn = ds.getConnection();
st = conn.prepareStatement(query);
st.setInt(1, 0); //reset failed attempt count
st.setTimestamp(2, null); //reset failed password timestamp
st.setString(3, username);
st.executeUpdate();
}
catch (Exception e){
log("Error resetting failed password values");
log(e.getMessage());
}
finally {
try {
st.close();
conn.close();
}
catch(Exception e) {
log(e.getMessage());
}
conn = null;
}
}
//extracted to own method, not using doFailedUpdate, to avoid overhead of manipulating multiple fields
private void incrementFailedUpdate(String username) {
String query = "UPDATE user_users SET failed_password_attempts = failed_password_attempts + 1 WHERE username = ?";
PreparedStatement st = null;
try {
createDS();
//TX for UPDATE
conn = ds.getConnection();
st = conn.prepareStatement(query);
st.setString(1, username);
st.executeUpdate();
}
catch (Exception e){
log("Error incrementing failed password value");
log(e.getMessage());
}
finally {
try {
st.close();
conn.close();
}
catch(Exception e) {
log(e.getMessage());
}
conn = null;
}
}
private void doFailedUpdate(String username, java.sql.Timestamp windowStart, int failedAttempts, boolean setLock){
String query = "UPDATE user_users SET failed_password_attempts = ? , failed_password_window_start = ? , is_locked_out = ?, lockout_begin = ? WHERE username = ?";
PreparedStatement st = null;
java.sql.Timestamp lockoutBegin = null;
if (setLock) {
GregorianCalendar tempCal = new GregorianCalendar(java.util.TimeZone.getTimeZone("GMT"));
lockoutBegin = new java.sql.Timestamp(tempCal.getTimeInMillis());
}
try {
createDS();
//TX for UPDATE
conn = ds.getConnection();
st = conn.prepareStatement(query);
st.setInt(1, failedAttempts);
st.setTimestamp(2, windowStart);
st.setBoolean(3, setLock);
st.setTimestamp(4, lockoutBegin);
st.setString(5, username);
st.executeUpdate();
}
catch (Exception e){
log("Error updating failed password values");
log(e.getMessage());
}
finally {
try {
st.close();
conn.close();
}
catch(Exception e) {
log(e.getMessage());
}
conn = null;
}
}
public static Logger getLogger() {
return _logger;
}
/*
* Helper method
*
* Simple message print method used throughout the program
*/
public void log(String message){
if (logLevel == null) logLevel = Level.INFO;
_logger.log(logLevel, "PHRRealm:" + message );
}
}