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