001package com.identityworksllc.iiq.common.access;
002
003import com.identityworksllc.iiq.common.*;
004import org.apache.commons.logging.Log;
005import org.apache.commons.logging.LogFactory;
006import sailpoint.api.DynamicScopeMatchmaker;
007import sailpoint.api.Matchmaker;
008import sailpoint.authorization.Authorizer;
009import sailpoint.authorization.UnauthorizedAccessException;
010import sailpoint.object.*;
011import sailpoint.plugin.PluginContext;
012import sailpoint.rest.plugin.BasePluginResource;
013import sailpoint.server.Environment;
014import sailpoint.tools.GeneralException;
015import sailpoint.tools.Util;
016import sailpoint.web.UserContext;
017
018import java.util.*;
019import java.util.concurrent.ConcurrentHashMap;
020import java.util.function.Supplier;
021
022/**
023 * Static methods for implementing access checks. This is used directly by {@link ThingAccessUtils},
024 * but allows migration to this better interface.
025 *
026 * @author Devin Rosenbauer
027 * @author Instrumental Identity
028 */
029public final class AccessCheck {
030    /**
031     * The container object to hold the cached ThingAccessUtil results
032     */
033    public static final class SecurityResult implements Supplier<Optional<AccessCheckResponse>> {
034        /**
035         * The epoch millisecond timestamp when this object expires, one minute after creation
036         */
037        private final long expiration;
038
039        /**
040         * The actual cached result
041         */
042        private final Object result;
043
044        /**
045         * Store the result with an expiration time
046         * @param result The result to cache
047         */
048        public SecurityResult(AccessCheckResponse result) {
049            this.result = result;
050            this.expiration = System.currentTimeMillis() + (1000L * 60);
051        }
052
053        /**
054         * Returns the cached result
055         * @return The cached result
056         */
057        public Optional<AccessCheckResponse> get() {
058            if (this.result instanceof AccessCheckResponse && !this.isExpired()) {
059                return Optional.of((AccessCheckResponse) this.result);
060            } else {
061                return Optional.empty();
062            }
063        }
064
065        /**
066         * Returns true if the current epoch timestamp is later than the expiration date
067         * @return True if expired
068         */
069        private boolean isExpired() {
070            return System.currentTimeMillis() >= expiration;
071        }
072    }
073
074    /**
075     * The container object to identify the cached ThingAccessUtil inputs.
076     *
077     * NOTE: It is very important that this work properly across plugin
078     * classloader contexts, even if the plugin has its own version of
079     * ThingAccessUtils.
080     */
081    public static final class SecurityCacheToken {
082        /**
083         * The CommonSecurityConfig object associated with the cached result
084         */
085        private final Map<String, Object> commonSecurityConfig;
086
087        /**
088         * The version of the plugin cache to invalidate records whenever
089         * a new plugin is installed. This will prevent wacky class cast
090         * problems.
091         */
092        private final int pluginVersion;
093
094        /**
095         * The name of the source identity
096         */
097        private final String source;
098
099        /**
100         * The optional state map
101         */
102        private final Map<String, Object> state;
103
104        /**
105         * The name of the target identity
106         */
107        private final String target;
108
109        /**
110         * Constructs a new cache entry
111         * @param csc The security config
112         * @param source The source identity name
113         * @param target The target identity name
114         * @param state The state of the security operation
115         */
116        public SecurityCacheToken(CommonSecurityConfig csc, String source, String target, Map<String, Object> state) {
117            this.commonSecurityConfig = csc.toMap();
118            this.target = target;
119            this.source = source;
120            this.state = new HashMap<>();
121
122            if (state != null) {
123                this.state.putAll(state);
124            }
125
126            this.pluginVersion = Environment.getEnvironment().getPluginsCache().getVersion();
127        }
128
129        /**
130         * Constructs a new cache entry based on the input
131         * @param input The input object
132         * @throws GeneralException the errors
133         */
134        public SecurityCacheToken(AccessCheckInput input) throws GeneralException {
135            this(
136                    input.getConfiguration(),
137                    input.getUserContext().getLoggedInUserName(),
138                    (input.getTarget() == null || input.getTarget().getName() == null) ? "null" : input.getTarget().getName(),
139                    input.getState()
140            );
141        }
142
143        @Override
144        public boolean equals(Object o) {
145            if (this == o) return true;
146            if (o == null || getClass() != o.getClass()) return false;
147            SecurityCacheToken that = (SecurityCacheToken) o;
148            return this.pluginVersion == that.pluginVersion && Objects.equals(commonSecurityConfig, that.commonSecurityConfig) && Objects.equals(target, that.target) && Objects.equals(source, that.source) && Objects.equals(state, that.state);
149        }
150
151        @Override
152        public int hashCode() {
153            return Objects.hash(pluginVersion, commonSecurityConfig, target, source, state);
154        }
155    }
156    /**
157     * The access check name used for an anonymous input
158     */
159    public static final String ANONYMOUS_THING = "anonymous";
160    /**
161     * The cache key in CustomGlobal
162     */
163    private static final String CACHE_KEY = "idw.ThingAccessUtils.cache";
164
165    /**
166     * The logger
167     */
168    private static final Log log = LogFactory.getLog(AccessCheck.class);
169
170    /**
171     * Private constructor to prevent instantiation
172     */
173    private AccessCheck() {
174        /* Utility class cannot be instantiated */
175    }
176
177    /**
178     * Returns an 'allowed' response if the logged in user can access the item based on the
179     * common configuration parameters.
180     *
181     * Results for the same CommonSecurityConfig, source, and target user will be cached for up to one minute
182     * unless the CommonSecurityConfig object has noCache set to true.
183     *
184     * @param input The input containing the configuration for the checkThingAccess utility
185     * @return True if the user has access to the thing based on the configuration
186     */
187    public static AccessCheckResponse accessCheck(AccessCheckInput input) {
188        if (input.getConfiguration() == null) {
189            throw new IllegalArgumentException("An access check must contain a CommonSecurityConfig");
190        }
191
192        if (input.getUserContext() == null) {
193            throw new IllegalArgumentException("An access check must specify a UserContext for accessing the IIQ context and the logged in user");
194        }
195
196        AccessCheckResponse result;
197        try {
198            if (!input.getConfiguration().isNoCache()) {
199                SecurityCacheToken cacheToken = new SecurityCacheToken(input);
200                Optional<AccessCheckResponse> cachedResult = getCachedResult(cacheToken);
201                if (cachedResult.isPresent()) {
202                    return cachedResult.get();
203                }
204                result = accessCheckImpl(input);
205                getCacheMap().put(cacheToken, new SecurityResult(result));
206            } else {
207                result = accessCheckImpl(input);
208            }
209        } catch(Exception e) {
210            result = new AccessCheckResponse();
211            result.denyMessage("Caught an exception evaluating criteria: " + e.getMessage());
212            log.error("Caught an exception evaluating access criteria to " + input.getThingName(), e);
213        }
214        return result;
215    }
216
217    /**
218     * Returns an allowed response if the logged in user can access the item based on
219     * the common configuration parameters.
220     *
221     * @param input The inputs to the access check
222     * @return True if the user has access to the thing based on the configuration
223     * @throws GeneralException if any check failures occur (this should be interpreted as "no access")
224     */
225    private static AccessCheckResponse accessCheckImpl(final AccessCheckInput input) throws GeneralException {
226        AccessCheckResponse result = new AccessCheckResponse();
227        UserContext pluginContext = input.getUserContext();
228
229        final Identity currentUser = pluginContext.getLoggedInUser();
230        final Identity target = (input.getTarget() != null) ? input.getTarget() : currentUser;
231        final String currentUserName = pluginContext.getLoggedInUserName();
232        final CommonSecurityConfig config = input.getConfiguration();
233        final String thingName = input.getThingName();
234
235        Configuration systemConfig = Configuration.getSystemConfig();
236        boolean beanshellGetsPluginContext = systemConfig.getBoolean("IIQCommon.ThingAccessUtils.beanshellGetsPluginContext", false);
237
238        if (log.isTraceEnabled()) {
239            log.trace("START: Checking access for subject = " + currentUser.getName() + ", target = " + target.getName() + ", thing = " + thingName + ", config = " + config);
240        }
241
242        if (config.isDisabled()) {
243            result.denyMessage("Access denied to " + thingName + " because the configuration is marked disabled");
244        }
245        if (result.isAllowed() && Utilities.isNotEmpty(config.getOneOf())) {
246            boolean anyMatch = false;
247            for(CommonSecurityConfig sub : config.getOneOf()) {
248                AccessCheckInput child = new AccessCheckInput(input, sub);
249                AccessCheckResponse childResponse = accessCheckImpl(child);
250                if (childResponse.isAllowed()) {
251                    anyMatch = true;
252                    break;
253                }
254            }
255            if (!anyMatch) {
256                result.denyMessage("Access denied to " + thingName + " because none of the items in the 'oneOf' list resolved to true");
257            }
258        }
259        if (result.isAllowed() && Utilities.isNotEmpty(config.getAllOf())) {
260            boolean allMatch = true;
261            for(CommonSecurityConfig sub : config.getAllOf()) {
262                AccessCheckInput child = new AccessCheckInput(input, sub);
263                AccessCheckResponse childResponse = accessCheckImpl(child);
264                if (!childResponse.isAllowed()) {
265                    allMatch = false;
266                    break;
267                }
268            }
269            if (!allMatch) {
270                result.denyMessage("Access denied to " + thingName + " because at least one of the items in the 'allOf' list resolved to 'deny'");
271            }
272        }
273        if (result.isAllowed() && Utilities.isNotEmpty(config.getNot())) {
274            boolean anyMatch = false;
275            for(CommonSecurityConfig sub : config.getNot()) {
276                AccessCheckInput child = new AccessCheckInput(input, sub);
277                AccessCheckResponse childResponse = accessCheckImpl(child);
278                if (childResponse.isAllowed()) {
279                    anyMatch = true;
280                    break;
281                }
282            }
283            if (anyMatch) {
284                result.denyMessage("Access denied to " + thingName + " because at least one of the items in the 'not' list resolved to 'allow'");
285            }
286        }
287        if (result.isAllowed() && Util.isNotNullOrEmpty(config.getSettingOffSwitch()) && pluginContext instanceof PluginContext) {
288            boolean isDisabled = ((PluginContext) pluginContext).getSettingBool(config.getSettingOffSwitch());
289            if (isDisabled) {
290                result.denyMessage("Access denied to " + thingName + " because the feature " + config.getSettingOffSwitch() + " is disabled in plugin settings");
291            }
292        }
293        if (result.isAllowed() && config.getAccessCheckScript() != null && Util.isNotNullOrEmpty(config.getAccessCheckScript().getSource())) {
294            Script script = Utilities.getAsScript(config.getAccessCheckScript());
295            Map<String, Object> scriptArguments = new HashMap<>();
296            scriptArguments.put("subject", currentUser);
297            scriptArguments.put("target", target);
298            scriptArguments.put("requester", currentUser);
299            scriptArguments.put("identity", target);
300            scriptArguments.put("identityName", target.getName());
301            scriptArguments.put("manager", target.getManager());
302            scriptArguments.put("context", pluginContext.getContext());
303            scriptArguments.put("log", LogFactory.getLog(pluginContext.getClass()));
304            scriptArguments.put("state", input.getState());
305            if (beanshellGetsPluginContext && pluginContext instanceof BasePluginResource) {
306                scriptArguments.put("pluginContext", pluginContext);
307            } else {
308                scriptArguments.put("pluginContext", null);
309            }
310
311            Object output = pluginContext.getContext().runScript(script, scriptArguments);
312            // If the script returns a non-null value, it will be considered the authoritative
313            // response. No further checks will be done. If the output is null, the access
314            // checks will defer farther down.
315            if (output != null) {
316                boolean userAllowed = Util.otob(output);
317                if (!userAllowed) {
318                    result.denyMessage("Access denied to " + thingName + " because access check script returned false for subject user " + currentUserName);
319                }
320                return result;
321            }
322        }
323        if (result.isAllowed() && config.getAccessCheckRule() != null) {
324            Map<String, Object> scriptArguments = new HashMap<>();
325            scriptArguments.put("subject", currentUser);
326            scriptArguments.put("target", target);
327            scriptArguments.put("requester", currentUser);
328            scriptArguments.put("identity", target);
329            scriptArguments.put("identityName", target.getName());
330            scriptArguments.put("manager", target.getManager());
331            scriptArguments.put("context", pluginContext.getContext());
332            scriptArguments.put("log", LogFactory.getLog(pluginContext.getClass()));
333            scriptArguments.put("state", input.getState());
334            if (beanshellGetsPluginContext) {
335                scriptArguments.put("pluginContext", pluginContext);
336            } else {
337                scriptArguments.put("pluginContext", null);
338            }
339            if (log.isTraceEnabled()) {
340                log.trace("Running access check rule " + config.getAccessCheckRule().getName() + " for subject = " + currentUserName + ", target = " + target.getName());
341            }
342            Object output = pluginContext.getContext().runRule(config.getAccessCheckRule(), scriptArguments);
343            // If the rule returns a non-null value, it will be considered the authoritative
344            // response. No further checks will be done. If the output is null, the access
345            // checks will defer farther down.
346            if (output != null) {
347                boolean userAllowed = Util.otob(output);
348                if (!userAllowed) {
349                    result.denyMessage("Access denied to " + thingName + " because access check rule returned false for subject user " + currentUserName);
350                }
351                return result;
352            }
353        }
354        if (result.isAllowed() && !Util.isEmpty(config.getRequiredRights())) {
355            boolean userAllowed = false;
356            List<String> rights = config.getRequiredRights();
357            Collection<String> userRights = pluginContext.getLoggedInUserRights();
358            if (userRights != null) {
359                for(String right : Util.safeIterable(userRights)) {
360                    if (rights.contains(right)) {
361                        result.addMessage("Subject matched required SPRight: " + right);
362                        userAllowed = true;
363                        break;
364                    }
365                }
366            }
367            if (!userAllowed) {
368                result.denyMessage("Access denied to " + thingName + " because subject user " + currentUserName + " does not match any of the required rights " + rights);
369            }
370        }
371        if (result.isAllowed() && !Util.isEmpty(config.getRequiredCapabilities())) {
372            boolean userAllowed = false;
373            List<String> capabilities = config.getRequiredCapabilities();
374            List<Capability> loggedInUserCapabilities = pluginContext.getLoggedInUserCapabilities();
375            for(Capability cap : Util.safeIterable(loggedInUserCapabilities)) {
376                if (capabilities.contains(cap.getName())) {
377                    result.addMessage("Subject matched required capability: " + cap.getName());
378                    userAllowed = true;
379                    break;
380                }
381            }
382            if (!userAllowed) {
383                result.denyMessage("Access denied to " + thingName + " because subject user " + currentUserName + " does not match any of these required capabilities " + capabilities);
384            }
385        }
386        if (result.isAllowed() && !Util.isEmpty(config.getExcludedRights())) {
387            boolean userAllowed = true;
388            List<String> rights = config.getRequiredRights();
389            Collection<String> userRights = pluginContext.getLoggedInUserRights();
390            if (userRights != null) {
391                for(String right : Util.safeIterable(userRights)) {
392                    if (rights.contains(right)) {
393                        result.addMessage("Subject matched excluded SPRight: " + right);
394                        userAllowed = false;
395                        break;
396                    }
397                }
398            }
399            if (!userAllowed) {
400                result.denyMessage("Access denied to " + thingName + " because subject user " + currentUserName + " matches one of these excluded SPRights: " + rights);
401            }
402        }
403        if (result.isAllowed() && !Util.isEmpty(config.getExcludedCapabilities())) {
404            boolean userAllowed = true;
405            List<String> capabilities = config.getRequiredCapabilities();
406            List<Capability> loggedInUserCapabilities = pluginContext.getLoggedInUserCapabilities();
407            for(Capability cap : Util.safeIterable(loggedInUserCapabilities)) {
408                if (capabilities.contains(cap.getName())) {
409                    result.addMessage("Subject matched excluded Capability: " + cap.getName());
410                    userAllowed = false;
411                    break;
412                }
413            }
414            if (!userAllowed) {
415                result.denyMessage("Access denied to " + thingName + " because subject user " + currentUserName + " matches one of these excluded capabilities: " + capabilities);
416            }
417        }
418
419        if (result.isAllowed() && !Util.isEmpty(config.getExcludedWorkgroups())) {
420            List<String> workgroups = config.getExcludedWorkgroups();
421            boolean matchesWorkgroup = matchesAnyWorkgroup(currentUser, workgroups);
422            if (matchesWorkgroup) {
423                result.denyMessage("Access denied to " + thingName + " because subject user " + currentUserName + " is a member of an excluded workgroup in " + workgroups);
424            }
425        }
426        if (result.isAllowed() && !Util.isEmpty(config.getRequiredWorkgroups())) {
427            List<String> workgroups = config.getRequiredWorkgroups();
428            boolean userAllowed = matchesAnyWorkgroup(currentUser, workgroups);
429            if (!userAllowed) {
430                result.denyMessage("Access denied to " + thingName + " because subject user " + currentUserName + " does not match any of the required workgroups " + workgroups);
431            }
432        }
433        if (result.isAllowed() && Util.isNotNullOrEmpty(config.getAccessCheckFilter())) {
434            String filterString = config.getValidTargetFilter();
435            Filter compiledFilter = Filter.compile(filterString);
436
437            HybridObjectMatcher hom = new HybridObjectMatcher(pluginContext.getContext(), compiledFilter);
438
439            if (!hom.matches(currentUser)) {
440                result.denyMessage("Access denied to " + thingName + " because subject user " + currentUserName + " does not match the access check filter");
441            } else {
442                result.addMessage("Subject user matches filter: " + filterString);
443            }
444        }
445        if (result.isAllowed() && config.getAccessCheckSelector() != null) {
446            IdentitySelector selector = config.getAccessCheckSelector();
447            Matchmaker matchmaker = new Matchmaker(pluginContext.getContext());
448            if (!matchmaker.isMatch(selector, currentUser)) {
449                result.denyMessage("Access denied to " + thingName + " because subject user " + currentUserName + " does not match the access check selector");
450            } else {
451                result.addMessage("Subject user matches selector: " + selector.toXml());
452            }
453        }
454        if (result.isAllowed() && Util.isNotNullOrEmpty(config.getMirrorRole())) {
455            String role = config.getMirrorRole();
456            Bundle bundle = pluginContext.getContext().getObject(Bundle.class, role);
457            if (bundle.getSelector() == null && !Util.isEmpty(bundle.getProfiles())) {
458                if (log.isDebugEnabled()) {
459                    log.debug("Running mirrorRole access check on an IT role; this may have performance concerns");
460                }
461            }
462            MatchUtilities matchUtilities = new MatchUtilities(pluginContext.getContext());
463            if (!matchUtilities.matches(currentUser, bundle)) {
464                result.denyMessage("Access denied to " + thingName + " because subject user " + currentUserName + " does not match the selector or profile on role " + bundle.getName());
465            } else {
466                result.addMessage("Subject user matches role criteria: " + bundle.getName());
467            }
468        }
469        if (result.isAllowed() && Util.isNotNullOrEmpty(config.getMirrorQuicklinkPopulation())) {
470            String quicklinkPopulation = config.getMirrorQuicklinkPopulation();
471            if (Util.isNotNullOrEmpty(quicklinkPopulation)) {
472                DynamicScopeMatchmaker dynamicScopeMatchmaker = new DynamicScopeMatchmaker(pluginContext.getContext());
473                DynamicScope dynamicScope = pluginContext.getContext().getObject(DynamicScope.class, quicklinkPopulation);
474                boolean matchesDynamicScope = dynamicScopeMatchmaker.isMatch(dynamicScope, currentUser);
475                if (matchesDynamicScope) {
476                    result.addMessage("Subject user matches DynamicScope: " + quicklinkPopulation);
477                    DynamicScope.PopulationRequestAuthority populationRequestAuthority = dynamicScope.getPopulationRequestAuthority();
478                    if (populationRequestAuthority != null && !populationRequestAuthority.isAllowAll()) {
479                        matchesDynamicScope = dynamicScopeMatchmaker.isMember(currentUser, target, populationRequestAuthority);
480                        if (matchesDynamicScope) {
481                            result.addMessage("Target user matches DynamicScope: " + quicklinkPopulation);
482                        }
483                    }
484                }
485                if (!matchesDynamicScope) {
486                    result.denyMessage("Access denied to " + thingName + " because QuickLink population " + quicklinkPopulation + " does not match the subject and target");
487                }
488            }
489        }
490        if (result.isAllowed() && !Util.isEmpty(config.getValidTargetExcludedRights())) {
491            boolean userAllowed = true;
492            List<String> rights = config.getValidTargetExcludedRights();
493            Collection<String> userRights = target.getCapabilityManager().getEffectiveFlattenedRights();
494            if (userRights != null) {
495                for(String right : Util.safeIterable(userRights)) {
496                    if (rights.contains(right)) {
497                        result.addMessage("Target matched excluded right: " + right);
498                        userAllowed = false;
499                        break;
500                    }
501                }
502            }
503            if (!userAllowed) {
504                result.denyMessage("Access denied to " + thingName + " because target user " + target.getName() + " matches one or more of the excluded rights " + rights);
505            }
506        }
507        if (result.isAllowed() && !Util.isEmpty(config.getValidTargetExcludedCapabilities())) {
508            boolean userAllowed = true;
509            List<String> rights = config.getValidTargetExcludedCapabilities();
510            List<Capability> capabilities = target.getCapabilityManager().getEffectiveCapabilities();
511            if (capabilities != null) {
512                for(Capability capability : Util.safeIterable(capabilities)) {
513                    if (rights.contains(capability.getName())) {
514                        result.addMessage("Target matched excluded capability: " + capability.getName());
515                        userAllowed = false;
516                        break;
517                    }
518                }
519            }
520            if (!userAllowed) {
521                result.denyMessage("Access denied to " + thingName + " because target user " + target.getName() + " matches one or more of the excluded capabilities " + rights);
522            }
523        }
524
525        if (result.isAllowed() && Util.isNotNullOrEmpty(config.getInvalidTargetFilter())) {
526            String filterString = config.getValidTargetFilter();
527            Filter compiledFilter = Filter.compile(filterString);
528
529            HybridObjectMatcher hom = new HybridObjectMatcher(pluginContext.getContext(), compiledFilter);
530
531            if (hom.matches(target)) {
532                result.denyMessage("Access denied to " + thingName + " because target user " + target.getName() + " matches the invalid target filter");
533            }
534        }
535        if (result.isAllowed() && !Util.isEmpty(config.getValidTargetWorkgroups())) {
536            List<String> workgroups = config.getValidTargetWorkgroups();
537            boolean userAllowed = matchesAnyWorkgroup(target, workgroups);
538            if (!userAllowed) {
539                result.denyMessage("Access denied to " + thingName + " because target user " + target.getName() + " does not match any of the required workgroups " + workgroups);
540            }
541        }
542        if (result.isAllowed() && !Util.isEmpty(config.getValidTargetCapabilities())) {
543            boolean userAllowed = false;
544            List<String> rights = config.getValidTargetCapabilities();
545            List<Capability> capabilities = target.getCapabilityManager().getEffectiveCapabilities();
546            if (capabilities != null) {
547                for(Capability capability : Util.safeIterable(capabilities)) {
548                    if (rights.contains(capability.getName())) {
549                        result.addMessage("Target matched capability: " + capability.getName());
550                        userAllowed = true;
551                    }
552                }
553            }
554            if (!userAllowed) {
555                result.denyMessage("Access denied to " + thingName + " because target user " + target.getName() + " does not match one or more of the included capabilities " + rights);
556            }
557        }
558        if (result.isAllowed() && config.getValidTargetSelector() != null) {
559            IdentitySelector selector = config.getValidTargetSelector();
560            Matchmaker matchmaker = new Matchmaker(pluginContext.getContext());
561            if (!matchmaker.isMatch(selector, target)) {
562                result.denyMessage("Access denied to " + thingName + " because target user " + target.getName() + " does not match the valid target selector");
563            }
564        }
565        if (result.isAllowed() && Util.isNotNullOrEmpty(config.getValidTargetFilter())) {
566            String filterString = config.getValidTargetFilter();
567            Filter compiledFilter = Filter.compile(filterString);
568
569            HybridObjectMatcher hom = new HybridObjectMatcher(pluginContext.getContext(), compiledFilter);
570
571            if (!hom.matches(target)) {
572                result.denyMessage("Access denied to " + thingName + " because target user " + target.getName() + " does not match the valid target filter");
573            }
574        }
575        if (log.isTraceEnabled()) {
576            String resultString = result.isAllowed() ? "ALLOWED access" : "DENIED access";
577            log.trace("FINISH: " + resultString + " for subject = " + currentUser.getName() + ", target = " + target.getName() + ", thing = " + thingName + ", result = " + result);
578        }
579        return result;
580    }
581
582    /**
583     * An optional clear-cache method that can be used by plugin code
584     */
585    public static void clearCachedResults() {
586        ConcurrentHashMap<AccessCheck.SecurityCacheToken, AccessCheck.SecurityResult> cacheMap = getCacheMap();
587        cacheMap.clear();
588    }
589
590    /**
591     * Creates a native IIQ authorizer that performs a CommonSecurityConfig check
592     * @param config The configuration
593     * @return The authorizer
594     */
595    public static Authorizer createAuthorizer(CommonSecurityConfig config) {
596        return userContext -> {
597            AccessCheckInput input = new AccessCheckInput(userContext, config);
598            AccessCheckResponse response = AccessCheck.accessCheck(input);
599            if (!response.isAllowed()) {
600                log.debug("Access denied with messages: " + response.getMessages());
601                throw new UnauthorizedAccessException("Access denied");
602            }
603        };
604    }
605
606    /**
607     * Creates the cache map, which should be stored in CustomGlobal. If it does not exist,
608     * we create and store a new one. Since this is just for efficiency, we don't really
609     * care about synchronization.
610     *
611     * A new cache will be created whenever a new plugin is installed, incrementing the
612     * Environment's plugin version.
613     *
614     * @return The cache map
615     */
616    public static ConcurrentHashMap<SecurityCacheToken, SecurityResult> getCacheMap() {
617        String versionedKey = CACHE_KEY + "." + Utilities.getPluginVersion();
618        @SuppressWarnings("unchecked")
619        ConcurrentHashMap<SecurityCacheToken, SecurityResult> cacheMap = (ConcurrentHashMap<SecurityCacheToken, SecurityResult>) CustomGlobal.get(versionedKey);
620        if (cacheMap == null) {
621            cacheMap = new ConcurrentHashMap<>();
622            CustomGlobal.put(versionedKey, cacheMap);
623        }
624        return cacheMap;
625    }
626
627    /**
628     * Gets an optional cached result for the given cache token. An empty
629     * optional will be returned if there is no cached entry for the given token
630     * or if it has expired or if there is classloader weirdness.
631     *
632     * @param securityContext The security context
633     * @return The cached result, if one exists
634     */
635    private static Optional<AccessCheckResponse> getCachedResult(SecurityCacheToken securityContext) {
636        ConcurrentHashMap<SecurityCacheToken, SecurityResult> cacheMap = getCacheMap();
637        Supplier<Optional<AccessCheckResponse>> cachedEntry = cacheMap.get(securityContext);
638        if (cachedEntry == null) {
639            return Optional.empty();
640        } else {
641            return cachedEntry.get();
642        }
643    }
644
645    /**
646     * Returns true if the current user is a member of any of the given workgroups.
647     * Note that this check is NOT recursive and does not check whether a workgroup
648     * is a member of another workgroup.
649     *
650     * @param currentUser The user to check
651     * @param workgroups The workgroups to check
652     * @return true if the user is in the given workgroup
653     */
654    public static boolean matchesAnyWorkgroup(Identity currentUser, List<String> workgroups) {
655        boolean matchesWorkgroup = false;
656        List<Identity> userWorkgroups = currentUser.getWorkgroups();
657        if (userWorkgroups != null) {
658            for(Identity wg : userWorkgroups) {
659                String wgName = wg.getName();
660                if (workgroups.contains(wgName)) {
661                    matchesWorkgroup = true;
662                    break;
663                }
664            }
665        }
666        return matchesWorkgroup;
667    }
668
669    /**
670     * Returns a new {@link FluentAccessCheck}, permitting a nice flow-y API for this class.
671     *
672     * For example:
673     *
674     *   AccessCheck
675     *      .setup()
676     *      .config(commonSecurityObject)
677     *      .name("some name")
678     *      .subject(pluginResource) // contains the logged-in username, so counts as a subject
679     *      .target(targetIdentity)
680     *      .isAllowed()
681     *
682     * @return The fluent access check builder
683     */
684    public static FluentAccessCheck setup() {
685        return new FluentAccessCheck();
686    }
687}