com.evolveum.midpoint.model.impl.lens.projector.PolicyRuleProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.evolveum.midpoint.model.impl.lens.projector.PolicyRuleProcessor.java

Source

/**
 * 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.projector;

import java.util.*;
import java.util.stream.Collectors;

import javax.xml.namespace.QName;

import com.evolveum.midpoint.model.api.context.*;
import com.evolveum.midpoint.model.common.expression.ExpressionUtil;
import com.evolveum.midpoint.model.common.expression.ObjectDeltaObject;
import com.evolveum.midpoint.model.common.mapping.Mapping;
import com.evolveum.midpoint.model.common.mapping.MappingFactory;
import com.evolveum.midpoint.model.impl.lens.*;
import com.evolveum.midpoint.prism.*;
import com.evolveum.midpoint.prism.delta.*;
import com.evolveum.midpoint.prism.delta.builder.DeltaBuilder;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.prism.polystring.PolyString;
import com.evolveum.midpoint.schema.constants.ExpressionConstants;
import com.evolveum.midpoint.schema.constants.SchemaConstants;
import com.evolveum.midpoint.schema.util.ObjectTypeUtil;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.util.DOMUtil;
import com.evolveum.midpoint.util.exception.*;
import com.evolveum.midpoint.xml.ns._public.common.common_3.*;
import com.evolveum.prism.xml.ns._public.types_3.ModificationTypeType;
import org.apache.commons.collections4.CollectionUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import com.evolveum.midpoint.prism.query.ObjectQuery;
import com.evolveum.midpoint.prism.query.builder.QueryBuilder;
import com.evolveum.midpoint.prism.query.builder.S_AtomicFilterExit;
import com.evolveum.midpoint.prism.xml.XsdTypeMapper;
import com.evolveum.midpoint.repo.api.RepositoryService;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.util.MiscSchemaUtil;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;

/**
 * @author semancik
 *
 */
@Component
public class PolicyRuleProcessor {

    private static final Trace LOGGER = TraceManager.getTrace(PolicyRuleProcessor.class);

    @Autowired
    private PrismContext prismContext;

    @Autowired
    @Qualifier("cacheRepositoryService")
    private RepositoryService repositoryService;

    @Autowired
    private MappingFactory mappingFactory;

    @Autowired
    private MappingEvaluator mappingEvaluator;

    private static final QName CONDITION_OUTPUT_NAME = new QName(SchemaConstants.NS_C, "condition");

    /**
     * Evaluate the policies (policy rules, but also the legacy policies). Trigger the rules.
     * But do not enforce anything and do not make any context changes.
     */
    public <F extends FocusType> void processPolicies(LensContext<F> context,
            DeltaSetTriple<EvaluatedAssignmentImpl<F>> evaluatedAssignmentTriple, OperationResult result)
            throws PolicyViolationException, SchemaException {
        checkAssignmentRules(context, evaluatedAssignmentTriple, result);
        checkExclusionsLegacy(context, evaluatedAssignmentTriple.getPlusSet(),
                evaluatedAssignmentTriple.getNonNegativeValues());
        // in policy based situations, the comparison is not symmetric
        checkExclusionsRuleBased(context, evaluatedAssignmentTriple.getPlusSet(),
                evaluatedAssignmentTriple.getPlusSet());
        checkExclusionsRuleBased(context, evaluatedAssignmentTriple.getPlusSet(),
                evaluatedAssignmentTriple.getZeroSet());
        checkExclusionsRuleBased(context, evaluatedAssignmentTriple.getZeroSet(),
                evaluatedAssignmentTriple.getPlusSet());
        checkExclusionsRuleBased(context, evaluatedAssignmentTriple.getZeroSet(),
                evaluatedAssignmentTriple.getZeroSet());
        checkAssigneeConstraints(context, evaluatedAssignmentTriple, result);
        checkSecondaryConstraints(context, evaluatedAssignmentTriple, result);
    }

    private <F extends FocusType> void checkAssignmentRules(LensContext<F> context,
            DeltaSetTriple<EvaluatedAssignmentImpl<F>> evaluatedAssignmentTriple, OperationResult result)
            throws PolicyViolationException, SchemaException {
        checkAssignmentRules(context, evaluatedAssignmentTriple.getPlusSet(), PlusMinusZero.PLUS, result);
        checkAssignmentRules(context, evaluatedAssignmentTriple.getMinusSet(), PlusMinusZero.MINUS, result);
    }

    private <F extends FocusType> void checkAssignmentRules(LensContext<F> context,
            Collection<EvaluatedAssignmentImpl<F>> evaluatedAssignmentSet, PlusMinusZero whichSet,
            OperationResult result) throws PolicyViolationException, SchemaException {
        for (EvaluatedAssignmentImpl<F> evaluatedAssignment : evaluatedAssignmentSet) {
            Collection<EvaluatedPolicyRule> policyRules = evaluatedAssignment.getThisTargetPolicyRules();
            for (EvaluatedPolicyRule policyRule : policyRules) {
                PolicyConstraintsType policyConstraints = policyRule.getPolicyConstraints();
                if (policyConstraints == null) {
                    continue;
                }
                for (AssignmentPolicyConstraintType assignmentConstraint : policyConstraints.getAssignment()) {
                    if (matchesOperation(assignmentConstraint, whichSet)) {
                        List<QName> relationsToCheck = assignmentConstraint.getRelation().isEmpty()
                                ? Collections.singletonList(null)
                                : assignmentConstraint.getRelation();
                        for (QName constraintRelation : relationsToCheck) {
                            if (MiscSchemaUtil.compareRelation(constraintRelation,
                                    evaluatedAssignment.getRelation())) {
                                EvaluatedPolicyRuleTrigger trigger = new EvaluatedPolicyRuleTrigger<>(
                                        PolicyConstraintKindType.ASSIGNMENT, assignmentConstraint,
                                        "Assignment of " + evaluatedAssignment.getTarget());
                                evaluatedAssignment.triggerConstraint(policyRule, trigger);
                            }
                        }
                    }
                }
            }
        }
    }

    private boolean matchesOperation(AssignmentPolicyConstraintType constraint, PlusMinusZero whichSet) {
        List<ModificationTypeType> operations = constraint.getOperation();
        if (operations.isEmpty()) {
            return true;
        }
        switch (whichSet) {
        case PLUS:
            return operations.contains(ModificationTypeType.ADD);
        case MINUS:
            return operations.contains(ModificationTypeType.DELETE);
        case ZERO:
            return operations.contains(ModificationTypeType.REPLACE);
        default:
            throw new IllegalArgumentException("whichSet: " + whichSet);
        }
    }

    private <F extends FocusType> void checkExclusionsLegacy(LensContext<F> context,
            Collection<EvaluatedAssignmentImpl<F>> assignmentsA,
            Collection<EvaluatedAssignmentImpl<F>> assignmentsB) throws PolicyViolationException {
        for (EvaluatedAssignmentImpl<F> assignmentA : assignmentsA) {
            for (EvaluatedAssignmentImpl<F> assignmentB : assignmentsB) {
                if (assignmentA == assignmentB) {
                    continue; // Same thing, this cannot exclude itself
                }
                for (EvaluatedAssignmentTargetImpl eRoleA : assignmentA.getRoles().getAllValues()) {
                    if (eRoleA.appliesToFocus()) {
                        for (EvaluatedAssignmentTargetImpl eRoleB : assignmentB.getRoles().getAllValues()) {
                            if (eRoleB.appliesToFocus()) {
                                checkExclusionLegacy(assignmentA, assignmentB, eRoleA, eRoleB);
                            }
                        }
                    }
                }
            }
        }
    }

    private <F extends FocusType> void checkExclusionLegacy(EvaluatedAssignmentImpl<F> assignmentA,
            EvaluatedAssignmentImpl<F> assignmentB, EvaluatedAssignmentTargetImpl roleA,
            EvaluatedAssignmentTargetImpl roleB) throws PolicyViolationException {
        checkExclusionOneWayLegacy(assignmentA, assignmentB, roleA, roleB);
        checkExclusionOneWayLegacy(assignmentB, assignmentA, roleB, roleA);
    }

    private <F extends FocusType> void checkExclusionOneWayLegacy(EvaluatedAssignmentImpl<F> assignmentA,
            EvaluatedAssignmentImpl<F> assignmentB, EvaluatedAssignmentTargetImpl roleA,
            EvaluatedAssignmentTargetImpl roleB) throws PolicyViolationException {
        for (ExclusionPolicyConstraintType exclusionA : roleA.getExclusions()) {
            checkAndTriggerExclusionConstraintViolationLegacy(assignmentA, assignmentB, roleA, roleB, exclusionA);
        }
    }

    private <F extends FocusType> void checkAndTriggerExclusionConstraintViolationLegacy(
            EvaluatedAssignmentImpl<F> assignmentA, @NotNull EvaluatedAssignmentImpl<F> assignmentB,
            EvaluatedAssignmentTargetImpl roleA, EvaluatedAssignmentTargetImpl roleB,
            ExclusionPolicyConstraintType constraint) throws PolicyViolationException {
        ObjectReferenceType targetRef = constraint.getTargetRef();
        if (roleB.getOid().equals(targetRef.getOid())) {
            EvaluatedExclusionTrigger trigger = new EvaluatedExclusionTrigger(constraint,
                    "Violation of SoD policy: " + roleA.getTarget() + " excludes " + roleB.getTarget()
                            + ", they cannot be assigned at the same time",
                    assignmentB, roleA.getTarget() != null ? roleA.getTarget().asObjectable() : null,
                    roleB.getTarget() != null ? roleB.getTarget().asObjectable() : null, roleA.getAssignmentPath(),
                    roleB.getAssignmentPath());
            assignmentA.triggerConstraint(null, trigger);
        }
    }

    private <F extends FocusType> void checkExclusionsRuleBased(LensContext<F> context,
            Collection<EvaluatedAssignmentImpl<F>> assignmentsA,
            Collection<EvaluatedAssignmentImpl<F>> assignmentsB) throws PolicyViolationException {
        for (EvaluatedAssignmentImpl<F> assignmentA : assignmentsA) {
            checkExclusionsRuleBased(context, assignmentA, assignmentsB);
        }
    }

    private <F extends FocusType> void checkExclusionsRuleBased(LensContext<F> context,
            EvaluatedAssignmentImpl<F> assignmentA, Collection<EvaluatedAssignmentImpl<F>> assignmentsB)
            throws PolicyViolationException {

        // We consider all policy rules, i.e. also from induced targets. (It is not possible to collect local
        // rules for individual targets in the chain - rules are computed only for directly evaluated assignments.)
        for (EvaluatedPolicyRule policyRule : assignmentA.getAllTargetsPolicyRules()) {
            if (policyRule.getPolicyConstraints() == null
                    || policyRule.getPolicyConstraints().getExclusion().isEmpty()) {
                continue;
            }
            // In order to avoid false positives, we consider all targets from the current assignment as "allowed"
            Set<String> allowedTargetOids = assignmentA.getNonNegativeTargets().stream()
                    .filter(t -> t.appliesToFocus()).map(t -> t.getOid()).collect(Collectors.toSet());

            for (EvaluatedAssignmentImpl<F> assignmentB : assignmentsB) {
                for (EvaluatedAssignmentTargetImpl targetB : assignmentB.getNonNegativeTargets()) {
                    if (!targetB.appliesToFocus() || allowedTargetOids.contains(targetB.getOid())) {
                        continue;
                    }
                    for (ExclusionPolicyConstraintType exclusionConstraint : policyRule.getPolicyConstraints()
                            .getExclusion()) {
                        if (excludes(exclusionConstraint, targetB)) {
                            triggerExclusionConstraintViolation(assignmentA, assignmentB, targetB,
                                    exclusionConstraint, policyRule);
                        }
                    }
                }
            }
        }
    }

    private boolean excludes(ExclusionPolicyConstraintType constraint, EvaluatedAssignmentTargetImpl target) {
        if (constraint.getTargetRef() == null || target.getOid() == null) {
            return false; // shouldn't occur
        } else {
            return target.getOid().equals(constraint.getTargetRef().getOid());
        }
        // We could speculate about resolving targets at runtime, but that's inefficient. More appropriate is
        // to specify an expression, and evaluate (already resolved) target with regards to this expression.
    }

    private <F extends FocusType> void triggerExclusionConstraintViolation(EvaluatedAssignmentImpl<F> assignmentA,
            @NotNull EvaluatedAssignmentImpl<F> assignmentB, EvaluatedAssignmentTargetImpl targetB,
            ExclusionPolicyConstraintType constraint, EvaluatedPolicyRule policyRule)
            throws PolicyViolationException {

        AssignmentPath pathA = policyRule.getAssignmentPath();
        AssignmentPath pathB = targetB.getAssignmentPath();
        String infoA = computeAssignmentInfo(pathA, assignmentA.getTarget());
        String infoB = computeAssignmentInfo(pathB, targetB.getTarget());
        ObjectType objectA = getConflictingObject(pathA, assignmentA.getTarget());
        ObjectType objectB = getConflictingObject(pathB, targetB.getTarget());
        EvaluatedExclusionTrigger trigger = new EvaluatedExclusionTrigger(constraint,
                "Violation of SoD policy: " + infoA + " excludes " + infoB
                        + ", they cannot be assigned at the same time",
                assignmentB, objectA, objectB, pathA, pathB);
        assignmentA.triggerConstraint(policyRule, trigger);
    }

    private ObjectType getConflictingObject(AssignmentPath path, PrismObject<?> defaultObject) {
        if (path == null) {
            return ObjectTypeUtil.toObjectable(defaultObject);
        }
        List<ObjectType> objects = path.getFirstOrderChain();
        return objects.isEmpty() ? ObjectTypeUtil.toObjectable(defaultObject) : objects.get(objects.size() - 1);
    }

    private String computeAssignmentInfo(AssignmentPath path, PrismObject<?> defaultObject) {
        if (path == null) {
            return String.valueOf(defaultObject); // shouldn't occur
        }
        List<ObjectType> objects = path.getFirstOrderChain();
        if (objects.isEmpty()) { // shouldn't occur
            return String.valueOf(defaultObject);
        }
        ObjectType last = objects.get(objects.size() - 1);
        StringBuilder sb = new StringBuilder();
        sb.append(last);
        if (objects.size() > 1) {
            sb.append(objects.stream().map(o -> PolyString.getOrig(o.getName()))
                    .collect(Collectors.joining("->", " (", ")")));
        }
        return sb.toString();
    }

    private <F extends FocusType> void checkAssigneeConstraints(LensContext<F> context,
            DeltaSetTriple<EvaluatedAssignmentImpl<F>> evaluatedAssignmentTriple, OperationResult result)
            throws PolicyViolationException, SchemaException {
        for (EvaluatedAssignmentImpl<F> assignment : evaluatedAssignmentTriple.union()) {
            if (evaluatedAssignmentTriple.presentInPlusSet(assignment)) {
                if (!assignment.isPresentInCurrentObject()) {
                    checkAssigneeConstraints(context, assignment, PlusMinusZero.PLUS, result); // only really new assignments
                }
            } else if (evaluatedAssignmentTriple.presentInZeroSet(assignment)) {
                // No need to check anything here. Maintain status quo.
            } else {
                if (assignment.isPresentInCurrentObject()) {
                    checkAssigneeConstraints(context, assignment, PlusMinusZero.MINUS, result); // only assignments that are really deleted
                }
            }
        }
    }

    private <F extends FocusType> void checkAssigneeConstraints(LensContext<F> context,
            EvaluatedAssignment<F> assignment, PlusMinusZero plusMinus, OperationResult result)
            throws PolicyViolationException, SchemaException {
        PrismObject<?> target = assignment.getTarget();
        if (target == null || !(target.asObjectable() instanceof AbstractRoleType)) {
            return;
        }
        AbstractRoleType targetRole = (AbstractRoleType) target.asObjectable();
        QName relation = ObjectTypeUtil.normalizeRelation(assignment.getRelation());
        Collection<EvaluatedPolicyRule> policyRules = assignment.getThisTargetPolicyRules();
        for (EvaluatedPolicyRule policyRule : policyRules) {
            PolicyConstraintsType policyConstraints = policyRule.getPolicyConstraints();
            if (policyConstraints == null) {
                continue;
            }
            List<MultiplicityPolicyConstraintType> relevantMinAssignees = getForRelation(
                    policyConstraints.getMinAssignees(), relation);
            List<MultiplicityPolicyConstraintType> relevantMaxAssignees = getForRelation(
                    policyConstraints.getMaxAssignees(), relation);
            if (relevantMinAssignees.isEmpty() && relevantMaxAssignees.isEmpty()) {
                continue;
            }
            String focusOid = null;
            if (context.getFocusContext() != null) {
                focusOid = context.getFocusContext().getOid();
            }
            int currentAssigneesExceptMyself = getNumberOfAssigneesExceptMyself(targetRole, focusOid, relation,
                    result);
            for (MultiplicityPolicyConstraintType constraint : relevantMinAssignees) {
                Integer requiredMultiplicity = XsdTypeMapper.multiplicityToInteger(constraint.getMultiplicity());
                if (requiredMultiplicity <= 0) {
                    continue; // unbounded or 0
                }
                // Complain only if the situation is getting worse
                if (currentAssigneesExceptMyself < requiredMultiplicity && plusMinus == PlusMinusZero.MINUS) {
                    EvaluatedPolicyRuleTrigger<MultiplicityPolicyConstraintType> trigger = new EvaluatedPolicyRuleTrigger<>(
                            PolicyConstraintKindType.MIN_ASSIGNEES, constraint,
                            target + " requires at least " + requiredMultiplicity
                                    + " assignees with the relation of '" + relation.getLocalPart()
                                    + "'. The operation would result in " + currentAssigneesExceptMyself
                                    + " assignees.");
                    assignment.triggerConstraint(policyRule, trigger);
                }
            }
            for (MultiplicityPolicyConstraintType constraint : relevantMaxAssignees) {
                Integer requiredMultiplicity = XsdTypeMapper.multiplicityToInteger(constraint.getMultiplicity());
                if (requiredMultiplicity < 0) {
                    continue; // unbounded
                }
                // Complain only if the situation is getting worse
                if (currentAssigneesExceptMyself >= requiredMultiplicity && plusMinus == PlusMinusZero.PLUS) {
                    EvaluatedPolicyRuleTrigger<MultiplicityPolicyConstraintType> trigger = new EvaluatedPolicyRuleTrigger<>(
                            PolicyConstraintKindType.MAX_ASSIGNEES, constraint,
                            target + " requires at most " + requiredMultiplicity
                                    + " assignees with the relation of '" + relation.getLocalPart()
                                    + "'. The operation would result in " + (currentAssigneesExceptMyself + 1)
                                    + " assignees.");
                    assignment.triggerConstraint(policyRule, trigger);
                }
            }
        }
    }

    private List<MultiplicityPolicyConstraintType> getForRelation(List<MultiplicityPolicyConstraintType> all,
            QName relation) {
        return all.stream().filter(c -> containsRelation(c, relation)).collect(Collectors.toList());
    }

    private boolean containsRelation(MultiplicityPolicyConstraintType constraint, QName relation) {
        return getConstraintRelations(constraint).stream()
                .anyMatch(constraintRelation -> ObjectTypeUtil.relationMatches(constraintRelation, relation));
    }

    private List<QName> getConstraintRelations(MultiplicityPolicyConstraintType constraint) {
        return !constraint.getRelation().isEmpty() ? constraint.getRelation()
                : Collections.singletonList(SchemaConstants.ORG_DEFAULT);
    }

    /**
     * Returns numbers of assignees with the given relation name.
     */
    private int getNumberOfAssigneesExceptMyself(AbstractRoleType target, String selfOid, QName relation,
            OperationResult result) throws SchemaException {
        S_AtomicFilterExit q = QueryBuilder.queryFor(FocusType.class, prismContext)
                .item(FocusType.F_ASSIGNMENT, AssignmentType.F_TARGET_REF).ref(target.getOid());
        if (selfOid != null) {
            q = q.and().not().id(selfOid);
        }
        ObjectQuery query = q.build();
        List<PrismObject<FocusType>> assignees = repositoryService.searchObjects(FocusType.class, query, null,
                result);
        int count = 0;
        assignee: for (PrismObject<FocusType> assignee : assignees) {
            for (AssignmentType assignment : assignee.asObjectable().getAssignment()) {
                if (assignment.getTargetRef() != null
                        && ObjectTypeUtil.relationsEquivalent(relation, assignment.getTargetRef().getRelation())) {
                    count++;
                    continue assignee;
                }
            }
        }
        return count;
    }

    public <F extends FocusType> boolean processPruning(LensContext<F> context,
            DeltaSetTriple<EvaluatedAssignmentImpl<F>> evaluatedAssignmentTriple, OperationResult result)
            throws PolicyViolationException, SchemaException {
        Collection<EvaluatedAssignmentImpl<F>> plusSet = evaluatedAssignmentTriple.getPlusSet();
        if (plusSet == null) {
            return false;
        }
        boolean needToReevaluateAssignments = false;
        for (EvaluatedAssignmentImpl<F> plusAssignment : plusSet) {
            for (EvaluatedPolicyRule targetPolicyRule : plusAssignment.getAllTargetsPolicyRules()) {
                for (EvaluatedPolicyRuleTrigger trigger : targetPolicyRule.getTriggers()) {
                    if (!(trigger instanceof EvaluatedExclusionTrigger)) {
                        continue;
                    }
                    EvaluatedExclusionTrigger exclTrigger = (EvaluatedExclusionTrigger) trigger;
                    PolicyActionsType actions = targetPolicyRule.getActions();
                    if (actions == null || actions.getPrune() == null) {
                        continue;
                    }
                    EvaluatedAssignment<FocusType> conflictingAssignment = exclTrigger.getConflictingAssignment();
                    if (conflictingAssignment == null) {
                        throw new SystemException("Added assignment " + plusAssignment
                                + ", the exclusion prune rule was triggered but there is no conflicting assignment in the trigger");
                    }
                    LOGGER.debug("Pruning assignment {} because it conflicts with added assignment {}",
                            conflictingAssignment, plusAssignment);

                    PrismContainerValue<AssignmentType> assignmentValueToRemove = conflictingAssignment
                            .getAssignmentType().asPrismContainerValue().clone();
                    PrismObjectDefinition<F> focusDef = context.getFocusContext().getObjectDefinition();
                    ContainerDelta<AssignmentType> assignmentDelta = ContainerDelta
                            .createDelta(FocusType.F_ASSIGNMENT, focusDef);
                    assignmentDelta.addValuesToDelete(assignmentValueToRemove);
                    context.getFocusContext().swallowToSecondaryDelta(assignmentDelta);

                    needToReevaluateAssignments = true;
                }
            }
        }

        return needToReevaluateAssignments;
    }

    private <F extends FocusType> void checkSecondaryConstraints(LensContext<F> context,
            DeltaSetTriple<EvaluatedAssignmentImpl<F>> evaluatedAssignmentTriple, OperationResult result)
            throws SchemaException, PolicyViolationException {
        checkSecondaryConstraints(context, evaluatedAssignmentTriple.getPlusSet(), result);
        checkSecondaryConstraints(context, evaluatedAssignmentTriple.getZeroSet(), result);
        checkSecondaryConstraints(context, evaluatedAssignmentTriple.getMinusSet(), result);
    }

    private <F extends FocusType> void checkSecondaryConstraints(LensContext<F> context,
            Collection<EvaluatedAssignmentImpl<F>> evaluatedAssignmentSet, OperationResult result)
            throws PolicyViolationException, SchemaException {
        for (EvaluatedAssignmentImpl<F> evaluatedAssignment : evaluatedAssignmentSet) {
            checkSecondaryConstraints(evaluatedAssignment, result);
        }
    }

    private <F extends FocusType> void checkSecondaryConstraints(EvaluatedAssignmentImpl<F> evaluatedAssignment,
            OperationResult result) throws PolicyViolationException, SchemaException {

        // Single pass only (for the time being)
        // We consider only directly attached "situation" policy rules. In the future, we might configure this.
        // So, if someone wants to report (forward) triggers from a target, he must ensure that a particular
        // "situation" constraint is present directly on it.
        for (EvaluatedPolicyRule policyRule : evaluatedAssignment.getThisTargetPolicyRules()) {
            if (policyRule.getPolicyConstraints() == null) {
                continue;
            }
            for (PolicySituationPolicyConstraintType situationConstraint : policyRule.getPolicyConstraints()
                    .getSituation()) {
                Collection<EvaluatedPolicyRule> sourceRules = selectTriggeredRules(evaluatedAssignment,
                        situationConstraint.getSituation());
                if (sourceRules.isEmpty()) {
                    continue;
                }
                String message = sourceRules.stream()
                        .flatMap(r -> r.getTriggers().stream().map(EvaluatedPolicyRuleTrigger::getMessage))
                        .distinct().collect(Collectors.joining("; "));
                EvaluatedSituationTrigger trigger = new EvaluatedSituationTrigger(situationConstraint, message,
                        sourceRules);
                evaluatedAssignment.triggerConstraint(policyRule, trigger);
            }
        }
    }

    private <F extends FocusType> Collection<EvaluatedPolicyRule> selectTriggeredRules(
            EvaluatedAssignmentImpl<F> evaluatedAssignment, List<String> situations) {
        // We consider all rules here, i.e. also those that are triggered on targets induced by this one.
        // Decision whether to trigger such rules lies on "primary" policy constraints. (E.g. approvals would
        // not trigger, whereas exclusions probably would.) Overall, our responsibility is simply to collect
        // all triggered rules.
        return evaluatedAssignment.getAllTargetsPolicyRules().stream()
                .filter(r -> !r.getTriggers().isEmpty() && situations.contains(r.getPolicySituation()))
                .collect(Collectors.toList());
    }

    @NotNull
    private <F extends FocusType> List<ItemDelta<?, ?>> getAssignmentModificationDelta(
            EvaluatedAssignmentImpl<F> evaluatedAssignment, List<EvaluatedPolicyRuleTriggerType> triggers)
            throws SchemaException {
        Long id = evaluatedAssignment.getAssignmentType().getId();
        if (id == null) {
            throw new IllegalArgumentException("Assignment with no ID: " + evaluatedAssignment);
        }
        List<ItemDelta<?, ?>> deltas = new ArrayList<>();
        Set<String> currentSituations = new HashSet<>(evaluatedAssignment.getAssignmentType().getPolicySituation());
        Set<String> newSituations = new HashSet<>(evaluatedAssignment.getPolicySituations());
        CollectionUtils.addIgnoreNull(deltas,
                createSituationDelta(new ItemPath(FocusType.F_ASSIGNMENT, id, AssignmentType.F_POLICY_SITUATION),
                        currentSituations, newSituations));
        Set<EvaluatedPolicyRuleTriggerType> currentTriggers = new HashSet<>(
                evaluatedAssignment.getAssignmentType().getTrigger());
        Set<EvaluatedPolicyRuleTriggerType> newTriggers = new HashSet<>(triggers);
        CollectionUtils.addIgnoreNull(deltas, createTriggerDelta(
                new ItemPath(FocusType.F_ASSIGNMENT, id, AssignmentType.F_TRIGGER), currentTriggers, newTriggers));
        return deltas;
    }

    private <F extends FocusType> boolean shouldSituationBeUpdated(EvaluatedAssignment<F> evaluatedAssignment,
            List<EvaluatedPolicyRuleTriggerType> triggers) {
        Set<String> currentSituations = new HashSet<>(evaluatedAssignment.getAssignmentType().getPolicySituation());
        Set<EvaluatedPolicyRuleTriggerType> currentTriggers = new HashSet<>(
                evaluatedAssignment.getAssignmentType().getTrigger());
        // if the current situations different from the ones in the old assignment => update
        // (provided that the situations in the assignment were _not_ changed directly via a delta!!!) TODO check this
        if (!currentSituations.equals(new HashSet<>(evaluatedAssignment.getPolicySituations()))) {
            LOGGER.trace("computed policy situations are different from the current ones");
            return true;
        }
        if (!currentTriggers.equals(new HashSet<>(triggers))) {
            LOGGER.trace("computed policy rules triggers are different from the current ones");
            return true;
        }
        return false;
    }

    public <F extends FocusType> void storeFocusPolicySituation(LensContext<F> context, Task task,
            OperationResult result) throws SchemaException {
        LensFocusContext<F> focusContext = context.getFocusContext();
        if (focusContext == null) {
            return;
        }
        Set<String> currentSituations = focusContext.getObjectCurrent() != null
                ? new HashSet<>(focusContext.getObjectCurrent().asObjectable().getPolicySituation())
                : Collections.emptySet();
        Set<String> newSituations = new HashSet<>(focusContext.getPolicySituations());
        PropertyDelta<String> situationsDelta = createSituationDelta(new ItemPath(FocusType.F_POLICY_SITUATION),
                currentSituations, newSituations);
        if (situationsDelta != null) {
            focusContext.swallowToProjectionWaveSecondaryDelta(situationsDelta);
        }
    }

    @Nullable
    private PropertyDelta<String> createSituationDelta(ItemPath path, Set<String> currentSituations,
            Set<String> newSituations) throws SchemaException {
        if (newSituations.equals(currentSituations)) {
            return null;
        }
        @SuppressWarnings({ "unchecked", "raw" })
        PropertyDelta<String> situationsDelta = (PropertyDelta<String>) DeltaBuilder
                .deltaFor(FocusType.class, prismContext).item(path)
                .add(CollectionUtils.subtract(newSituations, currentSituations).toArray())
                .delete(CollectionUtils.subtract(currentSituations, newSituations).toArray()).asItemDelta();
        situationsDelta.setEstimatedOldValues(PrismPropertyValue.wrap(currentSituations));
        return situationsDelta;
    }

    private PropertyDelta<EvaluatedPolicyRuleTriggerType> createTriggerDelta(ItemPath path,
            Set<EvaluatedPolicyRuleTriggerType> currentTriggers, Set<EvaluatedPolicyRuleTriggerType> newTriggers)
            throws SchemaException {
        if (newTriggers.equals(currentTriggers)) {
            return null;
        }
        @SuppressWarnings({ "unchecked", "raw" })
        PropertyDelta<EvaluatedPolicyRuleTriggerType> triggersDelta = (PropertyDelta<EvaluatedPolicyRuleTriggerType>) DeltaBuilder
                .deltaFor(FocusType.class, prismContext).item(path).replace(newTriggers.toArray()) // TODO or add + delete?
                .asItemDelta();
        triggersDelta.setEstimatedOldValues(PrismPropertyValue.wrap(currentTriggers));
        return triggersDelta;
    }

    public <F extends FocusType> void addGlobalPoliciesToAssignments(LensContext<F> context,
            DeltaSetTriple<EvaluatedAssignmentImpl<F>> evaluatedAssignmentTriple, Task task, OperationResult result)
            throws SchemaException, ExpressionEvaluationException, ObjectNotFoundException {

        PrismObject<SystemConfigurationType> systemConfiguration = context.getSystemConfiguration();
        if (systemConfiguration == null) {
            return;
        }
        // We need to consider object before modification here.
        LensFocusContext<F> focusContext = context.getFocusContext();
        PrismObject<F> focus = focusContext.getObjectCurrent();
        if (focus == null) {
            focus = focusContext.getObjectNew(); // only if it does not exist, let's try the new one
        }

        for (GlobalPolicyRuleType globalPolicyRule : systemConfiguration.asObjectable().getGlobalPolicyRule()) {
            ObjectSelectorType focusSelector = globalPolicyRule.getFocusSelector();
            if (!repositoryService.selectorMatches(focusSelector, focus, LOGGER,
                    "Global policy rule " + globalPolicyRule.getName() + " focus selector: ")) {
                continue;
            }
            for (EvaluatedAssignmentImpl<F> evaluatedAssignment : evaluatedAssignmentTriple.getAllValues()) {
                for (EvaluatedAssignmentTargetImpl target : evaluatedAssignment.getRoles().getNonNegativeValues()) {
                    if (!repositoryService.selectorMatches(globalPolicyRule.getTargetSelector(), target.getTarget(),
                            LOGGER, "Global policy rule " + globalPolicyRule.getName() + " target selector: ")) {
                        continue;
                    }
                    if (!isRuleConditionTrue(globalPolicyRule, focus, evaluatedAssignment, context, task, result)) {
                        LOGGER.trace("Skipping global policy rule because the condition evaluated to false: {}",
                                globalPolicyRule);
                        continue;
                    }
                    EvaluatedPolicyRule evaluatedRule = new EvaluatedPolicyRuleImpl(globalPolicyRule,
                            target.getAssignmentPath() != null ? target.getAssignmentPath().clone() : null);
                    if (target.getAssignmentPath() != null && target.getAssignmentPath().size() == 1) {
                        evaluatedAssignment.addThisTargetPolicyRule(evaluatedRule);
                    } else {
                        evaluatedAssignment.addOtherTargetPolicyRule(evaluatedRule);
                    }
                }
            }
        }
    }

    private <F extends FocusType> boolean isRuleConditionTrue(GlobalPolicyRuleType globalPolicyRule,
            PrismObject<F> focus, EvaluatedAssignmentImpl<F> evaluatedAssignment, LensContext<F> context, Task task,
            OperationResult result) throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException {
        MappingType condition = globalPolicyRule.getCondition();
        if (condition == null) {
            return true;
        }

        Mapping.Builder<PrismPropertyValue<Boolean>, PrismPropertyDefinition<Boolean>> builder = mappingFactory
                .createMappingBuilder();
        ObjectDeltaObject<F> focusOdo = new ObjectDeltaObject<>(focus, null, focus);
        builder = builder.mappingType(condition)
                .contextDescription("condition in global policy rule " + globalPolicyRule.getName())
                .sourceContext(focusOdo)
                .defaultTargetDefinition(
                        new PrismPropertyDefinitionImpl<>(CONDITION_OUTPUT_NAME, DOMUtil.XSD_BOOLEAN, prismContext))
                .addVariableDefinition(ExpressionConstants.VAR_USER, focusOdo)
                .addVariableDefinition(ExpressionConstants.VAR_FOCUS, focusOdo)
                .addVariableDefinition(ExpressionConstants.VAR_TARGET, evaluatedAssignment.getTarget())
                .addVariableDefinition(ExpressionConstants.VAR_ASSIGNMENT, evaluatedAssignment) // TODO: ok?
                .rootNode(focusOdo);

        Mapping<PrismPropertyValue<Boolean>, PrismPropertyDefinition<Boolean>> mapping = builder.build();

        mappingEvaluator.evaluateMapping(mapping, context, task, result);

        PrismValueDeltaSetTriple<PrismPropertyValue<Boolean>> conditionTriple = mapping.getOutputTriple();
        return conditionTriple != null
                && ExpressionUtil.computeConditionResult(conditionTriple.getNonNegativeValues()); // TODO: null -> true (in the method) - ok?
    }

    public <F extends FocusType, T extends FocusType> void applyAssignmentSituationOnAdd(LensContext<F> context,
            PrismObject<T> objectToAdd) throws SchemaException {
        if (context.getEvaluatedAssignmentTriple() == null) {
            return;
        }
        T focus = objectToAdd.asObjectable();
        for (EvaluatedAssignmentImpl<?> evaluatedAssignment : context.getEvaluatedAssignmentTriple()
                .getNonNegativeValues()) {
            LOGGER.trace("Applying assignment situation on object ADD for {}", evaluatedAssignment);
            List<EvaluatedPolicyRuleTriggerType> triggers = getTriggers(evaluatedAssignment);
            if (!shouldSituationBeUpdated(evaluatedAssignment, triggers)) {
                continue;
            }
            AssignmentType assignment = evaluatedAssignment.getAssignmentType();
            if (assignment.getId() != null) {
                ItemDelta.applyTo(getAssignmentModificationDelta(evaluatedAssignment, triggers), objectToAdd);
            } else {
                int i = focus.getAssignment().indexOf(assignment);
                if (i < 0) {
                    throw new IllegalStateException("Assignment to be replaced not found in an object to add: "
                            + assignment + " / " + objectToAdd);
                }
                copyPolicyData(focus.getAssignment().get(i), evaluatedAssignment, triggers);
            }
        }
    }

    private List<EvaluatedPolicyRuleTriggerType> getTriggers(EvaluatedAssignmentImpl<?> evaluatedAssignment) {
        List<EvaluatedPolicyRuleTriggerType> rv = new ArrayList<>();
        for (EvaluatedPolicyRule policyRule : evaluatedAssignment.getAllTargetsPolicyRules()) {
            for (EvaluatedPolicyRuleTrigger<?> trigger : policyRule.getTriggers()) {
                EvaluatedPolicyRuleTriggerType triggerType = trigger.toEvaluatedPolicyRuleTriggerType(policyRule)
                        .clone();
                simplifyTrigger(triggerType);
                rv.add(triggerType);
            }
        }
        return rv;
    }

    private void simplifyTrigger(EvaluatedPolicyRuleTriggerType trigger) {
        deleteAssignments(trigger.getAssignmentPath());
        if (trigger instanceof EvaluatedExclusionTriggerType) {
            EvaluatedExclusionTriggerType exclusionTrigger = (EvaluatedExclusionTriggerType) trigger;
            deleteAssignments(exclusionTrigger.getConflictingObjectPath());
            exclusionTrigger.setConflictingAssignment(null);
        } else if (trigger instanceof EvaluatedSituationTriggerType) {
            for (EvaluatedPolicyRuleType sourceRule : ((EvaluatedSituationTriggerType) trigger).getSourceRule()) {
                for (EvaluatedPolicyRuleTriggerType sourceTrigger : sourceRule.getTrigger()) {
                    simplifyTrigger(sourceTrigger);
                }
            }
        }
    }

    private void deleteAssignments(AssignmentPathType path) {
        if (path == null) {
            return;
        }
        for (AssignmentPathSegmentType segment : path.getSegment()) {
            segment.setAssignment(null);
        }
    }

    public <F extends FocusType, T extends ObjectType> ObjectDelta<T> applyAssignmentSituationOnModify(
            LensContext<F> context, ObjectDelta<T> objectDelta) throws SchemaException {
        if (context.getEvaluatedAssignmentTriple() == null) {
            return objectDelta;
        }
        for (EvaluatedAssignmentImpl<?> evaluatedAssignment : context.getEvaluatedAssignmentTriple()
                .getNonNegativeValues()) {
            LOGGER.trace("Applying assignment situation on object MODIFY for {}", evaluatedAssignment);
            List<EvaluatedPolicyRuleTriggerType> triggers = getTriggers(evaluatedAssignment);
            if (!shouldSituationBeUpdated(evaluatedAssignment, triggers)) {
                continue;
            }
            AssignmentType assignment = evaluatedAssignment.getAssignmentType();
            if (assignment.getId() != null) {
                objectDelta = swallow(context, objectDelta,
                        getAssignmentModificationDelta(evaluatedAssignment, triggers));
            } else {
                if (objectDelta == null) {
                    throw new IllegalStateException("No object delta!");
                }
                ContainerDelta<Containerable> assignmentDelta = objectDelta
                        .findContainerDelta(FocusType.F_ASSIGNMENT);
                if (assignmentDelta == null) {
                    throw new IllegalStateException("Unnumbered assignment (" + assignment
                            + ") couldn't be found in object delta (no assignment modification): " + objectDelta);
                }
                PrismContainerValue<AssignmentType> assignmentInDelta = assignmentDelta
                        .findValueToAddOrReplace(assignment.asPrismContainerValue());
                if (assignmentInDelta == null) {
                    throw new IllegalStateException("Unnumbered assignment (" + assignment
                            + ") couldn't be found in object delta (no corresponding assignment value): "
                            + objectDelta);
                }
                copyPolicyData(assignmentInDelta.asContainerable(), evaluatedAssignment, triggers);
            }
        }
        return objectDelta;
    }

    private <T extends ObjectType, F extends FocusType> ObjectDelta<T> swallow(LensContext<F> context,
            ObjectDelta<T> objectDelta, List<ItemDelta<?, ?>> deltas) throws SchemaException {
        if (deltas.isEmpty()) {
            return objectDelta;
        }
        if (objectDelta == null) {
            if (context.getFocusClass() == null) {
                throw new IllegalStateException("No focus class in " + context);
            }
            if (context.getFocusContext() == null) {
                throw new IllegalStateException("No focus context in " + context);
            }
            objectDelta = (ObjectDelta) new ObjectDelta<F>(context.getFocusClass(), ChangeType.MODIFY,
                    prismContext);
            objectDelta.setOid(context.getFocusContext().getOid());
        }
        objectDelta.swallow(deltas);
        return objectDelta;
    }

    private void copyPolicyData(AssignmentType targetAssignment, EvaluatedAssignmentImpl<?> evaluatedAssignment,
            List<EvaluatedPolicyRuleTriggerType> triggers) {
        targetAssignment.getPolicySituation().clear();
        targetAssignment.getPolicySituation().addAll(evaluatedAssignment.getPolicySituations());
        targetAssignment.getTrigger().clear();
        targetAssignment.getTrigger().addAll(triggers);
    }

    public <O extends ObjectType> ObjectDelta<O> applyAssignmentSituation(LensContext<O> context,
            ObjectDelta<O> focusDelta) throws SchemaException {
        if (context.getFocusClass() == null || !FocusType.class.isAssignableFrom(context.getFocusClass())) {
            return focusDelta;
        }
        LensContext<? extends FocusType> contextOfFocus = (LensContext<FocusType>) context;
        if (focusDelta != null && focusDelta.isAdd()) {
            applyAssignmentSituationOnAdd(contextOfFocus,
                    (PrismObject<? extends FocusType>) focusDelta.getObjectToAdd());
            return focusDelta;
        } else if (focusDelta == null || focusDelta.isModify()) {
            return applyAssignmentSituationOnModify(contextOfFocus, focusDelta);
        } else {
            return focusDelta;
        }
    }
}