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}