/* * 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; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.evolveum.midpoint.model.api.context.SynchronizationPolicyDecision; import com.evolveum.midpoint.model.impl.lens.LensContext; import com.evolveum.midpoint.model.impl.lens.LensObjectDeltaOperation; import com.evolveum.midpoint.model.impl.lens.LensProjectionContext; import com.evolveum.midpoint.model.impl.lens.LensUtil; import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.provisioning.api.ProvisioningService; import com.evolveum.midpoint.schema.ResourceShadowDiscriminator; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.util.MiscSchemaUtil; import com.evolveum.midpoint.schema.util.ResourceTypeUtil; import com.evolveum.midpoint.util.exception.PolicyViolationException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectReferenceType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceObjectTypeDependencyStrictnessType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceObjectTypeDependencyType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowType; /** * @author Radovan Semancik * */ @Component public class DependencyProcessor { private static final Trace LOGGER = TraceManager.getTrace(DependencyProcessor.class); @Autowired private ProvisioningService provisioningService; public <F extends ObjectType> void resetWaves(LensContext<F> context) throws PolicyViolationException { } public <F extends ObjectType> void sortProjectionsToWaves(LensContext<F> context) throws PolicyViolationException { // Create a snapshot of the projection collection at the beginning of computation. // The collection may be changed during computation (projections may be added). We do not want to process // these added projections. They are already processed inside the computation. // This also avoids ConcurrentModificationException LensProjectionContext[] projectionArray = context.getProjectionContexts().toArray(new LensProjectionContext[0]); // Reset incomplete flag for those contexts that are not yet computed for (LensProjectionContext projectionContext: context.getProjectionContexts()) { if (projectionContext.getWave() < 0) { projectionContext.setWaveIncomplete(true); } } for (LensProjectionContext projectionContext: projectionArray) { determineProjectionWave(context, projectionContext, null, null); projectionContext.setWaveIncomplete(false); } if (LOGGER.isTraceEnabled()) { StringBuilder sb = new StringBuilder(); for (LensProjectionContext projectionContext: context.getProjectionContexts()) { sb.append("\n"); sb.append(projectionContext.getResourceShadowDiscriminator()); sb.append(": "); sb.append(projectionContext.getWave()); } LOGGER.trace("Projections sorted to waves (projection wave {}, execution wave {}):{}", new Object[]{context.getProjectionWave(), context.getExecutionWave(), sb.toString()}); } } public <F extends ObjectType> int computeMaxWaves(LensContext<F> context) { // Let's do one extra wave with no accounts in it. This time we expect to get the results of the execution to the user // via inbound, e.g. identifiers generated by the resource, DNs and similar things. Hence the +2 instead of +1 return context.getMaxWave() + 2; } private <F extends ObjectType> LensProjectionContext determineProjectionWave(LensContext<F> context, LensProjectionContext projectionContext, ResourceObjectTypeDependencyType inDependency, List<ResourceObjectTypeDependencyType> depPath) throws PolicyViolationException { if (!projectionContext.isWaveIncomplete()) { // This was already processed return projectionContext; } if (projectionContext.isDelete()) { // When deprovisioning (deleting) the dependencies needs to be processed in reverse LOGGER.trace("Determining wave for (deprovision): {}", projectionContext); return determineProjectionWaveDeprovision(context, projectionContext, inDependency, depPath); } else { LOGGER.trace("Determining wave for (provision): {}", projectionContext); return determineProjectionWaveProvision(context, projectionContext, inDependency, depPath); } } private <F extends ObjectType> LensProjectionContext determineProjectionWaveProvision(LensContext<F> context, LensProjectionContext projectionContext, ResourceObjectTypeDependencyType inDependency, List<ResourceObjectTypeDependencyType> depPath) throws PolicyViolationException { if (depPath == null) { depPath = new ArrayList<ResourceObjectTypeDependencyType>(); } int determinedWave = 0; int determinedOrder = 0; for (ResourceObjectTypeDependencyType outDependency: projectionContext.getDependencies()) { if (LOGGER.isTraceEnabled()) { LOGGER.trace("DEP: {}", outDependency); } if (inDependency != null && isHigerOrder(outDependency, inDependency)) { // There is incomming dependency. Deal only with dependencies of this order and lower // otherwise we can end up in endless loop even for legal dependencies. continue; } checkForCircular(depPath, outDependency); depPath.add(outDependency); ResourceShadowDiscriminator refDiscr = new ResourceShadowDiscriminator(outDependency, projectionContext.getResource().getOid(), projectionContext.getKind()); LensProjectionContext dependencyProjectionContext = findDependencyTargetContext(context, projectionContext, outDependency); // if (LOGGER.isTraceEnabled()) { // LOGGER.trace("DEP: {} -> {}", refDiscr, dependencyProjectionContext); // } if (dependencyProjectionContext == null || dependencyProjectionContext.isDelete()) { ResourceObjectTypeDependencyStrictnessType outDependencyStrictness = ResourceTypeUtil.getDependencyStrictness(outDependency); if (outDependencyStrictness == ResourceObjectTypeDependencyStrictnessType.STRICT) { throw new PolicyViolationException("Unsatisfied strict dependency of account "+projectionContext.getResourceShadowDiscriminator()+ " dependent on "+refDiscr+": Account not provisioned"); } else if (outDependencyStrictness == ResourceObjectTypeDependencyStrictnessType.LAX) { // independent object not in the context, just ignore it LOGGER.debug("Unsatisfied lax dependency of account "+projectionContext.getResourceShadowDiscriminator()+ " dependent on "+refDiscr+"; dependency skipped"); } else if (outDependencyStrictness == ResourceObjectTypeDependencyStrictnessType.RELAXED) { // independent object not in the context, just ignore it LOGGER.debug("Unsatisfied relaxed dependency of account "+projectionContext.getResourceShadowDiscriminator()+ " dependent on "+refDiscr+"; dependency skipped"); } else { throw new IllegalArgumentException("Unknown dependency strictness "+outDependency.getStrictness()+" in "+refDiscr); } } else { dependencyProjectionContext = determineProjectionWave(context, dependencyProjectionContext, outDependency, depPath); if (dependencyProjectionContext.getWave() + 1 > determinedWave) { determinedWave = dependencyProjectionContext.getWave() + 1; if (outDependency.getOrder() == null) { determinedOrder = 0; } else { determinedOrder = outDependency.getOrder(); } } } depPath.remove(outDependency); } LensProjectionContext resultAccountContext = projectionContext; if (projectionContext.getWave() >=0 && projectionContext.getWave() != determinedWave) { // Wave for this context was set during the run of this method (it was not set when we // started, we checked at the beginning). Therefore this context must have been visited again. // therefore there is a circular dependency. Therefore we need to create another context to split it. ResourceShadowDiscriminator origDiscr = projectionContext.getResourceShadowDiscriminator(); ResourceShadowDiscriminator discr = new ResourceShadowDiscriminator(origDiscr.getResourceOid(), origDiscr.getKind(), origDiscr.getIntent(), origDiscr.isThombstone()); discr.setOrder(determinedOrder); if (!projectionContext.compareResourceShadowDiscriminator(discr, true)){ resultAccountContext = createAnotherContext(context, projectionContext, discr); } } // LOGGER.trace("Wave for {}: {}", resultAccountContext.getResourceAccountType(), wave); resultAccountContext.setWave(determinedWave); return resultAccountContext; } private <F extends ObjectType> LensProjectionContext determineProjectionWaveDeprovision(LensContext<F> context, LensProjectionContext projectionContext, ResourceObjectTypeDependencyType inDependency, List<ResourceObjectTypeDependencyType> depPath) throws PolicyViolationException { if (depPath == null) { depPath = new ArrayList<ResourceObjectTypeDependencyType>(); } int determinedWave = 0; int determinedOrder = 0; // This needs to go in the reverse. We need to figure out who depends on us. for (DependencyAndSource ds: findReverseDependecies(context, projectionContext)) { LensProjectionContext dependencySourceContext = ds.sourceProjectionContext; ResourceObjectTypeDependencyType outDependency = ds.dependency; if (LOGGER.isTraceEnabled()) { LOGGER.trace("DEP(rev): {}", outDependency); } if (inDependency != null && isHigerOrder(outDependency, inDependency)) { // There is incomming dependency. Deal only with dependencies of this order and lower // otherwise we can end up in endless loop even for legal dependencies. continue; } checkForCircular(depPath, outDependency); depPath.add(outDependency); ResourceShadowDiscriminator refDiscr = new ResourceShadowDiscriminator(outDependency, projectionContext.getResource().getOid(), projectionContext.getKind()); dependencySourceContext = determineProjectionWave(context, dependencySourceContext, outDependency, depPath); if (dependencySourceContext.getWave() + 1 > determinedWave) { determinedWave = dependencySourceContext.getWave() + 1; if (outDependency.getOrder() == null) { determinedOrder = 0; } else { determinedOrder = outDependency.getOrder(); } } depPath.remove(outDependency); } LensProjectionContext resultAccountContext = projectionContext; if (projectionContext.getWave() >=0 && projectionContext.getWave() != determinedWave) { // Wave for this context was set during the run of this method (it was not set when we // started, we checked at the beginning). Therefore this context must have been visited again. // therefore there is a circular dependency. Therefore we need to create another context to split it. if (!projectionContext.isDelete()){ resultAccountContext = createAnotherContext(context, projectionContext, determinedOrder); } } // LOGGER.trace("Wave for {}: {}", resultAccountContext.getResourceAccountType(), wave); resultAccountContext.setWave(determinedWave); return resultAccountContext; } private <F extends ObjectType> Collection<DependencyAndSource> findReverseDependecies(LensContext<F> context, LensProjectionContext targetProjectionContext) throws PolicyViolationException { Collection<DependencyAndSource> deps = new ArrayList<>(); for (LensProjectionContext projectionContext: context.getProjectionContexts()) { for (ResourceObjectTypeDependencyType dependency: projectionContext.getDependencies()) { if (LensUtil.isDependencyTargetContext(projectionContext, targetProjectionContext, dependency)) { DependencyAndSource ds = new DependencyAndSource(); ds.dependency = dependency; ds.sourceProjectionContext = projectionContext; deps.add(ds); } } } return deps; } private void checkForCircular(List<ResourceObjectTypeDependencyType> depPath, ResourceObjectTypeDependencyType outDependency) throws PolicyViolationException { for (ResourceObjectTypeDependencyType pathElement: depPath) { if (pathElement.equals(outDependency)) { StringBuilder sb = new StringBuilder(); Iterator<ResourceObjectTypeDependencyType> iterator = depPath.iterator(); while (iterator.hasNext()) { ResourceObjectTypeDependencyType el = iterator.next(); ObjectReferenceType resourceRef = el.getResourceRef(); if (resourceRef != null) { sb.append(resourceRef.getOid()); } sb.append("(").append(el.getKind()).append("/"); sb.append(el.getIntent()).append(")"); if (iterator.hasNext()) { sb.append("->"); } } throw new PolicyViolationException("Circular dependency, path: "+sb.toString()); } } } private boolean isHigerOrder(ResourceObjectTypeDependencyType a, ResourceObjectTypeDependencyType b) { Integer ao = a.getOrder(); Integer bo = b.getOrder(); if (ao == null) { ao = 0; } if (bo == null) { bo = 0; } return ao > bo; } /** * Find context that has the closest order to the dependency. */ private <F extends ObjectType> LensProjectionContext findDependencyTargetContext( LensContext<F> context, LensProjectionContext sourceProjContext, ResourceObjectTypeDependencyType dependency) { ResourceShadowDiscriminator refDiscr = new ResourceShadowDiscriminator(dependency, sourceProjContext.getResource().getOid(), sourceProjContext.getKind()); LensProjectionContext selected = null; for (LensProjectionContext projectionContext: context.getProjectionContexts()) { if (!projectionContext.compareResourceShadowDiscriminator(refDiscr, false)) { continue; } int ctxOrder = projectionContext.getResourceShadowDiscriminator().getOrder(); if (ctxOrder > refDiscr.getOrder()) { continue; } if (selected == null) { selected = projectionContext; } else { if (ctxOrder > selected.getResourceShadowDiscriminator().getOrder()) { selected = projectionContext; } } } return selected; } // private <F extends ObjectType> boolean isDependencyTargetContext(LensProjectionContext sourceProjContext, LensProjectionContext targetProjectionContext, ResourceObjectTypeDependencyType dependency) { // ResourceShadowDiscriminator refDiscr = new ResourceShadowDiscriminator(dependency, // sourceProjContext.getResource().getOid(), sourceProjContext.getKind()); // return targetProjectionContext.compareResourceShadowDiscriminator(refDiscr, false); // } private <F extends ObjectType> LensProjectionContext createAnotherContext(LensContext<F> context, LensProjectionContext origProjectionContext, ResourceShadowDiscriminator discr) throws PolicyViolationException { LensProjectionContext otherCtx = context.createProjectionContext(discr); otherCtx.setResource(origProjectionContext.getResource()); // Force recon for the new context. This is a primitive way how to avoid phantom changes. otherCtx.setDoReconciliation(true); return otherCtx; } private <F extends ObjectType> LensProjectionContext createAnotherContext(LensContext<F> context, LensProjectionContext origProjectionContext, int determinedOrder) throws PolicyViolationException { ResourceShadowDiscriminator origDiscr = origProjectionContext.getResourceShadowDiscriminator(); ResourceShadowDiscriminator discr = new ResourceShadowDiscriminator(origDiscr.getResourceOid(), origDiscr.getKind(), origDiscr.getIntent(), origDiscr.isThombstone()); discr.setOrder(determinedOrder); LensProjectionContext otherCtx = createAnotherContext(context, origProjectionContext, discr); return otherCtx; } /** * Check that the dependencies are still satisfied. Also check for high-ordes vs low-order operation consistency * and stuff like that. */ public <F extends ObjectType> boolean checkDependencies(LensContext<F> context, LensProjectionContext projContext, OperationResult result) throws PolicyViolationException { if (projContext.isDelete()) { // It is OK if we depend on something that is not there if we are being removed ... for now return true; } if (projContext.getOid() == null || projContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.ADD) { // Check for lower-order contexts LensProjectionContext lowerOrderContext = null; for (LensProjectionContext projectionContext: context.getProjectionContexts()) { if (projContext == projectionContext) { continue; } if (projectionContext.compareResourceShadowDiscriminator(projContext.getResourceShadowDiscriminator(), false) && projectionContext.getResourceShadowDiscriminator().getOrder() < projContext.getResourceShadowDiscriminator().getOrder()) { if (projectionContext.getOid() != null) { lowerOrderContext = projectionContext; break; } } } if (lowerOrderContext != null) { if (lowerOrderContext.getOid() != null) { if (projContext.getOid() == null) { projContext.setOid(lowerOrderContext.getOid()); } if (projContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.ADD) { // This context cannot be ADD. There is a lower-order context with an OID // it means that the lower-order projection exists, we cannot add it twice projContext.setSynchronizationPolicyDecision(SynchronizationPolicyDecision.KEEP); } } if (lowerOrderContext.isDelete()) { projContext.setSynchronizationPolicyDecision(SynchronizationPolicyDecision.DELETE); } } } for (ResourceObjectTypeDependencyType dependency: projContext.getDependencies()) { ResourceShadowDiscriminator refRat = new ResourceShadowDiscriminator(dependency, projContext.getResource().getOid(), projContext.getKind()); LOGGER.trace("LOOKING FOR {}", refRat); LensProjectionContext dependencyAccountContext = context.findProjectionContext(refRat); ResourceObjectTypeDependencyStrictnessType strictness = ResourceTypeUtil.getDependencyStrictness(dependency); if (dependencyAccountContext == null) { if (strictness == ResourceObjectTypeDependencyStrictnessType.STRICT) { // This should not happen, it is checked before projection throw new PolicyViolationException("Unsatisfied strict dependency of " + projContext.getResourceShadowDiscriminator().toHumanReadableDescription() + " dependent on " + refRat.toHumanReadableDescription() + ": No context in dependency check"); } else if (strictness == ResourceObjectTypeDependencyStrictnessType.LAX) { // independent object not in the context, just ignore it LOGGER.trace("Unsatisfied lax dependency of account " + projContext.getResourceShadowDiscriminator().toHumanReadableDescription() + " dependent on " + refRat.toHumanReadableDescription() + "; dependency skipped"); } else if (strictness == ResourceObjectTypeDependencyStrictnessType.RELAXED) { // independent object not in the context, just ignore it LOGGER.trace("Unsatisfied relaxed dependency of account " + projContext.getResourceShadowDiscriminator().toHumanReadableDescription() + " dependent on " + refRat.toHumanReadableDescription() + "; dependency skipped"); } else { throw new IllegalArgumentException("Unknown dependency strictness "+dependency.getStrictness()+" in "+refRat); } } else { // We have the context of the object that we depend on. We need to check if it was provisioned. if (strictness == ResourceObjectTypeDependencyStrictnessType.STRICT || strictness == ResourceObjectTypeDependencyStrictnessType.RELAXED) { if (wasProvisioned(dependencyAccountContext, context.getExecutionWave())) { // everything OK } else { // We do not want to throw exception here. That will stop entire projection. // Let's just mark the projection as broken and skip it. LOGGER.warn("Unsatisfied dependency of account "+projContext.getResourceShadowDiscriminator()+ " dependent on "+refRat+": Account not provisioned in dependency check (execution wave "+context.getExecutionWave()+", account wave "+projContext.getWave() + ", dependency account wave "+dependencyAccountContext.getWave()+")"); projContext.setSynchronizationPolicyDecision(SynchronizationPolicyDecision.BROKEN); return false; } } else if (strictness == ResourceObjectTypeDependencyStrictnessType.LAX) { // we don't care what happened, just go on return true; // TODO why return here? shouldn't we check other dependencies as well? [med] } else { throw new IllegalArgumentException("Unknown dependency strictness "+dependency.getStrictness()+" in "+refRat); } } } return true; } public <F extends ObjectType> void preprocessDependencies(LensContext<F> context){ //in the first wave we do not have enough information to preprocess contexts if (context.getExecutionWave() == 0){ return; } for (LensProjectionContext projContext : context.getProjectionContexts()){ if (!projContext.isCanProject()){ continue; } for (ResourceObjectTypeDependencyType dependency: projContext.getDependencies()) { ResourceShadowDiscriminator refRat = new ResourceShadowDiscriminator(dependency, projContext.getResource().getOid(), projContext.getKind()); LOGGER.trace("LOOKING FOR {}", refRat); LensProjectionContext dependencyAccountContext = context.findProjectionContext(refRat); ResourceObjectTypeDependencyStrictnessType strictness = ResourceTypeUtil.getDependencyStrictness(dependency); if (dependencyAccountContext != null){ if (!dependencyAccountContext.isCanProject()){ continue; } // We have the context of the object that we depend on. We need to check if it was provisioned. if (strictness == ResourceObjectTypeDependencyStrictnessType.STRICT || strictness == ResourceObjectTypeDependencyStrictnessType.RELAXED) { if (wasExecuted(dependencyAccountContext)) { // everything OK if (ResourceTypeUtil.isForceLoadDependentShadow(dependency) && !dependencyAccountContext.isDelete()){ dependencyAccountContext.setDoReconciliation(true); projContext.setDoReconciliation(true); } } } } } } } /** * Finally checks for all the dependencies. Some dependencies cannot be checked during wave computations as * we might not have all activation decisions yet. */ public <F extends ObjectType> void checkDependenciesFinal(LensContext<F> context, OperationResult result) throws PolicyViolationException { for (LensProjectionContext accountContext: context.getProjectionContexts()) { checkDependencies(context, accountContext, result); } for (LensProjectionContext accountContext: context.getProjectionContexts()) { if (accountContext.isDelete() || accountContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.UNLINK) { // It is OK if we depend on something that is not there if we are being removed // but we still need to check if others depends on me for (LensProjectionContext projectionContext: context.getProjectionContexts()) { if (projectionContext.isDelete() || projectionContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.UNLINK || projectionContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.BROKEN || projectionContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.IGNORE) { // If someone who is being deleted depends on us then it does not really matter continue; } for (ResourceObjectTypeDependencyType dependency: projectionContext.getDependencies()) { String dependencyResourceOid = dependency.getResourceRef() != null ? dependency.getResourceRef().getOid() : projectionContext.getResource().getOid(); // TODO what to do if dependencyResourceOid or accountContext.getResource() is null? if (dependencyResourceOid != null && accountContext.getResource() != null && dependencyResourceOid.equals(accountContext.getResource().getOid()) && MiscSchemaUtil.equalsIntent(dependency.getIntent(), projectionContext.getResourceShadowDiscriminator().getIntent())) { // Someone depends on us if (ResourceTypeUtil.getDependencyStrictness(dependency) == ResourceObjectTypeDependencyStrictnessType.STRICT) { throw new PolicyViolationException("Cannot remove "+accountContext.getHumanReadableName() +" because "+projectionContext.getHumanReadableName()+" depends on it"); } } } } } } } private <F extends ObjectType> boolean wasProvisioned(LensProjectionContext accountContext, int executionWave) { int accountWave = accountContext.getWave(); if (accountWave >= executionWave) { // This had no chance to be provisioned yet, so we assume it will be provisioned return true; } if (accountContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.BROKEN || accountContext.getSynchronizationPolicyDecision() == SynchronizationPolicyDecision.IGNORE) { return false; } PrismObject<ShadowType> objectCurrent = accountContext.getObjectCurrent(); if (objectCurrent != null && objectCurrent.asObjectable().getFailedOperationType() != null) { // There is unfinished operation in the shadow. We cannot continue. return false; } if (accountContext.isExists()) { return true; } if (accountContext.isAdd()) { List<LensObjectDeltaOperation<ShadowType>> executedDeltas = accountContext.getExecutedDeltas(); if (executedDeltas == null || executedDeltas.isEmpty()) { return false; } for (LensObjectDeltaOperation<ShadowType> executedDelta: executedDeltas) { OperationResult executionResult = executedDelta.getExecutionResult(); if (executionResult == null || !executionResult.isSuccess()) { return false; } } return true; } return false; } private boolean wasExecuted(LensProjectionContext accountContext){ if (accountContext.isAdd()) { if (accountContext.getOid() == null){ return false; } List<LensObjectDeltaOperation<ShadowType>> executedDeltas = accountContext.getExecutedDeltas(); if (executedDeltas == null || executedDeltas.isEmpty()) { return false; } } return true; } class DependencyAndSource { ResourceObjectTypeDependencyType dependency; LensProjectionContext sourceProjectionContext; } }