From 1343a94e677d96e6ca40496aa81437017306d10b Mon Sep 17 00:00:00 2001
From: Timm Fitschen <t.fitschen@indiscale.com>
Date: Fri, 20 Nov 2020 01:26:00 +0100
Subject: [PATCH] More permission checks for transitions

---
 .../jobs/core/CheckStateTransition.java       | 26 ++++---
 .../server/jobs/core/EntityStateJob.java      | 67 ++++++++++++++++---
 .../server/jobs/core/InitEntityStateJobs.java |  4 +-
 3 files changed, 79 insertions(+), 18 deletions(-)

diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
index af3ba15a..ef3c8b27 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
@@ -1,5 +1,6 @@
 package org.caosdb.server.jobs.core;
 
+import java.util.Map;
 import org.apache.shiro.authz.AuthorizationException;
 import org.caosdb.server.entity.DeleteEntity;
 import org.caosdb.server.entity.Message;
@@ -18,6 +19,9 @@ import org.caosdb.server.utils.ServerMessages;
 @JobAnnotation(time = JobExecutionTime.POST_CHECK, transaction = WriteTransaction.class)
 public class CheckStateTransition extends EntityStateJob {
 
+  private static final String PERMISSION_STATE_FORCE_FINAL = "STATE:FORCE:FINAL";
+  private static final String PERMISSION_STATE_UNASSIGN = "STATE:UNASSIGN:";
+  private static final String PERMISSION_STATE_ASSIGN = "STATE:ASSIGN:";
   private static final Message TRANSITION_NOT_ALLOWED =
       new Message(MessageType.Error, "Transition not allowed.");
   private static final Message INITIAL_STATE_NOT_ALLOWED =
@@ -29,7 +33,7 @@ public class CheckStateTransition extends EntityStateJob {
    * The forceFinalState flag is especially useful if you want to delete entities in the middle of
    * the state machine's usual state cycle.
    */
-  private static final String FORCE_FINAL_STATE = "forceFinalState";
+  private static final String FLAG_FORCE_FINAL_STATE = "forceFinalState";
 
   @Override
   protected void run() {
@@ -131,14 +135,13 @@ public class CheckStateTransition extends EntityStateJob {
    */
   private void checkFinalState(State oldState) throws Message {
     if (!oldState.isFinal()) {
-      if ("true".equalsIgnoreCase(getTransaction().getContainer().getFlags().get(FORCE_FINAL_STATE))
-          || "true".equalsIgnoreCase(getEntity().getFlag(FORCE_FINAL_STATE))) {
-        // TODO permissions
-        return;
+      if (isForceFinal()) {
+        getUser().checkPermission(PERMISSION_STATE_FORCE_FINAL);
+      } else {
+        throw FINAL_STATE_NOT_ALLOWED;
       }
-      throw FINAL_STATE_NOT_ALLOWED;
     }
-    // TODO permissions
+    getUser().checkPermission(PERMISSION_STATE_UNASSIGN + oldState.getStateModelName());
   }
 
   /**
@@ -151,6 +154,13 @@ public class CheckStateTransition extends EntityStateJob {
     if (!newState.isInitial()) {
       throw INITIAL_STATE_NOT_ALLOWED;
     }
-    // TODO permissions
+    getUser().checkPermission(PERMISSION_STATE_ASSIGN + newState.getStateModelName());
+  }
+
+  private boolean isForceFinal() {
+    Map<String, String> containerFlags = getTransaction().getContainer().getFlags();
+    return (containerFlags != null
+            && "true".equalsIgnoreCase(containerFlags.get(FLAG_FORCE_FINAL_STATE)))
+        || "true".equalsIgnoreCase(getEntity().getFlag(FLAG_FORCE_FINAL_STATE));
   }
 }
diff --git a/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
index a18abd11..dc1eba1d 100644
--- a/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
+++ b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
@@ -2,8 +2,9 @@ package org.caosdb.server.jobs.core;
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -17,6 +18,7 @@ import org.caosdb.server.datatype.IndexedSingleValue;
 import org.caosdb.server.datatype.ReferenceDatatype;
 import org.caosdb.server.datatype.ReferenceDatatype2;
 import org.caosdb.server.datatype.ReferenceValue;
+import org.caosdb.server.datatype.TextDatatype;
 import org.caosdb.server.entity.ClientMessage;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.Message;
@@ -74,6 +76,8 @@ public abstract class EntityStateJob extends EntityJob {
   public static final String STATE_ATTRIBUTE_NAME = "name";
   public static final String STATE_ATTRIBUTE_DESCRIPTION = "description";
   public static final String STATE_ATTRIBUTE_ID = "id";
+  public static final String ENTITY_STATE_ROLE_MARKER = "?STATE?";
+  public static final String PERMISSION_STATE_TRANSION = "STATE:TRANSITION:";
 
   public static final Message STATE_MODEL_NOT_FOUND =
       new Message(MessageType.Error, "StateModel not found.");
@@ -103,12 +107,24 @@ public abstract class EntityStateJob extends EntityJob {
     private String description;
     private State fromState;
     private State toState;
+    private Map<String, String> transitionProperties;
 
     public Transition(EntityInterface transition) throws Message {
       this.name = transition.getName();
       this.description = transition.getDescription();
       this.fromState = getFromState(transition);
       this.toState = getToState(transition);
+      this.transitionProperties = getTransitionProperties(transition);
+    }
+
+    private Map<String, String> getTransitionProperties(EntityInterface transition) {
+      Map<String, String> result = new LinkedHashMap<>();
+      for (Property p : transition.getProperties()) {
+        if (p.getDatatype() instanceof TextDatatype) {
+          result.put(p.getName(), p.getValue().toString());
+        }
+      }
+      return result;
     }
 
     private State getToState(EntityInterface transition) throws Message {
@@ -185,18 +201,30 @@ public abstract class EntityStateJob extends EntityJob {
 
     public Element toElement() {
       Element result = new Element(TRANSITION_XML_TAG);
-      if (this.name != null) result.setAttribute(TRANSITION_ATTRIBUTE_NAME, this.name);
-      if (this.description != null)
+      if (this.transitionProperties != null) {
+        this.transitionProperties.forEach(
+            (String key, String value) -> {
+              result.setAttribute(key, value);
+            });
+      }
+      if (this.name != null) {
+        result.setAttribute(TRANSITION_ATTRIBUTE_NAME, this.name);
+      }
+      if (this.description != null) {
         result.setAttribute(TRANSITION_ATTRIBUTE_DESCRIPTION, this.description);
+      }
       Element to = new Element(TO_XML_TAG);
       to.setAttribute(STATE_ATTRIBUTE_NAME, this.toState.stateName);
+      if (this.toState.stateDescription != null) {
+        to.setAttribute(STATE_ATTRIBUTE_DESCRIPTION, this.toState.stateDescription);
+      }
       Element from = new Element(FROM_XML_TAG);
       from.setAttribute(STATE_ATTRIBUTE_NAME, this.fromState.stateName);
       return result.addContent(from).addContent(to);
     }
 
     public boolean isPermitted(Subject user) {
-      return user.isPermitted("STATE:TRANSITION:" + this.name);
+      return user.isPermitted(PERMISSION_STATE_TRANSION + this.name);
     }
   }
 
@@ -229,6 +257,7 @@ public abstract class EntityStateJob extends EntityJob {
     private String stateDescription = null;
     private Integer stateId = null;
     private EntityACL stateACL = null;
+    private Map<String, String> stateProperties;
 
     public State(String stateName, String stateModelName) throws Message {
       this.stateName = stateName;
@@ -243,12 +272,23 @@ public abstract class EntityStateJob extends EntityJob {
       this.stateModelEntity = stateModelEntity;
       this.stateModelName = stateModelEntity.getName();
       this.stateACL = createStateACL(stateEntity.getEntityACL());
+      this.stateProperties = createStateProperties(stateEntity);
+    }
+
+    private Map<String, String> createStateProperties(EntityInterface stateEntity) {
+      Map<String, String> result = new LinkedHashMap<>();
+      for (Property p : stateEntity.getProperties()) {
+        if (p.getDatatype() instanceof TextDatatype) {
+          result.put(p.getName(), p.getValue().toString());
+        }
+      }
+      return result;
     }
 
     private EntityACL createStateACL(EntityACL entityACL) {
       LinkedList<EntityACI> rules = new LinkedList<>();
       for (EntityACI aci : entityACL.getRules()) {
-        if (aci.getResponsibleAgent().toString().startsWith("?STATE?")) {
+        if (aci.getResponsibleAgent().toString().startsWith(ENTITY_STATE_ROLE_MARKER)) {
           int end = aci.getResponsibleAgent().toString().length() - 1;
           String role = aci.getResponsibleAgent().toString().substring(7, end);
           rules.add(
@@ -289,6 +329,15 @@ public abstract class EntityStateJob extends EntityJob {
     @Override
     public void addToElement(Element ret) {
       Element e = new Element(STATE_XML_TAG);
+      if (this.stateProperties == null && this.stateEntity != null) {
+        this.stateProperties = createStateProperties(this.stateEntity);
+      }
+      if (this.stateProperties != null && this.stateProperties.size() > 0) {
+        this.stateProperties.forEach(
+            (String key, String value) -> {
+              e.setAttribute(key, value);
+            });
+      }
       if (this.stateModelName != null) {
         e.setAttribute(STATE_ATTRIBUTE_MODEL, this.stateModelName);
       }
@@ -318,7 +367,7 @@ public abstract class EntityStateJob extends EntityJob {
       ret.addContent(e);
     }
 
-    private String getStateModelName() {
+    public String getStateModelName() {
       return this.stateModelName;
     }
 
@@ -452,7 +501,7 @@ public abstract class EntityStateJob extends EntityJob {
      * @throws Message if the transitions could ne be created.
      */
     private Set<Transition> createTransitions(Property p) throws Message {
-      Set<Transition> result = new HashSet<>();
+      Set<Transition> result = new LinkedHashSet<>();
       try {
         if (!(p.getDatatype() instanceof AbstractCollectionDatatype)) {
           return result;
@@ -486,7 +535,7 @@ public abstract class EntityStateJob extends EntityJob {
     private Set<State> getStates(Set<Transition> transitions, StateModel stateModel)
         throws Message {
       Iterator<Transition> it = transitions.iterator();
-      Set<State> result = new HashSet<>();
+      Set<State> result = new LinkedHashSet<>();
       while (it.hasNext()) {
         Transition t = it.next();
         result.add(t.getFromState());
@@ -657,7 +706,7 @@ public abstract class EntityStateJob extends EntityJob {
       EntityInterface stateEntity = cache.get("state" + Integer.toString(refid.getId()));
       boolean cached = true;
       if (stateEntity == null || !cached) {
-        stateEntity = retrieveValidSparseEntityById(refid.getId(), null);
+        stateEntity = retrieveValidEntity(refid.getId());
         cache.put("state" + Integer.toString(refid.getId()), stateEntity);
       }
 
diff --git a/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java
index 5dec5d49..2afcd0ee 100644
--- a/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java
+++ b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java
@@ -76,7 +76,9 @@ public class InitEntityStateJobs extends EntityStateJob implements Observer {
         oldState = null;
         if (states.size() == 1) {
           oldState = states.get(0);
-          ((UpdateEntity) getEntity()).getOriginal().setEntityACL(getEntity().getEntityACL());
+          if (newState != null) {
+            ((UpdateEntity) getEntity()).getOriginal().setEntityACL(getEntity().getEntityACL());
+          }
         }
         if (!Objects.equals(newState, oldState)) {
           getEntity().acceptObserver(this);
-- 
GitLab