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}