001package com.identityworksllc.iiq.common;
002
003import java.lang.reflect.InvocationTargetException;
004import java.lang.reflect.Method;
005import java.util.ArrayList;
006import java.util.Arrays;
007import java.util.HashMap;
008import java.util.List;
009import java.util.Map;
010import java.util.Objects;
011import java.util.stream.Collectors;
012
013import sailpoint.api.DynamicScopeMatchmaker;
014import sailpoint.api.EntitlementCorrelator;
015import sailpoint.api.IdentityService;
016import sailpoint.api.Matchmaker;
017import sailpoint.api.SailPointContext;
018import sailpoint.api.SailPointFactory;
019import sailpoint.object.Bundle;
020import sailpoint.object.CompoundFilter;
021import sailpoint.object.DynamicScope;
022import sailpoint.object.Filter;
023import sailpoint.object.GroupDefinition;
024import sailpoint.object.Identity;
025import sailpoint.object.IdentityFilter;
026import sailpoint.object.IdentitySelector;
027import sailpoint.object.Link;
028import sailpoint.object.Profile;
029import sailpoint.object.QueryOptions;
030import sailpoint.object.SailPointObject;
031import sailpoint.object.Filter.BaseFilterVisitor;
032import sailpoint.persistence.HibernatePersistenceManager;
033import sailpoint.search.MapMatcher;
034import sailpoint.tools.GeneralException;
035
036/**
037 * Utilities for matching objects using IIQ APIs
038 */
039public class MatchUtilities extends AbstractBaseUtility {
040
041        /**
042         * Basic constructor
043         * @param c The IIQ context
044         */
045        public MatchUtilities(SailPointContext c) {
046                super(c);
047        }
048        
049        /**
050         * Returns the list of matching Link objects by application for this role. Each profile
051         * may match more than one Link. Each Link will only be returned once, even if it matches
052         * multiple profiles.
053         *
054         * Use {@link #identityMatchesSimpleProfiles(Bundle, Identity)} first to determine an overall
055         * match and then this method to extract the matches.
056         *
057         * The result will be empty if the role has no profiles or is null.
058         *
059         * @param role The role from which profiles should be checked
060         * @param identity The identity from which to extract account details
061         * @return A map of Application Name -> List of matching Links
062         * @throws GeneralException if any lookup failures occur
063         */
064        public Map<String, List<Link>> findMatchingLinksByApplication(Bundle role, Identity identity) throws GeneralException {
065                Map<String, List<Link>> results = new HashMap<>();
066                // Don't match an empty role
067                if (role == null || role.getProfiles() == null || role.getProfiles().isEmpty()) {
068                        return results;
069                }
070                IdentityService ids = new IdentityService(context);
071                for(Profile profile : role.getProfiles()) {
072                        List<Link> links = new ArrayList<>();
073                        if (profile.getApplication() != null) {
074                                links = ids.getLinks(identity, profile.getApplication());
075                        }
076                        if (links != null && links.size() > 0) {
077                                final String application = profile.getApplication().getName();
078                                for(Link link : links) {
079                                        boolean matched = linkMatchesProfile(profile, link);
080                                        if (matched) {
081                                                if (!results.containsKey(application)) {
082                                                        results.put(application, new ArrayList<>());
083                                                }
084                                                if (!results.get(application).contains(link)) {
085                                                        results.get(application).add(link);
086                                                }
087                                        }
088                                }
089                        }
090                }
091                return results;
092        }
093
094        /**
095         * Performs some reflection digging into the Sailpoint API to transform a standard
096         * {@link Filter} into a Hibernate HQL query.
097         * @param target The target filter to transform
098         * @param targetClass The target class (determines the queried tables)
099         * @param columns The columns to return
100         * @return The HQL query
101         * @throws GeneralException if any failures occur accessing the internal APIs
102         */
103        public String getFilterHQL(Filter target, Class<? extends SailPointObject> targetClass, String... columns) throws GeneralException {
104                SailPointContext privateSession = SailPointFactory.createPrivateContext();
105                try {
106                        HibernatePersistenceManager manager = HibernatePersistenceManager.getHibernatePersistenceManager(privateSession);
107                        Method visitHQLFilter = manager.getClass().getDeclaredMethod("visitHQLFilter", Class.class, QueryOptions.class, List.class);
108                        visitHQLFilter.setAccessible(true);
109                        try {
110                                manager.startTransaction();
111                                try {
112                                        QueryOptions qo = new QueryOptions();
113                                        qo.addFilter(target);
114                                        List<String> cols = new ArrayList<>(Arrays.asList(columns));
115                                        BaseFilterVisitor visitor = (BaseFilterVisitor) visitHQLFilter.invoke(manager, targetClass, qo, cols);
116                                        Method getQueryString = visitor.getClass().getDeclaredMethod("getQueryString");
117                                        getQueryString.setAccessible(true);
118                                        return (String)getQueryString.invoke(visitor);
119                                } catch(InvocationTargetException e) {
120                                        if (e.getTargetException() instanceof GeneralException) {
121                                                throw (GeneralException)e.getTargetException();
122                                        } else {
123                                                throw new GeneralException(e.getTargetException());
124                                        }
125                                } catch (IllegalAccessException | IllegalArgumentException e) {
126                                        throw new GeneralException(e);
127                                } finally {
128                                        manager.commitTransaction();
129                                }
130                        } finally {
131                                visitHQLFilter.setAccessible(false);
132                        }
133                } catch (NoSuchMethodException | SecurityException e1) {
134                        throw new GeneralException(e1);
135                } finally {
136                        SailPointFactory.releasePrivateContext(privateSession);
137                }
138        }
139
140        /**
141         * Returns true if the given identity has accounts matching all of the profiles on the given role
142         * @param role The role to check
143         * @param identity The identity whose links to check against the role
144         * @return True if the link matches all of the Bundle profiles
145         * @throws GeneralException if any failures occur
146         */
147        public boolean identityMatchesSimpleProfiles(Bundle role, Identity identity) throws GeneralException {
148                Objects.requireNonNull(identity, "Cannot match profiles against a null Identity");
149                int matchedCount = 0;
150                // Don't match an empty role
151                if (role == null || role.getProfiles() == null || role.getProfiles().isEmpty()) {
152                        return false;
153                }
154                IdentityService ids = new IdentityService(context);
155                for(Profile profile : role.getProfiles()) {
156                        List<Link> links = new ArrayList<>();
157                        if (profile.getApplication() != null) {
158                                links = ids.getLinks(identity, profile.getApplication());
159                        }
160                        if (links != null && links.size() > 0) {
161                                // Check each Link of the appropriate type against this Profile. The Profile may have multiple filters and they must ALL match to count.
162                                for(Link link : links) {
163                                        boolean matched = linkMatchesProfile(profile, link);
164                                        // If the link matched all filters, count it as a match toward this Profile
165                                        // and don't check any further Links.
166                                        if (matched) {
167                                                matchedCount++;
168                                                break;
169                                        }
170                                }
171                        }
172                }
173                if (role.isOrProfiles() && matchedCount > 1) {
174                        return true;
175                } else if (matchedCount == role.getProfiles().size()) {
176                        return true;
177                }
178                return false;
179        }
180        
181        /**
182         * Returns true if the given identity would be a member of the given IdentityFilter (e.g. used for searching)
183         * @param test The identity to test
184         * @param target The filter to check
185         * @param requestParameters Any parameters required by the filter script, if one exists
186         * @return true if the identity matches the filter criteria
187         * @throws GeneralException if any match failure occurs
188         */
189        public boolean identitySelectorMatches(Identity test, IdentityFilter target, Map<String, Object> requestParameters) throws GeneralException {
190                QueryOptions qo = target.buildQuery(requestParameters, context);
191                qo.addFilter(Filter.eq("Identity.id", test.getId()));
192                return (context.countObjects(Identity.class, qo) > 0);
193        }
194        
195        /**
196         * Returns true if the given identity would be a member of the given IdentityFilter (e.g. used for searching)
197         * @param test The identity to test
198         * @param selectorName The Identity Selector to query
199         * @param requestParameters Any parameters required by the filter script, if one exists
200         * @return true if the identity matches the filter criteria
201         * @throws GeneralException if any match failure occurs
202         */
203        public boolean identitySelectorMatches(Identity test, String selectorName, Map<String, Object> requestParameters) throws GeneralException {
204                IdentityService ids = new IdentityService(context);
205                Map<String, Object> suggestParams = new HashMap<String, Object>();
206                suggestParams.putAll(requestParameters);
207                suggestParams.put("suggestId", selectorName);
208                QueryOptions qo = ids.getIdentitySuggestQueryOptions(suggestParams, context.getObject(Identity.class, context.getUserName()));
209                qo.addFilter(Filter.eq("Identity.id", test.getId()));
210                return (context.countObjects(Identity.class, qo) > 0);
211        }
212        
213        /**
214         * Returns true if the given Profile matches the given Link via {@link MapMatcher}.
215         * @param profile The profile
216         * @param link The link
217         * @return true if the link matches the profile
218         * @throws GeneralException if any failures occur during matching (should never happen)
219         */
220        public boolean linkMatchesProfile(Profile profile, Link link) throws GeneralException {
221                boolean matched = true;
222                List<Filter> filters = profile.getConstraints();
223                if (filters != null) {
224                        for (Filter filter : filters) {
225                                MapMatcher matcher = new MapMatcher(filter);
226                                if (!matcher.matches(link.getAttributes())) {
227                                        matched = false;
228                                        break;
229                                }
230                        }
231                }
232                return matched;
233        }
234        
235        /**
236         * Returns true if the given link matches the profiles on the given role
237         * @param role The role to check
238         * @param link The link to check against the role
239         * @return True if the link matches all of the Bundle profiles
240         * @throws GeneralException if any failures occur
241         */
242        public boolean linkMatchesSimpleProfiles(Bundle role, Link link) throws GeneralException {
243                int matchedCount = 0;
244                // Don't match an empty role
245                if (role.getProfiles() == null || role.getProfiles().isEmpty()) {
246                        return false;
247                }
248                for(Profile profile : role.getProfiles()) {
249                        boolean matched = true;
250                        List<Filter> filters = profile.getConstraints();
251                        if (filters != null) {
252                                for (Filter filter : filters) {
253                                        MapMatcher matcher = new MapMatcher(filter);
254                                        if (!matcher.matches(link.getAttributes()))  {
255                                                matched = false;
256                                        }
257                                }
258                        }
259                        if (matched) {
260                                matchedCount++;
261                        }
262                }
263                if (role.isOrProfiles() && matchedCount > 0) {
264                        return true;
265                } else if (matchedCount == role.getProfiles().size()) {
266                        return true;
267                }
268                return false;
269        }
270        
271        /**
272         * Returns true if the given Identity would be a member of the target Bundle, either by assignment or detection
273         * @param target The role to check
274         * @param test The identity to test
275         * @return true if the identity matches the role criteria
276         * @throws GeneralException if any match failure occurs
277         */
278        public boolean matches(Bundle target, Identity test) throws GeneralException {
279                return matches(test, target);
280        }
281        
282        /**
283         * Returns true if the given Filter would by matched by the given Identity
284         * @param target The filter to run 
285         * @param test The Identity to test against the filter
286         * @return true if the identity matches the Filter
287         * @throws GeneralException if a query exception occurs
288         */
289        public boolean matches(Filter target, Identity test) throws GeneralException {
290                return matches(test, target);
291        }
292        
293        /**
294         * Returns true if the given Identity would be a member of the target Bundle, either by assignment or detection
295         * @param target The identity to test
296         * @param role The bundle to test
297         * @return true if the identity matches the role criteria
298         * @throws GeneralException if any match failure occurs
299         */
300        public boolean matches(Identity target, Bundle role) throws GeneralException {
301                boolean isAssignmentRole = true;
302                if (role.getSelector() == null) {
303                        isAssignmentRole = false;
304                        if (role.getProfiles() == null || role.getProfiles().isEmpty()) {
305                                throw new IllegalArgumentException("Role " + role.getName() + " has no selector and no profiles");
306                        }
307                }
308                if (isAssignmentRole) {
309                        // Business role needs assignment selector
310                        Matchmaker matcher = new Matchmaker(context);
311                        matcher.setArgument("iiqNoCompilationCache", "true");
312                        matcher.setArgument("roleName", role.getName());
313                        matcher.setArgument("identity", target);
314                        return matcher.isMatch(role.getSelector(), target);
315                } else {
316                        EntitlementCorrelator correlator = new EntitlementCorrelator(context);
317                        correlator.setDoRoleAssignment(true);
318                        correlator.setNoPersistentIdentity(true);
319                        correlator.analyzeIdentity(target);
320                        
321                        Identity dummyTarget = new Identity();
322                        correlator.saveDetectionAnalysis(dummyTarget);
323
324                        return dummyTarget.getDetectedRoles().stream().map(SailPointObject::getName).collect(Collectors.toList()).contains(role.getName());
325                }
326        }
327        
328        /**
329         * Returns true if the given identity would match the target filter
330         * @param target The target identity
331         * @param filter The compound filter to check
332         * @return True if the identity matches the filer
333         * @throws GeneralException if any match failure occurs
334         */
335        public boolean matches(Identity target, CompoundFilter filter) throws GeneralException {
336                IdentitySelector selector = new IdentitySelector();
337                selector.setFilter(filter);
338                return matches(target, selector);
339        }
340        
341        /**
342         * Returns true if the given identity matches the given Dynamic Scope
343         * @param target The target to check
344         * @param scope The scope to match against
345         * @return true if the identity matches the dynamic scope filter
346         * @throws GeneralException if any match failure occurs
347         */
348        public boolean matches(Identity target, DynamicScope scope) throws GeneralException {
349                DynamicScopeMatchmaker matchmaker = new DynamicScopeMatchmaker(context);
350                return matchmaker.isMatch(scope, target);
351        }
352        
353        /**
354         * Returns true if the given identity would match the target filter
355         * @param target The target identity
356         * @param filter The compound filter to check
357         * @return True if the identity matches the filer
358         * @throws GeneralException if any match failure occurs
359         */     
360        public boolean matches(Identity target, Filter filter) throws GeneralException {
361                CompoundFilter compound = new CompoundFilter();
362                compound.setFilter(filter);
363                return matches(target, compound);
364        }
365        
366        /**
367         * Returns true if the given identity would match the target population's filter
368         * @param target The target identity
369         * @param population The population to check
370         * @return True if the identity matches the filer
371         * @throws GeneralException if any match failure occurs
372         */
373        public boolean matches(Identity target, GroupDefinition population) throws GeneralException {
374                if (population.getFilter() == null) {
375                        throw new IllegalArgumentException("Population " + population.getName() + " has no filter defined");
376                }
377                IdentitySelector selector = new IdentitySelector();
378                selector.setPopulation(population);
379                return matches(target, selector);
380        }
381        
382        /**
383         * Returns true if the given identity would match the target filter
384         * @param target The target identity
385         * @param selector The selector / filter to check
386         * @return True if the identity matches the filer
387         * @throws GeneralException if any match failure occurs
388         */     
389        public boolean matches(Identity target, IdentitySelector selector) throws GeneralException {
390                Matchmaker matcher = new Matchmaker(context);
391                matcher.setArgument("iiqNoCompilationCache", "true");
392                matcher.setArgument("identity", target);
393                return matcher.isMatch(selector, target);
394        }
395        
396        /**
397         * Returns true if the given identity would match the target filter
398         * @param target The target identity
399         * @param filter The filter string to compile and check
400         * @return True if the identity matches the filer
401         * @throws GeneralException if any match failure occurs
402         */     
403        public boolean matches(Identity target, String filter) throws GeneralException {
404                Filter f = Filter.compile(filter);
405                return matches(target, f);
406        }
407        
408        /**
409         * Returns true if the given identity would match the target filter
410         * @param target The target selector
411         * @param test The identity to examine
412         * @return True if the identity matches the filer
413         * @throws GeneralException if any match failure occurs
414         */     
415        public boolean matches(IdentitySelector target, Identity test) throws GeneralException {
416                return matches(test, target);
417        }
418
419}