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