diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1415efae27e94cb5edea8469c5049d246300ec..6e560318bb4372e3882b21c0acb12f2bdd443643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* New EntityState plug-in. The plug-in disabled by default and can be enabled + by setting the server property `EXT_ENTITY_STATE=ENABLED`. See + [!62](https://gitlab.com/caosdb/caosdb-server/-/merge_requests/62) for more + information. * `ETag` property for the query. The `ETag` is assigned to the query cache each time the cache is cleared (currently whenever the server state is being updated, i.e. the stored entities change). diff --git a/caosdb-webui b/caosdb-webui index 8c59cc861d646cbdba0ec749ba052656f67fd58d..5dfe879722bd01acc5209c581b60bf0ac49635b6 160000 --- a/caosdb-webui +++ b/caosdb-webui @@ -1 +1 @@ -Subproject commit 8c59cc861d646cbdba0ec749ba052656f67fd58d +Subproject commit 5dfe879722bd01acc5209c581b60bf0ac49635b6 diff --git a/conf/core/server.conf b/conf/core/server.conf index c9151888e785b8114f104fdcd14c43714fc80b37..76ed6030523f24f977f41a510c703fe075f30042 100644 --- a/conf/core/server.conf +++ b/conf/core/server.conf @@ -188,3 +188,11 @@ GLOBAL_ENTITY_PERMISSIONS_FILE=./conf/core/global_entity_permissions.xml # If set to true, versioning of entities' history is enabled. ENTITY_VERSIONING_ENABLED=true + + +# -------------------------------------------------- +# Extension settings +# -------------------------------------------------- + +# Enabling the state machine extension +# EXT_STATE_ENTITY=ENABLE diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveAll.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveAll.java index ec0c0c1b0a067d529ecb72309d7fe83a8b37f819..7af4022595f64b909231c72c83ae1e151a9d06c1 100644 --- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveAll.java +++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveAll.java @@ -27,10 +27,14 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import org.apache.shiro.SecurityUtils; +import org.caosdb.server.database.DatabaseUtils; import org.caosdb.server.database.access.Access; import org.caosdb.server.database.backend.interfaces.RetrieveAllImpl; import org.caosdb.server.database.exceptions.TransactionException; import org.caosdb.server.entity.Role; +import org.caosdb.server.permissions.EntityACL; +import org.caosdb.server.permissions.EntityPermission; public class MySQLRetrieveAll extends MySQLTransaction implements RetrieveAllImpl { @@ -38,17 +42,20 @@ public class MySQLRetrieveAll extends MySQLTransaction implements RetrieveAllImp super(access); } - public static final String STMT_GET_ALL_HEAD = "Select id from entities where id > 99"; + public static final String STMT_GET_ALL_HEAD = + "SELECT e.id AS ID, a.acl AS ACL FROM entities AS e JOIN entity_acl AS a ON (e.acl = a.id) WHERE e.id > 99"; public static final String STMT_ENTITY_WHERE_CLAUSE = - " AND ( role=? OR role='" + " AND ( e.role='" + + Role.Record + + "' OR e.role='" + Role.RecordType - + "' OR role='" + + "' OR e.role='" + Role.Property - + "' OR role='" + + "' OR e.role='" + Role.File + "'" + " )"; - public static final String STMT_OTHER_ROLES = " AND role=?"; + public static final String STMT_OTHER_ROLES = " AND e.role=?"; @Override public List<Integer> execute(final String role) throws TransactionException { @@ -58,10 +65,7 @@ public class MySQLRetrieveAll extends MySQLTransaction implements RetrieveAllImp + (role.equalsIgnoreCase("ENTITY") ? STMT_ENTITY_WHERE_CLAUSE : STMT_OTHER_ROLES); final PreparedStatement stmt = prepareStatement(STMT_GET_ALL); - if (role.equalsIgnoreCase("ENTITY")) { - stmt.setString(1, Role.Record.toString()); - - } else { + if (!role.equalsIgnoreCase("ENTITY")) { stmt.setString(1, role); } @@ -69,7 +73,11 @@ public class MySQLRetrieveAll extends MySQLTransaction implements RetrieveAllImp try { final ArrayList<Integer> ret = new ArrayList<Integer>(); while (rs.next()) { - ret.add(rs.getInt(1)); + String acl = DatabaseUtils.bytes2UTF8(rs.getBytes("ACL")); + if (EntityACL.deserialize(acl) + .isPermitted(SecurityUtils.getSubject(), EntityPermission.RETRIEVE_ENTITY)) { + ret.add(rs.getInt("ID")); + } } return ret; } finally { diff --git a/src/main/java/org/caosdb/server/entity/ClientMessage.java b/src/main/java/org/caosdb/server/entity/ClientMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..f6b996770b1d1d88992a56551acc0d5aaa5db1f0 --- /dev/null +++ b/src/main/java/org/caosdb/server/entity/ClientMessage.java @@ -0,0 +1,90 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +package org.caosdb.server.entity; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import org.jdom2.Attribute; +import org.jdom2.Element; + +/** + * Class which represents client messages. Client messages is a way to extend the Entity API with + * special properties which may be used by plug-ins. + * + * <p>If no plug-in handles the client message, it is printed back to the response unaltered. + * + * <p>Client message can have arbitrary key-value (string-string typed) tuples {@link #properties}. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public class ClientMessage extends Message { + + private static final long serialVersionUID = 1L; + private Map<String, String> properties = new HashMap<>(); + + public ClientMessage(String type, String body) { + super(type, null, null, null); + } + + @Override + public Element toElement() { + final Element e = new Element(this.type); + for (Entry<String, String> a : this.properties.entrySet()) { + e.setAttribute(a.getKey(), a.getValue()); + } + return e; + } + + @Override + public void addToElement(final Element parent) { + final Element e = toElement(); + parent.addContent(e); + } + + /** NB: This is the only place where properties are set in this class. */ + public static ClientMessage fromXML(Element pe) { + ClientMessage result = new ClientMessage(pe.getName(), pe.getText()); + for (Attribute a : pe.getAttributes()) { + result.properties.put(a.getName(), a.getValue()); + } + return result; + } + + public String getProperty(String key) { + return properties.get(key); + } + + @Override + public String toString() { + return this.type + " - " + this.properties.toString(); + } + + @Override + public int hashCode() { + return type.hashCode() + + (this.getBody() == null ? 0 : this.getBody().hashCode()) + + this.properties.hashCode(); + } +} diff --git a/src/main/java/org/caosdb/server/entity/Entity.java b/src/main/java/org/caosdb/server/entity/Entity.java index aa4c96127cfc97b03f308a50cb587dc12949c105..0a60f9af59f731098a0e194e4848b578094d4e68 100644 --- a/src/main/java/org/caosdb/server/entity/Entity.java +++ b/src/main/java/org/caosdb/server/entity/Entity.java @@ -35,6 +35,7 @@ import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.Permission; import org.apache.shiro.subject.Subject; import org.caosdb.server.CaosDBException; +import org.caosdb.server.accessControl.Principal; import org.caosdb.server.database.proto.SparseEntity; import org.caosdb.server.database.proto.VerySparseEntity; import org.caosdb.server.datatype.AbstractCollectionDatatype; @@ -104,10 +105,12 @@ public class Entity extends AbstractObservable implements EntityInterface { public void checkPermission(final Subject subject, final Permission permission) { try { if (!this.hasPermission(subject, permission)) { + String user = "The current user "; + if (subject.getPrincipal() instanceof Principal) { + user = ((Principal) subject.getPrincipal()).getUsername(); + } throw new AuthorizationException( - subject.getPrincipal().toString() - + " doesn't have permission " - + permission.toString()); + user + " doesn't have permission " + permission.toString()); } } catch (final NullPointerException e) { throw new AuthorizationException("This entity doesn't have an ACL!"); @@ -848,34 +851,7 @@ public class Entity extends AbstractObservable implements EntityInterface { } else if (getRole() == Role.QueryTemplate && pe.getName().equalsIgnoreCase("Query")) { setQueryTemplateDefinition(pe.getTextNormalize()); } else { - final String type = pe.getName(); - Integer code = null; - String localDescription = null; - String body = null; - - // Parse MESSAGE CODE. - if (pe.getAttribute("code") != null && !pe.getAttributeValue("code").equals("")) { - try { - code = Integer.parseInt(pe.getAttributeValue("code")); - } catch (final NumberFormatException e) { - addInfo("Message code was " + pe.getAttributeValue("code") + "."); - addError(ServerMessages.PARSING_FAILED); - setEntityStatus(EntityStatus.UNQUALIFIED); - } - } - - // Parse MESSAGE DESCRIPTION. - if (pe.getAttribute("description") != null - && !pe.getAttributeValue("description").equals("")) { - localDescription = pe.getAttributeValue("description"); - } - - // Parse MESSAGE BODY. - if (pe.getTextTrim() != null && !pe.getTextTrim().equals("")) { - body = pe.getTextTrim(); - } - - addMessage(new Message(type, code, localDescription, body)); + addMessage(ClientMessage.fromXML(pe)); } } diff --git a/src/main/java/org/caosdb/server/entity/Message.java b/src/main/java/org/caosdb/server/entity/Message.java index 743a2b62fa87570e0dbca05a1f279e6764920f0b..3e469b2dc84b07e3ac3de378f8990cc520989cc0 100644 --- a/src/main/java/org/caosdb/server/entity/Message.java +++ b/src/main/java/org/caosdb/server/entity/Message.java @@ -27,7 +27,7 @@ import org.jdom2.Element; public class Message extends Exception implements Comparable<Message>, ToElementable { - private final String type; + protected final String type; private final Integer code; private final String description; private final String body; @@ -127,7 +127,7 @@ public class Message extends Exception implements Comparable<Message>, ToElement return this.code; } - public final Element toElement() { + public Element toElement() { final Element e = new Element(this.type); if (this.code != null) { e.setAttribute("code", Integer.toString(this.code)); @@ -142,7 +142,7 @@ public class Message extends Exception implements Comparable<Message>, ToElement } @Override - public final void addToElement(final Element parent) { + public void addToElement(final Element parent) { final Element e = toElement(); parent.addContent(e); } diff --git a/src/main/java/org/caosdb/server/entity/UpdateEntity.java b/src/main/java/org/caosdb/server/entity/UpdateEntity.java index aa6d591602df66cc9317351cf6a1ea980bc53ea3..884632b5ed3f92d740f2eea69ed49ef77511fd1b 100644 --- a/src/main/java/org/caosdb/server/entity/UpdateEntity.java +++ b/src/main/java/org/caosdb/server/entity/UpdateEntity.java @@ -22,11 +22,21 @@ */ package org.caosdb.server.entity; +import org.caosdb.server.transaction.WriteTransaction; import org.caosdb.server.utils.EntityStatus; import org.jdom2.Element; +/** + * UpdateEntity class represents entities which are to be updated. The previous version is appeded + * during the {@link WriteTransaction} transactions initialization. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ public class UpdateEntity extends WritableEntity { + /** The previous version of this entity. */ + private EntityInterface original = null; + public UpdateEntity(final Element element) { super(element); } @@ -35,4 +45,12 @@ public class UpdateEntity extends WritableEntity { public boolean skipJob() { return getEntityStatus() != EntityStatus.QUALIFIED; } + + public void setOriginal(EntityInterface original) { + this.original = original; + } + + public EntityInterface getOriginal() { + return this.original; + } } diff --git a/src/main/java/org/caosdb/server/jobs/Job.java b/src/main/java/org/caosdb/server/jobs/Job.java index 28dc512b669eb6f1df0cfd24e32e2051f734b697..86ba49fabeedf04833f4e82214876be698aaa8cd 100644 --- a/src/main/java/org/caosdb/server/jobs/Job.java +++ b/src/main/java/org/caosdb/server/jobs/Job.java @@ -50,10 +50,7 @@ import org.caosdb.server.entity.Message; import org.caosdb.server.entity.container.TransactionContainer; import org.caosdb.server.jobs.core.Mode; import org.caosdb.server.transaction.Transaction; -import org.caosdb.server.utils.AbstractObservable; import org.caosdb.server.utils.EntityStatus; -import org.caosdb.server.utils.Observable; -import org.caosdb.server.utils.Observer; import org.caosdb.server.utils.ServerMessages; import org.reflections.Reflections; @@ -62,7 +59,7 @@ import org.reflections.Reflections; * * @todo Describe me. */ -public abstract class Job extends AbstractObservable implements Observer { +public abstract class Job { private Transaction<? extends TransactionContainer> transaction = null; private Mode mode = null; @@ -139,10 +136,6 @@ public abstract class Job extends AbstractObservable implements Observer { getTransaction().getSchedule().runJob(entity, jobclass); } - protected void runJobFromSchedule(ScheduledJob job) { - getTransaction().getSchedule().runJob(job); - } - public EntityInterface getEntity() { return this.entity; } @@ -237,14 +230,6 @@ public abstract class Job extends AbstractObservable implements Observer { } } - @Override - public boolean notifyObserver(final String e, final Observable o) { - if (getEntity().getEntityStatus() != EntityStatus.UNQUALIFIED) { - getTransaction().getSchedule().runJob(this); - } - return true; - } - static HashMap<String, Class<? extends Job>> allClasses = null; private static List<Class<? extends Job>> loadAlways; @@ -463,10 +448,6 @@ public abstract class Job extends AbstractObservable implements Observer { + "]"; } - public void finish() { - super.removeAllObservers(); - } - public void print() { System.out.println(toString()); } diff --git a/src/main/java/org/caosdb/server/jobs/Schedule.java b/src/main/java/org/caosdb/server/jobs/Schedule.java index 7f35106bcdda8ddc00512197091fa096a6bc157e..77bc57c9e3f4183bf572b6a95dfe16fc79b6e003 100644 --- a/src/main/java/org/caosdb/server/jobs/Schedule.java +++ b/src/main/java/org/caosdb/server/jobs/Schedule.java @@ -22,94 +22,48 @@ */ package org.caosdb.server.jobs; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import org.caosdb.server.entity.EntityInterface; -class ScheduledJob { - - long runtime = 0; - final Job job; - private long startTime = -1; - - public ScheduledJob(final Job j) { - this.job = j; - } - - public void run() { - if (!hasStarted()) { - start(); - this.job.run(); - finish(); - - this.job.notifyObservers(null); - } - } - - private void start() { - this.startTime = System.currentTimeMillis(); - } - - private void finish() { - this.runtime += System.currentTimeMillis() - this.startTime; - this.job - .getContainer() - .getTransactionBenchmark() - .addMeasurement(this.job.getClass().getSimpleName(), this.runtime); - } - - void pause() { - this.runtime += System.currentTimeMillis() - this.startTime; - } - - void unpause() { - start(); - } - - private boolean hasStarted() { - return this.startTime != -1; - } - - public JobExecutionTime getExecutionTime() { - return this.job.getExecutionTime(); - } - - public boolean skip() { - return this.job.getTarget().skipJob(); - } -} - public class Schedule { - private final CopyOnWriteArrayList<ScheduledJob> jobs = new CopyOnWriteArrayList<ScheduledJob>(); + private final Map<Integer, List<ScheduledJob>> jobLists = new HashMap<>(); private ScheduledJob running = null; - public void addAll(final Collection<Job> jobs) { + public List<ScheduledJob> addAll(final Collection<Job> jobs) { + List<ScheduledJob> result = new ArrayList<ScheduledJob>(jobs.size()); for (final Job j : jobs) { - add(j); + result.add(add(j)); } + return result; } public ScheduledJob add(final Job j) { ScheduledJob ret = new ScheduledJob(j); - this.jobs.add(ret); + List<ScheduledJob> jobs = jobLists.get(ret.getExecutionTime().ordinal()); + if (jobs == null) { + jobs = new CopyOnWriteArrayList<ScheduledJob>(); + jobLists.put(ret.getExecutionTime().ordinal(), jobs); + } + jobs.add(ret); return ret; } public void runJobs(final JobExecutionTime time) { - for (final ScheduledJob scheduledJob : this.jobs) { - if (scheduledJob.getExecutionTime().ordinal() == time.ordinal() - || (time.ordinal() <= JobExecutionTime.POST_CHECK.ordinal() - && scheduledJob.getExecutionTime().ordinal() < time.ordinal())) { + List<ScheduledJob> jobs = this.jobLists.get(time.ordinal()); + if (jobs != null) { + for (final ScheduledJob scheduledJob : jobs) { runJob(scheduledJob); } } } - protected void runJob(final ScheduledJob scheduledJob) { - if (!this.jobs.contains(scheduledJob)) { - throw new RuntimeException("Job was not in schedule."); - } + public void runJob(final ScheduledJob scheduledJob) { if (scheduledJob.skip()) { return; } @@ -127,18 +81,12 @@ public class Schedule { } } - public void runJob(final Job j) { - for (final ScheduledJob scheduledJob : this.jobs) { - if (scheduledJob.job == j) { - scheduledJob.run(); - return; - } - } - throw new RuntimeException("Job was not in schedule."); - } - public void runJob(final EntityInterface entity, final Class<? extends Job> jobclass) { - for (final ScheduledJob scheduledJob : this.jobs) { + List<ScheduledJob> jobs = + jobclass.isAnnotationPresent(JobAnnotation.class) + ? this.jobLists.get(jobclass.getAnnotation(JobAnnotation.class).time().ordinal()) + : this.jobLists.get(JobExecutionTime.CHECK.ordinal()); + for (final ScheduledJob scheduledJob : jobs) { if (jobclass.isInstance(scheduledJob.job)) { if (scheduledJob.job.getEntity() == entity) { runJob(scheduledJob); diff --git a/src/main/java/org/caosdb/server/jobs/ScheduledJob.java b/src/main/java/org/caosdb/server/jobs/ScheduledJob.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0c29805d7500987f5823f2c7d3eaec8f6ed50b --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/ScheduledJob.java @@ -0,0 +1,88 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +package org.caosdb.server.jobs; + +/** + * ScheduledJob is a wrapper class for jobs held by the Scheduler. + * + * <p>It is mainly a means to have simplified interface for the Scheduler which also measures the + * execution time of the job "from outside". + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public class ScheduledJob { + + long runtime = 0; + final Job job; + private long startTime = -1; + + ScheduledJob(final Job j) { + this.job = j; + } + + void run() { + if (!hasStarted()) { + start(); + this.job.run(); + finish(); + } + } + + @Override + public String toString() { + return this.job.toString(); + } + + private void start() { + this.startTime = System.currentTimeMillis(); + } + + private void finish() { + this.runtime += System.currentTimeMillis() - this.startTime; + this.job + .getContainer() + .getTransactionBenchmark() + .addMeasurement(this.job.getClass().getSimpleName(), this.runtime); + } + + void pause() { + this.runtime += System.currentTimeMillis() - this.startTime; + } + + void unpause() { + start(); + } + + private boolean hasStarted() { + return this.startTime != -1; + } + + public JobExecutionTime getExecutionTime() { + return this.job.getExecutionTime(); + } + + public boolean skip() { + return this.job.getTarget().skipJob(); + } +} diff --git a/src/main/java/org/caosdb/server/jobs/core/Atomic.java b/src/main/java/org/caosdb/server/jobs/core/Atomic.java index 509f87fc8515ca08daa1a5cb8ef6571dfe6fa441..45b5de8e4d5835bd835a797290e2d3c875687e44 100644 --- a/src/main/java/org/caosdb/server/jobs/core/Atomic.java +++ b/src/main/java/org/caosdb/server/jobs/core/Atomic.java @@ -30,13 +30,14 @@ import org.caosdb.server.jobs.JobExecutionTime; import org.caosdb.server.transaction.WriteTransaction; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.Observable; +import org.caosdb.server.utils.Observer; import org.caosdb.server.utils.ServerMessages; @JobAnnotation( - time = JobExecutionTime.POST_CHECK, + time = JobExecutionTime.PRE_TRANSACTION, transaction = WriteTransaction.class, loadAlways = true) -public class Atomic extends ContainerJob { +public class Atomic extends ContainerJob implements Observer { private boolean doCheck() { if (getContainer().getStatus() == EntityStatus.QUALIFIED) { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java b/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java index ce9943ecbefb4d669003a097e74bf9baff078167..f46013d278f268b95b1b105130e1eabd74f3a9f4 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java @@ -33,6 +33,7 @@ import org.caosdb.server.entity.Message; import org.caosdb.server.entity.Role; import org.caosdb.server.jobs.EntityJob; import org.caosdb.server.jobs.Job; +import org.caosdb.server.jobs.ScheduledJob; import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; @@ -81,8 +82,8 @@ public final class CheckDatatypePresent extends EntityJob { // run jobsreturn this.entities; final List<Job> datatypeJobs = loadDataTypeSpecificJobs(); - getTransaction().getSchedule().addAll(datatypeJobs); - for (final Job job : datatypeJobs) { + List<ScheduledJob> scheduledJobs = getTransaction().getSchedule().addAll(datatypeJobs); + for (final ScheduledJob job : scheduledJobs) { getTransaction().getSchedule().runJob(job); } } else { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java index c6d10a8f4f7cd0656f571a4cbf70385ab9cb7725..02cb96fc1214712c7a8ef1bb98327ba9fb39e9d0 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java @@ -32,6 +32,8 @@ import org.caosdb.server.entity.Role; import org.caosdb.server.entity.wrapper.Parent; import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.jobs.EntityJob; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.JobExecutionTime; import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; @@ -41,6 +43,7 @@ import org.caosdb.server.utils.ServerMessages; * * @author tf */ +@JobAnnotation(time = JobExecutionTime.PRE_CHECK) public class CheckParValid extends EntityJob { @Override public final void run() { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java index 10ce6295a5b4910084d769278ca6348287e6223a..f95104bd2f2552a642e7e4bcd2fc13f7fd2788d4 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java @@ -31,6 +31,8 @@ import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.Message; import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.jobs.EntityJob; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.JobExecutionTime; import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; @@ -40,6 +42,7 @@ import org.caosdb.server.utils.ServerMessages; * * @author tf */ +@JobAnnotation(time = JobExecutionTime.PRE_CHECK) public class CheckPropValid extends EntityJob { @Override public final void run() { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java b/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java index 59fcfe1d877ca61c98674ee85919f03b7bd4278e..b9f804f519948b605a9fa8820c91495618463fba 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java @@ -37,6 +37,7 @@ import org.caosdb.server.entity.wrapper.Parent; import org.caosdb.server.jobs.EntityJob; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.Observable; +import org.caosdb.server.utils.Observer; import org.caosdb.server.utils.ServerMessages; /** @@ -45,7 +46,7 @@ import org.caosdb.server.utils.ServerMessages; * * @author tf */ -public class CheckRefidIsaParRefid extends EntityJob { +public class CheckRefidIsaParRefid extends EntityJob implements Observer { private void doJob() { try { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java index c9d06cb5487ff62a276ff1028c22864afa850562..8563ea716072602684dc1a78870392d573dc3a22 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java @@ -36,6 +36,7 @@ import org.caosdb.server.jobs.EntityJob; import org.caosdb.server.permissions.EntityPermission; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.Observable; +import org.caosdb.server.utils.Observer; import org.caosdb.server.utils.ServerMessages; /** @@ -43,7 +44,7 @@ import org.caosdb.server.utils.ServerMessages; * * @author tf */ -public class CheckRefidValid extends EntityJob { +public class CheckRefidValid extends EntityJob implements Observer { @Override public final void run() { try { diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java new file mode 100644 index 0000000000000000000000000000000000000000..cd187cf17ad72c5431fcd3f2eb0b95890941faf9 --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java @@ -0,0 +1,200 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +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; +import org.caosdb.server.entity.Message.MessageType; +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. + * + * <p>This job checks if the attempted state transition is in compliance with the state model. This + * job runs during the CHECK phase and should do all necessary consistency and permission checks. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +@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 = + new Message(MessageType.Error, "Initial state not allowed."); + private static final Message FINAL_STATE_NOT_ALLOWED = + new Message(MessageType.Error, "Final state not allowed."); + + /** + * 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 FLAG_FORCE_FINAL_STATE = "forceFinalState"; + + @Override + protected void run() { + try { + State newState = getState(); + if (newState != null) { + checkStateValid(newState); + } + if (getEntity() instanceof UpdateEntity) { + State oldState = getState(((UpdateEntity) getEntity()).getOriginal()); + checkStateTransition(oldState, newState); + } else if (getEntity() instanceof DeleteEntity) { + if (newState != null) checkFinalState(newState); + } else { // fresh Entity + if (newState != null) checkInitialState(newState); + } + } catch (Message m) { + getEntity().addError(m); + } catch (AuthorizationException e) { + getEntity().addError(ServerMessages.AUTHORIZATION_ERROR); + getEntity().addInfo(e.getMessage()); + } + } + + /** + * Check if the state belongs to the state model. + * + * <p>In practical terms, throw a Message if the state is invalid. + * + * @param state + * @throws Message + */ + private void checkStateValid(State state) throws Message { + if (state.isFinal() || state.isInitial() || state.getStateModel().getStates().contains(state)) { + return; + } + throw STATE_NOT_IN_STATE_MODEL; + } + + /** + * Check if state is valid and transition is allowed. + * + * <p>Especially, transitions between {@code null} states are allowed, non-trivial transitions + * from or to {@code null} must be initial or final states, respectively ({@link + * FORCE_FINAL_STATE} exception applies). + * + * @param oldState + * @param newState + * @throws Message if not + */ + private void checkStateTransition(State oldState, State newState) throws Message { + if (oldState == null && newState == null) { + return; + } else if (oldState == null && newState != null) { + checkInitialState(newState); + return; + } else if (newState == null && oldState != null) { + checkFinalState(oldState); + return; + } + + StateModel stateModel = findMatchingStateModel(oldState, newState); + if (stateModel == null) { + // change from one stateModel to another + checkInitialState(newState); + checkFinalState(oldState); + return; + } + + boolean transition_defined = false; + for (Transition t : stateModel.getTransitions()) { + if (t.isFromState(oldState) && t.isToState(newState)) { + 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; + } + + /** + * Return the two State's common StateModel, or {@code null} if they don't have one in common. + * + * @param oldState + * @param newState + * @return the state model which contains both of the states. + * @throws Message if the state model of one of the states cannot be constructed. + */ + private StateModel findMatchingStateModel(State oldState, State newState) throws Message { + if (oldState.getStateModel().equals(newState.getStateModel())) { + return oldState.getStateModel(); + } + return null; + } + + /** + * Check if the old state is final or if the {@link FORCE_FINAL_STATE} flag is true. + * + * @param oldState + * @throws Message if the state is not final. + */ + private void checkFinalState(State oldState) throws Message { + if (!oldState.isFinal()) { + if (isForceFinal()) { + getUser().checkPermission(PERMISSION_STATE_FORCE_FINAL); + } else { + throw FINAL_STATE_NOT_ALLOWED; + } + } + getUser().checkPermission(PERMISSION_STATE_UNASSIGN + oldState.getStateModelName()); + } + + /** + * Check if the new state is an initial state. + * + * @param newState + * @throws Message if not + */ + private void checkInitialState(State newState) throws Message { + if (!newState.isInitial()) { + throw INITIAL_STATE_NOT_ALLOWED; + } + 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/CheckValueParsable.java b/src/main/java/org/caosdb/server/jobs/core/CheckValueParsable.java index 34e1960961f72118567653a77e4f48fe357dfedc..1dd457143f8cc23c0136f2c51cb40edf8552ba15 100644 --- a/src/main/java/org/caosdb/server/jobs/core/CheckValueParsable.java +++ b/src/main/java/org/caosdb/server/jobs/core/CheckValueParsable.java @@ -28,6 +28,7 @@ import org.caosdb.server.entity.Message; import org.caosdb.server.jobs.EntityJob; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.Observable; +import org.caosdb.server.utils.Observer; /** * Check whether the value of an entity is parsable according to the entity's data type. This job @@ -37,7 +38,7 @@ import org.caosdb.server.utils.Observable; * * @author tf */ -public class CheckValueParsable extends EntityJob { +public class CheckValueParsable extends EntityJob implements Observer { @Override public final void run() { @@ -96,7 +97,6 @@ public class CheckValueParsable extends EntityJob { // Therefore, not the whole test has to be run again. if (getEntity().hasDatatype()) { parseValue(); - super.notifyObservers(e); return false; } } diff --git a/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java new file mode 100644 index 0000000000000000000000000000000000000000..c20b18b6e4950c07967d2cfcdd776c95c25f4a53 --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java @@ -0,0 +1,857 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +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; +import java.util.Map.Entry; +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; +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.DeleteEntity; +import org.caosdb.server.entity.EntityInterface; +import org.caosdb.server.entity.Message; +import org.caosdb.server.entity.Message.MessageType; +import org.caosdb.server.entity.StatementStatus; +import org.caosdb.server.entity.container.TransactionContainer; +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; + +/** + * The EntityStateJob is the abstract base class for four EntityJobs: + * + * <p>1. The {@link InitEntityState} job reads ClientMessages or StateProperties with tag state and + * converts them into instances of State. This job runs during WriteTransactions. This job runs + * during the INIT Phase and does not perform any checks other than those necessary for the + * conversion. + * + * <p>2. The {@link CheckStateTransition} job checks if the attempted state transition is in + * compliance with the state model. This job runs during the CHECK phase and should do all necessary + * consistency and permission checks. + * + * <p>3. The {@link MakeStateProperty} job constructs an ordinary Property from the State right + * before the entity is being written to the back-end and after any checks run. + * + * <p>4. The {@link MakeStateMessage} job converts a state property (back) into State messages and + * appends them to the entity. + * + * <p>Only the 4th job ({@link MakeStateMessage}) runs during Retrieve transitions. During + * WriteTransactions all four jobs do run. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +public abstract class EntityStateJob extends EntityJob { + + protected static final String SERVER_PROPERTY_EXT_ENTITY_STATE = "EXT_ENTITY_STATE"; + + public static final String TO_STATE_PROPERTY_NAME = "to"; + public static final String FROM_STATE_PROPERTY_NAME = "from"; + public static final String FINAL_STATE_PROPERTY_NAME = "final"; + public static final String INITIAL_STATE_PROPERTY_NAME = "initial"; + 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 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 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."); + public static final Message STATE_NOT_IN_STATE_MODEL = + new Message(MessageType.Error, "State does not exist in this StateModel."); + public static final Message COULD_NOT_CONSTRUCT_STATE_MESSAGE = + new Message(MessageType.Error, "Could not construct the state message."); + public static final Message COULD_NOT_CONSTRUCT_TRANSITIONS = + new Message(MessageType.Error, "Could not construct the transitions."); + public static final Message STATE_MODEL_NOT_SPECIFIED = + new Message(MessageType.Error, "State model not specified."); + public static final Message STATE_NOT_SPECIFIED = + new Message(MessageType.Error, "State not specified."); + + /** + * Represents a Transition which is identified by a name and the two States from and to which an + * entity is being transitioned. + * + * <p>Currently, only exactly one toState and one fromState can be defined. However, it might be + * allowed in the future to have multiple states here. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ + public class Transition { + + private String name; + private String description; + private State fromState; + private State toState; + private Map<String, String> transitionProperties; + + /** + * @param transition The transition Entity, from which the Transition is created. Relevant + * Properties are "to" and "from" + */ + 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 { + for (Property p : transition.getProperties()) { + if (p.getName().equals(TO_STATE_PROPERTY_NAME)) { + return createState(p); + } + } + return null; + } + + private State getFromState(EntityInterface transition) throws Message { + for (Property p : transition.getProperties()) { + if (p.getName().equals(FROM_STATE_PROPERTY_NAME)) { + return createState(p); + } + } + return null; + } + + /** + * @param previousState + * @return true iff the previous state is a fromState of this transition. + */ + public boolean isFromState(State previousState) { + return this.fromState.equals(previousState); + } + + /** + * @param nextState + * @return true iff the next state is a toState of this transition. + */ + public boolean isToState(State nextState) { + return this.toState.equals(nextState); + } + + public State getToState() { + return this.toState; + } + + public State getFromState() { + return this.fromState; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Transition) { + Transition that = (Transition) obj; + return Objects.equals(this.getName(), that.getName()) + && Objects.equals(this.getFromState(), that.getFromState()) + && Objects.equals(this.getToState(), that.getToState()); + } + return false; + } + + public String getName() { + return this.name; + } + + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + return "Transition (name=" + + getName() + + ", from=" + + getFromState().getStateName() + + ", to=" + + getToState().getStateName() + + ")"; + } + + public Element toElement() { + Element result = new Element(TRANSITION_XML_TAG); + 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(PERMISSION_STATE_TRANSION + this.name); + } + } + + /** + * The State instance represents a single entity state. This class is used for concrete states + * (the state of a stateful entity, say a Record) and abstract states (states which are part of a + * {@link StateModel}). + * + * <p>States are identified via their name and the name of the model to which they belong. + * + * <p>States are represented by Records with the state's name as the Record name. They belong to a + * StateModel iff the StateModel RecordType references the State Record. Each State should only + * belong to one StateModel. + * + * <p>Furthermore, States are the start or end point of {@link Transition Transitions} which + * belong to the same StateModel. Each State can be part of several transitions at the same time. + * + * <p>Note: The purpose of this should not be confused with {@link EntityStatus} which is purely + * for internal use. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ + public class State implements ToElementable { + + private String stateModelName = null; + private String stateName = null; + private EntityInterface stateEntity = null; + private EntityInterface stateModelEntity = null; + private StateModel stateModel; + private String stateDescription = null; + private Integer stateId = null; + private EntityACL stateACL = null; + private Map<String, String> stateProperties; + + 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()); + 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(ENTITY_STATE_ROLE_MARKER)) { + 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 + public boolean equals(Object obj) { + if (obj instanceof State) { + State that = (State) obj; + return Objects.equals(that.getStateName(), this.getStateName()) + && Objects.equals(that.getStateModelName(), this.getStateModelName()); + } + return false; + } + + @Override + public int hashCode() { + return 21364234 + this.getStateName().hashCode() + this.getStateModelName().hashCode(); + } + + /** + * Serialize this State into XML. + * + * <p>The result looks approximately like this: {@code <State name="My name" model="Model's + * name"/>} + */ + @Override + public void addToElement(Element ret) { + Element e = new Element(STATE_XML_TAG); + if (this.stateProperties != null) { + this.stateProperties.forEach( + (String key, String value) -> { + e.setAttribute(key, value); + }); + } + 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.stateModel != null) { + this.stateModel.transitions.forEach( + (Transition t) -> { + if (t.isFromState(this) && t.isPermitted(getUser())) { + e.addContent(t.toElement()); + } + }); + } + ret.addContent(e); + } + + public String getStateModelName() { + return this.stateModelName; + } + + private String getStateName() { + return this.stateName; + } + + public StateModel getStateModel() throws Message { + if (this.stateModel == null) { + this.stateModel = new StateModel(this.stateModelEntity); + } + return this.stateModel; + } + + /** + * @return true iff this state is an initial state of its StateModel. + * @throws Message + */ + public boolean isInitial() throws Message { + return Objects.equals(this, getStateModel().initialState); + } + + /** + * @return true iff this state is a final state of its StateModel. + * @throws Message + */ + public boolean isFinal() throws Message { + return Objects.equals(this, getStateModel().finalState); + } + + /** + * Create a Property which represents the current entity state of a stateful entity. + * + * @return stateProperty + * @throws Message + */ + public Property createStateProperty() throws Message { + EntityInterface stateRecordType = getStateRecordType(); + Property stateProperty = new Property(stateRecordType); + stateProperty.setDatatype(new ReferenceDatatype2(stateRecordType)); + stateProperty.setValue(new ReferenceValue(getStateEntity(), false)); + stateProperty.setStatementStatus(StatementStatus.FIX); + return stateProperty; + } + + public EntityInterface getStateEntity() { + return this.stateEntity; + } + + public EntityInterface getStateModelEntity() { + return this.stateModelEntity; + } + + @Override + public String toString() { + String isInitial = null; + String isFinal = null; + try { + isInitial = String.valueOf(isInitial()); + } catch (Message e) { + isInitial = "null"; + } + try { + isFinal = String.valueOf(isFinal()); + } catch (Message e) { + isFinal = "null"; + } + return "State (name=" + + getStateName() + + ", model=" + + getStateModelName() + + ", initial=" + + isInitial + + ", final=" + + isFinal + + ")"; + } + } + + /** + * A StateModel is an abstract definition of a Finite State Machine for entities. + * + * <p>It consists of a set of States, a set of transitions, a initial state and a final state. + * + * <p>If the StateModel has no initial state, it cannot be initialized (no entity will ever be in + * any of the StateModel's states) without using the forceInitialState flag. + * + * <p>If the StateModel has not final state, an entity with any of the states from this StateModel + * cannot leave this StateModel (and cannot be deleted either) without using the forceFinalState + * flag. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ + public class StateModel { + + private String name; + private Set<State> states; + private Set<Transition> transitions; + private State initialState; + private State finalState; + + public StateModel(EntityInterface stateModelEntity) throws Message { + this.name = stateModelEntity.getName(); + this.transitions = getTransitions(stateModelEntity); + this.states = getStates(transitions, this); + this.finalState = getFinalState(stateModelEntity); + this.initialState = getInitialState(stateModelEntity); + } + + private State getInitialState(EntityInterface stateModelEntity) throws Message { + // TODO maybe check if there is more than one "initial" Property? + for (Property p : stateModelEntity.getProperties()) { + if (p.getName().equals(INITIAL_STATE_PROPERTY_NAME)) { + return createState(p); + } + } + return null; + } + + private State getFinalState(EntityInterface stateModelEntity) throws Message { + // TODO maybe check if there is more than one "final" Property? + for (Property p : stateModelEntity.getProperties()) { + if (p.getName().equals(FINAL_STATE_PROPERTY_NAME)) { + return createState(p); + } + } + return null; + } + + /** Transitions are taken from list Property with name="Transition". */ + private Set<Transition> getTransitions(EntityInterface stateModelEntity) throws Message { + for (Property p : stateModelEntity.getProperties()) { + if (p.getName().equals(TRANSITION_RECORD_TYPE_NAME)) { + return createTransitions(p); + } + } + return null; + } + + /** + * Read out the "Transition" property and create Transition instances. + * + * @param p the transition property + * @return a set of transitions + * @throws Message if the transitions could ne be created. + */ + private Set<Transition> createTransitions(Property p) throws Message { + Set<Transition> result = new LinkedHashSet<>(); + try { + if (!(p.getDatatype() instanceof AbstractCollectionDatatype)) { + // FIXME raise an exception instead? + return result; + } + p.parseValue(); + CollectionValue vals = (CollectionValue) p.getValue(); + for (IndexedSingleValue val : vals) { + if (val.getWrapped() instanceof ReferenceValue) { + Integer refid = ((ReferenceValue) val.getWrapped()).getId(); + + String key = "transition" + Integer.toString(refid); + EntityInterface transition = getCached(key); + if (transition == null) { + transition = retrieveValidEntity(refid); + putCache(key, transition); + } + result.add(new Transition(transition)); + } + } + } catch (Exception e) { + throw COULD_NOT_CONSTRUCT_TRANSITIONS; + } + return result; + } + + /** + * Collect all possible states from the set of transitions. + * + * <p>This function does not perform any consistency checks. It only add all toStates and + * fromStates of the transitions to the result. + * + * @param transitions + * @param stateModel + * @return set of states. + * @throws Message + */ + private Set<State> getStates(Set<Transition> transitions, StateModel stateModel) + throws Message { + // TODO Move outside of this class + Iterator<Transition> it = transitions.iterator(); + Set<State> result = new LinkedHashSet<>(); + while (it.hasNext()) { + Transition t = it.next(); + result.add(t.getFromState()); + result.add(t.getToState()); + } + return result; + } + + public String getName() { + return this.name; + } + + public Set<State> getStates() { + return this.states; + } + + public Set<Transition> getTransitions() { + return this.transitions; + } + + public State getFinalState() { + return this.finalState; + } + + public State getInitialState() { + return this.initialState; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof StateModel) { + return ((StateModel) obj).getName().equals(this.getName()); + } + return false; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("StateModel (name="); + sb.append(this.getName()); + sb.append(", initial="); + sb.append(this.getInitialState().stateName); + sb.append(", final="); + sb.append(this.getFinalState().stateName); + sb.append(", transitions=["); + Iterator<Transition> iterator = this.transitions.iterator(); + while (iterator.hasNext()) { + sb.append(iterator.next().name); + sb.append(" -> "); + sb.append(iterator.next().name); + sb.append(", "); + } + sb.append("])"); + return sb.toString(); + } + } + + private EntityInterface retrieveStateEntity(String stateName) throws Message { + try { + return retrieveValidEntity(retrieveValidIDByName(stateName)); + } catch (EntityDoesNotExistException e) { + throw STATE_NOT_IN_STATE_MODEL; + } + } + + private EntityInterface retrieveStateModelEntity(String stateModel) throws Message { + try { + return retrieveValidEntity(retrieveValidIDByName(stateModel)); + } catch (EntityDoesNotExistException e) { + throw STATE_MODEL_NOT_FOUND; + } + } + + protected EntityInterface getStateRecordType() throws Message { + EntityInterface stateRecordType = getCached(STATE_RECORD_TYPE_NAME); + if (stateRecordType == null) { + stateRecordType = retrieveValidSparseEntityByName(STATE_RECORD_TYPE_NAME); + putCache(STATE_RECORD_TYPE_NAME, stateRecordType); + } + return stateRecordType; + } + + protected State getState() { + return getState(false); + } + + protected State getState(EntityInterface entity) { + return getState(entity, false); + } + + protected State getState(EntityInterface entity, boolean remove) { + Iterator<ToElementable> messages = entity.getMessages().iterator(); + while (messages.hasNext()) { + ToElementable s = messages.next(); + if (s instanceof State) { + if (remove) { + messages.remove(); + } + return (State) s; + } + } + return null; + } + + protected State getState(boolean remove) { + return getState(getEntity(), remove); + } + + /** Return (and possibly remove) the States Properties of `entity`. */ + protected List<Property> getStateProperties(EntityInterface entity, boolean remove) { + Iterator<Property> it = entity.getProperties().iterator(); + List<Property> result = new ArrayList<>(); + while (it.hasNext()) { + Property p = it.next(); + if (Objects.equals(p.getName(), STATE_RECORD_TYPE_NAME)) { + if (!(p.getDatatype() instanceof ReferenceDatatype)) { + continue; + } + if (remove) { + it.remove(); + } + result.add(p); + } + } + return result; + } + + protected List<Property> getStateProperties(boolean remove) { + return getStateProperties(getEntity(), remove); + } + + /** Get the {@code ClientMessage}s which denote a state. */ + protected List<ClientMessage> getStateClientMessages(EntityInterface entity, boolean remove) { + Iterator<ToElementable> stateMessages = entity.getMessages().iterator(); + List<ClientMessage> result = new ArrayList<>(); + while (stateMessages.hasNext()) { + ToElementable s = stateMessages.next(); + if (s instanceof ClientMessage && STATE_XML_TAG.equals(((ClientMessage) s).getType())) { + if (remove) { + stateMessages.remove(); + } + result.add((ClientMessage) s); + } + } + return result; + } + + protected List<ClientMessage> getStateClientMessages(boolean remove) { + return getStateClientMessages(getEntity(), remove); + } + + protected State createState(ClientMessage s) throws Message { + String stateModel = s.getProperty(STATE_ATTRIBUTE_MODEL); + if (stateModel == null) { + throw STATE_MODEL_NOT_SPECIFIED; + } + String stateName = s.getProperty(STATE_ATTRIBUTE_NAME); + if (stateName == null) { + throw STATE_NOT_SPECIFIED; + } + String stateModelKey = "statemodel:" + stateModel; + + EntityInterface stateModelEntity = getCached(stateModelKey); + if (stateModelEntity == null) { + stateModelEntity = retrieveStateModelEntity(stateModel); + putCache(stateModelKey, stateModelEntity); + } + + String stateKey = "namestate:" + stateName; + + EntityInterface stateEntity = getCached(stateKey); + if (stateEntity == null) { + stateEntity = retrieveStateEntity(stateName); + putCache(stateKey, stateEntity); + } + return new State(stateEntity, stateModelEntity); + } + + /** + * Create a State instance from the value of the state property. + * + * <p>This method also retrieves the state entity from the back-end. The StateModel is deduced + * from finding an appropriately referencing StateModel Record. + * + * @param p the entity's state property + * @return The state of the entity + * @throws Message + */ + protected State createState(Property p) throws Message { + try { + p.parseValue(); + ReferenceValue refid = (ReferenceValue) p.getValue(); + String key = "idstate" + Integer.toString(refid.getId()); + + EntityInterface stateEntity = getCached(key); + if (stateEntity == null) { + stateEntity = retrieveValidEntity(refid.getId()); + putCache(key, stateEntity); + } + + EntityInterface stateModelEntity = findStateModel(stateEntity); + return new State(stateEntity, stateModelEntity); + } catch (Message e) { + throw e; + } catch (Exception e) { + throw COULD_NOT_CONSTRUCT_STATE_MESSAGE; + } + } + + private static final Map<String, EntityInterface> cache = new HashMap<>(); + private static final Set<Integer> id_in_cache = new HashSet<>(); + + EntityInterface findStateModel(EntityInterface stateEntity) throws Exception { + boolean cached = true; + String key = "modelof" + Integer.toString(stateEntity.getId()); + + EntityInterface result = getCached(key); + if (result != null && cached) { + return result; + } + // TODO This should throw a meaningful Exception if no matching StateModel can be found. + TransactionContainer c = new TransactionContainer(); + Query query = + new Query( + "FIND RECORD " + + STATE_MODEL_RECORD_TYPE_NAME + + " WHICH REFERENCES " + + TRANSITION_RECORD_TYPE_NAME + + " WHICH REFERENCES " + + Integer.toString(stateEntity.getId()), + getUser(), + c); + query.execute(getTransaction().getAccess()); + result = retrieveValidEntity(c.get(0).getId()); + putCache(key, result); + return result; + } + + private EntityInterface getCached(String key) { + EntityInterface result; + synchronized (cache) { + result = cache.get(key); + } + return result; + } + + private void putCache(String key, EntityInterface value) { + synchronized (cache) { + if (value instanceof DeleteEntity) { + throw new RuntimeException("Delete entity in cache. This is an implementation error."); + } + id_in_cache.add(value.getId()); + cache.put(key, value); + } + } + + protected void removeCached(EntityInterface entity) { + synchronized (cache) { + if (id_in_cache.contains(entity.getId())) { + id_in_cache.remove(entity.getId()); + + List<String> remove = new LinkedList<>(); + for (Entry<String, EntityInterface> entry : cache.entrySet()) { + if (entry.getValue().getId().equals(entity.getId())) { + remove.add(entry.getKey()); + } + } + for (String key : remove) { + cache.remove(key); + } + } + } + } +} diff --git a/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java b/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java index b6583f024071e3c1151cf974b387ffe72d1579b7..7f5444b36104526101ebf9b3f2aa547b3d91cbc9 100644 --- a/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java +++ b/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java @@ -22,6 +22,7 @@ */ package org.caosdb.server.jobs.core; +import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.entity.Message; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.JobAnnotation; @@ -51,5 +52,8 @@ public class ExecuteQuery extends FlagJob { getContainer().addMessage(new Message(e.getMessage())); } getContainer().addMessage(queryInstance); + for (EntityInterface entity : getContainer()) { + getTransaction().getSchedule().addAll(loadJobs(entity, getTransaction())); + } } } diff --git a/src/main/java/org/caosdb/server/jobs/core/InheritInitialState.java b/src/main/java/org/caosdb/server/jobs/core/InheritInitialState.java new file mode 100644 index 0000000000000000000000000000000000000000..bda4b8ebe885aa49df13ecf36ab14663c7ead270 --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/core/InheritInitialState.java @@ -0,0 +1,36 @@ +package org.caosdb.server.jobs.core; + +import java.util.List; +import org.caosdb.server.entity.Message; +import org.caosdb.server.entity.wrapper.Parent; +import org.caosdb.server.entity.wrapper.Property; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.JobExecutionTime; + +@JobAnnotation(time = JobExecutionTime.CHECK) +public class InheritInitialState extends EntityStateJob { + + @Override + protected void run() { + try { + State parentState = getFirstParentState(); + if (parentState != null) { + getEntity().addMessage(parentState); + parentState.getStateEntity(); + getEntity().setEntityACL(parentState.getStateACL()); + } + } catch (Message e) { + getEntity().addError(e); + } + } + + private State getFirstParentState() throws Message { + for (Parent par : getEntity().getParents()) { + List<Property> stateProperties = getStateProperties(par, false); + if (stateProperties.size() > 0) { + return createState(stateProperties.get(0)); + } + } + return null; + } +} diff --git a/src/main/java/org/caosdb/server/jobs/core/Inheritance.java b/src/main/java/org/caosdb/server/jobs/core/Inheritance.java index d49bec09c909c5e884eae80619a5911a50b1f333..107c5768a5c15fbb05ea6d68b0cb956cbb235687 100644 --- a/src/main/java/org/caosdb/server/jobs/core/Inheritance.java +++ b/src/main/java/org/caosdb/server/jobs/core/Inheritance.java @@ -34,6 +34,7 @@ import org.caosdb.server.entity.StatementStatus; import org.caosdb.server.entity.UpdateEntity; import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.jobs.EntityJob; +import org.caosdb.server.jobs.ScheduledJob; import org.caosdb.server.utils.EntityStatus; /** @@ -65,7 +66,7 @@ public class Inheritance extends EntityJob { protected void run() { if (getEntity() instanceof InsertEntity || getEntity() instanceof UpdateEntity) { if (getEntity().hasParents()) { - final ArrayList<EntityInterface> transfer = new ArrayList<EntityInterface>(); + final ArrayList<Property> transfer = new ArrayList<>(); parentLoop: for (final EntityInterface parent : getEntity().getParents()) { try { @@ -100,23 +101,25 @@ public class Inheritance extends EntityJob { // transfer properties if they are not implemented yet outerLoop: - for (final EntityInterface prop : transfer) { - for (final EntityInterface eprop : getEntity().getProperties()) { + for (final Property prop : transfer) { + for (final Property eprop : getEntity().getProperties()) { if (prop.hasId() && eprop.hasId() && prop.getId().equals(eprop.getId())) { continue outerLoop; } } // prop's Datatype might need to be resolved. - this.appendJob(prop, CheckDatatypePresent.class); - getEntity().addProperty(new Property(prop)); + ScheduledJob job = this.appendJob(prop, CheckDatatypePresent.class); + getTransaction().getSchedule().runJob(job); + + getEntity().addProperty(new Property(prop.getWrapped())); } } // implement properties if (getEntity().hasProperties()) { propertyLoop: - for (final EntityInterface property : getEntity().getProperties()) { - final ArrayList<EntityInterface> transfer = new ArrayList<EntityInterface>(); + for (final Property property : getEntity().getProperties()) { + final ArrayList<Property> transfer = new ArrayList<>(); try { if (property.getFlags().get("inheritance") == null) { break propertyLoop; @@ -156,15 +159,17 @@ public class Inheritance extends EntityJob { // transfer properties if they are not implemented yet outerLoop: - for (final EntityInterface prop : transfer) { - for (final EntityInterface eprop : property.getProperties()) { + for (final Property prop : transfer) { + for (final Property eprop : property.getProperties()) { if (prop.hasId() && eprop.hasId() && prop.getId() == eprop.getId()) { continue outerLoop; } } // prop's Datatype might need to be resolved. - this.appendJob(prop, CheckDatatypePresent.class); - property.addProperty(new Property(prop)); + ScheduledJob job = this.appendJob(prop, CheckDatatypePresent.class); + getTransaction().getSchedule().runJob(job); + + property.addProperty(new Property(prop.getWrapped())); } } } @@ -182,9 +187,9 @@ public class Inheritance extends EntityJob { * @param inheritance */ private void collectInheritedProperties( - List<EntityInterface> transfer, EntityInterface from, INHERITANCE_MODE inheritance) { + List<Property> transfer, EntityInterface from, INHERITANCE_MODE inheritance) { if (from.hasProperties()) { - for (final EntityInterface propProperty : from.getProperties()) { + for (final Property propProperty : from.getProperties()) { switch (inheritance) { // the following cases are ordered according to their importance level and use a // fall-through. diff --git a/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java new file mode 100644 index 0000000000000000000000000000000000000000..7dbff695a342e5dcd2a19a2d72116c14fba78a6c --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java @@ -0,0 +1,200 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +package org.caosdb.server.jobs.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.caosdb.server.CaosDBServer; +import org.caosdb.server.entity.ClientMessage; +import org.caosdb.server.entity.DeleteEntity; +import org.caosdb.server.entity.Entity; +import org.caosdb.server.entity.EntityInterface; +import org.caosdb.server.entity.InsertEntity; +import org.caosdb.server.entity.Message; +import org.caosdb.server.entity.Message.MessageType; +import org.caosdb.server.entity.Role; +import org.caosdb.server.entity.UpdateEntity; +import org.caosdb.server.entity.WritableEntity; +import org.caosdb.server.entity.wrapper.Property; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.transaction.WriteTransaction; +import org.caosdb.server.utils.EntityStatus; +import org.caosdb.server.utils.Observable; +import org.caosdb.server.utils.Observer; + +/** + * Initialize the other entity jobs by converting the client message with type "State" or + * StateProperties into {@link State} instances. + * + * <p>This job also needs to initialize the other jobs even if the current entity version does not + * have a state anymore but the previous version had, because it has to be checked if the stateModel + * allows to leave in this state. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +@JobAnnotation( + loadAlways = true, + time = JobExecutionTime.INIT, + transaction = WriteTransaction.class) +public class InitEntityStateJobs extends EntityStateJob implements Observer { + + @Override + protected void run() { + if ("ENABLED".equals(CaosDBServer.getServerProperty(SERVER_PROPERTY_EXT_ENTITY_STATE))) { + State newState = handleNewState(); + State oldState = handleOldState(newState); + if (newState != null || oldState != null) { + if (!(getEntity() instanceof DeleteEntity)) { + + appendJob(MakeStateProperty.class); + } + appendJob(CheckStateTransition.class); + appendJob(MakeStateMessage.class); + } else if (newState == null + && getEntity().getRole() == Role.Record + && getEntity() instanceof InsertEntity) { + appendJob(InheritInitialState.class); + appendJob(CheckStateTransition.class); + appendJob(MakeStateProperty.class); + appendJob(MakeStateMessage.class); + } + if (getEntity() instanceof WritableEntity || getEntity() instanceof DeleteEntity) { + removeCached(getEntity()); + } + } + } + + /** + * Converts the state property of the original entity into a state message (only needed for + * updates). + * + * <p>Also, this method adds an observer to the entity state which handles a corner case where the + * entity changes the state, but no other property changes. In this case Update.deriveUpdate + * cannot detect any changes and will mark this entity as "to-be-skipped". The observer waits for + * that to happen and changes the {@EntityStatus} back to normal. + * + * @param newState + * @return The old state or null. + */ + private State handleOldState(State newState) { + State oldState = null; + try { + if (getEntity() instanceof UpdateEntity) { + List<State> states = initStateMessage(((UpdateEntity) getEntity()).getOriginal()); + oldState = null; + if (states.size() == 1) { + oldState = states.get(0); + if (newState != null) { + ((UpdateEntity) getEntity()).getOriginal().setEntityACL(getEntity().getEntityACL()); + } + } + if (!Objects.equals(newState, oldState)) { + getEntity().acceptObserver(this); + } + } + } catch (Message m) { + getEntity().addWarning(STATE_ERROR_IN_ORIGINAL_ENTITY(m)); + } + return oldState; + } + + /** + * Converts the state property of this entity into a state message. + * + * @return The new state or null. + */ + private State handleNewState() { + State newState = null; + try { + List<State> states = initStateMessage(getEntity()); + if (states.size() > 1) { + throw new Message( + MessageType.Error, "Currently, each entity can only have one state at a time."); + } else if (states.size() == 1) { + newState = states.get(0); + if (getEntity().getRole() == Role.Record) { + 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()); + } + + /** + * Return a list of states from their representations as properties or client messages in the + * entity. + * + * @param entity + * @return list of state instances for the entity. + * @throws Message + */ + private List<State> initStateMessage(EntityInterface entity) throws Message { + List<ClientMessage> stateClientMessages = getStateClientMessages(entity, true); + List<State> result = new ArrayList<>(); + if (stateClientMessages != null) { + for (ClientMessage s : stateClientMessages) { + State stateMessage = createState(s); + entity.addMessage(stateMessage); + result.add(stateMessage); + } + } + List<Property> stateProperties = getStateProperties(entity, true); + if (stateProperties != null) { + for (Property p : stateProperties) { + State stateMessage = createState(p); + entity.addMessage(stateMessage); + result.add(stateMessage); + } + } + return result; + } + + @Override + public boolean notifyObserver(String e, Observable o) { + if (e == Entity.ENTITY_STATUS_CHANGED_EVENT) { + if (o == getEntity() && getEntity().getEntityStatus() == EntityStatus.VALID) { + // The Update.deriveUpdate method didn't recognize that the state is changing and set the + // entity to "VALID" + getEntity().setEntityStatus(EntityStatus.QUALIFIED); + return false; + } + } + return true; + } +} diff --git a/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java b/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..6e98704a5f6d73199993f2ed531e6d6277934e64 --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java @@ -0,0 +1,108 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +package org.caosdb.server.jobs.core; + +import java.util.List; +import java.util.Map; +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 + * which is then being appended to the entity's messages. + * + * <p>If this job belongs to a Write transaction there is already a State instance present and the + * conversion is not necessary. + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +@JobAnnotation( + loadAlways = true, + transaction = Retrieve.class, + time = JobExecutionTime.POST_TRANSACTION) +public class MakeStateMessage extends EntityStateJob { + + public static final String SPARSE_FLAG = "sparseState"; + + @Override + protected void run() { + + if ("ENABLED".equals(CaosDBServer.getServerProperty(SERVER_PROPERTY_EXT_ENTITY_STATE))) { + try { + // fetch all state properties and remove them from the entity (indicated by "true") + List<Property> stateProperties = getStateProperties(true); + State stateMessage = getState(false); + + if (stateMessage != null) { + // trigger retrieval of state model because when the XML Writer calls the addToElement + // method, it is to late. + stateMessage.getStateModel(); + } else if (stateProperties != null && stateProperties.size() > 0) { + for (Property s : stateProperties) { + getEntity().addMessage(getMessage(s, isSparse())); + } + } + } catch (Message e) { + getEntity().addError(e); + } + } + } + + private ToElementable getMessage(Property s, boolean sparse) throws Message { + if (sparse) { + return getSparseStateMessage(s); + } + State stateMessage = createState(s); + + // trigger retrieval of state model because when the XML Writer calls the addToElement method, + // it is to late. + stateMessage.getStateModel(); + return stateMessage; + } + + private ToElementable getSparseStateMessage(Property s) { + return 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); + } + }; + } + + private boolean isSparse() { + Map<String, String> flags = getTransaction().getContainer().getFlags(); + if (flags != null) { + return "true".equals(flags.getOrDefault(SPARSE_FLAG, "false")); + } + return false; + } +} diff --git a/src/main/java/org/caosdb/server/jobs/core/MakeStateProperty.java b/src/main/java/org/caosdb/server/jobs/core/MakeStateProperty.java new file mode 100644 index 0000000000000000000000000000000000000000..56682921563d77f462903567a2c128fcd5d92555 --- /dev/null +++ b/src/main/java/org/caosdb/server/jobs/core/MakeStateProperty.java @@ -0,0 +1,53 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * ** end header + */ + +package org.caosdb.server.jobs.core; + +import org.caosdb.server.entity.Message; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.JobExecutionTime; +import org.caosdb.server.transaction.WriteTransaction; + +/** + * This job constructs an ordinary Property from the State right before the entity is being written + * to the back-end and after any checks run. + */ +@JobAnnotation(transaction = WriteTransaction.class, time = JobExecutionTime.PRE_TRANSACTION) +public class MakeStateProperty extends EntityStateJob { + + @Override + protected void run() { + State s = getState(); + if (s != null) { + try { + addStateProperty(s); + } catch (Message e) { + getEntity().addError(e); + } + } + } + + private void addStateProperty(State stateEntity) throws Message { + getEntity().addProperty(stateEntity.createStateProperty()); + } +} diff --git a/src/main/java/org/caosdb/server/jobs/core/PickUp.java b/src/main/java/org/caosdb/server/jobs/core/PickUp.java index dff574a4ad0aa5199fe3ccdfdd9c72d2a7b2bb67..b91fccf72d54022c6f6d19ba26c2b0974a54829f 100644 --- a/src/main/java/org/caosdb/server/jobs/core/PickUp.java +++ b/src/main/java/org/caosdb/server/jobs/core/PickUp.java @@ -33,9 +33,10 @@ import org.caosdb.server.jobs.JobAnnotation; import org.caosdb.server.jobs.JobExecutionTime; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.Observable; +import org.caosdb.server.utils.Observer; @JobAnnotation(time = JobExecutionTime.INIT) -public class PickUp extends EntityJob { +public class PickUp extends EntityJob implements Observer { @Override protected void run() { diff --git a/src/main/java/org/caosdb/server/jobs/core/ProcessNameProperties.java b/src/main/java/org/caosdb/server/jobs/core/ProcessNameProperties.java index 950c61d36255b68f52e3066616b1dee5d9f259c3..e7e8f0e4d6aab3f3d0b3e9392d408a88aaa0abcc 100644 --- a/src/main/java/org/caosdb/server/jobs/core/ProcessNameProperties.java +++ b/src/main/java/org/caosdb/server/jobs/core/ProcessNameProperties.java @@ -34,14 +34,17 @@ import org.caosdb.server.entity.Message; import org.caosdb.server.entity.wrapper.Parent; import org.caosdb.server.entity.wrapper.Property; import org.caosdb.server.jobs.EntityJob; +import org.caosdb.server.jobs.JobAnnotation; +import org.caosdb.server.jobs.JobExecutionTime; import org.caosdb.server.utils.EntityStatus; import org.caosdb.server.utils.ServerMessages; /** - * To be called after CheckPropValid. + * To be called after CheckPropValid and Inheritance. * * @author tf */ +@JobAnnotation(time = JobExecutionTime.PRE_TRANSACTION) public class ProcessNameProperties extends EntityJob { @Override diff --git a/src/main/java/org/caosdb/server/jobs/core/RetrieveAllJob.java b/src/main/java/org/caosdb/server/jobs/core/RetrieveAllJob.java index 1ca43cec45413d80c307df2f4d09c6b8d34cfb49..603d861ad24b659fab262291d888420c02f6bcf5 100644 --- a/src/main/java/org/caosdb/server/jobs/core/RetrieveAllJob.java +++ b/src/main/java/org/caosdb/server/jobs/core/RetrieveAllJob.java @@ -23,6 +23,7 @@ package org.caosdb.server.jobs.core; import org.caosdb.server.database.backend.transaction.RetrieveAll; +import org.caosdb.server.entity.EntityInterface; import org.caosdb.server.jobs.FlagJob; import org.caosdb.server.jobs.JobAnnotation; import org.caosdb.server.jobs.JobExecutionTime; @@ -37,6 +38,9 @@ public class RetrieveAllJob extends FlagJob { value = "ENTITY"; } execute(new RetrieveAll(getContainer(), value)); + for (EntityInterface entity : getContainer()) { + getTransaction().getSchedule().addAll(loadJobs(entity, getTransaction())); + } } } } diff --git a/src/main/java/org/caosdb/server/permissions/AbstractEntityACLFactory.java b/src/main/java/org/caosdb/server/permissions/AbstractEntityACLFactory.java index 3a3d03d18bc7b0913a522e2edd6c235f9a7d39a7..c8cd93e4a1d12125235819b4a2ba69fa12bd0256 100644 --- a/src/main/java/org/caosdb/server/permissions/AbstractEntityACLFactory.java +++ b/src/main/java/org/caosdb/server/permissions/AbstractEntityACLFactory.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; @@ -168,32 +169,75 @@ public abstract class AbstractEntityACLFactory<T extends EntityACL> { return create(acis); } + /** + * Normalize the permission rules. + * + * <p>This means that rules which are overriden by other rules are removed. E.g. a granting rule + * for the permission X and the agent P would be removed if there is a denial of X (for P) with + * the same or a higher priority. Likewise, A denial of Y for agent Q would be removed if there is + * a granting rule of Y (for Q) with a higher priority. + */ private void normalize() { - for (final Entry<ResponsibleAgent, Long> set : this.priorityDenials.entrySet()) { - if (this.priorityGrants.containsKey(set.getKey())) { - this.priorityGrants.put( - set.getKey(), this.priorityGrants.get(set.getKey()) & ~set.getValue()); + // 1. run through all prioritized denials and remove overriden rules + // (priority grants, normal grants and normal denials) + Iterator<Entry<ResponsibleAgent, Long>> iterator = this.priorityDenials.entrySet().iterator(); + while (iterator.hasNext()) { + Entry<ResponsibleAgent, Long> next = iterator.next(); + final ResponsibleAgent agent = next.getKey(); + long bitset = next.getValue(); + if (bitset == 0L) { + iterator.remove(); + continue; } - if (this.normalDenials.containsKey(set.getKey())) { - this.normalDenials.put( - set.getKey(), this.normalDenials.get(set.getKey()) & ~set.getValue()); + if (this.priorityGrants.containsKey(agent)) { + this.priorityGrants.put(agent, this.priorityGrants.get(agent) & ~bitset); } - if (this.normalGrants.containsKey(set.getKey())) { - this.normalGrants.put(set.getKey(), this.normalGrants.get(set.getKey()) & ~set.getValue()); + if (this.normalDenials.containsKey(agent)) { + this.normalDenials.put(agent, this.normalDenials.get(agent) & ~bitset); + } + if (this.normalGrants.containsKey(agent)) { + this.normalGrants.put(agent, this.normalGrants.get(agent) & ~bitset); + } + } + // 2. run through all prioritized grants and remove overriden rules (normal + // denials and grants) + iterator = this.priorityGrants.entrySet().iterator(); + while (iterator.hasNext()) { + Entry<ResponsibleAgent, Long> next = iterator.next(); + final ResponsibleAgent agent = next.getKey(); + long bitset = next.getValue(); + if (bitset == 0L) { + iterator.remove(); + continue; + } + if (this.normalDenials.containsKey(agent)) { + this.normalDenials.put(agent, this.normalDenials.get(agent) & ~bitset); + } + if (this.normalGrants.containsKey(agent)) { + this.normalGrants.put(agent, this.normalGrants.get(agent) & ~bitset); } } - for (final Entry<ResponsibleAgent, Long> set : this.priorityGrants.entrySet()) { - if (this.normalDenials.containsKey(set.getKey())) { - this.normalDenials.put( - set.getKey(), this.normalDenials.get(set.getKey()) & ~set.getValue()); + // 3. run through all normal denials and remove overriden rules (normal grants) + iterator = this.normalDenials.entrySet().iterator(); + while (iterator.hasNext()) { + Entry<ResponsibleAgent, Long> next = iterator.next(); + final ResponsibleAgent agent = next.getKey(); + long bitset = next.getValue(); + if (bitset == 0L) { + iterator.remove(); + continue; } - if (this.normalGrants.containsKey(set.getKey())) { - this.normalGrants.put(set.getKey(), this.normalGrants.get(set.getKey()) & ~set.getValue()); + if (this.normalGrants.containsKey(agent)) { + this.normalGrants.put(agent, this.normalGrants.get(agent) & ~bitset); } } - for (final Entry<ResponsibleAgent, Long> set : this.normalDenials.entrySet()) { - if (this.normalGrants.containsKey(set.getKey())) { - this.normalGrants.put(set.getKey(), this.normalGrants.get(set.getKey()) & ~set.getValue()); + // finally, remove all remaining empty grants + iterator = this.normalGrants.entrySet().iterator(); + while (iterator.hasNext()) { + Entry<ResponsibleAgent, Long> next = iterator.next(); + long bitset = next.getValue(); + if (bitset == 0L) { + iterator.remove(); } } } @@ -206,4 +250,63 @@ public abstract class AbstractEntityACLFactory<T extends EntityACL> { } protected abstract T create(Collection<EntityACI> acis); + + /** + * Remove all rules of the `other` EntityACL from this factory. + * + * <p>This is mainly used for removing all rules which belong to the global entity ACL from this + * ACL before storing it to the backend. + * + * @param other + * @return the same object with changed rule set. + */ + public AbstractEntityACLFactory<T> remove(EntityACL other) { + if (other != null) { + normalize(); + for (EntityACI aci : other.getRules()) { + if (EntityACL.isAllowance(aci.getBitSet())) { + if (EntityACL.isPriorityBitSet(aci.getBitSet())) { + Long bitset = this.priorityGrants.get(aci.getResponsibleAgent()); + if (bitset == null) { + continue; + } + long bitset2 = bitset; + bitset2 &= aci.getBitSet(); + bitset ^= bitset2; + this.priorityGrants.put(aci.getResponsibleAgent(), bitset); + } else { + Long bitset = this.normalGrants.get(aci.getResponsibleAgent()); + if (bitset == null) { + continue; + } + long bitset2 = bitset; + bitset2 &= aci.getBitSet(); + bitset ^= bitset2; + this.normalGrants.put(aci.getResponsibleAgent(), bitset); + } + } else { + if (EntityACL.isPriorityBitSet(aci.getBitSet())) { + Long bitset = this.priorityDenials.get(aci.getResponsibleAgent()); + if (bitset == null) { + continue; + } + long bitset2 = bitset; + bitset2 &= aci.getBitSet(); + bitset ^= bitset2; + this.priorityDenials.put(aci.getResponsibleAgent(), bitset); + } else { + Long bitset = this.normalDenials.get(aci.getResponsibleAgent()); + if (bitset == null) { + continue; + } + long bitset2 = bitset; + bitset2 &= aci.getBitSet(); + bitset ^= bitset2; + this.normalDenials.put(aci.getResponsibleAgent(), bitset); + } + } + } + } + return this; + } } diff --git a/src/main/java/org/caosdb/server/permissions/CaosPermission.java b/src/main/java/org/caosdb/server/permissions/CaosPermission.java index 1b793bb27f6a14bcf7fd0c5f828c20b73f3d11e5..bfbb7eb2e8515f71bb2d611d4fd75916cfae50e7 100644 --- a/src/main/java/org/caosdb/server/permissions/CaosPermission.java +++ b/src/main/java/org/caosdb/server/permissions/CaosPermission.java @@ -24,7 +24,9 @@ package org.caosdb.server.permissions; import java.util.HashSet; import java.util.Map; +import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.Permission; +import org.apache.shiro.subject.Subject; import org.eclipse.jetty.util.ajax.JSON; public class CaosPermission extends HashSet<PermissionRule> implements Permission { @@ -52,9 +54,10 @@ public class CaosPermission extends HashSet<PermissionRule> implements Permissio boolean grant = false; boolean deny = false; boolean grant_priority = false; + Subject subject = SecurityUtils.getSubject(); for (final PermissionRule r : this) { - if (r.getPermission().implies(p)) { + if (r.getPermission(subject).implies(p)) { if (r.isGrant()) { if (r.isPriority()) { grant_priority = true; diff --git a/src/main/java/org/caosdb/server/permissions/EntityACL.java b/src/main/java/org/caosdb/server/permissions/EntityACL.java index df1915bd460079eb52dfc6d4f3bbf4fe42795918..cfa436d59ae25971a08d4314a7f668a70cf75bbf 100644 --- a/src/main/java/org/caosdb/server/permissions/EntityACL.java +++ b/src/main/java/org/caosdb/server/permissions/EntityACL.java @@ -253,7 +253,9 @@ public class EntityACL { public final Element toElement() { final Element ret = new Element("EntityACL"); - for (final EntityACI aci : this.acl) { + final List<EntityACI> localAcl = new ArrayList<>(this.acl); + localAcl.addAll(GLOBAL_PERMISSIONS.acl); + for (final EntityACI aci : localAcl) { final boolean isDenial = isDenial(aci.getBitSet()); final boolean isPriority = isPriorityBitSet(aci.getBitSet()); final Element e = new Element(isDenial ? "Deny" : "Grant"); @@ -323,7 +325,7 @@ public class EntityACL { } } } - return factory.create(); + return factory.remove(GLOBAL_PERMISSIONS).create(); } public static BitSet convert(final long value) { @@ -379,7 +381,9 @@ public class EntityACL { public Element getPermissionsFor(final Subject subject) { final Element ret = new Element("Permissions"); - final Set<EntityPermission> permissionsFor = getPermissionsFor(subject, this.acl); + final List<EntityACI> localAcl = new ArrayList<>(this.acl); + localAcl.addAll(GLOBAL_PERMISSIONS.acl); + final Set<EntityPermission> permissionsFor = getPermissionsFor(subject, localAcl); for (final EntityPermission p : permissionsFor) { ret.addContent(p.toElement()); } diff --git a/src/main/java/org/caosdb/server/permissions/PermissionRule.java b/src/main/java/org/caosdb/server/permissions/PermissionRule.java index b6ee915156771e1164f3a0ee221d3e14ab964833..85d3b62834a67a4fc46ea9b3c38d19e4b8261d74 100644 --- a/src/main/java/org/caosdb/server/permissions/PermissionRule.java +++ b/src/main/java/org/caosdb/server/permissions/PermissionRule.java @@ -26,23 +26,21 @@ import java.util.HashMap; import java.util.Map; import org.apache.shiro.authz.Permission; import org.apache.shiro.authz.permission.WildcardPermission; +import org.apache.shiro.subject.Subject; +import org.caosdb.server.accessControl.Principal; import org.jdom2.Element; public class PermissionRule { - private final WildcardPermission permission; + private final String permission; private final boolean priority; private final boolean grant; public PermissionRule(final String grant, final String priority, final String permission) { - this( - Boolean.parseBoolean(grant), - Boolean.parseBoolean(priority), - new WildcardPermission(permission)); + this(Boolean.parseBoolean(grant), Boolean.parseBoolean(priority), permission); } - public PermissionRule( - final boolean grant, final boolean priority, final WildcardPermission permission) { + public PermissionRule(final boolean grant, final boolean priority, final String permission) { this.grant = grant; this.priority = priority; this.permission = permission; @@ -56,8 +54,9 @@ public class PermissionRule { return this.priority; } - public Permission getPermission() { - return this.permission; + public Permission getPermission(String realm, String username) { + return new WildcardPermission( + permission.replaceAll("\\?REALM\\?", realm).replaceAll("\\?USERNAME\\?", username)); } public static PermissionRule parse(final Map<String, String> rule) { @@ -69,7 +68,7 @@ public class PermissionRule { if (isPriority()) { ret.setAttribute("priority", Boolean.toString(true)); } - ret.setAttribute("permission", getPermission().toString()); + ret.setAttribute("permission", permission); return ret; } @@ -77,14 +76,19 @@ public class PermissionRule { return new PermissionRule( e.getName().equalsIgnoreCase("Grant"), e.getAttribute("priority") != null && Boolean.parseBoolean(e.getAttributeValue("priority")), - new WildcardPermission(e.getAttributeValue("permission"))); + e.getAttributeValue("permission")); } public Map<String, String> getMap() { final HashMap<String, String> ret = new HashMap<String, String>(); ret.put("priority", Boolean.toString(isPriority())); ret.put("grant", Boolean.toString(isGrant())); - ret.put("permission", getPermission().toString()); + ret.put("permission", permission); return ret; } + + public Permission getPermission(Subject subject) { + Principal principal = (Principal) subject.getPrincipal(); + return getPermission(principal.getRealm(), principal.getUsername()); + } } diff --git a/src/main/java/org/caosdb/server/transaction/Retrieve.java b/src/main/java/org/caosdb/server/transaction/Retrieve.java index fd7bae8a82e95554d527135f76124d397c23f0c4..19f840e92a1a6812e2ae2610ed3ace23b7e3cee7 100644 --- a/src/main/java/org/caosdb/server/transaction/Retrieve.java +++ b/src/main/java/org/caosdb/server/transaction/Retrieve.java @@ -30,6 +30,7 @@ import org.caosdb.server.entity.container.RetrieveContainer; import org.caosdb.server.entity.xml.SetFieldStrategy; import org.caosdb.server.entity.xml.ToElementStrategy; import org.caosdb.server.entity.xml.ToElementable; +import org.caosdb.server.jobs.ScheduledJob; import org.caosdb.server.jobs.core.Mode; import org.caosdb.server.jobs.core.RemoveDuplicates; import org.caosdb.server.jobs.core.ResolveNames; @@ -50,15 +51,19 @@ public class Retrieve extends Transaction<RetrieveContainer> { setAccess(getAccessManager().acquireReadAccess(this)); // resolve names - final ResolveNames r = new ResolveNames(); - r.init(Mode.SHOULD, null, this); - getSchedule().add(r); - getSchedule().runJob(r); - - final RemoveDuplicates job = new RemoveDuplicates(); - job.init(Mode.MUST, null, this); - getSchedule().add(job); - getSchedule().runJob(job); + { + final ResolveNames r = new ResolveNames(); + r.init(Mode.SHOULD, null, this); + ScheduledJob scheduledJob = getSchedule().add(r); + getSchedule().runJob(scheduledJob); + } + + { + final RemoveDuplicates job = new RemoveDuplicates(); + job.init(Mode.MUST, null, this); + ScheduledJob scheduledJob = getSchedule().add(job); + getSchedule().runJob(scheduledJob); + } // make schedule for all parsed entities makeSchedule(); diff --git a/src/main/java/org/caosdb/server/transaction/Transaction.java b/src/main/java/org/caosdb/server/transaction/Transaction.java index c2bd7e0feb62fa31dadd5580990350b829ff4591..97ed4954b93c55830d1bed5f74bad6e157e70776 100644 --- a/src/main/java/org/caosdb/server/transaction/Transaction.java +++ b/src/main/java/org/caosdb/server/transaction/Transaction.java @@ -41,6 +41,7 @@ import org.caosdb.server.entity.container.TransactionContainer; import org.caosdb.server.jobs.Job; import org.caosdb.server.jobs.JobExecutionTime; import org.caosdb.server.jobs.Schedule; +import org.caosdb.server.jobs.ScheduledJob; import org.caosdb.server.jobs.core.AccessControl; import org.caosdb.server.jobs.core.CheckDatatypePresent; import org.caosdb.server.jobs.core.CheckEntityACLRoles; @@ -88,8 +89,8 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra protected void makeSchedule() throws Exception { // load flag jobs final Job loadContainerFlags = Job.getJob("LoadContainerFlagJobs", Mode.MUST, null, this); - this.schedule.add(loadContainerFlags); - this.schedule.runJob(loadContainerFlags); + ScheduledJob scheduledJob = this.schedule.add(loadContainerFlags); + this.schedule.runJob(scheduledJob); // AccessControl this.schedule.add(Job.getJob(AccessControl.class.getSimpleName(), Mode.MUST, null, this)); @@ -104,17 +105,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra // additionally load datatype job if (e.hasValue()) { - boolean found = false; - for (final Job j : loadJobs) { - - if (CheckDatatypePresent.class.isInstance(j) - && ((CheckDatatypePresent) j).getEntity() == e) { - found = true; - } - } - if (!found) { - this.schedule.add(new CheckDatatypePresent().init(Mode.MUST, e, this)); - } + this.schedule.add(new CheckDatatypePresent().init(Mode.MUST, e, this)); } // load pickup job if necessary diff --git a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java index abda78b6feeb0de7104de5365877b28d474058fe..570fce2c2b001699fdb48c1379e807ae05d62996 100644 --- a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java +++ b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java @@ -215,13 +215,7 @@ public class WriteTransaction extends Transaction<WritableContainer> .setFile(oldEntity.getFileProperties().retrieveFromFileSystem()); } - try { - checkPermissions(entity, deriveUpdate(entity, oldEntity)); - } catch (final AuthorizationException exc) { - entity.setEntityStatus(EntityStatus.UNQUALIFIED); - entity.addError(ServerMessages.AUTHORIZATION_ERROR); - entity.addInfo(exc.getMessage()); - } + ((UpdateEntity) entity).setOriginal(oldEntity); } break innerLoop; } @@ -290,6 +284,18 @@ public class WriteTransaction extends Transaction<WritableContainer> @Override protected void preCheck() throws InterruptedException, Exception { for (final EntityInterface entity : getContainer()) { + try { + if (entity.getEntityStatus() == EntityStatus.QUALIFIED) { + checkPermissions(entity, deriveUpdate(entity, ((UpdateEntity) entity).getOriginal())); + } + } catch (final AuthorizationException exc) { + entity.setEntityStatus(EntityStatus.UNQUALIFIED); + entity.addError(ServerMessages.AUTHORIZATION_ERROR); + entity.addInfo(exc.getMessage()); + } catch (ClassCastException exc) { + // not an update entity. ignore. + } + // set default EntityACL if none present if (entity.getEntityACL() == null) { entity.setEntityACL(EntityACL.getOwnerACLFor(SecurityUtils.getSubject())); diff --git a/src/main/java/org/caosdb/server/utils/AbstractObservable.java b/src/main/java/org/caosdb/server/utils/AbstractObservable.java index a19fff9d44ce7339a9cdc6712e0806ef42bdfd40..abc7f0a0ae1cdcbeff613c2778637de97a507c57 100644 --- a/src/main/java/org/caosdb/server/utils/AbstractObservable.java +++ b/src/main/java/org/caosdb/server/utils/AbstractObservable.java @@ -37,6 +37,7 @@ public abstract class AbstractObservable implements Observable { return this.observers.add(o); } + /** @param e A String denoting the notification event. */ @Override public void notifyObservers(final String e) { if (this.observers != null) { diff --git a/src/main/java/org/caosdb/server/utils/Observer.java b/src/main/java/org/caosdb/server/utils/Observer.java index be89dff4b882adf24bcfd250c3dea1387997360d..b4c8f5444b957a1d18efc7f864c67b3daff8ca33 100644 --- a/src/main/java/org/caosdb/server/utils/Observer.java +++ b/src/main/java/org/caosdb/server/utils/Observer.java @@ -25,6 +25,8 @@ package org.caosdb.server.utils; public interface Observer { /** + * Notify this observer that an event {@code e} has happened to the {@code sender}. + * * @param e * @param sender * @return true, iff the Observable has to keep it in the list of observers. diff --git a/src/main/java/org/caosdb/server/utils/fsm/ActionNotAllowedException.java b/src/main/java/org/caosdb/server/utils/fsm/ActionNotAllowedException.java deleted file mode 100644 index ad643a0aade5e8285fa0aea7a92026995b9ccd61..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/ActionNotAllowedException.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.utils.fsm; - -public class ActionNotAllowedException extends RuntimeException { - - private final String action; - - public ActionNotAllowedException(final String action) { - this.action = action; - } - - private static final long serialVersionUID = -4324962066954446942L; - - @Override - public String getMessage() { - return "The action `" + this.action + "` is not allowed in the current state."; - } -} diff --git a/src/main/java/org/caosdb/server/utils/fsm/FiniteStateMachine.java b/src/main/java/org/caosdb/server/utils/fsm/FiniteStateMachine.java deleted file mode 100644 index f8bdccac726bfe25d7644f6dc689b54113f981c7..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/FiniteStateMachine.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.utils.fsm; - -import java.util.List; -import java.util.Map; - -public abstract class FiniteStateMachine<S extends State, T extends Transition> { - - public FiniteStateMachine(final S initial, final Map<S, Map<T, S>> transitions) - throws StateNotReachableException { - this.currentState = initial; - this.transitions = transitions; - checkEveryStateReachable(); - } - - private void checkEveryStateReachable() throws StateNotReachableException { - for (final State s : getAllStates()) { - if (!s.equals(this.currentState) && !stateIsReachable(s)) { - throw new StateNotReachableException(s); - } - } - } - - private boolean stateIsReachable(final State s) { - for (final Map<T, S> map : this.transitions.values()) { - if (map.containsValue(s)) { - return true; - } - } - return false; - } - - private final Map<S, Map<T, S>> transitions; - private S currentState = null; - - public void trigger(final T t) throws TransitionNotAllowedException { - final S old = this.currentState; - this.currentState = getNextState(t); - onAfterTransition(old, t, this.currentState); - } - - S getNextState(final T t) throws TransitionNotAllowedException { - final Map<T, S> map = this.transitions.get(this.currentState); - if (map != null && map.containsKey(t)) { - return map.get(t); - } - throw new TransitionNotAllowedException(this.getCurrentState(), t); - } - - public S getCurrentState() { - return this.currentState; - } - - public List<? extends State> getAllStates() { - return this.currentState.getAllStates(); - } - - /** - * Override this method in subclasses. The method is called immediately after a transition - * finished. - * - * @param from - * @param transition - * @param to - */ - protected void onAfterTransition(final S from, final T transition, final S to) {} -} diff --git a/src/main/java/org/caosdb/server/utils/fsm/MissingImplementationException.java b/src/main/java/org/caosdb/server/utils/fsm/MissingImplementationException.java deleted file mode 100644 index 8282e5dd7e7bea4368acf2c7e77b536fab301ad6..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/MissingImplementationException.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.utils.fsm; - -public class MissingImplementationException extends Exception { - - public MissingImplementationException(final State s) { - super("The state `" + s.toString() + "` has no implementation."); - } - - private static final long serialVersionUID = -1138551658177420875L; -} diff --git a/src/main/java/org/caosdb/server/utils/fsm/State.java b/src/main/java/org/caosdb/server/utils/fsm/State.java deleted file mode 100644 index 81b76aa599a90de2c8b3c8ddd295e62ce47dae12..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/State.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.utils.fsm; - -import java.util.List; - -public interface State { - - public List<State> getAllStates(); -} diff --git a/src/main/java/org/caosdb/server/utils/fsm/StateNotReachableException.java b/src/main/java/org/caosdb/server/utils/fsm/StateNotReachableException.java deleted file mode 100644 index 3970f60e5cf6e86096b0992419b14350c43b42ee..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/StateNotReachableException.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.utils.fsm; - -public class StateNotReachableException extends Exception { - - public StateNotReachableException(final State s) { - super("The state `" + s.toString() + "` is not reachable."); - } - - private static final long serialVersionUID = -1826791324169513493L; -} diff --git a/src/main/java/org/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java b/src/main/java/org/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java deleted file mode 100644 index d37d83783bd7f4aaa1db573f76315d0b51bd5fbb..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/StrategyFiniteStateMachine.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.utils.fsm; - -import java.util.Map; - -public class StrategyFiniteStateMachine<S extends State, T extends Transition, I> - extends FiniteStateMachine<S, T> { - - public StrategyFiniteStateMachine( - final S initial, final Map<S, I> stateImplementations, final Map<S, Map<T, S>> transitions) - throws MissingImplementationException, StateNotReachableException { - super(initial, transitions); - this.stateImplementations = stateImplementations; - checkImplementationsComplete(); - } - - /** - * Check if every state has it's implementation. - * - * @throws MissingImplementationException - */ - private void checkImplementationsComplete() throws MissingImplementationException { - for (final State s : getAllStates()) { - if (!this.stateImplementations.containsKey(s)) { - throw new MissingImplementationException(s); - } - } - } - - private final Map<S, I> stateImplementations; - - public I getImplementation() { - return getImplementation(getCurrentState()); - } - - public I getImplementation(final S state) { - return this.stateImplementations.get(state); - } -} diff --git a/src/main/java/org/caosdb/server/utils/fsm/Transition.java b/src/main/java/org/caosdb/server/utils/fsm/Transition.java deleted file mode 100644 index 41921b76cfc7ceed6e396551807e4b199486f0cf..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/Transition.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.utils.fsm; - -public interface Transition {} diff --git a/src/main/java/org/caosdb/server/utils/fsm/TransitionNotAllowedException.java b/src/main/java/org/caosdb/server/utils/fsm/TransitionNotAllowedException.java deleted file mode 100644 index f0547704e250db5c5088c5019a85974c2f4e6254..0000000000000000000000000000000000000000 --- a/src/main/java/org/caosdb/server/utils/fsm/TransitionNotAllowedException.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.utils.fsm; - -public class TransitionNotAllowedException extends Exception { - - private static final long serialVersionUID = -7236981582249457939L; - - public TransitionNotAllowedException(final State state, final Transition transition) { - super( - "The transition `" - + transition.toString() - + "` is not allowed in state `" - + state.toString() - + "."); - } -} diff --git a/src/test/java/org/caosdb/server/permissions/EntityACLTest.java b/src/test/java/org/caosdb/server/permissions/EntityACLTest.java index 0e0e3ee121b09332b6cbb7d86a22458bb5fbf17f..1787c902f48124d692f8c53e4a73ed04564dfe8f 100644 --- a/src/test/java/org/caosdb/server/permissions/EntityACLTest.java +++ b/src/test/java/org/caosdb/server/permissions/EntityACLTest.java @@ -22,6 +22,7 @@ */ package org.caosdb.server.permissions; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -307,7 +308,7 @@ public class EntityACLTest { @Test public void testFactory() { - final EntityACLFactory f = new EntityACLFactory(); + final AbstractEntityACLFactory<EntityACL> f = new EntityACLFactory(); org.caosdb.server.permissions.Role role1 = org.caosdb.server.permissions.Role.create("role1"); Config config1 = new Config(); @@ -349,60 +350,49 @@ public class EntityACLTest { Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.UPDATE_NAME))); } - // @Test - // public void niceFactoryStuff() { - // final EntityACLFactory f = new EntityACLFactory(); - // f.grant("user1", "*"); - // final EntityACL acl1 = f.create(); - // Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.EDIT_ACL)); - // Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.DELETE)); - // Assert.assertTrue(acl1.isPermitted("user1", - // EntityPermission.RETRIEVE_ENTITY)); - // Assert.assertTrue(acl1.isPermitted("user1", - // EntityPermission.UPDATE_DATA_TYPE)); - // Assert.assertTrue(acl1.isPermitted("user1", - // EntityPermission.USE_AS_PROPERTY)); - // - // f.grant("?OWNER?", "DELETE", "EDIT:ACL", "RETRIEVE:*", "UPDATE:*", - // "USE:*"); - // f.grant("user2", "EDIT:ACL"); - // final EntityACL acl2 = f.create(); - // Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.EDIT_ACL)); - // Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.DELETE)); - // Assert.assertTrue(acl2.isPermitted("user2", - // EntityPermission.RETRIEVE_ENTITY)); - // Assert.assertTrue(acl2.isPermitted("user2", - // EntityPermission.UPDATE_DATA_TYPE)); - // Assert.assertTrue(acl2.isPermitted("user2", - // EntityPermission.USE_AS_PROPERTY)); - // - // } - // - // @Test - // public void testDeny() { - // EntityACLFactory f = new EntityACLFactory(); - // f.deny("test", "DELETE"); - // Assert.assertFalse(f.create().isPermitted("test", - // EntityPermission.DELETE)); - // - // System.out.println(Utils.element2String(f.create().toElement())); - // - // System.out.println(Utils.element2String(EntityACL.GLOBAL_PERMISSIONS.toElement())); - // - // f.grant("test", "USE:*"); - // Assert.assertFalse(f.create().isPermitted("test", - // EntityPermission.DELETE)); - // - // System.out.println(Utils.element2String(f.create().toElement())); - // - // f = new EntityACLFactory(); - // f.grant(EntityACL.OTHER_ROLE, "RETRIEVE:*"); - // f.deny(EntityACL.OTHER_ROLE, "DELETE"); - // final EntityACL a = f.create(); - // - // System.out.println(Utils.element2String(a.toElement())); - // - // System.out.println(Utils.element2String(EntityACL.deserialize(a.serialize()).toElement())); - // } + @Test + public void testRemove() { + EntityACLFactory f = new EntityACLFactory(); + f.grant(org.caosdb.server.permissions.Role.create("role1"), false, EntityPermission.DELETE); + f.deny(org.caosdb.server.permissions.Role.create("role2"), false, EntityPermission.EDIT_ACL); + f.grant( + org.caosdb.server.permissions.Role.create("role3"), true, EntityPermission.RETRIEVE_ACL); + f.deny( + org.caosdb.server.permissions.Role.create("role4"), true, EntityPermission.RETRIEVE_ENTITY); + + EntityACL other = f.create(); + + f.grant(org.caosdb.server.permissions.Role.create("role2"), false, EntityPermission.EDIT_ACL); + f.grant( + org.caosdb.server.permissions.Role.create("role5"), false, EntityPermission.RETRIEVE_FILE); + + f.remove(other); // normalize and remove "other" + + EntityACL tester = f.create(); + assertEquals( + "only the very last rule survived, the others have been overriden or removed", + 1, + tester.getRules().size()); + for (EntityACI aci : tester.getRules()) { + assertEquals(aci.getResponsibleAgent(), org.caosdb.server.permissions.Role.create("role5")); + } + } + @Test + public void testNormalize() { + EntityACLFactory f = new EntityACLFactory(); + f.grant(org.caosdb.server.permissions.Role.create("role1"), false, EntityPermission.DELETE); + f.deny(org.caosdb.server.permissions.Role.create("role1"), false, EntityPermission.DELETE); + f.grant(org.caosdb.server.permissions.Role.create("role1"), true, EntityPermission.DELETE); + f.deny(org.caosdb.server.permissions.Role.create("role1"), true, EntityPermission.DELETE); + + // priority denail overrides everything else + EntityACL denyDelete = f.create(); + assertEquals(1, denyDelete.getRules().size()); + for (EntityACI aci : denyDelete.getRules()) { + assertEquals(org.caosdb.server.permissions.Role.create("role1"), aci.getResponsibleAgent()); + assertTrue(EntityACL.isDenial(aci.getBitSet())); + assertTrue(EntityACL.isPriorityBitSet(aci.getBitSet())); + } + } } diff --git a/src/test/java/org/caosdb/server/resource/TestScriptingResource.java b/src/test/java/org/caosdb/server/resource/TestScriptingResource.java index 845f589a9700a9d3d25eb8da2878c13ee114cd7d..7f7434528678dbd2e1886fade2116d0c9f766740 100644 --- a/src/test/java/org/caosdb/server/resource/TestScriptingResource.java +++ b/src/test/java/org/caosdb/server/resource/TestScriptingResource.java @@ -29,7 +29,6 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import org.apache.shiro.SecurityUtils; -import org.apache.shiro.authz.permission.WildcardPermission; import org.apache.shiro.subject.Subject; import org.caosdb.server.CaosDBServer; import org.caosdb.server.accessControl.AnonymousAuthenticationToken; @@ -95,9 +94,7 @@ public class TestScriptingResource { HashSet<PermissionRule> result = new HashSet<>(); result.add( new PermissionRule( - true, - false, - new WildcardPermission(ScriptingPermissions.PERMISSION_EXECUTION("anonymous_ok")))); + true, false, ScriptingPermissions.PERMISSION_EXECUTION("anonymous_ok"))); return result; } diff --git a/src/test/java/org/caosdb/server/utils/fsm/TestFiniteStateMachine.java b/src/test/java/org/caosdb/server/utils/fsm/TestFiniteStateMachine.java deleted file mode 100644 index 2a7b726b84464f3a7d287434b2767a34baaf4b50..0000000000000000000000000000000000000000 --- a/src/test/java/org/caosdb/server/utils/fsm/TestFiniteStateMachine.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.utils.fsm; - -import static org.junit.Assert.assertEquals; - -import com.google.common.collect.Lists; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -class SimpleFiniteStateMachine extends FiniteStateMachine<State, Transition> { - - public SimpleFiniteStateMachine( - final State initial, final Map<State, Map<Transition, State>> transitions) - throws StateNotReachableException { - super(initial, transitions); - } -} - -enum TestState implements State { - State1, - State2, - State3; - - @Override - public List<State> getAllStates() { - return Lists.newArrayList(values()); - } -} - -enum TestTransition implements Transition { - toState2, - toState3 -} - -public class TestFiniteStateMachine { - - @Rule public ExpectedException exc = ExpectedException.none(); - - @Test - public void testTransitionNotAllowedException() - throws StateNotReachableException, TransitionNotAllowedException { - final Map<State, Map<Transition, State>> map = new HashMap<>(); - final HashMap<Transition, State> from1 = new HashMap<>(); - from1.put(TestTransition.toState2, TestState.State2); - from1.put(TestTransition.toState3, TestState.State3); - map.put(TestState.State1, from1); - - final SimpleFiniteStateMachine fsm = new SimpleFiniteStateMachine(TestState.State1, map); - assertEquals(TestState.State1, fsm.getCurrentState()); - fsm.trigger(TestTransition.toState2); - assertEquals(TestState.State2, fsm.getCurrentState()); - - // only 1->2 and from 1->3 is allowed. not 2->3 - this.exc.expect(TransitionNotAllowedException.class); - fsm.trigger(TestTransition.toState3); - } - - @Test - public void testStateNotReachable() throws StateNotReachableException { - final Map<State, Map<Transition, State>> empty = new HashMap<>(); - - this.exc.expect(StateNotReachableException.class); - new SimpleFiniteStateMachine(TestState.State1, empty); - } -} diff --git a/src/test/java/org/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java b/src/test/java/org/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java deleted file mode 100644 index ebf719b1eaa4b56f3b64bdd9371f121231b4ed68..0000000000000000000000000000000000000000 --- a/src/test/java/org/caosdb/server/utils/fsm/TestStrategyFiniteStateMachine.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * ** header v3.0 - * This file is a part of the CaosDB Project. - * - * Copyright (C) 2018 Research Group Biomedical Physics, - * Max-Planck-Institute for Dynamics and Self-Organization Göttingen - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - * - * ** end header - */ -package org.caosdb.server.utils.fsm; - -import java.util.HashMap; -import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -public class TestStrategyFiniteStateMachine { - - @Rule public ExpectedException exc = ExpectedException.none(); - - @Test - public void testStateHasNoImplementation() - throws MissingImplementationException, StateNotReachableException { - final Map<State, Map<Transition, State>> map = new HashMap<>(); - final HashMap<Transition, State> from1 = new HashMap<>(); - from1.put(TestTransition.toState2, TestState.State2); - from1.put(TestTransition.toState3, TestState.State3); - map.put(TestState.State1, from1); - - final Map<State, Object> stateImplementations = new HashMap<>(); - - this.exc.expect(MissingImplementationException.class); - new StrategyFiniteStateMachine<State, Transition, Object>( - TestState.State1, stateImplementations, map); - } -}