001package com.identityworksllc.iiq.common.password;
002
003import com.identityworksllc.iiq.common.logging.SLogger;
004import org.apache.commons.logging.Log;
005import sailpoint.api.PasswordPolice;
006import sailpoint.api.PasswordPolicyException;
007import sailpoint.api.SailPointContext;
008import sailpoint.api.passwordConstraints.PasswordConstraint;
009import sailpoint.object.Identity;
010import sailpoint.object.PasswordPolicy;
011import sailpoint.tools.GeneralException;
012
013import java.lang.reflect.Field;
014import java.util.ArrayList;
015import java.util.List;
016import java.util.Locale;
017import java.util.TimeZone;
018
019/**
020 * An extension of the OOTB PasswordPolice that allows adding custom constraints. Note that you
021 * should NOT reuse an instance of this class to check two different users' passwords, as the
022 * superclass does some odd stuff.
023 */
024public class ExtendedPasswordPolice extends PasswordPolice {
025    /**
026     * Logger
027     */
028    private static final SLogger log = new SLogger(ExtendedPasswordPolice.class);
029
030    /**
031     * The extra rules that we've added
032     */
033    private final List<ExtendedPasswordConstraint> extraRules;
034
035    /**
036     * Constructs a new ExtendedPasswordPolice with an empty list of extra rules.
037     *
038     * @param con the SailPointContext
039     * @throws GeneralException if there is an error initializing the PasswordPolice
040     */
041    public ExtendedPasswordPolice(SailPointContext con) throws GeneralException {
042        super(con);
043        this.extraRules = new ArrayList<>();
044    }
045
046    /**
047     * Constructs a new ExtendedPasswordPolice with the given PasswordPolicy and an empty list of extra rules.
048     *
049     * @param con the SailPointContext
050     * @param policy the PasswordPolicy to use
051     * @throws GeneralException if there is an error initializing the PasswordPolice
052     */
053    public ExtendedPasswordPolice(SailPointContext con, PasswordPolicy policy) throws GeneralException {
054        super(con, policy);
055
056        this.extraRules = new ArrayList<>();
057    }
058
059    /**
060     * Adds a new password constraint to the list of rules. Since the _rules field is private,
061     * we use reflection to access it and add the new constraint.
062     *
063     * There is no uniqueness check, so if you call this method twice with the same
064     * constraint, you will end up wasting your time.
065     *
066     * @param constraint the ExtendedPasswordConstraint to add
067     * @throws GeneralException if there is an error accessing the _rules field
068     */
069    @SuppressWarnings("unchecked")
070    public void addConstraint(ExtendedPasswordConstraint constraint) throws GeneralException {
071        try {
072            Field rulesField = PasswordPolice.class.getDeclaredField("_rules");
073            rulesField.setAccessible(true);
074
075            List<PasswordConstraint> rules = (List<PasswordConstraint>) rulesField.get(this);
076            rules.add(constraint);
077
078            if (log.isDebugEnabled()) {
079                log.debug("Added new password constraint: " + constraint.getDescription());
080            }
081        } catch(Exception e) {
082            throw new GeneralException("Failed to access _rules field in PasswordPolice", e);
083        }
084    }
085
086    @Override
087    public void checkPassword(Identity identity, String password, boolean isSystemAdmin) throws GeneralException {
088        if (log.isDebugEnabled()) {
089            log.debug("Checking password for identity: " + identity.getName() + ", isSystemAdmin: " + isSystemAdmin);
090        }
091        super.checkPassword(identity, password, isSystemAdmin);
092    }
093
094    /**
095     * Generates the descriptions of the various configured constraints, including any of our
096     * extra rules.
097     *
098     * @param locale the locale to use for formatting
099     * @param timeZone the time zone to use for formatting
100     * @param showNoConstraintMessage whether to show the "no constraints" message
101     * @return a list of constraint descriptions
102     */
103    @Override
104    public List<String> getIIQPasswordConstraints(Locale locale, TimeZone timeZone, boolean showNoConstraintMessage) throws GeneralException {
105        // If we have extra rules, we never want to show the "no constraints" message
106        boolean reallyShowNoConstraintMessage = showNoConstraintMessage && this.extraRules.isEmpty();
107
108        List<String> constraints = super.getIIQPasswordConstraints(locale, timeZone, reallyShowNoConstraintMessage);
109        for (ExtendedPasswordConstraint constraint : this.extraRules) {
110            constraints.add(constraint.getDescription());
111        }
112        return constraints;
113    }
114
115    /**
116     * Validates the current state of the PasswordPolice, ensuring that any extra rules
117     * have been set to admin mode if the _admin field is true.
118     *
119     * Note that this will re-add all of the OOTB constraints to _rules, which suggests that
120     * SP doesn't particularly care about efficiency here.
121     *
122     * @throws GeneralException if there is an error accessing the _admin field
123     * @throws sailpoint.api.PasswordPolicyException if there are validation errors in the password policy
124     */
125    @Override
126    public void validate() throws GeneralException {
127        try {
128            Field adminField = PasswordPolice.class.getDeclaredField("_admin");
129            adminField.setAccessible(true);
130
131            boolean isAdmin = (boolean) adminField.get(this);
132
133            if (isAdmin) {
134                if (log.isDebugEnabled()) {
135                    log.debug("Setting admin mode for all extra password constraints");
136                }
137                for(ExtendedPasswordConstraint constraint : this.extraRules) {
138                    constraint.setAdmin(true);
139                }
140            }
141        } catch(Exception e) {
142            throw new GeneralException("Failed to access _admin field in PasswordPolice", e);
143        }
144        try {
145            super.validate();
146        } catch(PasswordPolicyException e) {
147            if (log.isDebugEnabled()) {
148                log.debug("Password policy validation failed: {0}", e);
149            }
150        }
151    }
152}