Skip to content

Commit

Permalink
Block provisioning changes for simulation tasks
Browse files Browse the repository at this point in the history
The provisioning will refuse to carry out any "state-changing"
operations, including script execution. This is done at various levels,
to avoid e.g. recording such operations as pending, with the danger
of later execution.

These are blocked at:

- shadows facade level (i.e. almost at provisioning API level),
- ResourceObjectConverter (i.e. when UCF API is called),
- implementations of ConnectorInstance (i.e. right within ucf-xxx-impl).

At all of these places, an exception is thrown, i.e. the caller is
responsible to avoid making such calls. A special case is the code that
deals with the shadow refresh, as it can be invoked quite autonomously.
(This may be reconsidered.)
  • Loading branch information
mederly committed Feb 2, 2023
1 parent a2c8df2 commit d3c7746
Show file tree
Hide file tree
Showing 15 changed files with 226 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -477,9 +477,9 @@ public boolean isInMaintenance() {
}

/**
* Returns true if the definition of the current resource object is in "production" lifecycle state
* Returns `true` if the definition of the current resource object is in "production" lifecycle state
* (`active` or `deprecated`). This determines the behavior of some processing components, as described in
* https://docs.lab.evolveum.com/midpoint/devel/design/simulations/simulated-shadows/.
* https://docs.evolveum.com/midpoint/devel/design/simulations/simulated-shadows/.
*/
public boolean isObjectDefinitionInProduction() {
if (!SimulationUtil.isInProduction(resource)) {
Expand Down Expand Up @@ -659,4 +659,26 @@ public FetchErrorReportingMethodType getErrorReportingMethod() {
public boolean isProductionConfigurationTask() {
return task.getExecutionMode().isProductionConfiguration();
}

public boolean isInSimulation() {
return !task.isPersistentExecution();
}

/**
* This is a check that we are not going to cause any modification on a resource.
*
* This method should be called before any modifying connector operation, as well as before any such operation is called
* at the level of provisioning module itself - to ensure that e.g. no changes are queued for maintenance mode or
* operation grouping scenarios.
*
* @see UcfExecutionContext#checkNotInSimulation()
*/
public void checkNotInSimulation() {
if (isInSimulation()) {
LOGGER.error("MidPoint tried to execute an operation on {}. This is unexpected, as the task is running in simulation"
+ " mode ({}). Please report this as a bug. Resource object definition: {}",
resource, task.getExecutionMode(), resourceObjectDefinition);
throw new IllegalStateException("Invoking 'modifying' provisioning operation while being in a simulation mode");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ public AsynchronousOperationReturnValue<ShadowType> addResourceObject(

LOGGER.trace("Adding resource object {}", shadow);

ctx.checkNotInSimulation();

// We might be modifying the shadow (e.g. for simulated capabilities). But we do not want the changes
// to propagate back to the calling code. Hence the clone.
ShadowType shadowClone = shadow.clone();
Expand Down Expand Up @@ -423,6 +425,8 @@ public AsynchronousOperationResult deleteResourceObject(

LOGGER.trace("Deleting resource object {}", shadow);

ctx.checkNotInSimulation();

checkForCapability(ctx, DeleteCapabilityType.class);

Collection<? extends ResourceAttribute<?>> identifiers = getIdentifiers(ctx, shadow);
Expand Down Expand Up @@ -757,6 +761,8 @@ private AsynchronousOperationReturnValue<Collection<PropertyModificationOperatio
LOGGER.trace("Resource object modification operations: {}", operations);
}

ctx.checkNotInSimulation();

checkForCapability(ctx, UpdateCapabilityType.class);

if (!ShadowUtil.hasPrimaryIdentifier(identifiers, objectDefinition)) {
Expand Down Expand Up @@ -1732,6 +1738,7 @@ private void executeProvisioningScripts(
if (operations == null || operations.isEmpty()) {
return;
}
ctx.checkNotInSimulation();
ConnectorInstance connector = ctx.getConnector(ScriptCapabilityType.class, result);
for (ExecuteProvisioningScriptOperation operation : operations) {
UcfExecutionContext ucfCtx = new UcfExecutionContext(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,7 @@ static TwoStateRealToSimulatedConverter<LockoutStatusType> create(
}

<S> boolean convertProperty(N nativeValue, ShadowType shadow, OperationResult result)
throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException,
ExpressionEvaluationException {
throws SchemaException {
LOGGER.trace("Creating attribute for simulated {}: {}", description, simulatingAttributeName);

ResourceAttribute<S> simulatingAttribute =
Expand All @@ -109,8 +108,7 @@ <S> boolean convertProperty(N nativeValue, ShadowType shadow, OperationResult re
}

private <S> S determineSimulatingAttributeRealValue(N nativeValue,
ResourceAttribute<S> simulatingAttribute) throws ObjectNotFoundException, SchemaException, CommunicationException,
ConfigurationException, ExpressionEvaluationException {
ResourceAttribute<S> simulatingAttribute) {

if (nativeValue == null || nativePositiveValue.equals(nativeValue)) {
return getPositiveSimulationValue(simulatingAttribute);
Expand Down Expand Up @@ -142,8 +140,7 @@ private <S> void setSimulatingAttribute(ResourceAttribute<S> simulatingAttribute
}

<S> PropertyModificationOperation<S> convertDelta(N nativeValue, ShadowType shadow, OperationResult result)
throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException,
ExpressionEvaluationException {
throws SchemaException {
PropertyDelta<S> simulatingAttributeDelta;
ResourceAttribute<S> simulatingAttribute = createEmptySimulatingAttribute(shadow, result);
if (simulatingAttribute == null) {
Expand Down Expand Up @@ -183,8 +180,7 @@ private <S> PropertyDelta<S> createActivationPropDelta(ResourceAttributeDefiniti
}

private <S> ResourceAttribute<S> createEmptySimulatingAttribute(ShadowType shadow,
OperationResult result) throws ObjectNotFoundException, SchemaException,
CommunicationException, ConfigurationException, ExpressionEvaluationException {
OperationResult result) throws SchemaException {
LOGGER.trace("Name of the simulating attribute for {}: {}", description, simulatingAttributeName);

ResourceAttributeDefinition<?> attributeDefinition = ctx.findAttributeDefinition(simulatingAttributeName);
Expand All @@ -203,9 +199,7 @@ private <S> ResourceAttribute<S> createEmptySimulatingAttribute(ShadowType shado
return (ResourceAttribute<S>) attributeDefinition.instantiate(simulatingAttributeName);
}

private <S> S getPositiveSimulationValue(ResourceAttribute<S> simulatingAttribute)
throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException,
ExpressionEvaluationException {
private <S> S getPositiveSimulationValue(ResourceAttribute<S> simulatingAttribute) {
if (simulatedPositiveValues.isEmpty()) {
return null;
} else {
Expand All @@ -214,9 +208,7 @@ private <S> S getPositiveSimulationValue(ResourceAttribute<S> simulatingAttribut
}
}

private <S> S getNegativeSimulationValue(ResourceAttribute<S> simulatingAttribute)
throws ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException,
ExpressionEvaluationException {
private <S> S getNegativeSimulationValue(ResourceAttribute<S> simulatingAttribute) {
if (simulatedNegativeValues.isEmpty()) {
return null;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ public Object executeScript(String resourceOid, ProvisioningScriptType script, T
ExecuteProvisioningScriptOperation scriptOperation = ProvisioningUtil.convertToScriptOperation(script, "script on " + resource, prismContext);
try {
UcfExecutionContext ucfCtx = new UcfExecutionContext(lightweightIdentifierGenerator, resource, task);
ucfCtx.checkNotInSimulation();
return connectorInstance.executeScript(scriptOperation, ucfCtx, result);
} catch (GenericFrameworkException e) {
// Not expected. Transform to system exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ static String executeDirectly(
resourceObjectToAdd.debugDumpLazily(1));

ProvisioningContext ctx = establishProvisioningContext(resourceObjectToAdd, task, result);
ctx.checkNotInSimulation();
AddOperationState opState = new AddOperationState();
return new ShadowAddOperation(ctx, resourceObjectToAdd, scripts, opState, options)
.execute(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ static ShadowType executeDirectly(
try {
ctx = ShadowsLocalBeans.get().ctxFactory.createForShadow(repoShadow, task, result);
ctx.assertDefinition();
ctx.checkNotInSimulation();
} catch (ObjectNotFoundException ex) {
// If the force option is set, delete shadow from the repo even if the resource does not exist.
if (ProvisioningOperationOptions.isForce(options)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ static String executeDirectly(
task,
result);
ctx.assertDefinition();
ctx.checkNotInSimulation();

ModifyOperationState opState = new ModifyOperationState(repoShadow);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ class ShadowRefreshHelper {
throw SystemException.unexpected(e, "when refreshing provisioning indexes");
}

if (ctx.isInSimulation()) {
// Unlike other places related to the simulation mode, we do not throw an exception here. The shadow refresh may be
// invoked in various situations, and it is not sure that the caller(s) have full responsibility of these. Hence, we
// silently ignore these requests here.
LOGGER.trace("Skipping refresh of {} pending operations because the task is in simulation mode", repoShadow);
return new RefreshShadowOperation(repoShadow);
}

RefreshShadowOperation refreshShadowOperation = processPendingOperations(ctx, repoShadow, options, task, result);

XMLGregorianCalendar now = clock.currentTimeXMLGregorianCalendar();
Expand Down Expand Up @@ -134,7 +142,7 @@ class ShadowRefreshHelper {
}

if (ctx.isInMaintenance()) {
LOGGER.trace("Skipping refresh of {} pending operations because resource is in the maintenance mode.", repoShadow);
LOGGER.trace("Skipping refresh of {} pending operations because resource is in the maintenance mode", repoShadow);
return new RefreshShadowOperation(repoShadow);
}

Expand Down Expand Up @@ -376,7 +384,7 @@ private boolean needsRefresh(PendingOperationType pendingOperation) {
List<PendingOperationType> sortedOperations,
ProvisioningOperationOptions options,
OperationResult parentResult)
throws ObjectNotFoundException, SchemaException, ConfigurationException {
throws ObjectNotFoundException, SchemaException {
OperationResult retryResult = new OperationResult(OP_REFRESH_RETRY);
if (ShadowUtil.isDead(repoShadow)) {
RefreshShadowOperation rso = new RefreshShadowOperation(repoShadow);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -914,7 +914,7 @@ protected void assertCountConfiguredCapability(CountObjectsCapabilityType capCou
assertNotNull("configured count capability not present", capCount);
CountObjectsSimulateType simulate = capCount.getSimulate();
assertNotNull("simulate not present in configured count capability", simulate);
assertEquals("Wrong similate in configured count capability", getCountSimulationMode(), simulate);
assertEquals("Wrong simulate in configured count capability", getCountSimulationMode(), simulate);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.Collection;
import javax.xml.datatype.XMLGregorianCalendar;

import com.evolveum.midpoint.schema.TaskExecutionMode;
import com.evolveum.midpoint.test.asserter.PendingOperationsAsserter;

import org.springframework.test.annotation.DirtiesContext;
Expand Down Expand Up @@ -638,6 +639,37 @@ public void test124RefreshAccountMorganCommunicationFailure() throws Exception {
assertSteadyResources();
}

/**
* Refresh while the resource is down and in simulation mode.
* Nothing should happen - the request to retry the operations should be ignored.
*/
@Test
public void test125RefreshAccountMorganCommunicationFailureInSimulationMode() throws Exception {
Task task = getTestTask();
OperationResult result = task.getResult();

clockForward("PT17M");

syncServiceMock.reset();

dummyResource.setBreakMode(BreakMode.NETWORK);

PrismObject<ShadowType> shadowRepoBefore = getShadowRepo(shadowMorganOid);

when();
task.setExecutionMode(TaskExecutionMode.SIMULATED_PRODUCTION);
provisioningService.refreshShadow(shadowRepoBefore, null, task, result);

then();
display("Result", result);
assertSuccess(result);
syncServiceMock.assertNoNotifications();

assertUnmodifiedMorgan(1, 2, ACCOUNT_MORGAN_FULLNAME_HM);

assertSteadyResources();
}

/**
* Wait for retry interval to pass. Now provisioning should retry the operation.
* But no luck yet. Resource is still down.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

import com.evolveum.midpoint.schema.util.Resource;

import com.evolveum.midpoint.util.exception.*;

import org.jetbrains.annotations.NotNull;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
Expand All @@ -35,13 +37,13 @@
import com.evolveum.midpoint.task.api.Task;
import com.evolveum.midpoint.schema.TaskExecutionMode;
import com.evolveum.midpoint.test.DummyTestResource;
import com.evolveum.midpoint.util.exception.CommonException;
import com.evolveum.midpoint.util.exception.ConfigurationException;
import com.evolveum.midpoint.util.exception.SchemaException;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ResourceType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowKindType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.ShadowType;

/**
* Checks the effects of `lifecycleState` at the level of resource and object type/class definition.
*/
@ContextConfiguration(locations = "classpath:ctx-provisioning-test-main.xml")
@DirtiesContext
@Listeners({ com.evolveum.midpoint.tools.testng.AlphabeticalMethodInterceptor.class })
Expand Down Expand Up @@ -411,6 +413,108 @@ public void test200CreateAccountsInProduction() throws Exception {
.assertNotSimulated();
}

/** Shadow creation in simulation mode should be forbidden. */
@Test
public void test210CreateAccountsInSimulationMode() throws Exception {
Task task = getTestTask();
OperationResult result = task.getResult();

task.setExecutionMode(SIMULATED_PRODUCTION);
int shadowsBefore = repositoryService.countObjects(ShadowType.class, null, null, result);

when("account is attempted to be created (maintenance mode off)");
tryCreatingAccount(task, result);

when("account is attempted to be created (maintenance mode on)");
try {
turnMaintenanceModeOn(RESOURCE_DUMMY_ACTIVE.oid, result);
tryCreatingAccount(task, result);
} finally {
turnMaintenanceModeOff(RESOURCE_DUMMY_ACTIVE.oid, result);
}

then("there are no new shadows");
int shadowsAfter = repositoryService.countObjects(ShadowType.class, null, null, result);
assertThat(shadowsAfter).as("shadows after").isEqualTo(shadowsBefore);
}

private void tryCreatingAccount(Task task, OperationResult result) throws CommonException {
try {
provisioningService.addObject(
createShadow(RESOURCE_DUMMY_ACTIVE, I_EMPLOYEE, "e_test210"),
null, null, task, result);
fail("unexpected success");
} catch (IllegalStateException e) {
then("an exception is thrown");
displayExpectedException(e);
}
}

/** Shadow modification or deletion in simulation mode should be forbidden. */
@Test
public void test220ModifyOrDeleteAccountsInSimulationMode() throws Exception {
Task task = getTestTask();
OperationResult result = task.getResult();

given("an account exists on the resource");
String oid = provisioningService.addObject(
createShadow(RESOURCE_DUMMY_ACTIVE, I_EMPLOYEE, "e_test220"),
null, null, task, result);

task.setExecutionMode(SIMULATED_PRODUCTION);
int shadowsBefore = repositoryService.countObjects(ShadowType.class, null, null, result);

when("account is attempted to be modified and deleted (maintenance mode off)");
tryModifyingAccount(oid, task, result);
tryDeletingAccount(oid, task, result);

when("account is attempted to be modified and deleted (maintenance mode on)");
try {
turnMaintenanceModeOn(RESOURCE_DUMMY_ACTIVE.oid, result);
tryModifyingAccount(oid, task, result);
tryDeletingAccount(oid, task, result);
} finally {
turnMaintenanceModeOff(RESOURCE_DUMMY_ACTIVE.oid, result);
}

and("there are no new nor deleted shadows");
int shadowsAfter = repositoryService.countObjects(ShadowType.class, null, null, result);
assertThat(shadowsAfter).as("shadows after").isEqualTo(shadowsBefore);

and("there are no pending operations");
assertShadowNoFetch(oid)
.pendingOperations()
.assertNone();
}

private void tryModifyingAccount(String oid, Task task, OperationResult result) throws CommonException {
try {
provisioningService.modifyObject(
ShadowType.class,
oid,
Resource.of(RESOURCE_DUMMY_ACTIVE.get())
.deltaFor(RI_ACCOUNT_OBJECT_CLASS)
.item(ICFS_NAME_PATH)
.replace("changed")
.asItemDeltas(),
null, null, task, result);
fail("unexpected success");
} catch (IllegalStateException e) {
then("an exception is thrown");
displayExpectedException(e);
}
}

private void tryDeletingAccount(String oid, Task task, OperationResult result) throws CommonException {
try {
provisioningService.deleteObject(ShadowType.class, oid, null, null, task, result);
fail("unexpected success");
} catch (IllegalStateException e) {
then("an exception is thrown");
displayExpectedException(e);
}
}

private void changeIntent(String shadowOid, String newIntent, OperationResult result) throws CommonException {
repositoryService.modifyObject(
ShadowType.class,
Expand Down

0 comments on commit d3c7746

Please sign in to comment.