001package com.identityworksllc.iiq.common; 002 003import sailpoint.api.ObjectUtil; 004import sailpoint.api.SailPointContext; 005import sailpoint.api.SailPointFactory; 006import sailpoint.object.Attributes; 007import sailpoint.object.Identity; 008import sailpoint.object.Link; 009import sailpoint.object.ProvisioningPlan; 010import sailpoint.tools.GeneralException; 011import sailpoint.tools.Util; 012 013import java.util.*; 014import java.util.function.Consumer; 015import java.util.function.Supplier; 016 017/** 018 * A fluent builder for provisioning plans, especially useful for quickly writing 019 * rules or test cases. Import the methods from this class statically and begin with 020 * one of the plan() methods. The builder model will enforce only calling appropriate 021 * operations in any given context. 022 * 023 * The interfaces nested within this class will be used to implement a chain of 024 * modifiers to the 'current' object. 025 * 026 * Example: 027 * 028 * ``` 029 * ProvisioningPlan plan = 030 * PlanBuilder.plan(existingLink, 031 * removeAllValues(existingLink, "emails"), 032 * attribute("firstName", set("John"))); 033 * ``` 034 * 035 * This results in a ProvisioningPlan containing an AccountRequest to modify the 036 * given account. The AccountRequest contains AttributeRequests to remove all 037 * existing values from 'email' and to set the 'firstName' attribute to 'John'. 038 */ 039public class PlanBuilder { 040 041 /** 042 * Interface for modifying existing links, can be passed to the Link plan() method 043 * for better fluency 044 */ 045 public interface ExistingLinkModifier {} 046 047 /** 048 * Can modify the given provisioning plan 049 */ 050 public interface PlanModifier extends ExistingLinkModifier { 051 /** 052 * Modifies the given plan 053 * @param plan The plan to modify 054 * @throws GeneralException if anything goes wrong 055 */ 056 void modifyPlan(ProvisioningPlan plan) throws GeneralException; 057 } 058 059 /** 060 * Can modify the given account request 061 */ 062 public interface AccountRequestModifier extends ExistingLinkModifier { 063 void modifyAccountRequest(ProvisioningPlan.AccountRequest accountRequest); 064 } 065 066 /** 067 * Can modify the given attribute request 068 */ 069 public interface AttributeRequestModifier { 070 void modifyAttributeRequest(ProvisioningPlan.AttributeRequest attributeRequest); 071 } 072 073 /** 074 * Can be used in any context 075 */ 076 public interface AnyModifier extends PlanModifier, AccountRequestModifier, AttributeRequestModifier { 077 078 } 079 080 /** 081 * Handles the case of a string parameter to operation() 082 */ 083 public interface OperationModifier extends AccountRequestModifier, AttributeRequestModifier { 084 085 } 086 087 /** 088 * returns a plan modifier that adds an account request to the plan 089 * @param modifiers Any modifiers to apply to the account request 090 * @return The plan modifier 091 */ 092 public static PlanModifier account(AccountRequestModifier... modifiers) { 093 return (plan) -> { 094 ProvisioningPlan.AccountRequest accountRequest = new ProvisioningPlan.AccountRequest(); 095 for(AccountRequestModifier modifier : safeIterable(modifiers)) { 096 modifier.modifyAccountRequest(accountRequest); 097 } 098 plan.add(accountRequest); 099 }; 100 } 101 102 /** 103 * Returns an AccountRequestModifier that sets the application on the account request 104 * @param application The application name 105 * @return The AccountRequestModifier 106 */ 107 public static AccountRequestModifier application(String application) { 108 return (ar) -> ar.setApplication(application); 109 } 110 111 /** 112 * Returns a modifier for any object that adds an argument to the object's `arguments` Map. 113 * This method works on plans, requests, and attributes. 114 * 115 * @param name The argument name 116 * @param value The argument value 117 * @return The modifier 118 */ 119 public static AnyModifier argument(String name, Object value) { 120 return new AnyModifier() { 121 @Override 122 public void modifyAccountRequest(ProvisioningPlan.AccountRequest accountRequest) { 123 setArgument(accountRequest::getArguments, accountRequest::setArguments, name, value); 124 } 125 126 @Override 127 public void modifyAttributeRequest(ProvisioningPlan.AttributeRequest attributeRequest) { 128 setArgument(attributeRequest::getArguments, attributeRequest::setArguments, name, value); 129 } 130 131 @Override 132 public void modifyPlan(ProvisioningPlan plan) throws GeneralException { 133 setArgument(plan::getArguments, plan::setArguments, name, value); 134 } 135 }; 136 } 137 138 /** 139 * Creates a modifier to add an AttributeRequest to the AccountRequest 140 * @param name The name of the attribute 141 * @param modifiers Any modifiers to apply to the attribute request 142 * @return the resulting modifier implementation 143 */ 144 public static AccountRequestModifier attribute(String name, AttributeRequestModifier... modifiers) { 145 return (ar) -> { 146 ProvisioningPlan.AttributeRequest attributeRequest = new ProvisioningPlan.AttributeRequest(); 147 attributeRequest.setName(name); 148 149 for(AttributeRequestModifier modifier : safeIterable(modifiers)) { 150 modifier.modifyAttributeRequest(attributeRequest); 151 } 152 153 ar.add(attributeRequest); 154 }; 155 } 156 157 /** 158 * Locates an Enum constant case-insensitively by name and returns it 159 * @param enumClass The enum class 160 * @param value The value to find 161 * @return The matching enum constant 162 * @param <T> The enum type 163 * @throws IllegalArgumentException if no matching constant is found 164 */ 165 private static <T extends Enum<T>> T findValue(Class<T> enumClass, String value) { 166 EnumSet<T> values = EnumSet.allOf(enumClass); 167 for(T val : values) { 168 if (Util.nullSafeCaseInsensitiveEq(val.name(), value)) { 169 return val; 170 } 171 } 172 throw new IllegalArgumentException("No such enum value in " + enumClass.getName() + ": " + value); 173 } 174 175 /** 176 * Sets the identity on the plan to the given identity 177 * @param identity The identity to set 178 * @return A plan modifier that changes the plan Identity 179 */ 180 public static PlanModifier identity(Identity identity) { 181 return (plan) -> plan.setIdentity(identity); 182 } 183 184 /** 185 * Creates a plan modifier that sets the identity on the plan to the identity with the given name or ID 186 * @param idOrName The identity name or ID 187 * @return The plan modifier 188 * @throws GeneralException if the identity cannot be found 189 */ 190 public static PlanModifier identity(String idOrName) throws GeneralException { 191 SailPointContext context = SailPointFactory.getCurrentContext(); 192 Identity identity = context.getObject(Identity.class, idOrName); 193 return (plan) -> plan.setIdentity(identity); 194 } 195 196 /** 197 * Merges the new value into the list of values in the attribute request 198 * @param attributeRequest The attribute request to modify 199 * @param value The new value 200 */ 201 private static void mergeValue(ProvisioningPlan.AttributeRequest attributeRequest, Object value) { 202 List<Object> values = new ArrayList<>(); 203 Object existingValue = attributeRequest.getValue(); 204 if (existingValue instanceof Collection) { 205 values.addAll((Collection<?>) existingValue); 206 } else if (existingValue != null) { 207 values.add(existingValue); 208 } 209 if (value instanceof Collection) { 210 values.addAll((Collection<?>)value); 211 } else { 212 values.add(value); 213 } 214 attributeRequest.setValue(values); 215 } 216 217 /** 218 * Creates an AccountRequestModifier that sets the native identity on the account request 219 * @param ni The native identity to set 220 * @return The AccountRequestModifier 221 */ 222 public static AccountRequestModifier nativeIdentity(String ni) { 223 return (ar) -> ar.setNativeIdentity(ni); 224 } 225 226 /** 227 * Creates an OperationModifier that sets the operation on the account request or attribute request 228 * @param op The operation to set, as a string 229 * @return The OperationModifier 230 */ 231 public static OperationModifier operation(String op) { 232 return new OperationModifier() { 233 @Override 234 public void modifyAccountRequest(ProvisioningPlan.AccountRequest accountRequest) { 235 ProvisioningPlan.AccountRequest.Operation operation = findValue(ProvisioningPlan.AccountRequest.Operation.class, op); 236 accountRequest.setOperation(operation); 237 } 238 239 @Override 240 public void modifyAttributeRequest(ProvisioningPlan.AttributeRequest attributeRequest) { 241 ProvisioningPlan.Operation operation = findValue(ProvisioningPlan.Operation.class, op); 242 attributeRequest.setOperation(operation); 243 } 244 }; 245 } 246 247 /** 248 * Creates an AttributeRequestModifier that sets the operation on the attribute request 249 * @param operation The operation to set 250 * @return The AttributeRequestModifier 251 */ 252 public static AttributeRequestModifier operation(ProvisioningPlan.Operation operation) { 253 return (at) -> at.setOperation(operation); 254 } 255 256 /** 257 * Creates an AccountRequestModifier that sets the operation on the account request 258 * @param operation The operation to set 259 * @return The AccountRequestModifier 260 */ 261 public static AccountRequestModifier operation(ProvisioningPlan.AccountRequest.Operation operation) { 262 return (ar) -> ar.setOperation(operation); 263 } 264 265 /** 266 * Creates a provisioning plan based on the given link and any modifiers. 267 * This method is one of the entry points to the PlanBuilder. 268 * 269 * @param toModify The link to modify 270 * @param modifiers Any modifiers to apply to the plan or account request 271 * @return The resulting plan 272 * @throws GeneralException if anything goes wrong 273 */ 274 public static ProvisioningPlan plan(Link toModify, ExistingLinkModifier... modifiers) throws GeneralException { 275 ProvisioningPlan plan = new ProvisioningPlan(); 276 if (toModify.getIdentity() != null) { 277 plan.setIdentity(toModify.getIdentity()); 278 } else { 279 SailPointContext context = SailPointFactory.getCurrentContext(); 280 String identityName = ObjectUtil.getIdentityFromLink(context, toModify.getApplication(), toModify.getInstance(), toModify.getNativeIdentity()); 281 Identity identity = context.getObject(Identity.class, identityName); 282 plan.setIdentity(identity); 283 } 284 285 ProvisioningPlan.AccountRequest request = new ProvisioningPlan.AccountRequest(); 286 request.setApplication(toModify.getApplicationName()); 287 request.setNativeIdentity(toModify.getNativeIdentity()); 288 289 for(ExistingLinkModifier modifier : safeIterable(modifiers)) { 290 if (modifier instanceof PlanModifier) { 291 ((PlanModifier) modifier).modifyPlan(plan); 292 } else if (modifier instanceof AccountRequestModifier) { 293 ((AccountRequestModifier) modifier).modifyAccountRequest(request); 294 } 295 } 296 297 plan.add(request); 298 299 return plan; 300 } 301 302 /** 303 * Creates a provisioning plan based on the given modifiers. 304 * This method is one of the entry points to the PlanBuilder. 305 * 306 * @param planModifiers Any modifiers to apply to the plan 307 * @return The resulting plan 308 * @throws GeneralException if anything goes wrong 309 */ 310 public static ProvisioningPlan plan(PlanModifier... planModifiers) throws GeneralException { 311 ProvisioningPlan plan = new ProvisioningPlan(); 312 for(PlanModifier modifier : safeIterable(planModifiers)) { 313 modifier.modifyPlan(plan); 314 } 315 return plan; 316 } 317 318 /** 319 * Generates a complex AccountRequestModifier that removes all existing values from the given field 320 * @param existing The Link from which to extract existing values 321 * @param field The field to remove values from 322 * @return The AccountRequestModifier 323 */ 324 public static AccountRequestModifier removeAllValues(Link existing, String field) { 325 return (ar) -> { 326 if (existing == null || existing.getAttributes() == null) { 327 return; 328 } 329 ProvisioningPlan.AttributeRequest attributeRequest = ar.getAttributeRequest(field); 330 if (attributeRequest == null) { 331 attributeRequest = new ProvisioningPlan.AttributeRequest(); 332 attributeRequest.setName(field); 333 attributeRequest.setOperation(ProvisioningPlan.Operation.Remove); 334 } 335 List<String> existingValues = existing.getAttributes().getStringList(field); 336 mergeValue(attributeRequest, existingValues); 337 }; 338 } 339 340 /** 341 * Internal utility to create a safe Iterable object from an array 342 * which may be null or empty. 343 * 344 * @param array The array, which may be null 345 * @param <T> The type of the array 346 * @return A non-null Iterable object for use in a for loop 347 */ 348 private static <T> Iterable<T> safeIterable(T[] array) { 349 if (array == null || array.length == 0) { 350 return new ArrayList<>(); 351 } 352 return Arrays.asList(array); 353 } 354 355 /** 356 * Shortcut for 'operation("Set"), value(value)' 357 * @param value The value to set in the attribute request 358 * @return The AttributeRequestModifier 359 */ 360 public static AttributeRequestModifier set(Object value) { 361 return (at) -> { 362 at.setOperation(ProvisioningPlan.Operation.Set); 363 if (value instanceof Identity) { 364 at.setValue(((Identity) value).getName()); 365 } else { 366 at.setValue(value); 367 } 368 }; 369 } 370 371 /** 372 * Utility method to retrieve the arguments from the given object, add the 373 * argument specified to the arguments, then put the arguments back. This 374 * generically handles the case where the arguments do not yet exist. 375 * 376 * @param getArguments A reference to the object's getArguments 377 * @param setArguments A reference to the object's setArguments 378 * @param arg The argument to set 379 * @param val The value of the argument 380 */ 381 private static void setArgument(Supplier<Attributes<String, Object>> getArguments, Consumer<Attributes<String, Object>> setArguments, String arg, Object val) { 382 Attributes<String, Object> attributes = getArguments.get(); 383 if (attributes == null) { 384 attributes = new Attributes<>(); 385 } 386 attributes.put(arg, val); 387 setArguments.accept(attributes); 388 } 389 390 /** 391 * Adds the given value(s) to the Plan. If only one value is passed, it 392 * will be added as a single value. If value() is called more than once, 393 * if more than one value is passed, or if the attribute request already 394 * has a value, it will be transformed into a list and merged. 395 * 396 * @param values The values 397 * @return The input 398 */ 399 public static AttributeRequestModifier value(Object... values) { 400 return (at) -> { 401 if (values != null && values.length == 1 && at.getValue() == null) { 402 at.setValue(values[0]); 403 } else { 404 for (Object val : safeIterable(values)) { 405 mergeValue(at, val); 406 } 407 } 408 }; 409 } 410 411}