001package com.identityworksllc.iiq.common; 002 003import org.apache.commons.logging.Log; 004import org.apache.commons.logging.LogFactory; 005import sailpoint.connector.RPCService; 006import sailpoint.object.*; 007import sailpoint.tools.GeneralException; 008import sailpoint.tools.Util; 009 010import java.io.IOException; 011import java.io.InputStream; 012import java.lang.reflect.Constructor; 013import java.util.ArrayList; 014import java.util.HashMap; 015import java.util.List; 016import java.util.Map; 017import java.util.concurrent.atomic.AtomicBoolean; 018 019/** 020 * Utilities for interacting with the IQService, particularly for executing Powershell 021 */ 022public class IQServiceUtilities { 023 /** 024 * A fluent builder for {@link RpcRequest} objects 025 */ 026 @SuppressWarnings("javadoc") 027 public static class RPCRequestBuilder { 028 /** 029 * The connection info 030 */ 031 private Map<String, Object> connectionInfo; 032 033 /** 034 * The account request to pass to the Powershell script 035 */ 036 private ProvisioningPlan.AccountRequest inputs; 037 038 /** 039 * The name of the RPC service to call, usually ScriptExecutor 040 */ 041 private String rpcService; 042 043 /** 044 * The Rule to serialize and execute, often constructed ad hoc 045 */ 046 private Rule rule; 047 048 /** 049 * The rule template to use if {@link #withCommands(String)} is used. 050 */ 051 private String template; 052 053 private RPCRequestBuilder() { 054 this.rpcService = SCRIPT_EXECUTOR; 055 } 056 057 /** 058 * Creates a new {@link RPCRequestBuilder} object 059 * @return A new RPCRequestBuilder, ready for fluent building 060 */ 061 public static RPCRequestBuilder builder() { 062 return new RPCRequestBuilder(); 063 } 064 065 /** 066 * Builds a new RpcRequest using the inputs present, if enough are available. 067 * If the inputs are not correct, throws an exception. 068 * 069 * @return the new RpcRequest 070 * @throws GeneralException if input parameters are invalid or the creation of the request fails 071 */ 072 public RpcRequest build() throws GeneralException { 073 if (this.inputs == null) { 074 throw new GeneralException("The builder was not supplied with input parameters or an AccountRequest"); 075 } 076 if (this.rule == null) { 077 throw new GeneralException("The builder was not supplied with a Rule to execute"); 078 } 079 if (this.connectionInfo == null || this.connectionInfo.isEmpty()) { 080 throw new GeneralException("The builder was not supplied with populated connection parameters"); 081 } 082 if (!this.connectionInfo.containsKey(RPCService.IQSERVICE_CONFIGURATION) && !this.connectionInfo.containsKey(RPCService.CONFIG_IQSERVICE_HOST)) { 083 throw new GeneralException("The connection info is not valid IQService connection info; must contain either " + RPCService.IQSERVICE_CONFIGURATION + " or " + RPCService.CONFIG_IQSERVICE_HOST); 084 } 085 return new RpcRequest(this.rpcService, RUN_AFTER_SCRIPT, createRPCRequestContent(this.rule, this.connectionInfo, this.inputs)); 086 } 087 088 public RPCRequestBuilder withCommands(String commands, String template) throws GeneralException { 089 return withTemplate(template).withRule(wrapPowershellCommands(commands, this.template)); 090 } 091 092 public RPCRequestBuilder withCommands(String commands) throws GeneralException { 093 return withRule(wrapPowershellCommands(commands, this.template)); 094 } 095 096 public RPCRequestBuilder withConnectionInfo(Map<String, Object> connectionInfo) { 097 this.connectionInfo = connectionInfo; 098 return this; 099 } 100 101 public RPCRequestBuilder withConnectionInfo(Application application) { 102 return withConnectionInfo(application.getAttributes()); 103 } 104 105 public RPCRequestBuilder withInputs(ProvisioningPlan.AccountRequest inputs) { 106 this.inputs = inputs; 107 return this; 108 } 109 110 public RPCRequestBuilder withInputs(Map<String, Object> inputs) { 111 return withInputs(createFakeAccountRequest(inputs)); 112 } 113 114 public RPCRequestBuilder withRPCService(String serviceName) { 115 this.rpcService = serviceName; 116 return this; 117 } 118 119 public RPCRequestBuilder withRule(Rule rule) { 120 this.rule = rule; 121 return this; 122 } 123 124 public RPCRequestBuilder withTemplate(String template) { 125 this.template = template; 126 return this; 127 } 128 } 129 /** 130 * The 'hidden' input to disable hostname verification 131 */ 132 public static final String CONFIG_DISABLE_HOSTNAME_VERIFICATION = "disableHostnameVerification"; 133 /** 134 * The output variable returned by the default template 135 */ 136 public static final String DEFAULT_TEMPLATE_OUTPUT = "output"; 137 /** 138 * The input argument containing the Application's attributes 139 */ 140 public static final String IQSERVICE_FIELD_APPLICATION = "Application"; 141 /** 142 * The input argument containing an {@link sailpoint.object.ProvisioningPlan.AccountRequest} 143 */ 144 public static final String IQSERVICE_FIELD_REQUEST = "Request"; 145 /** 146 * The input argument containing the actual Powershell script to run 147 */ 148 public static final String IQSERVICE_FIELD_RULE = "postScript"; 149 /** 150 * The default Powershell script type 151 */ 152 public static final String RUN_AFTER_SCRIPT = "runAfterScript"; 153 /** 154 * The default RPC Service type, execution of a Powershell script 155 */ 156 public static final String SCRIPT_EXECUTOR = "ScriptExecutor"; 157 158 /** 159 * The path to the standard powershell template, which should be included with this library 160 */ 161 public static final String STANDARD_POWERSHELL_TEMPLATE = "/powershell.template.ps1"; 162 /** 163 * The token in the wrapper script to replace with the user commands 164 */ 165 public static final String TOKEN_USER_COMMAND = "%%USER_COMMAND%%"; 166 /** 167 * Logger 168 */ 169 private static final Log log = LogFactory.getLog(IQServiceUtilities.class); 170 /** 171 * The cached static result of the {@link #supportsTLS()} check 172 */ 173 private static final AtomicBoolean supportsTLS; 174 175 static { 176 boolean result = false; 177 try { 178 Constructor<RPCService> ignored = RPCService.class.getConstructor(String.class, Integer.TYPE, Boolean.TYPE, Boolean.TYPE); 179 result = true; 180 } catch(Exception e) { 181 /* Ignore */ 182 } 183 supportsTLS = new AtomicBoolean(result); 184 } 185 186 /** 187 * Checks the {@link RpcResponse} for error messages. If there are any, it throws an exception with the messages. 188 * If the RpcResponse contains any messages, they will be logged as warnings. 189 * 190 * The default logger will be used. If you wish to provide your own logger, use the two-argument 191 * {@link #checkRpcFailure(RpcResponse, Log)}. 192 * 193 * @param response the response to check 194 * @return the same response 195 * @throws GeneralException if there were any errors 196 */ 197 protected static RpcResponse checkRpcFailure(RpcResponse response) throws GeneralException { 198 return checkRpcFailure(response, log); 199 } 200 201 /** 202 * Checks the RpcResponse for error messages. If there are any, it throws an exception with the messages. 203 * If the RpcResponse contains any messages, they will be logged as warnings. 204 * 205 * @param response the response to check 206 * @param yourLogger The logger ot which the errors should be logged 207 * @return the same response 208 * @throws GeneralException if there were any errors 209 */ 210 public static RpcResponse checkRpcFailure(RpcResponse response, Log yourLogger) throws GeneralException { 211 if (response == null) { 212 return null; 213 } 214 if (response.getErrors() != null && !response.getErrors().isEmpty()) { 215 if (yourLogger != null) { 216 yourLogger.error("Received errors from the IQService: " + response.getErrors()); 217 } else { 218 log.error("Received errors from the IQService: " + response.getErrors()); 219 } 220 throw new GeneralException("Errors from IQService: " + response.getErrors().toString()); 221 } 222 if (response.getMessages() != null) { 223 for(String message : response.getMessages()) { 224 yourLogger.warn("Received a message from the IQService: " + message); 225 } 226 } 227 return response; 228 } 229 230 /** 231 * Constructs a fake account request to use for a call to IIQ 232 * 233 * @param parameters Any parameters to send to the IIQ call 234 * @return The account request 235 */ 236 public static ProvisioningPlan.AccountRequest createFakeAccountRequest(Map<String, Object> parameters) { 237 ProvisioningPlan.AccountRequest accountRequest = new ProvisioningPlan.AccountRequest(); 238 accountRequest.setApplication("PSUTIL"); 239 accountRequest.setNativeIdentity("*FAKE*"); 240 accountRequest.setOperation(ProvisioningPlan.AccountRequest.Operation.Modify); 241 List<ProvisioningPlan.AttributeRequest> fakeAttributeRequests = new ArrayList<ProvisioningPlan.AttributeRequest>(); 242 ProvisioningPlan.AttributeRequest attributeRequest = new ProvisioningPlan.AttributeRequest(); 243 attributeRequest.setName("CmdletResponse"); 244 attributeRequest.setOperation(ProvisioningPlan.Operation.Add); 245 attributeRequest.setValue(""); 246 fakeAttributeRequests.add(attributeRequest); 247 if (parameters != null) { 248 for (String key : parameters.keySet()) { 249 ProvisioningPlan.AttributeRequest fakeAttribute = new ProvisioningPlan.AttributeRequest(); 250 fakeAttribute.setOperation(ProvisioningPlan.Operation.Add); 251 fakeAttribute.setName(key); 252 fakeAttribute.setValue(parameters.get(key)); 253 fakeAttributeRequests.add(fakeAttribute); 254 } 255 } 256 accountRequest.setAttributeRequests(fakeAttributeRequests); 257 258 if (log.isTraceEnabled()) { 259 log.trace("For input arguments " + parameters + ", produced output " + Utilities.safeToXml(accountRequest)); 260 } 261 262 return accountRequest; 263 } 264 265 /** 266 * Creates a map to be passed to an RPCRequest. 267 * 268 * @param rule The rule to execute 269 * @param connectionInfo The Application containing connection into 270 * @param inputs The rule inputs 271 * @return The populated Map 272 */ 273 public static Map<String, Object> createRPCRequestContent(Rule rule, Application connectionInfo, Map<String, Object> inputs) { 274 return createRPCRequestContent(rule, connectionInfo.getAttributes(), inputs); 275 } 276 277 /** 278 * Creates a map to be passed to an RPCRequest. 279 * 280 * @param rule The rule to execute 281 * @param connectionInfo The Map containing connection into 282 * @param inputs The rule inputs 283 * @return The populated Map 284 */ 285 public static Map<String, Object> createRPCRequestContent(Rule rule, Map<String, Object> connectionInfo, ProvisioningPlan.AccountRequest inputs) { 286 Map<String, Object> dataMap = new HashMap<>(); 287 288 dataMap.put(IQSERVICE_FIELD_RULE, rule); 289 dataMap.put(IQSERVICE_FIELD_APPLICATION, connectionInfo); 290 dataMap.put(IQSERVICE_FIELD_REQUEST, inputs); 291 292 return dataMap; 293 } 294 295 /** 296 * Creates a map to be passed to an RPCRequest. 297 * 298 * @param rule The rule to execute 299 * @param connectionInfo The Map containing connection into 300 * @param inputs The rule inputs 301 * @return The populated Map 302 */ 303 public static Map<String, Object> createRPCRequestContent(Rule rule, Map<String, Object> connectionInfo, Map<String, Object> inputs) { 304 return createRPCRequestContent(rule, connectionInfo, createFakeAccountRequest(inputs)); 305 } 306 307 /** 308 * Creates a map to be passed to an RPCRequest. 309 * 310 * @param commands The Powershell commands to execute; will be translated via {@link #wrapPowershellCommands(String, String)} 311 * @param connectionInfo The Application containing connection into 312 * @param inputs The rule inputs 313 * @return The populated Map 314 * @throws GeneralException if constructing request input fails, usually because of failure to read the template 315 */ 316 public static Map<String, Object> createRPCRequestContent(String commands, Application connectionInfo, Map<String, Object> inputs) throws GeneralException { 317 return createRPCRequestContent(wrapPowershellCommands(commands, null), connectionInfo.getAttributes(), inputs); 318 } 319 320 /** 321 * Constructs an RPCService from the connection info provided 322 * 323 * @param connectionInfo The connection info from an Application or other config 324 * @return The resulting RPCService 325 */ 326 public static RPCService createRPCService(Map<String, Object> connectionInfo) { 327 RPCService service; 328 329 Map<String, Object> iqServiceConfig = findIQServiceConfig(connectionInfo); 330 boolean ignoreHostnameVerification = Util.otob(iqServiceConfig.get(CONFIG_DISABLE_HOSTNAME_VERIFICATION)); 331 boolean useTLS = Util.otob(iqServiceConfig.get(RPCService.CONFIG_IQSERVICE_TLS)); 332 333 String iqServiceHost = Util.otoa(iqServiceConfig.get(RPCService.CONFIG_IQSERVICE_HOST)); 334 int iqServicePort = Util.otoi(iqServiceConfig.get(RPCService.CONFIG_IQSERVICE_PORT)); 335 336 if (log.isDebugEnabled()) { 337 log.debug("Creating new RPCService for host = " + iqServiceHost + ", port = " + iqServicePort); 338 } 339 340 // If you pass true, we're going to assume you know what you're talking about 341 if (ignoreHostnameVerification || useTLS || supportsTLS()) { 342 service = new RPCService(iqServiceHost, iqServicePort, false, useTLS, ignoreHostnameVerification); 343 service.setConnectorServices(new sailpoint.connector.DefaultConnectorServices()); 344 } else { 345 service = new RPCService(iqServiceHost, iqServicePort, false); 346 } 347 return service; 348 } 349 350 /** 351 * Finds the IQService config. This is isolated here so that we can ignore warnings on 352 * as small a bit of code as possible 353 * @param connectionInfo The connection info from the application or other config 354 * @return The IQService config, either the original, or extracted 355 */ 356 @SuppressWarnings("unchecked") 357 public static Map<String, Object> findIQServiceConfig(Map<String, Object> connectionInfo) { 358 Map<String, Object> iqServiceConfig = connectionInfo; 359 if (connectionInfo.get(RPCService.IQSERVICE_CONFIGURATION) instanceof Map) { 360 iqServiceConfig = (Map<String, Object>) connectionInfo.get(RPCService.IQSERVICE_CONFIGURATION); 361 } 362 return iqServiceConfig; 363 } 364 365 /** 366 * Get standard template from the classpath 367 * @return The standard template 368 * @throws IOException If any errors occur opening the template stream 369 * @throws GeneralException If any errors occur parsing the template stream 370 */ 371 public static String getStandardTemplate() throws IOException, GeneralException { 372 try(InputStream templateStream = IQServiceUtilities.class.getResourceAsStream(STANDARD_POWERSHELL_TEMPLATE)) { 373 if (templateStream != null) { 374 return Util.readInputStream(templateStream); 375 } 376 } 377 return null; 378 } 379 380 /** 381 * Returns true if the RPCService in this instance of IIQ supports TLS. 382 * 383 * The TLS four and five-argument constructors to IQService are available 384 * in the following IIQ versions and higher: 385 * 386 * - 8.0 GA 387 * - 7.3p3 388 * - 7.2p4 389 * - 7.1p7 390 * 391 * This result is cached. 392 * 393 * @return True if it supports TLS, false otherwise 394 */ 395 public static boolean supportsTLS() { 396 return supportsTLS.get(); 397 } 398 399 /** 400 * Wraps Powershell commands into a Rule, substituting it into a template wrapper. 401 * This {@link Rule} should not be saved, but should be serialized directly with 402 * the input to the RPCService. 403 * 404 * @param commands The commands to execute in Powershell 405 * @param ruleTextTemplate The rule text template, or null to use the included default 406 * @return The resulting Rule object 407 * @throws GeneralException if constructing the new Rule fails 408 */ 409 public static Rule wrapPowershellCommands(String commands, String ruleTextTemplate) throws GeneralException { 410 String finalRuleText; 411 try { 412 String template = ruleTextTemplate; 413 if (Util.isNullOrEmpty(template)) { 414 template = getStandardTemplate(); 415 } 416 if (Util.isNullOrEmpty(template)) { 417 throw new IllegalArgumentException("No Powershell template is available"); 418 } 419 finalRuleText = template.replace(TOKEN_USER_COMMAND, commands); 420 } catch(IOException e) { 421 throw new GeneralException("Could not read Powershell template", e); 422 } 423 if (Util.isNotNullOrEmpty(finalRuleText)) { 424 Rule pretendPowershellRule = new Rule(); 425 pretendPowershellRule.setName("Powershell Rule Template Wrapper " + System.currentTimeMillis()); 426 pretendPowershellRule.setSource(finalRuleText); 427 pretendPowershellRule.setAttribute("ObjectOrientedScript", "true"); 428 pretendPowershellRule.setAttribute("disabled", "false"); 429 pretendPowershellRule.setAttribute("extension", ".ps1"); 430 pretendPowershellRule.setAttribute("program", "powershell.exe"); 431 pretendPowershellRule.setAttribute("powershellTimeout", 10); 432 433 if (log.isTraceEnabled()) { 434 log.trace("Transformed commands into Rule: " + pretendPowershellRule.toXml()); 435 } 436 437 return pretendPowershellRule; 438 } else { 439 throw new GeneralException(new IllegalStateException("The final generated Powershell rule text is null or empty; is the template on the class path?")); 440 } 441 } 442}