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