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