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