001package com.identityworksllc.iiq.common.integration; 002 003import com.identityworksllc.iiq.common.Utilities; 004import com.identityworksllc.iiq.common.connector.UnsupportedConnector; 005import org.apache.commons.logging.Log; 006import org.apache.commons.logging.LogFactory; 007import sailpoint.api.SailPointContext; 008import sailpoint.object.*; 009import sailpoint.tools.GeneralException; 010import sailpoint.tools.Util; 011 012import java.util.ArrayList; 013import java.util.Collection; 014import java.util.HashMap; 015import java.util.List; 016import java.util.Map; 017 018/** 019 * Implements a no-op integration executor, suitable for detached apps. The 020 * integration will accept any inputs and return success. The result will 021 * contain the existing Link with the given changes applied, which makes 022 * it suitable for optimistic provisioning. 023 * 024 * Note that you could specify this integration executor only for specific 025 * operations on the configuration or allow it to be used for any. 026 * 027 * You can optionally choose to run the Application's before and after 028 * provisioning rules, which by default are not invoked by integration 029 * executors. 030 * 031 * TODO ensure that the Customization rule is invoked on the returning ResourceObject 032 */ 033public class NoOpIntegrationExecutor extends AbstractCommonIntegrationExecutor { 034 035 private static final Log log = LogFactory.getLog(NoOpIntegrationExecutor.class); 036 037 /** 038 * The native identity rule, if needed 039 */ 040 private Rule nativeIdentityRule; 041 042 /** 043 * If true, the after provisioning rule will be executed 044 */ 045 private boolean runAfterRule; 046 047 /** 048 * If true, the before provisioning rule will be executed 049 */ 050 private boolean runBeforeRule; 051 052 /** 053 * Constructs a new NoOpIntegrationExecutor 054 */ 055 public NoOpIntegrationExecutor() { 056 this.runBeforeRule = false; 057 this.runAfterRule = false; 058 } 059 060 /** 061 * Always return committed from checkStatus 062 * @param requestId Unclear 063 * @return A committed result 064 * @throws Exception on failures 065 */ 066 @Override 067 public ProvisioningResult checkStatus(String requestId) throws Exception { 068 ProvisioningResult result = new ProvisioningResult(); 069 result.setStatus(ProvisioningResult.STATUS_COMMITTED); 070 return result; 071 } 072 073 @Override 074 public void configure(SailPointContext context, IntegrationConfig config) throws Exception { 075 super.configure(context, config); 076 077 Attributes<String, Object> attributes = config.getAttributes(); 078 if (attributes != null) { 079 if (attributes.containsKey("runBeforeRule")) { 080 this.runBeforeRule = attributes.getBoolean("runBeforeRule"); 081 } 082 if (attributes.containsKey("runAfterRule")) { 083 this.runAfterRule = attributes.getBoolean("runAfterRule"); 084 } 085 if (attributes.containsKey("nativeIdentityRule")) { 086 String ruleName = attributes.getString("nativeIdentityRule"); 087 if (Util.isNotNullOrEmpty(ruleName)) { 088 this.nativeIdentityRule = context.getObject(Rule.class, ruleName); 089 if (this.nativeIdentityRule != null) { 090 this.nativeIdentityRule = Utilities.detach(context, this.nativeIdentityRule); 091 } 092 } 093 } 094 } 095 } 096 097 /** 098 * Runs a rule to create the native identity, given the AccountRequest. This is 099 * required whenever the application would normally receive the native ID from 100 * the target system on create, such as an Azure GUID. 101 * 102 * @param application The application being provisioned to 103 * @param accountRequest The account request 104 * @return The native ID to use for this account 105 * @throws GeneralException if anything fails 106 */ 107 private String createNativeIdentity(Application application, ProvisioningPlan.AccountRequest accountRequest) throws GeneralException { 108 if (this.nativeIdentityRule != null) { 109 Map<String, Object> ruleInputs = new HashMap<>(); 110 ruleInputs.put("request", accountRequest); 111 ruleInputs.put("application", application); 112 113 Object output = getContext().runRule(this.nativeIdentityRule, ruleInputs); 114 115 if (output instanceof String) { 116 return (String) output; 117 } else if (output != null) { 118 log.warn("Native Identity rule did not return a string object"); 119 } 120 } 121 122 return null; 123 } 124 125 /** 126 * Finds the existing Link in IIQ based on the account request and (if that doesn't 127 * work) the provisioning plan. The Link will be located by application and native ID 128 * first, and then by Link + Identity if needed. 129 * 130 * @param plan The provisioning plan 131 * @param acctReq The account request being provisioned 132 * @return The Link, if any, otherwise null 133 * @throws GeneralException if the AccountRequest matches more than one Link 134 */ 135 protected Link findExistingLink(ProvisioningPlan plan, ProvisioningPlan.AccountRequest acctReq) throws GeneralException { 136 Filter filter = Filter.and(Filter.eq("application.name", acctReq.getApplicationName()), Filter.eq("nativeIdentity", acctReq.getNativeIdentity())); 137 QueryOptions qo = new QueryOptions(); 138 qo.addFilter(filter); 139 List<Link> existingLinks = super.getContext().getObjects(Link.class, qo); 140 if (existingLinks == null || existingLinks.size() == 0) { 141 return null; 142 } else if (existingLinks.size() == 1) { 143 return existingLinks.get(0); 144 } else { 145 int originalSize = existingLinks.size(); 146 if (plan.getIdentity() != null) { 147 qo.addFilter(Filter.eq("identity.id", plan.getIdentity().getId())); 148 existingLinks = super.getContext().getObjects(Link.class, qo); 149 150 if (existingLinks == null || existingLinks.size() == 0) { 151 return null; 152 } else if (existingLinks.size() == 1) { 153 return existingLinks.get(0); 154 } 155 } 156 throw new GeneralException("The provisioning plan with app = " + acctReq.getApplicationName() + ", native identity = " + acctReq.getNativeIdentity() + " matches " + originalSize + " existing Links??"); 157 } 158 } 159 160 /** 161 * Generates the ResourceObject from the existing link and the account request. This 162 * will be returned back through the connector like most of the "real" connectors. 163 * @param existingLink The existing Link, if one exists, or null 164 * @param acctReq The account request that was just provisioned 165 * @return The resulting ResourceObject with the current state of the account 166 * @throws GeneralException if any failures occur 167 */ 168 @SuppressWarnings("unchecked") 169 protected ResourceObject generateResourceObject(Link existingLink, ProvisioningPlan.AccountRequest acctReq) throws GeneralException { 170 ResourceObject ro = new ResourceObject(); 171 Application application = acctReq.getApplication(getContext()); 172 if (acctReq.getOperation() != null && acctReq.getOperation() == ProvisioningPlan.AccountRequest.Operation.Delete) { 173 ro.setDelete(true); 174 ro.setRemove(true); 175 ro.setAttribute(application.getAccountSchema().getIdentityAttribute(), acctReq.getNativeIdentity()); 176 } else { 177 String nativeIdentity = acctReq.getNativeIdentity(); 178 179 if (existingLink != null) { 180 ro.setAttributes(existingLink.getAttributes().mediumClone()); 181 if (Util.isNullOrEmpty(nativeIdentity) || Util.nullSafeEq("???", nativeIdentity)) { 182 nativeIdentity = existingLink.getNativeIdentity(); 183 } 184 } 185 186 if (Util.isNullOrEmpty(nativeIdentity) || Util.nullSafeEq("???", nativeIdentity)) { 187 nativeIdentity = createNativeIdentity(application, acctReq); 188 } 189 190 if (acctReq.getOperation() != null && acctReq.getOperation() == ProvisioningPlan.AccountRequest.Operation.Disable) { 191 ro.setAttribute("IIQDisabled", true); 192 } else if (acctReq.getOperation() != null && acctReq.getOperation() == ProvisioningPlan.AccountRequest.Operation.Enable) { 193 ro.setAttribute("IIQDisabled", false); 194 } 195 196 ro.setIdentity(nativeIdentity); 197 ro.setAttribute(application.getAccountSchema().getIdentityAttribute(), nativeIdentity); 198 199 if (acctReq.getAttributeRequests() != null) { 200 for(ProvisioningPlan.AttributeRequest attr : acctReq.getAttributeRequests()) { 201 if (attr.getOperation() == ProvisioningPlan.Operation.Set) { 202 ro.setAttribute(attr.getName(), attr.getValue()); 203 } else if (attr.getOperation() == ProvisioningPlan.Operation.Add) { 204 List<String> values = ro.getStringList(attr.getName()); 205 if (values == null) { 206 values = new ArrayList<>(); 207 } 208 if (attr.getValue() instanceof Collection) { 209 values.addAll((Collection<? extends String>) attr.getValue()); 210 } else { 211 String attrValue = Util.otoa(attr.getValue()); 212 if (Util.isNotNullOrEmpty(attrValue)) { 213 values.add(attrValue); 214 } 215 } 216 ro.setAttribute(attr.getName(), values); 217 } else if (attr.getOperation() == ProvisioningPlan.Operation.Remove) { 218 List<String> values = ro.getStringList(attr.getName()); 219 if (values == null) { 220 values = new ArrayList<>(); 221 } else { 222 values = new ArrayList<>(values); 223 } 224 if (attr.getValue() instanceof Collection) { 225 values.removeAll((Collection<? extends String>) attr.getValue()); 226 } else { 227 String attrValue = Util.otoa(attr.getValue()); 228 if (Util.isNotNullOrEmpty(attrValue)) { 229 values.remove(attrValue); 230 } 231 } 232 ro.setAttribute(attr.getName(), values); 233 } 234 } 235 } 236 } 237 if (log.isDebugEnabled()) { 238 log.debug("Returning ResourceObject " + ro.toXml()); 239 } 240 return ro; 241 } 242 243 /** 244 * Creates a new plan to use as a container for plans of a given application 245 * type. 246 * 247 * @param plan The existing plan to copy values from 248 * @param applicationName The application name 249 * @return The new plan 250 */ 251 private ProvisioningPlan newPlan(ProvisioningPlan plan, String applicationName) { 252 ProvisioningPlan appPlan = new ProvisioningPlan(); 253 if (plan.getArguments() != null) { 254 appPlan.setArguments(new Attributes<>(plan.getArguments())); 255 } 256 appPlan.setIdentity(plan.getIdentity()); 257 appPlan.setIntegrationData(plan.getIntegrationData()); 258 appPlan.setTargetIntegration(plan.getTargetIntegration()); 259 appPlan.setComments(plan.getComments()); 260 261 if (appPlan.getArguments() == null) { 262 appPlan.setArguments(new Attributes<>()); 263 } 264 appPlan.getArguments().put("noOpApplication", applicationName); 265 return appPlan; 266 } 267 268 /** 269 * Fakes invocation of the provisioning operations for each of the account requests in the 270 * given plan. The before and after provisioning rules may optionally be invoked. 271 * 272 * @param plan The plan being provisioned 273 * @return the provisioning result 274 * @throws GeneralException on failures 275 */ 276 @Override 277 public ProvisioningResult provision(ProvisioningPlan plan) throws GeneralException { 278 beforeProvision(plan); 279 ProvisioningResult result = new ProvisioningResult(); 280 281 if (plan.getAccountRequests() != null) { 282 Map<String, ProvisioningPlan> plansByApplication = new HashMap<>(); 283 for(ProvisioningPlan.AccountRequest acctReq : plan.getAccountRequests()) { 284 plansByApplication.computeIfAbsent(acctReq.getApplicationName(), (k) -> newPlan(plan, acctReq.getApplicationName())); 285 plansByApplication.get(acctReq.getApplicationName()).add(acctReq); 286 } 287 288 for(String app : plansByApplication.keySet()) { 289 ProvisioningPlan dividedPlan = plansByApplication.get(app); 290 if (this.runBeforeRule) { 291 runBeforeProvisioningRule(dividedPlan, app); 292 } 293 for(ProvisioningPlan.AccountRequest acctReq : Util.safeIterable(dividedPlan.getAccountRequests())) { 294 provision(dividedPlan, acctReq); 295 } 296 if (this.runAfterRule) { 297 runAfterProvisioningRule(dividedPlan, app); 298 } 299 } 300 } 301 result.setStatus(ProvisioningResult.STATUS_COMMITTED); 302 if (log.isDebugEnabled()) { 303 log.debug(result.toXml()); 304 } 305 afterProvision(plan, result); 306 return result; 307 } 308 309 /** 310 * Provisions the account request in question, calling the various hook and default 311 * implementation methods to perform the various steps. 312 * 313 * @param plan The provisioning plan 314 * @param acctReq The account request 315 * @throws GeneralException if any failures occur 316 */ 317 private void provision(ProvisioningPlan plan, ProvisioningPlan.AccountRequest acctReq) throws GeneralException { 318 beforeProvisionAccount(plan, acctReq); 319 Link existingLink = findExistingLink(plan, acctReq); 320 ProvisioningResult accountResult = new ProvisioningResult(); 321 accountResult.setStatus(ProvisioningResult.STATUS_COMMITTED); 322 accountResult.setObject(generateResourceObject(existingLink, acctReq)); 323 acctReq.setResult(accountResult); 324 afterProvisionAccount(plan, acctReq); 325 } 326 327 /** 328 * Runs the Application's after-prov rule. These are run by default when the Connector's 329 * IntegrationExecutor is used, but in the context of another IntegrationExecutor, we 330 * need to run it manually. 331 * 332 * You will receive a 'connector' variable, but all method calls on it will result in 333 * an UnsupportedOperationException. 334 * 335 * Additionally, the variable 'noOpIntegration' will be present and set to Boolean.TRUE. 336 * 337 * @param plan The plan 338 * @param applicationName The application name 339 * @throws GeneralException on failures 340 */ 341 private void runAfterProvisioningRule(ProvisioningPlan plan, String applicationName) throws GeneralException { 342 Application application = getContext().getObject(Application.class, applicationName); 343 if (application != null && application.getAfterProvisioningRule() != null) { 344 Rule afterProvRule = getContext().getObject(Rule.class, application.getAfterProvisioningRule()); 345 if (afterProvRule != null) { 346 Map<String, Object> ruleInputs = new HashMap<>(); 347 ruleInputs.put("plan", plan); 348 ruleInputs.put("application", application); 349 ruleInputs.put("noOpIntegration", Boolean.TRUE); 350 ruleInputs.put("result", plan.getResult()); 351 ruleInputs.put("connector", new UnsupportedConnector()); 352 353 getContext().runRule(afterProvRule, ruleInputs); 354 } 355 } 356 } 357 358 /** 359 * Runs the Application's before-prov rule. These are run by default when the Connector's 360 * IntegrationExecutor is used, but in the context of another IntegrationExecutor, we 361 * need to run it manually. 362 * 363 * You will receive a 'connector' variable, but all method calls on it will result in 364 * an UnsupportedOperationException. 365 * 366 * Additionally, the variable 'noOpIntegration' will be present and set to Boolean.TRUE. 367 * 368 * @param plan The plan 369 * @param applicationName The application name 370 * @throws GeneralException on failures 371 */ 372 private void runBeforeProvisioningRule(ProvisioningPlan plan, String applicationName) throws GeneralException { 373 Application application = getContext().getObject(Application.class, applicationName); 374 if (application != null && application.getBeforeProvisioningRule() != null) { 375 Rule beforeProvRule = getContext().getObject(Rule.class, application.getBeforeProvisioningRule()); 376 if (beforeProvRule != null) { 377 Map<String, Object> ruleInputs = new HashMap<>(); 378 ruleInputs.put("plan", plan); 379 ruleInputs.put("application", application); 380 ruleInputs.put("noOpIntegration", Boolean.TRUE); 381 ruleInputs.put("connector", new UnsupportedConnector()); 382 383 getContext().runRule(beforeProvRule, ruleInputs); 384 } 385 } 386 } 387 388 389}