Skip to content

Commit

Permalink
Merge branch 'tmp/post-4.8-fixes'
Browse files Browse the repository at this point in the history
  • Loading branch information
mederly committed Oct 17, 2023
2 parents ff9a20b + cacb9ee commit a5cdb55
Show file tree
Hide file tree
Showing 13 changed files with 544 additions and 233 deletions.
127 changes: 127 additions & 0 deletions docs/cases/authorizations.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
= Case Management Authorizations

== Model level

Case management authorization deals with _cases_ (`CaseType` objects) and their constituents, the most prominent being _work items_ residing in `workItem` container.

These objects are acted upon by the following actions:

.Relevant actions
[%autowidth]
|===
| Action | Description | Related objects

| `<prefix>#read`
| Getting objects, searching objects, counting objects and so on.
| cases and their parts (including work items)

| `<prefix>#completeWorkItem`
| Completing (e.g., approving and rejecting) individual work items.
| work items

| `<prefix>#delegateWorkItem`
| Delegating individual work items.
| work items
|===

`*<prefix>*` is the standard prefix denoting authorization actions at the model level, i.e. `http://midpoint.evolveum.com/xml/ns/public/security/authorization-model-3`.

The following is an example of these authorizations:

.Listing 1: Standard authorizations for an approver
[source,xml]
----
<authorization>
<name>own-workitems-read-complete-delegate</name>
<description>
Allows reading, completion, and delegation of own work items.
</description>
<action>http://midpoint.evolveum.com/xml/ns/public/security/authorization-model-3#read</action>
<action>http://midpoint.evolveum.com/xml/ns/public/security/authorization-model-3#completeWorkItem</action>
<action>http://midpoint.evolveum.com/xml/ns/public/security/authorization-model-3#delegateWorkItem</action>
<object>
<parent>
<type>CaseType</type>
<path>workItem</path>
</parent>
<assignee>
<special>self</special>
</assignee>
</object>
</authorization>
<authorization>
<name>cases-read</name>
<description>
Allows to see parts of the cases containing work items assigned to the user.
</description>
<action>http://midpoint.evolveum.com/xml/ns/public/security/authorization-model-3#read</action>
<object>
<type>CaseType</type>
<assignee>
<special>self</special>
</assignee>
</object>
<exceptItem>workItem</exceptItem>
</authorization>
----

The first authorization (`own-workitems-read-complete-delegate`) allows its holder to get, search, and count _work items_ they have assigned.
The assignment can be direct or indirect, via delegation.
For example, if Alice is assigned a given work item, and Bob is a deputy of Alice, then Bob is also considered to be the assignee for the sake of the above authorization.

The second authorization (`cases-read`) allows its holder to get, search, and count _cases_ that are assigned to them, i.e. the cases where the user is an assignee of any work item in that particular case.

Note the `exceptItem` clause.
It makes sure that the user sees everything in the relevant case, except for work items.
But the first authorization ensures that they see their own work items.
Both authorizations combined ensure that the user sees the whole case, except for work items they are not assignee for.

=== Claiming and Releasing of Work Items

The "claim" and "release" operations do not require any special authorizations:
Any user who is a candidate actor for a work item can claim it.
Any user that is a sole assignee of a work item can release it, provided that there are any candidate users the work item can be offered to.

=== Deprecated Authorizations

Some of the legacy authorizations (like `#completeAllWorkItems`) were deprecated.
As described https://docs.evolveum.com/midpoint/devel/design/schema-cleanup-4.8/authorizations/[here], they should be replaced by their current equivalents.

== GUI level

At the GUI level, there are the following action URIs available.
`*<prefix>*` is the standard prefix for GUI authorization actions, i.e. `http://midpoint.evolveum.com/xml/ns/public/security/authorization-ui-3`.

[%autowidth]
|===
| GUI page | Action URI | Alternate action URI

| All cases
| <prefix>#cases
.4+| <prefix>#casesAll

| My cases (requested by me)
| <prefix>#casesView

| Specific case collections (e.g. All manual cases, All requests, All approvals)
| <prefix>#casesView

| Single case details
| <prefix>#case

| All work items
| <prefix>#allWorkItems
.5+| <prefix>#workItemsAll

| My work items
| <prefix>#myWorkItems

| Attorney items
| <prefix>#attorneyWorkItems

| Work items claimable by me
| <prefix>#claimableWorkItems

| Single work item details
| <prefix>#workItem
|===
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ public abstract class SchemaConstants {
// Case is open - work items are created, completed, delegated, etc. Associated work is carried out.
public static final String CASE_STATE_OPEN = "open";
public static final QName CASE_STATE_OPEN_QNAME = new QName(NS_CASE, CASE_STATE_OPEN);
public static final String CASE_STATE_OPEN_URI = qNameToUri(CASE_STATE_OPEN_QNAME);

// All human interaction regarding the case is over. But there might be some actions pending, e.g.
// submitting change execution task, waiting for subtasks to be closed, and so on.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,10 +478,15 @@
<xsd:annotation>
<xsd:documentation>
This stage is silently skipped. This is useful for situations where we don't even want to start
an approval process if there are no approvers in it.
an approval process if there are no approvers in it. (Hence, if there is a process with all stages
marked as "skip", it is skipped altogether.)

Skipping whole approval process is currently supported only partly: when using approver relations.
For approver expressions, these are always evaluated within context of a workflow process.
If all approval processes are to be skipped, the whole approval processing is skipped, and the operation
is carried out immediately.

However, if only some of the approval processed are to be skipped, deltas associated with them are
treated just like other deltas that go without approval: they are either executed immediately,
or executed after all the other approvals are made.
</xsd:documentation>
<xsd:appinfo>
<jaxb:typesafeEnumMember name="SKIP"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import com.evolveum.midpoint.authentication.api.AutheticationFailedData;

import com.evolveum.midpoint.authentication.api.util.AuthUtil;
import com.evolveum.midpoint.schema.util.cases.CaseTypeUtil;
import com.evolveum.midpoint.security.api.*;

import com.evolveum.midpoint.security.enforcer.api.ValueAuthorizationParameters;
Expand Down Expand Up @@ -7650,4 +7651,24 @@ protected void markShadow(String oid, String markOid, Task task, OperationResult
.type(PolicyStatementTypeType.APPLY);
modifyObjectAddContainer(ShadowType.class, oid, ShadowType.F_POLICY_STATEMENT, task, result, statement);
}

protected @NotNull CaseType getOpenCaseRequired(List<CaseType> cases) {
var openCases = cases.stream()
.filter(c -> QNameUtil.matchUri(c.getState(), CASE_STATE_OPEN_URI))
.toList();
return MiscUtil.extractSingletonRequired(
openCases,
() -> new AssertionError("More than one open case: " + openCases),
() -> new AssertionError("No open case in: " + cases));
}

protected @NotNull CaseWorkItemType getOpenWorkItemRequired(CaseType aCase) {
var openWorkItems = aCase.getWorkItem().stream()
.filter(wi -> CaseTypeUtil.isCaseWorkItemNotClosed(wi))
.toList();
return MiscUtil.extractSingletonRequired(
openWorkItems,
() -> new AssertionError("More than one open work item: " + openWorkItems),
() -> new AssertionError("No open work items in: " + aCase));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ private boolean getExpandRolesDefaultValue(OperationResult result) throws Schema

public String evaluateAutoCompleteExpression(ApprovalStageDefinitionType stageDef, VariablesMap variables,
Task opTask, OperationResult result) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException, CommunicationException, ConfigurationException, SecurityViolationException {
List<String> outcomes = evaluationHelper.evaluateExpression(stageDef.getAutomaticallyCompleted(), variables,
List<String> outcomes = evaluationHelper.evaluateExpression(
stageDef.getAutomaticallyCompleted(), variables,
"automatic completion expression", String.class,
DOMUtil.XSD_STRING, false, createOutcomeConvertor(), opTask, result);
LOGGER.trace("Pre-completed = {} for stage {}", outcomes, stageDef);
Expand All @@ -190,8 +191,7 @@ private Function<Object, Object> createOutcomeConvertor() {
} else if (o instanceof QName) {
return QNameUtil.qNameToUri((QName) o);
} else {
//throw new IllegalArgumentException("Couldn't create an URI from " + o);
return o; // let someone else complain about this
return o; // let someone else complain about this
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public class PcpStartInstruction extends StartInstruction {

private boolean isObjectCreationInstruction;

private ObjectTreeDeltas<?> deltasToApprove;

private PcpStartInstruction(@NotNull ChangeProcessor changeProcessor, @NotNull String archetypeOid) {
super(changeProcessor, archetypeOid);
aCase.setApprovalContext(new ApprovalContextType());
Expand Down Expand Up @@ -94,9 +96,14 @@ public void setDeltasToApprove(ObjectDelta<? extends ObjectType> delta) throws S

public void setDeltasToApprove(ObjectTreeDeltas<?> objectTreeDeltas) throws SchemaException {
isObjectCreationInstruction = isObjectCreationInstruction(objectTreeDeltas);
this.deltasToApprove = objectTreeDeltas;
getApprovalContext().setDeltasToApprove(ObjectTreeDeltas.toObjectTreeDeltasType(objectTreeDeltas));
}

ObjectTreeDeltas<?> getDeltasToApprove() {
return deltasToApprove;
}

private boolean isObjectCreationInstruction(ObjectTreeDeltas<?> deltasToApprove) {
return deltasToApprove != null && deltasToApprove.getFocusChange() != null && deltasToApprove.getFocusChange().isAdd();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

import com.google.common.base.Preconditions;
import jakarta.annotation.PostConstruct;

import com.evolveum.midpoint.wf.impl.processors.primary.cases.CaseClosing;
import com.evolveum.midpoint.wf.impl.util.MiscHelper;

import org.apache.commons.collections4.CollectionUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

Expand All @@ -41,7 +44,6 @@
import com.evolveum.midpoint.schema.expression.VariablesMap;
import com.evolveum.midpoint.schema.result.OperationResult;
import com.evolveum.midpoint.schema.util.cases.CaseTypeUtil;
import com.evolveum.midpoint.util.DebugUtil;
import com.evolveum.midpoint.util.QNameUtil;
import com.evolveum.midpoint.util.exception.*;
import com.evolveum.midpoint.util.logging.LoggingUtils;
Expand Down Expand Up @@ -90,8 +92,8 @@ public void init() {
// =================================================================================== Processing model invocation

// beware, may damage model context during execution
public List<PcpStartInstruction> previewModelInvocation(@NotNull ModelInvocationContext<?> context,
@NotNull OperationResult result)
public List<PcpStartInstruction> previewModelInvocation(
@NotNull ModelInvocationContext<?> context, @NotNull OperationResult result)
throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException, CommunicationException,
ConfigurationException, SecurityViolationException {
List<PcpStartInstruction> rv = new ArrayList<>();
Expand All @@ -110,11 +112,18 @@ public HookOperationMode processModelInvocation(@NotNull ModelInvocationContext<
}
}

private <O extends ObjectType> HookOperationMode previewOrProcessModelInvocation(@NotNull ModelInvocationContext<O> ctx,
boolean previewOnly, List<PcpStartInstruction> startInstructionsHolder, @NotNull OperationResult parentResult)
private <O extends ObjectType> HookOperationMode previewOrProcessModelInvocation(
@NotNull ModelInvocationContext<O> ctx,
boolean previewOnly,
@Nullable List<PcpStartInstruction> startInstructionsHolder,
@NotNull OperationResult parentResult)
throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException, CommunicationException,
ConfigurationException, SecurityViolationException {

if (previewOnly) {
Preconditions.checkNotNull(startInstructionsHolder, "startInstructionsHolder");
}

OperationResult result = parentResult.subresult(OP_PREVIEW_OR_PROCESS_MODEL_INVOCATION)
.addParam("previewOnly", previewOnly)
.build();
Expand All @@ -136,22 +145,29 @@ private <O extends ObjectType> HookOperationMode previewOrProcessModelInvocation
return null;
}

// examine the request using process aspects
// Examine the request using "aspects" -> start instructions
ObjectTreeDeltas<O> changesBeingDecomposed = objectTreeDeltas.clone();
List<PcpStartInstruction> startInstructions = gatherStartInstructions(changesBeingDecomposed, ctx, result);

// start the process(es)
removeEmptyProcesses(startInstructions, ctx, result);
if (startInstructions.isEmpty()) {
LOGGER.debug("There are no workflow processes to be started, exiting.");
return null;
}
// Remove empty processes, and return all changes from them back into "changes being decomposed"
removeEmptyProcesses(startInstructions, changesBeingDecomposed, ctx, result);

if (previewOnly) {
startInstructionsHolder.addAll(startInstructions);
return null;
} else {
return executeStartInstructions(startInstructions, ctx, changesBeingDecomposed, result);
}

// Now start the process(es)
if (startInstructions.isEmpty()) {
LOGGER.debug("There are no workflow processes to be started, exiting.");
// Although the changes in empty processes were returned to changesBeingDecomposed, and these are ignored
// outside this method, it is not a problem. Because we return "null", the clockwork processing continues
// with the original deltas.
return null;
}
// "Changes being decomposed" contains changes that do not require approval. They are stored into separate case.
return executeStartInstructions(startInstructions, ctx, changesBeingDecomposed, result);

} catch (Throwable t) {
result.recordException(t);
throw t;
Expand All @@ -160,14 +176,20 @@ private <O extends ObjectType> HookOperationMode previewOrProcessModelInvocation
}
}

private void removeEmptyProcesses(List<PcpStartInstruction> instructions, ModelInvocationContext<?> ctx,
OperationResult result) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException,
private <O extends ObjectType> void removeEmptyProcesses(
@NotNull List<PcpStartInstruction> instructions,
@NotNull ObjectTreeDeltas<O> changesWithoutApproval,
@NotNull ModelInvocationContext<O> ctx,
@NotNull OperationResult result) throws SchemaException, ObjectNotFoundException, ExpressionEvaluationException,
CommunicationException, ConfigurationException, SecurityViolationException {
for (Iterator<PcpStartInstruction> iterator = instructions.iterator(); iterator.hasNext(); ) {
PcpStartInstruction instruction = iterator.next();
if (instruction.startsWorkflowProcess() &&
isEmpty(instruction, stageComputeHelper, ctx, result)) {
LOGGER.debug("Skipping empty processing instruction: {}", DebugUtil.debugDumpLazily(instruction));
if (instruction.startsWorkflowProcess()
&& isEmpty(instruction, stageComputeHelper, ctx, result)) {
LOGGER.debug("Skipping empty processing instruction (returning deltas to the 'without approval' set): {}",
instruction.debugDumpLazily());
//noinspection unchecked
changesWithoutApproval.merge((ObjectTreeDeltas<O>) instruction.getDeltasToApprove());
iterator.remove();
}
}
Expand Down Expand Up @@ -222,7 +244,7 @@ private <O extends ObjectType> List<PcpStartInstruction> gatherStartInstructions
ctx.wfConfiguration != null ? ctx.wfConfiguration.getPrimaryChangeProcessor() : null;
List<PcpStartInstruction> startProcessInstructions = new ArrayList<>();
for (PrimaryChangeAspect aspect : getActiveChangeAspects(processorConfigurationType)) {
if (changesBeingDecomposed.isEmpty()) { // nothing left
if (changesBeingDecomposed.isEmpty()) { // nothing left
break;
}
List<PcpStartInstruction> instructions = aspect.getStartInstructions(changesBeingDecomposed, ctx, result);
Expand Down Expand Up @@ -258,7 +280,8 @@ private void logAspectResult(PrimaryChangeAspect aspect, List<? extends StartIns
}
}

private HookOperationMode executeStartInstructions(List<PcpStartInstruction> instructions, ModelInvocationContext<?> ctx,
private HookOperationMode executeStartInstructions(
List<PcpStartInstruction> instructions, ModelInvocationContext<?> ctx,
ObjectTreeDeltas<?> changesWithoutApproval, OperationResult parentResult) {
// Note that this result cannot be minor, because we need to be able to retrieve case OID. And minor results get cut off.
OperationResult result = parentResult.subresult(OP_EXECUTE_START_INSTRUCTIONS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,13 @@ protected void approveWorkItem(CaseWorkItemType workItem, Task task, OperationRe
task, result);
}

/** Assumes single open work item. */
protected void approveCase(CaseType aCase, Task task, OperationResult result) throws CommonException {
approveWorkItem(
getOpenWorkItemRequired(aCase),
task, result);
}

protected void rejectWorkItem(CaseWorkItemType workItem, Task task, OperationResult result)
throws CommunicationException, ObjectNotFoundException, ObjectAlreadyExistsException, PolicyViolationException,
SchemaException, SecurityViolationException, ConfigurationException, ExpressionEvaluationException {
Expand Down

0 comments on commit a5cdb55

Please sign in to comment.