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}