001package com.identityworksllc.iiq.common; 002 003import com.identityworksllc.iiq.common.annotation.Experimental; 004import com.identityworksllc.iiq.common.query.ContextConnectionWrapper; 005import sailpoint.api.Identitizer; 006import sailpoint.api.ObjectUtil; 007import sailpoint.api.SailPointContext; 008import sailpoint.api.SailPointFactory; 009import sailpoint.object.Attributes; 010import sailpoint.object.Filter; 011import sailpoint.object.Identity; 012import sailpoint.object.ObjectAttribute; 013import sailpoint.object.ObjectConfig; 014import sailpoint.object.QueryOptions; 015import sailpoint.tools.GeneralException; 016import sailpoint.tools.Util; 017 018import java.sql.Connection; 019import java.sql.PreparedStatement; 020import java.sql.SQLException; 021import java.util.ArrayList; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Map; 025 026/** 027 * Utilities for handling Identity operations 028 */ 029public class BaseIdentityUtilities extends AbstractBaseUtility { 030 031 public BaseIdentityUtilities(SailPointContext context) { 032 super(context); 033 } 034 035 /** 036 * Gets the default set of refresh options, with or without process-events. 037 * 038 * The refresh options set to true are: 039 * 040 * - provision 041 * - correlateEntitlements 042 * - promoteManagedAttributes 043 * - refreshRoleMetadata 044 * - promoteAttributes 045 * - synchronizeAttributes 046 * - refreshManagerStatus 047 * - noResetNeedsRefresh 048 * - refreshProvisioningRequests 049 * - checkHistory 050 * 051 * If the provided _shouldProcessEvents_ is true, then _processTriggers_ will also be 052 * set to true. This is optional because triggers can prolong a refresh considerably. 053 * 054 * @param shouldProcessEvents True if we should also process events, false if not 055 * @return A new Attributes with the default set of refresh options 056 */ 057 public Attributes<String, Object> getDefaultRefreshOptions(boolean shouldProcessEvents) { 058 Attributes<String, Object> args = new Attributes<>(); 059 args.put(Identitizer.ARG_PROVISION, true); 060 args.put(Identitizer.ARG_CORRELATE_ENTITLEMENTS, true); 061 args.put(Identitizer.ARG_PROCESS_TRIGGERS, shouldProcessEvents); 062 args.put(Identitizer.ARG_PROMOTE_MANAGED_ATTRIBUTES, true); 063 args.put(Identitizer.ARG_REFRESH_ROLE_METADATA, true); 064 args.put(Identitizer.ARG_PROMOTE_ATTRIBUTES, true); 065 args.put(Identitizer.ARG_SYNCHRONIZE_ATTRIBUTES, true); 066 args.put(Identitizer.ARG_REFRESH_MANAGER_STATUS, true); 067 args.put(Identitizer.ARG_NO_RESET_NEEDS_REFRESH, true); 068 args.put(Identitizer.ARG_REFRESH_PROVISIONING_REQUESTS, true); 069 args.put(Identitizer.ARG_CHECK_HISTORY, true); 070 return args; 071 } 072 073 /** 074 * Returns true if the user has at least one of the detected role 075 * @param identity The identity to check 076 * @param roleName The role name to look for 077 * @return true if the user has at least one detected role of this name 078 */ 079 public boolean hasDetectedRole(Identity identity, String roleName) { 080 long count = Utilities.safeStream(identity.getRoleDetections()).filter(rd -> Util.nullSafeEq(rd.getRoleName(), roleName)).count(); 081 return (count > 0); 082 } 083 084 /** 085 * Returns true if the user has the given role more than one time (either via assignment or detection or both) 086 * @param identity The identity to check 087 * @param roleName The role name to look for 088 * @return true if the user has at least two assigned/detected roles of this name 089 */ 090 public boolean hasMultiple(Identity identity, String roleName) { 091 long assignedCount = Utilities.safeStream(identity.getRoleAssignments()).filter(rd -> Util.nullSafeEq(rd.getRoleName(), roleName)).count(); 092 long detectedCount = Utilities.safeStream(identity.getRoleAssignments()).filter(rd -> Util.nullSafeEq(rd.getRoleName(), roleName)).count(); 093 return (assignedCount + detectedCount) > 1; 094 } 095 096 /** 097 * Transforms the existing Map in place by replacing attributes of type Secret with asterisks 098 * @param attributes The attribute map to modify 099 */ 100 public void maskSecretAttributes(Map<String, Object> attributes) { 101 ObjectConfig identityObjectConfig = Identity.getObjectConfig(); 102 for(ObjectAttribute attribute : identityObjectConfig.getObjectAttributes()) { 103 if (attribute.getType().equals(ObjectAttribute.TYPE_SECRET)) { 104 if (attributes.containsKey(attribute.getName())) { 105 attributes.put(attribute.getName(), "********"); 106 } 107 } 108 } 109 } 110 111 /** 112 * Returns a recursive list of all subordinates of the given Identity by recursively navigating 113 * other Identity objects starting with this one as their 'manager'. 114 * 115 * @param parent The parent Identity 116 * @return A list of object arrays, containing the 'id' and 'name' of any Identities 117 * @throws GeneralException if this fails 118 */ 119 public List<Object[]> recursivelyExplodeHierarchy(Identity parent) throws GeneralException { 120 return recursivelyExplodeHierarchy(parent.getId(), "manager"); 121 } 122 123 /** 124 * Returns the entire tree below the 'parent' Identity by recursively querying for other 125 * objects that reference it via the given attribute. For example, this might return 126 * a manager's entire tree of subordinates. 127 * 128 * @param parent an Identity ID to search in the given attribute 129 * @param attribute the attribute containing an Identity ID reference (e.g., `manager`) 130 * @return A list of object arrays, containing the 'id' and 'name' of any Identities 131 * @throws GeneralException if this fails 132 */ 133 public List<Object[]> recursivelyExplodeHierarchy(String parent, String attribute) throws GeneralException { 134 List<Object[]> outputBucket = new ArrayList<>(); 135 QueryOptions qo = new QueryOptions(); 136 qo.addFilter(Filter.eq(attribute, parent)); 137 List<String> props = new ArrayList<>(); 138 props.add("id"); 139 props.add("name"); 140 Iterator<Object[]> subordinates = context.search(Identity.class, qo, props); 141 if (subordinates != null) { 142 try { 143 while (subordinates.hasNext()) { 144 Object[] so = subordinates.next(); 145 outputBucket.add(so); 146 outputBucket.addAll(recursivelyExplodeHierarchy((String)so[0], attribute)); 147 } 148 } finally { 149 Util.flushIterator(subordinates); 150 } 151 } 152 return outputBucket; 153 } 154 155 /** 156 * Recursively expands the input Identity, returning a list of workgroup members. If the input 157 * Identity is not a workgroup, it is returned alone. If any members of a workgroup are themselves 158 * workgroups, they will be recursively expanded. 159 * 160 * This can be used, for example, to send a notification to an entire workgroup. 161 * 162 * @param possibleWorkgroup an {@link Identity} object, which is likely a workgroup 163 * @return The list of Identities in the given workgroup, and any child workgroups 164 * @throws GeneralException if this fails 165 */ 166 public List<Identity> recursivelyExplodeWorkgroup(Identity possibleWorkgroup) throws GeneralException { 167 List<Identity> identities = new ArrayList<>(); 168 if (!possibleWorkgroup.isWorkgroup()) { 169 identities.add(possibleWorkgroup); 170 return identities; 171 } 172 List<String> props = new ArrayList<>(); 173 props.add("id"); 174 Iterator<Object[]> members = ObjectUtil.getWorkgroupMembers(context, possibleWorkgroup, props); 175 try { 176 while (members.hasNext()) { 177 Object[] dehydrated = members.next(); 178 Identity hydrated = ObjectUtil.getIdentityOrWorkgroup(context, (String) dehydrated[0]); 179 if (hydrated.isWorkgroup()) { 180 identities.addAll(recursivelyExplodeWorkgroup(hydrated)); 181 } else { 182 identities.add(hydrated); 183 } 184 } 185 } finally { 186 Util.flushIterator(members); 187 } 188 return identities; 189 } 190 191 /** 192 * Performs a refresh with default options on the identity 193 * @param id The identity in question 194 * @throws GeneralException if any IIQ failure occurs 195 */ 196 public void refresh(Identity id) throws GeneralException { 197 refresh(id, false); 198 } 199 200 /** 201 * Performs a refresh with mostly-default options on the identity 202 * @param id The identity to target 203 * @param shouldProcessEvents if true, processEvents will also be added 204 * @throws GeneralException if any IIQ failure occurs 205 */ 206 public void refresh(Identity id, boolean shouldProcessEvents) throws GeneralException { 207 Identity reloaded = context.getObjectById(Identity.class, id.getId()); 208 209 Attributes<String, Object> args = getDefaultRefreshOptions(shouldProcessEvents); 210 211 refresh(reloaded, args); 212 } 213 214 /** 215 * Performs a refresh against the identity with the given arguments 216 * @param id The target identity 217 * @param args the refresh arguments 218 * @throws GeneralException if any IIQ failure occurs 219 */ 220 public void refresh(Identity id, Map<String, Object> args) throws GeneralException { 221 Attributes<String, Object> attributes = new Attributes<>(); 222 attributes.putAll(args); 223 224 Identitizer identitizer = new Identitizer(context, attributes); 225 identitizer.refresh(id); 226 } 227 228 /** 229 * Attempt to do a best effort rename of a user. Note that this will not catch usernames stored in: 230 * 231 * (1) ProvisioningPlan objects 232 * (2) Running workflow variables 233 * 234 * @param target The Identity object to rename 235 * @param newName The new name of the identity 236 * @throws GeneralException if any renaming failures occur 237 */ 238 @Experimental 239 public void rename(Identity target, String newName) throws GeneralException { 240 // TODO: CertificationDefinition selfCertificationViolationOwner 241 // TODO there is probably more to do around certifications (e.g. Certification.getCertifiers()) 242 // CertificationItem has a getIdentity() but it depends on CertificationEntity, so we're fine 243 String[] queries = new String[] { 244 "update spt_application_activity set identity_name = ? where identity_name = ?", 245 // This needs to run twice, once for MSMITH and once for Identity:MSMITH 246 "update spt_audit_event set target = ? where target = ?", 247 "update spt_audit_event set source = ? where source = ?", 248 "update spt_identity_request set owner_name = ? where owner_name = ?", // TODO approver_name? 249 "update spt_identity_snapshot set identity_name = ? where identity_name = ?", 250 "update spt_provisioning_transaction set identity_name = ? where identity_name = ?", 251 "update spt_certification set creator = ? where creator = ?", 252 "update spt_certification set manager = ? where manager = ?", 253 // Oddly, this is actually the name, not the ID 254 "update spt_certification_entity set identity_id = ? where identity_id = ?", 255 "update spt_certification_item set target_name = ? where target_name = ?", 256 "update spt_identity_history_item set actor = ? where actor = ?", 257 "update spt_task_result set launcher = ? where launcher = ?", 258 259 }; 260 SailPointContext privateContext = SailPointFactory.createPrivateContext(); 261 try { 262 Identity privateIdentity = privateContext.getObjectById(Identity.class, target.getId()); 263 privateIdentity.setName(newName); 264 privateContext.saveObject(privateIdentity); 265 privateContext.commitTransaction(); 266 267 try (Connection db = ContextConnectionWrapper.getConnection(privateContext)) { 268 try { 269 db.setAutoCommit(false); 270 for (String query : queries) { 271 try (PreparedStatement stmt = db.prepareStatement(query)) { 272 stmt.setString(1, newName); 273 stmt.setString(2, target.getName()); 274 stmt.executeUpdate(); 275 } 276 } 277 db.commit(); 278 } finally { 279 db.setAutoCommit(true); 280 } 281 } catch(SQLException e) { 282 throw new GeneralException(e); 283 } 284 285 } catch(GeneralException e) { 286 privateContext.rollbackTransaction(); 287 } finally { 288 SailPointFactory.releasePrivateContext(privateContext); 289 } 290 291 } 292 293}