/** * Copyright (c) 2017 Evolveum * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.evolveum.midpoint.model.impl.lens; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.namespace.QName; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import com.evolveum.midpoint.common.Clock; import com.evolveum.midpoint.model.api.ModelService; import com.evolveum.midpoint.model.api.hooks.HookOperationMode; import com.evolveum.midpoint.model.common.expression.ObjectDeltaObject; import com.evolveum.midpoint.model.impl.controller.ModelUtils; import com.evolveum.midpoint.model.impl.lens.projector.ComplexConstructionConsumer; import com.evolveum.midpoint.model.impl.lens.projector.ConstructionProcessor; import com.evolveum.midpoint.model.impl.lens.projector.ObjectTemplateProcessor; import com.evolveum.midpoint.prism.Objectable; import com.evolveum.midpoint.prism.PrismContext; import com.evolveum.midpoint.prism.PrismObject; import com.evolveum.midpoint.prism.PrismObjectDefinition; import com.evolveum.midpoint.prism.PrismPropertyValue; import com.evolveum.midpoint.prism.PrismReferenceValue; import com.evolveum.midpoint.prism.delta.DeltaMapTriple; import com.evolveum.midpoint.prism.delta.DeltaSetTriple; import com.evolveum.midpoint.prism.delta.ItemDelta; import com.evolveum.midpoint.prism.delta.ObjectDelta; import com.evolveum.midpoint.repo.api.RepositoryService; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.util.FocusTypeUtil; import com.evolveum.midpoint.schema.util.MiscSchemaUtil; import com.evolveum.midpoint.schema.util.ObjectResolver; import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.util.DebugUtil; import com.evolveum.midpoint.util.HumanReadableDescribable; import com.evolveum.midpoint.util.QNameUtil; import com.evolveum.midpoint.util.exception.CommunicationException; import com.evolveum.midpoint.util.exception.ConfigurationException; import com.evolveum.midpoint.util.exception.ExpressionEvaluationException; import com.evolveum.midpoint.util.exception.ObjectAlreadyExistsException; import com.evolveum.midpoint.util.exception.ObjectNotFoundException; import com.evolveum.midpoint.util.exception.PolicyViolationException; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.SecurityViolationException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.FocusType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectReferenceType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectTemplateType; import com.evolveum.midpoint.xml.ns._public.common.common_3.ObjectType; import com.evolveum.midpoint.xml.ns._public.common.common_3.PersonaConstructionType; /** * Runs persona-related changes after the primary operation is all done. Executed directly from clockwork. * * Not entirely clean solution. Ideally, this should be somehow integrated in the clockwork process (nested contexts). * But should be good enough for now. * * @author semancik */ @Component public class PersonaProcessor { private static final Trace LOGGER = TraceManager.getTrace(PersonaProcessor.class); @Autowired(required=true) private ConstructionProcessor constructionProcessor; @Autowired(required=true) private ObjectTemplateProcessor objectTemplateProcessor; @Autowired(required=true) private ObjectResolver objectResolver; @Autowired @Qualifier("cacheRepositoryService") private transient RepositoryService repositoryService; @Autowired(required = true) private ContextFactory contextFactory; @Autowired(required = true) private Clockwork clockwork; @Autowired(required=true) private Clock clock; @Autowired(required=true) private PrismContext prismContext; @SuppressWarnings({ "unchecked", "rawtypes" }) public <O extends ObjectType> HookOperationMode processPersonaChanges(LensContext<O> context, Task task, OperationResult result) throws ObjectAlreadyExistsException, ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException, SecurityViolationException, ExpressionEvaluationException, PolicyViolationException { LensFocusContext<O> focusContext = context.getFocusContext(); if (focusContext == null) { return HookOperationMode.FOREGROUND; } if (!FocusType.class.isAssignableFrom(focusContext.getObjectTypeClass())) { // We can do this only for FocusType. return HookOperationMode.FOREGROUND; } if (focusContext.isDelete()) { // Special case. Simply delete all the existing personas. // TODO: maybe we need to do this before actual focus delete? LOGGER.trace("Focus delete -> delete all personas"); // TODO return HookOperationMode.FOREGROUND; } return processPersonaChangesFocus((LensContext) context, task, result); } public <F extends FocusType> HookOperationMode processPersonaChangesFocus(LensContext<F> context, Task task, OperationResult result) throws ObjectAlreadyExistsException, ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException, SecurityViolationException, ExpressionEvaluationException, PolicyViolationException { DeltaSetTriple<EvaluatedAssignmentImpl<F>> evaluatedAssignmentTriple = (DeltaSetTriple)context.getEvaluatedAssignmentTriple(); if (evaluatedAssignmentTriple == null || evaluatedAssignmentTriple.isEmpty()) { return HookOperationMode.FOREGROUND; } DeltaSetTriple<PersonaKey> activePersonaKeyTriple = new DeltaSetTriple<>(); ComplexConstructionConsumer<PersonaKey,PersonaConstruction<F>> consumer = new ComplexConstructionConsumer<PersonaKey,PersonaConstruction<F>>() { @Override public boolean before(PersonaKey key) { return true; } @Override public void onAssigned(PersonaKey key, String desc) { activePersonaKeyTriple.addToPlusSet(key); } @Override public void onUnchangedValid(PersonaKey key, String desc) { activePersonaKeyTriple.addToZeroSet(key); } @Override public void onUnchangedInvalid(PersonaKey key, String desc) { } @Override public void onUnassigned(PersonaKey key, String desc) { activePersonaKeyTriple.addToMinusSet(key); } @Override public void after(PersonaKey key, String desc, DeltaMapTriple<PersonaKey, ConstructionPack<PersonaConstruction<F>>> constructionMapTriple) { } }; DeltaMapTriple<PersonaKey, ConstructionPack<PersonaConstruction<F>>> constructionMapTriple = constructionProcessor.processConstructions(context, evaluatedAssignmentTriple, evaluatedAssignment -> evaluatedAssignment.getPersonaConstructionTriple(), construction -> new PersonaKey(construction.getConstructionType()), consumer, task, result); LOGGER.trace("activePersonaKeyTriple:\n{}", activePersonaKeyTriple.debugDumpLazily()); List<FocusType> existingPersonas = readExistingPersonas(context, task, result); LOGGER.trace("existingPersonas:\n{}", existingPersonas); for (PersonaKey key: activePersonaKeyTriple.getNonNegativeValues()) { FocusType existingPersona = findPersona(existingPersonas, key); LOGGER.trace("existingPersona: {}", existingPersona); // TODO: add ability to merge several constructions ConstructionPack<PersonaConstruction<F>> pack = constructionMapTriple.getPlusMap().get(key); if (pack == null) { pack = constructionMapTriple.getZeroMap().get(key); } Collection<PrismPropertyValue<PersonaConstruction<F>>> constructions = pack.getConstructions(); if (constructions.isEmpty()) { continue; } if (constructions.size() > 1) { throw new UnsupportedOperationException("Merging of multiple persona constructions is not supported yet"); } PersonaConstruction<F> construction = constructions.iterator().next().getValue(); LOGGER.trace("construction:\n{}", construction.debugDumpLazily()); if (existingPersona == null) { personaAdd(context, key, construction, task, result); } else { personaModify(context, key, construction, existingPersona.asPrismObject(), task, result); } } for (PersonaKey key: activePersonaKeyTriple.getMinusSet()) { FocusType existingPersona = findPersona(existingPersonas, key); if (existingPersona != null) { personaDelete(context, key, existingPersona, task, result); } } return HookOperationMode.FOREGROUND; } public <F extends FocusType> List<FocusType> readExistingPersonas(LensContext<F> context, Task task, OperationResult result) { LensFocusContext<F> focusContext = context.getFocusContext(); PrismObject<F> focus = focusContext.getObjectNew(); List<FocusType> personas = new ArrayList<>(); String desc = "personaRef in "+focus; for (ObjectReferenceType personaRef: focus.asObjectable().getPersonaRef()) { try { FocusType persona = objectResolver.resolve(personaRef, FocusType.class, null, desc, task, result); personas.add(persona); } catch (ObjectNotFoundException | SchemaException e) { LOGGER.warn("Cannot find persona {} referenced from {}", personaRef.getOid(), focus); // But go on... } } return personas; } private FocusType findPersona(List<FocusType> personas, PersonaKey key) { for (FocusType persona: personas) { if (personaMatches(persona, key)) { return persona; } } return null; } private boolean personaMatches(FocusType persona, PersonaKey key) { PrismObject<? extends FocusType> personaObj = persona.asPrismObject(); QName personaType = personaObj.getDefinition().getTypeName(); if (!QNameUtil.match(personaType, key.getType())) { return false; } List<String> objectSubtypes = FocusTypeUtil.determineSubTypes(personaObj); for (String keySubtype: key.getSubtypes()) { if (!objectSubtypes.contains(keySubtype)) { return false; } } return true; } public <F extends FocusType, T extends FocusType> void personaAdd(LensContext<F> context, PersonaKey key, PersonaConstruction<F> construction, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException, PolicyViolationException, ObjectAlreadyExistsException, CommunicationException, ConfigurationException, SecurityViolationException { PrismObject<F> focus = context.getFocusContext().getObjectNew(); LOGGER.debug("Adding persona {} for {} using construction {}", key, focus, construction); PersonaConstructionType constructionType = construction.getConstructionType(); ObjectReferenceType objectMappingRef = constructionType.getObjectMappingRef(); ObjectTemplateType objectMappingType = objectResolver.resolve(objectMappingRef, ObjectTemplateType.class, null, "object mapping in persona construction in "+focus, task, result); QName targetType = constructionType.getTargetType(); PrismObjectDefinition<T> objectDef = prismContext.getSchemaRegistry().findObjectDefinitionByType(targetType); PrismObject<T> target = objectDef.instantiate(); FocusTypeUtil.setSubtype(target, constructionType.getTargetSubtype()); // pretend ADD focusOdo. We need to push all the items through the object template ObjectDeltaObject<F> focusOdo = new ObjectDeltaObject<>(null, focus.createAddDelta(), focus); ObjectDelta<T> targetDelta = target.createAddDelta(); String contextDesc = "object mapping "+objectMappingType+ " for persona construction for "+focus; XMLGregorianCalendar now = clock.currentTimeXMLGregorianCalendar(); Collection<ItemDelta<?, ?>> itemDeltas = objectTemplateProcessor.processObjectMapping(context, objectMappingType, focusOdo, target, targetDelta, contextDesc, now, task, result); if (LOGGER.isTraceEnabled()) { LOGGER.trace("itemDeltas:\n{}", DebugUtil.debugDump(itemDeltas)); } for (ItemDelta itemDelta: itemDeltas) { itemDelta.applyTo(target); } LOGGER.trace("Creating persona:\n{}", target.debugDumpLazily()); executePersonaDelta(targetDelta, task, result); link(context, target.asObjectable(), result); } public <F extends FocusType, T extends FocusType> void personaModify(LensContext<F> context, PersonaKey key, PersonaConstruction<F> construction, PrismObject<T> existingPersona, Task task, OperationResult result) throws ObjectNotFoundException, SchemaException, ExpressionEvaluationException, PolicyViolationException, ObjectAlreadyExistsException, CommunicationException, ConfigurationException, SecurityViolationException { PrismObject<F> focus = context.getFocusContext().getObjectNew(); LOGGER.debug("Modifying persona {} for {} using construction {}", key, focus, construction); PersonaConstructionType constructionType = construction.getConstructionType(); ObjectReferenceType objectMappingRef = constructionType.getObjectMappingRef(); ObjectTemplateType objectMappingType = objectResolver.resolve(objectMappingRef, ObjectTemplateType.class, null, "object mapping in persona construction in "+focus, task, result); ObjectDeltaObject<F> focusOdo = context.getFocusContext().getObjectDeltaObject(); String contextDesc = "object mapping "+objectMappingType+ " for persona construction for "+focus; XMLGregorianCalendar now = clock.currentTimeXMLGregorianCalendar(); Collection<ItemDelta<?, ?>> itemDeltas = objectTemplateProcessor.processObjectMapping(context, objectMappingType, focusOdo, existingPersona, null, contextDesc, now, task, result); if (LOGGER.isTraceEnabled()) { LOGGER.trace("itemDeltas:\n{}", DebugUtil.debugDump(itemDeltas)); } ObjectDelta<T> targetDelta = existingPersona.createModifyDelta(); for (ItemDelta itemDelta: itemDeltas) { targetDelta.addModification(itemDelta); } executePersonaDelta(targetDelta, task, result); } public <F extends FocusType> void personaDelete(LensContext<F> context, PersonaKey key, FocusType existingPersona, Task task, OperationResult result) throws ObjectAlreadyExistsException, ObjectNotFoundException, SchemaException, ExpressionEvaluationException, CommunicationException, ConfigurationException, PolicyViolationException, SecurityViolationException { PrismObject<F> focus = context.getFocusContext().getObjectOld(); LOGGER.debug("Deleting persona {} for {}: ", key, focus, existingPersona); ObjectDelta<? extends FocusType> targetDelta = existingPersona.asPrismObject().createDeleteDelta(); executePersonaDelta(targetDelta, task, result); unlink(context, existingPersona, result); } private <F extends FocusType> void link(LensContext<F> context, FocusType persona, OperationResult result) throws ObjectNotFoundException, SchemaException, ObjectAlreadyExistsException { ObjectDelta<F> delta = context.getFocusContext().getObjectNew().createModifyDelta(); PrismReferenceValue refValue = new PrismReferenceValue(); refValue.setOid(persona.getOid()); refValue.setTargetType(persona.asPrismObject().getDefinition().getTypeName()); delta.addModificationAddReference(FocusType.F_PERSONA_REF, refValue); repositoryService.modifyObject(delta.getObjectTypeClass(), delta.getOid(), delta.getModifications(), result); } private <F extends FocusType> void unlink(LensContext<F> context, FocusType persona, OperationResult result) throws ObjectNotFoundException, SchemaException, ObjectAlreadyExistsException { ObjectDelta<F> delta = context.getFocusContext().getObjectNew().createModifyDelta(); PrismReferenceValue refValue = new PrismReferenceValue(); refValue.setOid(persona.getOid()); refValue.setTargetType(persona.asPrismObject().getDefinition().getTypeName()); delta.addModificationDeleteReference(FocusType.F_PERSONA_REF, refValue); repositoryService.modifyObject(delta.getObjectTypeClass(), delta.getOid(), delta.getModifications(), result); } private <O extends ObjectType> void executePersonaDelta(ObjectDelta<O> delta, Task task, OperationResult result) throws SchemaException, ObjectNotFoundException, CommunicationException, ConfigurationException, PolicyViolationException, ExpressionEvaluationException, ObjectAlreadyExistsException, SecurityViolationException { Collection<ObjectDelta<? extends ObjectType>> deltas = MiscSchemaUtil.createCollection(delta); LensContext<? extends ObjectType> context = contextFactory.createContext(deltas, null, task, result); // Persona changes are all "secondary" changes, trigerred by roles and policies. We do not want to authorize // them as REQUEST. Assignment of the persona role was REQUEST. Changes in persona itself is all EXECUTION. context.setExecutionPhaseOnly(true); clockwork.run(context, task, result); } class PersonaKey implements HumanReadableDescribable { private QName type; private List<String> subtypes; public PersonaKey(PersonaConstructionType constructionType) { super(); this.type = constructionType.getTargetType(); this.subtypes = constructionType.getTargetSubtype(); } public QName getType() { return type; } public List<String> getSubtypes() { return subtypes; } @Override public String toHumanReadableDescription() { return "persona "+type.getLocalPart()+"/"+subtypes+"'"; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + getOuterType().hashCode(); result = prime * result + ((subtypes == null) ? 0 : subtypes.hashCode()); result = prime * result + ((type == null) ? 0 : type.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; PersonaKey other = (PersonaKey) obj; if (!getOuterType().equals(other.getOuterType())) return false; if (subtypes == null) { if (other.subtypes != null) return false; } else if (!subtypes.equals(other.subtypes)) return false; if (type == null) { if (other.type != null) return false; } else if (!type.equals(other.type)) return false; return true; } @Override public String toString() { return "PersonaKey(" + type + "/" + subtypes + ")"; } private PersonaProcessor getOuterType() { return PersonaProcessor.this; } } }