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}