/* * 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.model.impl.lens; import com.evolveum.midpoint.audit.api.AuditEventRecord; import com.evolveum.midpoint.audit.api.AuditEventStage; import com.evolveum.midpoint.audit.api.AuditEventType; import com.evolveum.midpoint.audit.api.AuditService; import com.evolveum.midpoint.common.Clock; import com.evolveum.midpoint.model.api.ModelAuthorizationAction; import com.evolveum.midpoint.model.api.ModelExecuteOptions; import com.evolveum.midpoint.model.api.context.EvaluatedAssignment; import com.evolveum.midpoint.model.api.context.ModelState; import com.evolveum.midpoint.model.api.hooks.ChangeHook; import com.evolveum.midpoint.model.api.hooks.HookOperationMode; import com.evolveum.midpoint.model.api.hooks.HookRegistry; import com.evolveum.midpoint.model.common.SystemObjectCache; import com.evolveum.midpoint.model.common.expression.ExpressionVariables; import com.evolveum.midpoint.model.common.expression.evaluator.caching.AssociationSearchExpressionEvaluatorCache; import com.evolveum.midpoint.model.common.expression.script.ScriptExpression; import com.evolveum.midpoint.model.common.expression.script.ScriptExpressionFactory; import com.evolveum.midpoint.model.impl.ModelObjectResolver; import com.evolveum.midpoint.model.impl.controller.ModelUtils; import com.evolveum.midpoint.model.impl.lens.projector.ContextLoader; import com.evolveum.midpoint.model.impl.lens.projector.FocusConstraintsChecker; import com.evolveum.midpoint.model.impl.lens.projector.Projector; import com.evolveum.midpoint.model.impl.sync.RecomputeTaskHandler; import com.evolveum.midpoint.model.impl.util.Utils; import com.evolveum.midpoint.prism.*; import com.evolveum.midpoint.prism.delta.*; import com.evolveum.midpoint.prism.delta.builder.DeltaBuilder; import com.evolveum.midpoint.prism.marshaller.QueryConvertor; import com.evolveum.midpoint.prism.path.ItemPath; import com.evolveum.midpoint.prism.query.ObjectFilter; import com.evolveum.midpoint.prism.query.builder.QueryBuilder; import com.evolveum.midpoint.prism.util.CloneUtil; import com.evolveum.midpoint.prism.xml.XmlTypeConverter; import com.evolveum.midpoint.provisioning.api.ChangeNotificationDispatcher; import com.evolveum.midpoint.provisioning.api.ProvisioningService; import com.evolveum.midpoint.provisioning.api.ResourceObjectChangeListener; import com.evolveum.midpoint.provisioning.api.ResourceOperationListener; import com.evolveum.midpoint.repo.api.RepositoryService; import com.evolveum.midpoint.schema.ObjectDeltaOperation; import com.evolveum.midpoint.schema.constants.ExpressionConstants; import com.evolveum.midpoint.schema.constants.ObjectTypes; import com.evolveum.midpoint.schema.constants.SchemaConstants; import com.evolveum.midpoint.schema.internals.InternalsConfig; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.schema.result.OperationResultStatus; import com.evolveum.midpoint.schema.util.ObjectTypeUtil; import com.evolveum.midpoint.schema.util.SystemConfigurationTypeUtil; import com.evolveum.midpoint.security.api.ObjectSecurityConstraints; import com.evolveum.midpoint.security.api.OwnerResolver; import com.evolveum.midpoint.security.api.SecurityEnforcer; import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.task.api.TaskBinding; import com.evolveum.midpoint.task.api.TaskCategory; import com.evolveum.midpoint.task.api.TaskExecutionStatus; import com.evolveum.midpoint.task.api.TaskManager; import com.evolveum.midpoint.util.DebugUtil; import com.evolveum.midpoint.util.Holder; import com.evolveum.midpoint.util.exception.AuthorizationException; import com.evolveum.midpoint.util.exception.CommunicationException; import com.evolveum.midpoint.util.exception.ConfigurationException; import com.evolveum.midpoint.util.exception.ExpressionEvaluationException; import com.evolveum.midpoint.util.exception.ObjectAlreadyExistsException; import com.evolveum.midpoint.util.exception.ObjectNotFoundException; import com.evolveum.midpoint.util.exception.PolicyViolationException; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.SecurityViolationException; import com.evolveum.midpoint.util.exception.SystemException; import com.evolveum.midpoint.util.logging.LoggingUtils; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.*; import com.evolveum.prism.xml.ns._public.query_3.QueryType; import com.evolveum.prism.xml.ns._public.query_3.SearchFilterType; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.namespace.QName; import java.util.*; import java.util.Map.Entry; /** * @author semancik * */ @Component public class Clockwork { private static final int DEFAULT_NUMBER_OF_RESULTS_TO_KEEP = 5; private static final Trace LOGGER = TraceManager.getTrace(Clockwork.class); @Autowired(required=true) private Projector projector; // This is ugly // TODO: cleanup @Autowired private ContextLoader contextLoader; @Autowired(required=true) private ChangeExecutor changeExecutor; @Autowired(required = false) private HookRegistry hookRegistry; @Autowired private AuditService auditService; @Autowired private SecurityEnforcer securityEnforcer; @Autowired private Clock clock; @Autowired @Qualifier("cacheRepositoryService") private transient RepositoryService repositoryService; @Autowired private ModelObjectResolver objectResolver; @Autowired private SystemObjectCache systemObjectCache; @Autowired private transient ProvisioningService provisioningService; @Autowired private transient ChangeNotificationDispatcher changeNotificationDispatcher; @Autowired private ScriptExpressionFactory scriptExpressionFactory; @Autowired(required=true) private PersonaProcessor personaProcessor; @Autowired private PrismContext prismContext; @Autowired private TaskManager taskManager; @Autowired private OperationalDataManager metadataManager; private LensDebugListener debugListener; public LensDebugListener getDebugListener() { return debugListener; } public void setDebugListener(LensDebugListener debugListener) { this.debugListener = debugListener; } private static final int DEFAULT_MAX_CLICKS = 200; public <F extends ObjectType> HookOperationMode run(LensContext<F> context, Task task, OperationResult result) throws SchemaException, PolicyViolationException, ExpressionEvaluationException, ObjectNotFoundException, ObjectAlreadyExistsException, CommunicationException, ConfigurationException, SecurityViolationException { LOGGER.trace("Running clockwork for context {}", context); if (InternalsConfig.consistencyChecks) { context.checkConsistence(); } int clicked = 0; try { FocusConstraintsChecker.enterCache(); enterAssociationSearchExpressionEvaluatorCache(); provisioningService.enterConstraintsCheckerCache(); while (context.getState() != ModelState.FINAL) { // TODO implement in model context (as transient or even non-transient attribute) to allow for checking in more complex scenarios int maxClicks = getMaxClicks(context, result); if (clicked >= maxClicks) { throw new IllegalStateException("Model operation took too many clicks (limit is " + maxClicks + "). Is there a cycle?"); } clicked++; HookOperationMode mode = click(context, task, result); if (mode == HookOperationMode.BACKGROUND) { result.recordInProgress(); return mode; } else if (mode == HookOperationMode.ERROR) { return mode; } } // One last click in FINAL state return click(context, task, result); } finally { FocusConstraintsChecker.exitCache(); exitAssociationSearchExpressionEvaluatorCache(); provisioningService.exitConstraintsCheckerCache(); } } private void enterAssociationSearchExpressionEvaluatorCache() { AssociationSearchExpressionEvaluatorCache cache = AssociationSearchExpressionEvaluatorCache.enterCache(); AssociationSearchExpressionCacheInvalidator invalidator = new AssociationSearchExpressionCacheInvalidator(cache); cache.setClientContextInformation(invalidator); changeNotificationDispatcher.registerNotificationListener((ResourceObjectChangeListener) invalidator); changeNotificationDispatcher.registerNotificationListener((ResourceOperationListener) invalidator); } private void exitAssociationSearchExpressionEvaluatorCache() { AssociationSearchExpressionEvaluatorCache cache = AssociationSearchExpressionEvaluatorCache.exitCache(); if (cache == null) { return; // shouldn't occur } Object invalidator = cache.getClientContextInformation(); if (invalidator == null || !(invalidator instanceof AssociationSearchExpressionCacheInvalidator)) { return; // shouldn't occur either } changeNotificationDispatcher.unregisterNotificationListener((ResourceObjectChangeListener) invalidator); changeNotificationDispatcher.unregisterNotificationListener((ResourceOperationListener) invalidator); } private <F extends ObjectType> int getMaxClicks(LensContext<F> context, OperationResult result) throws SchemaException, ObjectNotFoundException { PrismObject<SystemConfigurationType> systemConfiguration = systemObjectCache.getSystemConfiguration(result); Integer maxClicks = SystemConfigurationTypeUtil.getMaxModelClicks(systemConfiguration); if (maxClicks == null) { return DEFAULT_MAX_CLICKS; } else { return maxClicks; } } public <F extends ObjectType> HookOperationMode click(LensContext<F> context, Task task, OperationResult result) throws SchemaException, PolicyViolationException, ExpressionEvaluationException, ObjectNotFoundException, ObjectAlreadyExistsException, CommunicationException, ConfigurationException, SecurityViolationException { // DO NOT CHECK CONSISTENCY of the context here. The context may not be fresh and consistent yet. Project will fix // that. Check consistency afterwards (and it is also checked inside projector several times). if (context.getDebugListener() == null) { context.setDebugListener(debugListener); } try { XMLGregorianCalendar now = clock.currentTimeXMLGregorianCalendar(); // We need to determine focus before auditing. Otherwise we will not know user // for the accounts (unless there is a specific delta for it). // This is ugly, but it is the easiest way now (TODO: cleanup). contextLoader.determineFocusContext((LensContext<? extends FocusType>) context, result); ModelState state = context.getState(); if (state == ModelState.INITIAL) { if (debugListener != null) { debugListener.beforeSync(context); } metadataManager.applyRequestMetadata(context, now, task, result); context.getStats().setRequestTimestamp(now); // We need to do this BEFORE projection. If we would do that after projection // there will be secondary changes that are not part of the request. audit(context, AuditEventStage.REQUEST, task, result); } boolean recompute = false; if (!context.isFresh()) { LOGGER.trace("Context is not fresh -- forcing cleanup and recomputation"); recompute = true; } else if (context.getExecutionWave() > context.getProjectionWave()) { // should not occur LOGGER.warn("Execution wave is greater than projection wave -- forcing cleanup and recomputation"); recompute = true; } if (recompute) { context.cleanup(); projector.project(context, "PROJECTOR ("+state+")", task, result); } else if (context.getExecutionWave() == context.getProjectionWave()) { LOGGER.trace("Running projector for current execution wave"); projector.resume(context, "PROJECTOR ("+state+")", task, result); } else { LOGGER.trace("Skipping projection because the context is fresh and projection for current wave has already run"); } if (!context.isRequestAuthorized()) { authorizeContextRequest(context, task, result); } LensUtil.traceContext(LOGGER, "CLOCKWORK (" + state + ")", "before processing", true, context, false); if (InternalsConfig.consistencyChecks) { try { context.checkConsistence(); } catch (IllegalStateException e) { throw new IllegalStateException(e.getMessage()+" in clockwork, state="+state, e); } } if (InternalsConfig.encryptionChecks && !ModelExecuteOptions.isNoCrypt(context.getOptions())) { context.checkEncrypted(); } // LOGGER.info("CLOCKWORK: {}: {}", state, context); switch (state) { case INITIAL: processInitialToPrimary(context, task, result); break; case PRIMARY: processPrimaryToSecondary(context, task, result); break; case SECONDARY: processSecondary(context, task, result); break; case FINAL: HookOperationMode mode = processFinal(context, task, result); if (debugListener != null) { debugListener.afterSync(context); } return mode; } result.recomputeStatus(); result.cleanupResult(); return invokeHooks(context, task, result); } catch (CommunicationException | ConfigurationException | ExpressionEvaluationException | ObjectNotFoundException | PolicyViolationException | SchemaException | SecurityViolationException | RuntimeException | ObjectAlreadyExistsException e) { processClockworkException(context, e, task, result); throw e; } } /** * Invokes hooks, if there are any. * * @return * - ERROR, if any hook reported error; otherwise returns * - BACKGROUND, if any hook reported switching to background; otherwise * - FOREGROUND (if all hooks reported finishing on foreground) */ private HookOperationMode invokeHooks(LensContext context, Task task, OperationResult result) throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException, PolicyViolationException { // TODO: following two parts should be merged together in later versions // Execute configured scripting hooks PrismObject<SystemConfigurationType> systemConfiguration = systemObjectCache.getSystemConfiguration(result); // systemConfiguration may be null in some tests if (systemConfiguration != null) { ModelHooksType modelHooks = systemConfiguration.asObjectable().getModelHooks(); if (modelHooks != null) { HookListType changeHooks = modelHooks.getChange(); if (changeHooks != null) { for (HookType hookType: changeHooks.getHook()) { String shortDesc; if (hookType.getName() != null) { shortDesc = "hook '"+hookType.getName()+"'"; } else { shortDesc = "scripting hook in system configuration"; } if (hookType.isEnabled() != null && !hookType.isEnabled()) { // Disabled hook, skip continue; } if (hookType.getState() != null) { if (!context.getState().toModelStateType().equals(hookType.getState())) { continue; } } if (hookType.getFocusType() != null) { if (context.getFocusContext() == null) { continue; } QName hookFocusTypeQname = hookType.getFocusType(); ObjectTypes hookFocusType = ObjectTypes.getObjectTypeFromTypeQName(hookFocusTypeQname); if (hookFocusType == null) { throw new SchemaException("Unknown focus type QName "+hookFocusTypeQname+" in "+shortDesc); } Class focusClass = context.getFocusClass(); Class<? extends ObjectType> hookFocusClass = hookFocusType.getClassDefinition(); if (!hookFocusClass.isAssignableFrom(focusClass)) { continue; } } ScriptExpressionEvaluatorType scriptExpressionEvaluatorType = hookType.getScript(); if (scriptExpressionEvaluatorType == null) { continue; } try { evaluateScriptingHook(context, hookType, scriptExpressionEvaluatorType, shortDesc, task, result); } catch (ExpressionEvaluationException e) { LOGGER.error("Evaluation of {} failed: {}", new Object[]{shortDesc, e.getMessage(), e}); throw new ExpressionEvaluationException("Evaluation of "+shortDesc+" failed: "+e.getMessage(), e); } catch (ObjectNotFoundException e) { LOGGER.error("Evaluation of {} failed: {}", new Object[]{shortDesc, e.getMessage(), e}); throw new ObjectNotFoundException("Evaluation of "+shortDesc+" failed: "+e.getMessage(), e); } catch (SchemaException e) { LOGGER.error("Evaluation of {} failed: {}", new Object[]{shortDesc, e.getMessage(), e}); throw new SchemaException("Evaluation of "+shortDesc+" failed: "+e.getMessage(), e); } } } } } // Execute registered Java hooks HookOperationMode resultMode = HookOperationMode.FOREGROUND; if (hookRegistry != null) { for (ChangeHook hook : hookRegistry.getAllChangeHooks()) { HookOperationMode mode = hook.invoke(context, task, result); if (mode == HookOperationMode.ERROR) { resultMode = HookOperationMode.ERROR; } else if (mode == HookOperationMode.BACKGROUND) { if (resultMode != HookOperationMode.ERROR) { resultMode = HookOperationMode.BACKGROUND; } } } } return resultMode; } private void evaluateScriptingHook(LensContext context, HookType hookType, ScriptExpressionEvaluatorType scriptExpressionEvaluatorType, String shortDesc, Task task, OperationResult result) throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException { LOGGER.trace("Evaluating {}", shortDesc); // TODO: it would be nice to cache this // null output definition: this script has no output ScriptExpression scriptExpression = scriptExpressionFactory.createScriptExpression(scriptExpressionEvaluatorType, null, shortDesc); ExpressionVariables variables = new ExpressionVariables(); variables.addVariableDefinition(ExpressionConstants.VAR_PRISM_CONTEXT, prismContext); variables.addVariableDefinition(ExpressionConstants.VAR_MODEL_CONTEXT, context); LensFocusContext focusContext = context.getFocusContext(); PrismObject focus = null; if (focusContext != null) { focus = focusContext.getObjectAny(); } variables.addVariableDefinition(ExpressionConstants.VAR_FOCUS, focus); Utils.evaluateScript(scriptExpression, context, variables, false, shortDesc, task, result); } private <F extends ObjectType> void processInitialToPrimary(LensContext<F> context, Task task, OperationResult result) { // Context loaded, nothing special do. Bump state to PRIMARY. context.setState(ModelState.PRIMARY); } private <F extends ObjectType> void processPrimaryToSecondary(LensContext<F> context, Task task, OperationResult result) { // Nothing to do now. The context is already recomputed. context.setState(ModelState.SECONDARY); } private <F extends ObjectType> void processSecondary(LensContext<F> context, Task task, OperationResult result) throws ObjectAlreadyExistsException, ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException, SecurityViolationException, ExpressionEvaluationException, PolicyViolationException { if (context.getExecutionWave() > context.getMaxWave() + 1) { context.setState(ModelState.FINAL); return; } Holder<Boolean> restartRequestedHolder = new Holder<>(); LensUtil.partialExecute("execution", () -> { boolean restartRequested = changeExecutor.executeChanges(context, task, result); restartRequestedHolder.setValue(restartRequested); }, context.getPartialProcessingOptions()::getExecution); audit(context, AuditEventStage.EXECUTION, task, result); rotContext(context); boolean restartRequested = false; if (restartRequestedHolder.getValue() != null) { restartRequested = restartRequestedHolder.getValue(); } if (!restartRequested) { // TODO what if restart is requested indefinitely? context.incrementExecutionWave(); } LensUtil.traceContext(LOGGER, "CLOCKWORK (" + context.getState() + ")", "change execution", false, context, false); } /** * Force recompute for the next wave. Recompute only those contexts that were changed. * This is more inteligent than context.rot() */ private <F extends ObjectType> void rotContext(LensContext<F> context) throws SchemaException { boolean rot = false; for (LensProjectionContext projectionContext: context.getProjectionContexts()) { if (projectionContext.getWave() != context.getExecutionWave()) { LOGGER.trace("Context rot: projection {} NOT rotten because of wrong wave number", projectionContext); continue; } // if (!projectionContext.isDoReconciliation()) { // meaning volatility is NONE // LOGGER.trace("Context rot: projection {} NOT rotten because the resource is non-volatile", projectionContext); // continue; // } ObjectDelta<ShadowType> execDelta = projectionContext.getExecutableDelta(); if (isSignificant(execDelta)) { LOGGER.trace("Context rot: projection {} rotten because of delta {}", projectionContext, execDelta); projectionContext.setFresh(false); projectionContext.setFullShadow(false); rot = true; // Propagate to higher-order projections for (LensProjectionContext relCtx: LensUtil.findRelatedContexts(context, projectionContext)) { relCtx.setFresh(false); relCtx.setFullShadow(false); } } else { LOGGER.trace("Context rot: projection {} NOT rotten because no delta", projectionContext); } } LensFocusContext<F> focusContext = context.getFocusContext(); if (focusContext != null) { ObjectDelta<F> execDelta = focusContext.getWaveDelta(context.getExecutionWave()); if (execDelta != null && !execDelta.isEmpty()) { rot = true; } if (rot) { // It is OK to refresh focus all the time there was any change. This is cheap. focusContext.setFresh(false); } //remove secondary deltas from other than execution wave - we need to recompute them.. // cleanUpSecondaryDeltas(context); } if (rot) { context.setFresh(false); } } // // TODO this is quite unclear. Originally here was keeping the delta from the current wave (plus delta from wave #1). // // The reason was not clear. // // Let us erase everything. // private <F extends ObjectType> void cleanUpSecondaryDeltas(LensContext<F> context){ // LensFocusContext focusContext = context.getFocusContext(); // ObjectDeltaWaves<F> executionWaveDeltaList = focusContext.getSecondaryDeltas(); // executionWaveDeltaList.clear(); // } private <P extends ObjectType> boolean isSignificant(ObjectDelta<P> delta) { if (delta == null || delta.isEmpty()) { return false; } if (delta.isAdd() || delta.isDelete()) { return true; } Collection<? extends ItemDelta<?,?>> attrDeltas = delta.findItemDeltasSubPath(new ItemPath(ShadowType.F_ATTRIBUTES)); if (attrDeltas != null && !attrDeltas.isEmpty()) { return true; } return false; } private <F extends ObjectType> HookOperationMode processFinal(LensContext<F> context, Task task, OperationResult result) throws ObjectAlreadyExistsException, ObjectNotFoundException, SchemaException, CommunicationException, ConfigurationException, SecurityViolationException, ExpressionEvaluationException, PolicyViolationException { auditFinalExecution(context, task, result); logFinalReadable(context, task, result); recordOperationExecution(context, null, task, result); HookOperationMode opmode = personaProcessor.processPersonaChanges(context, task, result); if (opmode == HookOperationMode.BACKGROUND) { return opmode; } return triggerReconcileAffected(context, task, result); } private <F extends ObjectType> void recordOperationExecution(LensContext<F> context, Throwable clockworkException, Task task, OperationResult result) throws SchemaException, ObjectAlreadyExistsException, ObjectNotFoundException { XMLGregorianCalendar now = clock.currentTimeXMLGregorianCalendar(); try { recordFocusOperationExecution(context, now, clockworkException, task, result); for (LensProjectionContext projectionContext : context.getProjectionContexts()) { recordProjectionOperationExecution(context, projectionContext, now, task, result); } } catch (Throwable t) { LoggingUtils.logUnexpectedException(LOGGER, "Couldn't record operation execution. Model context:\n{}", t, context.debugDump()); // Let us ignore this for the moment. It should not have happened, sure. But it's not that crucial. // Administrator will be able to learn about the problem from the log. } } private <F extends ObjectType> void recordFocusOperationExecution(LensContext<F> context, XMLGregorianCalendar now, Throwable clockworkException, Task task, OperationResult result) throws SchemaException, ObjectAlreadyExistsException, ObjectNotFoundException { LensFocusContext<F> focusContext = context.getFocusContext(); if (focusContext == null || focusContext.isDelete()) { return; } PrismObject<F> objectNew = focusContext.getObjectNew(); Validate.notNull(objectNew, "No focus object even if the context is not of 'delete' type"); List<LensObjectDeltaOperation<F>> executedDeltas; if (clockworkException == null) { executedDeltas = focusContext.getExecutedDeltas(); } else { executedDeltas = new ArrayList<>(focusContext.getExecutedDeltas()); LensObjectDeltaOperation<F> odo = new LensObjectDeltaOperation<>(); ObjectDelta<F> primaryDelta = focusContext.getPrimaryDelta(); if (primaryDelta != null) { odo.setObjectDelta(primaryDelta); } else { @SuppressWarnings({"unchecked", "raw"}) Class<F> fClass = (Class<F>) objectNew.asObjectable().getClass(); ObjectDelta<F> fakeDelta = new ObjectDelta<>(fClass, ChangeType.MODIFY, prismContext); odo.setObjectDelta(fakeDelta); } odo.setExecutionResult(result); // we rely on the fact that 'result' already contains record of the exception executedDeltas.add(odo); } recordOperationExecution(objectNew, false, executedDeltas, now, context.getChannel(), task, result); } private <F extends ObjectType> void recordProjectionOperationExecution(LensContext<F> context, LensProjectionContext projectionContext, XMLGregorianCalendar now, Task task, OperationResult result) throws SchemaException, ObjectAlreadyExistsException, ObjectNotFoundException { PrismObject<ShadowType> object = projectionContext.getObjectAny(); if (object == null) { return; // this can happen } recordOperationExecution(object, true, projectionContext.getExecutedDeltas(), now, context.getChannel(), task, result); } private <F extends ObjectType> void recordOperationExecution(PrismObject<F> object, boolean deletedOk, List<LensObjectDeltaOperation<F>> executedDeltas, XMLGregorianCalendar now, String channel, Task task, OperationResult result) throws ObjectAlreadyExistsException, ObjectNotFoundException, SchemaException { OperationExecutionType operation = new OperationExecutionType(prismContext); OperationResult summaryResult = new OperationResult("dummy"); String oid = object.getOid(); for (LensObjectDeltaOperation<F> deltaOperation : executedDeltas) { operation.getOperation().add(createObjectDeltaOperation(deltaOperation)); if (deltaOperation.getExecutionResult() != null) { summaryResult.addSubresult(deltaOperation.getExecutionResult()); } if (oid == null && deltaOperation.getObjectDelta() != null) { oid = deltaOperation.getObjectDelta().getOid(); } } if (oid == null) { // e.g. if there is an exception in provisioning.addObject method return; } summaryResult.computeStatus(); OperationResultStatusType overallStatus = summaryResult.getStatus().createStatusType(); setOperationContext(operation, overallStatus, now, channel, task); storeOperationExecution(object, oid, operation, deletedOk, result); } private <F extends ObjectType> void storeOperationExecution(@NotNull PrismObject<F> object, @NotNull String oid, @NotNull OperationExecutionType executionToAdd, boolean deletedOk, OperationResult result) throws SchemaException, ObjectAlreadyExistsException, ObjectNotFoundException { Integer recordsToKeep; Long deleteBefore; PrismObject<SystemConfigurationType> systemConfiguration = systemObjectCache.getSystemConfiguration(result); if (systemConfiguration != null && systemConfiguration.asObjectable().getCleanupPolicy() != null && systemConfiguration.asObjectable().getCleanupPolicy().getObjectResults() != null) { CleanupPolicyType policy = systemConfiguration.asObjectable().getCleanupPolicy().getObjectResults(); recordsToKeep = policy.getMaxRecords(); if (policy.getMaxAge() != null) { XMLGregorianCalendar limit = XmlTypeConverter.addDuration( XmlTypeConverter.createXMLGregorianCalendar(new Date()), policy.getMaxAge().negate()); deleteBefore = XmlTypeConverter.toMillis(limit); } else { deleteBefore = null; } } else { recordsToKeep = DEFAULT_NUMBER_OF_RESULTS_TO_KEEP; deleteBefore = null; } List<OperationExecutionType> executionsToDelete = new ArrayList<>(); List<OperationExecutionType> executions = new ArrayList<>(object.asObjectable().getOperationExecution()); // delete all executions related to current task and all old ones String taskOid = executionToAdd.getTaskRef() != null ? executionToAdd.getTaskRef().getOid() : null; for (Iterator<OperationExecutionType> iterator = executions.iterator(); iterator.hasNext(); ) { OperationExecutionType execution = iterator.next(); if (taskOid != null && execution.getTaskRef() != null && taskOid.equals(execution.getTaskRef().getOid()) || deleteBefore != null && XmlTypeConverter.toMillis(execution.getTimestamp()) < deleteBefore) { executionsToDelete.add(execution); iterator.remove(); } } // delete all surplus executions if (recordsToKeep != null && object.asObjectable().getOperationExecution().size() > recordsToKeep - 1) { executions.sort(Comparator.nullsFirst(Comparator.comparing(e -> XmlTypeConverter.toDate(e.getTimestamp())))); executionsToDelete.addAll(executions.subList(0, executions.size() - (recordsToKeep - 1))); } // construct and execute the delta Class<? extends ObjectType> objectClass = object.asObjectable().getClass(); List<ItemDelta<?, ?>> deltas = DeltaBuilder.deltaFor(objectClass, prismContext) .item(ObjectType.F_OPERATION_EXECUTION) .add(executionToAdd) .delete(PrismContainerValue.toPcvList(CloneUtil.cloneCollectionMembers(executionsToDelete))) .asItemDeltas(); LOGGER.trace("Operation execution delta:\n{}", DebugUtil.debugDumpLazily(deltas)); try { repositoryService.modifyObject(objectClass, oid, deltas, result); } catch (ObjectNotFoundException e) { if (!deletedOk) { throw e; } else { LOGGER.trace("Object {} deleted but this was expected.", oid); result.deleteLastSubresultIfError(); } } } private void setOperationContext(OperationExecutionType operation, OperationResultStatusType overallStatus, XMLGregorianCalendar now, String channel, Task task) { if (task.isPersistent()) { operation.setTaskRef(ObjectTypeUtil.createObjectRef(task.getTaskPrismObject())); } operation.setStatus(overallStatus); operation.setInitiatorRef(ObjectTypeUtil.createObjectRef(task.getOwner())); // TODO what if the real initiator is different? (e.g. when executing approved changes) operation.setChannel(channel); operation.setTimestamp(now); } private <F extends ObjectType> ObjectDeltaOperationType createObjectDeltaOperation(LensObjectDeltaOperation<F> deltaOperation) { ObjectDeltaOperationType odo; try { odo = simplifyOperation(deltaOperation).toLensObjectDeltaOperationType().getObjectDeltaOperation(); } catch (SchemaException e) { LoggingUtils.logUnexpectedException(LOGGER, "Couldn't create operation information", e); odo = new ObjectDeltaOperationType(); OperationResult r = new OperationResult(Clockwork.class.getName() + ".createObjectDeltaOperation"); r.recordFatalError("Couldn't create operation information: " + e.getMessage(), e); odo.setExecutionResult(r.createOperationResultType()); } return odo; } private <F extends ObjectType> LensObjectDeltaOperation<F> simplifyOperation(ObjectDeltaOperation<F> operation) { LensObjectDeltaOperation<F> rv = new LensObjectDeltaOperation<>(); rv.setObjectDelta(simplifyDelta(operation.getObjectDelta())); rv.setExecutionResult(simplifyResult(operation.getExecutionResult())); rv.setObjectName(operation.getObjectName()); rv.setResourceName(operation.getResourceName()); rv.setResourceOid(operation.getResourceOid()); return rv; } private OperationResult simplifyResult(OperationResult result) { return new OperationResult(result.getOperation(), result.getStatus(), result.getMessageCode(), result.getMessage()); } private <F extends ObjectType> ObjectDelta<F> simplifyDelta(ObjectDelta<F> delta) { return new ObjectDelta<>(delta.getObjectTypeClass(), delta.getChangeType(), prismContext); } private <F extends ObjectType> HookOperationMode triggerReconcileAffected(LensContext<F> context, Task task, OperationResult result) throws SchemaException { // check applicability if (!ModelExecuteOptions.isReconcileAffected(context.getOptions())) { return HookOperationMode.FOREGROUND; } if (context.getFocusClass() == null || !RoleType.class.isAssignableFrom(context.getFocusClass())) { LOGGER.warn("ReconcileAffected requested but not available for {}. Doing nothing.", context.getFocusClass()); return HookOperationMode.FOREGROUND; } // check preconditions if (context.getFocusContext() == null) { throw new IllegalStateException("No focus context when expected it"); } PrismObject<RoleType> role = (PrismObject) context.getFocusContext().getObjectAny(); if (role == null) { throw new IllegalStateException("No role when expected it"); } // preparing the recompute/reconciliation task Task reconTask; if (task.isPersistent()) { reconTask = task.createSubtask(); } else { reconTask = task; } assert !reconTask.isPersistent(); // creating object query PrismPropertyDefinition propertyDef = prismContext.getSchemaRegistry() .findPropertyDefinitionByElementName(SchemaConstants.MODEL_EXTENSION_OBJECT_QUERY); PrismReferenceValue referenceValue = new PrismReferenceValue(context.getFocusContext().getOid(), RoleType.COMPLEX_TYPE); ObjectFilter refFilter = QueryBuilder.queryFor(FocusType.class, prismContext) .item(FocusType.F_ASSIGNMENT, AssignmentType.F_TARGET_REF).ref(referenceValue) .buildFilter(); SearchFilterType filterType = QueryConvertor.createSearchFilterType(refFilter, prismContext); QueryType queryType = new QueryType(); queryType.setFilter(filterType); PrismProperty<QueryType> property = propertyDef.instantiate(); property.setRealValue(queryType); reconTask.addExtensionProperty(property); // other parameters reconTask.setName("Recomputing users after changing role " + role.asObjectable().getName()); reconTask.setBinding(TaskBinding.LOOSE); reconTask.setInitialExecutionStatus(TaskExecutionStatus.RUNNABLE); reconTask.setHandlerUri(RecomputeTaskHandler.HANDLER_URI); reconTask.setCategory(TaskCategory.RECOMPUTATION); taskManager.switchToBackground(reconTask, result); result.setBackgroundTaskOid(reconTask.getOid()); result.recordStatus(OperationResultStatus.IN_PROGRESS, "Reconciliation task switched to background"); return HookOperationMode.BACKGROUND; } private <F extends ObjectType> void audit(LensContext<F> context, AuditEventStage stage, Task task, OperationResult result) throws SchemaException { if (context.isLazyAuditRequest()) { if (stage == AuditEventStage.REQUEST) { // We skip auditing here, we will do it before execution } else if (stage == AuditEventStage.EXECUTION) { Collection<ObjectDeltaOperation<? extends ObjectType>> unauditedExecutedDeltas = context.getUnauditedExecutedDeltas(); if ((unauditedExecutedDeltas == null || unauditedExecutedDeltas.isEmpty())) { // No deltas, nothing to audit in this wave return; } if (!context.isRequestAudited()) { auditEvent(context, AuditEventStage.REQUEST, context.getStats().getRequestTimestamp(), false, task, result); } auditEvent(context, stage, null, false, task, result); } } else { auditEvent(context, stage, null, false, task, result); } } /** * Make sure that at least one execution is audited if a request was already audited. We don't want * request without execution in the audit logs. */ private <F extends ObjectType> void auditFinalExecution(LensContext<F> context, Task task, OperationResult result) throws SchemaException { if (!context.isRequestAudited()) { return; } if (context.isExecutionAudited()) { return; } auditEvent(context, AuditEventStage.EXECUTION, null, true, task, result); } private <F extends ObjectType> void processClockworkException(LensContext<F> context, Exception e, Task task, OperationResult result) throws SchemaException, ObjectAlreadyExistsException, ObjectNotFoundException { LOGGER.trace("Processing clockwork exception {}", e.toString()); result.recordFatalError(e); auditEvent(context, AuditEventStage.EXECUTION, null, true, task, result); recordOperationExecution(context, e, task, result); reclaimSequences(context, task, result); } private <F extends ObjectType> void auditEvent(LensContext<F> context, AuditEventStage stage, XMLGregorianCalendar timestamp, boolean alwaysAudit, Task task, OperationResult result) throws SchemaException { PrismObject<? extends ObjectType> primaryObject; ObjectDelta<? extends ObjectType> primaryDelta; if (context.getFocusContext() != null) { primaryObject = context.getFocusContext().getObjectOld(); if (primaryObject == null) { primaryObject = context.getFocusContext().getObjectNew(); } primaryDelta = context.getFocusContext().getDelta(); } else { Collection<LensProjectionContext> projectionContexts = context.getProjectionContexts(); if (projectionContexts == null || projectionContexts.isEmpty()) { throw new IllegalStateException("No focus and no projections in "+context); } if (projectionContexts.size() > 1) { throw new IllegalStateException("No focus and more than one projection in "+context); } LensProjectionContext projection = projectionContexts.iterator().next(); primaryObject = projection.getObjectOld(); if (primaryObject == null) { primaryObject = projection.getObjectNew(); } primaryDelta = projection.getDelta(); } AuditEventType eventType; if (primaryDelta == null) { eventType = AuditEventType.SYNCHRONIZATION; } else if (primaryDelta.isAdd()) { eventType = AuditEventType.ADD_OBJECT; } else if (primaryDelta.isModify()) { eventType = AuditEventType.MODIFY_OBJECT; } else if (primaryDelta.isDelete()) { eventType = AuditEventType.DELETE_OBJECT; } else { throw new IllegalStateException("Unknown state of delta "+primaryDelta); } AuditEventRecord auditRecord = new AuditEventRecord(eventType, stage); if (primaryObject != null) { auditRecord.setTarget(primaryObject.clone()); // } else { // throw new IllegalStateException("No primary object in:\n"+context.dump()); } auditRecord.setChannel(context.getChannel()); if (stage == AuditEventStage.REQUEST) { Collection<ObjectDeltaOperation<? extends ObjectType>> clonedDeltas = ObjectDeltaOperation.cloneDeltaCollection(context.getPrimaryChanges()); checkNamesArePresent(clonedDeltas, primaryObject); auditRecord.addDeltas(clonedDeltas); if (auditRecord.getTarget() == null) { auditRecord.setTarget(Utils.determineAuditTargetDeltaOps(clonedDeltas)); } } else if (stage == AuditEventStage.EXECUTION) { auditRecord.setOutcome(result.getComputeStatus()); Collection<ObjectDeltaOperation<? extends ObjectType>> unauditedExecutedDeltas = context.getUnauditedExecutedDeltas(); if (!alwaysAudit && (unauditedExecutedDeltas == null || unauditedExecutedDeltas.isEmpty())) { // No deltas, nothing to audit in this wave return; } Collection<ObjectDeltaOperation<? extends ObjectType>> clonedDeltas = ObjectDeltaOperation.cloneCollection(unauditedExecutedDeltas); checkNamesArePresent(clonedDeltas, primaryObject); auditRecord.addDeltas(clonedDeltas); } else { throw new IllegalStateException("Unknown audit stage "+stage); } if (timestamp != null) { auditRecord.setTimestamp(XmlTypeConverter.toMillis(timestamp)); } addRecordMessage(auditRecord, result); auditService.audit(auditRecord, task); if (stage == AuditEventStage.EXECUTION) { // We need to clean up so these deltas will not be audited again in next wave context.markExecutedDeltasAudited(); context.setExecutionAudited(true); } else if (stage == AuditEventStage.REQUEST) { context.setRequestAudited(true); } else { throw new IllegalStateException("Unknown audit stage "+stage); } } private void checkNamesArePresent(Collection<ObjectDeltaOperation<? extends ObjectType>> deltas, PrismObject<? extends ObjectType> primaryObject) { if (primaryObject != null) { for (ObjectDeltaOperation<? extends ObjectType> delta : deltas) { if (delta.getObjectName() == null) { delta.setObjectName(primaryObject.getName()); } } } } /** * Adds a message to the record by pulling the messages from individual delta results. */ private void addRecordMessage(AuditEventRecord auditRecord, OperationResult result) { if (auditRecord.getMessage() != null) { return; } if (!StringUtils.isEmpty(result.getMessage())) { String message = result.getMessage(); auditRecord.setMessage(message); return; } Collection<ObjectDeltaOperation<? extends ObjectType>> deltas = auditRecord.getDeltas(); if (deltas == null || deltas.isEmpty()) { return; } StringBuilder sb = new StringBuilder(); for (ObjectDeltaOperation<? extends ObjectType> delta: deltas) { OperationResult executionResult = delta.getExecutionResult(); if (executionResult != null) { String message = executionResult.getMessage(); if (!StringUtils.isEmpty(message)) { if (sb.length() != 0) { sb.append("; "); } sb.append(message); } } } auditRecord.setMessage(sb.toString()); } public static void throwException(Throwable e) throws ObjectAlreadyExistsException, ObjectNotFoundException { if (e instanceof ObjectAlreadyExistsException) { throw (ObjectAlreadyExistsException)e; } else if (e instanceof ObjectNotFoundException) { throw (ObjectNotFoundException)e; } else if (e instanceof SystemException) { throw (SystemException)e; } else { throw new SystemException("Unexpected exception "+e.getClass()+" "+e.getMessage(), e); } } /** * Logs the entire operation in a human-readable fashion. */ private <F extends ObjectType> void logFinalReadable(LensContext<F> context, Task task, OperationResult result) throws SchemaException { if (!LOGGER.isDebugEnabled()) { return; } // a priori: sync delta boolean hasSyncDelta = false; for (LensProjectionContext projectionContext: context.getProjectionContexts()) { ObjectDelta<ShadowType> syncDelta = projectionContext.getSyncDelta(); if (syncDelta != null) { hasSyncDelta = true; } } Collection<ObjectDeltaOperation<? extends ObjectType>> executedDeltas = context.getExecutedDeltas(); if (!hasSyncDelta && executedDeltas == null || executedDeltas.isEmpty()) { // Not worth mentioning return; } StringBuilder sb = new StringBuilder(); String channel = context.getChannel(); if (channel != null) { sb.append("Channel: ").append(channel).append("\n"); } if (hasSyncDelta) { sb.append("Triggered by synchronization delta\n"); for (LensProjectionContext projectionContext: context.getProjectionContexts()) { ObjectDelta<ShadowType> syncDelta = projectionContext.getSyncDelta(); if (syncDelta != null) { sb.append(syncDelta.debugDump(1)); sb.append("\n"); } DebugUtil.debugDumpLabel(sb, "Situation", 1); sb.append(" "); sb.append(projectionContext.getSynchronizationSituationDetected()); sb.append(" -> "); sb.append(projectionContext.getSynchronizationSituationResolved()); sb.append("\n"); } } for (LensProjectionContext projectionContext: context.getProjectionContexts()) { if (projectionContext.isSyncAbsoluteTrigger()) { sb.append("Triggered by absolute state of ").append(projectionContext.getHumanReadableName()); sb.append(": "); sb.append(projectionContext.getSynchronizationSituationDetected()); sb.append(" -> "); sb.append(projectionContext.getSynchronizationSituationResolved()); sb.append("\n"); } } // focus primary LensFocusContext<F> focusContext = context.getFocusContext(); if (focusContext != null) { ObjectDelta<F> focusPrimaryDelta = focusContext.getPrimaryDelta(); if (focusPrimaryDelta != null) { sb.append("Triggered by focus primary delta\n"); DebugUtil.indentDebugDump(sb, 1); sb.append(focusPrimaryDelta.toString()); sb.append("\n"); } } // projection primary Collection<ObjectDelta<ShadowType>> projPrimaryDeltas = new ArrayList<ObjectDelta<ShadowType>>(); for (LensProjectionContext projectionContext: context.getProjectionContexts()) { ObjectDelta<ShadowType> projPrimaryDelta = projectionContext.getPrimaryDelta(); if (projPrimaryDelta != null) { projPrimaryDeltas.add(projPrimaryDelta); } } if (!projPrimaryDeltas.isEmpty()) { sb.append("Triggered by projection primary delta\n"); for (ObjectDelta<ShadowType> projDelta: projPrimaryDeltas) { DebugUtil.indentDebugDump(sb, 1); sb.append(projDelta.toString()); sb.append("\n"); } } if (focusContext != null) { sb.append("Focus: ").append(focusContext.getHumanReadableName()).append("\n"); } if (!context.getProjectionContexts().isEmpty()) { sb.append("Projections (").append(context.getProjectionContexts().size()).append("):\n"); for (LensProjectionContext projectionContext: context.getProjectionContexts()) { DebugUtil.indentDebugDump(sb, 1); sb.append(projectionContext.getHumanReadableName()); sb.append(": "); sb.append(projectionContext.getSynchronizationPolicyDecision()); sb.append("\n"); } } if (executedDeltas == null || executedDeltas.isEmpty()) { sb.append("Executed: nothing\n"); } else { sb.append("Executed:\n"); for (ObjectDeltaOperation<? extends ObjectType> executedDelta: executedDeltas) { ObjectDelta<? extends ObjectType> delta = executedDelta.getObjectDelta(); OperationResult deltaResult = executedDelta.getExecutionResult(); DebugUtil.indentDebugDump(sb, 1); sb.append(delta.toString()); sb.append(": "); sb.append(deltaResult.getStatus()); sb.append("\n"); } } LOGGER.debug("\n###[ CLOCKWORK SUMMARY ]######################################\n{}" + "##############################################################", sb.toString()); } private <F extends ObjectType> void authorizeContextRequest(LensContext<F> context, Task task, OperationResult parentResult) throws SecurityViolationException, SchemaException { OperationResult result = parentResult.createMinorSubresult(Clockwork.class.getName()+".authorizeRequest"); try { final LensFocusContext<F> focusContext = context.getFocusContext(); OwnerResolver ownerResolver = new LensOwnerResolver<>(context, objectResolver, task, result); if (focusContext != null) { authorizeElementContext(context, focusContext, ownerResolver, true, task, result); } for (LensProjectionContext projectionContext: context.getProjectionContexts()) { authorizeElementContext(context, projectionContext, ownerResolver, false, task, result); } context.setRequestAuthorized(true); result.recordSuccess(); } catch (SecurityViolationException | SchemaException e) { result.recordFatalError(e); throw e; } } private <F extends ObjectType, O extends ObjectType> ObjectSecurityConstraints authorizeElementContext(LensContext<F> context, LensElementContext<O> elementContext, OwnerResolver ownerResolver, boolean isFocus, Task task, OperationResult result) throws SecurityViolationException, SchemaException { ObjectDelta<O> primaryDelta = elementContext.getPrimaryDelta(); // If there is no delta then there is no request to authorize if (primaryDelta != null) { primaryDelta = primaryDelta.clone(); PrismObject<O> object = elementContext.getObjectNew(); if (primaryDelta.isDelete()) { object = elementContext.getObjectCurrent(); } String operationUrl = ModelUtils.getOperationUrlFromDelta(primaryDelta); ObjectSecurityConstraints securityConstraints = securityEnforcer.compileSecurityConstraints(object, ownerResolver); if (securityConstraints == null) { throw new AuthorizationException("Access denied"); } if (isFocus) { // Process assignments first. If the assignments are allowed then we // have to ignore the assignment item in subsequent security checks ContainerDelta<Containerable> assignmentDelta = primaryDelta.findContainerDelta(FocusType.F_ASSIGNMENT); if (assignmentDelta != null) { AuthorizationDecisionType assignmentItemDecision = securityConstraints.findItemDecision(new ItemPath(FocusType.F_ASSIGNMENT), operationUrl, getRequestAuthorizationPhase(context)); if (assignmentItemDecision == AuthorizationDecisionType.ALLOW) { // Nothing to do, operation is allowed for all values } else if (assignmentItemDecision == AuthorizationDecisionType.DENY) { throw new AuthorizationException("Access denied"); } else { AuthorizationDecisionType actionDecision = securityConstraints.getActionDecision(operationUrl, getRequestAuthorizationPhase(context)); if (actionDecision == AuthorizationDecisionType.ALLOW) { // Nothing to do, operation is allowed for all values } else if (actionDecision == AuthorizationDecisionType.DENY) { throw new AuthorizationException("Access denied"); } else { // No explicit decision for assignment modification yet // process each assignment individually DeltaSetTriple<EvaluatedAssignmentImpl<?>> evaluatedAssignmentTriple = context.getEvaluatedAssignmentTriple(); authorizeAssignmentRequest(context, ModelAuthorizationAction.ASSIGN.getUrl(), object, ownerResolver, evaluatedAssignmentTriple.getPlusSet(), true, result); // We want to allow unassignment even if there are policies. Otherwise we would not be able to get // rid of that assignment authorizeAssignmentRequest(context, ModelAuthorizationAction.UNASSIGN.getUrl(), object, ownerResolver, evaluatedAssignmentTriple.getMinusSet(), false, result); } } // assignments were authorized explicitly. Therefore we need to remove them from primary delta to avoid another // authorization if (primaryDelta.isAdd()) { PrismObject<O> objectToAdd = primaryDelta.getObjectToAdd(); objectToAdd.removeContainer(FocusType.F_ASSIGNMENT); } else if (primaryDelta.isModify()) { primaryDelta.removeContainerModification(FocusType.F_ASSIGNMENT); } } } // Process credential changes explicitly. There is a special authorization for that. if (!primaryDelta.isDelete()) { if (primaryDelta.isAdd()) { PrismObject<O> objectToAdd = primaryDelta.getObjectToAdd(); PrismContainer<CredentialsType> credentialsContainer = objectToAdd.findContainer(UserType.F_CREDENTIALS); if (credentialsContainer != null) { for (Item<?,?> item: credentialsContainer.getValue().getItems()) { ContainerDelta<?> cdelta = new ContainerDelta(item.getPath(), (PrismContainerDefinition)item.getDefinition(), prismContext); cdelta.addValuesToAdd(((PrismContainer)item).getValue().clone()); AuthorizationDecisionType cdecision = evaluateCredentialDecision(context, securityConstraints, cdelta); LOGGER.trace("AUTZ: credential add {} decision: {}", item.getPath(), cdecision); if (cdecision == AuthorizationDecisionType.ALLOW) { // Remove it from primary delta, so it will not be evaluated later objectToAdd.removeContainer(item.getPath()); } else if (cdecision == AuthorizationDecisionType.DENY) { throw new AuthorizationException("Access denied"); } else { // Do nothing. The access will be evaluated later in a normal way } } } } else { // modify Collection<? extends ItemDelta<?, ?>> credentialChanges = primaryDelta.findItemDeltasSubPath(new ItemPath(UserType.F_CREDENTIALS)); for (ItemDelta credentialChange: credentialChanges) { AuthorizationDecisionType cdecision = evaluateCredentialDecision(context, securityConstraints, credentialChange); LOGGER.trace("AUTZ: credential delta {} decision: {}", credentialChange.getPath(), cdecision); if (cdecision == AuthorizationDecisionType.ALLOW) { // Remove it from primary delta, so it will not be evaluated later primaryDelta.removeModification(credentialChange); } else if (cdecision == AuthorizationDecisionType.DENY) { throw new AuthorizationException("Access denied"); } else { // Do nothing. The access will be evaluated later in a normal way } } } } if (primaryDelta != null && !primaryDelta.isEmpty()) { // TODO: optimize, avoid evaluating the constraints twice securityEnforcer.authorize(operationUrl, getRequestAuthorizationPhase(context) , object, primaryDelta, null, ownerResolver, result); } return securityConstraints; } else { return null; } } private <F extends ObjectType> AuthorizationPhaseType getRequestAuthorizationPhase(LensContext<F> context) { if (context.isExecutionPhaseOnly()) { return AuthorizationPhaseType.EXECUTION; } else { return AuthorizationPhaseType.REQUEST; } } private <F extends ObjectType> AuthorizationDecisionType evaluateCredentialDecision(LensContext<F> context, ObjectSecurityConstraints securityConstraints, ItemDelta credentialChange) { return securityConstraints.findItemDecision(credentialChange.getPath(), ModelAuthorizationAction.CHANGE_CREDENTIALS.getUrl(), getRequestAuthorizationPhase(context)); } private <F extends ObjectType,O extends ObjectType> void authorizeAssignmentRequest(LensContext<F> context, String assignActionUrl, PrismObject<O> object, OwnerResolver ownerResolver, Collection<EvaluatedAssignmentImpl<?>> evaluatedAssignments, boolean prohibitPolicies, OperationResult result) throws SecurityViolationException, SchemaException { if (evaluatedAssignments == null) { return; } for (EvaluatedAssignment<?> evaluatedAssignment: evaluatedAssignments) { PrismObject target = evaluatedAssignment.getTarget(); if (prohibitPolicies) { AssignmentType assignmentType = evaluatedAssignment.getAssignmentType(); if (assignmentType.getPolicyRule() != null || !assignmentType.getPolicyException().isEmpty() || !assignmentType.getPolicySituation().isEmpty()) { securityEnforcer.failAuthorization("with assignment because of policies in the assignment", getRequestAuthorizationPhase(context), object, null, target, result); } } ObjectDelta<O> assignmentObjectDelta = object.createModifyDelta(); ContainerDelta<AssignmentType> assignmentDelta = assignmentObjectDelta.createContainerModification(FocusType.F_ASSIGNMENT); // We do not care if this is add or delete. All that matters for authorization is that it is in a delta. assignmentDelta.addValuesToAdd(evaluatedAssignment.getAssignmentType().asPrismContainerValue().clone()); if (securityEnforcer.isAuthorized(assignActionUrl, getRequestAuthorizationPhase(context), object, assignmentObjectDelta, target, ownerResolver)) { LOGGER.trace("Operation authorized with {} authorization", assignActionUrl); continue; } QName relation = evaluatedAssignment.getRelation(); if (ObjectTypeUtil.isDelegationRelation(relation)) { if (securityEnforcer.isAuthorized(ModelAuthorizationAction.DELEGATE.getUrl(), getRequestAuthorizationPhase(context), object, assignmentObjectDelta, target, ownerResolver)) { LOGGER.trace("Operation authorized with {} authorization", ModelAuthorizationAction.DELEGATE.getUrl()); continue; } } securityEnforcer.failAuthorization("with assignment", getRequestAuthorizationPhase(context), object, null, target, result); } } private <F extends ObjectType> void reclaimSequences(LensContext<F> context, Task task, OperationResult result) throws SchemaException { Map<String, Long> sequenceMap = context.getSequences(); LOGGER.trace("Context sequence map: {}", sequenceMap); for (Entry<String, Long> sequenceMapEntry: sequenceMap.entrySet()) { Collection<Long> unusedValues = new ArrayList<>(1); unusedValues.add(sequenceMapEntry.getValue()); try { LOGGER.trace("Returning value {} to sequence {}", sequenceMapEntry.getValue(), sequenceMapEntry.getKey()); repositoryService.returnUnusedValuesToSequence(sequenceMapEntry.getKey(), unusedValues, result); } catch (ObjectNotFoundException e) { LOGGER.error("Cannot return unused value to sequence {}: it does not exist", sequenceMapEntry.getKey(), e); // ... but otherwise ignore it and go on } } } }