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}