001package com.identityworksllc.iiq.common.task; 002 003import com.identityworksllc.iiq.common.Ref; 004import com.identityworksllc.iiq.common.Utilities; 005import sailpoint.api.ObjectUtil; 006import sailpoint.api.SailPointContext; 007import sailpoint.api.Terminator; 008import sailpoint.object.*; 009import sailpoint.tools.GeneralException; 010import sailpoint.tools.Message; 011import sailpoint.tools.Util; 012 013import java.util.*; 014 015/** 016 * A class that can prune a subset of Sailpoint objects in a more granular 017 * way. The objects to prune can be supplied by a script or by a search filter. 018 * In the case of a filter, objects can be further narrowed by a post-selector 019 * script that can arbitrarily reject specific objects. 020 * 021 * All selectors are defined in a Configuration object. 022 * 023 * If a filter is used, the {@link TimeTokenizer} class allows for dynamic time 024 * based filtering (e.g. older than five days ago). 025 */ 026public class SmartObjectPruner extends AbstractThreadedTask<Reference> { 027 /** 028 * The allowed list of object types for deletion 029 */ 030 private static final List<String> OBJECT_TYPES = Arrays.asList( 031 AuditEvent.class.getSimpleName(), 032 Bundle.class.getSimpleName(), 033 Identity.class.getSimpleName(), 034 IdentityRequest.class.getSimpleName(), 035 Link.class.getSimpleName(), 036 Request.class.getSimpleName(), 037 ProvisioningTransaction.class.getSimpleName(), 038 ProvisioningRequest.class.getSimpleName(), 039 SyslogEvent.class.getSimpleName(), 040 TaskResult.class.getSimpleName(), 041 WorkflowCase.class.getSimpleName(), 042 WorkItem.class.getSimpleName() 043 ); 044 045 /** 046 * Gets the list of objects to prune 047 */ 048 @Override 049 protected Iterator<? extends Reference> getObjectIterator(SailPointContext context, Attributes<String, Object> attributes) throws GeneralException { 050 String prunerConfig = "IDW - Smart Pruner Configuration"; 051 if (Util.isNotNullOrEmpty(attributes.getString("prunerConfigName"))) { 052 prunerConfig = attributes.getString("prunerConfigName"); 053 } 054 Configuration smartPrunerConfiguration = context.getObjectByName(Configuration.class, prunerConfig); 055 056 if (smartPrunerConfiguration == null || smartPrunerConfiguration.getAttributes() == null || smartPrunerConfiguration.getAttributes().isEmpty()) { 057 taskResult.addMessage(Message.warn("Smart pruner configuration " + prunerConfig + " does not exist or is empty; aborting")); 058 return null; 059 } 060 061 List<Reference> toDelete = new ArrayList<>(); 062 063 for(String objectType : OBJECT_TYPES) { 064 if (terminated.get()) { 065 break; 066 } 067 if (!smartPrunerConfiguration.containsKey(objectType)) { 068 continue; 069 } 070 @SuppressWarnings("unchecked") 071 Class<? extends SailPointObject> spClass = ObjectUtil.getSailPointClass(objectType); 072 073 @SuppressWarnings("unchecked") 074 Map<String, Object> objectConfig = (Map<String, Object>) smartPrunerConfiguration.get(objectType); 075 if (objectConfig != null && !objectConfig.isEmpty()) { 076 String selectorType = Util.otoa(objectConfig.get("type")); 077 if (Util.isNullOrEmpty(selectorType)) { 078 throw new IllegalArgumentException("A selector 'type' must be specified for class " + objectType); 079 } 080 081 Object selector = objectConfig.get("selector"); 082 Script postSelectorScript = Utilities.getAsScript(objectConfig.get("postSelectorScript")); 083 List<String> props = new ArrayList<>(); 084 props.add("id"); 085 if (selectorType.equalsIgnoreCase("all")) { 086 QueryOptions qo = new QueryOptions(); 087 Iterator<Object[]> objects = context.search(spClass, qo, props); 088 while(objects.hasNext()) { 089 Object[] result = objects.next(); 090 String id = Util.otoa(result[0]); 091 maybeAdd(context, toDelete, spClass, id, postSelectorScript); 092 } 093 } else if (selectorType.equalsIgnoreCase("filter")) { 094 String filterString = Util.otoa(selector); 095 if (Util.isNullOrEmpty(filterString)) { 096 throw new IllegalArgumentException("For object type " + objectType + " with selector type filter, the filter string is null or empty"); 097 } 098 String modifiedFilterString = TimeTokenizer.parseTimeComponents(taskSchedule, filterString, null); 099 Filter objectFilter = Filter.compile(modifiedFilterString); 100 QueryOptions qo = new QueryOptions(); 101 qo.addFilter(objectFilter); 102 Iterator<Object[]> objects = context.search(spClass, qo, props); 103 while(objects.hasNext()) { 104 Object[] result = objects.next(); 105 String id = Util.otoa(result[0]); 106 maybeAdd(context, toDelete, spClass, id, postSelectorScript); 107 } 108 } else if (selectorType.equalsIgnoreCase("script")) { 109 Script scriptSelector = Utilities.getAsScript(selector); 110 if (scriptSelector != null) { 111 Map<String, Object> params = new HashMap<>(); 112 params.put("type", objectType); 113 params.put("typeClass", spClass); 114 params.put("environment", attributes); 115 params.put("configuration", objectConfig); 116 117 Object output = context.runScript(scriptSelector, params); 118 if (output instanceof List) { 119 List<Object> objectList = (List<Object>)output; 120 for(Object obj : objectList) { 121 if (obj != null) { 122 if (obj instanceof String) { 123 toDelete.add(Ref.of(spClass, (String) obj)); 124 } else if (obj instanceof SailPointObject) { 125 SailPointObject spo = (SailPointObject) obj; 126 toDelete.add(Ref.of(spo)); 127 128 context.decache(spo); 129 } else { 130 throw new IllegalStateException("Illegal output list element from selector script for object type " + objectType + ": " + obj.getClass().getName()); 131 } 132 } 133 } 134 } else if (output != null) { 135 throw new IllegalStateException("Illegal output type from selector script for object type " + objectType + ": " + output.getClass().getName()); 136 } 137 } 138 } else { 139 throw new IllegalArgumentException("Invalid selector type: " + selectorType); 140 } 141 } 142 } 143 return Util.safeIterable(toDelete).iterator(); 144 } 145 146 /** 147 * Executes the post-selector script if one exists. If the script returns true, 148 * adds the item to the list. 149 * 150 * If no post-selector script is defined, adds the item to the list. 151 * 152 * @param context The context to use for the script execution 153 * @param toDelete The list to which the objects should be added on deletion 154 * @param spClass The queried class 155 * @param objectId The object ID to check 156 * @param postSelectorScript The script, which may be null 157 * @throws GeneralException if a script failure occurs 158 */ 159 private void maybeAdd(SailPointContext context, List<Reference> toDelete, Class<? extends SailPointObject> spClass, String objectId, Script postSelectorScript) throws GeneralException { 160 if (postSelectorScript != null) { 161 SailPointObject spo = context.getObjectById(spClass, objectId); 162 Map<String, Object> params = new HashMap<>(); 163 params.put("object", spo); 164 params.put("context", context); 165 boolean shouldAdd = Util.otob(context.runScript(postSelectorScript, params)); 166 if (shouldAdd) { 167 toDelete.add(Ref.of(spo.getClass(), spo.getId())); 168 } 169 170 context.decache(spo); 171 } else { 172 toDelete.add(Ref.of(spClass, objectId)); 173 } 174 } 175 176 /** 177 * Deletes the input 178 * @param threadContext A private IIQ context for the current JVM thread 179 * @param parameters A set of default parameters suitable for a Rule or Script. In the default implementation, the object will be in this map as 'object'. 180 * @param ref The object to terminate in this thread 181 * @return always null 182 * @throws GeneralException if a failure occurs deleting the object 183 */ 184 @Override 185 public Object threadExecute(SailPointContext threadContext, Map<String, Object> parameters, Reference ref) throws GeneralException { 186 SailPointObject spo = ref.resolve(threadContext); 187 if (spo != null) { 188 if (spo instanceof Identity && ((Identity) spo).isProtected()) { 189 log.warn("Filter returned Identity " + ((Identity) spo).getDisplayableName() + " but it is protected; ignoring it"); 190 return null; 191 } 192 Terminator terminator = new Terminator(threadContext); 193 terminator.deleteObject(spo); 194 } 195 return null; 196 } 197}