From 3f58086dc00a466bff2876043a3d86e010a9ca0a Mon Sep 17 00:00:00 2001 From: Timm Fitschen <t.fitschen@indiscale.com> Date: Thu, 19 Nov 2020 10:27:57 +0100 Subject: [PATCH] WIP: fsm v0.2 --- .../jobs/core/CheckStateTransition.java | 17 ++- .../server/jobs/core/EntityStateJob.java | 127 ++++++++++++++++-- .../server/jobs/core/InitEntityStateJobs.java | 8 ++ .../server/jobs/core/MakeStateMessage.java | 28 +++- 4 files changed, 161 insertions(+), 19 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 b64acf50..91843299 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 org.apache.shiro.authz.AuthorizationException; import org.caosdb.server.entity.DeleteEntity; import org.caosdb.server.entity.Message; import org.caosdb.server.entity.Message.MessageType; @@ -7,6 +8,7 @@ import org.caosdb.server.entity.UpdateEntity; import org.caosdb.server.jobs.JobAnnotation; import org.caosdb.server.jobs.JobExecutionTime; import org.caosdb.server.transaction.WriteTransaction; +import org.caosdb.server.utils.ServerMessages; /** * Check if the attempted state transition is allowed. @@ -46,6 +48,9 @@ public class CheckStateTransition extends EntityStateJob { } } catch (Message m) { getEntity().addError(m); + } catch (AuthorizationException e) { + getEntity().addError(ServerMessages.AUTHORIZATION_ERROR); + getEntity().addInfo(e.getMessage()); } } @@ -88,12 +93,20 @@ public class CheckStateTransition extends EntityStateJob { return; } + boolean transition_defined = false; for (Transition t : stateModel.getTransitions()) { if (t.isFromState(oldState) && t.isToState(newState)) { - // TODO permissions - return; + transition_defined = true; + if (t.isPermitted(getUser())) { + return; + } } } + if (transition_defined) { + throw new AuthorizationException( + getUser().getPrincipal().toString() + + " doesn't have permission to perform this transition."); + } throw TRANSITION_NOT_ALLOWED; } 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 39f4d675..a18abd11 100644 --- a/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java +++ b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java @@ -1,11 +1,15 @@ 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.LinkedList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; +import org.apache.shiro.subject.Subject; import org.caosdb.server.database.exceptions.EntityDoesNotExistException; import org.caosdb.server.datatype.AbstractCollectionDatatype; import org.caosdb.server.datatype.CollectionValue; @@ -21,6 +25,8 @@ import org.caosdb.server.entity.StatementStatus; import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.entity.xml.ToElementable; import org.caosdb.server.jobs.EntityJob; +import org.caosdb.server.permissions.EntityACI; +import org.caosdb.server.permissions.EntityACL; import org.caosdb.server.query.Query; import org.caosdb.server.utils.EntityStatus; import org.jdom2.Element; @@ -58,9 +64,16 @@ public abstract class EntityStateJob extends EntityJob { public static final String STATE_RECORD_TYPE_NAME = "State"; public static final String STATE_MODEL_RECORD_TYPE_NAME = "StateModel"; public static final String TRANSITION_RECORD_TYPE_NAME = "Transition"; + public static final String TRANSITION_XML_TAG = "Transition"; + public static final String TRANSITION_ATTRIBUTE_NAME = "name"; + public static final String TRANSITION_ATTRIBUTE_DESCRIPTION = "description"; + public static final String TO_XML_TAG = "ToState"; + public static final String FROM_XML_TAG = "FromState"; public static final String STATE_XML_TAG = "State"; - public static final String MODEL_ATTRIBUTE_NAME = "model"; + public static final String STATE_ATTRIBUTE_MODEL = "model"; 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 Message STATE_MODEL_NOT_FOUND = new Message(MessageType.Error, "StateModel not found."); @@ -86,12 +99,14 @@ public abstract class EntityStateJob extends EntityJob { */ public class Transition { + private String name; + private String description; private State fromState; private State toState; - private String name; public Transition(EntityInterface transition) throws Message { this.name = transition.getName(); + this.description = transition.getDescription(); this.fromState = getFromState(transition); this.toState = getToState(transition); } @@ -153,6 +168,10 @@ public abstract class EntityStateJob extends EntityJob { return this.name; } + public String getDescription() { + return this.description; + } + @Override public String toString() { return "Transition (name=" @@ -163,6 +182,22 @@ public abstract class EntityStateJob extends EntityJob { + getToState().getStateName() + ")"; } + + 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) + result.setAttribute(TRANSITION_ATTRIBUTE_DESCRIPTION, this.description); + Element to = new Element(TO_XML_TAG); + to.setAttribute(STATE_ATTRIBUTE_NAME, this.toState.stateName); + 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); + } } /** @@ -191,6 +226,9 @@ public abstract class EntityStateJob extends EntityJob { private EntityInterface stateEntity = null; private EntityInterface stateModelEntity = null; private StateModel stateModel; + private String stateDescription = null; + private Integer stateId = null; + private EntityACL stateACL = null; public State(String stateName, String stateModelName) throws Message { this.stateName = stateName; @@ -199,9 +237,37 @@ public abstract class EntityStateJob extends EntityJob { public State(EntityInterface stateEntity, EntityInterface stateModelEntity) throws Message { this.stateEntity = stateEntity; + this.stateDescription = stateEntity.getDescription(); + this.stateId = stateEntity.getId(); this.stateName = stateEntity.getName(); this.stateModelEntity = stateModelEntity; this.stateModelName = stateModelEntity.getName(); + this.stateACL = createStateACL(stateEntity.getEntityACL()); + } + + private EntityACL createStateACL(EntityACL entityACL) { + LinkedList<EntityACI> rules = new LinkedList<>(); + for (EntityACI aci : entityACL.getRules()) { + if (aci.getResponsibleAgent().toString().startsWith("?STATE?")) { + int end = aci.getResponsibleAgent().toString().length() - 1; + String role = aci.getResponsibleAgent().toString().substring(7, end); + rules.add( + new EntityACI(org.caosdb.server.permissions.Role.create(role), aci.getBitSet())); + } + } + return new EntityACL(rules); + } + + public EntityACL getStateACL() { + return this.stateACL; + } + + public String getStateDescription() throws Message { + return this.stateDescription; + } + + public Integer getStateId() throws Message { + return this.stateId; } @Override @@ -223,11 +289,31 @@ public abstract class EntityStateJob extends EntityJob { @Override public void addToElement(Element ret) { Element e = new Element(STATE_XML_TAG); - if (this.getStateModelName() != null) { - e.setAttribute(MODEL_ATTRIBUTE_NAME, this.getStateModelName()); + if (this.stateModelName != null) { + e.setAttribute(STATE_ATTRIBUTE_MODEL, this.stateModelName); + } + if (this.stateName != null) { + e.setAttribute(STATE_ATTRIBUTE_NAME, this.stateName); + } + if (this.stateDescription != null) { + e.setAttribute(STATE_ATTRIBUTE_DESCRIPTION, this.stateDescription); + } + if (this.stateId != null) { + e.setAttribute(STATE_ATTRIBUTE_ID, Integer.toString(this.stateId)); } - if (this.getStateName() != null) { - e.setAttribute(STATE_ATTRIBUTE_NAME, this.getStateName()); + if (this.stateModelEntity != null) { + try { + this.getStateModel() + .transitions + .forEach( + (Transition t) -> { + if (t.isFromState(this) && t.isPermitted(getUser())) { + e.addContent(t.toElement()); + } + }); + } catch (Message m) { + getEntity().addError(m); + } } ret.addContent(e); } @@ -281,6 +367,9 @@ public abstract class EntityStateJob extends EntityJob { public EntityInterface getStateEntity() throws Message { if (this.stateEntity == null) { this.stateEntity = retrieveStateEntity(this.getStateName()); + this.stateDescription = this.stateEntity.getDescription(); + this.stateId = this.stateEntity.getId(); + this.stateACL = createStateACL(this.stateEntity.getEntityACL()); } return this.stateEntity; } @@ -321,16 +410,11 @@ public abstract class EntityStateJob extends EntityJob { private State finalState; public StateModel(EntityInterface stateModelEntity) throws Message { - long time1 = System.currentTimeMillis(); this.name = stateModelEntity.getName(); this.transitions = getTransitions(stateModelEntity); this.states = getStates(transitions, this); this.finalState = getFinalState(stateModelEntity); this.initialState = getInitialState(stateModelEntity); - long time2 = System.currentTimeMillis(); - getTransaction() - .getTransactionBenchmark() - .addMeasurement(this.getClass().getSimpleName() + "::1", time2 - time1); } private State getInitialState(EntityInterface stateModelEntity) throws Message { @@ -546,7 +630,7 @@ public abstract class EntityStateJob extends EntityJob { } protected State createState(ClientMessage s) throws Message { - String stateModel = s.getProperty(MODEL_ATTRIBUTE_NAME); + String stateModel = s.getProperty(STATE_ATTRIBUTE_MODEL); if (stateModel == null) { throw STATE_MODEL_NOT_SPECIFIED; } @@ -570,7 +654,12 @@ public abstract class EntityStateJob extends EntityJob { try { p.parseValue(); ReferenceValue refid = (ReferenceValue) p.getValue(); - EntityInterface stateEntity = retrieveValidSparseEntityById(refid.getId(), null); + EntityInterface stateEntity = cache.get("state" + Integer.toString(refid.getId())); + boolean cached = true; + if (stateEntity == null || !cached) { + stateEntity = retrieveValidSparseEntityById(refid.getId(), null); + cache.put("state" + Integer.toString(refid.getId()), stateEntity); + } EntityInterface stateModelEntity = findStateModel(stateEntity); return new State(stateEntity, stateModelEntity); @@ -581,8 +670,16 @@ public abstract class EntityStateJob extends EntityJob { } } + private static final Map<String, EntityInterface> cache = new HashMap<>(); + EntityInterface findStateModel(EntityInterface stateEntity) throws Exception { // TODO this should be cached + + boolean cached = true; + EntityInterface result = cache.get("modelof" + Integer.toString(stateEntity.getId())); + if (result != null && cached) { + return result; + } Query query = new Query( "FIND RECORD " @@ -594,6 +691,8 @@ public abstract class EntityStateJob extends EntityJob { getUser(), null); query.execute(getTransaction().getAccess()); - return retrieveValidEntity(query.getResultSet().get(0)); + result = retrieveValidEntity(query.getResultSet().get(0)); + cache.put("modelof" + Integer.toString(stateEntity.getId()), result); + return result; } } 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 8bb1e00a..e4a32bc5 100644 --- a/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java +++ b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java @@ -67,6 +67,7 @@ public class InitEntityStateJobs extends EntityStateJob implements Observer { oldState = null; if (states.size() == 1) { oldState = states.get(0); + ((UpdateEntity) getEntity()).getOriginal().setEntityACL(getEntity().getEntityACL()); } if (!Objects.equals(newState, oldState)) { getEntity().acceptObserver(this); @@ -92,13 +93,20 @@ public class InitEntityStateJobs extends EntityStateJob implements Observer { MessageType.Error, "Currently, each entity can only have one state at a time."); } else if (states.size() == 1) { newState = states.get(0); + transferEntityACL(getEntity(), newState); } } catch (Message m) { getEntity().addError(m); } + return newState; } + private void transferEntityACL(EntityInterface entity, State newState) throws Message { + newState.getStateEntity(); + entity.setEntityACL(newState.getStateACL()); + } + private static final Message STATE_ERROR_IN_ORIGINAL_ENTITY(Message m) { return new Message( MessageType.Warning, "State error in previous entity version\n" + m.getDescription()); diff --git a/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java b/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java index 38fd2398..f2090d82 100644 --- a/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java +++ b/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java @@ -4,9 +4,11 @@ import java.util.List; import org.caosdb.server.CaosDBServer; import org.caosdb.server.entity.Message; import org.caosdb.server.entity.wrapper.Property; +import org.caosdb.server.entity.xml.ToElementable; import org.caosdb.server.jobs.JobAnnotation; import org.caosdb.server.jobs.JobExecutionTime; import org.caosdb.server.transaction.Retrieve; +import org.jdom2.Element; /** * Remove the state property from the entity and, iff necessary, convert it into a State instance @@ -23,6 +25,8 @@ import org.caosdb.server.transaction.Retrieve; time = JobExecutionTime.POST_TRANSACTION) public class MakeStateMessage extends EntityStateJob { + public static final String SPARSE_FLAG = "sparseState"; + @Override protected void run() { @@ -33,9 +37,27 @@ public class MakeStateMessage extends EntityStateJob { // only add the State during Retrieve. In all other cases, the State is already present. if (stateProperties != null && getTransaction() instanceof Retrieve) { - for (Property s : stateProperties) { - State stateMessage = createState(s); - getEntity().addMessage(stateMessage); + String sparse = + getTransaction().getContainer().getFlags().getOrDefault(SPARSE_FLAG, "false"); + if ("true".equals(sparse)) { + for (Property s : stateProperties) { + getEntity() + .addMessage( + new ToElementable() { + @Override + public void addToElement(Element ret) { + Element state = new Element(STATE_XML_TAG); + state.setAttribute(STATE_ATTRIBUTE_ID, s.getValue().toString()); + ret.addContent(state); + } + }); + } + } else { + for (Property s : stateProperties) { + State stateMessage = createState(s); + stateMessage.getStateModel(); + getEntity().addMessage(stateMessage); + } } } } catch (Message e) { -- GitLab