/* * Copyright (c) 2010-2017 Evolveum * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.evolveum.midpoint.provisioning.impl; import java.util.Collection; import java.util.HashMap; import java.util.Map; import javax.xml.namespace.QName; import com.evolveum.midpoint.common.refinery.*; import com.evolveum.midpoint.prism.query.builder.QueryBuilder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.evolveum.midpoint.prism.ModificationType; import com.evolveum.midpoint.prism.PrismContainer; import com.evolveum.midpoint.prism.PrismContainerDefinition; import com.evolveum.midpoint.prism.PrismContainerValue; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.prism.PrismProperty; import com.evolveum.midpoint.prism.PrismPropertyValue; import com.evolveum.midpoint.prism.delta.ContainerDelta; 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.util.PrismUtil; import com.evolveum.midpoint.provisioning.api.GenericConnectorException; import com.evolveum.midpoint.provisioning.ucf.api.AttributesToReturn; import com.evolveum.midpoint.provisioning.ucf.api.ConnectorInstance; import com.evolveum.midpoint.provisioning.ucf.api.GenericFrameworkException; import com.evolveum.midpoint.provisioning.ucf.api.Operation; import com.evolveum.midpoint.provisioning.ucf.api.PropertyModificationOperation; import com.evolveum.midpoint.provisioning.ucf.api.ResultHandler; import com.evolveum.midpoint.provisioning.util.ProvisioningUtil; import com.evolveum.midpoint.schema.processor.ResourceAttribute; import com.evolveum.midpoint.schema.processor.ResourceAttributeContainer; import com.evolveum.midpoint.schema.processor.ResourceObjectIdentification; import com.evolveum.midpoint.schema.processor.SearchHierarchyConstraints; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.util.ResourceTypeUtil; import com.evolveum.midpoint.schema.util.ShadowUtil; import com.evolveum.midpoint.util.exception.CommunicationException; import com.evolveum.midpoint.util.exception.ConfigurationException; import com.evolveum.midpoint.util.exception.ObjectNotFoundException; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.SecurityViolationException; import com.evolveum.midpoint.util.exception.TunnelException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceObjectAssociationDirectionType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceObjectReferenceType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowAssociationType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowKindType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowType; import com.evolveum.midpoint.xml.ns._public.resource.capabilities_3.ReadCapabilityType; /** * Class that collects the entitlement-related methods used by ResourceObjectConverter * * Should NOT be used by any class other than ResourceObjectConverter. * * @author Radovan Semancik * */ @Component class EntitlementConverter { private static final Trace LOGGER = TraceManager.getTrace(EntitlementConverter.class); @Autowired(required=true) private ResourceObjectReferenceResolver resourceObjectReferenceResolver; @Autowired(required=true) private PrismContext prismContext; @Autowired(required = true) private MatchingRuleRegistry matchingRuleRegistry; ////////// // GET ///////// public void postProcessEntitlementsRead(ProvisioningContext subjectCtx, PrismObject<ShadowType> resourceObject, OperationResult parentResult) throws SchemaException, CommunicationException, ObjectNotFoundException, ConfigurationException, SecurityViolationException { ResourceType resourceType = subjectCtx.getResource(); LOGGER.trace("Starting postProcessEntitlementRead"); RefinedObjectClassDefinition objectClassDefinition = subjectCtx.getObjectClassDefinition(); Collection<RefinedAssociationDefinition> entitlementAssociationDefs = objectClassDefinition.getEntitlementAssociationDefinitions(); if (entitlementAssociationDefs != null) { ResourceAttributeContainer attributesContainer = ShadowUtil.getAttributesContainer(resourceObject); RefinedResourceSchema refinedSchema = RefinedResourceSchemaImpl.getRefinedSchema(resourceType); PrismContainerDefinition<ShadowAssociationType> associationDef = resourceObject.getDefinition().findContainerDefinition(ShadowType.F_ASSOCIATION); PrismContainer<ShadowAssociationType> associationContainer = associationDef.instantiate(); for (RefinedAssociationDefinition assocDefType: entitlementAssociationDefs) { for (String intent: assocDefType.getIntents()) { LOGGER.trace("Resolving association {} for intent {}", assocDefType.getName(), intent); ProvisioningContext entitlementCtx = subjectCtx.spawn(ShadowKindType.ENTITLEMENT, intent); RefinedObjectClassDefinition entitlementDef = entitlementCtx.getObjectClassDefinition(); if (entitlementDef == null) { throw new SchemaException("No definition for entitlement intent(s) '"+assocDefType.getIntents()+"' in "+resourceType); } ResourceObjectAssociationDirectionType direction = assocDefType.getResourceObjectAssociationType().getDirection(); if (direction == ResourceObjectAssociationDirectionType.SUBJECT_TO_OBJECT) { postProcessEntitlementSubjectToEntitlement(resourceType, resourceObject, objectClassDefinition, assocDefType, entitlementDef, attributesContainer, associationContainer, parentResult); } else if (direction == ResourceObjectAssociationDirectionType.OBJECT_TO_SUBJECT) { if (assocDefType.getResourceObjectAssociationType().getShortcutAssociationAttribute() != null) { postProcessEntitlementSubjectToEntitlement(resourceType, resourceObject, objectClassDefinition, assocDefType, entitlementDef, attributesContainer, associationContainer, assocDefType.getResourceObjectAssociationType().getShortcutAssociationAttribute(), assocDefType.getResourceObjectAssociationType().getShortcutValueAttribute(), parentResult); } else { postProcessEntitlementEntitlementToSubject(subjectCtx, resourceObject, assocDefType, entitlementCtx, attributesContainer, associationContainer, parentResult); } } else { throw new IllegalArgumentException("Unknown entitlement direction "+direction+" in association "+assocDefType+" in "+resourceType); } } } if (!associationContainer.isEmpty()) { resourceObject.add(associationContainer); } } } private <S extends ShadowType,T> void postProcessEntitlementSubjectToEntitlement(ResourceType resourceType, PrismObject<S> resourceObject, RefinedObjectClassDefinition objectClassDefinition, RefinedAssociationDefinition assocDefType, RefinedObjectClassDefinition entitlementDef, ResourceAttributeContainer attributesContainer, PrismContainer<ShadowAssociationType> associationContainer, OperationResult parentResult) throws SchemaException { QName assocAttrName = assocDefType.getResourceObjectAssociationType().getAssociationAttribute(); QName valueAttrName = assocDefType.getResourceObjectAssociationType().getValueAttribute(); postProcessEntitlementSubjectToEntitlement(resourceType, resourceObject, objectClassDefinition, assocDefType, entitlementDef, attributesContainer, associationContainer, assocAttrName, valueAttrName, parentResult); } private <S extends ShadowType,T> void postProcessEntitlementSubjectToEntitlement(ResourceType resourceType, PrismObject<S> resourceObject, RefinedObjectClassDefinition objectClassDefinition, RefinedAssociationDefinition assocDefType, RefinedObjectClassDefinition entitlementDef, ResourceAttributeContainer attributesContainer, PrismContainer<ShadowAssociationType> associationContainer, QName assocAttrName, QName valueAttrName, OperationResult parentResult) throws SchemaException { QName associationName = assocDefType.getName(); if (associationName == null) { throw new SchemaException("No name in entitlement association "+assocDefType+" in "+resourceType); } if (assocAttrName == null) { throw new SchemaException("No association attribute defined in entitlement association '"+associationName+"' in "+resourceType); } RefinedAttributeDefinition assocAttrDef = objectClassDefinition.findAttributeDefinition(assocAttrName); if (assocAttrDef == null) { throw new SchemaException("Association attribute '"+assocAttrName+"'defined in entitlement association '"+associationName+"' was not found in schema for "+resourceType); } ResourceAttribute<T> assocAttr = attributesContainer.findAttribute(assocAttrName); if (assocAttr == null || assocAttr.isEmpty()) { // Nothing to do. No attribute to base the association on. return; } if (valueAttrName == null) { throw new SchemaException("No value attribute defined in entitlement association '"+associationName+"' in "+resourceType); } RefinedAttributeDefinition valueAttrDef = entitlementDef.findAttributeDefinition(valueAttrName); for (PrismPropertyValue<T> assocAttrPVal : assocAttr.getValues()) { ResourceAttribute<T> valueAttribute = valueAttrDef.instantiate(); valueAttribute.add(assocAttrPVal.clone()); PrismContainerValue<ShadowAssociationType> associationCVal = associationContainer.createNewValue(); associationCVal.asContainerable().setName(associationName); ResourceAttributeContainer identifiersContainer = new ResourceAttributeContainer( ShadowAssociationType.F_IDENTIFIERS, entitlementDef.toResourceAttributeContainerDefinition(), prismContext); associationCVal.add(identifiersContainer); identifiersContainer.add(valueAttribute); LOGGER.trace("Assocciation attribute value resolved to valueAtrribute {} and identifiers container {}", valueAttribute, identifiersContainer); } } private <S extends ShadowType,T> void postProcessEntitlementEntitlementToSubject(ProvisioningContext subjectCtx, final PrismObject<S> resourceObject, RefinedAssociationDefinition assocDefType, final ProvisioningContext entitlementCtx, ResourceAttributeContainer attributesContainer, final PrismContainer<ShadowAssociationType> associationContainer, OperationResult parentResult) throws SchemaException, CommunicationException, ObjectNotFoundException, ConfigurationException, SecurityViolationException { ResourceType resourceType = subjectCtx.getResource(); final QName associationName = assocDefType.getName(); final RefinedObjectClassDefinition entitlementDef = entitlementCtx.getObjectClassDefinition(); if (associationName == null) { throw new SchemaException("No name in entitlement association "+assocDefType+" in "+resourceType); } QName associationAuxiliaryObjectClass = assocDefType.getAuxiliaryObjectClass(); if (associationAuxiliaryObjectClass != null && associationAuxiliaryObjectClass.getNamespaceURI() != null && !associationAuxiliaryObjectClass.getNamespaceURI().equals(ResourceTypeUtil.getResourceNamespace(resourceType))) { LOGGER.warn("Auxiliary object class {} in association {} does not have namespace that matches {}", associationAuxiliaryObjectClass, assocDefType.getName(), resourceType); } if (associationAuxiliaryObjectClass != null && !subjectCtx.getObjectClassDefinition().hasAuxiliaryObjectClass(associationAuxiliaryObjectClass)) { LOGGER.trace("Ignoring association {} because subject does not have auxiliary object class {}, it has {}", associationName, associationAuxiliaryObjectClass, subjectCtx.getObjectClassDefinition().getAuxiliaryObjectClassDefinitions()); return; } QName assocAttrName = assocDefType.getResourceObjectAssociationType().getAssociationAttribute(); if (assocAttrName == null) { throw new SchemaException("No association attribute defined in entitlement association '"+associationName+"' in "+resourceType); } RefinedAttributeDefinition assocAttrDef = entitlementDef.findAttributeDefinition(assocAttrName); if (assocAttrDef == null) { throw new SchemaException("Association attribute '"+assocAttrName+"'defined in entitlement association '"+associationName+"' was not found in schema for "+resourceType); } QName valueAttrName = assocDefType.getResourceObjectAssociationType().getValueAttribute(); if (valueAttrName == null) { throw new SchemaException("No value attribute defined in entitlement association '"+associationName+"' in "+resourceType); } ResourceAttribute<T> valueAttr = attributesContainer.findAttribute(valueAttrName); if (valueAttr == null || valueAttr.isEmpty()) { LOGGER.trace("Ignoring association {} because subject does not have any value in attribute {}", associationName, valueAttrName); return; } if (valueAttr.size() > 1) { throw new SchemaException("Value attribute "+valueAttrName+" has no more than one value; attribute defined in entitlement association '"+associationName+"' in "+resourceType); } ObjectQuery query = createQuery(assocDefType, assocAttrDef, valueAttr); AttributesToReturn attributesToReturn = ProvisioningUtil.createAttributesToReturn(entitlementCtx); SearchHierarchyConstraints searchHierarchyConstraints = null; ResourceObjectReferenceType baseContextRef = entitlementDef.getBaseContext(); if (baseContextRef != null) { // TODO: this should be done once per search. Not in every run of postProcessEntitlementEntitlementToSubject // this has to go outside of this method PrismObject<ShadowType> baseContextShadow = resourceObjectReferenceResolver.resolve(subjectCtx, baseContextRef, null, "base context specification in "+entitlementDef, parentResult); RefinedObjectClassDefinition baseContextObjectClassDefinition = subjectCtx.getRefinedSchema().determineCompositeObjectClassDefinition(baseContextShadow); ResourceObjectIdentification baseContextIdentification = ShadowUtil.getResourceObjectIdentification(baseContextShadow, baseContextObjectClassDefinition); searchHierarchyConstraints = new SearchHierarchyConstraints(baseContextIdentification, null); } ResultHandler<ShadowType> handler = new ResultHandler<ShadowType>() { @Override public boolean handle(PrismObject<ShadowType> entitlementShadow) { PrismContainerValue<ShadowAssociationType> associationCVal = associationContainer.createNewValue(); associationCVal.asContainerable().setName(associationName); Collection<ResourceAttribute<?>> entitlementIdentifiers = ShadowUtil.getAllIdentifiers(entitlementShadow); try { ResourceAttributeContainer identifiersContainer = new ResourceAttributeContainer( ShadowAssociationType.F_IDENTIFIERS, entitlementDef.toResourceAttributeContainerDefinition(), prismContext); associationCVal.add(identifiersContainer); identifiersContainer.getValue().addAll(ResourceAttribute.cloneCollection(entitlementIdentifiers)); // Remember the full shadow in user data. This is used later as an optimization to create the shadow in repo identifiersContainer.setUserData(ResourceObjectConverter.FULL_SHADOW_KEY, entitlementShadow); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Processed entitlement-to-subject association for account {} and entitlement {}", ShadowUtil.getHumanReadableName(resourceObject), ShadowUtil.getHumanReadableName(entitlementShadow)); } } catch (SchemaException e) { throw new TunnelException(e); } return true; } }; ConnectorInstance connector = subjectCtx.getConnector(ReadCapabilityType.class, parentResult); try { if (LOGGER.isTraceEnabled()) { LOGGER.trace("Processed entitlement-to-subject association for account {}: query {}", ShadowUtil.getHumanReadableName(resourceObject), query); } try { connector.search(entitlementDef, query, handler, attributesToReturn, null, searchHierarchyConstraints, subjectCtx, parentResult); } catch (GenericFrameworkException e) { throw new GenericConnectorException("Generic error in the connector " + connector + ". Reason: " + e.getMessage(), e); } } catch (TunnelException e) { throw (SchemaException)e.getCause(); } } // precondition: valueAttr has exactly one value private <TV,TA> ObjectQuery createQuery(RefinedAssociationDefinition assocDefType, RefinedAttributeDefinition<TA> assocAttrDef, ResourceAttribute<TV> valueAttr) throws SchemaException{ MatchingRule<TA> matchingRule = matchingRuleRegistry.getMatchingRule(assocDefType.getResourceObjectAssociationType().getMatchingRule(), assocAttrDef.getTypeName()); PrismPropertyValue<TA> converted = PrismUtil.convertPropertyValue(valueAttr.getValue(0), valueAttr.getDefinition(), assocAttrDef); TA normalizedRealValue = matchingRule.normalize(converted.getValue()); PrismPropertyValue<TA> normalized = new PrismPropertyValue<TA>(normalizedRealValue); LOGGER.trace("Converted entitlement filter value: {} ({}) def={}", normalized, normalized.getValue().getClass(), assocAttrDef); ObjectQuery query = QueryBuilder.queryFor(ShadowType.class, prismContext) .item(new ItemPath(ShadowType.F_ATTRIBUTES, assocAttrDef.getName()), assocAttrDef).eq(normalized) .build(); query.setAllowPartialResults(true); return query; } ////////// // ADD ///////// public void processEntitlementsAdd(ProvisioningContext ctx, PrismObject<ShadowType> shadow) throws SchemaException, ConfigurationException, ObjectNotFoundException, CommunicationException { PrismContainer<ShadowAssociationType> associationContainer = shadow.findContainer(ShadowType.F_ASSOCIATION); if (associationContainer == null || associationContainer.isEmpty()) { return; } Map<QName, PropertyModificationOperation> operationMap = new HashMap<>(); collectEntitlementToAttrsDelta(ctx, operationMap, associationContainer.getValues(), ModificationType.ADD); for (PropertyModificationOperation operation : operationMap.values()) { operation.getPropertyDelta().applyTo(shadow); } } public <T> PrismObject<ShadowType> collectEntitlementsAsObjectOperationInShadowAdd(ProvisioningContext ctx, Map<ResourceObjectDiscriminator, ResourceObjectOperations> roMap, PrismObject<ShadowType> shadow, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, SecurityViolationException, ConfigurationException { PrismContainer<ShadowAssociationType> associationContainer = shadow.findContainer(ShadowType.F_ASSOCIATION); if (associationContainer == null || associationContainer.isEmpty()) { return shadow; } return collectEntitlementsAsObjectOperation(ctx, roMap, associationContainer.getValues(), null, shadow, ModificationType.ADD, result); } ////////// // MODIFY ///////// /** * Collects entitlement changes from the shadow to entitlement section into attribute operations. * NOTE: only collects SUBJECT_TO_ENTITLEMENT entitlement direction. */ public void collectEntitlementChange(ProvisioningContext ctx, ContainerDelta<ShadowAssociationType> itemDelta, Collection<Operation> operations) throws SchemaException, ConfigurationException, ObjectNotFoundException, CommunicationException { Map<QName, PropertyModificationOperation> operationsMap = new HashMap<QName, PropertyModificationOperation>(); collectEntitlementToAttrsDelta(ctx, operationsMap, itemDelta.getValuesToAdd(), ModificationType.ADD); collectEntitlementToAttrsDelta(ctx, operationsMap, itemDelta.getValuesToDelete(), ModificationType.DELETE); collectEntitlementToAttrsDelta(ctx, operationsMap, itemDelta.getValuesToReplace(), ModificationType.REPLACE); operations.addAll(operationsMap.values()); } public <T> PrismObject<ShadowType> collectEntitlementsAsObjectOperation(ProvisioningContext ctx, Map<ResourceObjectDiscriminator, ResourceObjectOperations> roMap, ContainerDelta<ShadowAssociationType> containerDelta, PrismObject<ShadowType> subjectShadowBefore, PrismObject<ShadowType> subjectShadowAfter, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, SecurityViolationException, ConfigurationException { subjectShadowAfter = collectEntitlementsAsObjectOperation(ctx, roMap, containerDelta.getValuesToAdd(), subjectShadowBefore, subjectShadowAfter, ModificationType.ADD, result); subjectShadowAfter = collectEntitlementsAsObjectOperation(ctx, roMap, containerDelta.getValuesToDelete(), subjectShadowBefore, subjectShadowAfter, ModificationType.DELETE, result); subjectShadowAfter = collectEntitlementsAsObjectOperation(ctx, roMap, containerDelta.getValuesToReplace(), subjectShadowBefore, subjectShadowAfter, ModificationType.REPLACE, result); return subjectShadowAfter; } ///////// // DELETE ///////// /** * This is somehow different that all the other methods. We are not following the content of a shadow or delta. We are following * the definitions. This is to avoid the need to read the object that is going to be deleted. In fact, the object should not be there * any more, but we still want to clean up entitlement membership based on the information from the shadow. */ public <T> void collectEntitlementsAsObjectOperationDelete(ProvisioningContext subjectCtx, final Map<ResourceObjectDiscriminator, ResourceObjectOperations> roMap, PrismObject<ShadowType> subjectShadow, OperationResult parentResult) throws SchemaException, CommunicationException, ObjectNotFoundException, ConfigurationException, SecurityViolationException { Collection<RefinedAssociationDefinition> entitlementAssociationDefs = subjectCtx.getObjectClassDefinition().getEntitlementAssociationDefinitions(); if (entitlementAssociationDefs == null || entitlementAssociationDefs.isEmpty()) { // Nothing to do LOGGER.trace("No associations in deleted shadow"); return; } ResourceAttributeContainer subjectAttributesContainer = ShadowUtil.getAttributesContainer(subjectShadow); for (final RefinedAssociationDefinition assocDefType: subjectCtx.getObjectClassDefinition().getEntitlementAssociationDefinitions()) { if (assocDefType.getResourceObjectAssociationType().getDirection() != ResourceObjectAssociationDirectionType.OBJECT_TO_SUBJECT) { // We can ignore these. They will die together with the object. No need to explicitly delete them. LOGGER.trace("Ignoring subject-to-object association in deleted shadow"); continue; } if (!assocDefType.requiresExplicitReferentialIntegrity()) { // Referential integrity not required for this one LOGGER.trace("Ignoring association in deleted shadow because it does not require explicit referential integrity assurance"); continue; } if (assocDefType.getAuxiliaryObjectClass() != null && !subjectCtx.getObjectClassDefinition().hasAuxiliaryObjectClass(assocDefType.getAuxiliaryObjectClass())) { LOGGER.trace("Ignoring association in deleted shadow because subject does not have {} auxiliary object class", assocDefType.getAuxiliaryObjectClass()); continue; } QName associationName = assocDefType.getName(); if (associationName == null) { throw new SchemaException("No name in entitlement association "+assocDefType+" in "+subjectCtx.getResource()); } for (String intent: assocDefType.getIntents()) { final ProvisioningContext entitlementCtx = subjectCtx.spawn(ShadowKindType.ENTITLEMENT, intent); final RefinedObjectClassDefinition entitlementOcDef = entitlementCtx.getObjectClassDefinition(); if (entitlementOcDef == null) { throw new SchemaException("No definition for entitlement intent(s) '"+assocDefType.getIntents()+"' defined in entitlement association "+associationName+" in "+subjectCtx.getResource()); } final QName assocAttrName = assocDefType.getResourceObjectAssociationType().getAssociationAttribute(); if (assocAttrName == null) { throw new SchemaException("No association attribute defined in entitlement association '"+associationName+"' in "+subjectCtx.getResource()); } final RefinedAttributeDefinition assocAttrDef = entitlementOcDef.findAttributeDefinition(assocAttrName); if (assocAttrDef == null) { throw new SchemaException("Association attribute '"+assocAttrName+"'defined in entitlement association '"+associationName+"' was not found in schema for "+subjectCtx.getResource()); } QName valueAttrName = assocDefType.getResourceObjectAssociationType().getValueAttribute(); if (valueAttrName == null) { throw new SchemaException("No value attribute defined in entitlement association '"+associationName+"' in "+subjectCtx.getResource()); } final ResourceAttribute<T> valueAttr = subjectAttributesContainer.findAttribute(valueAttrName); if (valueAttr == null || valueAttr.isEmpty()) { // We really want to throw the exception here. We cannot ignore this. If we ignore it then there may be // entitlement membership value left undeleted and this situation will go undetected. // Although we cannot really remedy the situation now, we at least throw an error so the problem is detected. throw new SchemaException("Value attribute "+valueAttrName+" has no value; attribute defined in entitlement association '"+associationName+"' in "+subjectCtx.getResource()); } if (valueAttr.size() > 1) { throw new SchemaException("Value attribute "+valueAttrName+" has no more than one value; attribute defined in entitlement association '"+associationName+"' in "+subjectCtx.getResource()); } ObjectQuery query = createQuery(assocDefType, assocAttrDef, valueAttr); AttributesToReturn attributesToReturn = ProvisioningUtil.createAttributesToReturn(entitlementCtx); SearchHierarchyConstraints searchHierarchyConstraints = null; ResourceObjectReferenceType baseContextRef = entitlementOcDef.getBaseContext(); if (baseContextRef != null) { PrismObject<ShadowType> baseContextShadow = resourceObjectReferenceResolver.resolve(subjectCtx, baseContextRef, null, "base context specification in "+entitlementOcDef, parentResult); RefinedObjectClassDefinition baseContextObjectClassDefinition = subjectCtx.getRefinedSchema().determineCompositeObjectClassDefinition(baseContextShadow); ResourceObjectIdentification baseContextIdentification = ShadowUtil.getResourceObjectIdentification(baseContextShadow, baseContextObjectClassDefinition); searchHierarchyConstraints = new SearchHierarchyConstraints(baseContextIdentification, null); } ResultHandler<ShadowType> handler = new ResultHandler<ShadowType>() { @Override public boolean handle(PrismObject<ShadowType> entitlementShadow) { Collection<? extends ResourceAttribute<?>> primaryIdentifiers = ShadowUtil.getPrimaryIdentifiers(entitlementShadow); ResourceObjectDiscriminator disc = new ResourceObjectDiscriminator(entitlementOcDef.getTypeName(), primaryIdentifiers); ResourceObjectOperations operations = roMap.get(disc); if (operations == null) { operations = new ResourceObjectOperations(); roMap.put(disc, operations); operations.setResourceObjectContext(entitlementCtx); Collection<? extends ResourceAttribute<?>> allIdentifiers = ShadowUtil.getAllIdentifiers(entitlementShadow); operations.setAllIdentifiers(allIdentifiers); } PropertyDelta<T> attributeDelta = null; for(Operation operation: operations.getOperations()) { if (operation instanceof PropertyModificationOperation) { PropertyModificationOperation propOp = (PropertyModificationOperation)operation; if (propOp.getPropertyDelta().getElementName().equals(assocAttrName)) { attributeDelta = propOp.getPropertyDelta(); } } } if (attributeDelta == null) { attributeDelta = assocAttrDef.createEmptyDelta(new ItemPath(ShadowType.F_ATTRIBUTES, assocAttrName)); PropertyModificationOperation attributeModification = new PropertyModificationOperation(attributeDelta); attributeModification.setMatchingRuleQName(assocDefType.getMatchingRule()); operations.add(attributeModification); } attributeDelta.addValuesToDelete(valueAttr.getClonedValues()); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Association in deleted shadow delta:\n{}", attributeDelta.debugDump()); } return true; } }; try { LOGGER.trace("Searching for associations in deleted shadow, query: {}", query); ConnectorInstance connector = subjectCtx.getConnector(ReadCapabilityType.class, parentResult); connector.search(entitlementOcDef, query, handler, attributesToReturn, null, searchHierarchyConstraints, subjectCtx, parentResult); } catch (TunnelException e) { throw (SchemaException)e.getCause(); } catch (GenericFrameworkException e) { throw new GenericConnectorException(e.getMessage(), e); } } } } ///////// // common ///////// private <T> void collectEntitlementToAttrsDelta(ProvisioningContext subjectCtx, Map<QName, PropertyModificationOperation> operationMap, Collection<PrismContainerValue<ShadowAssociationType>> set, ModificationType modificationType) throws SchemaException, ConfigurationException, ObjectNotFoundException, CommunicationException { if (set == null) { return; } for (PrismContainerValue<ShadowAssociationType> associationCVal: set) { collectEntitlementToAttrDelta(subjectCtx, operationMap, associationCVal, modificationType); } } /** * Collects entitlement changes from the shadow to entitlement section into attribute operations. * Collects a single value. * NOTE: only collects SUBJECT_TO_ENTITLEMENT entitlement direction. */ private <T> void collectEntitlementToAttrDelta(ProvisioningContext ctx, Map<QName, PropertyModificationOperation> operationMap, PrismContainerValue<ShadowAssociationType> associationCVal, ModificationType modificationType) throws SchemaException, ConfigurationException, ObjectNotFoundException, CommunicationException { RefinedObjectClassDefinition objectClassDefinition = ctx.getObjectClassDefinition(); ShadowAssociationType associationType = associationCVal.asContainerable(); QName associationName = associationType.getName(); if (associationName == null) { throw new SchemaException("No name in entitlement association "+associationCVal); } RefinedAssociationDefinition assocDefType = objectClassDefinition.findEntitlementAssociationDefinition(associationName); if (assocDefType == null) { throw new SchemaException("No entitlement association with name "+associationName+" in schema of "+ctx.getResource()); } ResourceObjectAssociationDirectionType direction = assocDefType.getResourceObjectAssociationType().getDirection(); if (direction != ResourceObjectAssociationDirectionType.SUBJECT_TO_OBJECT) { // Process just this one direction. The other direction means modification of another object and // therefore will be processed later return; } QName assocAttrName = assocDefType.getResourceObjectAssociationType().getAssociationAttribute(); if (assocAttrName == null) { throw new SchemaException("No association attribute definied in entitlement association '"+associationName+"' in "+ctx.getResource()); } RefinedAttributeDefinition assocAttrDef = objectClassDefinition.findAttributeDefinition(assocAttrName); if (assocAttrDef == null) { throw new SchemaException("Association attribute '"+assocAttrName+"'definied in entitlement association '"+associationName+"' was not found in schema for "+ctx.getResource()); } PropertyModificationOperation attributeOperation = operationMap.get(assocAttrName); if (attributeOperation == null) { attributeOperation = new PropertyModificationOperation(assocAttrDef.createEmptyDelta(new ItemPath(ShadowType.F_ATTRIBUTES, assocAttrName))); attributeOperation.setMatchingRuleQName(assocDefType.getMatchingRule()); operationMap.put(assocAttrName, attributeOperation); } QName valueAttrName = assocDefType.getResourceObjectAssociationType().getValueAttribute(); if (valueAttrName == null) { throw new SchemaException("No value attribute defined in entitlement association '"+associationName+"' in "+ctx.getResource()); } ResourceAttributeContainer identifiersContainer = ShadowUtil.getAttributesContainer(associationCVal, ShadowAssociationType.F_IDENTIFIERS); PrismProperty<T> valueAttr = identifiersContainer.findProperty(valueAttrName); if (valueAttr == null) { throw new SchemaException("No value attribute "+valueAttrName+" present in entitlement association '"+associationName+"' in shadow for "+ctx.getResource()); } if (modificationType == ModificationType.ADD) { attributeOperation.getPropertyDelta().addValuesToAdd(valueAttr.getClonedValues()); } else if (modificationType == ModificationType.DELETE) { attributeOperation.getPropertyDelta().addValuesToDelete(valueAttr.getClonedValues()); } else if (modificationType == ModificationType.REPLACE) { // TODO: check if already exists attributeOperation.getPropertyDelta().setValuesToReplace(valueAttr.getClonedValues()); } } private <T> PrismObject<ShadowType> collectEntitlementsAsObjectOperation(ProvisioningContext ctx, Map<ResourceObjectDiscriminator, ResourceObjectOperations> roMap, Collection<PrismContainerValue<ShadowAssociationType>> set, PrismObject<ShadowType> subjectShadowBefore, PrismObject<ShadowType> subjectShadowAfter, ModificationType modificationType, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, SecurityViolationException, ConfigurationException { if (set == null) { return subjectShadowAfter; } for (PrismContainerValue<ShadowAssociationType> associationCVal: set) { subjectShadowAfter = collectEntitlementAsObjectOperation(ctx, roMap, associationCVal, subjectShadowBefore, subjectShadowAfter, modificationType, result); } return subjectShadowAfter; } private <TV,TA> PrismObject<ShadowType> collectEntitlementAsObjectOperation(ProvisioningContext subjectCtx, Map<ResourceObjectDiscriminator, ResourceObjectOperations> roMap, PrismContainerValue<ShadowAssociationType> associationCVal, PrismObject<ShadowType> subjectShadowBefore, PrismObject<ShadowType> subjectShadowAfter, ModificationType modificationType, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, SecurityViolationException, ConfigurationException { ResourceType resource = subjectCtx.getResource(); ShadowAssociationType associationType = associationCVal.asContainerable(); QName associationName = associationType.getName(); if (associationName == null) { throw new SchemaException("No name in entitlement association "+associationCVal); } RefinedAssociationDefinition assocDefType = subjectCtx.getObjectClassDefinition().findEntitlementAssociationDefinition(associationName); if (assocDefType == null) { throw new SchemaException("No entitlement association with name "+assocDefType+" in schema of "+resource); } ResourceObjectAssociationDirectionType direction = assocDefType.getResourceObjectAssociationType().getDirection(); if (direction != ResourceObjectAssociationDirectionType.OBJECT_TO_SUBJECT) { // Process just this one direction. The other direction was processed before return subjectShadowAfter; } Collection<String> entitlementIntents = assocDefType.getIntents(); if (entitlementIntents == null || entitlementIntents.isEmpty()) { throw new SchemaException("No entitlement intent specified in association "+associationCVal+" in "+resource); } for (String entitlementIntent: entitlementIntents) { ProvisioningContext entitlementCtx = subjectCtx.spawn(ShadowKindType.ENTITLEMENT, entitlementIntent); RefinedObjectClassDefinition entitlementOcDef = entitlementCtx.getObjectClassDefinition(); if (entitlementOcDef == null) { throw new SchemaException("No definition of entitlement intent(s) '"+entitlementIntents+"' specified in association "+associationCVal+" in "+resource); } QName assocAttrName = assocDefType.getResourceObjectAssociationType().getAssociationAttribute(); if (assocAttrName == null) { throw new SchemaException("No association attribute defined in entitlement association in "+resource); } RefinedAttributeDefinition assocAttrDef = entitlementOcDef.findAttributeDefinition(assocAttrName); if (assocAttrDef == null) { throw new SchemaException("Association attribute '"+assocAttrName+"'defined in entitlement association was not found in entitlement intent(s) '"+entitlementIntents+"' in schema for "+resource); } ResourceAttributeContainer identifiersContainer = ShadowUtil.getAttributesContainer(associationCVal, ShadowAssociationType.F_IDENTIFIERS); Collection<ResourceAttribute<?>> entitlementIdentifiersFromAssociation = identifiersContainer.getAttributes(); ResourceObjectDiscriminator disc = new ResourceObjectDiscriminator(entitlementOcDef.getTypeName(), entitlementIdentifiersFromAssociation); ResourceObjectOperations operations = roMap.get(disc); if (operations == null) { operations = new ResourceObjectOperations(); operations.setResourceObjectContext(entitlementCtx); roMap.put(disc, operations); } QName valueAttrName = assocDefType.getResourceObjectAssociationType().getValueAttribute(); if (valueAttrName == null) { throw new SchemaException("No value attribute defined in entitlement association in "+resource); } // Which shadow would we use - shadowBefore or shadowAfter? // // If the operation is ADD or REPLACE, we use current version of the shadow (shadowAfter), because we want // to ensure that we add most-recent data to the subject. // // If the operation is DELETE, we have two possibilities: // - if the resource provides referential integrity, the subject has already // new data (because the object operation was already carried out), so we use shadowAfter // - if the resource does not provide referential integrity, the subject has OLD data // so we use shadowBefore PrismObject<ShadowType> subjectShadow; if (modificationType != ModificationType.DELETE) { subjectShadow = subjectShadowAfter; } else { if (assocDefType.requiresExplicitReferentialIntegrity()) { // we must ensure the referential integrity subjectShadow = subjectShadowBefore; } else { // i.e. resource has ref integrity assured by itself subjectShadow = subjectShadowAfter; } } ResourceAttribute<TV> valueAttr = ShadowUtil.getAttribute(subjectShadow, valueAttrName); if (valueAttr == null) { if (!ShadowUtil.isFullShadow(subjectShadow)) { Collection<ResourceAttribute<?>> subjectIdentifiers = ShadowUtil.getAllIdentifiers(subjectShadow); LOGGER.trace("Fetching {} ({})", subjectShadow, subjectIdentifiers); subjectShadow = resourceObjectReferenceResolver.fetchResourceObject(subjectCtx, subjectIdentifiers, null, result); subjectShadowAfter = subjectShadow; valueAttr = ShadowUtil.getAttribute(subjectShadow, valueAttrName); } if (valueAttr == null) { LOGGER.error("No value attribute {} in shadow\n{}", valueAttrName, subjectShadow.debugDump()); // TODO: check schema and try to fetch full shadow if necessary throw new SchemaException("No value attribute "+valueAttrName+" in " + subjectShadow); } } PropertyDelta<TA> attributeDelta = null; for(Operation operation: operations.getOperations()) { if (operation instanceof PropertyModificationOperation) { PropertyModificationOperation propOp = (PropertyModificationOperation)operation; if (propOp.getPropertyDelta().getElementName().equals(assocAttrName)) { attributeDelta = propOp.getPropertyDelta(); } } } if (attributeDelta == null) { attributeDelta = assocAttrDef.createEmptyDelta(new ItemPath(ShadowType.F_ATTRIBUTES, assocAttrName)); } PrismProperty<TA> changedAssocAttr = PrismUtil.convertProperty(valueAttr, assocAttrDef); if (modificationType == ModificationType.ADD) { attributeDelta.addValuesToAdd(changedAssocAttr.getClonedValues()); } else if (modificationType == ModificationType.DELETE) { attributeDelta.addValuesToDelete(changedAssocAttr.getClonedValues()); } else if (modificationType == ModificationType.REPLACE) { // TODO: check if already exists attributeDelta.setValuesToReplace(changedAssocAttr.getClonedValues()); } if (ResourceTypeUtil.isAvoidDuplicateValues(resource)) { PrismObject<ShadowType> currentObjectShadow = operations.getCurrentShadow(); if (currentObjectShadow == null) { LOGGER.trace("Fetching entitlement shadow {} to avoid value duplication (intent={})", entitlementIdentifiersFromAssociation, entitlementIntent); currentObjectShadow = resourceObjectReferenceResolver.fetchResourceObject(entitlementCtx, entitlementIdentifiersFromAssociation, null, result); operations.setCurrentShadow(currentObjectShadow); } // TODO it seems that duplicate values are checked twice: once here and the second time in ResourceObjectConverter.executeModify // TODO check that and fix if necessary PropertyDelta<TA> attributeDeltaAfterNarrow = ProvisioningUtil.narrowPropertyDelta(attributeDelta, currentObjectShadow, assocDefType.getMatchingRule(), matchingRuleRegistry); if (LOGGER.isTraceEnabled() && (attributeDeltaAfterNarrow == null || attributeDeltaAfterNarrow.isEmpty())) { LOGGER.trace("Not collecting entitlement object operations ({}) association {}: attribute delta is empty after narrow, orig delta: {}", modificationType, associationName.getLocalPart(), attributeDelta); } attributeDelta = attributeDeltaAfterNarrow; } if (attributeDelta != null && !attributeDelta.isEmpty()) { PropertyModificationOperation attributeModification = new PropertyModificationOperation(attributeDelta); attributeModification.setMatchingRuleQName(assocDefType.getMatchingRule()); LOGGER.trace("Collecting entitlement object operations ({}) association {}: {}", modificationType, associationName.getLocalPart(), attributeModification); operations.add(attributeModification); } } return subjectShadowAfter; } }