001/** 
002 * Copyright (C) 2009 "Darwin V. Felix" <darwinfelix@users.sourceforge.net>
003 * 
004 * This library is free software; you can redistribute it and/or
005 * modify it under the terms of the GNU Lesser General Public
006 * License as published by the Free Software Foundation; either
007 * version 2.1 of the License, or (at your option) any later version.
008 * 
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
012 * Lesser General Public License for more details.
013 * 
014 * You should have received a copy of the GNU Lesser General Public
015 * License along with this library; if not, write to the Free Software
016 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017 */
018
019package net.sourceforge.spnego;
020
021import java.io.FileInputStream;
022import java.io.IOException;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Hashtable;
028import java.util.List;
029import java.util.Map;
030import java.util.Properties;
031import java.util.Set;
032import java.util.concurrent.locks.Lock;
033import java.util.concurrent.locks.ReentrantReadWriteLock;
034import java.util.logging.Logger;
035
036import javax.naming.Context;
037import javax.naming.NamingEnumeration;
038import javax.naming.NamingException;
039import javax.naming.directory.Attribute;
040import javax.naming.directory.Attributes;
041import javax.naming.directory.SearchControls;
042import javax.naming.directory.SearchResult;
043import javax.naming.ldap.InitialLdapContext;
044import javax.naming.ldap.LdapContext;
045
046/**
047 * The <code>LdapAccessControl</code> class is a reference implementation 
048 * of the {@link UserAccessControl} interface. This class only performs 
049 * user authorization (authZ) and not user authentication (authN). This 
050 * class implements an authZ model in a similar style as the 
051 * <a href="https://en.wikipedia.org/wiki/Role-based_access_control">Role 
052 * Based Access Control</a> (RBAC) model and the  
053 * <a href="https://en.wikipedia.org/wiki/Attribute-Based_Access_Control">Attribute 
054 * Based Access Control</a> (ABAC) model. However, this pedagogical implementation 
055 * is much simpler and limited than the two formal models.
056 * 
057 * <p>
058 * Usage examples and semantics can be found in the javadoc of the interface 
059 * that this class implements ({@link UserAccessControl}).
060 * </p>
061 * 
062 * <p>
063 * The <code>LdapAccessControl</code> implementation makes calls to an 
064 * LDAP server (e.g. Microsoft's Active Directory (AD) server) when performing 
065 * a lookup to determine if a user has one or more attributes defined. The 
066 * LDAP queries are based on LDAP search/fiter criteria(s) defined at the time  
067 * an instance of this class is placed into service. An instance of this class 
068 * is considered to be in service after invoking the <code>init</code> method. 
069 * </p>
070 * 
071 * <p>
072 * In the SPNEGO Library, the <code>SpnegoHttpFilter</code> class is the mechanism 
073 * that performs user authentication (authN) whilst the <code>LdapAccessControl</code> 
074 * class is the default mechanism that performs user authorization (authZ). This 
075 * default can be replaced by any class that implement the <code>UserAccessControl</code> 
076 * interface. To change the default, specify the new class in the SPNEGO Library's 
077 * filter definition section of the web.xml file.
078 * </p>
079 * 
080 * <p>
081 * Authorization (authZ) is an optional feature of the SPNEGO Library. The SPNEGO 
082 * Library provides an interface, {@link SpnegoAccessControl}, to applications 
083 * that need authZ capability. Applications can check a user's authZ by calling 
084 * methods defined in the <code>SpnegoAccessControl</code> interface.
085 * </p>
086 * 
087 * <p>
088 * The <code>LdapAccessControl</code> class is configured within the same web.xml 
089 * filter section as the <code>SpnegoHttpFilter</code> class. The configuration is 
090 * specified by adding additional filter parameters to the <code>SpnegoHttpFilter</code> 
091 * filter definition.
092 * </p>
093 * 
094 * <p>
095 * <b>Example web.xml Configuration:</b>
096 * <pre>
097 * &lt;filter&gt;
098 *     &lt;filter-name&gt;SpnegoHttpFilter&lt;/filter-name&gt;
099 *     &lt;filter-class&gt;net.sourceforge.spnego.SpnegoHttpFilter&lt;/filter-class&gt;
100 *     
101 *     &lt;!-- spnego http filter params (authN) --&gt;
102 *     ... existing authN params here just as before ...
103 *     
104 *     &lt;!-- spnego http filter params (authZ) --&gt;     
105 *     &lt;init-param&gt;
106 *         &lt;param-name&gt;spnego.authz.class&lt;/param-name&gt;
107 *         &lt;param-value&gt;net.sourceforge.spnego.LdapAccessControl&lt;/param-value&gt;
108 *     &lt;/init-param&gt;
109 *     &lt;init-param&gt;
110 *         &lt;param-name&gt;spnego.authz.ldap.url&lt;/param-name&gt;
111 *         &lt;param-value&gt;ldap://athena.local:389&lt;/param-value&gt;
112 *     &lt;/init-param&gt;
113 *     
114 *     &lt;!-- an example user-defined resource label --&gt;
115 *     &lt;init-param&gt;
116 *         &lt;param-name&gt;spnego.authz.resource.name.1&lt;/param-name&gt;
117 *         &lt;param-value&gt;admin-buttons&lt;/param-value&gt;
118 *     &lt;/init-param&gt;
119 *     &lt;init-param&gt;
120 *         &lt;param-name&gt;spnego.authz.resource.access.1&lt;/param-name&gt;
121 *         &lt;param-value&gt;Biz. Analyst,Los Angeles,IT Group&lt;/param-value&gt;
122 *     &lt;/init-param&gt;
123 *     &lt;init-param&gt;
124 *         &lt;param-name&gt;spnego.authz.resource.type.1&lt;/param-name&gt;
125 *         &lt;param-value&gt;has&lt;/param-value&gt;
126 *     &lt;/init-param&gt;
127 *     
128 *     &lt;!-- CDATA required since specifying filter(s) in web.xml (vs. a policy file) --&gt;
129 *     &lt;!-- also notice the %1$s and the %2$s tokens (always required) --&gt;
130 *     &lt;init-param&gt;
131 *         &lt;param-name&gt;spnego.authz.ldap.filter.1&lt;/param-name&gt;
132 *          &lt;param-value&gt;&lt;![CDATA[(&amp;(sAMAccountName=%1$s)(memberOf:1.2.840.113556.1.4.1941:=CN=%2$s,OU=Groups,OU=Los Angeles,DC=athena,DC=local))]]&gt;&lt;/param-value&gt;
133 *     &lt;/init-param&gt;
134 *     &lt;init-param&gt;
135 *         &lt;param-name&gt;spnego.authz.ldap.filter.2&lt;/param-name&gt;
136 *         &lt;param-value&gt;&lt;![CDATA[(&amp;(sAMAccountType=805306368)(sAMAccountName=%1$s)(&amp;(sAMAccountType=805306368)(department=%2$s)))]]&gt;&lt;/param-value&gt;
137 *     &lt;/init-param&gt;
138 * &lt;/filter&gt;
139 * </pre>
140 * </p>
141 * 
142 * <p>
143 * As an alternative option, the <code>spnego.authz.ldap.filter.[i]</code> parameters and 
144 * the <code>spnego.authz.resource.[name|access|type].[i]</code> parameters may be specified 
145 * in a policy file.
146 * </p>
147 * 
148 * <p>
149 * <b>Example Policy File Configuration:</b>
150 * <pre>
151 * &lt;filter&gt;
152 *     &lt;filter-name&gt;SpnegoHttpFilter&lt;/filter-name&gt;
153 *     &lt;filter-class&gt;net.sourceforge.spnego.SpnegoHttpFilter&lt;/filter-class&gt;
154 *     
155 *     &lt;!-- spnego http filter params (authN) --&gt;
156 *     ... existing authN params here just as before ...
157 *     
158 *     &lt;!-- spnego http filter params (authZ) --&gt;     
159 *     &lt;init-param&gt;
160 *         &lt;param-name&gt;spnego.authz.class&lt;/param-name&gt;
161 *         &lt;param-value&gt;net.sourceforge.spnego.LdapAccessControl&lt;/param-value&gt;
162 *     &lt;/init-param&gt;
163 *     &lt;init-param&gt;
164 *         &lt;param-name&gt;spnego.authz.ldap.url&lt;/param-name&gt;
165 *         &lt;param-value&gt;ldap://athena.local:389&lt;/param-value&gt;
166 *     &lt;/init-param&gt;
167 *     &lt;init-param&gt;
168 *         &lt;param-name&gt;spnego.authz.policy.file&lt;/param-name&gt;
169 *         &lt;param-value&gt;C:/Apache Software Foundation/Tomcat 7.0/conf/spnego.policy&lt;/param-value&gt;
170 *     &lt;/init-param&gt;
171 * &lt;/filter&gt;
172 * </pre>
173 * 
174 * Policy file contents:
175 * <pre>
176 * # an example user-defined resource label
177 * spnego.authz.resource.name.1=admin-buttons
178 * spnego.authz.resource.access.1=Biz. Analyst,Los Angeles,IT Group
179 * spnego.authz.resource.type.1=has
180 * 
181 * # do NOT use CDATA like in the web.xml file
182 * # the %1$s and the %2$s tokens are always required
183 * spnego.authz.ldap.filter.1=(&amp;(sAMAccountName=%1$s)(memberOf:1.2.840.113556.1.4.1941:=CN=%2$s,OU=Groups,OU=Los Angeles,DC=athena,DC=local))
184 * spnego.authz.ldap.filter.2=(&amp;(sAMAccountType=805306368)(sAMAccountName=%1$s)(&amp;(sAMAccountType=805306368)(department=%2$s)))
185 * </pre>
186 * </p>
187 * 
188 * <p>
189 * For more information on how a web application/service can leverage access controls  
190 * or to view some usage examples, please read the javadoc of the {@link SpnegoAccessControl} 
191 * interface and the javadoc of the {@link UserAccessControl} interface.
192 * </p>
193 * 
194 * <p>
195 * Also, take a look at the <a href="http://spnego.sourceforge.net/reference_docs.html" 
196 * target="_blank">reference docs</a> for a complete list of configuration parameters.
197 * </p>
198 * 
199 * <p>
200 * Finally, to see a working example and instructions, take a look at the 
201 * <a href="http://spnego.sourceforge.net/user_access_control.html" 
202 * target="_blank">authZ for standalone apps</a> example and the 
203 * <a href="http://spnego.sourceforge.net/enable_authZ_ldap.html" 
204 * target="_blank">enable authZ with LDAP</a> guide. 
205 * </p>
206 * 
207 * 
208 * @author Darwin V. Felix
209 *
210 */
211public class LdapAccessControl implements UserAccessControl {
212    
213    private static final Logger LOGGER = 
214            Logger.getLogger(LdapAccessControl.class.getName());
215    
216    private static final String POLICY_FILE = "spnego.authz.policy.file";
217    
218    private static final String SERVER_REALM = "spnego.server.realm";
219    
220    private static final String LDAP_FACTORY = "spnego.authz.ldap.factory";
221    
222    private static final String LDAP_AUTHN = "spnego.authz.ldap.authn";
223    
224    private static final String LDAP_POOL = "spnego.authz.ldap.pool";
225    
226    private static final String LDAP_DEECE = "spnego.authz.ldap.deecee";
227
228    private static final String LDAP_URL = "spnego.authz.ldap.url";
229
230    private static final String LDAP_USERNAME = "spnego.authz.ldap.username";
231    
232    private static final String KRB5_USERNAME = "spnego.preauth.username";
233    
234    private static final String LDAP_PASSWORD = "spnego.authz.ldap.password";
235    
236    private static final String KRB5_PASSWORD = "spnego.preauth.password";
237    
238    private static final String TTL = "spnego.authz.ttl";
239    
240    private static final String UNIQUE = "spnego.authz.unique";
241    
242    private static final String PREFIX_FILTER = "spnego.authz.ldap.filter.";
243    
244    private static final String PREFIX_NAME = "spnego.authz.resource.name.";
245    
246    private static final String PREFIX_TYPE = "spnego.authz.resource.type.";
247    
248    private static final String PREFIX_ACCESS = "spnego.authz.resource.access.";
249    
250    private static final String HAS = "has";
251    
252    private static final String ANY = "any";
253    
254    /** case-sensitive. e.g. values mail,department,name,memberOf, etc. */
255    private static final String USER_INFO = "spnego.authz.user.info";
256    
257    /** e.g. (&(sAMAccountType=805306368)(sAMAccountName=%1$s)) */
258    private static final String USER_INFO_FILTER = "spnego.authz.ldap.user.filter";
259    
260    /** default is 20 minutes. */
261    private static final long DEFAULT_TTL = 20 * 60 * 1000;
262    
263    /** maximum number of ldap filters is 200 */
264    private static final int MAX_NUM_FILTERS = 200;
265    
266    /** read lock for reading instance variables and write lock for ldap search. */
267    private final transient ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
268    private final transient Lock readLock = readWriteLock.readLock();
269    private final transient Lock writeLock = readWriteLock.writeLock();
270    
271    /** cache LDAP results to minimize trips to ldap server. */
272    private final transient Map<String, Long> matchedList = new HashMap<String, Long>();
273    private final transient Map<String, Long> unMatchedList = new HashMap<String, Long>();
274    private final transient Map<String, UserInfo> userInfoList = new HashMap<String, UserInfo>();
275    
276    private transient Hashtable<String, String> environment;
277    private transient SearchControls srchCntrls;
278    
279    /** DC= base portionS of the ldap search filter. */
280    private transient String deecee = "";
281    
282    /** ldap search filter(s). */
283    private transient Set<String> policy = new HashSet<String>();
284    
285    /** determines how long to keep in cache. */
286    private transient long expiration = DEFAULT_TTL;
287    
288    /** determines if an exception should be thrown if it finds a duplicate. */
289    private transient boolean uniqueOnly = true;
290    
291    /** access resources by using a user-defined label. */
292    private transient Map<String, Map<String, String[]>> resources = 
293            new HashMap<String, Map<String, String[]>>();
294    
295    private transient List<String> userInfoLabels = new ArrayList<String>();
296    
297    private transient String userInfoFilter;
298    
299    /**
300     * Default constructor.
301     */
302    public LdapAccessControl() {
303        // default constructor
304    }
305    
306    @Override
307    public void destroy() {
308        LOGGER.info("destroy()...");
309        this.writeLock.lock();
310        try {
311            this.matchedList.clear();
312            this.unMatchedList.clear();
313            this.environment.clear();
314            this.environment = null;
315            this.srchCntrls = null;
316            this.deecee = "";
317            this.policy.clear();
318            this.expiration = DEFAULT_TTL;
319            this.resources.clear();
320            this.userInfoLabels.clear();
321            this.userInfoFilter = null;
322        } finally {
323            this.writeLock.unlock();
324        }
325    }
326    
327    /*
328     * (non-Javadoc)
329     * @see net.sourceforge.spnego.UserAccessControl#anyRole(java.lang.String, java.lang.String[])
330     */
331    @Override
332    public boolean anyRole(final String username, final String... attributes) {
333        for (String role : attributes) {
334            if (hasRole(username, role)) {
335                return true;
336            }
337        }
338        return false;
339    }
340    
341    /*
342     * (non-Javadoc)
343     * @see net.sourceforge.spnego.UserAccessControl#hasRole(java.lang.String, java.lang.String)
344     */
345    @Override
346    public boolean hasRole(final String username, final String attribute) {
347        final String key = username + "_attr_" + attribute;
348        final long now = System.currentTimeMillis();
349
350        try {
351            if (!matchedExpired(key, now)) {
352                return true;
353            }
354            
355            if (!unMatchedExpired(key, now)) {
356                return false;
357            }
358
359            // query AD to update both MapS and expiration time
360            LOGGER.fine("username: " + username + "; role: " + attribute);
361
362            this.writeLock.lock();
363            try {
364                // remove from cache if exists
365                this.matchedList.remove(key);
366                this.unMatchedList.remove(key);
367                
368                int count = 0;
369                final LdapContext context = new InitialLdapContext(environment, null);
370                for (String filter : this.policy) {
371                    // perform AD lookup add to cache 
372                    final NamingEnumeration<SearchResult> results = 
373                            context.search(this.deecee
374                                    , String.format(filter, username, attribute)
375                                    , this.srchCntrls);
376                    
377                    final boolean found = results.hasMoreElements();
378                    results.close();
379                    
380                    // add to cache
381                    if (found) {
382                        count++;
383                        //LOGGER.info("add attribute to matchedList: " + attribute);
384                        this.matchedList.put(key, System.currentTimeMillis());
385                        if (!this.uniqueOnly) {
386                            break;                            
387                        }
388                    }
389                    
390                    // check if we have a duplicate attribute
391                    if (count > 1 && this.uniqueOnly) {
392                        this.matchedList.remove(key);
393                        throw new IllegalArgumentException("Uniqueness property violated. "
394                                + "Found duplicate role/attribute:" + attribute 
395                                + ". This MAY be caused by an improper policy definition" 
396                                + "; filter=" + filter 
397                                + "; policy=" + this.policy);                            
398                    }
399                }
400                context.close();
401                
402                if (0 == count) {
403                    //LOGGER.info("add attribute to unMatchedList: " + attribute);
404                    this.unMatchedList.put(key, System.currentTimeMillis());                    
405                } else {
406                    cacheUserInfo(username);
407                }
408                
409            } finally {
410                this.writeLock.unlock();
411            }
412        } catch (NamingException lex) {
413            LOGGER.severe(lex.getMessage());
414            throw new RuntimeException(lex);
415        }
416        
417        return hasRole(username, attribute);
418    }
419    
420    /*
421     * (non-Javadoc)
422     * @see net.sourceforge.spnego.UserAccessControl#hasRole(java.lang.String, java.lang.String, java.lang.String[])
423     */
424    @Override
425    public boolean hasRole(final String username, final String attributeX, final String... attributeYs) {
426        
427        // assert
428        if (null == attributeYs || 0 == attributeYs.length) {
429            final String errorMsg = "Must provide at least two parameters";
430            LOGGER.severe(errorMsg);
431            throw new IllegalArgumentException(errorMsg);
432        }
433        
434        boolean found = false;
435        final boolean featX = hasRole(username, attributeX);
436        
437        for (String featY : attributeYs) {
438            found = featX && hasRole(username, featY);
439            if (found) {
440                break;
441            }
442        }
443        
444        return found;
445    }
446    
447    /*
448     * (non-Javadoc)
449     * @see net.sourceforge.spnego.UserAccessControl#anyAccess(java.lang.String, java.lang.String[])
450     */
451    @Override
452    public boolean anyAccess(final String username, final String... resources) {
453        for (String resource : resources) {
454            if (hasAccess(username, resource)) {
455                return true;
456            }
457        }
458        return false;
459    }
460    
461    /*
462     * (non-Javadoc)
463     * @see net.sourceforge.spnego.UserAccessControl#hasAccess(java.lang.String, java.lang.String)
464     */
465    @Override
466    public boolean hasAccess(final String username, final String resource) {
467        final String key = username + "_res_" + resource;
468        final long now = System.currentTimeMillis();
469        
470        if (!matchedExpired(key, now)) {
471            return true;
472        }
473        
474        if (!unMatchedExpired(key, now)) {
475            return false;
476        }
477
478        // query AD to update both MapS and expiration time
479        LOGGER.fine("username: " + username + "; resource: " + resource);
480        
481        boolean matched = false;
482        boolean containsHas = false;
483        boolean containsAny = false;
484        String[] attributes = new String[] {};
485        
486        this.readLock.lock();
487        try {
488            // assert
489            if (!this.resources.containsKey(resource)) {
490                throw new IllegalArgumentException(
491                        "Policy not found for user-defined Resource labeled: " + resource);
492            }
493            
494            containsHas = this.resources.get(resource).containsKey(HAS);
495            containsAny = this.resources.get(resource).containsKey(ANY);
496            if (containsHas) {
497                attributes = this.resources.get(resource).get(HAS);
498            } else if (containsAny) {
499                attributes = this.resources.get(resource).get(ANY);
500            }
501        } finally {
502            this.readLock.unlock();
503        }
504        
505        if (containsHas) {
506            if (attributes.length > 1) {
507                matched = this.hasRole(username, attributes[0]
508                        , Arrays.copyOfRange(attributes, 1, attributes.length));                
509            } else if (attributes.length == 1) {
510                matched = this.hasRole(username, attributes[0]);
511            } else {
512                throw new IllegalStateException("No attribute(s) defined for resource: " + resource);
513            }
514        } else if (containsAny) {
515            matched = this.anyRole(username, attributes);
516        } else {
517            throw new UnsupportedOperationException("Allowed resource.type(s): [any|has]");
518        }
519        
520        this.writeLock.lock();
521        try {
522            if (matched) {
523                //LOGGER.info("add resource to matchedList: " + resource);
524                this.matchedList.put(key, now);
525            } else {
526                //LOGGER.info("add resource to unMatchedList: " + resource);
527                this.unMatchedList.put(key, now);
528            }
529        } finally {
530            this.writeLock.unlock();
531        }
532        
533        return matched;
534    }
535    
536    /*
537     * (non-Javadoc)
538     * @see net.sourceforge.spnego.UserAccessControl#hasAccess(java.lang.String, java.lang.String, java.lang.String[])
539     */
540    @Override
541    public boolean hasAccess(final String username, final String resourceX, final String... resourceYs) {
542        
543        // assert
544        if (null == resourceYs || 0 == resourceYs.length) {
545            final String errorMsg = "Must provide at least two parameters";
546            LOGGER.severe(errorMsg);
547            throw new IllegalArgumentException(errorMsg);
548        }
549        
550        boolean found = false;
551        final boolean resX = hasAccess(username, resourceX);
552        
553        for (String resY : resourceYs) {
554            found = resX && hasAccess(username, resY);
555            if (found) {
556                break;
557            }
558        }
559        
560        return found;
561    }
562    
563    /**
564     * Returns a user info object if specified in  web.xml or the spnego.policy file.
565     * 
566     * <p>Case-sensitive</br>
567     * 
568     * <p>
569     * <b>web.xml Example:</b></br />
570     * <pre>
571     *     ...
572     *     &lt;init-param&gt;
573     *         &lt;param-name&gt;spnego.authz.user.info&lt;/param-name&gt;
574     *         &lt;param-value&gt;mail,department,memberOf,displayName&lt;/param-value&gt;
575     *     &lt;/init-param&gt;
576     *     &lt;init-param&gt;
577     *         &lt;param-name&gt;spnego.authz.ldap.user.filter&lt;/param-name&gt;
578     *         &lt;param-value&gt;&lt;![CDATA[(&amp;(sAMAccountType=805306368)(sAMAccountName=%1$s))]]&gt;&lt;/param-value&gt;
579     *     &lt;/init-param&gt;
580     *     ...
581     * </pre>
582     * </p>
583     * 
584     * <p>
585     * <b>spnego.policy File Example:</b><br />
586     * <pre>
587     * ...
588     * # case-sensitive
589     * spnego.authz.user.info=mail,department,memberOf,displayName
590     * spnego.authz.ldap.user.filter=(&amp;(sAMAccountType=805306368)(sAMAccountName=%1$s))
591     * ...
592     * </pre>
593     * </p>
594     * </p>
595     * 
596     * @param username e.g. dfelix
597     * @return UserInfo object with the specified ldap attributes
598     */
599    @Override
600    public UserInfo getUserInfo(final String username) {
601        final long now = System.currentTimeMillis();
602        final boolean expired = matchedExpired(username, now);
603
604        this.readLock.lock();
605        try {
606            if (!expired) {
607                return this.userInfoList.get(username);
608            }
609        } finally {
610            this.readLock.unlock();
611        }
612        
613        this.writeLock.lock();
614        try {
615            return cacheUserInfo(username);
616        } catch (NamingException nex) {
617            final String errorMessage = "Could not get user info for: " + username;
618            LOGGER.warning(errorMessage);
619            throw new IllegalStateException(errorMessage, nex);
620        } finally {
621            this.writeLock.unlock();
622        }
623    }
624    
625    @Override
626    public void init(final Properties props) {
627        LOGGER.info("init()...");
628        
629        this.readLock.lock();
630        try {
631            if (this.environment != null) {
632                // must call the destroy method before re-initializing
633                throw new IllegalStateException("LdapAccessControl already initialized");
634            }            
635        } finally {
636            this.readLock.unlock();
637        }
638        
639        final String policyFile = props.getProperty(POLICY_FILE, "");
640        final Properties policies = new Properties();
641        if (!policyFile.isEmpty()) {
642            try {
643                LOGGER.info("policy file: " + policyFile);
644                final FileInputStream fis =new FileInputStream(policyFile);
645                try {
646                    policies.load(fis);
647                } finally {
648                    fis.close();
649                }
650            } catch (IOException e) {
651                throw new IllegalArgumentException(
652                        "Policy File NOT Found: " + policyFile, e);
653            }            
654        }
655        
656        // use defaults if not specified
657        final Hashtable<String, String> env = new Hashtable<String, String>();
658        env.put(Context.INITIAL_CONTEXT_FACTORY
659                , policies.getProperty(LDAP_FACTORY
660                        , props.getProperty(LDAP_FACTORY
661                                , "com.sun.jndi.ldap.LdapCtxFactory")));
662        env.put(Context.SECURITY_AUTHENTICATION
663                , policies.getProperty(LDAP_AUTHN
664                        , props.getProperty(LDAP_AUTHN
665                                , "Simple")));
666        env.put("com.sun.jndi.ldap.connect.pool"
667                , policies.getProperty(LDAP_POOL
668                        , props.getProperty(LDAP_POOL
669                                , "true")));
670        
671        // if deecee was not provided, calculate using server's realm
672        String dc = policies.getProperty(LDAP_DEECE
673                , props.getProperty(LDAP_DEECE, ""));
674        if (dc.isEmpty()) {
675            final String tmp = props.getProperty(SERVER_REALM
676                    , policies.getProperty(SERVER_REALM, ""));
677            if (tmp.trim().isEmpty()) {
678                throw new IllegalArgumentException("MUST provide the serve's deecee. "
679                        + " specify a value for the " + LDAP_DEECE + " property.");
680            }
681            dc = "DC=" + tmp.replaceAll("\\.", ",DC=");
682        }
683        LOGGER.info(dc);
684        
685        // assert that an ldap url was provided
686        if (policies.getProperty(LDAP_URL, props.getProperty(LDAP_URL, "")).isEmpty()) {
687            final String errorMessage = "Must provide a value for the spnego.authz.ldap.url parameter";
688            LOGGER.severe(errorMessage);
689            throw new IllegalStateException(errorMessage);
690        } else {
691            env.put(Context.PROVIDER_URL
692                    , policies.getProperty(LDAP_URL, props.getProperty(LDAP_URL)));
693            LOGGER.info("ldap provider url: " + env.get(Context.PROVIDER_URL));            
694        }
695
696        // if username/password not provided, default to krb5 username/password
697        // if nothing is specified because a keytab file was specified... error.
698        if (policies.getProperty(LDAP_USERNAME, props.getProperty(LDAP_USERNAME
699                , props.getProperty(KRB5_USERNAME, policies.getProperty(KRB5_USERNAME, "")))).isEmpty()) {
700            final String errorMessage = "Must provide a username to use for connecting to the LDAP server";
701            LOGGER.severe(errorMessage);
702            throw new IllegalArgumentException(errorMessage);
703        }
704        if (policies.getProperty(LDAP_PASSWORD, props.getProperty(LDAP_PASSWORD
705            , props.getProperty(KRB5_PASSWORD, policies.getProperty(KRB5_PASSWORD, "")))).isEmpty()) {
706            final String errorMessage = "Must provide a password to use for connecting to the LDAP server";
707            LOGGER.severe(errorMessage);
708            throw new IllegalArgumentException(errorMessage);
709        }
710        
711        env.put(Context.SECURITY_PRINCIPAL
712                , policies.getProperty(LDAP_USERNAME, props.getProperty(LDAP_USERNAME
713                        , props.getProperty(KRB5_USERNAME, policies.getProperty(KRB5_USERNAME)))));
714        env.put(Context.SECURITY_CREDENTIALS
715                , policies.getProperty(LDAP_PASSWORD, props.getProperty(LDAP_PASSWORD
716                        , props.getProperty(KRB5_PASSWORD, policies.getProperty(KRB5_PASSWORD)))));
717        LOGGER.info("ldap security principal: " + env.get(Context.SECURITY_PRINCIPAL));
718        
719        // specifiy how many minutes the cache is good for
720        // used to control when to re-query/perform another ldap search
721        long ttl = DEFAULT_TTL;
722        ttl = Long.parseLong(policies.getProperty(TTL, props.getProperty(TTL, "-1")));
723        LOGGER.info("spnego.authz.ttl: " + ttl);
724        
725        // determine if we're allowed to violate the uniqueness property
726        this.uniqueOnly = Boolean.parseBoolean(policies.getProperty(UNIQUE
727                , props.getProperty(UNIQUE, "true")));
728        
729        LOGGER.info("uniqueness property enabled: " + uniqueOnly);
730        
731        this.writeLock.lock();
732        try {
733            this.deecee = dc;
734            
735            // create policy statements
736            loadPolicies(policies, props);
737            
738            // optional labels for resources
739            loadResourceNames(policies, props);
740            
741            if (ttl < 1) {
742                this.expiration = DEFAULT_TTL;
743            } else {
744                this.expiration = ttl * 60 * 1000;
745            }
746            LOGGER.info("cache expiration in millis: " + this.expiration);
747            
748            this.srchCntrls = new SearchControls();
749            this.srchCntrls.setSearchScope(SearchControls.SUBTREE_SCOPE);
750            this.environment = env;
751            
752            final String[] labels = policies.getProperty(USER_INFO
753                    , props.getProperty(USER_INFO, "")).split(",");
754            
755            LOGGER.info("UserInfo label count: " + labels.length);
756            for (String label : labels) {
757                LOGGER.info(label);
758                this.userInfoLabels.add(label.trim());
759            }
760            
761            this.userInfoFilter = policies.getProperty(USER_INFO_FILTER
762                    , props.getProperty(USER_INFO_FILTER, ""));
763            
764            LOGGER.info("UserInfo filter: " + this.userInfoFilter);
765
766        } finally {
767            this.writeLock.unlock();
768        }
769    }
770    
771    private boolean matchedExpired(final String key, final long now) {
772        final boolean matched = this.matchedList.containsKey(key);
773        boolean matchExpired = true;
774
775        this.readLock.lock();
776        try {
777            // if has role, check if not expired
778            if (matched) {
779                matchExpired = now - this.matchedList.get(key) > expiration;
780            }
781            return !(matched && !matchExpired);
782        } finally {
783            this.readLock.unlock();
784        }
785    }
786    
787    private boolean unMatchedExpired(final String key, final long now) {
788        final boolean unMatched = this.unMatchedList.containsKey(key);
789        boolean unMatchedExpired = true;
790
791        this.readLock.lock();
792        try {
793            // check if we know it's missing and we've checked recently
794            if (unMatched) {
795                unMatchedExpired = now - this.unMatchedList.get(key) > expiration;
796            }
797            return !(unMatched && !unMatchedExpired);
798        } finally {
799            this.readLock.unlock();
800        }
801    }
802    
803    // pre-condition is that caller has write lock
804    private void loadPolicies(final Properties props, final Properties policies) {
805        for (int i=0; i<=MAX_NUM_FILTERS; i++) {
806            final int idx = i+1;
807            final String filter = policies.getProperty(PREFIX_FILTER + idx
808                    , props.getProperty(PREFIX_FILTER + idx, "")).trim();
809            if (MAX_NUM_FILTERS == i) {
810                final String errorMessage = "Over the max number of filters allowed: " + i;
811                LOGGER.severe(errorMessage);
812                throw new IllegalArgumentException(errorMessage);
813            } else if (filter.isEmpty()) {
814                break;
815            }
816            this.policy.add(filter);
817        }
818        
819        // need minimum of one policy to execute
820        if (0 == this.policy.size()) {
821            final String errorMessage = "Must specify at least one spnego.authz.ldap.filter.1";
822            LOGGER.severe(errorMessage);
823            throw new IllegalStateException(errorMessage);
824        } 
825    }
826    
827    // pre-condition is that caller has write lock
828    private void loadResourceNames(final Properties props, final Properties policies) {
829        this.resources = new HashMap<String, Map<String, String[]>>();
830        for (int i=0; i<=MAX_NUM_FILTERS; i++) {
831            final int idx = i+1;
832            final Map<String, String[]> access = new HashMap<String, String[]>();
833            
834            final String resname = policies.getProperty(PREFIX_NAME + idx
835                    , props.getProperty(PREFIX_NAME + idx, "")).trim(); 
836            
837            final String restype = policies.getProperty(PREFIX_TYPE + idx
838                    , props.getProperty(PREFIX_TYPE + idx, "").toLowerCase().trim()); 
839            
840            final String[] resaccess = policies.getProperty(PREFIX_ACCESS + idx
841                    , props.getProperty(PREFIX_ACCESS + idx, "")).trim().split(",");
842            
843            for (int j=0; j<resaccess.length; j++) {
844                resaccess[j] = resaccess[j].trim();
845            }
846            
847            access.put(restype, resaccess);
848            
849            if (MAX_NUM_FILTERS == i) {
850                final String errorMessage = "Over the max number of resources allowed: " + i;
851                LOGGER.severe(errorMessage);
852                throw new IllegalArgumentException(errorMessage);
853            } else if (resname.isEmpty()) {
854                break;
855            }
856            
857            this.resources.put(resname, access);
858        }        
859    }
860    
861    // pre-condition is that caller has write lock
862    private UserInfo cacheUserInfo(final String username) throws NamingException {
863        
864        if (null == this.userInfoFilter 
865                || this.userInfoFilter.isEmpty() 
866                || this.userInfoLabels.size() == 0) {
867            LOGGER.info(USER_INFO_FILTER + " was empty OR no value(s) specified for the "  
868                    + USER_INFO +" property");
869            return null;
870        }
871
872        // perform AD lookup add to cache 
873        final LdapContext context = new InitialLdapContext(this.environment, null);
874        final NamingEnumeration<SearchResult> results = 
875                context.search(this.deecee
876                        , String.format(this.userInfoFilter, username)
877                        , this.srchCntrls);
878        
879        boolean found = false;
880        final Map<String, List<String>> labelInfo = new HashMap<String, List<String>>();
881        while (results.hasMoreElements()) {
882            found = true;
883            final SearchResult result = (SearchResult) results.nextElement();
884            final Attributes attributes = result.getAttributes();
885            for (@SuppressWarnings("rawtypes")
886                NamingEnumeration iter = attributes.getAll(); iter.hasMore();) {
887                final Attribute attribute = (Attribute) iter.next();
888                final String label = attribute.getID();
889                final List<String> info = new ArrayList<String>();
890                if (this.userInfoLabels.contains(label)) {
891                    labelInfo.put(label, info);
892                    for (@SuppressWarnings("rawtypes")
893                        NamingEnumeration enmr = attribute.getAll(); enmr.hasMore();) {
894                        info.add(enmr.next().toString());
895                    }
896                }
897            }
898        }
899        results.close();
900        context.close();
901        
902        // add to cache
903        final UserInfo userInfoObject;
904        if (found) {
905            //LOGGER.info("add to cache userInfoList");
906            userInfoObject = new UserInfo() {
907                private final Map<String, List<String>> info = labelInfo;
908                private final String labels = userInfoLabels.toString();
909                
910                @Override
911                public List<String> getInfo(final String label) {
912                    if (!hasInfo(label)) {
913                        throw new NullPointerException(
914                                "UserInfo label not found or not in user store: " + label
915                                + " - labels specified in property file: " + labels);
916                    }
917                    return new ArrayList<String>(info.get(label));
918                }
919
920                @Override
921                public List<String> getLabels() {
922                    return new ArrayList<String>(info.keySet());
923                }
924
925                @Override
926                public boolean hasInfo(final String label) {
927                    return info.containsKey(label);
928                }
929            };
930            this.userInfoList.put(username, userInfoObject);
931        } else {
932            throw new IllegalArgumentException("UserInfo not found. "
933                    + ". This MAY be caused by an incorrect spnego.authz.ldap.user.filter definition" 
934                    + "; filter=" + this.userInfoFilter
935                    + "; policy=" + this.policy);            
936        }
937        
938        return userInfoObject;
939    }
940}