/* * 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.sync; import static com.evolveum.midpoint.schema.internals.InternalsConfig.consistencyChecks; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.xml.namespace.QName; import com.evolveum.midpoint.model.impl.expr.ExpressionEnvironment; import com.evolveum.midpoint.model.impl.expr.ModelExpressionThreadLocalHolder; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import com.evolveum.midpoint.common.SynchronizationUtils; import com.evolveum.midpoint.model.api.ModelExecuteOptions; import com.evolveum.midpoint.model.common.SystemObjectCache; import com.evolveum.midpoint.model.common.expression.ExpressionFactory; import com.evolveum.midpoint.model.common.expression.ExpressionUtil; import com.evolveum.midpoint.model.common.expression.ExpressionVariables; import com.evolveum.midpoint.model.impl.lens.Clockwork; import com.evolveum.midpoint.model.impl.lens.ContextFactory; import com.evolveum.midpoint.model.impl.lens.LensContext; import com.evolveum.midpoint.model.impl.lens.LensFocusContext; import com.evolveum.midpoint.model.impl.lens.LensProjectionContext; import com.evolveum.midpoint.model.impl.util.Utils; import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.prism.PrismProperty; import com.evolveum.midpoint.prism.PrismPropertyValue; import com.evolveum.midpoint.prism.delta.ChangeType; import com.evolveum.midpoint.prism.delta.ItemDelta; import com.evolveum.midpoint.prism.delta.ObjectDelta; import com.evolveum.midpoint.prism.delta.PropertyDelta; import com.evolveum.midpoint.prism.polystring.PolyString; import com.evolveum.midpoint.provisioning.api.ResourceObjectShadowChangeDescription; import com.evolveum.midpoint.repo.api.RepositoryService; import com.evolveum.midpoint.schema.GetOperationOptions; import com.evolveum.midpoint.schema.ResourceShadowDiscriminator; import com.evolveum.midpoint.schema.SelectorOptions; import com.evolveum.midpoint.schema.constants.ObjectTypes; import com.evolveum.midpoint.schema.constants.SchemaConstants; import com.evolveum.midpoint.schema.internals.InternalsConfig; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.result.OperationResultStatus; import com.evolveum.midpoint.schema.statistics.StatisticsUtil; import com.evolveum.midpoint.schema.statistics.SynchronizationInformation; import com.evolveum.midpoint.schema.util.ShadowUtil; import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.util.QNameUtil; import com.evolveum.midpoint.util.exception.CommunicationException; import com.evolveum.midpoint.util.exception.ConfigurationException; import com.evolveum.midpoint.util.exception.ExpressionEvaluationException; import com.evolveum.midpoint.util.exception.ObjectAlreadyExistsException; 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.SecurityViolationException; import com.evolveum.midpoint.util.exception.SystemException; import com.evolveum.midpoint.util.logging.LoggingUtils; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.BeforeAfterType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ExpressionType; import com.evolveum.midpoint.xml.ns._public.common.common_3.FocusType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectReferenceType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectSynchronizationType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectTemplateType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowKindType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowType; import com.evolveum.midpoint.xml.ns._public.common.common_3.SynchronizationActionType; import com.evolveum.midpoint.xml.ns._public.common.common_3.SynchronizationReactionType; import com.evolveum.midpoint.xml.ns._public.common.common_3.SynchronizationSituationType; import com.evolveum.midpoint.xml.ns._public.common.common_3.SynchronizationType; import com.evolveum.midpoint.xml.ns._public.common.common_3.SystemConfigurationType; import com.evolveum.midpoint.xml.ns._public.common.common_3.UserType; /** * Synchronization service receives change notifications from provisioning. It * decides which synchronization policy to use and evaluates it (correlation, * confirmation, situations, reaction, ...) * * @author lazyman * @author Radovan Semancik * * Note: don't autowire this bean by implementing class, as it is * proxied by Spring AOP. Use the interface instead. */ @Service(value = "synchronizationService") public class SynchronizationServiceImpl implements SynchronizationService { private static final Trace LOGGER = TraceManager.getTrace(SynchronizationServiceImpl.class); @Autowired(required = true) private ActionManager<Action> actionManager; @Autowired private CorrelationConfirmationEvaluator correlationConfirmationEvaluator; @Autowired(required = true) @Qualifier("cacheRepositoryService") private RepositoryService repositoryService; @Autowired(required = true) private ContextFactory contextFactory; @Autowired(required = true) private Clockwork clockwork; @Autowired(required = true) private ExpressionFactory expressionFactory; @Autowired(required = true) private SystemObjectCache systemObjectCache; @Override public void notifyChange(ResourceObjectShadowChangeDescription change, Task task, OperationResult parentResult) { validate(change); Validate.notNull(parentResult, "Parent operation result must not be null."); boolean logDebug = isLogDebug(change); if (logDebug) { LOGGER.debug("SYNCHRONIZATION: received change notification {}", change); } else { LOGGER.trace("SYNCHRONIZATION: received change notification {}", change); } OperationResult subResult = parentResult.createSubresult(NOTIFY_CHANGE); PrismObject<? extends ShadowType> currentShadow = change.getCurrentShadow(); PrismObject<? extends ShadowType> applicableShadow = currentShadow; if (applicableShadow == null) { // We need this e.g. in case of delete applicableShadow = change.getOldShadow(); } SynchronizationEventInformation eventInfo = new SynchronizationEventInformation(applicableShadow, change.getSourceChannel(), task); try { ResourceType resourceType = change.getResource().asObjectable(); PrismObject<SystemConfigurationType> configuration = systemObjectCache.getSystemConfiguration(subResult); ObjectSynchronizationType synchronizationPolicy = determineSynchronizationPolicy(resourceType, applicableShadow, configuration, task, subResult); if (LOGGER.isTraceEnabled()) { String policyDesc = null; if (synchronizationPolicy != null) { if (synchronizationPolicy.getName() == null) { policyDesc = "(kind=" + synchronizationPolicy.getKind() + ", intent=" + synchronizationPolicy.getIntent() + ", objectclass=" + synchronizationPolicy.getObjectClass() + ")"; } else { policyDesc = synchronizationPolicy.getName(); } } LOGGER.trace("SYNCHRONIZATION determined policy: {}", policyDesc); } if (synchronizationPolicy == null) { String message = "SYNCHRONIZATION no matching policy for " + applicableShadow + " (" + applicableShadow.asObjectable().getObjectClass() + ") " + " on " + resourceType + ", ignoring change from channel " + change.getSourceChannel(); LOGGER.debug(message); subResult.recordStatus(OperationResultStatus.NOT_APPLICABLE, message); eventInfo.setNoSynchronizationPolicy(); eventInfo.record(task); return; } if (!isSynchronizationEnabled(synchronizationPolicy)) { String message = "SYNCHRONIZATION is not enabled for " + resourceType + " ignoring change from channel " + change.getSourceChannel(); LOGGER.debug(message); subResult.recordStatus(OperationResultStatus.NOT_APPLICABLE, message); eventInfo.setSynchronizationNotEnabled(); eventInfo.record(task); return; } // check if the kind/intent in the syncPolicy satisfy constraints // defined in task if (!satisfyTaskConstraints(synchronizationPolicy, task)) { LOGGER.trace( "SYNCHRONIZATION skipping {} because it does not match kind/intent defined in task", new Object[] { applicableShadow }); subResult.recordStatus(OperationResultStatus.NOT_APPLICABLE, "Skipped because it does not match objectClass/kind/intent"); eventInfo.setDoesNotMatchTaskSpecification(); eventInfo.record(task); return; } if (isProtected((PrismObject<ShadowType>) currentShadow)) { if (StringUtils.isNotBlank(synchronizationPolicy.getIntent())) { List<PropertyDelta<?>> modifications = SynchronizationUtils .createSynchronizationTimestampsDelta(currentShadow); PropertyDelta<String> intentDelta = PropertyDelta.createModificationReplaceProperty( ShadowType.F_INTENT, currentShadow.getDefinition(), synchronizationPolicy.getIntent()); modifications.add(intentDelta); try { repositoryService.modifyObject(ShadowType.class, currentShadow.getOid(), modifications, subResult); task.recordObjectActionExecuted(currentShadow, ChangeType.MODIFY, null); } catch (Throwable t) { task.recordObjectActionExecuted(currentShadow, ChangeType.MODIFY, t); } finally { task.markObjectActionExecutedBoundary(); } } subResult.recordSuccess(); eventInfo.record(task); LOGGER.debug("SYNCHRONIZATION: DONE (dry run) for protected shadow {}", currentShadow); return; } Class<? extends FocusType> focusType = determineFocusClass(synchronizationPolicy, resourceType); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Synchronization is enabled, focus class: {}, found applicable policy: {}", focusType, Utils.getPolicyDesc(synchronizationPolicy)); } SynchronizationSituation situation = determineSituation(focusType, change, synchronizationPolicy, configuration.asObjectable(), task, subResult); if (logDebug) { LOGGER.debug("SYNCHRONIZATION: SITUATION: '{}', currentOwner={}, correlatedOwner={}", situation.getSituation().value(), situation.getCurrentOwner(), situation.getCorrelatedOwner()); } else { LOGGER.trace("SYNCHRONIZATION: SITUATION: '{}', currentOwner={}, correlatedOwner={}", situation.getSituation().value(), situation.getCurrentOwner(), situation.getCorrelatedOwner()); } eventInfo.setOriginalSituation(situation.getSituation()); eventInfo.setNewSituation(situation.getSituation()); // overwritten // later // (TODO fix // this!) if (change.isUnrelatedChange() || Utils.isDryRun(task)) { PrismObject object = null; if (change.getCurrentShadow() != null) { object = change.getCurrentShadow(); } else if (change.getOldShadow() != null) { object = change.getOldShadow(); } Collection modifications = SynchronizationUtils .createSynchronizationSituationAndDescriptionDelta(object, situation.getSituation(), task.getChannel(), false); if (StringUtils.isNotBlank(synchronizationPolicy.getIntent())) { modifications.add(PropertyDelta.createModificationReplaceProperty(ShadowType.F_INTENT, object.getDefinition(), synchronizationPolicy.getIntent())); } try { repositoryService.modifyObject(ShadowType.class, object.getOid(), modifications, subResult); task.recordObjectActionExecuted(object, ChangeType.MODIFY, null); } catch (Throwable t) { task.recordObjectActionExecuted(object, ChangeType.MODIFY, t); } finally { task.markObjectActionExecutedBoundary(); } subResult.recordSuccess(); eventInfo.record(task); LOGGER.debug("SYNCHRONIZATION: DONE (dry run/unrelated) for {}", object); return; } // must be here, because when the reaction has no action, the // situation will be not set. PrismObject<ShadowType> newCurrentShadow = saveSyncMetadata( (PrismObject<ShadowType>) currentShadow, situation, change, synchronizationPolicy, task, parentResult); if (newCurrentShadow != null) { change.setCurrentShadow(newCurrentShadow); } SynchronizationSituationType newSituation = reactToChange(focusType, change, synchronizationPolicy, situation, resourceType, logDebug, configuration, task, subResult); eventInfo.setNewSituation(newSituation); eventInfo.record(task); subResult.computeStatus(); } catch (SystemException ex) { // avoid unnecessary re-wrap eventInfo.setException(ex); eventInfo.record(task); subResult.recordFatalError(ex); throw ex; } catch (Exception ex) { eventInfo.setException(ex); eventInfo.record(task); subResult.recordFatalError(ex); throw new SystemException(ex); } finally { task.markObjectActionExecutedBoundary(); // if (LOGGER.isTraceEnabled()) { // LOGGER.trace(subResult.dump()); // } } LOGGER.debug("SYNCHRONIZATION: DONE for {}", currentShadow); } private boolean satisfyTaskConstraints(ObjectSynchronizationType synchronizationPolicy, Task task) { PrismProperty<ShadowKindType> kind = task.getExtensionProperty(SchemaConstants.MODEL_EXTENSION_KIND); if (kind != null && !kind.isEmpty()) { ShadowKindType kindValue = kind.getRealValue(); ShadowKindType policyKind = synchronizationPolicy.getKind(); if (policyKind == null) { policyKind = ShadowKindType.ACCOUNT; // TODO is this ok? [med] } if (!policyKind.equals(kindValue)) { return false; } } PrismProperty<String> intent = task.getExtensionProperty(SchemaConstants.MODEL_EXTENSION_INTENT); if (intent != null && !intent.isEmpty()) { String intentValue = intent.getRealValue(); if (StringUtils.isEmpty(synchronizationPolicy.getIntent())) { return false; } if (!synchronizationPolicy.getIntent().equals(intentValue)) { return false; } } return true; } private boolean isProtected(PrismObject<ShadowType> shadow) { if (shadow == null) { return false; } ShadowType currentShadowType = shadow.asObjectable(); if (currentShadowType.isProtectedObject() == null) { return false; } return currentShadowType.isProtectedObject(); } private <F extends FocusType> Class<F> determineFocusClass( ObjectSynchronizationType synchronizationPolicy, ResourceType resource) throws ConfigurationException { if (synchronizationPolicy == null) { throw new IllegalStateException("synchronizationPolicy is null"); } QName focusTypeQName = synchronizationPolicy.getFocusType(); if (focusTypeQName == null) { return (Class<F>) UserType.class; } ObjectTypes objectType = ObjectTypes.getObjectTypeFromTypeQName(focusTypeQName); if (objectType == null) { throw new ConfigurationException( "Unknown focus type " + focusTypeQName + " in synchronization policy in " + resource); } return (Class<F>) objectType.getClassDefinition(); } @Override public ObjectSynchronizationType determineSynchronizationPolicy(ResourceType resourceType, PrismObject<? extends ShadowType> currentShadow, PrismObject<SystemConfigurationType> configuration, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException { SynchronizationType synchronization = resourceType.getSynchronization(); if (synchronization == null) { return null; } for (ObjectSynchronizationType objectSynchronization : synchronization.getObjectSynchronization()) { if (isPolicyApplicable(currentShadow, objectSynchronization, resourceType.asPrismObject(), configuration, task, result)) { return objectSynchronization; } } return null; } private boolean isPolicyApplicable(PrismObject<? extends ShadowType> currentShadow, ObjectSynchronizationType synchronizationPolicy, PrismObject<ResourceType> resource, PrismObject<SystemConfigurationType> configuration, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException { if (!SynchronizationUtils.isPolicyApplicable(currentShadow, synchronizationPolicy, resource)) { return false; } Boolean conditionResult = evaluateSynchronizationPolicyCondition(synchronizationPolicy, currentShadow, resource, configuration, task, result); if (conditionResult != null) { return conditionResult.booleanValue(); } return true; } private Boolean evaluateSynchronizationPolicyCondition(ObjectSynchronizationType synchronizationPolicy, PrismObject<? extends ShadowType> currentShadow, PrismObject<ResourceType> resource, PrismObject<SystemConfigurationType> configuration, Task task, OperationResult result) throws SchemaException, ExpressionEvaluationException, ObjectNotFoundException { if (synchronizationPolicy.getCondition() == null) { return null; } ExpressionType conditionExpressionType = synchronizationPolicy.getCondition(); String desc = "condition in object synchronization " + synchronizationPolicy.getName(); ExpressionVariables variables = Utils.getDefaultExpressionVariables(null, currentShadow, null, resource, configuration, null); try { ModelExpressionThreadLocalHolder.pushExpressionEnvironment(new ExpressionEnvironment<>(task, result)); PrismPropertyValue<Boolean> evaluateCondition = ExpressionUtil.evaluateCondition(variables, conditionExpressionType, expressionFactory, desc, task, result); return evaluateCondition.getValue(); } finally { ModelExpressionThreadLocalHolder.popExpressionEnvironment(); } } private boolean isLogDebug(ResourceObjectShadowChangeDescription change) { // Reconciliation changes are routine. Do not let it polute the // logfiles. return !SchemaConstants.CHANGE_CHANNEL_RECON_URI.equals(change.getSourceChannel()); } private void validate(ResourceObjectShadowChangeDescription change) { Validate.notNull(change, "Resource object shadow change description must not be null."); Validate.isTrue(change.getCurrentShadow() != null || change.getObjectDelta() != null, "Object delta and current shadow are null. At least one must be provided."); Validate.notNull(change.getResource(), "Resource in change must not be null."); if (consistencyChecks) { if (change.getCurrentShadow() != null) { change.getCurrentShadow().checkConsistence(); ShadowUtil.checkConsistence(change.getCurrentShadow(), "current shadow in change description"); } if (change.getObjectDelta() != null) { change.getObjectDelta().checkConsistence(); } } } // @Override // public void notifyFailure(ResourceOperationFailureDescription // failureDescription, // Task task, OperationResult parentResult) { // Validate.notNull(failureDescription, "Resource object shadow failure // description must not be null."); // Validate.notNull(failureDescription.getCurrentShadow(), "Current shadow // in resource object shadow failure description must not be null."); // Validate.notNull(failureDescription.getObjectDelta(), "Delta in resource // object shadow failure description must not be null."); // Validate.notNull(failureDescription.getResource(), "Resource in failure // must not be null."); // Validate.notNull(failureDescription.getResult(), "Result in failure // description must not be null."); // Validate.notNull(parentResult, "Parent operation result must not be // null."); // // LOGGER.debug("SYNCHRONIZATION: received failure notifiation {}", // failureDescription); // // LOGGER.error("Provisioning error: {}", // failureDescription.getResult().getMessage()); // // // TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO // TODO TODO TODO TODO // } private boolean isSynchronizationEnabled(ObjectSynchronizationType synchronization) { if (synchronization == null || synchronization.isEnabled() == null) { return false; } return synchronization.isEnabled(); } /** * XXX: in situation when one account belongs to two different idm users * (repository returns only first user, method * {@link com.evolveum.midpoint.model.api.ModelService#findShadowOwner(String, Task, OperationResult)} * (String, com.evolveum.midpoint.schema.result.OperationResult)} ). It * should be changed because otherwise we can't find * {@link SynchronizationSituationType#DISPUTED} situation */ private <F extends FocusType> SynchronizationSituation determineSituation(Class<F> focusType, ResourceObjectShadowChangeDescription change, ObjectSynchronizationType synchronizationPolicy, SystemConfigurationType configurationType, Task task, OperationResult result) { OperationResult subResult = result.createSubresult(CHECK_SITUATION); LOGGER.trace("Determining situation for resource object shadow."); SynchronizationSituation situation = null; try { String shadowOid = getOidFromChange(change); Validate.notEmpty(shadowOid, "Couldn't get resource object shadow oid from change."); PrismObject<F> owner = repositoryService.searchShadowOwner(shadowOid, SelectorOptions.createCollection(GetOperationOptions.createAllowNotFound()), subResult); if (owner != null) { F ownerType = owner.asObjectable(); LOGGER.trace("Shadow OID {} does have owner: {}", shadowOid, ownerType.getName()); SynchronizationSituationType state = null; switch (getModificationType(change)) { case ADD: case MODIFY: // if user is found it means account/group is linked to // resource state = SynchronizationSituationType.LINKED; break; case DELETE: state = SynchronizationSituationType.DELETED; } situation = new SynchronizationSituation<>(ownerType, null, state); } else { LOGGER.trace("Resource object shadow doesn't have owner."); situation = determineSituationWithCorrelation(focusType, change, synchronizationPolicy, owner, configurationType, task, result); } } catch (Exception ex) { LOGGER.error("Error occurred during resource object shadow owner lookup."); throw new SystemException( "Error occurred during resource object shadow owner lookup, reason: " + ex.getMessage(), ex); } finally { subResult.computeStatus(); } return situation; } private String getOidFromChange(ResourceObjectShadowChangeDescription change) { if (change.getCurrentShadow() != null && StringUtils.isNotEmpty(change.getCurrentShadow().getOid())) { return change.getCurrentShadow().getOid(); } if (change.getOldShadow() != null && StringUtils.isNotEmpty(change.getOldShadow().getOid())) { return change.getOldShadow().getOid(); } if (change.getObjectDelta() == null || StringUtils.isEmpty(change.getObjectDelta().getOid())) { throw new IllegalArgumentException( "Oid was not defined in change (not in current, old shadow, delta)."); } return change.getObjectDelta().getOid(); } /** * Tries to match specified focus and shadow. Return true if it matches, * false otherwise. */ @Override public <F extends FocusType> boolean matchUserCorrelationRule(PrismObject<ShadowType> shadow, PrismObject<F> focus, ResourceType resourceType, PrismObject<SystemConfigurationType> configuration, Task task, OperationResult result) throws ConfigurationException, SchemaException, ObjectNotFoundException, ExpressionEvaluationException { ObjectSynchronizationType synchronizationPolicy = determineSynchronizationPolicy(resourceType, shadow, configuration, task, result); Class<F> focusClass; // TODO is this correct? The problem is that synchronizationPolicy can // be null... if (synchronizationPolicy != null) { focusClass = determineFocusClass(synchronizationPolicy, resourceType); } else { focusClass = (Class) focus.asObjectable().getClass(); } return correlationConfirmationEvaluator.matchUserCorrelationRule(focusClass, shadow, focus, synchronizationPolicy, resourceType, configuration == null ? null : configuration.asObjectable(), task, result); } /** * account is not linked to user. you have to use correlation and * confirmation rule to be sure user for this account doesn't exists * resourceShadow only contains the data that were in the repository before * the change. But the correlation/confirmation should work on the updated * data. Therefore let's apply the changes before running * correlation/confirmation */ private <F extends FocusType> SynchronizationSituation determineSituationWithCorrelation( Class<F> focusType, ResourceObjectShadowChangeDescription change, ObjectSynchronizationType synchronizationPolicy, PrismObject<F> owner, SystemConfigurationType configurationType, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException { if (ChangeType.DELETE.equals(getModificationType(change))) { // account was deleted and we know it didn't have owner return new SynchronizationSituation<>(owner == null ? null : owner.asObjectable(), null, SynchronizationSituationType.DELETED); } PrismObject<? extends ShadowType> resourceShadow = change.getCurrentShadow(); ObjectDelta syncDelta = change.getObjectDelta(); if (resourceShadow == null && syncDelta != null && ChangeType.ADD.equals(syncDelta.getChangeType())) { LOGGER.trace("Trying to compute current shadow from change delta add."); PrismObject<ShadowType> shadow = syncDelta.computeChangedObject(syncDelta.getObjectToAdd()); resourceShadow = shadow; change.setCurrentShadow(shadow); } Validate.notNull(resourceShadow, "Current shadow must not be null."); ResourceType resource = change.getResource().asObjectable(); validateResourceInShadow(resourceShadow.asObjectable(), resource); SynchronizationSituationType state = null; LOGGER.trace( "SYNCHRONIZATION: CORRELATION: Looking for list of {} objects based on correlation rule.", focusType.getSimpleName()); List<PrismObject<F>> users = correlationConfirmationEvaluator.findFocusesByCorrelationRule(focusType, resourceShadow.asObjectable(), synchronizationPolicy.getCorrelation(), resource, configurationType, task, result); if (users == null) { users = new ArrayList<>(); } if (users.size() > 1) { if (synchronizationPolicy.getConfirmation() == null) { LOGGER.trace("SYNCHRONIZATION: CONFIRMATION: no confirmation defined."); } else { LOGGER.debug( "SYNCHRONIZATION: CONFIRMATION: Checking objects from correlation with confirmation rule."); users = correlationConfirmationEvaluator.findUserByConfirmationRule(focusType, users, resourceShadow.asObjectable(), resource, configurationType, synchronizationPolicy.getConfirmation(), task, result); } } F user = null; switch (users.size()) { case 0: state = SynchronizationSituationType.UNMATCHED; break; case 1: switch (getModificationType(change)) { case ADD: case MODIFY: state = SynchronizationSituationType.UNLINKED; break; case DELETE: state = SynchronizationSituationType.DELETED; break; } user = users.get(0).asObjectable(); break; default: state = SynchronizationSituationType.DISPUTED; } return new SynchronizationSituation(null, user, state); } private void validateResourceInShadow(ShadowType shadow, ResourceType resource) { if (shadow.getResource() != null || shadow.getResourceRef() != null) { return; } ObjectReferenceType reference = new ObjectReferenceType(); reference.setOid(resource.getOid()); reference.setType(ObjectTypes.RESOURCE.getTypeQName()); shadow.setResourceRef(reference); } /** * @param change * @return method checks change type in object delta if available, otherwise * returns {@link ChangeType#ADD} */ private ChangeType getModificationType(ResourceObjectShadowChangeDescription change) { if (change.getObjectDelta() != null) { return change.getObjectDelta().getChangeType(); } return ChangeType.ADD; } private <F extends FocusType> SynchronizationSituationType reactToChange(Class<F> focusClass, ResourceObjectShadowChangeDescription change, ObjectSynchronizationType synchronizationPolicy, SynchronizationSituation<F> situation, ResourceType resource, boolean logDebug, PrismObject<SystemConfigurationType> configuration, Task task, OperationResult parentResult) throws ConfigurationException, ObjectNotFoundException, SchemaException, PolicyViolationException, ExpressionEvaluationException, ObjectAlreadyExistsException, CommunicationException, SecurityViolationException { SynchronizationSituationType newSituation = situation.getSituation(); SynchronizationReactionType reactionDefinition = findReactionDefinition(synchronizationPolicy, situation, change.getSourceChannel(), resource); if (reactionDefinition == null) { LOGGER.trace("No reaction is defined for situation {} in {}", situation.getSituation(), resource); return newSituation; } // seems to be unused so commented it out [med] // PrismObject<? extends ObjectType> shadow = null; // if (change.getCurrentShadow() != null) { // shadow = change.getCurrentShadow(); // } else if (change.getOldShadow() != null) { // shadow = change.getOldShadow(); // } Boolean doReconciliation = determineReconciliation(synchronizationPolicy, reactionDefinition); if (doReconciliation == null) { // We have to do reconciliation if we have got a full shadow and no // delta. // There is no other good way how to reflect the changes from the // shadow. if (change.getObjectDelta() == null) { doReconciliation = true; } } Boolean limitPropagation = determinePropagationLimitation(synchronizationPolicy, reactionDefinition, change.getSourceChannel()); ModelExecuteOptions options = new ModelExecuteOptions(); options.setReconcile(doReconciliation); options.setLimitPropagation(limitPropagation); final boolean willSynchronize = isSynchronize(reactionDefinition); LensContext<F> lensContext = null; if (willSynchronize) { lensContext = createLensContext(focusClass, change, reactionDefinition, synchronizationPolicy, situation, options, configuration, parentResult); } if (LOGGER.isTraceEnabled() && lensContext != null) { LOGGER.trace("---[ SYNCHRONIZATION context before action execution ]-------------------------\n" + "{}\n------------------------------------------", lensContext.debugDump()); } if (willSynchronize) { // there's no point in calling executeAction without context - so // the actions are executed only if synchronize == true executeActions(reactionDefinition, lensContext, situation, BeforeAfterType.BEFORE, resource, logDebug, task, parentResult); Iterator<LensProjectionContext> iterator = lensContext.getProjectionContextsIterator(); LensProjectionContext originalProjectionContext = iterator.hasNext() ? iterator.next() : null; try { clockwork.run(lensContext, task, parentResult); } catch (ConfigurationException | ObjectNotFoundException | SchemaException | PolicyViolationException | ExpressionEvaluationException | ObjectAlreadyExistsException | CommunicationException | SecurityViolationException e) { LOGGER.error("SYNCHRONIZATION: Error in synchronization on {} for situation {}: {}: {}. Change was {}", new Object[] {resource, situation.getSituation(), e.getClass().getSimpleName(), e.getMessage(), change, e}); // what to do here? We cannot throw the error back. All that the notifyChange method // could do is to convert it to SystemException. But that indicates an internal error and it will // break whatever code called the notifyChange in the first place. We do not want that. // If the clockwork could not do anything with the exception then perhaps nothing can be done at all. // So just log the error (the error should be remembered in the result and task already) // and then just go on. } // note: actions "AFTER" seem to be useless here (basically they // modify lens context - which is relevant only if followed by // clockwork run) executeActions(reactionDefinition, lensContext, situation, BeforeAfterType.AFTER, resource, logDebug, task, parentResult); if (originalProjectionContext != null) { newSituation = originalProjectionContext.getSynchronizationSituationResolved(); } } else { LOGGER.trace("Skipping clockwork run on {} for situation {}, synchronize is set to false.", new Object[] { resource, situation.getSituation() }); } return newSituation; } private Boolean determineReconciliation(ObjectSynchronizationType synchronizationPolicy, SynchronizationReactionType reactionDefinition) { if (reactionDefinition.isReconcile() != null) { return reactionDefinition.isReconcile(); } if (synchronizationPolicy.isReconcile() != null) { return synchronizationPolicy.isReconcile(); } return null; } private Boolean determinePropagationLimitation(ObjectSynchronizationType synchronizationPolicy, SynchronizationReactionType reactionDefinition, String channel) { if (StringUtils.isNotBlank(channel)) { QName channelQName = QNameUtil.uriToQName(channel); // Discovery channel is used when compensating some inconsistent // state. Therefore we do not want to propagate changes to other // resources. We only want to resolve the problem and continue in // previous provisioning/synchronization during which this // compensation was triggered. if (SchemaConstants.CHANGE_CHANNEL_DISCOVERY.equals(channelQName) && SynchronizationSituationType.DELETED != reactionDefinition.getSituation()) { return true; } } if (reactionDefinition.isLimitPropagation() != null) { return reactionDefinition.isLimitPropagation(); } if (synchronizationPolicy.isLimitPropagation() != null) { return synchronizationPolicy.isLimitPropagation(); } return null; } @NotNull private <F extends FocusType> LensContext<F> createLensContext(Class<F> focusClass, ResourceObjectShadowChangeDescription change, SynchronizationReactionType reactionDefinition, ObjectSynchronizationType synchronizationPolicy, SynchronizationSituation<F> situation, ModelExecuteOptions options, PrismObject<SystemConfigurationType> configuration, OperationResult parentResult) throws ObjectNotFoundException, SchemaException { LensContext<F> context = contextFactory.createSyncContext(focusClass, change); context.setLazyAuditRequest(true); context.setSystemConfiguration(configuration); context.setOptions(options); ResourceType resource = change.getResource().asObjectable(); if (ModelExecuteOptions.isLimitPropagation(options)) { context.setTriggeredResource(resource); } context.rememberResource(resource); PrismObject<ShadowType> shadow = getShadowFromChange(change); if (InternalsConfig.consistencyChecks) shadow.checkConsistence(); // Projection context ShadowKindType kind = getKind(shadow, synchronizationPolicy); String intent = getIntent(shadow, synchronizationPolicy); boolean thombstone = isThombstone(change); ResourceShadowDiscriminator descr = new ResourceShadowDiscriminator(resource.getOid(), kind, intent, thombstone); LensProjectionContext projectionContext = context.createProjectionContext(descr); projectionContext.setResource(resource); projectionContext.setOid(getOidFromChange(change)); projectionContext.setSynchronizationSituationDetected(situation.getSituation()); // insert object delta if available in change ObjectDelta<? extends ShadowType> delta = change.getObjectDelta(); if (delta != null) { projectionContext.setSyncDelta((ObjectDelta<ShadowType>) delta); } else { projectionContext.setSyncAbsoluteTrigger(true); } // we insert account if available in change PrismObject<ShadowType> currentAccount = shadow; if (currentAccount != null) { projectionContext.setLoadedObject(currentAccount); if (!thombstone) { projectionContext.setFullShadow(true); } projectionContext.setFresh(true); } if (delta != null && delta.isDelete()) { projectionContext.setExists(false); } else { projectionContext.setExists(true); } projectionContext.setDoReconciliation(ModelExecuteOptions.isReconcile(options)); // Focus context if (situation.getCurrentOwner() != null) { F focusType = situation.getCurrentOwner(); LensFocusContext<F> focusContext = context.createFocusContext(); PrismObject<F> focusOld = (PrismObject<F>) focusType.asPrismObject(); focusContext.setLoadedObject(focusOld); } // Global stuff ObjectReferenceType objectTemplateRef = null; if (reactionDefinition.getObjectTemplateRef() != null) { objectTemplateRef = reactionDefinition.getObjectTemplateRef(); } else if (synchronizationPolicy.getObjectTemplateRef() != null) { objectTemplateRef = synchronizationPolicy.getObjectTemplateRef(); } if (objectTemplateRef != null) { ObjectTemplateType objectTemplate = repositoryService .getObject(ObjectTemplateType.class, objectTemplateRef.getOid(), null, parentResult) .asObjectable(); context.setFocusTemplate(objectTemplate); } return context; } protected PrismObject<ShadowType> getShadowFromChange(ResourceObjectShadowChangeDescription change) { if (change.getCurrentShadow() != null) { return (PrismObject<ShadowType>) change.getCurrentShadow(); } if (change.getOldShadow() != null) { return (PrismObject<ShadowType>) change.getOldShadow(); } return null; } private ShadowKindType getKind(PrismObject<ShadowType> shadow, ObjectSynchronizationType synchronizationPolicy) { ShadowKindType shadowKind = shadow.asObjectable().getKind(); if (shadowKind != null) { return shadowKind; } if (synchronizationPolicy.getKind() != null) { return synchronizationPolicy.getKind(); } return ShadowKindType.ACCOUNT; } private String getIntent(PrismObject<ShadowType> shadow, ObjectSynchronizationType synchronizationPolicy) { String shadowIntent = shadow.asObjectable().getIntent(); if (shadowIntent != null) { return shadowIntent; } return synchronizationPolicy.getIntent(); } private boolean isThombstone(ResourceObjectShadowChangeDescription change) { PrismObject<? extends ShadowType> shadow = null; if (change.getOldShadow() != null) { shadow = change.getOldShadow(); } else if (change.getCurrentShadow() != null) { shadow = change.getCurrentShadow(); } if (shadow != null) { if (shadow.asObjectable().isDead() != null) { return shadow.asObjectable().isDead().booleanValue(); } } ObjectDelta<? extends ShadowType> objectDelta = change.getObjectDelta(); if (objectDelta == null) { return false; } return objectDelta.isDelete(); } private boolean isSynchronize(SynchronizationReactionType reactionDefinition) { if (reactionDefinition.isSynchronize() != null) { return reactionDefinition.isSynchronize(); } return !reactionDefinition.getAction().isEmpty(); } private SynchronizationReactionType findReactionDefinition( ObjectSynchronizationType synchronizationPolicy, SynchronizationSituation situation, String channel, ResourceType resource) throws ConfigurationException { SynchronizationReactionType defaultReaction = null; for (SynchronizationReactionType reaction : synchronizationPolicy.getReaction()) { SynchronizationSituationType reactionSituation = reaction.getSituation(); if (reactionSituation == null) { throw new ConfigurationException("No situation defined for a reaction in " + resource); } if (reactionSituation.equals(situation.getSituation())) { if (reaction.getChannel() != null && !reaction.getChannel().isEmpty()) { if (reaction.getChannel().contains("") || reaction.getChannel().contains(null)) { defaultReaction = reaction; } if (reaction.getChannel().contains(channel)) { return reaction; } else { LOGGER.trace("Skipping reaction {} because the channel does not match {}", reaction, channel); continue; } } else { defaultReaction = reaction; } } } LOGGER.trace("Using default reaction {}", defaultReaction); return defaultReaction; } /** * Saves situation, timestamps, kind and intent (if needed) */ private PrismObject<ShadowType> saveSyncMetadata(PrismObject<ShadowType> shadow, SynchronizationSituation situation, ResourceObjectShadowChangeDescription change, ObjectSynchronizationType synchronizationPolicy, Task task, OperationResult parentResult) { if (shadow == null) { return null; } ShadowType shadowType = shadow.asObjectable(); // new situation description List<PropertyDelta<?>> deltas = SynchronizationUtils .createSynchronizationSituationAndDescriptionDelta(shadow, situation.getSituation(), change.getSourceChannel(), true); if (shadowType.getKind() == null) { ShadowKindType kind = synchronizationPolicy.getKind(); if (kind == null) { kind = ShadowKindType.ACCOUNT; } PropertyDelta<ShadowKindType> kindDelta = PropertyDelta.createReplaceDelta(shadow.getDefinition(), ShadowType.F_KIND, kind); deltas.add(kindDelta); } if (shadowType.getIntent() == null) { String intent = synchronizationPolicy.getIntent(); if (intent == null) { intent = SchemaConstants.INTENT_DEFAULT; } PropertyDelta<String> intentDelta = PropertyDelta.createReplaceDelta(shadow.getDefinition(), ShadowType.F_INTENT, intent); deltas.add(intentDelta); } try { repositoryService.modifyObject(shadowType.getClass(), shadow.getOid(), deltas, parentResult); ItemDelta.applyTo(deltas, shadow); task.recordObjectActionExecuted(shadow, ChangeType.MODIFY, null); return shadow; } catch (ObjectNotFoundException ex) { task.recordObjectActionExecuted(shadow, ChangeType.MODIFY, ex); // This may happen e.g. during some recon-livesync interactions. // If the shadow is gone then it is gone. No point in recording the // situation any more. LOGGER.debug( "Could not update situation in account, because shadow {} does not exist any more (this may be harmless)", shadow.getOid()); parentResult.getLastSubresult().setStatus(OperationResultStatus.HANDLED_ERROR); } catch (ObjectAlreadyExistsException | SchemaException ex) { task.recordObjectActionExecuted(shadow, ChangeType.MODIFY, ex); LoggingUtils.logException(LOGGER, "### SYNCHRONIZATION # notifyChange(..): Save of synchronization situation failed: could not modify shadow " + shadow.getOid() + ": " + ex.getMessage(), ex); parentResult.recordFatalError("Save of synchronization situation failed: could not modify shadow " + shadow.getOid() + ": " + ex.getMessage(), ex); throw new SystemException("Save of synchronization situation failed: could not modify shadow " + shadow.getOid() + ": " + ex.getMessage(), ex); } catch (Throwable t) { task.recordObjectActionExecuted(shadow, ChangeType.MODIFY, t); throw t; } return null; } private <F extends FocusType> void executeActions(SynchronizationReactionType reactionDef, LensContext<F> context, SynchronizationSituation<F> situation, BeforeAfterType order, ResourceType resource, boolean logDebug, Task task, OperationResult parentResult) throws ConfigurationException, SchemaException { for (SynchronizationActionType actionDef : reactionDef.getAction()) { if ((actionDef.getOrder() == null && order == BeforeAfterType.BEFORE) || (actionDef.getOrder() != null && actionDef.getOrder() == order)) { String handlerUri = actionDef.getHandlerUri(); if (handlerUri == null) { handlerUri = actionDef.getRef(); } if (handlerUri == null) { LOGGER.error("Action definition in resource {} doesn't contain handler URI", resource); throw new ConfigurationException( "Action definition in resource " + resource + " doesn't contain handler URI"); } Action action = actionManager.getActionInstance(handlerUri); if (action == null) { LOGGER.warn("Couldn't create action with uri '{}' in resource {}, skipping action.", new Object[] { handlerUri, resource }); continue; } // TODO: legacy userTemplate Map<QName, Object> parameters = null; if (actionDef.getParameters() != null) { // TODO: process parameters // parameters = actionDef.getParameters().getAny(); } if (logDebug) { LOGGER.debug("SYNCHRONIZATION: ACTION: Executing: {}.", new Object[] { action.getClass() }); } else { LOGGER.trace("SYNCHRONIZATION: ACTION: Executing: {}.", new Object[] { action.getClass() }); } action.handle(context, situation, parameters, task, parentResult); } } } /* * (non-Javadoc) * * @see com.evolveum.midpoint.provisioning.api.ResourceObjectChangeListener# * getName () */ @Override public String getName() { return "model synchronization service"; } private static class SynchronizationEventInformation { private String objectName; private String objectDisplayName; private String objectOid; private Throwable exception; private long started; private String channel; private SynchronizationInformation.Record originalStateIncrement = new SynchronizationInformation.Record(); private SynchronizationInformation.Record newStateIncrement = new SynchronizationInformation.Record(); public SynchronizationEventInformation(PrismObject<? extends ShadowType> currentShadow, String channel, Task task) { this.channel = channel; started = System.currentTimeMillis(); if (currentShadow != null) { final ShadowType shadow = currentShadow.asObjectable(); objectName = PolyString.getOrig(shadow.getName()); objectDisplayName = StatisticsUtil.getDisplayName(shadow); objectOid = currentShadow.getOid(); } task.recordSynchronizationOperationStart(objectName, objectDisplayName, ShadowType.COMPLEX_TYPE, objectOid); if (SchemaConstants.CHANGE_CHANNEL_LIVE_SYNC_URI.equals(channel)) { // livesync processing is not controlled via model -> so we // cannot do this in upper layers task.recordIterativeOperationStart(objectName, objectDisplayName, ShadowType.COMPLEX_TYPE, objectOid); } } public void setProtected() { originalStateIncrement.setCountProtected(1); newStateIncrement.setCountProtected(1); } public void setNoSynchronizationPolicy() { originalStateIncrement.setCountNoSynchronizationPolicy(1); newStateIncrement.setCountNoSynchronizationPolicy(1); } public void setSynchronizationNotEnabled() { originalStateIncrement.setCountSynchronizationDisabled(1); newStateIncrement.setCountSynchronizationDisabled(1); } public void setDoesNotMatchTaskSpecification() { originalStateIncrement.setCountNotApplicableForTask(1); newStateIncrement.setCountNotApplicableForTask(1); } private void setSituation(SynchronizationInformation.Record increment, SynchronizationSituationType situation) { if (situation != null) { switch (situation) { case LINKED: increment.setCountLinked(1); break; case UNLINKED: increment.setCountUnlinked(1); break; case DELETED: increment.setCountDeleted(1); break; case DISPUTED: increment.setCountDisputed(1); break; case UNMATCHED: increment.setCountUnmatched(1); break; default: // noop (or throw exception?) } } } public void setOriginalSituation(SynchronizationSituationType situation) { setSituation(originalStateIncrement, situation); } public void setNewSituation(SynchronizationSituationType situation) { newStateIncrement = new SynchronizationInformation.Record(); // brutal // hack, // TODO // fix // this! setSituation(newStateIncrement, situation); } public void setException(Exception ex) { exception = ex; } public void record(Task task) { task.recordSynchronizationOperationEnd(objectName, objectDisplayName, ShadowType.COMPLEX_TYPE, objectOid, started, exception, originalStateIncrement, newStateIncrement); if (SchemaConstants.CHANGE_CHANNEL_LIVE_SYNC_URI.equals(channel)) { // livesync processing is not controlled via model -> so we // cannot do this in upper layers task.recordIterativeOperationEnd(objectName, objectDisplayName, ShadowType.COMPLEX_TYPE, objectOid, started, exception); } } } }