/* The contents of this file are subject to the license and copyright terms * detailed in the license directory at the root of the source tree (also * available online at http://fedora-commons.org/license/). */ package fedora.server.security.servletfilters; import java.util.Calendar; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * @author Bill Niebel */ public class CacheElement { private static final Log LOG = LogFactory.getLog(CacheElement.class); private static final Calendar EARLIER; private static final boolean s_expired_default = true; // safest private static final long MILLIS_IN_SECOND = 1000; private static final long MILLIS_IN_MINUTE = 60 * MILLIS_IN_SECOND; private static final long MILLIS_IN_HOUR = 60 * MILLIS_IN_MINUTE; private static final long MILLIS_IN_DAY = 24 * MILLIS_IN_HOUR; private final String m_userid; private final String m_cacheid; private final String m_cacheabbrev; private String m_password = null; private boolean m_valid = false; private Calendar m_expiration = null; private Boolean m_authenticated = null; private Map m_namedValues = null; private String m_errorMessage = null; static { Calendar temp = Calendar.getInstance(); temp.set(Calendar.YEAR, 1999); EARLIER = temp; } /////////////////////////////////////////////////////////////////////////// // Primary Contract /////////////////////////////////////////////////////////////////////////// /** * Create a new cache element with the given userid. * * Note: The element will start in the invalid state. */ public CacheElement(String userid, String cacheid, String cacheabbrev) { m_userid = userid; m_cacheid = cacheid; m_cacheabbrev = cacheabbrev; this.invalidate(); } /** * Gets the user id associated with this cache element. */ public String getUserid() { return m_userid; } /** * Populates this cache element with the given authenticated state * and named values, then puts it in the valid state. * * Note: Prior to the call, the element must be in the invalid state. * TODO: The predicates parameter is deprecated and should be removed. * For now, callers can avoid a warning by giving it as null. */ public final void populate(Boolean authenticated, Set predicates, Map namedValues, String errorMessage) { String m = m_cacheabbrev + " populate() "; LOG.debug(m + ">"); try { if (predicates != null) { LOG.warn(m + " predicates are deprecated; will be ignored"); } assertInvalid(); if (errorMessage != null) { LOG.error(m + "errorMessage==" + errorMessage); throw new Exception(errorMessage); } else { validate(authenticated, namedValues); // can't set expiration here -- don't have cache reference // can't set pwd here, don't have it } } catch (Throwable t) { LOG.error(m + "invalidating to be sure"); this.invalidate(errorMessage); } finally { LOG.debug(m + "<"); } } /** * If in the valid state and not expired: * If authenticated and given password is instance password, return true. * Else return false. * If invalid or expired: * Re-initialize this element by setting it to invalid state and running * the underlying authN code. * If never authenticated or currently not valid, return m_authenticated * If authenticated, set thekkkk instance password to the given one and return true. * If not authenticated, return false. */ public final synchronized Boolean authenticate(Cache cache, String pwd) { // Original Comment: // Synchronized so evaluation of cache item state will be sequential, // non-interlaced. This protects against overlapping calls resulting in // redundant authenticator calls. String m = m_cacheabbrev + " authenticate() "; LOG.debug(m + ">"); Boolean rc = null; try { LOG.debug(m + "m_valid==" + m_valid); if (m_valid && !CacheElement.isExpired(m_expiration)) { LOG.debug(m + "valid and not expired, so use"); if (!isAuthenticated()) { LOG.debug(m + "auth==" + m_authenticated); rc = m_authenticated; } else { LOG.debug(m + "already authd, request password==" + pwd); if (pwd == null) { LOG.debug(m + "null request password"); rc = Boolean.FALSE; } else if ("".equals(pwd)) { LOG.debug(m + "zero-length request password"); rc = Boolean.FALSE; } else { LOG.debug(m + "stored password==" + m_password); rc = pwd.equals(m_password); } } } else { // expired or invalid LOG.debug(m + "expired or invalid, so try to repopulate"); this.invalidate(); CacheElementPopulator cePop = cache.getCacheElementPopulator(); cePop.populateCacheElement(this, pwd); int duration = 0; String unit = null; m_password = null; if (m_authenticated == null || !m_valid) { duration = cache.getAuthExceptionTimeoutDuration(); unit = cache.getAuthExceptionTimeoutUnit(); LOG.debug(m + "couldn't complete population"); } else { LOG.debug(m + "populate completed"); if (isAuthenticated()) { m_password = pwd; duration = cache.getAuthSuccessTimeoutDuration(); unit = cache.getAuthSuccessTimeoutUnit(); LOG.debug(m + "populate succeeded"); } else { duration = cache.getAuthFailureTimeoutDuration(); unit = cache.getAuthFailureTimeoutUnit(); LOG.debug(m + "populate failed"); } } m_expiration = CacheElement.calcExpiration(duration, unit); rc = m_authenticated; } } catch (Throwable th) { this.invalidate(); rc = m_authenticated; LOG.error(m + "invalidating to be sure"); } finally { audit(); LOG.debug(m + "< " + rc); } return rc; } public final synchronized Map getNamedValues(Cache cache, String pwd) { // Original Comment: // Synchronized so evaluation of cache item state will be sequential, // non-interlaced. This protects against overlapping calls resulting in // redundant (authenticator?) calls. // TODO: refactor method name so that it doesn't look like "getter" String m = m_cacheabbrev + " namedValues "; LOG.debug(m + ">"); Map rc = null; try { LOG.debug(m + "valid==" + m_valid); if (m_valid && !CacheElement.isExpired(m_expiration)) { LOG.debug(m + "valid and not expired, so use"); } else { LOG.debug(m + "expired or invalid, so try to repopulate"); this.invalidate(); CacheElementPopulator cePop = cache.getCacheElementPopulator(); cePop.populateCacheElement(this, pwd); int duration = 0; String unit = null; if (m_namedValues == null || !m_valid) { duration = cache.getAuthExceptionTimeoutDuration(); unit = cache.getAuthExceptionTimeoutUnit(); LOG.debug(m + "couldn't complete population"); } else { LOG.debug(m + "populate completed"); if (m_namedValues == null) { duration = cache.getAuthFailureTimeoutDuration(); unit = cache.getAuthFailureTimeoutUnit(); LOG.debug(m + "populate failed"); } else { m_password = pwd; duration = cache.getAuthSuccessTimeoutDuration(); unit = cache.getAuthSuccessTimeoutUnit(); LOG.debug(m + "populate succeeded"); } } m_expiration = CacheElement.calcExpiration(duration, unit); } } catch (Throwable th) { String msg = m + "invalidating to be sure"; this.invalidate(msg); LOG.error(msg); } finally { audit(); rc = m_namedValues; if (rc == null) { rc = new Hashtable(); } LOG.debug(m + "< " + rc); } return rc; } /////////////////////////////////////////////////////////////////////////// // Logging/Debugging Contract /////////////////////////////////////////////////////////////////////////// public String getInstanceId() { String rc = toString(); int i = rc.indexOf("@"); if (i > 0) { rc = rc.substring(i + 1); } return rc; } public final void audit() { String m = m_cacheabbrev + " audit() "; if (LOG.isDebugEnabled()) { try { Calendar now = Calendar.getInstance(); LOG.debug(m + "> " + m_cacheid + " " + getInstanceId() + " @ " + format(now)); LOG.debug(m + "valid==" + m_valid); LOG.debug(m + "userid==" + getUserid()); LOG.debug(m + "password==" + m_password); LOG.debug(m + "authenticated==" + m_authenticated); LOG.debug(m + "errorMessage==" + m_errorMessage); LOG.debug(m + "expiration==" + format(m_expiration)); LOG.debug(m + compareForExpiration(now, m_expiration)); if (m_namedValues == null) { LOG.debug(m + "(no named attributes"); } else { CacheElement.auditNamedValues(m, m_namedValues); } } finally { LOG.debug(m + "<"); } } } /////////////////////////////////////////////////////////////////////////// // Private Instance Methods /////////////////////////////////////////////////////////////////////////// private boolean isAuthenticated() { if (m_authenticated == null) return false; return m_authenticated.booleanValue(); } private void invalidate() { invalidate(null); } private void invalidate(String errorMessage) { String m = m_cacheabbrev + " invalidate() "; m_valid = false; m_errorMessage = errorMessage; m_authenticated = null; m_namedValues = null; m_expiration = EARLIER; m_password = null; if (m_errorMessage != null) { LOG.debug(m + m_errorMessage); } } private final void assertInvalid() { assert m_authenticated == null; assert m_namedValues == null; assert !m_valid; assert isExpired(m_expiration, false); assert m_password == null; } private static final void checkCalcExpiration(int duration, int unit) throws IllegalArgumentException { if (duration < 0) { throw new IllegalArgumentException("bad duration==" + duration); } switch (unit) { case Calendar.MILLISECOND: case Calendar.SECOND: case Calendar.MINUTE: case Calendar.HOUR: break; default: throw new IllegalArgumentException("bad unit==" + unit); } } private void validate(Boolean authenticated, Map namedValues) { assertInvalid(); m_authenticated = authenticated; m_namedValues = namedValues; m_errorMessage = null; m_valid = true; } /////////////////////////////////////////////////////////////////////////// // Private Class Methods /////////////////////////////////////////////////////////////////////////// private static final void auditNamedValues(String m, Map namedValues) { if (LOG.isDebugEnabled()) { assert namedValues != null; for (Iterator outer = namedValues.keySet().iterator(); outer .hasNext();) { Object name = outer.next(); assert name instanceof String : "not a string, name==" + name; StringBuffer sb = new StringBuffer(m + name + "=="); Object temp = namedValues.get(name); assert temp instanceof String || temp instanceof Set : "neither string nor set, temp==" + temp; if (temp instanceof String) { sb.append(temp.toString()); } else if (temp instanceof Set) { Set values = (Set) temp; sb.append("(" + values.size() + ") {"); String punct = ""; for (Iterator it = values.iterator(); it.hasNext();) { temp = it.next(); if (!(temp instanceof String)) { LOG.error(m + "set member not string, ==" + temp); } else { String value = (String) temp; sb.append(punct + value); punct = ","; } } sb.append("}"); } LOG.debug(sb.toString()); } } } private static final String pad(long i, String pad, boolean padLeft) { String rc = ""; String st = Long.toString(i); if (st.length() == pad.length()) { rc = st; } else if (st.length() > pad.length()) { rc = st.substring(0, pad.length()); } else { String padNeeded = pad.substring(0, pad.length() - st.length()); if (padLeft) { rc = padNeeded + st; } else { rc = st + padNeeded; } } return rc; } private static final String pad(long i, String pad) { return CacheElement.pad(i, pad, true); } private static final String format(long day, long hour, long minute, long second, long millisecond, String dayPad) { StringBuffer sb = new StringBuffer(); if (dayPad != null) { sb.append(CacheElement.pad(day, "00")); sb.append(" "); } else { sb.append(Long.toString(day)); sb.append(" days "); } sb.append(CacheElement.pad(hour, "00")); sb.append(":"); sb.append(CacheElement.pad(minute, "00")); sb.append(":"); sb.append(CacheElement.pad(second, "00")); sb.append("."); sb.append(CacheElement.pad(millisecond, "000")); return sb.toString(); } private static final String format(long year, long month, long day, long hour, long minute, long second, long millisecond) { StringBuffer sb = new StringBuffer(); sb.append(CacheElement.pad(year, "0000")); sb.append("-"); sb.append(CacheElement.pad(month, "00")); sb.append("-"); sb.append(format(day, hour, minute, second, millisecond, "00")); return sb.toString(); } private static final String format(Calendar time) { return format(time.get(Calendar.YEAR), time.get(Calendar.MONTH) + 1, time.get(Calendar.DATE), time.get(Calendar.HOUR_OF_DAY), time.get(Calendar.MINUTE), time.get(Calendar.SECOND), time.get(Calendar.MILLISECOND)); } private static final String difference(Calendar earlier, Calendar later) { long milliseconds = later.getTimeInMillis() - earlier.getTimeInMillis(); long days = milliseconds / MILLIS_IN_DAY; milliseconds = milliseconds % MILLIS_IN_DAY; long hours = milliseconds / MILLIS_IN_HOUR; milliseconds = milliseconds % MILLIS_IN_HOUR; long minutes = milliseconds / MILLIS_IN_MINUTE; milliseconds = milliseconds % MILLIS_IN_MINUTE; long seconds = milliseconds / MILLIS_IN_SECOND; milliseconds = milliseconds % MILLIS_IN_SECOND; String rc = format(days, hours, minutes, seconds, milliseconds, null); return rc; } private static final String compareForExpiration(Calendar first, Calendar second) { String rc = null; if (first.before(second)) { rc = "expires in " + difference(first, second); } else { rc = "expired " + difference(second, first) + " ago"; } return rc; } private static final Calendar calcExpiration(int duration, int unit) { String m = "- calcExpiration(int,int) "; LOG.debug(m + ">"); Calendar now = Calendar.getInstance(); Calendar rc = Calendar.getInstance(); try { CacheElement.checkCalcExpiration(duration, unit); if (duration > 0) { rc.add(unit, duration); LOG.debug(m + CacheElement.compareForExpiration(now, rc)); } else { LOG.debug(m + "timeout set to now (effectively, no caching)"); } } finally { if (LOG.isDebugEnabled()) { LOG.debug(m + "< " + format(rc)); } } return rc; } private static final int calcCalendarUnit(String unit) { String m = "- calcCalendarUnit() "; int rc = Calendar.SECOND; if (!unit.endsWith("s")) { unit += "s"; } if ("milliseconds".equalsIgnoreCase(unit)) { rc = Calendar.MILLISECOND; } else if ("seconds".equalsIgnoreCase(unit)) { rc = Calendar.SECOND; } else if ("minutes".equalsIgnoreCase(unit)) { rc = Calendar.MINUTE; } else if ("hours".equalsIgnoreCase(unit)) { rc = Calendar.HOUR; } else { String msg = "illegal Calendar unit: " + unit; LOG.error(m + "(" + msg + ")"); throw new IllegalArgumentException(msg); } return rc; } private static final Calendar calcExpiration(int duration, String unit) { String m = "- calcExpiration(int,String) "; Calendar rc = Calendar.getInstance(); int calendarUnit = Calendar.SECOND; try { calendarUnit = calcCalendarUnit(unit); } catch (Throwable t) { duration = 0; LOG.error(m + "using duration==" + duration); LOG.error(m + "using calendarUnit==" + calendarUnit); } finally { rc = CacheElement.calcExpiration(duration, calendarUnit); } return rc; } private static final boolean isExpired(Calendar now, Calendar expiration, boolean verbose) { String m = "- isExpired() "; if (verbose) { LOG.debug(m + ">"); } boolean rc = CacheElement.s_expired_default; try { if (now == null) { String msg = "illegal parm now==" + now; LOG.error(m + "(" + msg + ")"); throw new IllegalArgumentException(msg); } if (expiration == null) { String msg = "illegal parm expiration==" + expiration; LOG.error(m + "(" + msg + ")"); throw new IllegalArgumentException(msg); } if (verbose) { LOG.debug(m + "now==" + format(now)); LOG.debug(m + "exp==" + format(expiration)); } rc = !now.before(expiration); } catch (Throwable th) { LOG.error(m + "failed comparison"); rc = CacheElement.s_expired_default; } finally { if (verbose) { LOG.debug(m + compareForExpiration(now, expiration)); LOG.debug(m + "< " + rc); } } return rc; } private static final boolean isExpired(Calendar expiration, boolean verbose) { String m = "- isExpired() "; boolean rc = CacheElement.s_expired_default; try { if (expiration == null) { String msg = "illegal parm expiration==" + expiration; LOG.error(m + "(" + msg + ")"); throw new IllegalArgumentException(msg); } Calendar now = Calendar.getInstance(); rc = CacheElement.isExpired(now, expiration, verbose); } catch (Throwable th) { LOG.error(m + "failed comparison"); rc = CacheElement.s_expired_default; } return rc; } private static final boolean isExpired(Calendar now) { return isExpired(now, true); } }