001package com.identityworksllc.iiq.common.task.export; 002 003import com.identityworksllc.iiq.common.Metered; 004import com.identityworksllc.iiq.common.TaskUtil; 005import com.identityworksllc.iiq.common.Utilities; 006import com.identityworksllc.iiq.common.query.NamedParameterStatement; 007import org.apache.commons.logging.Log; 008import sailpoint.api.IncrementalObjectIterator; 009import sailpoint.api.IncrementalProjectionIterator; 010import sailpoint.api.Meter; 011import sailpoint.api.SailPointContext; 012import sailpoint.object.*; 013import sailpoint.tools.GeneralException; 014import sailpoint.tools.Util; 015 016import java.sql.Connection; 017import java.sql.SQLException; 018import java.util.ArrayList; 019import java.util.Date; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.concurrent.atomic.AtomicInteger; 027 028public class ExportLinksPartition extends ExportPartition { 029 030 protected static final String ATTRIBUTE_VALUE_FIELD = "attributeValue"; 031 032 public static final String DELETE_LINK = 033 "delete from de_link where id = :id"; 034 035 public static final String DELETE_LINK_ATTRS = 036 "delete from de_link_attr where id = :id"; 037 038 /** 039 * The application name used to specify fields we do not want to export on EVERY type of account 040 */ 041 protected static final String GLOBAL_SETTING = "global"; 042 043 private static final String INSERT_LINK = 044 "insert into de_link " + 045 "( id, identity_id, application, native_identity, created, modified, last_refresh, de_timestamp ) " + 046 "values ( :id, :identityId, :application, :nativeIdentity, :created, :modified, :lastRefresh, :now )"; 047 048 private static final String INSERT_LINK_ATTR = 049 "insert into de_link_attr ( id, attribute_name, attribute_value ) values ( :id, :attributeName, :attributeValue )"; 050 051 public static final int LINKS_BATCH_SIZE = 25; 052 public static final String METER_FETCH = "ExportLinkPartition.fetch"; 053 public static final String METER_LINK = "ExportLinkPartition.link"; 054 public static final String METER_STORE = "ExportLinkPartition.store"; 055 056 public static final String REGEX_PREFIX = "regex:"; 057 058 /** 059 * Exports the identified Link objects to the export table 060 * @param context The context 061 * @param connection The connection to the target database 062 * @param logger The logger 063 * @throws GeneralException if there are any failures 064 */ 065 @Override 066 protected void export(SailPointContext context, Connection connection, Log logger) throws GeneralException { 067 // Mapped from Application name to a set of column names 068 Map<String, Set<String>> excludedByApplication = getExcludedColumnsByApplication(context); 069 070 Date exportDate = new Date(exportTimestamp); 071 072 QueryOptions qo = new QueryOptions(); 073 qo.addFilter(Filter.compile(filterString)); 074 if (Util.isNotNullOrEmpty(filterString2)) { 075 qo.addFilter(Filter.compile(filterString2)); 076 } 077 qo.addFilter(Filter.or(Filter.gt("created", new Date(cutoffDate)), Filter.gt("modified", new Date(cutoffDate)))); 078 qo.setCacheResults(false); 079 qo.setTransactionLock(false); 080 081 TaskUtil.withLockedPartitionResult(monitor, (partitionResult) -> { 082 monitor.updateProgress(partitionResult, "Executing query", -1); 083 }); 084 085 List<String> projectionFields = new ArrayList<>(); 086 projectionFields.add("id"); // 0 087 projectionFields.add("nativeIdentity"); 088 projectionFields.add("application.name"); 089 projectionFields.add("identity.id"); 090 projectionFields.add("displayName"); 091 projectionFields.add("created"); // 5 092 projectionFields.add("modified"); 093 projectionFields.add("lastRefresh"); 094 projectionFields.add("attributes"); 095 096 long count = context.countObjects(Link.class, qo); 097 098 IncrementalProjectionIterator links = new IncrementalProjectionIterator(context, Link.class, qo, projectionFields); 099 100 int batchCount = 0; 101 AtomicInteger totalCount = new AtomicInteger(); 102 103 Map<String, Schema> schemaMap = new HashMap<>(); 104 105 try (NamedParameterStatement deleteAttrs = new NamedParameterStatement(connection, DELETE_LINK_ATTRS); NamedParameterStatement deleteLink = new NamedParameterStatement(connection, DELETE_LINK); NamedParameterStatement insertLink = new NamedParameterStatement(connection, INSERT_LINK); NamedParameterStatement insertAttribute = new NamedParameterStatement(connection, INSERT_LINK_ATTR)) { 106 while (links.hasNext()) { 107 if (isTerminated()) { 108 logger.info("Thread has been terminated; exiting cleanly"); 109 break; 110 } 111 Meter.enterByName(METER_LINK); 112 try { 113 Meter.enterByName(METER_FETCH); 114 Object[] link = links.next(); 115 Meter.exitByName(METER_FETCH); 116 117 String linkId = Util.otoa(link[0]); 118 String nativeIdentity = Util.otoa(link[1]); 119 String applicationName = Util.otoa(link[2]); 120 String identityId = Util.otoa(link[3]); 121 String displayName = Util.otoa(link[4]); 122 123 Date created = (Date) link[5]; 124 Date modified = (Date) link[6]; 125 Date lastRefresh = (Date) link[7]; 126 127 @SuppressWarnings("unchecked") 128 Attributes<String, Object> attributes = (Attributes<String, Object>) link[8]; 129 130 if (logger.isTraceEnabled()) { 131 logger.trace("Exporting Link " + linkId + ": " + applicationName + " " + nativeIdentity); 132 } 133 134 deleteLink.setString("id", linkId); 135 deleteLink.addBatch(); 136 137 deleteAttrs.setString("id", linkId); 138 deleteAttrs.addBatch(); 139 140 insertLink.setString("id", linkId); 141 if (identityId != null) { 142 insertLink.setString("identityId", identityId); 143 } else { 144 logger.warn("Link with ID " + linkId + " is orphaned and does not have an Identity"); 145 continue; 146 } 147 insertLink.setString("application", applicationName); 148 insertLink.setString("nativeIdentity", nativeIdentity); 149 150 addCommonDateFields(insertLink, exportDate, created, modified, lastRefresh); 151 152 insertLink.addBatch(); 153 154 if (!schemaMap.containsKey(applicationName)) { 155 Application application = context.getObjectByName(Application.class, applicationName); 156 schemaMap.put(applicationName, application.getAccountSchema()); 157 } 158 159 Schema schema = schemaMap.get(applicationName); 160 161 Set<String> excludedColumns = excludedByApplication.get(applicationName); 162 163 for (AttributeDefinition attribute : schema.getAttributes()) { 164 String attrName = attribute.getName(); 165 166 if (excludedColumns != null) { 167 boolean excluded = excludedColumns.contains(attrName); 168 if (excluded) { 169 continue; 170 } 171 } 172 173 Object value = attributes.get(attrName); 174 if (!Utilities.isNothing(value)) { 175 insertAttribute.setString("id", linkId); 176 insertAttribute.setString("attributeName", attrName); 177 178 if (attribute.isMulti()) { 179 for (String val : Util.otol(value)) { 180 insertAttribute.setString(ATTRIBUTE_VALUE_FIELD, Util.truncate(val, 4000)); 181 insertAttribute.addBatch(); 182 } 183 } else { 184 insertAttribute.setString(ATTRIBUTE_VALUE_FIELD, Util.truncate(Util.otoa(value), 4000)); 185 insertAttribute.addBatch(); 186 } 187 } 188 } 189 190 if (batchCount++ > LINKS_BATCH_SIZE) { 191 Meter.enterByName(METER_STORE); 192 try { 193 deleteLink.executeBatch(); 194 deleteAttrs.executeBatch(); 195 insertLink.executeBatch(); 196 insertAttribute.executeBatch(); 197 198 connection.commit(); 199 } finally { 200 Meter.exitByName(METER_STORE); 201 } 202 batchCount = 0; 203 } 204 205 int currentCount = totalCount.incrementAndGet(); 206 if ((currentCount % 100) == 0) { 207 TaskUtil.withLockedPartitionResult(monitor, (partitionResult) -> { 208 monitor.updateProgress(partitionResult, "Processed " + currentCount + " of " + count + " links", -1); 209 partitionResult.setInt("exportedLinks", currentCount); 210 }); 211 } 212 } finally{ 213 Meter.exitByName(METER_LINK); 214 } 215 } 216 217 deleteLink.executeBatch(); 218 deleteAttrs.executeBatch(); 219 insertLink.executeBatch(); 220 insertAttribute.executeBatch(); 221 222 connection.commit(); 223 } catch(SQLException e) { 224 throw new GeneralException(e); 225 } 226 } 227 228 /** 229 * If the given timestamp is positive, a Date representing that timestamp 230 * will be returned. Otherwise, null will be returned. 231 * 232 * @param timestamp The timestamp 233 * @return The date, or null 234 */ 235 private Date toDate(long timestamp) { 236 if (timestamp > 0) { 237 return new Date(timestamp); 238 } else { 239 return null; 240 } 241 } 242 243 /** 244 * Builds the map of excluded attributes by application. This can be mapped as a global 245 * list, as regular expressions on the name, as the name itself, or as the connector type. 246 * 247 * @param context The Sailpoint context for querying the DB 248 * @return The resulting set of exclusions 249 * @throws GeneralException if any failures occur reading the Application objects 250 */ 251 private Map<String, Set<String>> getExcludedColumnsByApplication(SailPointContext context) throws GeneralException { 252 Map<String, Set<String>> excludeLinkColumns = new HashMap<>(); 253 254 if (configuration.containsAttribute("excludeLinkColumns")) { 255 Object config = configuration.get("excludeLinkColumns"); 256 if (config instanceof Map) { 257 258 Map<String, Object> mapConfig = (Map<String, Object>) config; 259 260 List<Application> allApplications = context.getObjects(Application.class); 261 for(Application a : allApplications) { 262 Set<String> mergedExclude = getMergedExcludeSet(a, mapConfig); 263 if (!mergedExclude.isEmpty()) { 264 excludeLinkColumns.put(a.getName(), mergedExclude); 265 } 266 } 267 } else { 268 throw new GeneralException("Invalid configuration: excludeLinkColumns must be an instance of a Map"); 269 } 270 } 271 272 return excludeLinkColumns; 273 } 274 275 /** 276 * Merges together the various exclusion lists that apply to this application. 277 * 278 * @param application The application to find exclusion lists for 279 * @param excludeLinkColumns The resulting merged set of exclusions 280 * @return if any failures occur 281 */ 282 private Set<String> getMergedExcludeSet(Application application, Map<String, Object> excludeLinkColumns) { 283 List<String> globalSet = Util.otol(excludeLinkColumns.get(GLOBAL_SETTING)); 284 List<String> typeSpecific = Util.otol(excludeLinkColumns.get("connector:" + application.getType())); 285 286 287 Set<String> merged = new HashSet<>(); 288 if (globalSet != null) { 289 merged.addAll(globalSet); 290 } 291 292 if (typeSpecific != null) { 293 merged.addAll(typeSpecific); 294 } 295 296 String applicationName = application.getName(); 297 298 for(String key : excludeLinkColumns.keySet()) { 299 List<String> colsForKey = Util.otol(excludeLinkColumns.get(key)); 300 301 if (key.startsWith(REGEX_PREFIX)) { 302 String expression = key.substring(REGEX_PREFIX.length()); 303 if (Util.isNotNullOrEmpty(expression) && applicationName.matches(expression)) { 304 merged.addAll(colsForKey); 305 } 306 } else if (applicationName.equalsIgnoreCase(key)) { 307 merged.addAll(colsForKey); 308 } 309 } 310 311 return merged; 312 } 313}