001package com.identityworksllc.iiq.common; 002 003import sailpoint.api.SailPointContext; 004import sailpoint.object.*; 005import sailpoint.search.MapMatcher; 006import sailpoint.tools.GeneralException; 007import sailpoint.tools.Pair; 008import sailpoint.tools.Util; 009 010import java.util.*; 011import java.util.function.BiPredicate; 012import java.util.function.Predicate; 013import java.util.stream.Collectors; 014 015/** 016 * Utilities for acting on and generating ProvisioningPlan objects 017 */ 018@SuppressWarnings("unused") 019public class Plans { 020 021 /** 022 * A value you can pass to either removeAttributeRequest or hasAttributeRequest 023 * to indicate a literal null instead of a wildcard (which is the default interpretation 024 * of null inputs to those methods). 025 * 026 * This will match anything that {@link Utilities#isNothing(Object)} matches. 027 */ 028 public static final String EMPTY_VALUE = Plans.class.getName() + "*NULL*"; 029 030 /** 031 * Adds an AccountRequest to the given plan for each non-disabled account owned by the user 032 * unless the account is matched by the exceptFilter. 033 */ 034 public static void disableAccounts(SailPointContext context, Identity target, ProvisioningPlan plan, Filter exceptFilter) throws GeneralException { 035 MapMatcher matcher = null; 036 if (exceptFilter != null) { 037 matcher = new MapMatcher(exceptFilter); 038 } 039 for(Link link : Util.safeIterable(target.getLinks())) { 040 if (!link.isDisabled() && (matcher == null || !matcher.matches(Mapper.toMap(link)))) { 041 plan.add(link.getApplicationName(), link.getNativeIdentity(), ProvisioningPlan.AccountRequest.Operation.Disable); 042 } 043 } 044 } 045 046 /** 047 * Empties the input plan's account and object requests in place (i.e, by modifying 048 * the Lists within the plan itself). This can be used to cancel a provisioning operation 049 * in a Before Provisioning rule, for example. 050 * 051 * @param plan The plan 052 */ 053 public static void emptyPlan(ProvisioningPlan plan) { 054 if (plan != null) { 055 if (plan.getAccountRequests() != null) { 056 plan.getAccountRequests().clear(); 057 } 058 if (plan.getObjectRequests() != null) { 059 plan.getObjectRequests().clear(); 060 } 061 } 062 } 063 064 /** 065 * Adds an AccountRequest to the given plan for each non-disabled account owned by the user 066 * unless the account is matched by the exceptFilter. 067 */ 068 public static void enableAccounts(SailPointContext context, Identity target, ProvisioningPlan plan, Filter exceptFilter) throws GeneralException { 069 MapMatcher matcher = null; 070 if (exceptFilter != null) { 071 matcher = new MapMatcher(exceptFilter); 072 } 073 for(Link link : Util.safeIterable(target.getLinks())) { 074 if (link.isDisabled() && (matcher == null || !matcher.matches(Mapper.toMap(link)))) { 075 plan.add(link.getApplicationName(), link.getNativeIdentity(), ProvisioningPlan.AccountRequest.Operation.Enable); 076 } 077 } 078 } 079 080 /** 081 * If an entitlement is being added to the given application in the given plan, also 082 * add a separate Enable operation. This is important for connectors like Salesforce 083 * where entitlements cannot be added to disabled accounts. 084 */ 085 public static void enableOnEntitlementAdd(ProvisioningPlan plan, Application application, boolean enableFirst) { 086 Schema appSchema = application.getAccountSchema(); 087 boolean expandEnable = false; 088 ProvisioningPlan.AccountRequest template = null; 089 for(ProvisioningPlan.AccountRequest accountRequest : Util.safeIterable(plan.getAccountRequests(application.getName()))) { 090 for(ProvisioningPlan.AttributeRequest attributeRequest : Util.safeIterable(accountRequest.getAttributeRequests())) { 091 if (attributeRequest.getOperation().equals(ProvisioningPlan.Operation.Add) || attributeRequest.getOperation().equals(ProvisioningPlan.Operation.Set)) { 092 String attributeName = attributeRequest.getName(); 093 AttributeDefinition attributeDefinition = appSchema.getAttributeDefinition(attributeName); 094 if (attributeDefinition != null) { 095 if (attributeDefinition.isEntitlement() || attributeDefinition.isManaged()) { 096 expandEnable = true; 097 template = accountRequest; 098 break; 099 } 100 } 101 } 102 } 103 } 104 if (expandEnable) { 105 ProvisioningPlan.AccountRequest enable = new ProvisioningPlan.AccountRequest(); 106 enable.setOperation(ProvisioningPlan.AccountRequest.Operation.Enable); 107 enable.setNativeIdentity(template.getNativeIdentity()); 108 enable.setApplication(template.getApplicationName()); 109 enable.setInstance(template.getInstance()); 110 enable.setComments("Expand entitlement add to enable"); 111 enable.setArguments(template.getArguments()); 112 List<ProvisioningPlan.AccountRequest> requests = plan.getAccountRequests(); 113 if (enableFirst) { 114 requests.add(0, enable); 115 } else { 116 requests.add(enable); 117 } 118 plan.setAccountRequests(requests); 119 } 120 } 121 122 /** 123 * Extracts the target attribute from its AccountRequest into a new, second request 124 * against the same account. 125 */ 126 public static void extractToNewRequest(ProvisioningPlan plan, String targetAttribute) { 127 extractToNewRequest(plan, targetAttribute, false); 128 } 129 130 /** 131 * Extracts the target attribute from its AccountRequest into a new, second request 132 * against the same account. The new request will be placed at the beginning of the 133 * ProvisioningPlan sequence. 134 */ 135 public static void extractToNewRequest(ProvisioningPlan plan, String targetAttribute, boolean atBeginning) { 136 if (plan.getAccountRequests() == null) { 137 return; 138 } 139 List<ProvisioningPlan.AccountRequest> toAdd = new ArrayList<>(); 140 for(ProvisioningPlan.AccountRequest accountRequest : plan.getAccountRequests()) { 141 if (accountRequest.getAttributeRequests() == null) { 142 continue; 143 } 144 if (accountRequest.getAttributeRequest(targetAttribute) != null) { 145 ProvisioningPlan.AttributeRequest attribute = accountRequest.getAttributeRequest(targetAttribute); 146 ProvisioningPlan.AccountRequest newRequest = accountRequest.clone(); 147 newRequest.cloneAccountProperties(accountRequest); 148 newRequest.setAttributeRequests(new ArrayList<>()); 149 newRequest.add(attribute); 150 accountRequest.remove(attribute); 151 toAdd.add(newRequest); 152 } 153 } 154 List<ProvisioningPlan.AccountRequest> requests = plan.getAccountRequests(); 155 if (atBeginning) { 156 requests.addAll(0, toAdd); 157 } else { 158 requests.addAll(toAdd); 159 } 160 plan.setAccountRequests(requests); 161 } 162 163 /** 164 * Finds attribute requests in the provisioning plan where the account matches the filter 165 * 166 * @param accountRequest The provisioning plan 167 * @param findPredicate The account filter 168 * @return The list of account requests 169 */ 170 public static List<ProvisioningPlan.AttributeRequest> find(ProvisioningPlan.AccountRequest accountRequest, Predicate<ProvisioningPlan.AttributeRequest> findPredicate) { 171 return Utilities.safeStream(accountRequest.getAttributeRequests()).filter(findPredicate).collect(Collectors.toList()); 172 } 173 174 /** 175 * Finds attribute requests in the provisioning plan where the account matches the filter 176 * 177 * @param plan The provisioning plan 178 * @param findPredicate The account filter 179 * @return The list of account requests 180 */ 181 public static List<ProvisioningPlan.AccountRequest> find(ProvisioningPlan plan, Predicate<ProvisioningPlan.AccountRequest> findPredicate) { 182 return Utilities.safeStream(plan.getAccountRequests()).filter(findPredicate).collect(Collectors.toList()); 183 } 184 185 /** 186 * Find pairs of account/attribute requests in the provisioning plan where the 187 * account and attribute together match the filter. 188 * 189 * @param plan The provisioning plan 190 * @param findPredicate The account and attribute filter 191 * @return The list of account/attribute pairs 192 */ 193 public static List<Pair<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest>> find(ProvisioningPlan plan, BiPredicate<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest> findPredicate) { 194 List<Pair<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest>> pairs = new ArrayList<>(); 195 for(ProvisioningPlan.AccountRequest accountRequest : Util.safeIterable(plan.getAccountRequests())) { 196 for(ProvisioningPlan.AttributeRequest attributeRequest : Util.safeIterable(accountRequest.getAttributeRequests())) { 197 if (findPredicate.test(accountRequest, attributeRequest)) { 198 pairs.add(new Pair<>(accountRequest, attributeRequest)); 199 } 200 } 201 } 202 return pairs; 203 } 204 205 /** 206 * Find pairs of account/attribute requests in the provisioning plan where the 207 * account matches the first filter and the account/attribute combined match 208 * the second filter. 209 * 210 * @param plan The provisioning plan 211 * @param accountFilter The account filter 212 * @param attributeFilter The attribute filter 213 * @return The list of account/attribute pairs 214 */ 215 public static List<Pair<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest>> find(ProvisioningPlan plan, Predicate<ProvisioningPlan.AccountRequest> accountFilter, BiPredicate<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest> attributeFilter) { 216 BiPredicate<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest> combo = (a, b) -> { 217 if (accountFilter.test(a)) { 218 return attributeFilter.test(a, b); 219 } 220 return false; 221 }; 222 return find(plan, combo); 223 } 224 225 /** 226 * Finds attribute requests matching any of the given names 227 * 228 * @param attributeName The attribute name(s) 229 * @return The predicate object 230 */ 231 public static BiPredicate<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest> hasAttributeNames(final String... attributeName) { 232 final List<String> names = new ArrayList<>(); 233 if (attributeName != null) { 234 names.addAll(Arrays.asList(attributeName)); 235 } 236 237 return (accountRequest, attributeRequest) -> { 238 for(String name : names) { 239 if (Util.nullSafeEq(attributeRequest.getName(), name)) { 240 return true; 241 } 242 } 243 return false; 244 }; 245 } 246 247 /** 248 * Finds an attribute request matching the name, operation, and values as defined. Null 249 * and empty values are considered a "skip this match". 250 * 251 * @param attributeName The attribute name 252 * @param operation The operation to match 253 * @param value The value(s) to match 254 * @return The predicate object 255 */ 256 public static BiPredicate<ProvisioningPlan.AccountRequest, ProvisioningPlan.AttributeRequest> hasAttributeRequest(final String attributeName, final ProvisioningPlan.Operation operation, final Object value) { 257 return (accountRequest, attributeRequest) -> { 258 if (Util.isNullOrEmpty(attributeName) || Util.nullSafeEq(attributeRequest.getName(), attributeName)) { 259 if (operation == null || Util.nullSafeEq(attributeRequest.getOperation(), operation)) { 260 if (Utilities.isNothing(value) || Utilities.safeContainsAll(attributeRequest.getValue(), value)) { 261 return true; 262 } else if (Util.nullSafeEq(value, EMPTY_VALUE) && Utilities.isNothing(attributeRequest.getValue())) { 263 return true; 264 } 265 } 266 } 267 return false; 268 }; 269 } 270 271 /** 272 * Remove all assigned entitlements (i.e. AttributeAssignments requested via LCM or 273 * added via certification) from the user, except those matched by the 'exceptFilter'. 274 */ 275 public static void removeAssignedEntitlements(SailPointContext context, Identity target, ProvisioningPlan plan, Filter exceptFilter) throws GeneralException { 276 MapMatcher matcher = null; 277 if (exceptFilter != null) { 278 matcher = new MapMatcher(exceptFilter); 279 } 280 for(AttributeAssignment ra : Util.safeIterable(target.getAttributeAssignments())) { 281 if (matcher == null || !matcher.matches(Mapper.toMap(ra))) { 282 ProvisioningPlan.AccountRequest accountRequest = plan.getAccountRequest(ra.getApplicationName(), ra.getInstance(), ra.getNativeIdentity()); 283 if (accountRequest == null) { 284 accountRequest = new ProvisioningPlan.AccountRequest(); 285 accountRequest.setApplication(ra.getApplicationName()); 286 accountRequest.setNativeIdentity(ra.getNativeIdentity()); 287 accountRequest.setInstance(ra.getInstance()); 288 accountRequest.setOperation(ProvisioningPlan.AccountRequest.Operation.Modify); 289 plan.add(accountRequest); 290 } 291 ProvisioningPlan.AttributeRequest attributeRequest = new ProvisioningPlan.AttributeRequest(ra.getName(), ProvisioningPlan.Operation.Remove, ra.getStringValue()); 292 attributeRequest.setAssignment(true); 293 accountRequest.add(attributeRequest); 294 } 295 } 296 } 297 298 /** 299 * Remove all assigned roles from the given Identity by adding Remove operations to the 300 * given ProvisioningPlan. Role assignments matched by the 'exceptFilter' will not be removed. 301 */ 302 public static void removeAssignedRoles(SailPointContext context, Identity target, ProvisioningPlan plan, Filter exceptFilter) throws GeneralException { 303 MapMatcher matcher = null; 304 if (exceptFilter != null) { 305 matcher = new MapMatcher(exceptFilter); 306 } 307 for(RoleAssignment ra : Util.safeIterable(target.getRoleAssignments())) { 308 if (matcher == null || !matcher.matches(Mapper.toMap(ra))) { 309 ProvisioningPlan.AccountRequest iiqRequest = plan.getIIQAccountRequest(); 310 if (iiqRequest == null) { 311 iiqRequest = new ProvisioningPlan.AccountRequest(ProvisioningPlan.AccountRequest.Operation.Modify, "IIQ", null, target.getName()); 312 plan.add(iiqRequest); 313 } 314 ProvisioningPlan.AttributeRequest removeRequest = new ProvisioningPlan.AttributeRequest("assignedRoles", ProvisioningPlan.Operation.Remove, ra.getRoleName()); 315 removeRequest.setAssignmentId(ra.getAssignmentId()); 316 iiqRequest.add(removeRequest); 317 } 318 } 319 } 320 321 /** 322 * Removes the given attribute request(s) matching by either name or operation. 323 * 324 * @param plan The plan to modify 325 * @param attributeName The attribute to remove (by name) 326 * @param attributeOperation The attribute to remove (by operation) 327 */ 328 public static void removeAttributeRequest(ProvisioningPlan plan, String attributeName, ProvisioningPlan.Operation attributeOperation) { 329 removeAttributeRequest(plan, attributeName, attributeOperation, null); 330 } 331 332 /** 333 * Removes the given attribute request(s) matching by name. 334 * 335 * @param plan The plan to modify 336 * @param attributeName The attribute to remove (by name) 337 */ 338 public static void removeAttributeRequest(ProvisioningPlan plan, String attributeName) { 339 removeAttributeRequest(plan, attributeName, null, null); 340 } 341 342 /** 343 * Removes the given attribute request(s) matching by either name, operation, or both, from any 344 * account requests on this plan. Nulls provided for any of the three criteria will skip matching that 345 * attribute. 346 * 347 * If you want to match an actual null or empty value, use {@link #EMPTY_VALUE}. 348 * 349 * @param plan The plan to modify 350 * @param attributeName The attribute name (possibly null) to match 351 * @param attributeOperation The attribute operation (possibly null) to match 352 * @param attributeValue The attribute value (possibly null) to match 353 */ 354 public static void removeAttributeRequest(ProvisioningPlan plan, String attributeName, ProvisioningPlan.Operation attributeOperation, Object attributeValue) { 355 for(ProvisioningPlan.AccountRequest accountRequest : Util.safeIterable(plan.getAccountRequests())) { 356 if (accountRequest.getAttributeRequests() != null) { 357 removeAttributeRequest(accountRequest, attributeName, attributeOperation, attributeValue); 358 } 359 } 360 } 361 362 /** 363 * Removes the attribute requests matching by name, operation, and/or value from the given 364 * account request. Nulls provided for any of the three criteria will skip matching that 365 * attribute. 366 * 367 * @param accountRequest The account request to modify 368 * @param attributeName The attribute name 369 * @param attributeOperation The attribute operation 370 * @param attributeValue The attribute value 371 */ 372 public static void removeAttributeRequest(ProvisioningPlan.AccountRequest accountRequest, String attributeName, ProvisioningPlan.Operation attributeOperation, Object attributeValue) { 373 // Copy to a temporary list to avoid concurrent modification exceptions 374 List<ProvisioningPlan.AttributeRequest> temporaryList = new ArrayList<>(accountRequest.getAttributeRequests()); 375 for(ProvisioningPlan.AttributeRequest attributeRequest : temporaryList) { 376 if (Util.isNullOrEmpty(attributeName) || Util.nullSafeEq(attributeRequest.getName(), attributeName)) { 377 if (attributeOperation == null || Util.nullSafeEq(attributeRequest.getOperation(), attributeOperation)) { 378 if (attributeValue == null || Util.nullSafeEq(attributeRequest.getValue(), attributeValue)) { 379 accountRequest.remove(attributeRequest); 380 } else if (Util.nullSafeEq(attributeValue, EMPTY_VALUE) && Utilities.isNothing(attributeRequest.getValue())) { 381 accountRequest.remove(attributeRequest); 382 } 383 } 384 } 385 } 386 } 387 388 /** 389 * Remove all entitlements from the given Identity by adding Remove operations to the 390 * given ProvisioningPlan. Entitlements matched by the 'exceptFilter' will not be removed. 391 */ 392 public static void removeEntitlements(SailPointContext context, Identity target, ProvisioningPlan plan, Filter exceptFilter) throws GeneralException { 393 for(Link link : Util.safeIterable(target.getLinks())) { 394 Filter appFilter = Filter.eq("application.name", link.getApplicationName()); 395 MapMatcher matcher = null; 396 if (exceptFilter != null) { 397 matcher = new MapMatcher(exceptFilter); 398 } 399 Attributes<String, Object> entitlements = link.getEntitlementAttributes(); 400 if (entitlements != null) { 401 for(String name : entitlements.getKeys()) { 402 List<String> values = entitlements.getStringList(name); 403 for(String value : Util.safeIterable(values)) { 404 QueryOptions qo = new QueryOptions(); 405 qo.addFilter(appFilter); 406 qo.addFilter(Filter.eq("attribute", name)); 407 qo.addFilter(Filter.eq("value", value)); 408 qo.setResultLimit(1); 409 ManagedAttribute ma = Utilities.safeSubscript(context.getObjects(ManagedAttribute.class, qo), 0); 410 if (ma != null) { 411 Map<String, Object> maMap = Mapper.toMap(link, ma); 412 if (matcher == null || !matcher.matches(maMap)) { 413 plan.add(link.getApplicationName(), link.getNativeIdentity(), name, ProvisioningPlan.Operation.Remove, value); 414 } 415 } 416 } 417 } 418 } 419 } 420 } 421 422 /** 423 * Sets the given other attribute to the given value on any entitlement add. 424 */ 425 public static void setOnEntitlementAdd(ProvisioningPlan plan, Application application, String otherAttribute, Object value) { 426 Schema appSchema = application.getAccountSchema(); 427 boolean expandEnable = false; 428 ProvisioningPlan.AccountRequest template = null; 429 for(ProvisioningPlan.AccountRequest accountRequest : Util.safeIterable(plan.getAccountRequests(application.getName()))) { 430 for(ProvisioningPlan.AttributeRequest attributeRequest : Util.safeIterable(accountRequest.getAttributeRequests())) { 431 if (attributeRequest.getOperation().equals(ProvisioningPlan.Operation.Add) || attributeRequest.getOperation().equals(ProvisioningPlan.Operation.Set)) { 432 String attributeName = attributeRequest.getName(); 433 AttributeDefinition attributeDefinition = appSchema.getAttributeDefinition(attributeName); 434 if (attributeDefinition != null) { 435 if (attributeDefinition.isEntitlement() || attributeDefinition.isManaged()) { 436 expandEnable = true; 437 template = accountRequest; 438 break; 439 } 440 } 441 } 442 } 443 } 444 if (expandEnable) { 445 ProvisioningPlan.AccountRequest modify = new ProvisioningPlan.AccountRequest(); 446 modify.setOperation(ProvisioningPlan.AccountRequest.Operation.Modify); 447 modify.setNativeIdentity(template.getNativeIdentity()); 448 modify.setApplication(template.getApplicationName()); 449 modify.setInstance(template.getInstance()); 450 modify.setComments("Expand entitlement add to set " + otherAttribute); 451 modify.setArguments(template.getArguments()); 452 ProvisioningPlan.AttributeRequest attr = new ProvisioningPlan.AttributeRequest(otherAttribute, ProvisioningPlan.Operation.Set, value); 453 modify.add(attr); 454 plan.add(modify); 455 } 456 } 457 458 /** 459 * Sorts the AccountRequests in the given ProvisioningPlan using the given sorter. 460 * Several useful sorters are provided in {@link PlanComparators}. 461 */ 462 public static void sort(ProvisioningPlan plan, Comparator<ProvisioningPlan.AccountRequest> sorter) { 463 List<ProvisioningPlan.AccountRequest> requests = plan.getAccountRequests(); 464 if (requests != null) { 465 requests.sort(sorter); 466 plan.setAccountRequests(requests); 467 } 468 } 469 470 /** 471 * Sorts the AccountRequests in the given ProvisioningPlan using the default order, 472 * which is defined in {@link PlanComparators#defaultSequence()}. Attributes on each 473 * AccountRequest are then sorted using the default attribute comparator. 474 * 475 * The order is roughly create, modify, status changes, delete. This is what most 476 * connectors are expecting when more than one operation happens at once. 477 */ 478 public static void sort(ProvisioningPlan plan) { 479 sort(plan, PlanComparators.defaultSequence()); 480 for(ProvisioningPlan.AccountRequest accountRequest : Util.safeIterable(plan.getAccountRequests())) { 481 sort(accountRequest); 482 } 483 } 484 485 /** 486 * Sorts the AttributeRequessts in the given ProvisioningPlan using the default order, 487 * which is defined in {@link PlanComparators#defaultAttributeSequence()}. The essence 488 * is removes first, then sets, then adds. 489 */ 490 public static void sort(ProvisioningPlan.AccountRequest accountRequest) { 491 List<ProvisioningPlan.AttributeRequest> attributeRequests = accountRequest.getAttributeRequests(); 492 if (attributeRequests != null) { 493 attributeRequests.sort(PlanComparators.defaultAttributeSequence()); 494 accountRequest.setAttributeRequests(attributeRequests); 495 } 496 } 497 498}