001package com.identityworksllc.iiq.common.service;
002
003import com.identityworksllc.iiq.common.Utilities;
004import org.apache.commons.logging.Log;
005import org.apache.commons.logging.LogFactory;
006import sailpoint.api.IncrementalObjectIterator;
007import sailpoint.api.SailPointContext;
008import sailpoint.object.Application;
009import sailpoint.object.Custom;
010import sailpoint.object.Filter;
011import sailpoint.object.PersistenceOptions;
012import sailpoint.object.QueryOptions;
013import sailpoint.object.Server;
014import sailpoint.object.ServiceDefinition;
015import sailpoint.server.Service;
016import sailpoint.server.ServicerUtil;
017import sailpoint.tools.GeneralException;
018import sailpoint.tools.Util;
019
020import java.util.Date;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024
025/**
026 * A service to retain the last aggregation timestamps and other cache data
027 * for Applications in a Custom object called "Aggregation Date Storage".
028 *
029 * Since this data is stored in the Application XML itself, it is often inadvertently
030 * overwritten on deployment with an earlier version, or with null data. This will
031 * cause delta tasks to run long or fail entirely. With this service installed,
032 * the Application's dates will be restored as soon as possible after deployment.
033 *
034 * The data will be copied as follows:
035 *
036 * - If there is no data on the Application, but data exists in the Custom object,
037 *   the values on the Application will be replaced by the values in the Custom.
038 *
039 * - If there is no cache for the given Application in the Custom object, the values
040 *   in the Custom will always be set to the Application's values.
041 *
042 * - If the Application's acctAggregationEnd timestamp is newer than the values in the
043 *   Custom object, the Application's values will be copied to the Custom.
044 *
045 * - If the Custom's acctAggregationEnd timestamp is newer than the value in the
046 *   Application object, the Application's values will be replaced by the value
047 *   in the Custom.
048 *
049 * The following attributes are retained in the Custom for each Application:
050 *
051 * - acctAggregationEnd
052 * - acctAggregationStart
053 * - deltaAggregation
054 * - lastAggregationDate
055 *
056 * Three of the four are millisecond timestamps, while deltaAggregation often
057 * contains metadata, like cookies from Azure.
058 */
059public class AggregationDateRetentionService extends Service {
060
061    /**
062     * The field indicating aggregation end (a Date)
063     */
064    public static final String ACCT_AGGREGATION_END = "acctAggregationEnd";
065
066    /**
067     * The field indicating aggregation start (a Date)
068     */
069    public static final String ACCT_AGGREGATION_START = "acctAggregationStart";
070
071    /**
072     * The name of the Custom object storing application aggregation dates
073     */
074    private static final String CUSTOM_NAME = "Aggregation Date Storage";
075
076    /**
077     * The field containing delta aggregation details (e.g., dirSync data in AD)
078     */
079    public static final String DELTA_AGGREGATION = "deltaAggregation";
080
081    /**
082     * The last aggregation date column (used in, e.g., Workday)
083     */
084    public static final String LAST_AGGREGATION_DATE = "lastAggregationDate";
085
086    private static final Log log = LogFactory.getLog(AggregationDateRetentionService.class);
087
088    @Override
089    public void execute(SailPointContext context) throws GeneralException {
090        ServiceDefinition self = getDefinition();
091        Server target = null;
092        QueryOptions qo = new QueryOptions();
093        qo.setDirtyRead(false);
094        qo.setCacheResults(false);
095        qo.setCloneResults(true);
096        qo.addOrdering("name", true);
097        if (!Util.nullSafeCaseInsensitiveEq(self.getHosts(), "global")) {
098            qo.addFilter(Filter.in("name", Util.csvToList(self.getHosts())));
099        }
100        List<Server> servers = context.getObjects(Server.class, qo);
101        for(Server s : Util.safeIterable(servers)) {
102            if (!s.isInactive() && ServicerUtil.isServiceAllowedOnServer(context, self, s.getName())) {
103                target = s;
104                break;
105            }
106        }
107        if (target == null) {
108            // This would be VERY strange, since we are, in fact, running the service
109            // right now, in this very code
110            log.warn("There does not appear to be an active server allowed to run service " + self.getName());
111        }
112        String hostname = Util.getHostName();
113        if (target == null || target.getName().equals(hostname)) {
114            Utilities.withPrivateContext((privateContext) -> {
115                PersistenceOptions po = new PersistenceOptions();
116                po.setExplicitSaveMode(true);
117
118                privateContext.setPersistenceOptions(po);
119
120                implementation(privateContext);
121            });
122        }
123    }
124
125    /**
126     * Main method for the service, invoked if we are the alphabetically lowest host.
127     *
128     * Queries each application's last aggregation timestamps. If the application's
129     * data is missing or outdated, and there is a retained value, this method restores
130     * the retained value to the Application. If there is no retained value, or if the
131     * retained data is older than the Application data, the retention Custom is updated.
132     *
133     * @param context IIQ context
134     * @throws GeneralException if any failures occur
135     */
136    private void implementation(SailPointContext context) throws GeneralException {
137        Custom custom = context.getObjectByName(Custom.class, CUSTOM_NAME);
138        if (custom == null) {
139            custom = new Custom();
140            custom.setName(CUSTOM_NAME);
141        }
142
143        QueryOptions qo = new QueryOptions();
144        qo.addOrdering("name", true);
145
146        IncrementalObjectIterator<Application> iterator = new IncrementalObjectIterator<>(context, Application.class, qo);
147
148        while(iterator.hasNext()) {
149            Application application = iterator.next();
150
151            boolean updateCustom = false;
152            boolean updateApplication = false;
153
154            Map<String, Object> existingRecord = (Map<String, Object>) custom.get(application.getName());
155            Date lastAggregationEnd = (Date) application.getAttributeValue(ACCT_AGGREGATION_END);
156
157            if (lastAggregationEnd != null) {
158                if (existingRecord != null) {
159                    Date existingLastRunEnd = (Date) existingRecord.get(ACCT_AGGREGATION_END);
160                    if (existingLastRunEnd.before(lastAggregationEnd)) {
161                        updateCustom = true;
162                    } else {
163                        updateApplication = true;
164                    }
165                } else {
166                    updateCustom = true;
167                }
168            } else if (existingRecord != null) {
169                updateApplication = true;
170            }
171
172            if (updateCustom) {
173                if (log.isDebugEnabled()) {
174                    log.debug("Updating Date Retention Custom object for " + application.getName());
175                }
176                Map<String, Object> appData = new HashMap<>();
177                appData.put(ACCT_AGGREGATION_END, application.getAttributeValue(ACCT_AGGREGATION_END));
178                appData.put(ACCT_AGGREGATION_START, application.getAttributeValue(ACCT_AGGREGATION_START));
179                appData.put(DELTA_AGGREGATION, application.getAttributeValue(DELTA_AGGREGATION));
180                appData.put(LAST_AGGREGATION_DATE, application.getAttributeValue(LAST_AGGREGATION_DATE));
181                custom.put(application.getName(), appData);
182            }
183
184            if (updateApplication) {
185                if (log.isDebugEnabled()) {
186                    log.debug("Updating Application data for " + application.getName());
187                }
188                application.setAttribute(ACCT_AGGREGATION_END, existingRecord.get(ACCT_AGGREGATION_END));
189                application.setAttribute(ACCT_AGGREGATION_START, existingRecord.get(ACCT_AGGREGATION_START));
190                application.setAttribute(DELTA_AGGREGATION, existingRecord.get(DELTA_AGGREGATION));
191                application.setAttribute(LAST_AGGREGATION_DATE, existingRecord.get(LAST_AGGREGATION_DATE));
192                context.saveObject(application);
193            }
194        }
195
196        context.saveObject(custom);
197
198        context.commitTransaction();
199    }
200}