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}