/**
* Copyright (c) 2017 Evolveum
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.evolveum.midpoint.model.common.stringpolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import com.evolveum.midpoint.prism.Containerable;
import com.evolveum.midpoint.prism.PrismContainer;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.prism.crypto.EncryptionException;
import com.evolveum.midpoint.prism.crypto.Protector;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.prism.path.ItemPathSegment;
import com.evolveum.midpoint.prism.path.NameItemPathSegment;
import com.evolveum.midpoint.prism.xml.XmlTypeConverter;
import com.evolveum.midpoint.prism.xml.XsdTypeMapper;
import com.evolveum.midpoint.schema.constants.SchemaConstants;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.result.OperationResultStatus;
import com.evolveum.midpoint.security.api.SecurityUtil;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.util.QNameUtil;
import com.evolveum.midpoint.util.exception.ExpressionEvaluationException;
import com.evolveum.midpoint.util.exception.ObjectNotFoundException;
import com.evolveum.midpoint.util.exception.PolicyViolationException;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.exception.SystemException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.AbstractCredentialType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.CredentialPolicyType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.CredentialsPolicyType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.CredentialsType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.MetadataType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.NonceType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectReferenceType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.PasswordCredentialsPolicyType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.PasswordHistoryEntryType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.PasswordType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.SecurityPolicyType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.UserType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ValuePolicyType;
import com.evolveum.prism.xml.ns._public.types_3.ProtectedStringType;
/**
* Evaluator that validates the value of any object property. The validation means a checks whether
* the value is a valid for that property. It usually applies to credentials such as passwords.
* But it can be used also for other properties.
*
* This class may also generate value fitting for that property.
*
* TODO: generalize to all object types, not just user
* In that case we may need to move this class to the model-impl
* User template will be probably needed for this
*
* @author semancik
*
*/
public class ObjectValuePolicyEvaluator {
private static final Trace LOGGER = TraceManager.getTrace(ObjectValuePolicyEvaluator.class);
public static final String OPERATION_VALIDATE_VALUE = ObjectValuePolicyEvaluator.class + ".validateValue";
private Protector protector;
private ValuePolicyProcessor valuePolicyProcessor;
private SecurityPolicyType securityPolicy;
private XMLGregorianCalendar now;
private ItemPath valueItemPath;
private PrismObject<UserType> object;
// We need to get old credential as a configuration. We cannot determine it
// from the "object". E.g. in case of addition the object is the new object that
// is just being added. The password will conflict with itself.
private AbstractCredentialType oldCredentialType;
private String shortDesc;
private Task task;
// state
private boolean prepared = false;
private QName credentialQName = null;
private CredentialPolicyType credentialPolicy;
private ValuePolicyType valuePolicy;
public Protector getProtector() {
return protector;
}
public void setProtector(Protector protector) {
this.protector = protector;
}
public ValuePolicyProcessor getValuePolicyProcessor() {
return valuePolicyProcessor;
}
public void setValuePolicyProcessor(ValuePolicyProcessor valuePolicyProcessor) {
this.valuePolicyProcessor = valuePolicyProcessor;
}
public SecurityPolicyType getSecurityPolicy() {
return securityPolicy;
}
public void setSecurityPolicy(SecurityPolicyType securityPolicy) {
this.securityPolicy = securityPolicy;
}
public XMLGregorianCalendar getNow() {
return now;
}
public void setNow(XMLGregorianCalendar now) {
this.now = now;
}
public ItemPath getValueItemPath() {
return valueItemPath;
}
public void setValueItemPath(ItemPath valueItemPath) {
this.valueItemPath = valueItemPath;
}
public PrismObject<UserType> getObject() {
return object;
}
public void setObject(PrismObject<UserType> object) {
this.object = object;
}
public AbstractCredentialType getOldCredentialType() {
return oldCredentialType;
}
public void setOldCredentialType(AbstractCredentialType oldCredentialType) {
this.oldCredentialType = oldCredentialType;
}
public String getShortDesc() {
return shortDesc;
}
public void setShortDesc(String shortDesc) {
this.shortDesc = shortDesc;
}
public Task getTask() {
return task;
}
public void setTask(Task task) {
this.task = task;
}
public OperationResult validateProtectedStringValue(ProtectedStringType value) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException {
String clearValue = getClearValue(value);
return validateStringValue(clearValue);
}
public OperationResult validateStringValue(String clearValue) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException {
OperationResult result = new OperationResult(OPERATION_VALIDATE_VALUE);
// TODO: later we need to replace the string message with something more structured.
// something that can be localized
StringBuilder messageBuilder = new StringBuilder();
prepare();
validateMinAge(messageBuilder, result);
validateHistory(clearValue, messageBuilder, result);
validateStringPolicy(clearValue, messageBuilder, result);
String message = messageBuilder.toString();
if (message.isEmpty()) {
result.computeStatus();
} else {
result.computeStatus(message);
}
return result;
}
private void prepare() throws SchemaException {
if (!prepared) {
preparePassword();
prepareNonce();
prepareValuePolicy();
prepared = true;
}
}
private void prepareValuePolicy() {
if (credentialPolicy != null) {
ObjectReferenceType valuePolicyRef = credentialPolicy.getValuePolicyRef();
if (valuePolicyRef != null) {
PrismObject<ValuePolicyType> valuePolicyObj = valuePolicyRef.asReferenceValue().getObject();
if (valuePolicyObj != null) {
valuePolicy = valuePolicyObj.asObjectable();
}
}
}
// TODO: check value policy from the schema (definition)
}
private void preparePassword() {
if (!QNameUtil.match(UserType.F_CREDENTIALS, valueItemPath.getFirstName())) {
return;
}
ItemPathSegment secondPathSegment = valueItemPath.getSegments().get(1);
if (!(secondPathSegment instanceof NameItemPathSegment)) {
return;
}
credentialQName = ((NameItemPathSegment)secondPathSegment).getName();
if (!QNameUtil.match(CredentialsType.F_PASSWORD, credentialQName)) {
return;
}
credentialPolicy = SecurityUtil.getEffectivePasswordCredentialsPolicy(securityPolicy);
}
private void prepareNonce() throws SchemaException {
if (!QNameUtil.match(CredentialsType.F_NONCE, credentialQName)) {
return;
}
credentialPolicy = SecurityUtil.getEffectiveNonceCredentialsPolicy(securityPolicy);
}
private void validateMinAge(StringBuilder messageBuilder, OperationResult result) {
if (oldCredentialType == null) {
return;
}
Duration minAge = getMinAge();
if (minAge == null) {
return;
}
MetadataType currentCredentialMetadata = oldCredentialType.getMetadata();
if (currentCredentialMetadata == null) {
return;
}
XMLGregorianCalendar lastChangeTimestamp = currentCredentialMetadata.getModifyTimestamp();
if (lastChangeTimestamp == null) {
lastChangeTimestamp = currentCredentialMetadata.getCreateTimestamp();
}
if (lastChangeTimestamp == null) {
return;
}
XMLGregorianCalendar changeAllowedTimestamp = XmlTypeConverter.addDuration(lastChangeTimestamp, minAge);
if (changeAllowedTimestamp.compare(now) == DatatypeConstants.GREATER) {
LOGGER.trace("Password minAge violated. lastChange={}, minAge={}, now={}", lastChangeTimestamp, minAge, now);
String msg = shortDesc + " could not be changed because password minimal age was not yet reached.";
result.addSubresult(new OperationResult("Password minimal age",
OperationResultStatus.FATAL_ERROR, msg));
messageBuilder.append(msg);
messageBuilder.append("\n");
}
}
private void validateStringPolicy(String clearValue, StringBuilder messageBuilder, OperationResult result) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException {
if (clearValue == null) {
int minOccurs = getMinOccurs();
if (minOccurs == 0) {
return;
} else {
String msg = shortDesc + " must have a value.";
result.addSubresult(new OperationResult("minOccurs", OperationResultStatus.FATAL_ERROR, msg));
messageBuilder.append(msg);
messageBuilder.append("\n");
return;
}
}
if (valuePolicy == null) {
LOGGER.trace("Skipping validating {} value. Value policy not specified.", shortDesc);
return;
}
valuePolicyProcessor.validateValue(clearValue, valuePolicy, object, messageBuilder,
"user " + shortDesc + " value policy validation", task, result);
}
private void validateHistory(String clearValue, StringBuilder messageBuilder, OperationResult result) throws SchemaException {
if (!QNameUtil.match(CredentialsType.F_PASSWORD, credentialQName)) {
LOGGER.trace("Skipping validating {} history, only passowrd history is supported", shortDesc);
return;
}
int historyLegth = getHistoryLength();
if (historyLegth == 0) {
LOGGER.trace("Skipping validating {} history, because history length is set to zero", shortDesc);
return;
}
PasswordType currentPasswordType = (PasswordType)oldCredentialType;
if (currentPasswordType == null) {
LOGGER.trace("Skipping validating {} history, because it is empty", shortDesc);
return;
}
ProtectedStringType newPasswordPs = new ProtectedStringType();
newPasswordPs.setClearValue(clearValue);
if (passwordEquals(newPasswordPs, currentPasswordType.getValue())) {
LOGGER.trace("{} matched current value", shortDesc);
appendHistoryViolationMessage(messageBuilder, result);
return;
}
List<PasswordHistoryEntryType> sortedHistoryList = getSortedHistoryList(
currentPasswordType.asPrismContainerValue().findContainer(PasswordType.F_HISTORY_ENTRY), false);
int i = 1;
for (PasswordHistoryEntryType historyEntry: sortedHistoryList) {
if (i >= historyLegth) {
// success (history has more entries than needed)
return;
}
if (passwordEquals(newPasswordPs, historyEntry.getValue())) {
LOGGER.trace("Password history entry #{} matched (changed {})", i, historyEntry.getChangeTimestamp());
appendHistoryViolationMessage(messageBuilder, result);
return;
}
i++;
}
}
private int getHistoryLength() {
return SecurityUtil.getCredentialHistoryLength(credentialPolicy);
}
private Duration getMinAge() {
if (credentialPolicy == null) {
return null;
}
return credentialPolicy.getMinAge();
}
private int getMinOccurs() {
if (credentialPolicy == null) {
return 0;
}
String minOccurs = credentialPolicy.getMinOccurs();
if (minOccurs == null) {
return 0;
}
return XsdTypeMapper.multiplicityToInteger(minOccurs);
}
private List<PasswordHistoryEntryType> getSortedHistoryList(PrismContainer<PasswordHistoryEntryType> historyEntries, boolean ascending) {
if (historyEntries == null || historyEntries.isEmpty()) {
return new ArrayList<>();
}
List<PasswordHistoryEntryType> historyEntryValues = (List<PasswordHistoryEntryType>) historyEntries.getRealValues();
Collections.sort(historyEntryValues, (o1, o2) -> {
XMLGregorianCalendar changeTimestampFirst = o1.getChangeTimestamp();
XMLGregorianCalendar changeTimestampSecond = o2.getChangeTimestamp();
if (ascending) {
return changeTimestampFirst.compare(changeTimestampSecond);
} else {
return changeTimestampSecond.compare(changeTimestampFirst);
}
});
return historyEntryValues;
}
private void appendHistoryViolationMessage(StringBuilder messageBuilder, OperationResult result) {
String msg = "Password couldn't be changed because it was recently used.";
result.addSubresult(new OperationResult("history", OperationResultStatus.FATAL_ERROR, msg));
messageBuilder.append(msg);
messageBuilder.append("\n");
}
private String getClearValue(ProtectedStringType protectedString) {
if (protectedString == null) {
return null;
}
String passwordStr = protectedString.getClearValue();
if (passwordStr == null && protectedString.isEncrypted()) {
try {
passwordStr = protector.decryptString(protectedString);
} catch (EncryptionException e) {
throw new SystemException("Failed to deprypt " + shortDesc + ": " , e);
}
}
return passwordStr;
}
private boolean passwordEquals(ProtectedStringType newPasswordPs, ProtectedStringType currentPassword) throws SchemaException {
if (currentPassword == null) {
return newPasswordPs == null;
}
try {
return protector.compare(newPasswordPs, currentPassword);
} catch (EncryptionException e) {
throw new SystemException("Failed to compare " + shortDesc + ": " , e);
}
}
}