/*
* 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.wf.impl.processors.primary.policy;
import com.evolveum.midpoint.model.api.context.EvaluatedAssignment;
import com.evolveum.midpoint.model.api.context.EvaluatedPolicyRule;
import com.evolveum.midpoint.model.api.context.EvaluatedPolicyRuleTrigger;
import com.evolveum.midpoint.model.api.context.ModelContext;
import com.evolveum.midpoint.model.impl.lens.LensContext;
import com.evolveum.midpoint.model.impl.lens.LensFocusContext;
import com.evolveum.midpoint.prism.Objectable;
import com.evolveum.midpoint.prism.PrismContainerValue;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.PrismObject;
import com.evolveum.midpoint.prism.delta.ChangeType;
import com.evolveum.midpoint.prism.delta.DeltaSetTriple;
import com.evolveum.midpoint.prism.delta.ObjectDelta;
import com.evolveum.midpoint.prism.delta.PlusMinusZero;
import com.evolveum.midpoint.prism.delta.builder.DeltaBuilder;
import com.evolveum.midpoint.prism.delta.builder.S_ItemEntry;
import com.evolveum.midpoint.prism.delta.builder.S_ValuesEntry;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.prism.util.CloneUtil;
import com.evolveum.midpoint.schema.ObjectTreeDeltas;
import com.evolveum.midpoint.schema.constants.SchemaConstants;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.util.OidUtil;
import com.evolveum.midpoint.util.DebugUtil;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.wf.impl.processes.itemApproval.*;
import com.evolveum.midpoint.wf.impl.processors.primary.ModelInvocationContext;
import com.evolveum.midpoint.wf.impl.processors.primary.PcpChildWfTaskCreationInstruction;
import com.evolveum.midpoint.wf.impl.processors.primary.aspect.BasePrimaryChangeAspect;
import com.evolveum.midpoint.xml.ns._public.common.common_3.*;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.Validate;
import org.apache.velocity.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
import static com.evolveum.midpoint.schema.util.ObjectTypeUtil.createObjectRef;
import static com.evolveum.midpoint.wf.impl.util.MiscDataUtil.getFocusObjectName;
import static com.evolveum.midpoint.wf.impl.util.MiscDataUtil.getFocusObjectOid;
/**
*
* @author mederly
*/
@Component
public class PolicyRuleBasedAspect extends BasePrimaryChangeAspect {
private static final Trace LOGGER = TraceManager.getTrace(PolicyRuleBasedAspect.class);
@Autowired
protected PrismContext prismContext;
@Autowired
private ItemApprovalProcessInterface itemApprovalProcessInterface;
//region ------------------------------------------------------------ Things that execute on request arrival
@Override
public boolean isEnabledByDefault() {
return true;
}
@Override
protected boolean isFirst() {
return true;
}
@NotNull
@Override
public List<PcpChildWfTaskCreationInstruction> prepareTasks(@NotNull ObjectTreeDeltas objectTreeDeltas,
ModelInvocationContext ctx, @NotNull OperationResult result) throws SchemaException {
List<PcpChildWfTaskCreationInstruction> instructions = new ArrayList<>();
if (objectTreeDeltas.getFocusChange() != null) {
PrismObject<UserType> requester = baseModelInvocationProcessingHelper.getRequester(ctx.taskFromModel, result);
extractAssignmentBasedInstructions(objectTreeDeltas, requester, instructions, ctx, result);
extractObjectBasedInstructions(objectTreeDeltas, requester, instructions, ctx, result);
}
return instructions;
}
private void extractAssignmentBasedInstructions(ObjectTreeDeltas<?> objectTreeDeltas, PrismObject<UserType> requester,
List<PcpChildWfTaskCreationInstruction> instructions, ModelInvocationContext ctx, OperationResult result)
throws SchemaException {
DeltaSetTriple<? extends EvaluatedAssignment> evaluatedAssignmentTriple = ((LensContext<?>) ctx.modelContext).getEvaluatedAssignmentTriple();
LOGGER.trace("Processing evaluatedAssignmentTriple:\n{}", DebugUtil.debugDumpLazily(evaluatedAssignmentTriple));
if (evaluatedAssignmentTriple == null) {
return;
}
for (EvaluatedAssignment<?> newAssignment : evaluatedAssignmentTriple.getPlusSet()) {
CollectionUtils.addIgnoreNull(instructions,
createInstructionFromAssignment(newAssignment, PlusMinusZero.PLUS, objectTreeDeltas, requester, ctx, result));
}
for (EvaluatedAssignment<?> newAssignment : evaluatedAssignmentTriple.getMinusSet()) {
CollectionUtils.addIgnoreNull(instructions,
createInstructionFromAssignment(newAssignment, PlusMinusZero.MINUS, objectTreeDeltas, requester, ctx, result));
}
// Note: to implement assignment modifications we would need to fix subtractFromModification method below
}
private PcpChildWfTaskCreationInstruction<ItemApprovalSpecificContent> createInstructionFromAssignment(
EvaluatedAssignment<?> evaluatedAssignment, PlusMinusZero plusMinusZero, @NotNull ObjectTreeDeltas<?> objectTreeDeltas,
PrismObject<UserType> requester, ModelInvocationContext ctx, OperationResult result) throws SchemaException {
assert plusMinusZero == PlusMinusZero.PLUS || plusMinusZero == PlusMinusZero.MINUS;
// We collect all target rules; hoping that only relevant ones are triggered.
// For example, if we have assignment policy rule on induced role, it will get here.
// But projector will take care not to trigger it unless the rule is capable (e.g. configured)
// to be triggered in such a situation
List<EvaluatedPolicyRule> triggeredApprovalActionRules = getApprovalActionRules(evaluatedAssignment.getAllTargetsPolicyRules());
logApprovalActions(evaluatedAssignment, triggeredApprovalActionRules, plusMinusZero);
// Currently we can deal only with assignments that have a specific target
PrismObject<?> targetObject = evaluatedAssignment.getTarget();
if (targetObject == null) {
if (!triggeredApprovalActionRules.isEmpty()) {
throw new IllegalStateException("No target in " + evaluatedAssignment + ", but with "
+ triggeredApprovalActionRules.size() + " triggered approval action rule(s)");
} else {
return null;
}
}
// Let's construct the approval schema plus supporting triggered approval policy rule information
ApprovalSchemaBuilder.Result approvalSchemaResult = createSchemaWithRules(triggeredApprovalActionRules, plusMinusZero,
evaluatedAssignment, ctx, result);
if (approvalSchemaHelper.shouldBeSkipped(approvalSchemaResult.schemaType)) {
return null;
}
// Cut assignment from delta, prepare task instruction
@SuppressWarnings("unchecked")
PrismContainerValue<AssignmentType> assignmentValue = evaluatedAssignment.getAssignmentType().asPrismContainerValue();
boolean assignmentRemoved;
switch (plusMinusZero) {
case PLUS: assignmentRemoved = false; break;
case MINUS: assignmentRemoved = true; break;
default: throw new UnsupportedOperationException("Processing assignment zero set is not yet supported.");
}
boolean removed = objectTreeDeltas.subtractFromFocusDelta(new ItemPath(FocusType.F_ASSIGNMENT), assignmentValue, assignmentRemoved,
false);
if (!removed) {
ObjectDelta<?> secondaryDelta = ctx.modelContext.getFocusContext().getSecondaryDelta();
if (secondaryDelta != null && secondaryDelta.subtract(new ItemPath(FocusType.F_ASSIGNMENT), assignmentValue, assignmentRemoved, true)) {
LOGGER.trace("Assignment to be added/deleted was not found in primary delta. It is present in secondary delta, so there's nothing to be approved.");
return null;
}
String message = "Assignment to be added/deleted was not found in primary nor secondary delta."
+ "\nAssignment:\n" + assignmentValue.debugDump()
+ "\nPrimary delta:\n" + objectTreeDeltas.debugDump();
throw new IllegalStateException(message);
}
ObjectDelta<? extends ObjectType> focusDelta = objectTreeDeltas.getFocusChange();
if (focusDelta.isAdd()) {
miscDataUtil.generateFocusOidIfNeeded(ctx.modelContext, focusDelta);
}
return prepareAssignmentRelatedTaskInstruction(approvalSchemaResult, evaluatedAssignment, assignmentRemoved, ctx.modelContext, requester, result);
}
private List<EvaluatedPolicyRule> getApprovalActionRules(Collection<EvaluatedPolicyRule> rules) {
return rules.stream()
.filter(r -> !r.getTriggers().isEmpty() && r.getActions() != null && r.getActions().getApproval() != null)
.collect(Collectors.toList());
}
private ApprovalSchemaBuilder.Result createSchemaWithRules(List<EvaluatedPolicyRule> triggeredApprovalRules,
PlusMinusZero plusMinusZero, @NotNull EvaluatedAssignment<?> evaluatedAssignment, ModelInvocationContext ctx, OperationResult result) throws SchemaException {
PrismObject<?> targetObject = evaluatedAssignment.getTarget();
ApprovalSchemaBuilder builder = new ApprovalSchemaBuilder(this, approvalSchemaHelper);
// (1) legacy approvers (only if adding)
LegacyApproversSpecificationUsageType configuredUseLegacyApprovers =
baseConfigurationHelper.getUseLegacyApproversSpecification(ctx.wfConfiguration);
boolean useLegacyApprovers = configuredUseLegacyApprovers == LegacyApproversSpecificationUsageType.ALWAYS
|| configuredUseLegacyApprovers == LegacyApproversSpecificationUsageType.IF_NO_EXPLICIT_APPROVAL_POLICY_ACTION
&& triggeredApprovalRules.isEmpty();
if (plusMinusZero == PlusMinusZero.PLUS && useLegacyApprovers && targetObject.asObjectable() instanceof AbstractRoleType) {
AbstractRoleType abstractRole = (AbstractRoleType) targetObject.asObjectable();
if (abstractRole.getApprovalSchema() != null) {
builder.addPredefined(targetObject, abstractRole.getApprovalSchema().clone());
LOGGER.trace("Added legacy approval schema for {}", evaluatedAssignment);
} else if (!abstractRole.getApproverRef().isEmpty() || !abstractRole.getApproverExpression().isEmpty()) {
ApprovalStageDefinitionType level = new ApprovalStageDefinitionType(prismContext);
level.getApproverRef().addAll(CloneUtil.cloneCollectionMembers(abstractRole.getApproverRef()));
level.getApproverExpression().addAll(CloneUtil.cloneCollectionMembers(abstractRole.getApproverExpression()));
level.setAutomaticallyApproved(abstractRole.getAutomaticallyApproved());
// consider default (if expression returns no approvers) -- currently it is "reject"; it is probably correct
builder.addPredefined(targetObject, level);
LOGGER.trace("Added legacy approval schema (from approverRef, approverExpression, automaticallyApproved) for {}", evaluatedAssignment);
}
}
// (2) default policy action (only if adding)
if (triggeredApprovalRules.isEmpty() && plusMinusZero == PlusMinusZero.PLUS
&& baseConfigurationHelper.getUseDefaultApprovalPolicyRules(ctx.wfConfiguration) != DefaultApprovalPolicyRulesUsageType.NEVER) {
if (builder.addPredefined(targetObject, SchemaConstants.ORG_APPROVER, result)) {
LOGGER.trace("Added default approval action, as no explicit one was found for {}", evaluatedAssignment);
}
}
// (3) actions from triggered rules
for (EvaluatedPolicyRule approvalRule : triggeredApprovalRules) {
ApprovalPolicyActionType approvalAction = approvalRule.getActions().getApproval();
builder.add(getSchemaFromAction(approvalAction), approvalAction.getCompositionStrategy(), targetObject, approvalRule);
}
return builder.buildSchema(ctx, result);
}
private ApprovalSchemaType getSchemaFromAction(@NotNull ApprovalPolicyActionType approvalAction) {
// TODO approval process
if (approvalAction.getApprovalSchema() != null) {
return approvalAction.getApprovalSchema().clone();
} else {
ApprovalSchemaType rv = new ApprovalSchemaType(prismContext);
ApprovalStageDefinitionType stageDef = new ApprovalStageDefinitionType(prismContext);
stageDef.getApproverRef().addAll(CloneUtil.cloneCollectionMembers(approvalAction.getApproverRef()));
stageDef.getApproverRelation().addAll(approvalAction.getApproverRelation());
stageDef.getApproverExpression().addAll(approvalAction.getApproverExpression());
stageDef.setAutomaticallyApproved(approvalAction.getAutomaticallyApproved());
// TODO maybe use name + description as well
rv.getStage().add(stageDef);
return rv;
}
}
private void logApprovalActions(EvaluatedAssignment<?> newAssignment,
List<EvaluatedPolicyRule> triggeredApprovalActionRules, PlusMinusZero plusMinusZero) {
if (LOGGER.isDebugEnabled() && !triggeredApprovalActionRules.isEmpty()) {
LOGGER.trace("-------------------------------------------------------------");
LOGGER.debug("Assignment to be {}: {}: {} this target policy rules, {} triggered approval actions:",
plusMinusZero == PlusMinusZero.PLUS ? "added" : "deleted",
newAssignment, newAssignment.getThisTargetPolicyRules().size(), triggeredApprovalActionRules.size());
for (EvaluatedPolicyRule t : triggeredApprovalActionRules) {
LOGGER.debug(" - Approval action: {}", t.getActions().getApproval());
for (EvaluatedPolicyRuleTrigger trigger : t.getTriggers()) {
LOGGER.debug(" - {}", trigger);
}
}
}
}
private void extractObjectBasedInstructions(@NotNull ObjectTreeDeltas objectTreeDeltas, PrismObject<UserType> requester,
List<PcpChildWfTaskCreationInstruction> instructions, ModelInvocationContext ctx, @NotNull OperationResult result)
throws SchemaException {
ObjectDelta<?> focusDelta = objectTreeDeltas.getFocusChange();
LensFocusContext<?> focusContext = (LensFocusContext<?>) ctx.modelContext.getFocusContext();
PrismObject<?> object = focusContext.getObjectOld() != null ?
focusContext.getObjectOld() : focusContext.getObjectNew();
Map<Set<ItemPath>, ApprovalSchemaBuilder> schemaBuilders = new HashMap<>();
List<EvaluatedPolicyRule> approvalActionRules = getApprovalActionRules(focusContext.getPolicyRules());
LOGGER.trace("extractObjectBasedInstructions: approvalActionRules:\n{}", DebugUtil.debugDumpLazily(approvalActionRules));
for (EvaluatedPolicyRule rule : approvalActionRules) {
Set<ItemPath> key;
if (focusDelta.isAdd() || focusDelta.isDelete()) {
key = Collections.emptySet();
} else {
Set<ItemPath> items = getAffectedItems(rule.getTriggers());
Set<ItemPath> affectedItems;
if (!items.isEmpty()) {
affectedItems = items; // all items in triggered constraints were modified (that's how the constraints work)
} else {
affectedItems = new HashSet<>(focusDelta.getModifiedItems()); // whole object
}
key = affectedItems;
}
ApprovalSchemaBuilder builder = schemaBuilders.computeIfAbsent(key, k -> new ApprovalSchemaBuilder(this,
approvalSchemaHelper));
ApprovalPolicyActionType approvalAction = rule.getActions().getApproval();
builder.add(getSchemaFromAction(approvalAction), approvalAction.getCompositionStrategy(), object, rule);
}
// default rule
if (approvalActionRules.isEmpty()
&& baseConfigurationHelper.getUseDefaultApprovalPolicyRules(ctx.wfConfiguration) != DefaultApprovalPolicyRulesUsageType.NEVER) {
ApprovalSchemaBuilder builder = new ApprovalSchemaBuilder(this, approvalSchemaHelper);
if (builder.addPredefined(object, SchemaConstants.ORG_OWNER, result)) {
LOGGER.trace("Added default approval action, as no explicit one was found");
schemaBuilders.put(Collections.emptySet(), builder);
}
}
// create approval requests; also test for overlaps
Set<ItemPath> itemsProcessed = null;
for (Map.Entry<Set<ItemPath>, ApprovalSchemaBuilder> entry : schemaBuilders.entrySet()) {
ApprovalSchemaBuilder.Result builderResult = entry.getValue().buildSchema(ctx, result);
if (approvalSchemaHelper.shouldBeSkipped(builderResult.schemaType)) {
continue;
}
Set<ItemPath> items = entry.getKey();
if (itemsProcessed != null) {
if (items.isEmpty() || itemsProcessed.isEmpty() || CollectionUtils.containsAny(itemsProcessed, items)) {
throw new IllegalStateException("Overlapping modification-related policy rules. "
+ "Items processed = " + itemsProcessed + ", current items = " + items);
}
itemsProcessed.addAll(items);
} else {
itemsProcessed = items;
}
instructions.add(
prepareObjectRelatedTaskInstruction(builderResult, focusDelta, items, ctx.modelContext, requester, result));
}
}
private Set<ItemPath> getAffectedItems(Collection<EvaluatedPolicyRuleTrigger<?>> triggers) {
Set<ItemPath> rv = new HashSet<>();
for (EvaluatedPolicyRuleTrigger trigger : triggers) {
if (trigger.getConstraint() instanceof ModificationPolicyConstraintType) {
ModificationPolicyConstraintType modConstraint = (ModificationPolicyConstraintType) trigger.getConstraint();
if (modConstraint.getItem().isEmpty()) {
return Collections.emptySet(); // all items
} else {
modConstraint.getItem().forEach(
itemPathType -> rv.add(itemPathType.getItemPath()));
}
}
}
return rv;
}
private PcpChildWfTaskCreationInstruction<ItemApprovalSpecificContent> prepareAssignmentRelatedTaskInstruction(
ApprovalSchemaBuilder.Result builderResult,
EvaluatedAssignment<?> evaluatedAssignment, boolean assignmentRemoved, ModelContext<?> modelContext,
PrismObject<UserType> requester, OperationResult result) throws SchemaException {
String objectOid = getFocusObjectOid(modelContext);
String objectName = getFocusObjectName(modelContext);
@SuppressWarnings("unchecked")
PrismObject<? extends ObjectType> target = (PrismObject<? extends ObjectType>) evaluatedAssignment.getTarget();
Validate.notNull(target, "assignment target is null");
String targetName = target.getName() != null ? target.getName().getOrig() : "(unnamed)";
String operation = (assignmentRemoved
? "unassigning " + targetName + " from " :
"assigning " + targetName + " to ")
+ objectName;
String approvalTaskName = "Approve " + operation;
PcpChildWfTaskCreationInstruction<ItemApprovalSpecificContent> instruction =
PcpChildWfTaskCreationInstruction.createItemApprovalInstruction(getChangeProcessor(), approvalTaskName,
builderResult.schemaType, builderResult.attachedRules);
instruction.prepareCommonAttributes(this, modelContext, requester);
ObjectDelta<? extends FocusType> delta = assignmentToDelta(modelContext.getFocusClass(),
evaluatedAssignment.getAssignmentType(), assignmentRemoved, objectOid);
instruction.setDeltasToProcess(delta);
instruction.setObjectRef(modelContext, result);
instruction.setTargetRef(createObjectRef(target), result);
String andExecuting = instruction.isExecuteApprovedChangeImmediately() ? "and execution " : "";
instruction.setTaskName("Approval " + andExecuting + "of " + operation);
instruction.setProcessInstanceName(StringUtils.capitalizeFirstLetter(operation));
itemApprovalProcessInterface.prepareStartInstruction(instruction);
return instruction;
}
private PcpChildWfTaskCreationInstruction prepareObjectRelatedTaskInstruction(ApprovalSchemaBuilder.Result builderResult,
ObjectDelta<?> focusDelta, Set<ItemPath> paths, ModelContext<?> modelContext,
PrismObject<UserType> requester, OperationResult result) throws SchemaException {
//String objectOid = getFocusObjectOid(modelContext);
String objectName = getFocusObjectName(modelContext);
String opName;
if (focusDelta.isAdd()) {
opName = "addition";
} else if (focusDelta.isDelete()) {
opName = "deletion";
} else {
opName = "modification";
}
if (focusDelta.isAdd()) {
if (focusDelta.getObjectToAdd().getOid() == null) {
String newOid = OidUtil.generateOid();
focusDelta.getObjectToAdd().setOid(newOid);
((LensFocusContext<?>) modelContext.getFocusContext()).setOid(newOid);
}
}
String approvalTaskName = "Approve " + opName + " of " + objectName;
PcpChildWfTaskCreationInstruction<ItemApprovalSpecificContent> instruction =
PcpChildWfTaskCreationInstruction.createItemApprovalInstruction(getChangeProcessor(), approvalTaskName,
builderResult.schemaType, builderResult.attachedRules);
instruction.prepareCommonAttributes(this, modelContext, requester);
@SuppressWarnings("unchecked")
ObjectDelta<? extends FocusType> delta = (ObjectDelta<? extends FocusType>) subtractModifications(focusDelta, paths);
instruction.setDeltasToProcess(delta);
instruction.setObjectRef(modelContext, result);
String andExecuting = instruction.isExecuteApprovedChangeImmediately() ? "and execution " : "";
instruction.setTaskName("Approval " + andExecuting + "of " + opName + " of " + objectName);
instruction.setProcessInstanceName(StringUtils.capitalizeFirstLetter(opName) + " of " + objectName);
itemApprovalProcessInterface.prepareStartInstruction(instruction);
return instruction;
}
private ObjectDelta<?> subtractModifications(@NotNull ObjectDelta<?> focusDelta, @NotNull Set<ItemPath> itemPaths) {
if (itemPaths.isEmpty()) {
ObjectDelta<?> originalDelta = focusDelta.clone();
if (focusDelta.isAdd()) {
focusDelta.setObjectToAdd(null);
} else if (focusDelta.isModify()) {
focusDelta.getModifications().clear();
} else if (focusDelta.isDelete()) {
// hack: convert to empty ADD delta
focusDelta.setChangeType(ChangeType.ADD);
focusDelta.setObjectToAdd(null);
focusDelta.setOid(null);
} else {
throw new IllegalStateException("Unsupported delta type: " + focusDelta.getChangeType());
}
return originalDelta;
}
if (!focusDelta.isModify()) {
throw new IllegalStateException("Not a MODIFY delta; delta = " + focusDelta);
}
return focusDelta.subtract(itemPaths);
}
// creates an ObjectDelta that will be executed after successful approval of the given assignment
@SuppressWarnings("unchecked")
private ObjectDelta<? extends FocusType> assignmentToDelta(Class<? extends Objectable> focusClass,
AssignmentType assignmentType, boolean assignmentRemoved, String objectOid) throws SchemaException {
PrismContainerValue value = assignmentType.clone().asPrismContainerValue();
S_ValuesEntry item = DeltaBuilder.deltaFor(focusClass, prismContext)
.item(FocusType.F_ASSIGNMENT);
S_ItemEntry op = assignmentRemoved ? item.delete(value) : item.add(value);
return (ObjectDelta<? extends FocusType>) op.asObjectDelta(objectOid);
}
//endregion
}