001package com.identityworksllc.iiq.common;
002
003import sailpoint.api.IdentityService;
004import sailpoint.api.SailPointContext;
005import sailpoint.object.*;
006import sailpoint.tools.GeneralException;
007import sailpoint.tools.ObjectNotFoundException;
008import sailpoint.tools.Util;
009
010import javax.validation.constraints.Null;
011import java.util.ArrayList;
012import java.util.Collection;
013import java.util.HashMap;
014import java.util.List;
015import java.util.Map;
016import java.util.Objects;
017import java.util.Optional;
018
019/**
020 * A utility class for efficiently reading various types of information from
021 * Link objects. This class supplements IdentityService by providing additional
022 * methods for retrieving attributes of various types.
023 *
024 * This is very common logic in virtually all IIQ instances, and this will prevent
025 * us from having to reimplement it for both Beanshell and Java every time. It
026 * should also increase efficiency by being in compiled Java and not Beanshell.
027 *
028 * There are essentially two modes of operation, depending on your purposes. If
029 * you need to pre-load all Identity Links prior to running an operation, set the
030 * forceLoad flag to true using {@link #setForceLoad(boolean)}. If you do not do
031 * this, this class will echo the logic used by {@link IdentityService#getLinks(Identity, Application)}.
032 *
033 * You will generally NOT want to set forceLoad=true unless you need to repeatedly
034 * query the same Links from the Identity. The most notable example is when you
035 * are reading values from Links in more than a handful of IdentityAttribute rules.
036 *
037 * In all cases, the get-attribute methods take the 'failOnMultiple' flag into
038 * account. If the flag is false, as is default, the value from the newest Link
039 * will be retrieved.
040 */
041public class IdentityLinkUtil {
042
043    /**
044     * Finds a unique Link by Native ID and Application, returning a non-null Optional
045     * @param context The context for querying
046     * @param applicationName The application name
047     * @param nativeIdentity The native ID
048     * @return If no matches, an empty Optional. If one match, an Optional containing the Link
049     * @throws GeneralException if there is a query failure
050     * @throws TooManyResultsException if more than one Link matches the criteria
051     */
052    public static Optional<Link> findUniqueLink(SailPointContext context, String applicationName, String nativeIdentity) throws GeneralException {
053        QueryOptions qo = new QueryOptions();
054        Filter theFilter = getLinkFilter(applicationName, nativeIdentity);
055
056        qo.addFilter(theFilter);
057
058        List<Link> links = context.getObjects(Link.class, qo);
059        if (links == null || links.size() == 0) {
060            return Optional.empty();
061        } else if (links.size() == 1) {
062            return Optional.of(links.get(0));
063        } else {
064            throw new TooManyResultsException(Link.class, theFilter.getExpression(true), links.size());
065        }
066    }
067
068    /**
069     * Returns a Filter object for a Link
070     * @param applicationName The application name
071     * @param nativeIdentity The native ID
072     * @return the resulting Filter
073     */
074    public static Filter getLinkFilter(String applicationName, String nativeIdentity) {
075        return Filter.and(
076                Filter.eq("application.name", applicationName),
077                Filter.eq("nativeIdentity", nativeIdentity)
078        );
079    }
080
081    /**
082     * Gets a unique Link by Native ID and Application or else throws an exception
083     * @param context The context for querying
084     * @param applicationName The application name
085     * @param nativeIdentity The native ID
086     * @return Null if no matches, a single Link if there is a match
087     * @throws GeneralException if there is a query failure
088     * @throws TooManyResultsException if more than one Link matches the criteria
089     */
090    public static Link getUniqueLink(SailPointContext context, String applicationName, String nativeIdentity) throws GeneralException {
091        QueryOptions qo = new QueryOptions();
092        Filter theFilter = getLinkFilter(applicationName, nativeIdentity);
093
094        qo.addFilter(theFilter);
095
096        List<Link> links = context.getObjects(Link.class, qo);
097        if (links == null || links.size() == 0) {
098            return null;
099        } else if (links.size() == 1) {
100            return links.get(0);
101        } else {
102            throw new TooManyResultsException(Link.class, theFilter.getExpression(true), links.size());
103        }
104    }
105    /**
106     * The Sailpoint context
107     */
108    private final SailPointContext context;
109    /**
110     * If true, the various get-attribute methods will fail if the user has more
111     * than one of the same type.
112     */
113    private boolean failOnMultiple;
114    /**
115     * If true, the identity's Links will be forcibly loaded by calling load()
116     * on the whole collection before running any operation. This will make
117     * subsequent operations on the same object in the same session potentially
118     * faster. You also will want to use this option in an Identity Attribute
119     * rule, as those may be invoked before the Identity or Link is persisted.
120     */
121    private boolean forceLoad;
122    /**
123     * The global link filter, to be applied to any queries for a Link by application
124     */
125    private Filter globalLinkFilter;
126    /**
127     * The Identity associated with this utility
128     */
129    private final Identity identity;
130
131    /**
132     * Identity Link utility constructor
133     * @param context The Sailpoint context
134     * @param identity the Identity
135     */
136    public IdentityLinkUtil(SailPointContext context, Identity identity) {
137        this(context, identity, null);
138    }
139
140    /**
141     * Identity Link utility constructor
142     * @param context The Sailpoint context
143     * @param identity the Identity
144     */
145    public IdentityLinkUtil(SailPointContext context, Identity identity, Filter globalLinkFilter) {
146        this.context = Objects.requireNonNull(context);
147        this.identity = Objects.requireNonNull(identity);
148        this.forceLoad = false;
149        this.failOnMultiple = false;
150        this.globalLinkFilter = globalLinkFilter;
151    }
152
153    /**
154     * Iterates over the list of Links on this Identity and loads them all
155     */
156    private void checkLoaded() {
157        Iterable<Link> links = Util.safeIterable(identity.getLinks());
158        for (Link l : links) {
159            l.load();
160        }
161    }
162
163    /**
164     * Retrieves a managed attribute for the given IdentityEntitlement
165     * @param ie The IdentityEntitlement
166     * @return The associated managed attribute, or an empty optional
167     * @throws GeneralException If the query fails for some reason
168     * @throws TooManyResultsException If the entitlement matches more than 1 managed attribute
169     */
170    public Optional<ManagedAttribute> findManagedAttribute(IdentityEntitlement ie) throws GeneralException {
171        if (ie == null) {
172            throw new NullPointerException("IdentityEntitlement");
173        }
174        QueryOptions qo = new QueryOptions();
175        qo.addFilter(Filter.eq("application.name", ie.getApplication().getName()));
176        qo.addFilter(Filter.eq("attribute", ie.getName()));
177        qo.addFilter(Filter.eq("value", ie.getValue()));
178
179        List<ManagedAttribute> managedAttributes = context.getObjects(ManagedAttribute.class, qo);
180
181        if (managedAttributes == null || managedAttributes.size() == 0) {
182            return Optional.empty();
183        } else if (managedAttributes.size() == 1) {
184            return Optional.of(managedAttributes.get(0));
185        } else {
186            throw new TooManyResultsException(ManagedAttribute.class, qo.toString(), managedAttributes.size());
187        }
188    }
189
190    /**
191     * Retrieves all ManagedAttributes associated with the given Link
192     * @param link the Link to check
193     * @return A map from field name to a list of ManagedAttribute objects
194     * @throws GeneralException If the query fails for some reason
195     * @throws TooManyResultsException If the entitlement matches more than 1 managed attribute
196     */
197    public Map<String, List<ManagedAttribute>> findManagedAttributes(Link link) throws GeneralException {
198        if (link == null || link.getAttributes() == null) {
199            throw new NullPointerException("Link or Link.attributes is null");
200        }
201
202        Map<String, List<ManagedAttribute>> result = new HashMap<>();
203
204        String appName = link.getApplicationName();
205
206        @SuppressWarnings("unchecked")
207        Attributes<String, Object> entitlementAttributes = link.getEntitlementAttributes();
208
209        for(String fieldName : entitlementAttributes.getKeys()) {
210            List<String> values = Util.otol(entitlementAttributes.get(fieldName));
211            result.put(fieldName, new ArrayList<>());
212
213            for(String value : values) {
214                QueryOptions qo = new QueryOptions();
215                qo.addFilter(Filter.eq("application.name", appName));
216                qo.addFilter(Filter.eq("attribute", fieldName));
217                qo.addFilter(Filter.eq("value", value));
218
219                List<ManagedAttribute> managedAttributes = context.getObjects(ManagedAttribute.class, qo);
220
221                if (managedAttributes != null && managedAttributes.size() > 0) {
222                    if (managedAttributes.size() == 1) {
223                        result.get(fieldName).add(managedAttributes.get(0));
224                    } else {
225                        throw new TooManyResultsException(ManagedAttribute.class, qo.toString(), managedAttributes.size());
226                    }
227                } // else { no match, ignore it }
228            }
229        }
230
231        return result;
232    }
233
234    /**
235     * Gets the applied (possibly null) global link filter
236     * @return The applied global link filter
237     */
238    public Filter getGlobalLinkFilter() {
239        return globalLinkFilter;
240    }
241
242    /**
243     * Gets the Link from the Identity by native identity
244     * @param application The application type of the Link
245     * @param nativeIdentity The native identity of the Link
246     * @return The Link
247     * @throws GeneralException if any failures occur
248     */
249    public Link getLinkByNativeIdentity(Application application, String nativeIdentity) throws GeneralException {
250        if (forceLoad) {
251            checkLoaded();
252        }
253
254        IdentityService ids = new IdentityService(context);
255        return ids.getLink(identity, application, null, nativeIdentity);
256    }
257
258    /**
259     * @see #getLinkByNativeIdentity(Application, String)
260     */
261    public Link getLinkByNativeIdentity(String applicationName, String nativeIdentity) throws GeneralException {
262        Application application = context.getObject(Application.class, applicationName);
263
264        if (application == null) {
265            throw new ObjectNotFoundException(Application.class, applicationName);
266        }
267
268        return getLinkByNativeIdentity(application, nativeIdentity);
269    }
270
271    /**
272     * @see #getLinksByApplication(Application, Filter)
273     */
274    public List<Link> getLinksByApplication(Application application) throws GeneralException {
275        return getLinksByApplication(application, null);
276    }
277
278    /**
279     * Gets the list of Links of the given application type, applying the given optional
280     * filter to the links. If a filter is present, only Links matching the filter will be
281     * returned.
282     *
283     * @param application The application object
284     * @param linkFilter The filter object, optional
285     * @return A non-null list of links (optionally filtered) on this user of the given application type
286     * @throws GeneralException if any failures occur
287     */
288    public List<Link> getLinksByApplication(Application application, Filter linkFilter) throws GeneralException {
289        if (forceLoad) {
290            checkLoaded();
291        }
292
293        IdentityService ids = new IdentityService(context);
294        List<Link> links = ids.getLinks(identity, application);
295
296        if (links == null) {
297            links = new ArrayList<>();
298        } else {
299            // Ensure that the list is mutable and detached from the Identity
300            links = new ArrayList<>(links);
301        }
302        Filter finalFilter = null;
303
304        if (this.globalLinkFilter != null && linkFilter != null) {
305            finalFilter = Filter.and(this.globalLinkFilter, linkFilter);
306        } else if (this.globalLinkFilter != null) {
307            finalFilter = this.globalLinkFilter;
308        } else if (linkFilter != null) {
309            finalFilter = linkFilter;
310        }
311
312        if (finalFilter != null) {
313            List<Link> newList = new ArrayList<>();
314            HybridObjectMatcher matcher = new HybridObjectMatcher(context, finalFilter);
315            for(Link l : links) {
316                if (matcher.matches(l)) {
317                    newList.add(l);
318                }
319            }
320            links = newList;
321        }
322
323        return links;
324    }
325
326    /**
327     * @see #getLinksByApplication(Application, Filter)
328     */
329    public List<Link> getLinksByApplication(String applicationName) throws GeneralException {
330        Application application = context.getObject(Application.class, applicationName);
331
332        if (application == null) {
333            throw new ObjectNotFoundException(Application.class, applicationName);
334        }
335
336        return getLinksByApplication(application);
337    }
338
339    /**
340     * @see #getLinksByApplication(Application, Filter)
341     */
342    public List<Link> getLinksByApplication(String applicationName, Filter linkFilter) throws GeneralException {
343        Application application = context.getObject(Application.class, applicationName);
344
345        if (application == null) {
346            throw new ObjectNotFoundException(Application.class, applicationName);
347        }
348
349        return getLinksByApplication(application, linkFilter);
350    }
351
352    /**
353     * @see #getMultiValueLinkAttribute(Application, String, Filter)
354     */
355    public List<String> getMultiValueLinkAttribute(String applicationName, String attributeName) throws GeneralException {
356        return getMultiValueLinkAttribute(applicationName, attributeName, null);
357    }
358
359    /**
360     * @see #getMultiValueLinkAttribute(Application, String, Filter)
361     */
362    public List<String> getMultiValueLinkAttribute(String applicationName, String attributeName, Filter linkFilter) throws GeneralException {
363        Application application = context.getObject(Application.class, applicationName);
364
365        if (application == null) {
366            throw new ObjectNotFoundException(Application.class, applicationName);
367        }
368
369        return getMultiValueLinkAttribute(application, attributeName, linkFilter);
370    }
371
372    /**
373     * Gets the value of a multi-valued attribute from one Link of the given type
374     * belonging to this Identity. The actual type of the attribute doesn't matter.
375     * A CSV single-valued String will be converted to a List here.
376     *
377     * @param application The application type of the Links
378     * @param attributeName The attribute name to grab
379     * @param linkFilter The Link filter, optional
380     * @return The value of the attribute, or null
381     * @throws GeneralException if any errors occur
382     */
383    public List<String> getMultiValueLinkAttribute(Application application, String attributeName, Filter linkFilter) throws GeneralException {
384        List<Link> links = getLinksByApplication(application, linkFilter);
385
386        if (Util.isEmpty(links)) {
387            return null;
388        } else if (links.size() == 1) {
389            Object value = links.get(0).getAttribute(attributeName);
390            return Util.otol(value);
391        } else {
392            if (failOnMultiple) {
393                throw new GeneralException("Too many accounts of type " + application.getName());
394            } else {
395                SailPointObjectDateSorter.sort(links);
396                Object value = links.get(0).getAttribute(attributeName);
397                return Util.otol(value);
398            }
399        }
400    }
401
402    /**
403     * @see #getMultiValueLinkAttribute(Application, String, Filter)
404     */
405    public List<String> getMultiValueLinkAttribute(Application application, String attributeName) throws GeneralException {
406        return getMultiValueLinkAttribute(application, attributeName, null);
407    }
408
409    /**
410     * @see #getSingleValueLinkAttribute(Application, String, Filter)
411     */
412    public String getSingleValueLinkAttribute(String applicationName, String attributeName) throws GeneralException {
413        Application application = context.getObject(Application.class, applicationName);
414
415        if (application == null) {
416            throw new ObjectNotFoundException(Application.class, applicationName);
417        }
418
419        return getSingleValueLinkAttribute(application, attributeName, null);
420    }
421
422    /**
423     * @see #getSingleValueLinkAttribute(Application, String, Filter)
424     */
425    public String getSingleValueLinkAttribute(String applicationName, String attributeName, Filter linkFilter) throws GeneralException {
426        Application application = context.getObject(Application.class, applicationName);
427
428        if (application == null) {
429            throw new ObjectNotFoundException(Application.class, applicationName);
430        }
431
432        return getSingleValueLinkAttribute(application, attributeName, linkFilter);
433    }
434
435    /**
436     * @see #getSingleValueLinkAttribute(Application, String, Filter)
437     */
438    public String getSingleValueLinkAttribute(Application application, String attributeName) throws GeneralException {
439        return getSingleValueLinkAttribute(application, attributeName, null);
440    }
441
442    /**
443     * Gets the value of a single-valued attribute from one Link of the given type
444     * belonging to this Identity.
445     *
446     * @param application The application type of the Links
447     * @param attributeName The attribute name to grab
448     * @param linkFilter The Link filter, optional
449     * @return The value of the attribute, or null
450     * @throws GeneralException if any errors occur
451     */
452    public String getSingleValueLinkAttribute(Application application, String attributeName, Filter linkFilter) throws GeneralException {
453        List<Link> links = getLinksByApplication(application, linkFilter);
454
455        if (Util.isEmpty(links)) {
456            return null;
457        } else if (links.size() == 1) {
458            Object value = links.get(0).getAttribute(attributeName);
459            return Util.otoa(value);
460        } else {
461            if (failOnMultiple) {
462                throw new GeneralException("Too many accounts of type " + application.getName());
463            } else {
464                SailPointObjectDateSorter.sort(links);
465                Object value = links.get(0).getAttribute(attributeName);
466                return Util.otoa(value);
467            }
468        }
469    }
470
471    /**
472     * Returns true if the class is set to fail on multiple Links of the same type
473     * @see #failOnMultiple
474     */
475    public boolean isFailOnMultiple() {
476        return failOnMultiple;
477    }
478
479    /**
480     * Returns true if you want to force-load all Links on the Identity using {@link Identity#getLinks()},
481     * rather than using {@link IdentityService}
482     * @see #forceLoad
483     */
484    public boolean isForceLoad() {
485        return forceLoad;
486    }
487
488    /**
489     * @see #mergeLinkAttributes(Application, String, Filter)
490     */
491    public List<String> mergeLinkAttributes(String applicationName, String attributeName) throws GeneralException {
492        Application application = context.getObject(Application.class, applicationName);
493
494        if (application == null) {
495            throw new ObjectNotFoundException(Application.class, applicationName);
496        }
497
498        return mergeLinkAttributes(application, attributeName, null);
499    }
500
501    /**
502     * @see #mergeLinkAttributes(Application, String, Filter)
503     */
504    public List<String> mergeLinkAttributes(String applicationName, String attributeName, Filter linkFilter) throws GeneralException {
505        Application application = context.getObject(Application.class, applicationName);
506
507        if (application == null) {
508            throw new ObjectNotFoundException(Application.class, applicationName);
509        }
510
511        return mergeLinkAttributes(application, attributeName, linkFilter);
512    }
513
514    /**
515     * Extracts the named attribute from each Link of the given application and adds
516     * all values from each Link into a common List.
517     *
518     * @param application The application to query
519     * @param attributeName The attribute name to query
520     * @param linkFilter The link filter, optional
521     * @return The merged set of attributes from each application
522     * @throws GeneralException if any failures occur
523     */
524    public List<String> mergeLinkAttributes(Application application, String attributeName, Filter linkFilter) throws GeneralException {
525        List<Link> links = getLinksByApplication(application, linkFilter);
526
527        boolean isMultiValued = false;
528
529        Schema accountSchema = application.getAccountSchema();
530        if (accountSchema != null) {
531            AttributeDefinition attributeDefinition = accountSchema.getAttributeDefinition(attributeName);
532            if (attributeDefinition != null) {
533                isMultiValued = attributeDefinition.isMultiValued();
534            }
535        }
536
537        List<String> values = new ArrayList<>();
538        for(Link l : links) {
539            Object value = l.getAttribute(attributeName);
540            if (isMultiValued) {
541                value = Util.otol(value);
542            } else {
543                value = Util.otoa(value);
544            }
545
546            if (value instanceof String) {
547                values.add((String)value);
548            } else if (value instanceof Collection) {
549                @SuppressWarnings("unchecked")
550                Collection<String> c = (Collection<String>)value;
551                values.addAll(c);
552            }
553        }
554        return values;
555    }
556
557    /**
558     * If true, and the Identity has more than one (post-filter) Link of a given
559     * Application type, the get-attribute methods will throw an exception.
560     *
561     * @param failOnMultiple True if we should fail on multiple accounts
562     */
563    public void setFailOnMultiple(boolean failOnMultiple) {
564        this.failOnMultiple = failOnMultiple;
565    }
566
567    /**
568     * If true, the Identity's `links` container will be populated before searching for
569     * items. This will make the IdentityService faster in some circumstances, notably
570     * repeated queries of links in Identity Attributes.
571     *
572     * @param forceLoad True if we should always load the Link objects
573     */
574    public void setForceLoad(boolean forceLoad) {
575        this.forceLoad = forceLoad;
576    }
577
578    /**
579     * Sets a global link filter, allowing use of a constant
580     * @param globalLinkFilter The filter to apply to any operation
581     */
582    public void setGlobalLinkFilter(Filter globalLinkFilter) {
583        this.globalLinkFilter = globalLinkFilter;
584    }
585}