/* * Copyright (c) 2010-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.impl.lens.projector.credentials; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.namespace.QName; import com.evolveum.midpoint.model.common.stringpolicy.ObjectValuePolicyEvaluator; import com.evolveum.midpoint.model.common.stringpolicy.ValuePolicyProcessor; import com.evolveum.midpoint.model.impl.ModelObjectResolver; import com.evolveum.midpoint.model.impl.lens.LensContext; import com.evolveum.midpoint.model.impl.lens.LensFocusContext; import com.evolveum.midpoint.model.impl.lens.OperationalDataManager; import com.evolveum.midpoint.prism.Item; import com.evolveum.midpoint.prism.ItemDefinition; import com.evolveum.midpoint.prism.PrismContainer; import com.evolveum.midpoint.prism.PrismContainerDefinition; import com.evolveum.midpoint.prism.PrismContainerValue; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.prism.PrismProperty; import com.evolveum.midpoint.prism.PrismPropertyValue; import com.evolveum.midpoint.prism.PrismValue; import com.evolveum.midpoint.prism.crypto.EncryptionException; import com.evolveum.midpoint.prism.crypto.Protector; import com.evolveum.midpoint.prism.delta.ContainerDelta; import com.evolveum.midpoint.prism.delta.ItemDelta; import com.evolveum.midpoint.prism.delta.ObjectDelta; import com.evolveum.midpoint.prism.delta.PartiallyResolvedDelta; import com.evolveum.midpoint.prism.delta.PropertyDelta; import com.evolveum.midpoint.prism.path.ItemPath; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.security.api.SecurityUtil; import com.evolveum.midpoint.task.api.Task; 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.CredentialsStorageTypeType; import com.evolveum.midpoint.xml.ns._public.common.common_3.CredentialsType; import com.evolveum.midpoint.xml.ns._public.common.common_3.FocusType; import com.evolveum.midpoint.xml.ns._public.common.common_3.MetadataType; 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.prism.xml.ns._public.types_3.ProtectedStringType; /** * Processor for evaluating credential policies. This class is processing the credential-related settings * of security policy: credential lifetime, history and so on. This class is supposed to be * quite generic. It should be able to operate on all credential types. * * This class does NOT deal with value policies, validation and generation. That task is * delegated to ValuePolicyProcessor. * * @author mamut * @author katkav * @author semancik * */ public abstract class CredentialPolicyEvaluator<R extends AbstractCredentialType, P extends CredentialPolicyType> { private static final Trace LOGGER = TraceManager.getTrace(CredentialPolicyEvaluator.class); private static final ItemPath CREDENTIAL_RELATIVE_VALUE_PATH = new ItemPath(PasswordType.F_VALUE); // Configuration private PrismContext prismContext; private Protector protector; private OperationalDataManager metadataManager; private ValuePolicyProcessor valuePolicyProcessor; private ModelObjectResolver resolver; private LensContext<UserType> context; private XMLGregorianCalendar now; private Task task; private OperationResult result; // State private ObjectValuePolicyEvaluator objectValuePolicyEvaluator; private P credentialPolicy; public PrismContext getPrismContext() { return prismContext; } public void setPrismContext(PrismContext prismContext) { this.prismContext = prismContext; } public Protector getProtector() { return protector; } public void setProtector(Protector protector) { this.protector = protector; } public OperationalDataManager getMetadataManager() { return metadataManager; } public void setMetadataManager(OperationalDataManager metadataManager) { this.metadataManager = metadataManager; } public ValuePolicyProcessor getValuePolicyProcessor() { return valuePolicyProcessor; } public void setValuePolicyProcessor(ValuePolicyProcessor valuePolicyProcessor) { this.valuePolicyProcessor = valuePolicyProcessor; } public ModelObjectResolver getResolver() { return resolver; } public void setResolver(ModelObjectResolver resolver) { this.resolver = resolver; } public LensContext<UserType> getContext() { return context; } public void setContext(LensContext<UserType> context) { this.context = context; } public XMLGregorianCalendar getNow() { return now; } public void setNow(XMLGregorianCalendar now) { this.now = now; } public Task getTask() { return task; } public void setTask(Task task) { this.task = task; } public OperationResult getResult() { return result; } public void setResult(OperationResult result) { this.result = result; } /** * E.g. "credentials/password" */ protected abstract ItemPath getCredentialsContainerPath(); /** * E.g. "value" */ protected ItemPath getCredentialRelativeValuePath() { return CREDENTIAL_RELATIVE_VALUE_PATH; } /** * E.g. "credentials/password/value" */ protected ItemPath getCredentialValuePath() { return getCredentialsContainerPath().subPath(getCredentialRelativeValuePath()); } protected abstract String getCredentialHumanReadableName(); protected boolean supportsHistory() { return false; } protected P getCredentialPolicy() throws SchemaException { if (credentialPolicy == null) { credentialPolicy = determineEffectiveCredentialPolicy(); } return credentialPolicy; } protected abstract P determineEffectiveCredentialPolicy() throws SchemaException; protected SecurityPolicyType getSecurityPolicy() { return context.getFocusContext().getSecurityPolicy(); } private ObjectValuePolicyEvaluator getObjectValuePolicyEvaluator() { if (objectValuePolicyEvaluator == null) { PrismObject<UserType> user = getUser(); objectValuePolicyEvaluator = new ObjectValuePolicyEvaluator(); objectValuePolicyEvaluator.setNow(now); objectValuePolicyEvaluator.setObject(user); objectValuePolicyEvaluator.setProtector(protector); objectValuePolicyEvaluator.setSecurityPolicy(getSecurityPolicy()); objectValuePolicyEvaluator.setShortDesc(getCredentialHumanReadableName() + " for " + user); objectValuePolicyEvaluator.setTask(task); objectValuePolicyEvaluator.setValueItemPath(getCredentialValuePath()); objectValuePolicyEvaluator.setValuePolicyProcessor(valuePolicyProcessor); PrismContainer<R> currentCredentialContainer = getOldCredentialContainer(); if (currentCredentialContainer != null) { objectValuePolicyEvaluator.setOldCredentialType(currentCredentialContainer.getRealValue()); } } return objectValuePolicyEvaluator; } private PrismObject<UserType> getUser() { return context.getFocusContext().getObjectAny(); } public void process() throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException, PolicyViolationException { LensFocusContext<UserType> focusContext = context.getFocusContext(); PrismObject<UserType> focus = focusContext.getObjectAny(); if (focusContext.isAdd()) { if (focusContext.wasAddExecuted()) { LOGGER.trace("Skipping processing {} policies. User addition was already executed.", getCredentialHumanReadableName()); return; } PrismContainer<R> credentialsContainer = focus.findContainer(getCredentialsContainerPath()); if (credentialsContainer != null) { for (PrismContainerValue<R> cVal : credentialsContainer.getValues()) { processCredentialContainerValue(focus, cVal); } } } else if (focusContext.isModify()) { boolean credentialValueChanged = false; ObjectDelta<UserType> focusDelta = focusContext.getDelta(); ContainerDelta<R> containerDelta = focusDelta.findContainerDelta(getCredentialsContainerPath()); if (containerDelta != null) { if (containerDelta.isAdd()) { for (PrismContainerValue<R> cVal : containerDelta.getValuesToAdd()) { credentialValueChanged = true; processCredentialContainerValue(focus, cVal); } } if (containerDelta.isReplace()) { for (PrismContainerValue<R> cVal : containerDelta.getValuesToReplace()) { credentialValueChanged = true; processCredentialContainerValue(focus, cVal); } } } else { if (hasValueDelta(focusDelta, getCredentialsContainerPath())) { credentialValueChanged = true; processValueDelta(focusDelta); addMetadataDelta(); } } if (credentialValueChanged) { addHistoryDeltas(); } } else if (focusContext.isDelete()) { LOGGER.trace("Skipping processing {} policies. User will be deleted.", getCredentialHumanReadableName()); return; } } /** * Process values from credential deltas that add/replace the whole container. * E.g. $user/credentials/password, $user/credentials/securityQuestions */ protected void processCredentialContainerValue(PrismObject<UserType> focus, PrismContainerValue<R> cVal) throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException, PolicyViolationException { addMissingMetadata(cVal); validateCredentialContainerValues(cVal); } /** * Process values from credential deltas that add/replace values below value container * E.g. $user/credentials/password/value, * $user/credentials/securityQuestions/questionAnswer * $user/credentials/securityQuestions/questionAnswer/questionAnswer * * This implementation is OK for the password, nonce and similar simple cases. It needs to be * overridden for more complex cases. */ protected void processValueDelta(ObjectDelta<UserType> focusDelta) throws PolicyViolationException, SchemaException, ObjectNotFoundException, ExpressionEvaluationException { PropertyDelta<ProtectedStringType> valueDelta = focusDelta.findPropertyDelta(getCredentialValuePath()); if (valueDelta == null) { LOGGER.trace("Skipping processing {} policies. User delta does not contain value change.", getCredentialHumanReadableName()); return; } processPropertyValueCollection(valueDelta.getValuesToAdd()); processPropertyValueCollection(valueDelta.getValuesToReplace()); } private void processPropertyValueCollection(Collection<PrismPropertyValue<ProtectedStringType>> collection) throws PolicyViolationException, SchemaException, ObjectNotFoundException, ExpressionEvaluationException { if (collection == null) { return; } for (PrismPropertyValue<ProtectedStringType> val: collection) { validateProtectedStringValue(val.getValue()); } } protected void validateCredentialContainerValues(PrismContainerValue<R> cVal) throws PolicyViolationException, SchemaException, ObjectNotFoundException, ExpressionEvaluationException { PrismProperty<ProtectedStringType> credentialValueProperty = cVal.findProperty(getCredentialRelativeValuePath()); for (PrismPropertyValue<ProtectedStringType> credentialValuePropertyValue: credentialValueProperty.getValues()) { validateProtectedStringValue(credentialValuePropertyValue.getValue()); } } protected void validateProtectedStringValue(ProtectedStringType value) throws PolicyViolationException, SchemaException, ObjectNotFoundException, ExpressionEvaluationException { OperationResult validationResult = getObjectValuePolicyEvaluator().validateProtectedStringValue(value); result.addSubresult(validationResult); if (!validationResult.isAcceptable()) { throw new PolicyViolationException("Provided "+getCredentialHumanReadableName()+" does not satisfy the policies: " + validationResult.getMessage()); } } private void addMetadataDelta() throws SchemaException { Collection<? extends ItemDelta<?, ?>> metaDeltas = metadataManager.createModifyMetadataDeltas( context, getCredentialsContainerPath().subPath(AbstractCredentialType.F_METADATA), context.getFocusContext().getObjectDefinition(), now, task); for (ItemDelta<?, ?> metaDelta : metaDeltas) { context.getFocusContext().swallowToSecondaryDelta(metaDelta); } } private void addMissingMetadata(PrismContainerValue<R> cVal) throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException { if (hasValueChange(cVal)) { if (!hasMetadata(cVal)) { MetadataType metadataType = metadataManager.createCreateMetadata(context, now, task); ContainerDelta<MetadataType> metadataDelta = ContainerDelta.createModificationAdd( getCredentialsContainerPath().subPath(AbstractCredentialType.F_METADATA), UserType.class, prismContext, metadataType); context.getFocusContext().swallowToSecondaryDelta(metadataDelta); } } } private <F extends FocusType> boolean hasValueDelta(ObjectDelta<UserType> focusDelta, ItemPath credentialsPath) { if (focusDelta == null) { return false; } for (PartiallyResolvedDelta<PrismValue, ItemDefinition> partialDelta : focusDelta.findPartial(credentialsPath)) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("Residual delta:\n{}", partialDelta.debugDump()); } ItemPath residualPath = partialDelta.getResidualPath(); if (residualPath == null || residualPath.isEmpty()) { continue; } LOGGER.trace("PATH: {}", residualPath); QName name = ItemPath.getFirstName(residualPath); LOGGER.trace("NAME: {}", name); if (isValueElement(name)) { return true; } } return false; } private boolean hasValueChange(PrismContainerValue<R> cVal) { for (Item<?, ?> item : cVal.getItems()) { QName itemName = item.getElementName(); if (isValueElement(itemName)) { return true; } } return false; } private boolean isValueElement(QName itemName) { return !itemName.equals(AbstractCredentialType.F_FAILED_LOGINS) && !itemName.equals(AbstractCredentialType.F_LAST_FAILED_LOGIN) && !itemName.equals(AbstractCredentialType.F_LAST_SUCCESSFUL_LOGIN) && !itemName.equals(AbstractCredentialType.F_METADATA) && !itemName.equals(AbstractCredentialType.F_PREVIOUS_SUCCESSFUL_LOGIN); } private boolean hasMetadata(PrismContainerValue<R> cVal) { for (Item<?, ?> item : cVal.getItems()) { QName itemName = item.getElementName(); if (itemName.equals(AbstractCredentialType.F_METADATA)) { return true; } } return false; } protected PrismContainer<R> getOldCredentialContainer() { PrismObject<UserType> objectOld = context.getFocusContext().getObjectOld(); if (objectOld == null) { return null; } return objectOld.findContainer(getCredentialsContainerPath()); } private void addHistoryDeltas() throws SchemaException { if (!supportsHistory()) { return; } int historyLength = SecurityUtil.getCredentialHistoryLength(getCredentialPolicy()); PrismContainer<R> oldCredentialContainer = getOldCredentialContainer(); if (oldCredentialContainer == null) { return; } int addedValues = 0; // Note: historyLength=1 means that we need just compare with current password // The real number of values stored in the history is historyLength-1 if (historyLength > 1) { addedValues = createAddHistoryDelta(oldCredentialContainer); } createDeleteHistoryDeltasIfNeeded(historyLength, addedValues, oldCredentialContainer); } // TODO: generalize for other credentials private <F extends FocusType> int createAddHistoryDelta(PrismContainer<R> oldCredentialContainer) throws SchemaException { R oldCredentialContainerType = oldCredentialContainer.getValue().asContainerable(); MetadataType oldCredentialMetadata = oldCredentialContainerType.getMetadata(); PrismProperty<ProtectedStringType> oldValueProperty = oldCredentialContainer.findProperty(getCredentialRelativeValuePath()); if (oldValueProperty == null) { return 0; } ProtectedStringType newHistoryValue = oldValueProperty.getRealValue(); ProtectedStringType passwordPsForStorage = newHistoryValue.clone(); CredentialsStorageTypeType storageType = SecurityUtil.getCredentialStoragetTypeType(getCredentialPolicy().getHistoryStorageMethod()); if (storageType == null) { storageType = CredentialsStorageTypeType.HASHING; } prepareProtectedStringForStorage(passwordPsForStorage, storageType); PrismContainerDefinition<PasswordHistoryEntryType> historyEntryDefinition = oldCredentialContainer.getDefinition().findContainerDefinition(PasswordType.F_HISTORY_ENTRY); PrismContainer<PasswordHistoryEntryType> historyEntry = historyEntryDefinition.instantiate(); PrismContainerValue<PasswordHistoryEntryType> hisotryEntryValue = historyEntry.createNewValue(); PasswordHistoryEntryType entryType = hisotryEntryValue.asContainerable(); entryType.setValue(passwordPsForStorage); entryType.setMetadata(oldCredentialMetadata==null?null:oldCredentialMetadata.clone()); entryType.setChangeTimestamp(now); ContainerDelta<PasswordHistoryEntryType> addHisotryDelta = ContainerDelta .createModificationAdd(new ItemPath(UserType.F_CREDENTIALS, CredentialsType.F_PASSWORD, PasswordType.F_HISTORY_ENTRY), UserType.class, prismContext, entryType.clone()); context.getFocusContext().swallowToSecondaryDelta(addHisotryDelta); return 1; } // TODO: generalize for other credentials private <F extends FocusType> void createDeleteHistoryDeltasIfNeeded(int historyLength, int addedValues, PrismContainer<R> currentCredentialContainer) throws SchemaException { PrismContainer<PasswordHistoryEntryType> historyEntries = currentCredentialContainer.findOrCreateContainer(PasswordType.F_HISTORY_ENTRY); List<PrismContainerValue<PasswordHistoryEntryType>> historyEntryValues = historyEntries.getValues(); if (historyEntries.size() == 0) { return; } // We need to delete one more entry than intuitively expected - because we are computing from the history entries // in the old object. In the new object there will be one new history entry for the changed password. int numberOfHistoryEntriesToDelete = historyEntries.size() - historyLength + addedValues + 1; for (int i = 0; i < numberOfHistoryEntriesToDelete; i++) { ContainerDelta<PasswordHistoryEntryType> deleteHistoryDelta = ContainerDelta .createModificationDelete( new ItemPath(UserType.F_CREDENTIALS, CredentialsType.F_PASSWORD, PasswordType.F_HISTORY_ENTRY), UserType.class, prismContext, historyEntryValues.get(i).clone()); context.getFocusContext().swallowToSecondaryDelta(deleteHistoryDelta); } } private void prepareProtectedStringForStorage(ProtectedStringType ps, CredentialsStorageTypeType storageType) throws SchemaException { try { switch (storageType) { case ENCRYPTION: if (ps.isEncrypted()) { break; } if (ps.isHashed()) { throw new SchemaException("Cannot store hashed value in an encrypted form"); } protector.encrypt(ps); break; case HASHING: if (ps.isHashed()) { break; } protector.hash(ps); break; case NONE: throw new SchemaException("Cannot store value on NONE storage form"); default: throw new SchemaException("Unknown storage type: "+storageType); } } catch (EncryptionException e) { throw new SystemException(e.getMessage(), e); } } }