001/*
002 * Copyright (c) 2013 - 2016 TDP Ltd All Rights Reserved.
003 * TDP Ltd grants permission, free of charge, to any person obtaining copies
004 * of this software and its associated documentation files (the "Software"),
005 * to deal in the Software without restriction, including to use, copy, adapt,
006 * publish, distribute, display, perform, sublicense, and sell copies of the
007 * Software, subject to the following condition: You must include the above
008 * copyright notice and this permission notice in all full or partial copies
009 * of the Software.
010 * 
011 * TDP LTD PROVIDES THE SOFTWARE "AS IS," WITHOUT ANY EXPRESS OR IMPLIED WARRANTY,
012 * INCLUDING WITHOUT THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
013 * PARTICULAR PURPOSE, AND NON-INFRINGMENT. TDP LTD, THE AUTHORS OF THE SOFTWARE,
014 * AND THE OWNERS OF COPYRIGHT IN THE SOFTWARE ARE NOT LIABLE FOR ANY CLAIM, DAMAGES,
015 * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING
016 * FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
017 * THE SOFTWARE.
018 */
019package cz.tdp.kshield.client;
020
021import java.io.IOException;
022import java.net.URI;
023import java.net.URISyntaxException;
024import java.util.EnumSet;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.apache.http.HttpEntity;
029import org.apache.http.HttpStatus;
030import org.apache.http.client.config.RequestConfig;
031import org.apache.http.client.methods.CloseableHttpResponse;
032import org.apache.http.client.methods.HttpGet;
033import org.apache.http.client.methods.HttpPost;
034import org.apache.http.client.utils.URIBuilder;
035import org.apache.http.entity.ContentType;
036import org.apache.http.entity.StringEntity;
037import org.apache.http.impl.client.CloseableHttpClient;
038import org.apache.http.impl.client.HttpClients;
039
040import cz.tdp.kshield.client.UserInfo.AuthType;
041
042/**
043 * <b>This is central class in client package.</b> This class encapsulates communication with KeyShield SSO Server.
044 * 
045 * @see cz.tdp.kshield.client.UserInfo
046 * @see cz.tdp.kshield.integration.SimpleAuthenticationServiceImpl
047 * @see cz.tdp.kshield.integration.web.DefaultAuthenticationServiceImpl
048 */
049public class KShieldClient
050{
051  private static final String JSON_USER_BY_IP_CMD = "/json/userByIP/";
052  private static final String CLIENT_MESSAGE_CMD  = "/api/sendClientMessage";
053  
054  private final URI url;
055  private final String apiKey;
056  private final CloseableHttpClient httpclient;
057  private final String attributes;
058  private ResponseLevel responseLevel;
059  
060  /**
061   * Construct a new KShieldClient for given KeyShield SSO Server url, given a HttpClient
062   * 
063   * @param url URL of KeyShield SSO Server web service (http://hostname:port).
064   * 
065   * @throws IllegalArgumentException if url is null, empty or invalid
066   */
067  public KShieldClient(String url) {
068    this(createURI(url), null, null, null);
069  }
070  
071  /**
072   * Construct a new KShieldClient for given KeyShield SSO Server url, given a HttpClient
073   * 
074   * @param url URL of KeyShield SSO Server web service (http://hostname:port).
075   * @param httpclient configured instance of apache HTTP client - for example configured for multithreaded usage, which allows sharing single instance of KShieldClient between multiple threads
076   *                    Creates default http client if parameter is null.
077   * @param attrs Additional attributes retrieved from KeyShield SSO Server
078   * 
079   * @throws IllegalArgumentException if url is null, empty or invalid
080   */
081  public KShieldClient(String url, CloseableHttpClient httpclient, String... attrs) {
082    this(createURI(url), null, httpclient, createAttrs(attrs));
083  }
084  
085  /**
086   * Construct a new KShieldClient for given KeyShield SSO Server url, given a HttpClient
087   * 
088   * @param url URL of KeyShield SSO Server web service (http://hostname:port).
089   * @param apiKey API authorization key.
090   * @param httpclient configured instance of apache HTTP client - for example configured for multithreaded usage, which allows sharing single instance of KShieldClient between multiple threads
091   *                    Creates default http client if parameter is null.
092   * @param attrs Additional attributes retrieved from KeyShield SSO Server
093   * 
094   * @throws IllegalArgumentException if url is null, empty or invalid
095   */
096  public KShieldClient(String url, String apiKey, CloseableHttpClient httpclient, String... attrs) {
097    this(createURI(url), apiKey, httpclient, createAttrs(attrs));
098  }
099  
100  protected KShieldClient(URI url, String apiKey, CloseableHttpClient httpclient, String attrs) {
101    if (httpclient == null) {
102      this.httpclient = HttpClients.createDefault();
103    }
104    else {
105      this.httpclient = httpclient;
106    }
107    
108    this.url = url;
109    this.apiKey = apiKey;
110    this.attributes = attrs;
111  }
112  
113  public ResponseLevel getResponseLevel() {
114    return this.responseLevel;
115  }
116
117  public void setResponseLevel(ResponseLevel extendedResponse) {
118    this.responseLevel = extendedResponse;
119  }
120
121  /**
122   * Retrieves UserInfo for a given IP address
123   * 
124   * @param remoteIP IP address of workstation for which you want to obtain UserInfo
125   * @return new UserInfo instance
126   * @throws KShieldClientException
127   */
128  public UserInfo getUserByIP(String remoteIP) throws KShieldClientException {
129    return getUserByIP(remoteIP, null);
130  }
131  
132  /**
133   * Retrieves UserInfo for a given IP address
134   * 
135   * @param remoteIP IP address of workstation for which you want to obtain UserInfo
136   * @param usernameAttribute name of attribute for username, set it to null if you want use screenName instead of username
137   * @return new UserInfo instance or null if authentication method is not supported
138   * @throws KShieldClientException
139   */
140  public UserInfo getUserByIP(String remoteIP, String usernameAttribute) throws KShieldClientException {
141    try {
142      final URI uri = createUserByIPUri(remoteIP);
143      
144      return processGetUserByIP(uri, remoteIP, usernameAttribute);
145    }
146    catch (URISyntaxException e) {
147      throw new KShieldClientException("Invalid KeyShield SSO Server request URL", e);
148    }
149    catch (IllegalStateException|IOException  e) {
150      throw new KShieldClientException(e);
151    }
152  }
153
154  private URI createUserByIPUri(String remoteIP) throws URISyntaxException {
155    final URIBuilder builder = new URIBuilder(url)
156        .setPath(url.getPath() + JSON_USER_BY_IP_CMD + remoteIP);
157    
158    if (apiKey != null && apiKey.length() > 0) {
159      builder.addParameter("key", apiKey);
160      
161      if (responseLevel != null) {
162        builder.addParameter("level", responseLevel.name());
163      }
164    }
165    
166    if (attributes != null && attributes.length() > 0) {
167      builder.addParameter("attributes", attributes);
168    }
169    
170    return builder.build();
171  }
172
173  private UserInfo processGetUserByIP(final URI uri, String remoteIP, String usernameAttribute) throws KShieldClientException, IOException {
174    if (log.isDebugEnabled()) {
175      log.debug("Querying KShield: " + uri);
176    }
177    
178    final HttpGet httpget = new HttpGet(uri);
179
180    try (CloseableHttpResponse response = httpclient.execute(httpget)) {
181      final HttpEntity entity = response.getEntity();
182 
183      if (log.isDebugEnabled()) {
184        log.debug("Response status: " + response.getStatusLine());
185      }
186      
187      if (entity != null && response.getStatusLine() != null) {
188        final int httpStatus = response.getStatusLine().getStatusCode();
189        return createUserInfo(remoteIP, usernameAttribute, entity, httpStatus);
190      }
191      
192      log.warn("Missing HttpResponse entity");
193      throw new KShieldClientException("Missing HttpResponse entity");
194    }
195  }
196  
197  /**
198   * @param from
199   * @param to
200   * @param message
201   * @throws KShieldClientException
202   */
203  public void sendClientMessage(String from, String to, String message) throws KShieldClientException {
204    sendClientMessage(new ClientMessage(from, to, message));
205  }
206  
207  /**
208   * Sends client message to KeyShield server
209   * 
210   * @param msg Message data
211   * @throws KShieldClientException
212   */
213  public void sendClientMessage(ClientMessage msg) throws KShieldClientException {
214    try {
215      final URI uri = createClientMessageUri();
216      
217      processClientMessage(uri, msg);
218    }
219    catch (URISyntaxException e) {
220      throw new KShieldClientException("Invalid KeyShield SSO Server request URL", e);
221    }
222    catch (IllegalStateException|IOException e) {
223      throw new KShieldClientException(e);
224    }
225  }
226
227  protected URI createClientMessageUri() throws URISyntaxException {
228    final URIBuilder builder = new URIBuilder(url)
229        .setPath(url.getPath() + CLIENT_MESSAGE_CMD);
230    
231    if (apiKey != null && apiKey.length() > 0) {
232      builder.addParameter("key", apiKey);
233      
234      if (responseLevel != null) {
235        builder.addParameter("level", responseLevel.name());
236      }
237    }
238    
239    return builder.build();
240  }
241
242  private void processClientMessage(URI uri, ClientMessage msg) throws IOException, KShieldClientException {
243    if (log.isDebugEnabled()) {
244      log.debug("Sending KShield command: " + uri);
245    }
246    
247    final HttpPost httppost = new HttpPost(uri);
248    httppost.setEntity(new StringEntity(msg.toJSON(), ContentType.APPLICATION_JSON));
249    
250    if (log.isDebugEnabled()) {
251      log.debug("Client message: " + msg.toJSON());
252    }
253    
254    // setting timeout to 10 sec and socket timeout to 2 minutes, we expect possible long ldap lookup on server side
255    final RequestConfig config = RequestConfig.custom()
256        .setSocketTimeout(120 * 1000)
257        .setConnectTimeout(10 * 1000)
258        .build();
259    
260    httppost.setConfig(config);
261    
262    try (CloseableHttpResponse response = httpclient.execute(httppost)) {
263      final HttpEntity entity = response.getEntity();
264  
265      if (log.isDebugEnabled()) {
266        log.debug("Response status: " + response.getStatusLine());
267      }
268      
269      if (entity != null && response.getStatusLine() != null) {
270        final int httpStatus = response.getStatusLine().getStatusCode();
271        checkClientMessageResponse(entity, httpStatus);
272      }
273      else {
274        log.warn("Missing HttpResponse entity");
275        throw new KShieldClientException("Missing HttpResponse entity");
276      }
277    }
278  }
279  
280  private void checkClientMessageResponse(HttpEntity entity, int httpStatus) throws KShieldClientException {
281    if (httpStatus == HttpStatus.SC_FORBIDDEN) {
282      throw new KShieldInvalidApiKeyException(apiKey());
283    }
284    if (httpStatus != HttpStatus.SC_OK) {
285      throw new KShieldClientException("Invalid status returned: " + httpStatus);
286    }
287  }
288
289  private String apiKey() {
290    if (apiKey != null && apiKey.startsWith("key=")) {
291      return apiKey.substring(4);
292    }
293    return "";
294  }
295
296  private UserInfo createUserInfo(String remoteIP, String usernameAttribute, HttpEntity entity, int httpStatus) throws KShieldClientException, IOException {
297    final UserInfo userInfo = UserInfoJsonDecoder.decodeFromStream(entity.getContent());
298
299    if (log.isDebugEnabled()) {
300      log.debug("KShield response: " + userInfo.getUserID() + ", authType: " + userInfo.getAuthType() + ", ipAddress: " + userInfo.getIpAddress());
301    }
302    
303    if (httpStatus == HttpStatus.SC_FORBIDDEN) {
304      throw new KShieldInvalidApiKeyException(apiKey());
305    }
306    
307    if (userInfo.getUserID() != null && allowedAuthTypes.contains(userInfo.getAuthType())) {
308      if (!remoteIP.equals(userInfo.getIpAddress())) {
309        throw new KShieldInvalidRemoteIpException(remoteIP, userInfo.getIpAddress());
310      }
311      
312      userInfo.setUsernameAttribute(usernameAttribute);
313      
314      if (log.isDebugEnabled()) {
315        log.debug("Authenticated username: " + userInfo.getUsername());
316      }
317      
318      return userInfo;
319    }
320    
321    return null;
322  }
323  
324  /**
325   * Retrieves UserInfo for a given IP address
326   * @deprecated Please use instance methods instead
327   * 
328   * @param url KeyShield SSO Server url
329   * @param remoteIP IP address of workstation for which you want to obtain UserInfo
330   * @param usernameAttribute attribute for fetching username (could be null)
331   * @return value of user id attribute fetched from KeyShield SSO Server
332   */
333  @Deprecated
334  public static UserInfo getUserByIP(String url, String remoteIP, String usernameAttribute) {
335    return getUserByIP(url, null, remoteIP, usernameAttribute);
336  }
337
338  /**
339   * Retrieves UserInfo for a given IP address
340   * @deprecated Please use instance methods instead
341   * 
342   * @param url KeyShield SSO Server url
343   * @param remoteIP IP address of workstation for which you want to obtain UserInfo
344   * @param usernameAttribute attribute for fetching username (could be null)
345   * @return value of user id attribute fetched from KeyShield SSO Server
346   */
347  @Deprecated
348  public static UserInfo getUserByIP(String url, String apiKey, String remoteIP, String usernameAttribute) {
349    if (remoteIP != null && !remoteIP.isEmpty()) {
350      try {
351        final KShieldClient client;
352        
353        if (usernameAttribute != null) {
354          client = new KShieldClient(url, apiKey, null, usernameAttribute);
355        }
356        else {
357          client = new KShieldClient(url, apiKey, null);
358        }
359  
360        return client.getUserByIP(remoteIP, usernameAttribute);
361      }
362      catch (KShieldClientException e) {
363        log.error("client error", e);
364      }
365      catch (IllegalArgumentException e) {
366        log.error("Invalid or empty KeyShield SSO Server URL", e);
367      }
368    }
369
370    return null;
371  }
372  
373  /**
374   * Retrieves only username for a given IP address
375   * @deprecated Please use instance methods instead
376   * 
377   * @param url KeyShield SSO Server url
378   * @param remoteIP IP address of workstation for which you want to obtain username
379   * @return username fetched from KeyShield SSO Server
380   */
381  @Deprecated
382  public static String getUsernameByIP(String url, String remoteIP) {
383    return getUsernameByIP(url, remoteIP, "cn");
384  }
385  
386  /**
387   * Retrieves only username for a given IP address
388   * @deprecated Please use instance methods instead
389   * 
390   * @param url KeyShield SSO Server url
391   * @param remoteIP IP address of workstation for which you want to obtain username
392   * @param usernameAttribute name of username attribute, screenName is used if not defined
393   * @return username fetched from KeyShield SSO Server
394   */
395  @Deprecated
396  public static String getUsernameByIP(String url, String remoteIP, String usernameAttribute) {
397    final UserInfo userInfo = getUserByIP(url, remoteIP, usernameAttribute);
398    
399    if (userInfo != null) {
400      return userInfo.getUsername();
401    }
402    
403    return null;
404  }
405  
406  private EnumSet<AuthType> allowedAuthTypes = UserInfo.DEFAULT_ALLOWED_AUTH_TYPES;
407
408  /**
409   * Return allowed authentication types. See {@link AuthType}
410   */
411  public EnumSet<AuthType> getAllowedAuthTypes() {
412    return allowedAuthTypes;
413  }
414
415  /**
416   * Set allowed authentication types. See {@link AuthType}
417   */
418  public void setAllowedAuthTypes(EnumSet<AuthType> allowAuthTypes) {
419    if (allowAuthTypes != null) {
420      this.allowedAuthTypes = allowAuthTypes;
421    }
422    else {
423      allowedAuthTypes = UserInfo.DEFAULT_ALLOWED_AUTH_TYPES;
424    }
425  }
426  
427  private static URI createURI(String url) {
428    if (url == null || url.isEmpty()) throw new IllegalArgumentException("Missing or empty url parameter");
429    return URI.create(url);
430  }
431  
432  private static String createAttrs(String[] attrs) {
433    final StringBuilder buf = new StringBuilder(attrs != null ? ((attrs.length + 1) * 10) : 30);
434    
435    buf.append(UserInfo.GUID_ATTRIBUTE);
436    
437    if (attrs != null && attrs.length > 0) {
438      for (String attr : attrs) {
439        if (!UserInfo.GUID_ATTRIBUTE.equalsIgnoreCase(attr)) {
440          buf.append(',');
441          buf.append(attr);
442        }
443      }
444    }
445    
446    return buf.toString();
447  }
448  
449  private static final Log log = LogFactory.getLog(KShieldClient.class);
450}