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}