001package com.identityworksllc.iiq.common.task;
002
003import com.identityworksllc.iiq.common.HybridObjectMatcher;
004import com.identityworksllc.iiq.common.Utilities;
005import org.apache.commons.logging.Log;
006import org.apache.commons.logging.LogFactory;
007import sailpoint.api.IncrementalProjectionIterator;
008import sailpoint.api.ObjectUtil;
009import sailpoint.api.SailPointContext;
010import sailpoint.object.Attributes;
011import sailpoint.object.AuditEvent;
012import sailpoint.object.Filter;
013import sailpoint.object.Identity;
014import sailpoint.object.QueryOptions;
015import sailpoint.object.RoleAssignment;
016import sailpoint.object.TaskResult;
017import sailpoint.object.TaskSchedule;
018import sailpoint.task.AbstractTaskExecutor;
019import sailpoint.tools.GeneralException;
020import sailpoint.tools.Util;
021import sailpoint.tools.xml.AbstractXmlObject;
022
023import java.util.ArrayList;
024import java.util.List;
025import java.util.Map;
026import java.util.concurrent.atomic.AtomicBoolean;
027
028/**
029 * A task executor for finding and removing unwanted negative role assignments.
030 *
031 * An audit event of type 'negativeRoleAssignmentsRemoved' will be logged if action
032 * is taken.
033 */
034public class RemoveNegativeRoleAssignments extends AbstractTaskExecutor {
035
036    /**
037     * Logger
038     */
039    private final Log log;
040
041    /**
042     * Terminated flag, set by {@link #terminate()}
043     */
044    private final AtomicBoolean terminated;
045
046    /**
047     * Construct a new task executor
048     */
049    public RemoveNegativeRoleAssignments() {
050        this.terminated = new AtomicBoolean();
051        this.log = LogFactory.getLog(RemoveNegativeRoleAssignments.class);
052    }
053
054    @Override
055    public void execute(SailPointContext context, TaskSchedule taskSchedule, TaskResult taskResult, Attributes<String, Object> attributes) throws Exception {
056        // Identities matching this filter will be included in the cleanup
057        String additionalFilterString = attributes.getString("identityFilter");
058        Filter additionalFilter = Util.isNotNullOrEmpty(additionalFilterString) ? Filter.compile(additionalFilterString) : null;
059
060        QueryOptions qo = new QueryOptions();
061        qo.addFilter(Filter.like("preferences", "roleAssignments", Filter.MatchMode.ANYWHERE));
062
063        List<String> fields = new ArrayList<>();
064        fields.add("id");
065        fields.add("name");
066        fields.add("displayName");
067        fields.add("preferences");
068
069        IncrementalProjectionIterator results = new IncrementalProjectionIterator(context, Identity.class, qo, fields);
070        while(results.hasNext()) {
071            if (terminated.get()) {
072
073                break;
074            }
075            Object[] row = results.next();
076
077            String id = Util.otoa(row[0]);
078            String prefs = Util.otoa(row[3]);
079
080            @SuppressWarnings("unchecked")
081            Map<String, Object> prefsMap = (Map<String, Object>) AbstractXmlObject.parseXml(context, prefs);
082            if (prefsMap != null) {
083                @SuppressWarnings("unchecked")
084                List<RoleAssignment> roleAssignments = (List<RoleAssignment>) prefsMap.get("roleAssignments");
085                for(RoleAssignment ra : Util.safeIterable(roleAssignments)) {
086                    if (ra.isNegative()) {
087                        Utilities.withPrivateContext((privateContext) -> {
088                            processIdentity(privateContext, id, additionalFilter);
089                        });
090                    }
091                }
092            }
093        }
094    }
095
096    /**
097     * Processes a single Identity by removing any negative RoleAssignments
098     *
099     * TODO wrap this in a worker thread and a private IIQ context
100     *
101     * @param context The IIQ context to use
102     * @param identityId The identity ID
103     * @param additionalFilter Optionally, an additional filter to further constrain the users operated upon
104     * @throws GeneralException if anything goes wrong
105     */
106    private void processIdentity(SailPointContext context, String identityId, Filter additionalFilter) throws GeneralException {
107        // Lock the Identity, just in case we're getting refreshed or provisioned at the same time
108        boolean assignmentsRemoved = false;
109
110        Identity identity = ObjectUtil.lockIdentity(context, identityId);
111        try {
112            boolean shouldSkip = false;
113
114            if (additionalFilter != null) {
115                HybridObjectMatcher matcher = new HybridObjectMatcher(context, additionalFilter);
116                shouldSkip = !matcher.matches(identity);
117            }
118
119            if (shouldSkip) {
120                if (log.isDebugEnabled()) {
121                    log.debug("Skipping Identity " + identity.getDisplayableName() + " because it matches the skip filter");
122                }
123            } else {
124                List<RoleAssignment> roleAssignments = identity.getRoleAssignments();
125                for (RoleAssignment ra : Util.safeIterable(roleAssignments)) {
126                    if (ra.isNegative()) {
127                        assignmentsRemoved = true;
128                        identity.removeRoleAssignment(ra);
129                    }
130                }
131            }
132        } finally {
133            // This also saves and commits the Identity object, even if the lock has
134            // somehow gone away.
135            ObjectUtil.unlockIfNecessary(context, identity);
136        }
137
138        // Only audit if an assignment was actually removed
139        if (assignmentsRemoved) {
140            AuditEvent ae = new AuditEvent();
141            ae.setAction("negativeRoleAssignmentsRemoved");
142            ae.setTarget(identity.getName());
143            ae.setServerHost(Util.getHostName());
144            ae.setSource(this.getClass().getSimpleName());
145
146            // Force it, even if Auditor.log would skip it
147            context.saveObject(ae);
148            context.commitTransaction();
149        }
150
151    }
152
153    @Override
154    public boolean terminate() {
155        this.terminated.set(true);
156        return true;
157    }
158}