001    package org.hackystat.dailyprojectdata.server;
002    
003    import java.util.HashMap;
004    
005    import java.util.Map;
006    
007    import org.hackystat.sensorbase.client.SensorBaseClient;
008    import org.restlet.Context;
009    import org.restlet.Guard;
010    import org.restlet.data.ChallengeScheme;
011    import org.restlet.data.Request;
012    
013    /**
014     * Performs authentication of each HTTP request using HTTP Basic authentication. 
015     * Checks user/password credentials by pinging SensorBase, then caching authentic 
016     * user/password combinations.  If a cached user/password combo does not match the 
017     * current user/password combo, then the SensorBase is pinged again (because maybe
018     * the user has changed their password recently). 
019     * <p>
020     * Because DPD resources will always want to communicate with the underlying SensorBase, this 
021     * Authenticator also creates a map of user-to-SensorBaseClient instances which can be retrieved
022     * from the server context.  This keeps the user password info in this class, while making the 
023     * SensorBaseClient instance available to the service. 
024     * @author Philip Johnson
025     */
026    public class Authenticator extends Guard {
027      
028      /** A singleton map containing previously verified credentials. */
029      private static Map<String, String> credentials = new HashMap<String, String>();
030      
031      /** A singleton map containing SensorBaseClient instances, one per credentialed user. */
032      private static Map<String, SensorBaseClient> userClientMap = 
033        new HashMap<String, SensorBaseClient>();
034      
035      /** The sensorbase host, such as "http://localhost:9876/sensorbase/". */
036      private String sensorBaseHost;
037      
038      /** The key to be used to retrieve the sensorbaseclient map from the server context. */
039      public static final String AUTHENTICATOR_SENSORBASECLIENTS_KEY = "authenticator.clients";
040    
041      /**
042       * Initializes this Guard to do HTTP Basic authentication.
043       * Puts the credentials map in the server context so that Resources can get the 
044       * password associated with the uriUser for their own invocations to the SensorBase. 
045       * @param context The server context.
046       * @param sensorBaseHost The host, such as 'http://localhost:9876/sensorbase/'.
047       */
048      public Authenticator (Context context, String sensorBaseHost) {
049        super(context, ChallengeScheme.HTTP_BASIC,  "DailyProjectData");
050        this.sensorBaseHost = sensorBaseHost;
051        context.getAttributes().put(AUTHENTICATOR_SENSORBASECLIENTS_KEY, userClientMap);
052      }
053      
054      /**
055       * Returns true if the passed credentials are OK.
056       * @param request Ignored. 
057       * @param identifier The account name.
058       * @param secretCharArray The password. 
059       * @return If the credentials are valid.
060       */
061      @Override public boolean checkSecret(Request request, 
062          String identifier, char[] secretCharArray) {
063        /*
064         * I am synchronizing here on a static (class-wide) variable for two reasons:
065         * (1) JCS write-through caching fails when multiple threads access the same back-end file:
066         * <https://issues.apache.org/jira/browse/JCS-31>. Thus, it is vitally important to ensure
067         * that only one instance of a SensorBaseClient for any given user is created.
068         * (2) I do not know if Restlet allows multiple Authenticator instances.  Thus, I am 
069         * synchronizing on a class-wide variable just in case. 
070         * This synchronization creates a bottleneck on every request, but the benefits of reliable
071         * caching should outweigh this potential performance hit under high loads. 
072         */
073        synchronized (userClientMap) {
074          String secret = new String(secretCharArray);
075          // Return true if the user/password credentials are in the cache.
076          if (credentials.containsKey(identifier) && secret.equals(credentials.get(identifier))) {
077            return true;
078          }
079          // Otherwise we check the credentials with the SensorBase.
080          boolean isRegistered = SensorBaseClient.isRegistered(sensorBaseHost, identifier, secret);
081          if (isRegistered) {
082            // Credentials are good, so save them and create a sensorbase client for this user.
083            credentials.put(identifier, secret);
084            SensorBaseClient client = new SensorBaseClient(sensorBaseHost, identifier, secret);
085            // Set timeout to 60 minutes.
086            client.setTimeout(1000 * 60 * 60);
087            // Get the ServerProperties instance so we can determine if caching is enabled.
088            Server server = (Server) getContext().getAttributes().get("DailyProjectDataServer");
089            ServerProperties props = server.getServerProperties();
090            if (props.isCacheEnabled()) {
091              client.enableCaching(identifier, "dailyprojectdata", props.getCacheMaxLife(), props
092                  .getCacheCapacity());
093            }
094            userClientMap.put(identifier, client);
095          }
096        return isRegistered;
097        }
098      }
099    }