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