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