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}