/*
* Adito
*
* Copyright (C) 2003-2006 3SP LTD. All Rights Reserved
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package com.adito.security;
import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import javax.servlet.http.HttpSession;
import org.apache.commons.cache.Cache;
import org.apache.commons.cache.FileStash;
import org.apache.commons.cache.SimpleCache;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.adito.boot.ContextHolder;
import com.adito.boot.Util;
import com.adito.core.CoreUtil;
import com.adito.core.UserDatabaseManager;
import com.adito.policyframework.Policy;
import com.adito.policyframework.PolicyDatabaseFactory;
import com.adito.policyframework.ResourceUtil;
import com.adito.realms.Realm;
/**
* <p>
* State machine which holds the logon state so that the display to the user can
* be obfuscated.
*/
public class LogonStateAndCache {
final static Log log = LogFactory.getLog(LogonStateAndCache.class);
public static final String LOGON_STATE_MACHINE = "logonStateMachine";
public static final int STATE_INITIAL = 0;
public static final int STATE_STARTED = 1;
public static final int STATE_DISPLAY_USERNAME_ENTRY = 2;
public static final int STATE_DISPLAY_USERNAME_ENTERED = 3;
public static final int STATE_UNKNOWN_USERNAME = 4;
public static final int STATE_UNKNOWN_USERNAME_PROMPT_FOR_PASSWORD = 5;
public static final int STATE_USERNAME_KNOWN = 6;
public static final int STATE_KNOWN_USERNAME_SINGLE_SCHEME = 7;
public static final int STATE_KNOWN_USERNAME_MULTIPLE_SCHEMES = 8;
public static final int STATE_KNOWN_USERNAME_WRONG_PASSWORD = 9;
public static final int STATE_VALID_LOGON = 10;
public static final int STATE_RETURN_TO_LOGON = 11;
public static final int STATE_KNOWN_USERNAME_NO_SCHEME_SPOOF_PASSWORD_ENTRY = 12;
public static final int STATE_KNOWN_USERNAME_MULTIPLE_SCHEMES_SELECT = 13;
private int state = STATE_INITIAL;
private User user;
private String username;
private List<Integer> resourceIds = null;
private List<AuthenticationScheme> authSchemes = new ArrayList<AuthenticationScheme>();
private AuthenticationScheme highestPriorityScheme = null;
private String spoofedUsername;
/* Spoof cache used to store fake authentication schemes
*
* TODO Default to maximum of 2000 fake users. This should be configurable
*/
private static Cache spoofCache;
static {
File dir = new File(ContextHolder.getContext().getTempDirectory(), "spoof");
if(dir.exists()) {
Util.delTree(dir);
}
spoofCache = new SimpleCache(new FileStash(FileStash.DEFAULT_MAX_BYTES, 2000,
new File[] { dir }, true));
}
public LogonStateAndCache(int startState, HttpSession session) {
super();
session.setAttribute(LOGON_STATE_MACHINE, this);
this.setState(startState);
}
public int getState() {
return state;
}
public String getSpoofedUsername() {
return spoofedUsername;
}
public void setState(int newState) {
if (log.isDebugEnabled()){
log.debug("State" + state + " is to be changed to " + newState);
}
this.state = newState;
if (resourceIds != null && this.state == STATE_USERNAME_KNOWN){
if (resourceIds.size() == 0) {
this.setState(LogonStateAndCache.STATE_KNOWN_USERNAME_NO_SCHEME_SPOOF_PASSWORD_ENTRY);
} else if (resourceIds.size() == 1) {
this.setState(LogonStateAndCache.STATE_KNOWN_USERNAME_SINGLE_SCHEME);
} else if (resourceIds.size() > 1) {
this.setState(LogonStateAndCache.STATE_KNOWN_USERNAME_MULTIPLE_SCHEMES);
}
}
}
public void setUser(User user) throws Exception {
this.user = user;
this.authSchemes.clear();
setResourceIds();
this.highestPriorityScheme.setAccountLock(LogonControllerFactory.getInstance().checkForAccountLock(user.getPrincipalName(), user.getRealm().getResourceName()));
}
public boolean hasUser() {
return user == null ? false : true;
}
public User getUser() {
return user;
}
public boolean enabledSchemesGraeterThanOne() {
return this.authSchemes.size() > 1;
}
public List getResourceIds() {
return resourceIds;
}
private void setResourceIds() throws Exception {
List resourceIds = ResourceUtil.getSignonAuthenticationSchemeIDs(user);
int highestPriority = Integer.MAX_VALUE;
highestPriorityScheme = null;
for (AuthenticationScheme element : SystemDatabaseFactory.getInstance().getAuthenticationSchemeSequences()) {
if (resourceIds.contains(new Integer(element.getResourceId())) && !element.isSystemScheme() && element.getEnabled()) {
this.authSchemes.add(element);
if (element.getPriorityInt() < highestPriority) {
highestPriority = element.getPriorityInt();
highestPriorityScheme = element;
}
}
else{
resourceIds.remove(new Integer(element.getResourceId()));
}
}
if(highestPriorityScheme == null) {
throw new Exception("User is not attached to any policies that are assigned to any valid authentication schemes. " +
"This may be because they were assigned a scheme that contains an authentication module that no longer exists.");
}
this.resourceIds = resourceIds;
this.highestPriorityScheme.setUser(user);
this.setState(LogonStateAndCache.STATE_USERNAME_KNOWN);
}
public AuthenticationScheme getHighestPriorityScheme() {
return highestPriorityScheme;
}
public void forceHighestPriorityScheme(String id, String username) throws Exception {
this.highestPriorityScheme = SystemDatabaseFactory.getInstance().getAuthenticationSchemeSequence(Integer.parseInt(id));
if(!spoofCache.contains(username)) {
if (resourceIds.contains(new Integer(id))) {
this.highestPriorityScheme.setUser(user);
}
else {
throw new Exception("The selected scheme is not valid for the user.");
}
}
}
public List getAuthSchemes() {
return authSchemes;
}
/**
* Randomly choose a list of spoofed authentications schemes. This is
* to prevent an attacker from determining if a username is invalid or
* not by looking if there are multiple authentication schemes
* available. If there are none, he can assume the user is invalid.
* This method does its best to create a credible random list of
* possible schemes. None of them will actually work, but they
* will be presented to attacker.
*
* @param username username
* @throws Exception
*/
public void setSpoofedHighestPriorityScheme(String username) throws Exception {
Calendar now = Calendar.getInstance();
authSchemes = new ArrayList<AuthenticationScheme>();
spoofedUsername = username;
// Get the valid schemes// Look for cached scheme list
int[] authSchemeIds = (int[]) spoofCache.retrieve(username);
if (spoofCache.contains(username)) {
if (log.isDebugEnabled()) {
log.debug("Using cached spoofed schemes for " + username);
}
for (int schemeId : authSchemeIds) {
AuthenticationScheme scheme = SystemDatabaseFactory.getInstance().getAuthenticationSchemeSequence(schemeId);
// The scheme could have been deleted since it was cached
if (scheme != null) {
authSchemes.add(scheme);
}
}
} else {
if (log.isDebugEnabled()) {
log.debug("Building new list of spoofed schemes for " + username);
}
// Get the valid schemes
List<AuthenticationScheme> schemes = SystemDatabaseFactory.getInstance().getAuthenticationSchemeSequences();
for (AuthenticationScheme scheme : new ArrayList<AuthenticationScheme>(schemes)) {
if (scheme.isSystemScheme()) {
schemes.remove(scheme);
}
}
// Add any schemes that are available to anyone
Realm realm = UserDatabaseManager.getInstance().getDefaultRealm();
Policy p = PolicyDatabaseFactory.getInstance().getPolicy(PolicyDatabaseFactory.getInstance()
.getEveryonePolicyIDForRealm(realm));
for (AuthenticationScheme scheme : schemes) {
if (PolicyDatabaseFactory.getInstance().isResourceAttachedToPolicy(scheme, p, realm)) {
authSchemes.add(scheme);
}
}
// If no schemes were available to everyone, add a dummy default
if (authSchemes.size() == 0) {
AuthenticationScheme scheme = new DefaultAuthenticationScheme(-1, -1, "", "", now, now, true, 0);
scheme.addModule("Password");
authSchemes.add(scheme);
}
// If there is only one scheme, pick some randomly, each on gets 50/50 chance
if (authSchemes.size() == 1 && schemes.size() > 1) {
for (AuthenticationScheme scheme : schemes) {
if (scheme != authSchemes.get(0) && Math.random() >= 0.5) {
authSchemes.add(scheme);
}
}
}
// If there is still only one scheme, pick a single random one
if (authSchemes.size() == 1 && schemes.size() > 1) {
authSchemes.add(authSchemes.get(1 + (int) (Math.random() * (authSchemes.size() - 1))));
}
/* Cache the scheme id's so if the same user ID is attempted
* the same spoofed schemes will appear
*/
int[] schemeNames = new int[authSchemes.size()];
for (int idx = authSchemes.size() - 1; idx >= 0; idx--) {
schemeNames[idx] = authSchemes.get(idx).getResourceId();
}
// TODO Cache them for 3 days - make configurable?
if (log.isDebugEnabled()) {
log.debug("Caching spoofed schemes for " + username);
}
CoreUtil.storeToCache(spoofCache, username, schemeNames, 360000 * 24 * 3, 0);
}
//
resourceIds = new ArrayList<Integer>();
this.highestPriorityScheme = authSchemes.get(0);
}
/**
* Remove cached spoofed user information. This should be called
* as a user is succesfully found. This deals with the situation where
* a user tries to logon with an invalid name (a user that has not
* yet been created). This fails, but the administrator later adds
* the user. The user then tries to logon again before the spoof cache
* is cleared. Unless this method is called as soon as the valid
* username is found, the spoofing mechanism will think the user
* is still invalid.
*
* @param username username to remove from spoof cache
*/
public void removeFromSpoofCache(String username) {
spoofCache.clear(username);
}
}