diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java new file mode 100644 index 0000000000000000000000000000000000000000..9840a908dccff3155f892237b9f6b8c093c67136 --- /dev/null +++ b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java @@ -0,0 +1,308 @@ +package caosdb.server.jobs.extension; + +import caosdb.server.CaosDBServer; +import caosdb.server.accessControl.UserSources; +import caosdb.server.entity.Entity; +import caosdb.server.entity.EntityInterface; +import caosdb.server.entity.Message; +import caosdb.server.entity.wrapper.Property; +import caosdb.server.jobs.ContainerJob; +import caosdb.server.jobs.JobAnnotation; +import caosdb.server.jobs.core.CheckNoAdditionalPropertiesPresent; +import caosdb.server.jobs.core.CheckNoOverridesPresent; +import caosdb.server.jobs.core.CheckPropValid; +import caosdb.server.query.Query; +import caosdb.server.transaction.Insert; +import caosdb.server.transaction.Update; +import caosdb.server.utils.EntityStatus; +import caosdb.server.utils.ServerMessages; +import caosdb.server.utils.Utils; +import java.util.List; + +@JobAnnotation(transaction = caosdb.server.transaction.WriteTransaction.class, loadAlways = true) +public class AWIBoxLoan extends ContainerJob { + + private static final Message UNIQUE_USER = + new Message("The user must have a unique combination of first name and last name!"); + private static final Message BOX_HAS_LOAN = + new Message( + "This box cannot be be requested right now because it appears to have a Loan property attached to it. This usually means, that the box is already requested or borrowed by someone."); + + @Override + protected void run() { + if (isAnonymous() + && !(isRequestLoanSetUser() + || isRequestLoanInsertLoan() + || isRequestLoanUpdateBox() + || isRequestReturnSetUser() + || isRequestReturnUpdateLoan())) { + addError(ServerMessages.AUTHORIZATION_ERROR); + } + } + + boolean isAnonymous() { + return getUser().hasRole(UserSources.ANONYMOUS_ROLE); + } + + boolean isRequestReturnUpdateLoan() { + // is UPDATE transaction + if (getTransaction() instanceof Update) { + // Container has only loan elements with special properties + for (EntityInterface e : getContainer()) { + if (!isLoan(e) || !hasOnlyAllowedLoanProperties4RequestReturn(e)) { + return false; + } + setReturnRequestedDate(e); + } + return true; + } + return false; + } + + boolean isRequestReturnSetUser() { + // same as request_loan.set_user + return isRequestLoanSetUser(); + } + + boolean isRequestLoanUpdateBox() { + // is UPDATE transaction + if (getTransaction() instanceof Update) { + // Container has only box elements + for (EntityInterface e : getContainer()) { + if (boxHasLoanProperty(e)) { + e.addError(BOX_HAS_LOAN); + return true; + } + if (!isBox(e) || !hasOnlyAllowedBoxProperties4RequestLoan(e)) { + return false; + } + } + return true; + } + return false; + } + + private boolean boxHasLoanProperty(EntityInterface e) { + EntityInterface validBox = retrieveValidEntity(e.getId()); + for (Property p : validBox.getProperties()) { + if (p.getId() == getLoanId()) { + return true; + } + } + return false; + } + + /** + * Has only one new property -> Loan. + * + * @param e + * @return + */ + boolean hasOnlyAllowedBoxProperties4RequestLoan(EntityInterface e) { + int count = 0; + for (Property p : e.getProperties()) { + if (p.getEntityStatus() == EntityStatus.QUALIFIED) { // this means update + if (p.getId() == getLoanId()) { + count++; + continue; + } + return false; // this is not a Loan. + } + } + + // Box has only one update, a loan property + return count == 1; + } + + boolean isRequestLoanInsertLoan() { + // is INSERT transaction + if (getTransaction() instanceof Insert) { + // Container has only loan elements + for (EntityInterface e : getContainer()) { + if (!isLoan(e)) { + return false; + } + setLoanRequestDate(e); + // Loans have only a specific set of properties + appendJob(e, CheckNoAdditionalPropertiesPresent.class); + appendJob(e, CheckNoOverridesPresent.class); + } + return true; + } + return false; + } + + void setReturnRequestedDate(EntityInterface e) { + setDateProperty(e, getReturnRequestedId()); + } + + private void setDateProperty(EntityInterface e, Integer propertyId) { + EntityInterface p = retrieveValidEntity(propertyId); + p.setValue(getTransaction().getTimestamp()); + e.addProperty(new Property(p)); + } + + void setLoanRequestDate(EntityInterface e) { + setDateProperty(e, getLoanRequestedId()); + } + + boolean isRequestLoanSetUser() { + // is INSERT/UPDATE transaction + // Container has only one element, user + if ((getTransaction() instanceof Update || getTransaction() instanceof Insert) + && getContainer().size() == 1 + && isUser(getContainer().get(0)) + && checkUniqueName(getContainer().get(0)) + && checkEmail(getContainer().get(0))) { + appendJob(getContainer().get(0), CheckNoAdditionalPropertiesPresent.class); + appendJob(getContainer().get(0), CheckNoOverridesPresent.class); + return true; + } + return false; + } + + boolean checkEmail(Entity entity) { + runJobFromSchedule(entity, CheckPropValid.class); + for (Property p : entity.getProperties()) { + if (p.getId() == getEmailID()) { + if (!Utils.isRFC822Compliant(p.getValue().toString())) { + p.addError(ServerMessages.EMAIL_NOT_WELL_FORMED); + } + return true; + } + } + return false; + } + + private boolean checkUniqueName(Entity entity) { + String firstName = null; + String lastName = null; + Query q = + new Query( + "FIND " + + getUserID().toString() + + " WITH " + + getFirstNameId().toString() + + "='" + + firstName + + "' AND " + + getLastNameId().toString() + + "='" + + lastName + + "'", + getUser()) + .execute(getTransaction().getAccess()); + List<Integer> resultSet = q.getResultSet(); + if (resultSet.isEmpty() || (resultSet.size() == 1 && resultSet.get(0) == entity.getId())) { + return true; + } + entity.addError(UNIQUE_USER); + return false; + } + + /** Has single user parent. */ + boolean isUser(Entity entity) { + return entity.getParents().size() == 1 + && retrieveValidIDByName(entity.getParents().get(0).getName()) == getUserID(); + } + + /** Has single box parent. */ + boolean isBox(EntityInterface e) { + return e.getParents().size() == 1 + && retrieveValidIDByName(e.getParents().get(0).getName()) == getBoxId(); + } + + /** has single loan parent */ + private boolean isLoan(EntityInterface e) { + return e.getParents().size() == 1 + && retrieveValidIDByName(e.getParents().get(0).getName()) == getLoanId(); + } + /** + * Has only 5/6 new/updated properties: content, returnRequested, destination, Borrower, comment + * (optional), location + */ + boolean hasOnlyAllowedLoanProperties4RequestReturn(EntityInterface e) { + runJobFromSchedule(e, CheckPropValid.class); + for (Property p : e.getProperties()) { + if (p.getEntityStatus() == EntityStatus.QUALIFIED) { // this means update + if (p.getId() == getContentId()) { + + } else if (p.getId() == getDestinationId()) { + + } else if (p.getId() == getBorrowerId()) { + + } else if (p.getId() == getCommentId()) { + + } else if (p.getId() == getLocationId()) { + + } + return false; // this is not a property which may be updated by anonymous. + } + } + return true; + } + + Integer getIdOf(String string) { + String id = CaosDBServer.getServerProperty("EXT_AWI_" + string.toUpperCase() + "_ID"); + if (id != null && Utils.isNonNullInteger(id)) { + return new Integer(id); + } + String name = CaosDBServer.getServerProperty("EXT_AWI_" + string.toUpperCase() + "_NAME"); + if (name == null || name.isEmpty()) { + name = string; + } + return retrieveValidIDByName(name); + } + + Integer getBorrowerId() { + return getIdOf("Borrower"); + } + + Integer getCommentId() { + return getIdOf("comment"); + } + + Integer getLocationId() { + return getIdOf("location"); + } + + Integer getDestinationId() { + return getIdOf("destination"); + } + + Integer getContentId() { + return getIdOf("content"); + } + + Integer getBoxId() { + return getIdOf("Box"); + } + + Integer getLoanId() { + return getIdOf("Loan"); + } + + Integer getUserID() { + return getIdOf("User"); + } + + Integer getEmailID() { + return getIdOf("email"); + } + + Integer getLoanRequestedId() { + return getIdOf("loanRequested"); + } + + Integer getReturnRequestedId() { + return getIdOf("returnRequested"); + } + + Integer getLastNameId() { + return getIdOf("lastName"); + } + + Integer getFirstNameId() { + return getIdOf("firstName"); + } +} diff --git a/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java b/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java new file mode 100644 index 0000000000000000000000000000000000000000..59ce687d2d82be5d725ebdff1eb422ec5133d597 --- /dev/null +++ b/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java @@ -0,0 +1,277 @@ +package caosdb.server.jobs.extension; + +import static org.junit.Assert.assertEquals; + +import caosdb.server.entity.container.TransactionContainer; +import caosdb.server.jobs.core.Mode; +import caosdb.server.transaction.Transaction; +import caosdb.server.utils.EntityStatus; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authz.AuthorizationException; +import org.apache.shiro.authz.Permission; +import org.apache.shiro.session.Session; +import org.apache.shiro.subject.ExecutionException; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.Subject; +import org.junit.Test; + +public class TestAWIBoxLoan { + + @Test + public void testNonAnonymousUser() { + TransactionContainer container = new TransactionContainer(); + AWIBoxLoan j = + new AWIBoxLoan() { + + @Override + protected Subject getUser() { + return new Subject() { + + @Override + public void runAs(PrincipalCollection principals) + throws NullPointerException, IllegalStateException {} + + @Override + public PrincipalCollection releaseRunAs() { + return null; + } + + @Override + public void logout() {} + + @Override + public void login(AuthenticationToken token) throws AuthenticationException {} + + @Override + public boolean isRunAs() { + return false; + } + + @Override + public boolean isRemembered() { + return false; + } + + @Override + public boolean isPermittedAll(Collection<Permission> permissions) { + return false; + } + + @Override + public boolean isPermittedAll(String... permissions) { + return false; + } + + @Override + public boolean[] isPermitted(List<Permission> permissions) { + return null; + } + + @Override + public boolean[] isPermitted(String... permissions) { + return null; + } + + @Override + public boolean isPermitted(Permission permission) { + + return false; + } + + @Override + public boolean isPermitted(String permission) { + + return false; + } + + @Override + public boolean isAuthenticated() { + + return false; + } + + @Override + public boolean[] hasRoles(List<String> roleIdentifiers) { + + return null; + } + + @Override + public boolean hasRole(String roleIdentifier) { + + return false; + } + + @Override + public boolean hasAllRoles(Collection<String> roleIdentifiers) { + + return false; + } + + @Override + public Session getSession(boolean create) { + return null; + } + + @Override + public Session getSession() { + return null; + } + + @Override + public PrincipalCollection getPrincipals() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + + @Override + public PrincipalCollection getPreviousPrincipals() { + return null; + } + + @Override + public void execute(Runnable runnable) {} + + @Override + public <V> V execute(Callable<V> callable) throws ExecutionException { + return null; + } + + @Override + public void checkRoles(String... roleIdentifiers) throws AuthorizationException {} + + @Override + public void checkRoles(Collection<String> roleIdentifiers) + throws AuthorizationException {} + + @Override + public void checkRole(String roleIdentifier) throws AuthorizationException {} + + @Override + public void checkPermissions(Collection<Permission> permissions) + throws AuthorizationException {} + + @Override + public void checkPermissions(String... permissions) throws AuthorizationException {} + + @Override + public void checkPermission(Permission permission) throws AuthorizationException {} + + @Override + public void checkPermission(String permission) throws AuthorizationException {} + + @Override + public Runnable associateWith(Runnable runnable) { + return null; + } + + @Override + public <V> Callable<V> associateWith(Callable<V> callable) { + return null; + } + }; + } + }; + Transaction<TransactionContainer> t = + new Transaction<TransactionContainer>(container, null) { + + @Override + protected void transaction() throws Exception {} + + @Override + protected void preTransaction() throws InterruptedException {} + + @Override + protected void preCheck() throws InterruptedException, Exception {} + + @Override + protected void postTransaction() throws Exception {} + + @Override + protected void postCheck() {} + + @Override + public boolean logHistory() { + return false; + } + + @Override + protected void init() throws Exception {} + + @Override + protected void cleanUp() {} + }; + + j.init(Mode.MUST, null, t); + assertEquals(0, j.getContainer().getMessages().size()); + assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus()); + + // non-anonymous user + j.run(); + assertEquals(0, j.getContainer().getMessages().size()); + assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus()); + } + + @Test + public void testAnonymousUserUnqualified() { + TransactionContainer container = new TransactionContainer(); + AWIBoxLoan j = + new AWIBoxLoan() { + + @Override + protected Subject getUser() { + return null; + } + + @Override + boolean isAnonymous() { + return true; + } + }; + Transaction<TransactionContainer> t = + new Transaction<TransactionContainer>(container, null) { + + @Override + protected void transaction() throws Exception {} + + @Override + protected void preTransaction() throws InterruptedException {} + + @Override + protected void preCheck() throws InterruptedException, Exception {} + + @Override + protected void postTransaction() throws Exception {} + + @Override + protected void postCheck() {} + + @Override + public boolean logHistory() { + return false; + } + + @Override + protected void init() throws Exception {} + + @Override + protected void cleanUp() {} + }; + + j.init(Mode.MUST, null, t); + assertEquals(0, j.getContainer().getMessages().size()); + assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus()); + + j.run(); + assertEquals(1, j.getContainer().getMessages().size()); + assertEquals(EntityStatus.UNQUALIFIED, j.getContainer().getStatus()); + } +}