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