Skip to content

Commit

Permalink
Add "isMemberOf" implementation (MID-5366)
Browse files Browse the repository at this point in the history
This is a preliminary implementation, to be architecturally reviewed.
  • Loading branch information
mederly committed Jun 27, 2019
1 parent 160a9f9 commit 91695b8
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 21 deletions.
Expand Up @@ -40,6 +40,7 @@ public class ExpressionConstants {
public static final String VAR_PROJECTION = "projection";
public static final String VAR_SOURCE = "source";
public static final String VAR_ASSIGNMENT = "assignment";
public static final String VAR_ASSIGNMENT_EVALUATOR = "assignmentEvaluator";
public static final String VAR_EVALUATED_ASSIGNMENT = "evaluatedAssignment";
public static final String VAR_ASSIGNMENT_PATH = "assignmentPath";
public static final String VAR_IMMEDIATE_ASSIGNMENT = "immediateAssignment";
Expand Down
Expand Up @@ -15,18 +15,16 @@
*/
package com.evolveum.midpoint.model.impl.lens;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;

import com.evolveum.midpoint.common.ActivationComputer;
import com.evolveum.midpoint.model.api.ModelExecuteOptions;
import com.evolveum.midpoint.prism.delta.DeltaSetTriple;
import com.evolveum.midpoint.repo.common.ObjectResolver;
import com.evolveum.midpoint.repo.common.expression.ExpressionUtil;
import com.evolveum.midpoint.repo.common.expression.ExpressionVariables;
Expand Down Expand Up @@ -66,7 +64,6 @@
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.util.DOMUtil;
import com.evolveum.midpoint.util.Holder;
import com.evolveum.midpoint.util.exception.CommonException;
import com.evolveum.midpoint.util.exception.CommunicationException;
import com.evolveum.midpoint.util.exception.ConfigurationException;
import com.evolveum.midpoint.util.exception.ExpressionEvaluationException;
Expand Down Expand Up @@ -114,6 +111,9 @@ public class AssignmentEvaluator<AH extends AssignmentHolderType> {
private final EvaluatedAssignmentTargetCache evaluatedAssignmentTargetCache;
private final LifecycleStateModelType focusStateModel;

// Evaluation state
private final List<MemberOfInvocation> memberOfInvocations = new ArrayList<>(); // experimental

private AssignmentEvaluator(Builder<AH> builder) {
repository = builder.repository;
focusOdo = builder.focusOdo;
Expand Down Expand Up @@ -194,8 +194,11 @@ public MappingEvaluator getMappingEvaluator() {
return mappingEvaluator;
}

public void reset() {
public void reset(boolean alsoMemberOfInvocations) {
evaluatedAssignmentTargetCache.reset();
if (alsoMemberOfInvocations) {
memberOfInvocations.clear();
}
}

// This is to reduce the number of parameters passed between methods in this class.
Expand Down Expand Up @@ -1119,7 +1122,7 @@ private void collectMembership(AssignmentHolderType targetType, QName relation,
collectMembershipRefVal(refVal, targetType.getClass(), relation, targetType, ctx);
}

private void collectTenantRef(AssignmentHolderType targetType, AssignmentEvaluator<AH>.EvaluationContext ctx) {
private void collectTenantRef(AssignmentHolderType targetType, EvaluationContext ctx) {
if (targetType instanceof OrgType) {
if (BooleanUtils.isTrue(((OrgType)targetType).isTenant()) && ctx.evalAssignment.getTenantOid() == null) {
if (ctx.assignmentPath.hasOnlyOrgs()) {
Expand Down Expand Up @@ -1318,6 +1321,7 @@ public PrismValueDeltaSetTriple<PrismPropertyValue<Boolean>> evaluateCondition(M
.addVariableDefinition(ExpressionConstants.VAR_USER, focusOdo)
.addVariableDefinition(ExpressionConstants.VAR_FOCUS, focusOdo)
.addVariableDefinition(ExpressionConstants.VAR_SOURCE, source, ObjectType.class)
.addVariableDefinition(ExpressionConstants.VAR_ASSIGNMENT_EVALUATOR, this, AssignmentEvaluator.class)
.rootNode(focusOdo);
builder = LensUtil.addAssignmentPathVariables(builder, assignmentPathVariables, prismContext);

Expand All @@ -1334,6 +1338,83 @@ private QName getRelation(AssignmentType assignmentType) {
relationRegistry.normalizeRelation(assignmentType.getTargetRef().getRelation()) : null;
}

/*
* This "isMemberOf iteration" section is an experimental implementation of MID-5366.
*
* The main idea: In role/assignment/inducement conditions we test the membership not by querying roleMembershipRef
* on focus object but instead we call assignmentEvaluator.isMemberOf() method. This method - by default - inspects
* roleMembershipRef but also records the check result. Later, when assignment evaluation is complete, AssignmentProcessor
* will ask if all of these check results are still valid. If they are not, it requests re-evaluation of all the assignments,
* using updated check results.
*
* This should work unless there are some cyclic dependencies (like "this sentence is a lie" paradox).
*/
public boolean isMemberOf(String targetOid) {
MemberOfInvocation existingInvocation = findInvocation(targetOid);
if (existingInvocation != null) {
return existingInvocation.result;
} else {
boolean result = computeIsMemberOfDuringEvaluation(targetOid);
memberOfInvocations.add(new MemberOfInvocation(targetOid, result));
return result;
}
}

private MemberOfInvocation findInvocation(String targetOid) {
List<MemberOfInvocation> matching = memberOfInvocations.stream()
.filter(invocation -> targetOid.equals(invocation.targetOid))
.collect(Collectors.toList());
if (matching.isEmpty()) {
return null;
} else if (matching.size() == 1) {
return matching.get(0);
} else {
throw new IllegalStateException("More than one matching MemberOfInvocation for targetOid='" + targetOid + "': " + matching);
}
}

private boolean computeIsMemberOfDuringEvaluation(String targetOid) {
if (targetOid == null) {
throw new IllegalArgumentException("Null targetOid");
}
// TODO Or should we consider evaluateOld?
PrismObject<AH> focus = focusOdo.getNewObject();
return focus != null && containsMember(focus.asObjectable().getRoleMembershipRef(), targetOid);
}

public boolean isMemberOfInvocationResultChanged(DeltaSetTriple<EvaluatedAssignmentImpl<AH>> evaluatedAssignmentTriple) {
if (!memberOfInvocations.isEmpty()) {
// Similar code is in AssignmentProcessor.processMembershipAndDelegatedRefs -- check that if changing the business logic
List<ObjectReferenceType> membership = evaluatedAssignmentTriple.getNonNegativeValues().stream()
.filter(EvaluatedAssignmentImpl::isValid)
.flatMap(evaluatedAssignment -> evaluatedAssignment.getMembershipRefVals().stream())
.map(ref -> ObjectTypeUtil.createObjectRef(ref, false))
.collect(Collectors.toList());
LOGGER.trace("Computed new membership: {}", membership);
return updateMemberOfInvocations(membership);
} else {
return false;
}
}

private boolean updateMemberOfInvocations(List<ObjectReferenceType> newMembership) {
List<MemberOfInvocation> changed = new ArrayList<>();
for (MemberOfInvocation invocation : memberOfInvocations) {
boolean newResult = containsMember(newMembership, invocation.targetOid);
if (newResult != invocation.result) {
LOGGER.trace("Invocation result changed for {} - new one is '{}'", invocation, newResult);
invocation.result = newResult;
changed.add(invocation);
}
}
return !changed.isEmpty();
}

// todo generalize a bit (e.g. by including relation)
private boolean containsMember(List<ObjectReferenceType> membership, String targetOid) {
return membership.stream().anyMatch(ref -> targetOid.equals(ref.getOid()));
}

public static final class Builder<AH extends AssignmentHolderType> {
private RepositoryService repository;
private ObjectDeltaObject<AH> focusOdo;
Expand Down Expand Up @@ -1427,4 +1508,22 @@ public AssignmentEvaluator<AH> build() {
return new AssignmentEvaluator<>(this);
}
}

private static class MemberOfInvocation {
private final String targetOid;
private boolean result;

MemberOfInvocation(String targetOid, boolean result) {
this.targetOid = targetOid;
this.result = result;
}

@Override
public String toString() {
return "MemberOfInvocation{" +
"targetOid='" + targetOid + '\'' +
", result=" + result +
'}';
}
}
}
Expand Up @@ -239,6 +239,11 @@ private <AH extends AssignmentHolderType, F extends FocusType> void processAssig
// Evaluates all assignments and sorts them to triple: added, removed and untouched assignments.
// This is where most of the assignment-level action happens.
DeltaSetTriple<EvaluatedAssignmentImpl<AH>> evaluatedAssignmentTriple = assignmentTripleEvaluator.processAllAssignments();
if (assignmentTripleEvaluator.isMemberOfInvocationResultChanged(evaluatedAssignmentTriple)) {
LOGGER.debug("Re-evaluating assignments because isMemberOf invocation result has changed");
assignmentTripleEvaluator.reset(false);
evaluatedAssignmentTriple = assignmentTripleEvaluator.processAllAssignments();
}
policyRuleProcessor.addGlobalPolicyRulesToAssignments(context, evaluatedAssignmentTriple, task, result);
context.setEvaluatedAssignmentTriple((DeltaSetTriple)evaluatedAssignmentTriple);

Expand All @@ -254,7 +259,7 @@ private <AH extends AssignmentHolderType, F extends FocusType> void processAssig
if (needToReevaluateAssignments) {
LOGGER.debug("Re-evaluating assignments because exclusion pruning rule was triggered");

assignmentTripleEvaluator.reset();
assignmentTripleEvaluator.reset(true);
evaluatedAssignmentTriple = assignmentTripleEvaluator.processAllAssignments();
context.setEvaluatedAssignmentTriple((DeltaSetTriple)evaluatedAssignmentTriple);

Expand Down Expand Up @@ -1038,6 +1043,7 @@ public <F extends ObjectType> void processMembershipAndDelegatedRefs(LensContext
if (evaluatedAssignmentTriple == null) {
return; // could be if the "assignments" step is skipped
}
// Similar code is in AssignmentEvaluator.isMemberOfInvocationResultChanged -- check that if changing the business logic
for (EvaluatedAssignmentImpl<?> evalAssignment : evaluatedAssignmentTriple.getNonNegativeValues()) {
if (evalAssignment.isValid()) {
addReferences(shouldBeRoleRefs, evalAssignment.getMembershipRefVals());
Expand Down
Expand Up @@ -23,7 +23,6 @@

import com.evolveum.midpoint.common.ActivationComputer;
import com.evolveum.midpoint.model.api.ModelExecuteOptions;
import com.evolveum.midpoint.model.api.context.EvaluatedAssignment;
import com.evolveum.midpoint.model.api.context.SynchronizationPolicyDecision;
import com.evolveum.midpoint.model.impl.lens.AssignmentEvaluator;
import com.evolveum.midpoint.model.impl.lens.EvaluatedAssignmentImpl;
Expand All @@ -48,7 +47,6 @@
import com.evolveum.midpoint.schema.util.FocusTypeUtil;
import com.evolveum.midpoint.schema.util.SchemaDebugUtil;
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.util.DebugUtil;
import com.evolveum.midpoint.util.exception.CommunicationException;
import com.evolveum.midpoint.util.exception.ConfigurationException;
import com.evolveum.midpoint.util.exception.ExpressionEvaluationException;
Expand Down Expand Up @@ -156,8 +154,8 @@ public void setResult(OperationResult result) {
this.result = result;
}

public void reset() {
assignmentEvaluator.reset();
public void reset(boolean alsoMemberOfInvocations) {
assignmentEvaluator.reset(alsoMemberOfInvocations);
}

// public DeltaSetTriple<EvaluatedAssignmentImpl<AH>> preProcessAssignments(PrismObject<TaskType> taskType) throws ObjectNotFoundException, SchemaException, ExpressionEvaluationException, PolicyViolationException, SecurityViolationException, ConfigurationException, CommunicationException {
Expand Down Expand Up @@ -652,4 +650,7 @@ private PrismContainerDefinition<AssignmentType> getAssignmentContainerDefinitio
return focusContext.getObjectDefinition().findContainerDefinition(AssignmentHolderType.F_ASSIGNMENT);
}

boolean isMemberOfInvocationResultChanged(DeltaSetTriple<EvaluatedAssignmentImpl<AH>> evaluatedAssignmentTriple) {
return assignmentEvaluator.isMemberOfInvocationResultChanged(evaluatedAssignmentTriple);
}
}
Expand Up @@ -30,19 +30,17 @@
import com.evolveum.midpoint.xml.ns._public.common.common_3.UserType;


@ContextConfiguration(locations = { "classpath:ctx-story-test-main.xml" })
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)


/**
* testing inducements, no ressources, no accounts in use.
* testing inducements, no resources, no accounts in use.
* role "processor" is assigned to user, it contains inducements for role1, role2, role3 having following conditions
*
*
* role1: no condition
* role2: should not be induced when description of user equals "NO"
* role3: should not be induced when user is member of role named "lock" (directly or indirectly, therefore condition runs against rolemembershipRef)
* role3: should not be induced when user is member of role named "lock" (directly or indirectly, therefore condition runs against roleMembershipRef)
*/

@ContextConfiguration(locations = { "classpath:ctx-story-test-main.xml" })
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
public class TestInducement extends AbstractStoryTest {

public static final File TEST_DIR = new File(MidPointTestConstants.TEST_RESOURCES_DIR, "inducement");
Expand Down Expand Up @@ -214,4 +212,64 @@ public void test040Recomputed() throws Exception {
assertNotAssignedRole(user, ROLE_ROLE3_OID);
assertRoleMembershipRef(user, ROLE_PROCESSOR_OID, ROLE_LOCK_OID, ROLE_ROLE1_OID);
}

/**
* Unassign role "lock" from user
*/
@Test
public void test050InducementRole3ConditionTrue() throws Exception {
final String TEST_NAME = "test050InducementRole3ConditionTrue";
displayTestTitle(TEST_NAME);

// GIVEN
Task task = createTask(TEST_NAME);
OperationResult result = task.getResult();
dummyAuditService.clear();

// WHEN
unassignRole(USER_SIMPLE_OID, ROLE_LOCK_OID, task, result);

// THEN
assertSuccess(result);

PrismObject<UserType> user = getUser(USER_SIMPLE_OID);
display("User simple having role lock unassigned'", user);

assertAssignedRole(user, ROLE_PROCESSOR_OID);
assertNotAssignedRole(user, ROLE_LOCK_OID);
assertNotAssignedRole(user, ROLE_ROLE1_OID);
assertNotAssignedRole(user, ROLE_ROLE2_OID);
assertNotAssignedRole(user, ROLE_ROLE3_OID);
assertRoleMembershipRef(user, ROLE_PROCESSOR_OID, ROLE_ROLE1_OID, ROLE_ROLE3_OID);
}

/**
* same as Test50, just recomputed again
*/
@Test
public void test060Recomputed() throws Exception {
final String TEST_NAME = "test060Recomputed";
displayTestTitle(TEST_NAME);

// GIVEN
Task task = createTask(TEST_NAME);
OperationResult result = task.getResult();
dummyAuditService.clear();

// WHEN
recomputeUser(USER_SIMPLE_OID);

// THEN
assertSuccess(result);

PrismObject<UserType> user = getUser(USER_SIMPLE_OID);
display("User simple having role lock unassigned'", user);

assertAssignedRole(user, ROLE_PROCESSOR_OID);
assertNotAssignedRole(user, ROLE_LOCK_OID);
assertNotAssignedRole(user, ROLE_ROLE1_OID);
assertNotAssignedRole(user, ROLE_ROLE2_OID);
assertNotAssignedRole(user, ROLE_ROLE3_OID);
assertRoleMembershipRef(user, ROLE_PROCESSOR_OID, ROLE_ROLE1_OID, ROLE_ROLE3_OID);
}
}
Expand Up @@ -46,7 +46,7 @@
<script>
<relativityMode>absolute</relativityMode>
<code>
!roleMembershipRef?.oid?.contains("10000000-0000-0000-0000-10000000lock")
!assignmentEvaluator.isMemberOf('10000000-0000-0000-0000-10000000lock')
</code>
</script>
</expression>
Expand Down

0 comments on commit 91695b8

Please sign in to comment.