/* * Copyright (c) 2010-2016 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.provisioning.impl; import com.evolveum.midpoint.common.Clock; import com.evolveum.midpoint.common.refinery.*; import com.evolveum.midpoint.prism.*; 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.PropertyDelta; import com.evolveum.midpoint.prism.match.MatchingRule; import com.evolveum.midpoint.prism.match.MatchingRuleRegistry; import com.evolveum.midpoint.prism.path.ItemPath; import com.evolveum.midpoint.prism.query.ObjectQuery; import com.evolveum.midpoint.prism.query.builder.QueryBuilder; import com.evolveum.midpoint.prism.util.JavaTypeConverter; import com.evolveum.midpoint.prism.util.PrismUtil; import com.evolveum.midpoint.provisioning.api.GenericConnectorException; import com.evolveum.midpoint.provisioning.ucf.api.*; import com.evolveum.midpoint.provisioning.util.ProvisioningUtil; import com.evolveum.midpoint.repo.cache.RepositoryCache; import com.evolveum.midpoint.schema.CapabilityUtil; import com.evolveum.midpoint.schema.ResourceShadowDiscriminator; import com.evolveum.midpoint.schema.SearchResultMetadata; import com.evolveum.midpoint.schema.constants.SchemaConstants; import com.evolveum.midpoint.schema.internals.InternalsConfig; import com.evolveum.midpoint.schema.processor.*; import com.evolveum.midpoint.schema.result.AsynchronousOperationQueryable; import com.evolveum.midpoint.schema.result.AsynchronousOperationResult; import com.evolveum.midpoint.schema.result.AsynchronousOperationReturnValue; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.result.OperationResultStatus; import com.evolveum.midpoint.schema.util.ObjectTypeUtil; import com.evolveum.midpoint.schema.util.ResourceTypeUtil; import com.evolveum.midpoint.schema.util.SchemaDebugUtil; import com.evolveum.midpoint.schema.util.ShadowUtil; import com.evolveum.midpoint.util.*; import com.evolveum.midpoint.util.exception.*; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.*; import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ActivationCapabilityType; import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ActivationLockoutStatusCapabilityType; import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ActivationStatusCapabilityType; import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.AddRemoveAttributeValuesCapabilityType; import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.CreateCapabilityType; import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.DeleteCapabilityType; import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.LiveSyncCapabilityType; import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ReadCapabilityType; import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.UpdateCapabilityType; 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.stereotype.Component; import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.namespace.QName; import java.util.*; import java.util.Map.Entry; /** * * Responsibilities: * protected objects * simulated activation * script execution * avoid duplicate values * attributes returned by default/not returned by default * * Limitations: * must NOT access repository * does not know about OIDs * * @author Katarina Valalikova * @author Radovan Semancik * */ @Component public class ResourceObjectConverter { private static final String DOT_CLASS = ResourceObjectConverter.class.getName() + "."; public static final String OPERATION_MODIFY_ENTITLEMENT = DOT_CLASS + "modifyEntitlement"; private static final String OPERATION_ADD_RESOURCE_OBJECT = DOT_CLASS + "addResourceObject"; private static final String OPERATION_MODIFY_RESOURCE_OBJECT = DOT_CLASS + "modifyResourceObject"; private static final String OPERATION_DELETE_RESOURCE_OBJECT = DOT_CLASS + "deleteResourceObject"; private static final String OPERATION_REFRESH_OPERATION_STATUS = DOT_CLASS + "refreshOperationStatus"; @Autowired private EntitlementConverter entitlementConverter; @Autowired private MatchingRuleRegistry matchingRuleRegistry; @Autowired private ResourceObjectReferenceResolver resourceObjectReferenceResolver; @Autowired private Clock clock; @Autowired private PrismContext prismContext; private static final Trace LOGGER = TraceManager.getTrace(ResourceObjectConverter.class); static final String FULL_SHADOW_KEY = ResourceObjectConverter.class.getName()+".fullShadow"; public PrismObject<ShadowType> getResourceObject(ProvisioningContext ctx, Collection<? extends ResourceAttribute<?>> identifiers, boolean fetchAssociations, OperationResult parentResult) throws ObjectNotFoundException, CommunicationException, SchemaException, ConfigurationException, SecurityViolationException, GenericConnectorException { LOGGER.trace("Getting resource object {}", identifiers); AttributesToReturn attributesToReturn = ProvisioningUtil.createAttributesToReturn(ctx); PrismObject<ShadowType> resourceShadow = fetchResourceObject(ctx, identifiers, attributesToReturn, fetchAssociations, parentResult); // todo consider whether it is always necessary to fetch the entitlements LOGGER.trace("Got resource object {}", resourceShadow); return resourceShadow; } /** * Tries to get the object directly if primary identifiers are present. Tries to search for the object if they are not. */ public PrismObject<ShadowType> locateResourceObject(ProvisioningContext ctx, Collection<? extends ResourceAttribute<?>> identifiers, OperationResult parentResult) throws ObjectNotFoundException, CommunicationException, SchemaException, ConfigurationException, SecurityViolationException, GenericConnectorException { LOGGER.trace("Locating resource object {}", identifiers); ConnectorInstance connector = ctx.getConnector(ReadCapabilityType.class, parentResult); AttributesToReturn attributesToReturn = ProvisioningUtil.createAttributesToReturn(ctx); if (hasAllIdentifiers(identifiers, ctx.getObjectClassDefinition())) { return fetchResourceObject(ctx, identifiers, attributesToReturn, true, parentResult); // todo consider whether it is always necessary to fetch the entitlements } else { // Search Collection<? extends RefinedAttributeDefinition> secondaryIdentifierDefs = ctx.getObjectClassDefinition().getSecondaryIdentifiers(); // Assume single secondary identifier for simplicity if (secondaryIdentifierDefs.size() > 1) { throw new UnsupportedOperationException("Composite secondary identifier is not supported yet"); } else if (secondaryIdentifierDefs.isEmpty()) { throw new SchemaException("No secondary identifier defined, cannot search"); } RefinedAttributeDefinition<String> secondaryIdentifierDef = secondaryIdentifierDefs.iterator().next(); ResourceAttribute<?> secondaryIdentifier = null; for (ResourceAttribute<?> identifier: identifiers) { if (identifier.getElementName().equals(secondaryIdentifierDef.getName())) { secondaryIdentifier = identifier; } } if (secondaryIdentifier == null) { throw new SchemaException("No secondary identifier present, cannot search. Identifiers: "+identifiers); } final ResourceAttribute<?> finalSecondaryIdentifier = secondaryIdentifier; List<PrismPropertyValue<String>> secondaryIdentifierValues = (List) secondaryIdentifier.getValues(); PrismPropertyValue<String> secondaryIdentifierValue; if (secondaryIdentifierValues.size() > 1) { throw new IllegalStateException("Secondary identifier has more than one value: " + secondaryIdentifier.getValues()); } else if (secondaryIdentifierValues.size() == 1) { secondaryIdentifierValue = secondaryIdentifierValues.get(0).clone(); } else { secondaryIdentifierValue = null; } ObjectQuery query = QueryBuilder.queryFor(ShadowType.class, prismContext) .itemWithDef(secondaryIdentifierDef, ShadowType.F_ATTRIBUTES, secondaryIdentifierDef.getName()).eq(secondaryIdentifierValue) .build(); final Holder<PrismObject<ShadowType>> shadowHolder = new Holder<PrismObject<ShadowType>>(); ResultHandler<ShadowType> handler = new ResultHandler<ShadowType>() { @Override public boolean handle(PrismObject<ShadowType> shadow) { if (!shadowHolder.isEmpty()) { throw new IllegalStateException("More than one value found for secondary identifier "+finalSecondaryIdentifier); } shadowHolder.setValue(shadow); return true; } }; try { connector.search(ctx.getObjectClassDefinition(), query, handler, attributesToReturn, null, null, ctx, parentResult); if (shadowHolder.isEmpty()) { throw new ObjectNotFoundException("No object found for secondary identifier "+secondaryIdentifier); } PrismObject<ShadowType> shadow = shadowHolder.getValue(); PrismObject<ShadowType> finalShadow = postProcessResourceObjectRead(ctx, shadow, true, parentResult); LOGGER.trace("Located resource object {}", finalShadow); return finalShadow; } catch (GenericFrameworkException e) { throw new GenericConnectorException(e.getMessage(), e); } } } private boolean hasAllIdentifiers(Collection<? extends ResourceAttribute<?>> attributes, RefinedObjectClassDefinition objectClassDefinition) { Collection<? extends RefinedAttributeDefinition> identifierDefs = objectClassDefinition.getPrimaryIdentifiers(); for (RefinedAttributeDefinition identifierDef: identifierDefs) { boolean found = false; for(ResourceAttribute<?> attribute: attributes) { if (attribute.getElementName().equals(identifierDef.getName()) && !attribute.isEmpty()) { found = true; } } if (!found) { return false; } } return true; } public AsynchronousOperationReturnValue<PrismObject<ShadowType>> addResourceObject(ProvisioningContext ctx, PrismObject<ShadowType> shadow, OperationProvisioningScriptsType scripts, OperationResult parentResult) throws ObjectNotFoundException, SchemaException, CommunicationException, ObjectAlreadyExistsException, ConfigurationException, SecurityViolationException { OperationResult result = parentResult.createSubresult(OPERATION_ADD_RESOURCE_OBJECT); ResourceType resource = ctx.getResource(); LOGGER.trace("Adding resource object {}", shadow); // We might be modifying the shadow (e.g. for simulated capabilities). But we do not want the changes // to propagate back to the calling code. Hence the clone. PrismObject<ShadowType> shadowClone = shadow.clone(); ShadowType shadowType = shadowClone.asObjectable(); Collection<ResourceAttribute<?>> resourceAttributesAfterAdd = null; if (ProvisioningUtil.isProtectedShadow(ctx.getObjectClassDefinition(), shadowClone, matchingRuleRegistry)) { LOGGER.error("Attempt to add protected shadow " + shadowType + "; ignoring the request"); SecurityViolationException e = new SecurityViolationException("Cannot get protected shadow " + shadowType); result.recordFatalError(e); throw e; } Collection<Operation> additionalOperations = new ArrayList<Operation>(); addExecuteScriptOperation(additionalOperations, ProvisioningOperationTypeType.ADD, scripts, resource, result); entitlementConverter.processEntitlementsAdd(ctx, shadowClone); ConnectorInstance connector = ctx.getConnector(CreateCapabilityType.class, result); try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("PROVISIONING ADD operation on resource {}\n ADD object:\n{}\n additional operations:\n{}", resource.asPrismObject(), shadowType.asPrismObject().debugDump(), SchemaDebugUtil.debugDump(additionalOperations,2)); } transformActivationAttributesAdd(ctx, shadowType, result); if (!ResourceTypeUtil.isCreateCapabilityEnabled(resource)){ throw new UnsupportedOperationException("Resource does not support 'create' operation"); } AsynchronousOperationReturnValue<Collection<ResourceAttribute<?>>> ret = connector.addObject(shadowClone, additionalOperations, ctx, result); resourceAttributesAfterAdd = ret.getReturnValue(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("PROVISIONING ADD successful, returned attributes:\n{}", SchemaDebugUtil.prettyPrint(resourceAttributesAfterAdd)); } // Be careful not to apply this to the cloned shadow. This needs to be propagated // outside this method. applyAfterOperationAttributes(shadow, resourceAttributesAfterAdd); } catch (CommunicationException ex) { result.recordFatalError( "Could not create object on the resource. Error communicating with the connector " + connector + ": " + ex.getMessage(), ex); throw new CommunicationException("Error communicating with the connector " + connector + ": " + ex.getMessage(), ex); } catch (GenericFrameworkException ex) { result.recordFatalError("Could not create object on the resource. Generic error in connector: " + ex.getMessage(), ex); throw new GenericConnectorException("Generic error in connector: " + ex.getMessage(), ex); } catch (ObjectAlreadyExistsException ex){ result.recordFatalError("Could not create object on the resource. Object already exists on the resource: " + ex.getMessage(), ex); throw new ObjectAlreadyExistsException("Object already exists on the resource: " + ex.getMessage(), ex); } catch (ConfigurationException | SchemaException | RuntimeException | Error e){ result.recordFatalError(e); throw e; } // Execute entitlement modification on other objects (if needed) executeEntitlementChangesAdd(ctx, shadowClone, scripts, result); LOGGER.trace("Added resource object {}", shadow); computeResultStatus(result); return AsynchronousOperationReturnValue.wrap(shadow, result); } public AsynchronousOperationResult deleteResourceObject(ProvisioningContext ctx, PrismObject<ShadowType> shadow, OperationProvisioningScriptsType scripts, OperationResult parentResult) throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException, SecurityViolationException { OperationResult result = parentResult.createSubresult(OPERATION_DELETE_RESOURCE_OBJECT); LOGGER.trace("Deleting resource object {}", shadow); Collection<? extends ResourceAttribute<?>> identifiers = ShadowUtil .getAllIdentifiers(shadow); if (ProvisioningUtil.isProtectedShadow(ctx.getObjectClassDefinition(), shadow, matchingRuleRegistry)) { LOGGER.error("Attempt to delete protected resource object " + ctx.getObjectClassDefinition() + ": " + identifiers + "; ignoring the request"); SecurityViolationException e = new SecurityViolationException("Cannot delete protected resource object " + ctx.getObjectClassDefinition() + ": " + identifiers); result.recordFatalError(e); throw e; } //check idetifier if it is not null if (identifiers.isEmpty() && shadow.asObjectable().getFailedOperationType()!= null){ throw new GenericConnectorException( "Unable to delete object from the resource. Probably it has not been created yet because of previous unavailability of the resource."); } // Execute entitlement modification on other objects (if needed) executeEntitlementChangesDelete(ctx, shadow, scripts, result); Collection<Operation> additionalOperations = new ArrayList<Operation>(); addExecuteScriptOperation(additionalOperations, ProvisioningOperationTypeType.DELETE, scripts, ctx.getResource(), result); ConnectorInstance connector = ctx.getConnector(DeleteCapabilityType.class, result); try { if (LOGGER.isDebugEnabled()) { LOGGER.debug( "PROVISIONING DELETE operation on {}\n DELETE object, object class {}, identified by:\n{}\n additional operations:\n{}", ctx.getResource(), shadow.asObjectable().getObjectClass(), SchemaDebugUtil.debugDump(identifiers), SchemaDebugUtil.debugDump(additionalOperations)); } if (!ResourceTypeUtil.isDeleteCapabilityEnabled(ctx.getResource())){ UnsupportedOperationException e = new UnsupportedOperationException("Resource does not support 'delete' operation"); result.recordFatalError(e); throw e; } connector.deleteObject(ctx.getObjectClassDefinition(), additionalOperations, identifiers, ctx, result); computeResultStatus(result); LOGGER.debug("PROVISIONING DELETE: {}", result.getStatus()); } catch (ObjectNotFoundException ex) { result.recordFatalError("Can't delete object " + shadow + ". Reason: " + ex.getMessage(), ex); throw new ObjectNotFoundException("An error occured while deleting resource object " + shadow + "whith identifiers " + identifiers + ": " + ex.getMessage(), ex); } catch (CommunicationException ex) { result.recordFatalError( "Error communicating with the connector " + connector + ": " + ex.getMessage(), ex); throw new CommunicationException("Error communicating with the connector " + connector + ": " + ex.getMessage(), ex); } catch (ConfigurationException ex) { result.recordFatalError( "Configuration error in connector " + connector + ": " + ex.getMessage(), ex); throw new ConfigurationException("Configuration error in connector " + connector + ": " + ex.getMessage(), ex); } catch (GenericFrameworkException ex) { result.recordFatalError("Generic error in connector: " + ex.getMessage(), ex); throw new GenericConnectorException("Generic error in connector: " + ex.getMessage(), ex); } LOGGER.trace("Deleted resource object {}", shadow); return AsynchronousOperationResult.wrap(result); } public AsynchronousOperationReturnValue<Collection<PropertyDelta<PrismPropertyValue>>> modifyResourceObject( ProvisioningContext ctx, PrismObject<ShadowType> repoShadow, OperationProvisioningScriptsType scripts, Collection<? extends ItemDelta> itemDeltas, OperationResult parentResult) throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException, SecurityViolationException, ObjectAlreadyExistsException { OperationResult result = parentResult.createSubresult(OPERATION_MODIFY_RESOURCE_OBJECT); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Modifying resource object {}, deltas:\n", repoShadow, DebugUtil.debugDump(itemDeltas, 1)); } RefinedObjectClassDefinition objectClassDefinition = ctx.getObjectClassDefinition(); Collection<Operation> operations = new ArrayList<Operation>(); Collection<? extends ResourceAttribute<?>> identifiers = ShadowUtil.getAllIdentifiers(repoShadow); Collection<? extends ResourceAttribute<?>> primaryIdentifiers = ShadowUtil.getPrimaryIdentifiers(repoShadow); if (ProvisioningUtil.isProtectedShadow(ctx.getObjectClassDefinition(), repoShadow, matchingRuleRegistry)) { if (hasChangesOnResource(itemDeltas)) { LOGGER.error("Attempt to modify protected resource object " + objectClassDefinition + ": " + identifiers); SecurityViolationException e = new SecurityViolationException("Cannot modify protected resource object " + objectClassDefinition + ": " + identifiers); result.recordFatalError(e); throw e; } else { // Return immediately. This structure of the code makes sure that we do not execute any // resource operation for protected account even if there is a bug in the code below. LOGGER.trace("No resource modifications for protected resource object {}: {}; skipping", objectClassDefinition, identifiers); result.recordNotApplicableIfUnknown(); return AsynchronousOperationReturnValue.wrap(null, result); } } boolean hasVolatilityTriggerModification = false; boolean hasResourceModification = false; for (ItemDelta modification: itemDeltas) { ItemPath path = modification.getPath(); QName firstPathName = ItemPath.getFirstName(path); if (QNameUtil.match(firstPathName, ShadowType.F_ATTRIBUTES)) { hasResourceModification = true; QName attrName = ItemPath.getFirstName(path.rest()); RefinedAttributeDefinition<Object> attrDef = ctx.getObjectClassDefinition().findAttributeDefinition(attrName); if (attrDef.isVolatilityTrigger()) { LOGGER.trace("Will pre-read and re-read object because volatility trigger attribute {} has changed", attrName); hasVolatilityTriggerModification = true; break; } } else if (QNameUtil.match(firstPathName, ShadowType.F_ACTIVATION) || QNameUtil.match(firstPathName, ShadowType.F_CREDENTIALS) || QNameUtil.match(firstPathName, ShadowType.F_ASSOCIATION) || QNameUtil.match(firstPathName, ShadowType.F_AUXILIARY_OBJECT_CLASS)) { hasResourceModification = true; } } if (!hasResourceModification) { // Quit early, so we avoid potential pre-read and other processing when there is no point of doing so. // Also the read may fail which may invoke consistency mechanism which will complicate the situation. LOGGER.trace("No resource modification found for {}, skipping", identifiers); result.recordNotApplicableIfUnknown(); return AsynchronousOperationReturnValue.wrap(null, result); } /* * State of the shadow before execution of the deltas - e.g. with original attributes, as it may be recorded in such a way in * groups of which this account is a member of. (In case of object->subject associations.) * * This is used when the resource does NOT provide referential integrity by itself. This is e.g. the case of OpenDJ with default * settings. * * On the contrary, AD and OpenDJ with referential integrity plugin do provide automatic referential integrity, so this feature is * not needed. * * We decide based on setting of explicitReferentialIntegrity in association definition. */ collectAttributeAndEntitlementChanges(ctx, itemDeltas, operations, repoShadow, result); PrismObject<ShadowType> preReadShadow = null; Collection<PropertyModificationOperation> sideEffectOperations = null; //check identifier if it is not null if (primaryIdentifiers.isEmpty() && repoShadow.asObjectable().getFailedOperationType()!= null){ GenericConnectorException e = new GenericConnectorException( "Unable to modify object in the resource. Probably it has not been created yet because of previous unavailability of the resource."); result.recordFatalError(e); throw e; } if (hasVolatilityTriggerModification || ResourceTypeUtil.isAvoidDuplicateValues(ctx.getResource()) || isRename(ctx, operations)) { // We need to filter out the deltas that add duplicate values or remove values that are not there LOGGER.trace("Pre-reading resource shadow"); preReadShadow = preReadShadow(ctx, identifiers, operations, true, result); // yes, we need associations here if (LOGGER.isTraceEnabled()) { LOGGER.trace("Pre-read object:\n{}", preReadShadow.debugDump()); } } if (!operations.isEmpty()) { // This must go after the skip check above. Otherwise the scripts would be executed even if there is no need to. addExecuteScriptOperation(operations, ProvisioningOperationTypeType.MODIFY, scripts, ctx.getResource(), result); // Execute primary ICF operation on this shadow sideEffectOperations = executeModify(ctx, preReadShadow, identifiers, operations, result); } else { // We have to check BEFORE we add script operations, otherwise the check would be pointless LOGGER.trace("No modifications for connector object specified. Skipping processing of subject executeModify."); } Collection<PropertyDelta<PrismPropertyValue>> sideEffectDeltas = convertToPropertyDelta(sideEffectOperations); /* * State of the shadow after execution of the deltas - e.g. with new DN (if it was part of the delta), because this one should be recorded * in groups of which this account is a member of. (In case of object->subject associations.) */ PrismObject<ShadowType> shadowAfter = preReadShadow == null ? repoShadow.clone() : preReadShadow.clone(); for (ItemDelta itemDelta : itemDeltas) { itemDelta.applyTo(shadowAfter); } PrismObject<ShadowType> postReadShadow = null; if (hasVolatilityTriggerModification) { // There may be other changes that were not detected by the connector. Re-read the object and compare. LOGGER.trace("Post-reading resource shadow"); postReadShadow = preReadShadow(ctx, identifiers, operations, true, result); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Post-read object:\n{}", postReadShadow.debugDump()); } ObjectDelta<ShadowType> resourceShadowDelta = preReadShadow.diff(postReadShadow); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Determined side-effect changes by old-new diff:\n{}", resourceShadowDelta.debugDump()); } for (ItemDelta modification: resourceShadowDelta.getModifications()) { if (modification.getParentPath().startsWithName(ShadowType.F_ATTRIBUTES) && !ItemDelta.hasEquivalent(itemDeltas, modification)) { ItemDelta.merge(sideEffectDeltas, modification); } } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Side-effect changes after merging with old-new diff:\n{}", DebugUtil.debugDump(sideEffectDeltas)); } } Collection<? extends ItemDelta> allDeltas = new ArrayList<>(); ((Collection)allDeltas).addAll(itemDeltas); ((Collection)allDeltas).addAll(sideEffectDeltas); // Execute entitlement modification on other objects (if needed) shadowAfter = executeEntitlementChangesModify(ctx, preReadShadow == null ? repoShadow : preReadShadow, postReadShadow == null ? shadowAfter : postReadShadow, scripts, allDeltas, result); if (!sideEffectDeltas.isEmpty()) { if (preReadShadow != null) { PrismUtil.setDeltaOldValue(preReadShadow, sideEffectDeltas); } else { PrismUtil.setDeltaOldValue(repoShadow, sideEffectDeltas); } } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Modificaiton side-effect changes:\n{}", DebugUtil.debugDump(sideEffectDeltas)); } LOGGER.trace("Modified resource object {}", repoShadow); computeResultStatus(result); return AsynchronousOperationReturnValue.wrap(sideEffectDeltas, result); } private Collection<PropertyDelta<PrismPropertyValue>> convertToPropertyDelta( Collection<PropertyModificationOperation> sideEffectOperations) { Collection<PropertyDelta<PrismPropertyValue>> sideEffectDeltas = new ArrayList<PropertyDelta<PrismPropertyValue>>(); if (sideEffectOperations != null) { for (PropertyModificationOperation mod : sideEffectOperations){ sideEffectDeltas.add(mod.getPropertyDelta()); } } return sideEffectDeltas; } private Collection<PropertyModificationOperation> executeModify(ProvisioningContext ctx, PrismObject<ShadowType> currentShadow, Collection<? extends ResourceAttribute<?>> identifiers, Collection<Operation> operations, OperationResult parentResult) throws ObjectNotFoundException, CommunicationException, SchemaException, SecurityViolationException, ConfigurationException, ObjectAlreadyExistsException { Collection<PropertyModificationOperation> sideEffectChanges = new HashSet<>(); RefinedObjectClassDefinition objectClassDefinition = ctx.getObjectClassDefinition(); if (operations.isEmpty()){ LOGGER.trace("No modifications for resource object. Skipping modification."); return new ArrayList<>(0); } else { LOGGER.trace("Resource object modification operations: {}", operations); } if (!ShadowUtil.hasPrimaryIdentifier(identifiers, objectClassDefinition)) { Collection<? extends ResourceAttribute<?>> primaryIdentifiers = resourceObjectReferenceResolver.resolvePrimaryIdentifier(ctx, identifiers, "modification of resource object "+identifiers, parentResult); if (primaryIdentifiers == null || primaryIdentifiers.isEmpty()) { throw new ObjectNotFoundException("Cannot find repository shadow for identifiers "+identifiers); } identifiers = primaryIdentifiers; } // Invoke ICF ConnectorInstance connector = ctx.getConnector(UpdateCapabilityType.class, parentResult); try { if (ResourceTypeUtil.isAvoidDuplicateValues(ctx.getResource())) { if (currentShadow == null) { LOGGER.trace("Fetching shadow for duplicate filtering"); currentShadow = preReadShadow(ctx, identifiers, operations, false, parentResult); } Collection<Operation> filteredOperations = new ArrayList(operations.size()); for (Operation origOperation: operations) { if (origOperation instanceof PropertyModificationOperation) { PropertyModificationOperation modificationOperation = (PropertyModificationOperation)origOperation; PropertyDelta<?> propertyDelta = modificationOperation.getPropertyDelta(); PropertyDelta<?> filteredDelta = ProvisioningUtil.narrowPropertyDelta(propertyDelta, currentShadow, modificationOperation.getMatchingRuleQName(), matchingRuleRegistry); if (filteredDelta != null && !filteredDelta.isEmpty()) { if (propertyDelta == filteredDelta) { filteredOperations.add(origOperation); } else { PropertyModificationOperation newOp = new PropertyModificationOperation(filteredDelta); newOp.setMatchingRuleQName(modificationOperation.getMatchingRuleQName()); filteredOperations.add(newOp); } } else { LOGGER.trace("Filtering out modification {} because it has empty delta after narrow", propertyDelta); } } else if (origOperation instanceof ExecuteProvisioningScriptOperation){ filteredOperations.add(origOperation); } } if (filteredOperations.isEmpty()) { LOGGER.debug("No modifications for connector object specified (after filtering). Skipping processing."); parentResult.recordSuccess(); return new ArrayList<>(0); } operations = filteredOperations; } if (LOGGER.isDebugEnabled()) { LOGGER.debug( "PROVISIONING MODIFY operation on {}\n MODIFY object, object class {}, identified by:\n{}\n changes:\n{}", ctx.getResource(), objectClassDefinition.getHumanReadableName(), SchemaDebugUtil.debugDump(identifiers, 1), SchemaDebugUtil.debugDump(operations, 1)); } if (!ResourceTypeUtil.isUpdateCapabilityEnabled(ctx.getResource())){ if (operations == null || operations.isEmpty()){ LOGGER.debug("No modifications for connector object specified (after filtering). Skipping processing."); parentResult.recordSuccess(); return new ArrayList<>(0); } UnsupportedOperationException e = new UnsupportedOperationException("Resource does not support 'update' operation"); parentResult.recordFatalError(e); throw e; } Collection<ResourceAttribute<?>> identifiersWorkingCopy = cloneIdentifiers(identifiers); // because identifiers can be modified e.g. on rename operation List<Collection<Operation>> operationsWaves = sortOperationsIntoWaves(operations, objectClassDefinition); LOGGER.trace("Operation waves: {}", operationsWaves.size()); boolean inProgress = false; String asyncronousOperationReference = null; for (Collection<Operation> operationsWave : operationsWaves) { Collection<RefinedAttributeDefinition> readReplaceAttributes = determineReadReplace(operationsWave, objectClassDefinition); LOGGER.trace("Read+Replace attributes: {}", readReplaceAttributes); if (!readReplaceAttributes.isEmpty()) { AttributesToReturn attributesToReturn = new AttributesToReturn(); attributesToReturn.setReturnDefaultAttributes(false); attributesToReturn.setAttributesToReturn(readReplaceAttributes); // TODO eliminate this fetch if this is first wave and there are no explicitly requested attributes // but make sure currentShadow contains all required attributes LOGGER.trace("Fetching object because of READ+REPLACE mode"); currentShadow = fetchResourceObject(ctx, identifiersWorkingCopy, attributesToReturn, false, parentResult); operationsWave = convertToReplace(ctx, operationsWave, currentShadow); } if (!operationsWave.isEmpty()) { AsynchronousOperationReturnValue<Collection<PropertyModificationOperation>> ret = connector.modifyObject(objectClassDefinition, identifiersWorkingCopy, operationsWave, ctx, parentResult); Collection<PropertyModificationOperation> sideEffects = ret.getReturnValue(); if (sideEffects != null) { sideEffectChanges.addAll(sideEffects); // we accept that one attribute can be changed multiple times in sideEffectChanges; TODO: normalize } if (ret.isInProgress()) { inProgress = true; asyncronousOperationReference = ret.getOperationResult().getAsynchronousOperationReference(); } } } if (LOGGER.isDebugEnabled()) { LOGGER.debug("PROVISIONING MODIFY successful, inProgress={}, side-effect changes {}", inProgress, DebugUtil.debugDump(sideEffectChanges)); } if (inProgress) { parentResult.recordInProgress(); parentResult.setAsynchronousOperationReference(asyncronousOperationReference); } } catch (ObjectNotFoundException ex) { parentResult.recordFatalError("Object to modify not found: " + ex.getMessage(), ex); throw new ObjectNotFoundException("Object to modify not found: " + ex.getMessage(), ex); } catch (CommunicationException ex) { parentResult.recordFatalError( "Error communicating with the connector " + connector + ": " + ex.getMessage(), ex); throw new CommunicationException("Error communicating with connector " + connector + ": " + ex.getMessage(), ex); } catch (SchemaException ex) { parentResult.recordFatalError("Schema violation: " + ex.getMessage(), ex); throw new SchemaException("Schema violation: " + ex.getMessage(), ex); } catch (SecurityViolationException ex) { parentResult.recordFatalError("Security violation: " + ex.getMessage(), ex); throw new SecurityViolationException("Security violation: " + ex.getMessage(), ex); } catch (GenericFrameworkException ex) { parentResult.recordFatalError( "Generic error in the connector " + connector + ": " + ex.getMessage(), ex); throw new GenericConnectorException("Generic error in connector connector " + connector + ": " + ex.getMessage(), ex); } catch (ConfigurationException ex) { parentResult.recordFatalError("Configuration error: " + ex.getMessage(), ex); throw new ConfigurationException("Configuration error: " + ex.getMessage(), ex); } catch (ObjectAlreadyExistsException ex) { parentResult.recordFatalError("Conflict during modify: " + ex.getMessage(), ex); throw new ObjectAlreadyExistsException("Conflict during modify: " + ex.getMessage(), ex); } return sideEffectChanges; } private PrismObject<ShadowType> preReadShadow(ProvisioningContext ctx, Collection<? extends ResourceAttribute<?>> identifiers, Collection<Operation> operations, boolean fetchEntitlements, OperationResult parentResult) throws ObjectNotFoundException, CommunicationException, SchemaException, SecurityViolationException, ConfigurationException { PrismObject<ShadowType> currentShadow; List<RefinedAttributeDefinition> neededExtraAttributes = new ArrayList<>(); for (Operation operation : operations) { RefinedAttributeDefinition rad = getRefinedAttributeDefinitionIfApplicable(operation, ctx.getObjectClassDefinition()); if (rad != null && (!rad.isReturnedByDefault() || rad.getFetchStrategy() == AttributeFetchStrategyType.EXPLICIT)) { neededExtraAttributes.add(rad); } } AttributesToReturn attributesToReturn = new AttributesToReturn(); attributesToReturn.setAttributesToReturn(neededExtraAttributes); currentShadow = fetchResourceObject(ctx, identifiers, attributesToReturn, fetchEntitlements, parentResult); return currentShadow; } private Collection<RefinedAttributeDefinition> determineReadReplace(Collection<Operation> operations, RefinedObjectClassDefinition objectClassDefinition) { Collection<RefinedAttributeDefinition> retval = new ArrayList<>(); for (Operation operation : operations) { RefinedAttributeDefinition rad = getRefinedAttributeDefinitionIfApplicable(operation, objectClassDefinition); if (rad != null && isReadReplaceMode(rad, objectClassDefinition) && operation instanceof PropertyModificationOperation) { // third condition is just to be sure PropertyDelta propertyDelta = ((PropertyModificationOperation) operation).getPropertyDelta(); if (propertyDelta.isAdd() || propertyDelta.isDelete()) { retval.add(rad); // REPLACE operations are not needed to be converted to READ+REPLACE } } } return retval; } private boolean isReadReplaceMode(RefinedAttributeDefinition rad, RefinedObjectClassDefinition objectClassDefinition) { if (rad.getReadReplaceMode() != null) { return rad.getReadReplaceMode(); } // READ+REPLACE mode is if addRemoveAttributeCapability is NOT present return objectClassDefinition.getEffectiveCapability(AddRemoveAttributeValuesCapabilityType.class) == null; } private RefinedAttributeDefinition getRefinedAttributeDefinitionIfApplicable(Operation operation, RefinedObjectClassDefinition objectClassDefinition) { if (operation instanceof PropertyModificationOperation) { PropertyDelta propertyDelta = ((PropertyModificationOperation) operation).getPropertyDelta(); if (isAttributeDelta(propertyDelta)) { QName attributeName = propertyDelta.getElementName(); return objectClassDefinition.findAttributeDefinition(attributeName); } } return null; } /** * Converts ADD/DELETE VALUE operations into REPLACE VALUE, if needed */ private Collection<Operation> convertToReplace(ProvisioningContext ctx, Collection<Operation> operations, PrismObject<ShadowType> currentShadow) throws SchemaException, ConfigurationException, ObjectNotFoundException, CommunicationException { List<Operation> retval = new ArrayList<>(operations.size()); for (Operation operation : operations) { if (operation instanceof PropertyModificationOperation) { PropertyDelta propertyDelta = ((PropertyModificationOperation) operation).getPropertyDelta(); if (isAttributeDelta(propertyDelta)) { QName attributeName = propertyDelta.getElementName(); RefinedAttributeDefinition rad = ctx.getObjectClassDefinition().findAttributeDefinition(attributeName); if (isReadReplaceMode(rad, ctx.getObjectClassDefinition()) && (propertyDelta.isAdd() || propertyDelta.isDelete())) { PropertyModificationOperation newOp = convertToReplace(propertyDelta, currentShadow, rad.getMatchingRuleQName()); newOp.setMatchingRuleQName(((PropertyModificationOperation) operation).getMatchingRuleQName()); retval.add(newOp); continue; } } } retval.add(operation); // for yet-unprocessed operations } return retval; } private PropertyModificationOperation convertToReplace(PropertyDelta<?> propertyDelta, PrismObject<ShadowType> currentShadow, QName matchingRuleQName) throws SchemaException { if (propertyDelta.isReplace()) { // this was probably checked before throw new IllegalStateException("PropertyDelta is both ADD/DELETE and REPLACE"); } // let's extract (parent-less) current values PrismProperty<?> currentProperty = currentShadow.findProperty(propertyDelta.getPath()); Collection<PrismPropertyValue> currentValues = new ArrayList<>(); if (currentProperty != null) { for (PrismPropertyValue currentValue : currentProperty.getValues()) { currentValues.add(currentValue.clone()); } } final MatchingRule matchingRule; if (matchingRuleQName != null) { ItemDefinition def = propertyDelta.getDefinition(); QName typeName; if (def != null) { typeName = def.getTypeName(); } else { typeName = null; // we'll skip testing rule fitness w.r.t type } matchingRule = matchingRuleRegistry.getMatchingRule(matchingRuleQName, typeName); } else { matchingRule = null; } Comparator comparator = new Comparator<PrismPropertyValue<?>>() { @Override public int compare(PrismPropertyValue<?> o1, PrismPropertyValue<?> o2) { if (o1.equalsComplex(o2, true, false, matchingRule)) { return 0; } else { return 1; } } }; // add values that have to be added if (propertyDelta.isAdd()) { for (PrismPropertyValue valueToAdd : propertyDelta.getValuesToAdd()) { if (!PrismPropertyValue.containsValue(currentValues, valueToAdd, comparator)) { currentValues.add(valueToAdd.clone()); } else { LOGGER.warn("Attempting to add a value of {} that is already present in {}: {}", valueToAdd, propertyDelta.getElementName(), currentValues); } } } // remove values that should not be there if (propertyDelta.isDelete()) { for (PrismPropertyValue valueToDelete : propertyDelta.getValuesToDelete()) { Iterator<PrismPropertyValue> iterator = currentValues.iterator(); boolean found = false; while (iterator.hasNext()) { PrismPropertyValue pValue = iterator.next(); LOGGER.trace("Comparing existing {} to about-to-be-deleted {}, matching rule: {}", pValue, valueToDelete, matchingRule); if (comparator.compare(pValue, valueToDelete) == 0) { LOGGER.trace("MATCH! compared existing {} to about-to-be-deleted {}", pValue, valueToDelete); iterator.remove(); found = true; } } if (!found) { LOGGER.warn("Attempting to remove a value of {} that is not in {}: {}", valueToDelete, propertyDelta.getElementName(), currentValues); } } } PropertyDelta resultingDelta = new PropertyDelta(propertyDelta.getPath(), propertyDelta.getPropertyDefinition(), propertyDelta.getPrismContext()); resultingDelta.setValuesToReplace(currentValues); return new PropertyModificationOperation(resultingDelta); } private List<Collection<Operation>> sortOperationsIntoWaves(Collection<Operation> operations, RefinedObjectClassDefinition objectClassDefinition) { TreeMap<Integer,Collection<Operation>> waves = new TreeMap<>(); // operations indexed by priority List<Operation> others = new ArrayList<>(); // operations executed at the end (either non-priority ones or non-attribute modifications) for (Operation operation : operations) { RefinedAttributeDefinition rad = getRefinedAttributeDefinitionIfApplicable(operation, objectClassDefinition); if (rad != null && rad.getModificationPriority() != null) { putIntoWaves(waves, rad.getModificationPriority(), operation); continue; } others.add(operation); } // computing the return value List<Collection<Operation>> retval = new ArrayList<>(waves.size()+1); Map.Entry<Integer,Collection<Operation>> entry = waves.firstEntry(); while (entry != null) { retval.add(entry.getValue()); entry = waves.higherEntry(entry.getKey()); } retval.add(others); return retval; } private void putIntoWaves(Map<Integer, Collection<Operation>> waves, Integer key, Operation operation) { Collection<Operation> wave = waves.get(key); if (wave == null) { wave = new ArrayList<>(); waves.put(key, wave); } wave.add(operation); } private Collection<ResourceAttribute<?>> cloneIdentifiers(Collection<? extends ResourceAttribute<?>> identifiers) { Collection<ResourceAttribute<?>> retval = new HashSet<>(identifiers.size()); for (ResourceAttribute<?> identifier : identifiers) { retval.add(identifier.clone()); } return retval; } private boolean isRename(ProvisioningContext ctx, Collection<Operation> modifications) throws SchemaException, ConfigurationException, ObjectNotFoundException, CommunicationException { for (Operation op : modifications){ if (!(op instanceof PropertyModificationOperation)) { continue; } if (isIdentifierDelta(ctx, ((PropertyModificationOperation)op).getPropertyDelta())) { return true; } } return false; } private <T> boolean isIdentifierDelta(ProvisioningContext ctx, PropertyDelta<T> propertyDelta) throws SchemaException, ConfigurationException, ObjectNotFoundException, CommunicationException { return ctx.getObjectClassDefinition().isPrimaryIdentifier(propertyDelta.getElementName()) || ctx.getObjectClassDefinition().isSecondaryIdentifier(propertyDelta.getElementName()); } private PrismObject<ShadowType> executeEntitlementChangesAdd(ProvisioningContext ctx, PrismObject<ShadowType> shadow, OperationProvisioningScriptsType scripts, OperationResult parentResult) throws SchemaException, ObjectNotFoundException, CommunicationException, SecurityViolationException, ConfigurationException, ObjectAlreadyExistsException { Map<ResourceObjectDiscriminator, ResourceObjectOperations> roMap = new HashMap<>(); shadow = entitlementConverter.collectEntitlementsAsObjectOperationInShadowAdd(ctx, roMap, shadow, parentResult); executeEntitlements(ctx, roMap, parentResult); return shadow; } private PrismObject<ShadowType> executeEntitlementChangesModify(ProvisioningContext ctx, PrismObject<ShadowType> subjectShadowBefore, PrismObject<ShadowType> subjectShadowAfter, OperationProvisioningScriptsType scripts, Collection<? extends ItemDelta> subjectDeltas, OperationResult parentResult) throws SchemaException, ObjectNotFoundException, CommunicationException, SecurityViolationException, ConfigurationException, ObjectAlreadyExistsException { Map<ResourceObjectDiscriminator, ResourceObjectOperations> roMap = new HashMap<>(); if (LOGGER.isTraceEnabled()) { LOGGER.trace("executeEntitlementChangesModify, old shadow:\n{}", subjectShadowBefore.debugDump(1)); } for (ItemDelta subjectDelta : subjectDeltas) { ItemPath subjectItemPath = subjectDelta.getPath(); if (new ItemPath(ShadowType.F_ASSOCIATION).equivalent(subjectItemPath)) { ContainerDelta<ShadowAssociationType> containerDelta = (ContainerDelta<ShadowAssociationType>)subjectDelta; subjectShadowAfter = entitlementConverter.collectEntitlementsAsObjectOperation(ctx, roMap, containerDelta, subjectShadowBefore, subjectShadowAfter, parentResult); } else { ContainerDelta<ShadowAssociationType> associationDelta = ContainerDelta.createDelta(ShadowType.F_ASSOCIATION, subjectShadowBefore.getDefinition()); PrismContainer<ShadowAssociationType> associationContainer = subjectShadowBefore.findContainer(ShadowType.F_ASSOCIATION); if (associationContainer == null || associationContainer.isEmpty()){ LOGGER.trace("No shadow association container in old shadow. Skipping processing entitlements change for {}.", subjectItemPath); continue; } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Processing association container in old shadow for {}:\n{}", subjectItemPath, associationContainer.debugDump(1)); } // Delete + re-add association values that should ensure correct functioning in case of rename // This has to be done only for associations that require explicit referential integrity. // For these that do not, it is harmful, so it must be skipped. for (PrismContainerValue<ShadowAssociationType> associationValue : associationContainer.getValues()) { QName associationName = associationValue.asContainerable().getName(); if (associationName == null) { throw new IllegalStateException("No association name in " + associationValue); } RefinedAssociationDefinition associationDefinition = ctx.getObjectClassDefinition().findAssociationDefinition(associationName); if (associationDefinition == null) { throw new IllegalStateException("No association definition for " + associationValue); } if (!associationDefinition.requiresExplicitReferentialIntegrity()) { continue; } QName valueAttributeName = associationDefinition.getResourceObjectAssociationType().getValueAttribute(); if (!ShadowUtil.matchesAttribute(subjectItemPath, valueAttributeName)) { continue; } LOGGER.trace("Processing association {} on rename", associationName); associationDelta.addValuesToDelete(associationValue.clone()); associationDelta.addValuesToAdd(associationValue.clone()); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Resulting association delta for {}:\n{}", subjectItemPath, associationDelta.debugDump(1)); } if (!associationDelta.isEmpty()) { entitlementConverter.collectEntitlementsAsObjectOperation(ctx, roMap, associationDelta, subjectShadowBefore, subjectShadowAfter, parentResult); } // shadowAfter.findOrCreateContainer(ShadowType.F_ASSOCIATION).addAll((Collection) association.getClonedValues()); // entitlementConverter.processEntitlementsAdd(resource, shadowAfter, objectClassDefinition); } } executeEntitlements(ctx, roMap, parentResult); return subjectShadowAfter; } private void executeEntitlementChangesDelete(ProvisioningContext ctx, PrismObject<ShadowType> subjectShadow, OperationProvisioningScriptsType scripts, OperationResult parentResult) throws SchemaException { try { Map<ResourceObjectDiscriminator, ResourceObjectOperations> roMap = new HashMap<>(); entitlementConverter.collectEntitlementsAsObjectOperationDelete(ctx, roMap, subjectShadow, parentResult); executeEntitlements(ctx, roMap, parentResult); // TODO: now just log the errors, but not NOT re-throw the exception (except for some exceptions) // we want the original delete to take place, throwing an exception would spoil that } catch (SchemaException e) { throw e; } catch (CommunicationException e) { LOGGER.error(e.getMessage(), e); } catch (ObjectNotFoundException e) { LOGGER.error(e.getMessage(), e); } catch (SecurityViolationException e) { LOGGER.error(e.getMessage(), e); } catch (ConfigurationException e) { LOGGER.error(e.getMessage(), e); } catch (ObjectAlreadyExistsException e) { LOGGER.error(e.getMessage(), e); } } private void executeEntitlements(ProvisioningContext subjectCtx, Map<ResourceObjectDiscriminator, ResourceObjectOperations> roMap, OperationResult parentResult) throws ObjectNotFoundException, CommunicationException, SchemaException, SecurityViolationException, ConfigurationException, ObjectAlreadyExistsException { if (LOGGER.isTraceEnabled()) { LOGGER.trace("Excuting entitlement chanes, roMap:\n{}", DebugUtil.debugDump(roMap, 1)); } for (Entry<ResourceObjectDiscriminator,ResourceObjectOperations> entry: roMap.entrySet()) { ResourceObjectDiscriminator disc = entry.getKey(); ProvisioningContext entitlementCtx = entry.getValue().getResourceObjectContext(); Collection<? extends ResourceAttribute<?>> primaryIdentifiers = disc.getPrimaryIdentifiers(); ResourceObjectOperations resourceObjectOperations = entry.getValue(); Collection<? extends ResourceAttribute<?>> allIdentifiers = resourceObjectOperations.getAllIdentifiers(); if (allIdentifiers == null || allIdentifiers.isEmpty()) { allIdentifiers = primaryIdentifiers; } Collection<Operation> operations = resourceObjectOperations.getOperations(); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Excuting entitlement change identifiers={}:\n{}", allIdentifiers, DebugUtil.debugDump(operations, 1)); } OperationResult result = parentResult.createMinorSubresult(OPERATION_MODIFY_ENTITLEMENT); try { executeModify(entitlementCtx, entry.getValue().getCurrentShadow(), allIdentifiers, operations, result); result.recordSuccess(); } catch (ObjectNotFoundException | CommunicationException | SchemaException | SecurityViolationException | ConfigurationException | ObjectAlreadyExistsException e) { // We need to handle this specially. // E.g. ObjectNotFoundException means that the entitlement object was not found, // not that the subject was not found. It we throw ObjectNotFoundException here it may be // interpreted by the consistency code to mean that the subject is missing. Which is not // true. And that may cause really strange reactions. In fact we do not want to throw the // exception at all, because the primary operation was obviously successful. So just // properly record the operation in the result. LOGGER.error("Error while modifying entitlement {} of {}: {}", entitlementCtx, subjectCtx, e.getMessage(), e); result.recordFatalError(e); } catch (RuntimeException | Error e) { LOGGER.error("Error while modifying entitlement {} of {}: {}", entitlementCtx, subjectCtx, e.getMessage(), e); result.recordFatalError(e); throw e; } } } public SearchResultMetadata searchResourceObjects(final ProvisioningContext ctx, final ResultHandler<ShadowType> resultHandler, ObjectQuery query, final boolean fetchAssociations, final OperationResult parentResult) throws SchemaException, CommunicationException, ObjectNotFoundException, ConfigurationException, SecurityViolationException { LOGGER.trace("Searching resource objects, query: {}", query); RefinedObjectClassDefinition objectClassDef = ctx.getObjectClassDefinition(); AttributesToReturn attributesToReturn = ProvisioningUtil.createAttributesToReturn(ctx); SearchHierarchyConstraints searchHierarchyConstraints = null; ResourceObjectReferenceType baseContextRef = objectClassDef.getBaseContext(); if (baseContextRef != null) { PrismObject<ShadowType> baseContextShadow = resourceObjectReferenceResolver.resolve(ctx, baseContextRef, null, "base context specification in "+objectClassDef, parentResult); if (baseContextShadow == null) { throw new ObjectNotFoundException("No base context defined by "+baseContextRef+" in base context specification in "+objectClassDef); } RefinedObjectClassDefinition baseContextObjectClassDefinition = ctx.getRefinedSchema().determineCompositeObjectClassDefinition(baseContextShadow); ResourceObjectIdentification baseContextIdentification = ShadowUtil.getResourceObjectIdentification(baseContextShadow, baseContextObjectClassDefinition); searchHierarchyConstraints = new SearchHierarchyConstraints(baseContextIdentification, null); } if (InternalsConfig.consistencyChecks && query != null && query.getFilter() != null) { query.getFilter().checkConsistence(true); } ResultHandler<ShadowType> innerResultHandler = new ResultHandler<ShadowType>() { @Override public boolean handle(PrismObject<ShadowType> shadow) { // in order to utilize the cache right from the beginning... RepositoryCache.enter(); try { try { shadow = postProcessResourceObjectRead(ctx, shadow, fetchAssociations, parentResult); } catch (SchemaException | CommunicationException | ConfigurationException | SecurityViolationException | ObjectNotFoundException e) { throw new TunnelException(e); } return resultHandler.handle(shadow); } finally { RepositoryCache.exit(); } } }; ConnectorInstance connector = ctx.getConnector(ReadCapabilityType.class, parentResult); SearchResultMetadata metadata = null; try { metadata = connector.search(objectClassDef, query, innerResultHandler, attributesToReturn, objectClassDef.getPagedSearches(), searchHierarchyConstraints, ctx, parentResult); } catch (GenericFrameworkException e) { parentResult.recordFatalError("Generic error in the connector: " + e.getMessage(), e); throw new SystemException("Generic error in the connector: " + e.getMessage(), e); } catch (CommunicationException ex) { parentResult.recordFatalError( "Error communicating with the connector " + connector + ": " + ex.getMessage(), ex); throw new CommunicationException("Error communicating with the connector " + connector + ": " + ex.getMessage(), ex); } catch (SecurityViolationException ex) { parentResult.recordFatalError( "Security violation communicating with the connector " + connector + ": " + ex.getMessage(), ex); throw new SecurityViolationException("Security violation communicating with the connector " + connector + ": " + ex.getMessage(), ex); } catch (TunnelException e) { Throwable cause = e.getCause(); if (cause instanceof SchemaException) { throw (SchemaException)cause; } else if (cause instanceof CommunicationException) { throw (CommunicationException)cause; } else if (cause instanceof ObjectNotFoundException) { throw (ObjectNotFoundException)cause; } else if (cause instanceof ConfigurationException) { throw (ConfigurationException)cause; } else if (cause instanceof SecurityViolationException) { throw (SecurityViolationException)cause; } else if (cause instanceof GenericFrameworkException) { throw new GenericConnectorException(cause.getMessage(), cause); } else { throw new SystemException(cause.getMessage(), cause); } } computeResultStatus(parentResult); LOGGER.trace("Searching resource objects done: {}", parentResult.getStatus()); return metadata; } @SuppressWarnings("rawtypes") public PrismProperty fetchCurrentToken(ProvisioningContext ctx, OperationResult parentResult) throws ObjectNotFoundException, CommunicationException, SchemaException, ConfigurationException { Validate.notNull(parentResult, "Operation result must not be null."); LOGGER.trace("Fetcing current sync token for {}", ctx); PrismProperty lastToken; ConnectorInstance connector = ctx.getConnector(LiveSyncCapabilityType.class, parentResult); try { lastToken = connector.fetchCurrentToken(ctx.getObjectClassDefinition(), ctx, parentResult); } catch (GenericFrameworkException e) { parentResult.recordFatalError("Generic error in the connector: " + e.getMessage(), e); throw new CommunicationException("Generic error in the connector: " + e.getMessage(), e); } catch (CommunicationException ex) { parentResult.recordFatalError( "Error communicating with the connector " + connector + ": " + ex.getMessage(), ex); throw new CommunicationException("Error communicating with the connector " + connector + ": " + ex.getMessage(), ex); } LOGGER.trace("Got last token: {}", SchemaDebugUtil.prettyPrint(lastToken)); computeResultStatus(parentResult); return lastToken; } private PrismObject<ShadowType> fetchResourceObject(ProvisioningContext ctx, Collection<? extends ResourceAttribute<?>> identifiers, AttributesToReturn attributesToReturn, boolean fetchAssociations, OperationResult parentResult) throws ObjectNotFoundException, CommunicationException, SchemaException, SecurityViolationException, ConfigurationException { PrismObject<ShadowType> resourceObject = resourceObjectReferenceResolver.fetchResourceObject(ctx, identifiers, attributesToReturn, parentResult); return postProcessResourceObjectRead(ctx, resourceObject, fetchAssociations, parentResult); } @SuppressWarnings({ "rawtypes", "unchecked" }) private void applyAfterOperationAttributes(PrismObject<ShadowType> shadow, Collection<ResourceAttribute<?>> resourceAttributesAfterAdd) throws SchemaException { if (resourceAttributesAfterAdd == null) { return; } ResourceAttributeContainer attributesContainer = ShadowUtil.getAttributesContainer(shadow); for (ResourceAttribute attributeAfter : resourceAttributesAfterAdd) { ResourceAttribute attributeBefore = attributesContainer.findAttribute(attributeAfter.getElementName()); if (attributeBefore != null) { attributesContainer.remove(attributeBefore); } if (!attributesContainer.contains(attributeAfter)) { attributesContainer.add(attributeAfter.clone()); } } } private Collection<Operation> determineActivationChange(ProvisioningContext ctx, ShadowType shadow, Collection<? extends ItemDelta> objectChange, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException { ResourceType resource = ctx.getResource(); Collection<Operation> operations = new ArrayList<>(); CapabilitiesType connectorCapabilities = ctx.getConnectorCapabilities(UpdateCapabilityType.class); ActivationCapabilityType activationCapability = CapabilityUtil.getEffectiveCapability(connectorCapabilities, ActivationCapabilityType.class); // administrativeStatus PropertyDelta<ActivationStatusType> enabledPropertyDelta = PropertyDelta.findPropertyDelta(objectChange, SchemaConstants.PATH_ACTIVATION_ADMINISTRATIVE_STATUS); if (enabledPropertyDelta != null) { if (activationCapability == null) { SchemaException e = new SchemaException("Attempt to change activation administrativeStatus on "+resource+" which does not have the capability"); result.recordFatalError(e); throw e; } ActivationStatusType status = enabledPropertyDelta.getPropertyNewMatchingPath().getRealValue(); LOGGER.trace("Found activation administrativeStatus change to: {}", status); if (CapabilityUtil.hasNativeCapability(connectorCapabilities, ActivationCapabilityType.class)) { // Native activation, need to check if there is not also change to simulated activation which may be in conflict checkSimulatedActivationAdministrativeStatus(ctx, objectChange, status, activationCapability, shadow, result); operations.add(new PropertyModificationOperation(enabledPropertyDelta)); } else { // Try to simulate activation capability PropertyModificationOperation activationAttribute = convertToSimulatedActivationAdministrativeStatusAttribute( ctx, enabledPropertyDelta, shadow, status, activationCapability, result); if (activationAttribute != null) { operations.add(activationAttribute); } } } // validFrom PropertyDelta<XMLGregorianCalendar> validFromPropertyDelta = PropertyDelta.findPropertyDelta(objectChange, SchemaConstants.PATH_ACTIVATION_VALID_FROM); if (validFromPropertyDelta != null) { if (CapabilityUtil.getEffectiveActivationValidFrom(activationCapability) == null) { SchemaException e = new SchemaException("Attempt to change activation validFrom on "+resource+" which does not have the capability"); result.recordFatalError(e); throw e; } XMLGregorianCalendar xmlCal = validFromPropertyDelta.getPropertyNewMatchingPath().getRealValue(); LOGGER.trace("Found activation validFrom change to: {}", xmlCal); operations.add(new PropertyModificationOperation(validFromPropertyDelta)); } // validTo PropertyDelta<XMLGregorianCalendar> validToPropertyDelta = PropertyDelta.findPropertyDelta(objectChange, SchemaConstants.PATH_ACTIVATION_VALID_TO); if (validToPropertyDelta != null) { if (CapabilityUtil.getEffectiveActivationValidTo(activationCapability) == null) { SchemaException e = new SchemaException("Attempt to change activation validTo on "+resource+" which does not have the capability"); result.recordFatalError(e); throw e; } XMLGregorianCalendar xmlCal = validToPropertyDelta.getPropertyNewMatchingPath().getRealValue(); LOGGER.trace("Found activation validTo change to: {}", xmlCal); operations.add(new PropertyModificationOperation(validToPropertyDelta)); } PropertyDelta<LockoutStatusType> lockoutPropertyDelta = PropertyDelta.findPropertyDelta(objectChange, SchemaConstants.PATH_ACTIVATION_LOCKOUT_STATUS); if (lockoutPropertyDelta != null) { if (activationCapability == null) { SchemaException e = new SchemaException("Attempt to change activation lockoutStatus on "+resource+" which does not have the capability"); result.recordFatalError(e); throw e; } LockoutStatusType status = lockoutPropertyDelta.getPropertyNewMatchingPath().getRealValue(); LOGGER.trace("Found activation lockoutStatus change to: {}", status); if (CapabilityUtil.hasNativeCapability(connectorCapabilities, ActivationCapabilityType.class)) { // Native lockout, need to check if there is not also change to simulated activation which may be in conflict checkSimulatedActivationLockoutStatus(ctx, objectChange, status, activationCapability, shadow, result); operations.add(new PropertyModificationOperation(lockoutPropertyDelta)); } else { // Try to simulate lockout capability PropertyModificationOperation activationAttribute = convertToSimulatedActivationLockoutStatusAttribute( ctx, lockoutPropertyDelta, shadow, status, activationCapability, result); operations.add(activationAttribute); } } return operations; } private <T> void checkSimulatedActivationAdministrativeStatus(ProvisioningContext ctx, Collection<? extends ItemDelta> objectChange, ActivationStatusType status, ActivationCapabilityType activationCapabilityType, ShadowType shadow, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException{ ActivationStatusCapabilityType capActStatus = getActivationAdministrativeStatusFromSimulatedActivation(ctx, activationCapabilityType, shadow, result); ResourceAttribute<T> activationAttribute = getSimulatedActivationAdministrativeStatusAttribute(ctx, shadow, capActStatus, result); if (activationAttribute == null) { return; } PropertyDelta<T> simulatedActivationDelta = PropertyDelta.findPropertyDelta(objectChange, activationAttribute.getPath()); if (simulatedActivationDelta == null) { return; } PrismProperty<T> simulatedActivationProperty = simulatedActivationDelta.getPropertyNewMatchingPath(); Collection<T> realValues = simulatedActivationProperty.getRealValues(); if (realValues.isEmpty()) { //nothing to do, no value for simulatedActivation return; } if (realValues.size() > 1) { throw new SchemaException("Found more than one value for simulated activation."); } T simulatedActivationValue = realValues.iterator().next(); boolean transformedValue = getTransformedValue(ctx, activationCapabilityType, shadow, simulatedActivationValue, result); if (transformedValue && status == ActivationStatusType.ENABLED) { //this is ok, simulated value and also value for native capability resulted to the same vale } else{ throw new SchemaException("Found conflicting change for activation. Simulated activation resulted to " + transformedValue +", but native activation resulted to " + status); } } private void checkSimulatedActivationLockoutStatus(ProvisioningContext ctx, Collection<? extends ItemDelta> objectChange, LockoutStatusType status, ActivationCapabilityType activationCapability, ShadowType shadow, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException{ ActivationLockoutStatusCapabilityType capActStatus = getActivationLockoutStatusFromSimulatedActivation(ctx, activationCapability, shadow, result); ResourceAttribute<?> activationAttribute = getSimulatedActivationLockoutStatusAttribute(ctx, shadow, capActStatus, result); if (activationAttribute == null){ return; } PropertyDelta simulatedActivationDelta = PropertyDelta.findPropertyDelta(objectChange, activationAttribute.getPath()); PrismProperty simulatedActivationProperty = simulatedActivationDelta.getPropertyNewMatchingPath(); Collection realValues = simulatedActivationProperty.getRealValues(); if (realValues.isEmpty()) { //nothing to do, no value for simulatedActivation return; } if (realValues.size() > 1) { throw new SchemaException("Found more than one value for simulated lockout."); } Object simulatedActivationValue = realValues.iterator().next(); boolean transformedValue = getTransformedValue(ctx, activationCapability, shadow, simulatedActivationValue, result); // TODO this is strange; evaluating lockout but looking at status! [med] if (transformedValue && status == LockoutStatusType.NORMAL) { //this is ok, simulated value and also value for native capability resulted to the same vale } else { throw new SchemaException("Found conflicting change for activation lockout. Simulated lockout resulted to " + transformedValue +", but native activation resulted to " + status); } } private boolean getTransformedValue(ProvisioningContext ctx, ActivationCapabilityType activationCapabilityType, ShadowType shadow, Object simulatedValue, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException{ ActivationStatusCapabilityType capActStatus = getActivationAdministrativeStatusFromSimulatedActivation(ctx, activationCapabilityType, shadow, result); String simulatedAttributeStringValue = String.valueOf(simulatedValue); // TODO MID-3374: implement correctly (convert value list to native objects before comparison) List<String> disableValues = capActStatus.getDisableValue(); for (String disable : disableValues) { if (disable.equals(simulatedAttributeStringValue)) { return false; } } List<String> enableValues = capActStatus.getEnableValue(); for (String enable : enableValues) { if (enable.equals(simulatedAttributeStringValue)) { return true; } } throw new SchemaException("Could not map value for simulated activation: " + simulatedValue + " neither to enable nor disable values."); } private void transformActivationAttributesAdd(ProvisioningContext ctx, ShadowType shadow, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException { final ActivationType activation = shadow.getActivation(); if (activation == null) { return; } PrismContainer attributesContainer = shadow.asPrismObject().findContainer(ShadowType.F_ATTRIBUTES); CapabilitiesType connectorCapabilities = ctx.getConnectorCapabilities(CreateCapabilityType.class); ActivationCapabilityType activationCapability = CapabilityUtil.getEffectiveCapability(connectorCapabilities, ActivationCapabilityType.class); if (activation.getAdministrativeStatus() != null) { if (!CapabilityUtil.hasNativeCapability(connectorCapabilities, ActivationCapabilityType.class)) { ActivationStatusCapabilityType capActStatus = getActivationAdministrativeStatusFromSimulatedActivation( ctx, activationCapability, shadow, result); if (capActStatus == null) { throw new SchemaException("Attempt to change activation/administrativeStatus on "+ctx.getResource()+" that has neither native" + " nor simulated activation capability"); } ResourceAttribute<?> newSimulatedAttr = getSimulatedActivationAdministrativeStatusAttribute(ctx, shadow, capActStatus, result); if (newSimulatedAttr != null) { Class<?> simulatedAttrValueClass = getAttributeValueClass(ctx, shadow, newSimulatedAttr, capActStatus); Object newSimulatedAttrRealValue; if (activation.getAdministrativeStatus() == ActivationStatusType.ENABLED) { newSimulatedAttrRealValue = getEnableValue(capActStatus, simulatedAttrValueClass); } else { newSimulatedAttrRealValue = getDisableValue(capActStatus, simulatedAttrValueClass); } Item existingSimulatedAttr = attributesContainer.findItem(newSimulatedAttr.getElementName()); if (!isBlank(newSimulatedAttrRealValue)) { PrismPropertyValue newSimulatedAttrValue = new PrismPropertyValue(newSimulatedAttrRealValue); if (existingSimulatedAttr == null) { newSimulatedAttr.add(newSimulatedAttrValue); attributesContainer.add(newSimulatedAttr); } else { existingSimulatedAttr.replace(newSimulatedAttrValue); } } else if (existingSimulatedAttr != null) { attributesContainer.remove(existingSimulatedAttr); } activation.setAdministrativeStatus(null); } } } // TODO enable non-string lockout values (MID-3374) if (activation.getLockoutStatus() != null) { if (!CapabilityUtil.hasNativeCapability(connectorCapabilities, ActivationCapabilityType.class)) { ActivationLockoutStatusCapabilityType capActStatus = getActivationLockoutStatusFromSimulatedActivation( ctx, activationCapability, shadow, result); if (capActStatus == null) { throw new SchemaException("Attempt to change activation/lockout on "+ctx.getResource()+" that has neither native" + " nor simulated activation capability"); } ResourceAttribute<?> activationSimulateAttribute = getSimulatedActivationLockoutStatusAttribute(ctx, shadow, capActStatus, result); if (activationSimulateAttribute != null) { LockoutStatusType status = activation.getLockoutStatus(); String activationRealValue = null; if (status == LockoutStatusType.NORMAL) { activationRealValue = getLockoutNormalValue(capActStatus); } else { activationRealValue = getLockoutLockedValue(capActStatus); } Item existingAttribute = attributesContainer.findItem(activationSimulateAttribute.getElementName()); if (!StringUtils.isBlank(activationRealValue)) { activationSimulateAttribute.add(new PrismPropertyValue(activationRealValue)); if (attributesContainer.findItem(activationSimulateAttribute.getElementName()) == null){ attributesContainer.add(activationSimulateAttribute); } else{ attributesContainer.findItem(activationSimulateAttribute.getElementName()).replace(activationSimulateAttribute.getValue()); } } else if (existingAttribute != null) { attributesContainer.remove(existingAttribute); } activation.setLockoutStatus(null); } } } } @NotNull private Class<?> getAttributeValueClass(ProvisioningContext ctx, ShadowType shadow, ResourceAttribute<?> attribute, @NotNull ActivationStatusCapabilityType capActStatus) throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException { ResourceAttributeDefinition attributeDefinition = attribute.getDefinition(); Class<?> attributeValueClass = attributeDefinition != null ? attributeDefinition.getTypeClassIfKnown() : null; if (attributeValueClass == null) { LOGGER.warn("No definition for simulated administrative status attribute {} for shadow {} on {}, assuming String", attribute, shadow, ctx.getResource()); attributeValueClass = String.class; } return attributeValueClass; } private boolean isBlank(Object realValue) { if (realValue == null) { return true; } else if (realValue instanceof String) { return StringUtils.isBlank((String) realValue); } else { return false; } } private boolean hasChangesOnResource( Collection<? extends ItemDelta> itemDeltas) { for (ItemDelta itemDelta : itemDeltas) { if (isAttributeDelta(itemDelta) || SchemaConstants.PATH_PASSWORD.equals(itemDelta.getParentPath())) { return true; } else if (SchemaConstants.PATH_ACTIVATION.equivalent(itemDelta.getParentPath())){ return true; } else if (new ItemPath(ShadowType.F_ASSOCIATION).equivalent(itemDelta.getPath())) { return true; } } return false; } private void collectAttributeAndEntitlementChanges(ProvisioningContext ctx, Collection<? extends ItemDelta> objectChange, Collection<Operation> operations, PrismObject<ShadowType> shadow, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException { if (operations == null) { operations = new ArrayList<Operation>(); } RefinedObjectClassDefinition objectClassDefinition = ctx.getObjectClassDefinition(); for (ItemDelta itemDelta : objectChange) { if (isAttributeDelta(itemDelta) || SchemaConstants.PATH_PASSWORD.equivalent(itemDelta.getParentPath())) { if (itemDelta instanceof PropertyDelta) { PropertyModificationOperation attributeModification = new PropertyModificationOperation( (PropertyDelta) itemDelta); RefinedAttributeDefinition<Object> attrDef = objectClassDefinition.findAttributeDefinition(itemDelta.getElementName()); if (attrDef != null) { attributeModification.setMatchingRuleQName(attrDef.getMatchingRuleQName()); if (itemDelta.getDefinition() == null) { itemDelta.setDefinition(attrDef); } } operations.add(attributeModification); } else if (itemDelta instanceof ContainerDelta) { // skip the container delta - most probably password change // - it is processed earlier continue; } else { throw new UnsupportedOperationException("Not supported delta: " + itemDelta); } } else if (SchemaConstants.PATH_ACTIVATION.equivalent(itemDelta.getParentPath())){ Collection<Operation> activationOperations = determineActivationChange(ctx, shadow.asObjectable(), objectChange, result); if (activationOperations != null){ operations.addAll(activationOperations); } } else if (new ItemPath(ShadowType.F_ASSOCIATION).equivalent(itemDelta.getPath())) { if (itemDelta instanceof ContainerDelta) { entitlementConverter.collectEntitlementChange(ctx, (ContainerDelta<ShadowAssociationType>)itemDelta, operations); } else { throw new UnsupportedOperationException("Not supported delta: " + itemDelta); } } else if (new ItemPath(ShadowType.F_AUXILIARY_OBJECT_CLASS).equivalent(itemDelta.getPath())) { if (itemDelta instanceof PropertyDelta) { PropertyModificationOperation attributeModification = new PropertyModificationOperation( (PropertyDelta) itemDelta); operations.add(attributeModification); } else { throw new UnsupportedOperationException("Not supported delta: " + itemDelta); } } else { LOGGER.trace("Skip converting item delta: {}. It's not resource object change, but it is shadow change.", itemDelta); } } } private boolean isAttributeDelta(ItemDelta itemDelta) { return new ItemPath(ShadowType.F_ATTRIBUTES).equivalent(itemDelta.getParentPath()); } public List<Change> fetchChanges(ProvisioningContext ctx, PrismProperty<?> lastToken, OperationResult parentResult) throws SchemaException, CommunicationException, ConfigurationException, SecurityViolationException, GenericFrameworkException, ObjectNotFoundException { Validate.notNull(parentResult, "Operation result must not be null."); LOGGER.trace("START fetch changes, objectClass: {}", ctx.getObjectClassDefinition()); AttributesToReturn attrsToReturn = null; if (!ctx.isWildcard()) { attrsToReturn = ProvisioningUtil.createAttributesToReturn(ctx); } ConnectorInstance connector = ctx.getConnector(LiveSyncCapabilityType.class, parentResult); // get changes from the connector List<Change> changes = connector.fetchChanges(ctx.getObjectClassDefinition(), lastToken, attrsToReturn, ctx, parentResult); Iterator<Change> iterator = changes.iterator(); while (iterator.hasNext()) { Change change = iterator.next(); LOGGER.trace("Original change:\n{}", change.debugDump()); if (change.isTokenOnly()) { continue; } ProvisioningContext shadowCtx = ctx; AttributesToReturn shadowAttrsToReturn = attrsToReturn; PrismObject<ShadowType> currentShadow = change.getCurrentShadow(); ObjectClassComplexTypeDefinition changeObjectClassDefinition = change.getObjectClassDefinition(); if (changeObjectClassDefinition == null) { if (!ctx.isWildcard() || change.getObjectDelta() == null || !change.getObjectDelta().isDelete()) { throw new SchemaException("No object class definition in change "+change); } } if (ctx.isWildcard() && changeObjectClassDefinition != null) { shadowCtx = ctx.spawn(changeObjectClassDefinition.getTypeName()); if (shadowCtx.isWildcard()) { String message = "Unkown object class "+changeObjectClassDefinition.getTypeName()+" found in synchronization delta"; parentResult.recordFatalError(message); throw new SchemaException(message); } change.setObjectClassDefinition(shadowCtx.getObjectClassDefinition()); shadowAttrsToReturn = ProvisioningUtil.createAttributesToReturn(shadowCtx); } if (change.getObjectDelta() == null || !change.getObjectDelta().isDelete()) { if (currentShadow == null) { // There is no current shadow in a change. Add it by fetching it explicitly. try { LOGGER.trace("Re-fetching object {} because it is not in the change", change.getIdentifiers()); currentShadow = fetchResourceObject(shadowCtx, change.getIdentifiers(), shadowAttrsToReturn, true, parentResult); // todo consider whether it is always necessary to fetch the entitlements change.setCurrentShadow(currentShadow); } catch (ObjectNotFoundException ex) { parentResult.recordHandledError( "Object detected in change log no longer exist on the resource. Skipping processing this object.", ex); LOGGER.warn("Object detected in change log no longer exist on the resource. Skipping processing this object " + ex.getMessage()); // TODO: Maybe change to DELETE instead of this? iterator.remove(); continue; } } else { if (ctx.isWildcard()) { if (!MiscUtil.equals(shadowAttrsToReturn, attrsToReturn)) { // re-fetch the shadow if necessary (if attributesToGet does not match) ResourceObjectIdentification identification = ResourceObjectIdentification.create(shadowCtx.getObjectClassDefinition(), change.getIdentifiers()); identification.validatePrimaryIdenfiers(); LOGGER.trace("Re-fetching object {} because of attrsToReturn", identification); currentShadow = connector.fetchObject(ShadowType.class, identification, shadowAttrsToReturn, ctx, parentResult); } } PrismObject<ShadowType> processedCurrentShadow = postProcessResourceObjectRead(shadowCtx, currentShadow, true, parentResult); change.setCurrentShadow(processedCurrentShadow); } } LOGGER.trace("Processed change\n:{}", change.debugDump()); } computeResultStatus(parentResult); LOGGER.trace("END fetch changes ({} changes)", changes == null ? "null" : changes.size()); return changes; } /** * Process simulated activation, credentials and other properties that are added to the object by midPoint. */ private PrismObject<ShadowType> postProcessResourceObjectRead(ProvisioningContext ctx, PrismObject<ShadowType> resourceObject, boolean fetchAssociations, OperationResult parentResult) throws SchemaException, CommunicationException, ObjectNotFoundException, ConfigurationException, SecurityViolationException { ShadowType resourceObjectType = resourceObject.asObjectable(); ProvisioningUtil.setProtectedFlag(ctx, resourceObject, matchingRuleRegistry); if (resourceObjectType.isExists() != Boolean.FALSE) { resourceObjectType.setExists(true); } completeActivation(ctx, resourceObject, parentResult); // Entitlements if (fetchAssociations) { entitlementConverter.postProcessEntitlementsRead(ctx, resourceObject, parentResult); } return resourceObject; } /** * Completes activation state by determining simulated activation if necessary. */ private void completeActivation(ProvisioningContext ctx, PrismObject<ShadowType> resourceObject, OperationResult parentResult) throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException { ResourceType resourceType = ctx.getResource(); ShadowType resourceObjectType = resourceObject.asObjectable(); CapabilitiesType connectorCapabilities = ctx.getConnectorCapabilities(ReadCapabilityType.class); ActivationCapabilityType activationCapability = CapabilityUtil.getEffectiveCapability(connectorCapabilities, ActivationCapabilityType.class); if (resourceObjectType.getActivation() != null || CapabilityUtil.isCapabilityEnabled(activationCapability)) { ActivationType activationType = null; if (CapabilityUtil.hasNativeCapability(connectorCapabilities, ActivationCapabilityType.class)) { activationType = resourceObjectType.getActivation(); } else if (CapabilityUtil.isCapabilityEnabled(activationCapability)) { activationType = convertFromSimulatedActivationAttributes(resourceType, resourceObject, activationCapability, parentResult); } else { // No activation capability, nothing to do } LOGGER.trace("Determined activation, administrativeStatus: {}, lockoutStatus: {}", activationType == null ? "null activationType" : activationType.getAdministrativeStatus(), activationType == null ? "null activationType" : activationType.getLockoutStatus()); resourceObjectType.setActivation(activationType); } else { resourceObjectType.setActivation(null); } } private static ActivationType convertFromSimulatedActivationAttributes(ResourceType resource, PrismObject<ShadowType> resourceObject, ActivationCapabilityType activationCapability, OperationResult parentResult) { // LOGGER.trace("Start converting activation type from simulated activation attribute"); if (activationCapability == null) { return null; } ActivationType activationType = new ActivationType(); convertFromSimulatedActivationAdministrativeStatus(activationType, activationCapability, resource, resourceObject, parentResult); convertFromSimulatedActivationLockoutStatus(activationType, activationCapability, resource, resourceObject, parentResult); return activationType; } private static void convertFromSimulatedActivationAdministrativeStatus(ActivationType activationType, ActivationCapabilityType activationCapability, ResourceType resource, PrismObject<ShadowType> shadow, OperationResult parentResult) { ActivationStatusCapabilityType statusCapabilityType = CapabilityUtil.getEffectiveActivationStatus(activationCapability); if (statusCapabilityType == null) { return; } ResourceAttributeContainer attributesContainer = ShadowUtil.getAttributesContainer(shadow); ResourceAttribute<?> simulatedStatusAttribute = null; if (statusCapabilityType.getAttribute() != null) { simulatedStatusAttribute = attributesContainer.findAttribute(statusCapabilityType.getAttribute()); } // LOGGER.trace("activation property: {}", activationProperty.dump()); // if (activationProperty == null) { // LOGGER.debug("No simulated activation attribute was defined for the account."); // return null; // } Collection<Object> simulatedStatusAttributeValues = null; if (simulatedStatusAttribute != null) { simulatedStatusAttributeValues = simulatedStatusAttribute.getRealValues(Object.class); } convertFromSimulatedActivationAdministrativeStatusInternal(activationType, statusCapabilityType, resource, simulatedStatusAttributeValues, parentResult); LOGGER.trace( "Detected simulated activation administrativeStatus attribute {} on {} with value {}, resolved into {}", SchemaDebugUtil.prettyPrint(statusCapabilityType.getAttribute()), ObjectTypeUtil.toShortString(resource), simulatedStatusAttributeValues, activationType == null ? "null" : activationType.getAdministrativeStatus()); // Remove the attribute which is the source of simulated activation. If we leave it there then we // will have two ways to set activation. if (statusCapabilityType.isIgnoreAttribute() == null || statusCapabilityType.isIgnoreAttribute()) { if (simulatedStatusAttribute != null) { attributesContainer.remove(simulatedStatusAttribute); } } } /** * Moved to a separate method especially to enable good logging (see above). */ private static void convertFromSimulatedActivationAdministrativeStatusInternal(ActivationType activationType, ActivationStatusCapabilityType statusCapabilityType, ResourceType resource, Collection<Object> simulatedStatusAttributeValues, OperationResult parentResult) { List<String> disableValues = statusCapabilityType.getDisableValue(); List<String> enableValues = statusCapabilityType.getEnableValue(); if (MiscUtil.isNoValue(simulatedStatusAttributeValues)) { if (MiscUtil.hasNoValue(disableValues)) { activationType.setAdministrativeStatus(ActivationStatusType.DISABLED); return; } if (MiscUtil.hasNoValue(enableValues)) { activationType.setAdministrativeStatus(ActivationStatusType.ENABLED); return; } // No activation information. LOGGER.warn("The {} does not provide definition for null value of simulated activation attribute", ObjectTypeUtil.toShortString(resource)); if (parentResult != null) { parentResult.recordPartialError("The " + ObjectTypeUtil.toShortString(resource) + " has native activation capability but does not provide value for DISABLE attribute"); } } else { if (simulatedStatusAttributeValues.size() > 1) { LOGGER.warn("The {} provides {} values for simulated activation status attribute, expecting just one value", ObjectTypeUtil.toShortString(resource), disableValues.size()); if (parentResult != null) { parentResult.recordPartialError("The " + ObjectTypeUtil.toShortString(resource) + " provides " + disableValues.size() + " values for simulated activation status attribute, expecting just one value"); } } Object disableObj = simulatedStatusAttributeValues.iterator().next(); for (String disable : disableValues) { if (disable.equals(String.valueOf(disableObj))) { // TODO MID-3374: implement seriously activationType.setAdministrativeStatus(ActivationStatusType.DISABLED); return; } } for (String enable : enableValues) { if ("".equals(enable) || enable.equals(String.valueOf(disableObj))) { // TODO MID-3374: implement seriously activationType.setAdministrativeStatus(ActivationStatusType.ENABLED); return; } } } } private static void convertFromSimulatedActivationLockoutStatus(ActivationType activationType, ActivationCapabilityType activationCapability, ResourceType resource, PrismObject<ShadowType> shadow, OperationResult parentResult) { ActivationLockoutStatusCapabilityType statusCapabilityType = CapabilityUtil.getEffectiveActivationLockoutStatus(activationCapability); if (statusCapabilityType == null) { return; } ResourceAttributeContainer attributesContainer = ShadowUtil.getAttributesContainer(shadow); ResourceAttribute<?> activationProperty = null; if (statusCapabilityType.getAttribute() != null) { activationProperty = attributesContainer.findAttribute(statusCapabilityType.getAttribute()); } // LOGGER.trace("activation property: {}", activationProperty.dump()); // if (activationProperty == null) { // LOGGER.debug("No simulated activation attribute was defined for the account."); // return null; // } Collection<Object> activationValues = null; if (activationProperty != null) { activationValues = activationProperty.getRealValues(Object.class); } convertFromSimulatedActivationLockoutStatusInternal(activationType, statusCapabilityType, resource, activationValues, parentResult); LOGGER.trace( "Detected simulated activation lockout attribute {} on {} with value {}, resolved into {}", SchemaDebugUtil.prettyPrint(statusCapabilityType.getAttribute()), ObjectTypeUtil.toShortString(resource), activationValues, activationType == null ? "null" : activationType.getAdministrativeStatus()); // Remove the attribute which is the source of simulated activation. If we leave it there then we // will have two ways to set activation. if (statusCapabilityType.isIgnoreAttribute() == null || statusCapabilityType.isIgnoreAttribute()) { if (activationProperty != null) { attributesContainer.remove(activationProperty); } } } /** * Moved to a separate method especially to enable good logging (see above). */ private static void convertFromSimulatedActivationLockoutStatusInternal(ActivationType activationType, ActivationLockoutStatusCapabilityType statusCapabilityType, ResourceType resource, Collection<Object> activationValues, OperationResult parentResult) { List<String> lockedValues = statusCapabilityType.getLockedValue(); List<String> normalValues = statusCapabilityType.getNormalValue(); if (MiscUtil.isNoValue(activationValues)) { if (MiscUtil.hasNoValue(lockedValues)) { activationType.setLockoutStatus(LockoutStatusType.LOCKED); return; } if (MiscUtil.hasNoValue(normalValues)) { activationType.setLockoutStatus(LockoutStatusType.NORMAL); return; } // No activation information. LOGGER.warn("The {} does not provide definition for null value of simulated activation lockout attribute", resource); if (parentResult != null) { parentResult.recordPartialError("The " + resource + " has native activation capability but noes not provide value for lockout attribute"); } return; } else { if (activationValues.size() > 1) { LOGGER.warn("The {} provides {} values for lockout attribute, expecting just one value", lockedValues.size(), resource); if (parentResult != null) { parentResult.recordPartialError("The " + resource + " provides " + lockedValues.size() + " values for lockout attribute, expecting just one value"); } } Object activationValue = activationValues.iterator().next(); for (String lockedValue : lockedValues) { if (lockedValue.equals(String.valueOf(activationValue))) { activationType.setLockoutStatus(LockoutStatusType.LOCKED); return; } } for (String normalValue : normalValues) { if ("".equals(normalValue) || normalValue.equals(String.valueOf(activationValue))) { activationType.setLockoutStatus(LockoutStatusType.NORMAL); return; } } } } private ActivationStatusCapabilityType getActivationAdministrativeStatusFromSimulatedActivation(ProvisioningContext ctx, ActivationCapabilityType activationCapability, ShadowType shadow, OperationResult result) throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException { if (activationCapability == null) { result.recordWarning("Resource " + ctx.getResource() + " does not have native or simulated activation capability. Processing of activation for "+ shadow +" was skipped"); shadow.setFetchResult(result.createOperationResultType()); return null; } ActivationStatusCapabilityType capActStatus = CapabilityUtil.getEffectiveActivationStatus(activationCapability); if (capActStatus == null) { result.recordWarning("Resource " + ctx.getResource() + " does not have native or simulated activation status capability. Processing of activation for "+ shadow +" was skipped"); shadow.setFetchResult(result.createOperationResultType()); return null; } return capActStatus; } private <T> ResourceAttribute<T> getSimulatedActivationAdministrativeStatusAttribute(ProvisioningContext ctx, ShadowType shadow, ActivationStatusCapabilityType capActStatus, OperationResult result) throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException { if (capActStatus == null){ return null; } ResourceType resource = ctx.getResource(); QName enableAttributeName = capActStatus.getAttribute(); LOGGER.trace("Simulated attribute name: {}", enableAttributeName); if (enableAttributeName == null) { result.recordWarning("Resource " + ObjectTypeUtil.toShortString(resource) + " does not have attribute specification for simulated activation status capability. Processing of activation for "+ shadow +" was skipped"); shadow.setFetchResult(result.createOperationResultType()); return null; } ResourceAttributeDefinition<T> enableAttributeDefinition = ctx.getObjectClassDefinition() .findAttributeDefinition(enableAttributeName); if (enableAttributeDefinition == null) { result.recordWarning("Resource " + ObjectTypeUtil.toShortString(resource) + " attribute for simulated activation/enableDisable capability" + enableAttributeName + " in not present in the schema for objeclass " + ctx.getObjectClassDefinition()+". Processing of activation for "+ ObjectTypeUtil.toShortString(shadow)+" was skipped"); shadow.setFetchResult(result.createOperationResultType()); return null; } return enableAttributeDefinition.instantiate(enableAttributeName); } private PropertyModificationOperation convertToSimulatedActivationAdministrativeStatusAttribute(ProvisioningContext ctx, PropertyDelta activationDelta, ShadowType shadow, ActivationStatusType status, ActivationCapabilityType activationCapabilityType, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException { ResourceType resource = ctx.getResource(); ActivationStatusCapabilityType capActStatus = getActivationAdministrativeStatusFromSimulatedActivation(ctx, activationCapabilityType, shadow, result); if (capActStatus == null){ throw new SchemaException("Attempt to modify activation on "+resource+" which does not have activation capability"); } ResourceAttribute<?> simulatedAttribute = getSimulatedActivationAdministrativeStatusAttribute(ctx, shadow, capActStatus, result); if (simulatedAttribute == null) { return null; } Class<?> simulatedAttrValueClass = getAttributeValueClass(ctx, shadow, simulatedAttribute, capActStatus); PropertyDelta<?> simulatedAttrDelta; if (status == null && activationDelta.isDelete()){ LOGGER.trace("deleting activation property."); simulatedAttrDelta = PropertyDelta.createModificationDeleteProperty(new ItemPath(ShadowType.F_ATTRIBUTES, simulatedAttribute.getElementName()), simulatedAttribute.getDefinition(), simulatedAttribute.getRealValue()); } else if (status == ActivationStatusType.ENABLED) { Object enableValue = getEnableValue(capActStatus, simulatedAttrValueClass); simulatedAttrDelta = createActivationPropDelta(simulatedAttribute.getElementName(), simulatedAttribute.getDefinition(), enableValue); } else { Object disableValue = getDisableValue(capActStatus, simulatedAttrValueClass); simulatedAttrDelta = createActivationPropDelta(simulatedAttribute.getElementName(), simulatedAttribute.getDefinition(), disableValue); } PropertyModificationOperation attributeChange = new PropertyModificationOperation(simulatedAttrDelta); return attributeChange; } private PropertyModificationOperation convertToSimulatedActivationLockoutStatusAttribute(ProvisioningContext ctx, PropertyDelta activationDelta, ShadowType shadow, LockoutStatusType status, ActivationCapabilityType activationCapability, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException { ActivationLockoutStatusCapabilityType capActStatus = getActivationLockoutStatusFromSimulatedActivation(ctx, activationCapability, shadow, result); if (capActStatus == null){ throw new SchemaException("Attempt to modify lockout on "+ctx.getResource()+" which does not have activation lockout capability"); } ResourceAttribute<?> activationAttribute = getSimulatedActivationLockoutStatusAttribute(ctx, shadow, capActStatus, result); if (activationAttribute == null){ return null; } PropertyDelta<?> lockoutAttributeDelta = null; if (status == null && activationDelta.isDelete()){ LOGGER.trace("deleting activation property."); lockoutAttributeDelta = PropertyDelta.createModificationDeleteProperty(new ItemPath(ShadowType.F_ATTRIBUTES, activationAttribute.getElementName()), activationAttribute.getDefinition(), activationAttribute.getRealValue()); } else if (status == LockoutStatusType.NORMAL) { String normalValue = getLockoutNormalValue(capActStatus); lockoutAttributeDelta = createActivationPropDelta(activationAttribute.getElementName(), activationAttribute.getDefinition(), normalValue); } else { String lockedValue = getLockoutLockedValue(capActStatus); lockoutAttributeDelta = createActivationPropDelta(activationAttribute.getElementName(), activationAttribute.getDefinition(), lockedValue); } PropertyModificationOperation attributeChange = new PropertyModificationOperation(lockoutAttributeDelta); return attributeChange; } private PropertyDelta<?> createActivationPropDelta(QName attrName, ResourceAttributeDefinition attrDef, Object value) { if (isBlank(value)) { return PropertyDelta.createModificationReplaceProperty(new ItemPath(ShadowType.F_ATTRIBUTES, attrName), attrDef); } else { return PropertyDelta.createModificationReplaceProperty(new ItemPath(ShadowType.F_ATTRIBUTES, attrName), attrDef, value); } } private ActivationLockoutStatusCapabilityType getActivationLockoutStatusFromSimulatedActivation(ProvisioningContext ctx, ActivationCapabilityType activationCapability, ShadowType shadow, OperationResult result) throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException{ if (activationCapability == null) { result.recordWarning("Resource " + ctx.getResource() + " does not have native or simulated activation capability. Processing of activation for "+ shadow +" was skipped"); shadow.setFetchResult(result.createOperationResultType()); return null; } ActivationLockoutStatusCapabilityType capActStatus = CapabilityUtil.getEffectiveActivationLockoutStatus(activationCapability); if (capActStatus == null) { result.recordWarning("Resource " + ctx.getResource() + " does not have native or simulated activation lockout capability. Processing of activation for "+ shadow +" was skipped"); shadow.setFetchResult(result.createOperationResultType()); return null; } return capActStatus; } private ResourceAttribute<?> getSimulatedActivationLockoutStatusAttribute(ProvisioningContext ctx, ShadowType shadow, ActivationLockoutStatusCapabilityType capActStatus, OperationResult result) throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException{ QName enableAttributeName = capActStatus.getAttribute(); LOGGER.trace("Simulated lockout attribute name: {}", enableAttributeName); if (enableAttributeName == null) { result.recordWarning("Resource " + ObjectTypeUtil.toShortString(ctx.getResource()) + " does not have attribute specification for simulated activation lockout capability. Processing of activation for "+ shadow +" was skipped"); shadow.setFetchResult(result.createOperationResultType()); return null; } ResourceAttributeDefinition enableAttributeDefinition = ctx.getObjectClassDefinition() .findAttributeDefinition(enableAttributeName); if (enableAttributeDefinition == null) { result.recordWarning("Resource " + ObjectTypeUtil.toShortString(ctx.getResource()) + " attribute for simulated activation/lockout capability" + enableAttributeName + " in not present in the schema for objeclass " + ctx.getObjectClassDefinition()+". Processing of activation for "+ ObjectTypeUtil.toShortString(shadow)+" was skipped"); shadow.setFetchResult(result.createOperationResultType()); return null; } return enableAttributeDefinition.instantiate(enableAttributeName); } private <T> T getDisableValue(ActivationStatusCapabilityType capActStatus, Class<T> clazz) { //TODO some checks Object value = capActStatus.getDisableValue().iterator().next(); return JavaTypeConverter.convert(clazz, value); } private <T> T getEnableValue(ActivationStatusCapabilityType capActStatus, Class<T> clazz) { String value = capActStatus.getEnableValue().iterator().next(); return JavaTypeConverter.convert(clazz, value); } private String getLockoutNormalValue(ActivationLockoutStatusCapabilityType capActStatus) { String value = capActStatus.getNormalValue().iterator().next(); return value; } private String getLockoutLockedValue(ActivationLockoutStatusCapabilityType capActStatus) { String value = capActStatus.getLockedValue().iterator().next(); return value; } private RefinedObjectClassDefinition determineObjectClassDefinition(PrismObject<ShadowType> shadow, ResourceType resource) throws SchemaException, ConfigurationException { ShadowType shadowType = shadow.asObjectable(); RefinedResourceSchema refinedSchema = RefinedResourceSchemaImpl.getRefinedSchema(resource, prismContext); if (refinedSchema == null) { throw new ConfigurationException("No schema definied for "+resource); } RefinedObjectClassDefinition objectClassDefinition = null; ShadowKindType kind = shadowType.getKind(); String intent = shadowType.getIntent(); QName objectClass = shadow.asObjectable().getObjectClass(); if (kind != null) { objectClassDefinition = refinedSchema.getRefinedDefinition(kind, intent); } else { // Fallback to objectclass only if (objectClass == null) { throw new SchemaException("No kind nor objectclass definied in "+shadow); } objectClassDefinition = refinedSchema.findRefinedDefinitionByObjectClassQName(null, objectClass); } if (objectClassDefinition == null) { throw new SchemaException("Definition for "+shadow+" not found (objectClass=" + PrettyPrinter.prettyPrint(objectClass) + ", kind="+kind+", intent='"+intent+"') in schema of " + resource); } return objectClassDefinition; } private ObjectClassComplexTypeDefinition determineObjectClassDefinition( ResourceShadowDiscriminator discriminator, ResourceType resource) throws SchemaException { ResourceSchema schema = RefinedResourceSchemaImpl.getResourceSchema(resource, prismContext); // HACK FIXME ObjectClassComplexTypeDefinition objectClassDefinition = schema.findObjectClassDefinition(ShadowKindType.ACCOUNT, discriminator.getIntent()); if (objectClassDefinition == null) { // Unknown objectclass throw new SchemaException("Account type " + discriminator.getIntent() + " is not known in schema of " + resource); } return objectClassDefinition; } private void addExecuteScriptOperation(Collection<Operation> operations, ProvisioningOperationTypeType type, OperationProvisioningScriptsType scripts, ResourceType resource, OperationResult result) throws SchemaException { if (scripts == null) { // No warning needed, this is quite normal LOGGER.trace("Skipping creating script operation to execute. Scripts was not defined."); return; } for (OperationProvisioningScriptType script : scripts.getScript()) { for (ProvisioningOperationTypeType operationType : script.getOperation()) { if (type.equals(operationType)) { ExecuteProvisioningScriptOperation scriptOperation = ProvisioningUtil.convertToScriptOperation( script, "script value for " + operationType + " in " + resource, prismContext); scriptOperation.setScriptOrder(script.getOrder()); LOGGER.trace("Created script operation: {}", SchemaDebugUtil.prettyPrint(scriptOperation)); operations.add(scriptOperation); } } } } public OperationResultStatus refreshOperationStatus(ProvisioningContext ctx, PrismObject<ShadowType> shadow, String asyncRef, OperationResult parentResult) throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException { OperationResult result = parentResult.createSubresult(OPERATION_REFRESH_OPERATION_STATUS); ResourceType resource; ConnectorInstance connector; try { resource = ctx.getResource(); // TODO: not really correct. But good enough for now. connector = ctx.getConnector(UpdateCapabilityType.class, result); } catch (ObjectNotFoundException | SchemaException | CommunicationException | ConfigurationException e) { result.recordFatalError(e); throw e; } OperationResultStatus status = null; if (connector instanceof AsynchronousOperationQueryable) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("PROVISIONING REFRESH operation on {}, object: {}", resource, shadow); } try { status = ((AsynchronousOperationQueryable)connector).queryOperationStatus(asyncRef, result); } catch (ObjectNotFoundException | SchemaException e) { result.recordFatalError(e); throw e; } result.recordSuccess(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("PROVISIONING REFRESH successful, returned status: {}", status); } } else { LOGGER.trace("Ignoring refresh of shadow {}, because the connector is not async"); result.recordNotApplicableIfUnknown(); } return status; } private void computeResultStatus(OperationResult parentResult) { if (parentResult.isInProgress()) { return; } OperationResultStatus status = OperationResultStatus.SUCCESS; String asyncRef = null; for (OperationResult subresult: parentResult.getSubresults()) { if (OPERATION_MODIFY_ENTITLEMENT.equals(subresult.getOperation()) && subresult.isError()) { status = OperationResultStatus.PARTIAL_ERROR; } else if (subresult.isError()) { status = OperationResultStatus.FATAL_ERROR; } else if (subresult.isInProgress()) { status = OperationResultStatus.IN_PROGRESS; asyncRef = subresult.getAsynchronousOperationReference(); } } parentResult.setStatus(status); parentResult.setAsynchronousOperationReference(asyncRef); } }