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 deleteLink.setString("id", linkId); 160 deleteLink.addBatch(); 161 162 deleteAttrs.setString("id", linkId); 163 deleteAttrs.addBatch(); 164 165 insertLink.setString("id", linkId); 166 if (identityId != null) { 167 insertLink.setString("identityId", identityId); 168 } else { 169 logger.warn("Link with ID " + linkId + " is orphaned and does not have an Identity"); 170 continue; 171 } 172 insertLink.setString("application", applicationName); 173 insertLink.setString("nativeIdentity", nativeIdentity); 174 175 addCommonDateFields(insertLink, exportDate, created, modified, lastRefresh); 176 177 insertLink.addBatch(); 178 179 linksInBatch.add(applicationName + ": " + nativeIdentity); 180 181 if (!schemaMap.containsKey(applicationName)) { 182 Application application = context.getObjectByName(Application.class, applicationName); 183 schemaMap.put(applicationName, application.getAccountSchema()); 184 } 185 186 Schema schema = schemaMap.get(applicationName); 187 188 Set<String> excludedColumns = excludedByApplication.get(applicationName); 189 190 for (ObjectAttribute attribute : linkConfig.getObjectAttributes()) { 191 if (attribute.isSystem() || attribute.isStandard()) { 192 continue; 193 } 194 195 String attrName = attribute.getName(); 196 197 if (excludedColumns != null) { 198 boolean excluded = excludedColumns.contains(attrName); 199 if (excluded) { 200 continue; 201 } 202 } 203 204 Object value = attributes.get(attrName); 205 if (!Utilities.isNothing(value)) { 206 insertAttribute.setString("id", linkId); 207 insertAttribute.setString("attributeName", attrName); 208 209 if (attribute.isMulti()) { 210 for (String val : Util.otol(value)) { 211 insertAttribute.setString(ATTRIBUTE_VALUE_FIELD, Util.truncate(val, 3996)); 212 insertAttribute.addBatch(); 213 } 214 } else { 215 insertAttribute.setString(ATTRIBUTE_VALUE_FIELD, Util.truncate(Util.otoa(value), 3996)); 216 insertAttribute.addBatch(); 217 } 218 } 219 } 220 221 for (AttributeDefinition attribute : schema.getAttributes()) { 222 String attrName = attribute.getName(); 223 224 if (excludedColumns != null) { 225 boolean excluded = excludedColumns.contains(attrName); 226 if (excluded) { 227 continue; 228 } 229 } 230 231 Object value = attributes.get(attrName); 232 if (!Utilities.isNothing(value)) { 233 insertAttribute.setString("id", linkId); 234 insertAttribute.setString("attributeName", attrName); 235 236 if (attribute.isMulti()) { 237 for (String val : Util.otol(value)) { 238 insertAttribute.setString(ATTRIBUTE_VALUE_FIELD, Utilities.truncateStringToBytes(val, 4000, StandardCharsets.UTF_8)); 239 insertAttribute.addBatch(); 240 } 241 } else { 242 insertAttribute.setString(ATTRIBUTE_VALUE_FIELD, Utilities.truncateStringToBytes(Util.otoa(value), 4000, StandardCharsets.UTF_8)); 243 insertAttribute.addBatch(); 244 } 245 } 246 } 247 248 if (batchCount++ > linkBatchSize) { 249 Meter.enterByName(METER_STORE); 250 try { 251 deleteLink.executeBatch(); 252 deleteAttrs.executeBatch(); 253 insertLink.executeBatch(); 254 insertAttribute.executeBatch(); 255 256 connection.commit(); 257 } catch(SQLException e) { 258 logger.error("Caught an error committing a batch containing these accounts: " + linksInBatch, e); 259 throw e; 260 } finally { 261 linksInBatch.clear(); 262 Meter.exitByName(METER_STORE); 263 } 264 batchCount = 0; 265 } 266 267 int currentCount = totalCount.incrementAndGet(); 268 if ((currentCount % 100) == 0) { 269 TaskUtil.withLockedPartitionResult(monitor, (partitionResult) -> { 270 monitor.updateProgress(partitionResult, "Processed " + currentCount + " of " + count + " links", -1); 271 partitionResult.setInt("exportedLinks", currentCount); 272 }); 273 } 274 } finally{ 275 Meter.exitByName(METER_LINK); 276 } 277 } 278 279 try { 280 deleteLink.executeBatch(); 281 deleteAttrs.executeBatch(); 282 insertLink.executeBatch(); 283 insertAttribute.executeBatch(); 284 285 connection.commit(); 286 } catch(SQLException e) { 287 logger.error("Caught an error committing a batch containing these accounts: " + linksInBatch, e); 288 throw e; 289 } 290 } catch(SQLException e) { 291 throw new GeneralException(e); 292 } 293 } 294 295 /** 296 * If the given timestamp is positive, a Date representing that timestamp 297 * will be returned. Otherwise, null will be returned. 298 * 299 * @param timestamp The timestamp 300 * @return The date, or null 301 */ 302 private Date toDate(long timestamp) { 303 if (timestamp > 0) { 304 return new Date(timestamp); 305 } else { 306 return null; 307 } 308 } 309 310 /** 311 * Builds the map of excluded attributes by application. This can be mapped as a global 312 * list, as regular expressions on the name, as the name itself, or as the connector type. 313 * 314 * @param context The Sailpoint context for querying the DB 315 * @return The resulting set of exclusions 316 * @throws GeneralException if any failures occur reading the Application objects 317 */ 318 private Map<String, Set<String>> getExcludedColumnsByApplication(SailPointContext context) throws GeneralException { 319 Map<String, Set<String>> excludeLinkColumns = new HashMap<>(); 320 321 if (configuration.containsAttribute("excludeLinkColumns")) { 322 Object config = configuration.get("excludeLinkColumns"); 323 if (config instanceof Map) { 324 Map<String, Object> mapConfig = (Map<String, Object>) config; 325 326 List<Application> allApplications = context.getObjects(Application.class); 327 for(Application a : allApplications) { 328 Set<String> mergedExclude = getMergedExcludeSet(a, mapConfig); 329 if (!mergedExclude.isEmpty()) { 330 excludeLinkColumns.put(a.getName(), mergedExclude); 331 } 332 } 333 } else { 334 throw new GeneralException("Invalid configuration: excludeLinkColumns must be an instance of a Map"); 335 } 336 } 337 338 return excludeLinkColumns; 339 } 340 341 /** 342 * Merges together the various exclusion lists that apply to this application. 343 * 344 * @param application The application to find exclusion lists for 345 * @param excludeLinkColumns The resulting merged set of exclusions 346 * @return if any failures occur 347 */ 348 private Set<String> getMergedExcludeSet(Application application, Map<String, Object> excludeLinkColumns) { 349 List<String> globalSet = Util.otol(excludeLinkColumns.get(GLOBAL_SETTING)); 350 List<String> typeSpecific = Util.otol(excludeLinkColumns.get("connector:" + application.getType())); 351 352 353 Set<String> merged = new HashSet<>(); 354 if (globalSet != null) { 355 merged.addAll(globalSet); 356 } 357 358 if (typeSpecific != null) { 359 merged.addAll(typeSpecific); 360 } 361 362 String applicationName = application.getName(); 363 364 for(String key : excludeLinkColumns.keySet()) { 365 List<String> colsForKey = Util.otol(excludeLinkColumns.get(key)); 366 367 if (key.startsWith(REGEX_PREFIX)) { 368 String expression = key.substring(REGEX_PREFIX.length()); 369 if (Util.isNotNullOrEmpty(expression) && applicationName.matches(expression)) { 370 merged.addAll(colsForKey); 371 } 372 } else if (applicationName.equalsIgnoreCase(key)) { 373 merged.addAll(colsForKey); 374 } 375 } 376 377 return merged; 378 } 379}