Skip to content

Commit

Permalink
New password retention option: historyAllowExistingPasswordReuse
Browse files Browse the repository at this point in the history
  • Loading branch information
martin-lizner committed Feb 23, 2021
1 parent d360701 commit c2d2922
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 5 deletions.
Expand Up @@ -1962,6 +1962,20 @@
</xsd:appinfo>
</xsd:annotation>
</xsd:element>
<xsd:element name="historyAllowExistingPasswordReuse" type="xsd:boolean" minOccurs="0" maxOccurs="1" default="false">
<xsd:annotation>
<xsd:documentation>
If set to true, then existing focus password (last set password) can be reused and set again to the same value as the new password.
However, when maxAge constraint is set, value cannot be reused after existing password has expired.
If set to false, user must always provide fresh password when setting the new password.
This setting is effective only when historyLength > 0.
Default behaviour is that reuse is not allowed (setting is false).
</xsd:documentation>
<xsd:appinfo>
<a:since>4.3</a:since>
</xsd:appinfo>
</xsd:annotation>
</xsd:element>
<!-- TODO: similarity criteria (history vs new password) -->
</xsd:sequence>
</xsd:complexType>
Expand Down Expand Up @@ -2015,7 +2029,7 @@
<xsd:enumeration value="identityManagerMandatory">
<xsd:annotation>
<xsd:documentation>
Identity Manager Repository will by propagated.
Identity Manager Repository will be propagated always.
The user can choose where the other credentials will be propagated.
The propagation dialog will be shown.
</xsd:documentation>
Expand Down
Expand Up @@ -249,6 +249,26 @@ private void validateMinAge(List<LocalizableMessage> messages, OperationResult r
}
}


private boolean isMaxAgeViolated() {
if (oldCredential == null) {
return false;
}
Duration maxAge = getMaxAge();
MetadataType currentCredentialMetadata = oldCredential.getMetadata();
if (maxAge != null && currentCredentialMetadata != null) {
XMLGregorianCalendar lastChangeTimestamp = getLastChangeTimestamp(currentCredentialMetadata);
if (lastChangeTimestamp != null) {
XMLGregorianCalendar changeAllowedTimestamp = XmlTypeConverter.addDuration(lastChangeTimestamp, maxAge);
if (changeAllowedTimestamp.compare(now) == DatatypeConstants.LESSER) {
LOGGER.trace("Password maxAge violated. lastChange={}, maxAge={}, now={}", lastChangeTimestamp, maxAge, now);
return true;
}
}
}
return false;
}

private XMLGregorianCalendar getLastChangeTimestamp(MetadataType currentCredentialMetadata) {
XMLGregorianCalendar modifyTimestamp = currentCredentialMetadata.getModifyTimestamp();
if (modifyTimestamp != null) {
Expand Down Expand Up @@ -314,8 +334,11 @@ private void validateHistory(String clearValue, List<LocalizableMessage> message

if (passwordEquals(newPasswordPs, existingPassword.getValue())) {
LOGGER.trace("{} matched current value", shortDesc);
appendHistoryViolationMessage(messages, result);
return;

if (!SecurityUtil.isHistoryAllowExistingPasswordReuse(credentialPolicy) || isMaxAgeViolated()) { // existing password can be reused even when stored in focus, it has to be valid according to maxAge setting
appendHistoryViolationMessage(messages, result);
return;
}
}

//noinspection unchecked
Expand Down Expand Up @@ -345,6 +368,14 @@ private Duration getMinAge() {
}
}

private Duration getMaxAge() {
if (credentialPolicy != null) {
return credentialPolicy.getMaxAge();
} else {
return null;
}
}

private int getMinOccurs() {
if (credentialPolicy != null) {
String minOccursPhrase = credentialPolicy.getMinOccurs();
Expand Down
Expand Up @@ -238,10 +238,16 @@ private void processModify(LensFocusContext<F> focusContext)
}
} else {
if (hasValueDelta(focusDelta, getCredentialsContainerPath())) {
credentialValueChanged = true;
credentialValueChanged = isCredentialValueChanged(focusDelta);
checkMinOccurs = true; // might not be precise (e.g. might check minOccurs even if a value is being added)
processValueDelta(focusDelta);
addMetadataDelta();

// Do not add metadata to the password and do not append password history, if the new value is same as old (and password reuse in history is ON). This is because modifyTimestamp would change and could not be relied upon with maxAge.
if (!credentialValueChanged && SecurityUtil.isHistoryAllowExistingPasswordReuse(getCredentialPolicy())) {
LOGGER.trace("Skipping Metadata delta.");
} else {
addMetadataDelta();
}
}
}

Expand All @@ -255,6 +261,41 @@ private void processModify(LensFocusContext<F> focusContext)
}
}

private boolean isCredentialValueChanged(ObjectDelta<F> focusDelta) {
ProtectedStringType oldPassword;
ProtectedStringType newPassword = null;

PropertyDelta<ProtectedStringType> valueDelta = focusDelta.findPropertyDelta(getCredentialValuePath());
if (valueDelta != null && valueDelta.getValuesToReplace() != null) {
for (PrismPropertyValue val : valueDelta.getValuesToReplace()) {
newPassword = (ProtectedStringType) val.getValue();
break; // password should have only one value
}
}

// in case that password is added and not replaced:
if (newPassword == null && valueDelta.getValuesToAdd() != null) {
for (PrismPropertyValue val : valueDelta.getValuesToAdd()) {
newPassword = (ProtectedStringType) val.getValue();
break; // password should have only one value
}
}

if (getOldCredential() == null) {
return newPassword != null;
}
oldPassword = ((PasswordType) getOldCredential()).getValue();

if (newPassword == null) {
return oldPassword != null;
}
try {
return !protector.compareCleartext(oldPassword, newPassword);
} catch (EncryptionException | SchemaException e) {
throw new SystemException("Failed to compare passwords: " + e.getMessage(), e);
}
}

@NotNull
private PrismObject<F> getObjectNew(LensFocusContext<F> focusContext) throws SchemaException {
PrismObject<F> objectNew = focusContext.getObjectNew();
Expand Down
Expand Up @@ -1316,6 +1316,34 @@ public void test248ModifyUserJackPasswordGoodReuse() throws Exception {
USER_PASSWORD_VALID_1, USER_PASSWORD_VALID_3, USER_PASSWORD_VALID_4);
}

/**
* When historyAllowExistingPasswordReuse is true and password history is ON,
* existing password value (as found in UserType) can be set again as the new password.
* Only until maxAge.
*/
@Test
public void test250ExistingPasswordReuse() throws Exception {
// GIVEN
Task task = getTestTask();
OperationResult result = task.getResult();

modifyObjectReplaceProperty(SecurityPolicyType.class, getSecurityPolicyOid(),
ItemPath.create(SecurityPolicyType.F_CREDENTIALS, CredentialsPolicyType.F_PASSWORD, PasswordCredentialsPolicyType.F_HISTORY_ALLOW_EXISTING_PASSWORD_REUSE),
task, result, Boolean.TRUE);
modifyObjectReplaceProperty(SecurityPolicyType.class, getSecurityPolicyOid(),
ItemPath.create(SecurityPolicyType.F_CREDENTIALS, CredentialsPolicyType.F_PASSWORD, PasswordCredentialsPolicyType.F_MAX_AGE),
task, result, XmlTypeConverter.createDuration("PT10M"));

// WHEN
when();
modifyUserChangePassword(USER_JACK_OID, USER_PASSWORD_VALID_1, task, result);
modifyUserChangePassword(USER_JACK_OID, USER_PASSWORD_VALID_1, task, result); // modify with same pwd

// THEN
then();
assertSuccess(result);
}

private void doTestModifyUserJackPasswordSuccessWithHistory(
String newPassword, String... expectedPasswordHistory)
throws Exception {
Expand Down
Expand Up @@ -220,6 +220,9 @@ private static void copyDefaults(CredentialPolicyType defaults,
if (target.getWarningBeforeExpirationDuration() == null && defaults.getWarningBeforeExpirationDuration() != null) {
target.setWarningBeforeExpirationDuration(defaults.getWarningBeforeExpirationDuration());
}
if (target.isHistoryAllowExistingPasswordReuse() == null && defaults.isHistoryAllowExistingPasswordReuse() != null) {
target.setHistoryAllowExistingPasswordReuse(defaults.isHistoryAllowExistingPasswordReuse());
}
}

public static int getCredentialHistoryLength(CredentialPolicyType credentialPolicy) {
Expand All @@ -233,6 +236,17 @@ public static int getCredentialHistoryLength(CredentialPolicyType credentialPoli
return historyLength;
}

public static boolean isHistoryAllowExistingPasswordReuse(CredentialPolicyType credentialPolicy) {
if (credentialPolicy == null) {
return false;
}
Boolean historyAllowExistingPasswordReuse = credentialPolicy.isHistoryAllowExistingPasswordReuse();
if (historyAllowExistingPasswordReuse == null) {
return false;
}
return historyAllowExistingPasswordReuse;
}

public static CredentialsStorageTypeType getCredentialStorageTypeType(CredentialsStorageMethodType storageMethod) {
if (storageMethod == null) {
return null;
Expand Down

0 comments on commit c2d2922

Please sign in to comment.