From d3dceac4d8e4c855711e97b47b155bb3d7b1bf86 Mon Sep 17 00:00:00 2001
From: Timm Fitschen <t.fitschen@indiscale.com>
Date: Wed, 6 May 2020 15:12:43 +0200
Subject: [PATCH] WIP: one time token on startup

---
 pom.xml                                       |   5 +
 src/main/java/caosdb/server/CaosDBServer.java |  90 ++++-----
 .../OneTimeAuthenticationToken.java           | 178 ++++++++++++++++--
 .../accessControl/OneTimeTokenRealm.java      |   2 +-
 .../server/accessControl/SessionToken.java    |   2 +-
 .../server/resource/ScriptingResource.java    |  52 ++---
 .../scripting/ScriptingPermissions.java       |  11 ++
 .../scripting/ServerSideScriptingCaller.java  |   8 +-
 .../server/authentication/AuthTokenTest.java  | 110 ++++++++++-
 .../resource/TestScriptingResource.java       |  23 ++-
 .../server/utils/WebinterfaceUtilsTest.java   |   7 +-
 11 files changed, 397 insertions(+), 91 deletions(-)
 create mode 100644 src/main/java/caosdb/server/scripting/ScriptingPermissions.java

diff --git a/pom.xml b/pom.xml
index be9279b6..acd175a3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,6 +54,11 @@
       <artifactId>easy-units</artifactId>
       <version>0.0.1-SNAPSHOT</version>
     </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.dataformat</groupId>
+      <artifactId>jackson-dataformat-yaml</artifactId>
+      <version>2.11.0</version>
+    </dependency>
     <dependency>
       <groupId>org.apache.shiro</groupId>
       <artifactId>shiro-core</artifactId>
diff --git a/src/main/java/caosdb/server/CaosDBServer.java b/src/main/java/caosdb/server/CaosDBServer.java
index 34bfe3e2..4a6a3ad0 100644
--- a/src/main/java/caosdb/server/CaosDBServer.java
+++ b/src/main/java/caosdb/server/CaosDBServer.java
@@ -19,10 +19,54 @@
  */
 package caosdb.server;
 
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Properties;
+import java.util.TimeZone;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.config.Ini;
+import org.apache.shiro.config.Ini.Section;
+import org.apache.shiro.config.IniSecurityManagerFactory;
+import org.apache.shiro.mgt.SecurityManager;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.Factory;
+import org.apache.shiro.util.ThreadContext;
+import org.restlet.Application;
+import org.restlet.Component;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.Server;
+import org.restlet.data.CookieSetting;
+import org.restlet.data.Parameter;
+import org.restlet.data.Protocol;
+import org.restlet.data.Reference;
+import org.restlet.data.ServerInfo;
+import org.restlet.data.Status;
+import org.restlet.engine.Engine;
+import org.restlet.routing.Route;
+import org.restlet.routing.Router;
+import org.restlet.routing.Template;
+import org.restlet.routing.TemplateRoute;
+import org.restlet.routing.Variable;
+import org.restlet.util.Series;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import caosdb.server.accessControl.AnonymousRealm;
 import caosdb.server.accessControl.AuthenticationUtils;
 import caosdb.server.accessControl.CaosDBAuthorizingRealm;
 import caosdb.server.accessControl.CaosDBDefaultRealm;
+import caosdb.server.accessControl.OneTimeAuthenticationToken;
 import caosdb.server.accessControl.OneTimeTokenRealm;
 import caosdb.server.accessControl.Principal;
 import caosdb.server.accessControl.SessionToken;
@@ -64,49 +108,6 @@ import caosdb.server.utils.FileUtils;
 import caosdb.server.utils.Initialization;
 import caosdb.server.utils.NullPrintStream;
 import caosdb.server.utils.Utils;
-import java.io.BufferedReader;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.time.ZoneId;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Properties;
-import java.util.TimeZone;
-import java.util.logging.Handler;
-import java.util.logging.Level;
-import java.util.logging.LogRecord;
-import org.apache.shiro.SecurityUtils;
-import org.apache.shiro.config.Ini;
-import org.apache.shiro.config.Ini.Section;
-import org.apache.shiro.config.IniSecurityManagerFactory;
-import org.apache.shiro.mgt.SecurityManager;
-import org.apache.shiro.subject.Subject;
-import org.apache.shiro.util.Factory;
-import org.apache.shiro.util.ThreadContext;
-import org.restlet.Application;
-import org.restlet.Component;
-import org.restlet.Context;
-import org.restlet.Request;
-import org.restlet.Response;
-import org.restlet.Restlet;
-import org.restlet.Server;
-import org.restlet.data.CookieSetting;
-import org.restlet.data.Parameter;
-import org.restlet.data.Protocol;
-import org.restlet.data.Reference;
-import org.restlet.data.ServerInfo;
-import org.restlet.data.Status;
-import org.restlet.engine.Engine;
-import org.restlet.routing.Route;
-import org.restlet.routing.Router;
-import org.restlet.routing.Template;
-import org.restlet.routing.TemplateRoute;
-import org.restlet.routing.Variable;
-import org.restlet.util.Series;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class CaosDBServer extends Application {
 
@@ -261,7 +262,8 @@ public class CaosDBServer extends Application {
       init(args);
       initServerProperties();
       initTimeZone();
-    } catch (IOException | InterruptedException e1) {
+      OneTimeAuthenticationToken.init();
+    } catch (Exception e1) {
       logger.error("Could not configure the server.", e1);
       System.exit(1);
     }
diff --git a/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java b/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java
index ba7dff8d..ddb93080 100644
--- a/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java
+++ b/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java
@@ -22,15 +22,29 @@
  */
 package caosdb.server.accessControl;
 
-import caosdb.server.CaosDBServer;
-import caosdb.server.ServerProperties;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
 import java.util.UUID;
+import org.apache.shiro.subject.Subject;
 import org.eclipse.jetty.util.ajax.JSON;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import caosdb.server.CaosDBServer;
+import caosdb.server.ServerProperties;
 
 public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToken {
 
   private static final transient String PEPPER = java.util.UUID.randomUUID().toString();
   private final String[] permissions;
+  private String[] roles;
 
   public OneTimeAuthenticationToken(
       final Principal principal,
@@ -39,9 +53,11 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
       final String salt,
       final String curry,
       final String checksum,
-      final String... permissions) {
+      final String[] permissions,
+      final String[] roles) {
     super(principal, date, timeout, salt, curry, checksum);
     this.permissions = permissions;
+    this.roles = roles;
   }
 
   public OneTimeAuthenticationToken(
@@ -50,8 +66,9 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
       final long timeout,
       final String salt,
       final String curry,
-      final String... permissions) {
-    super(
+      final String[] permissions,
+      final String[] roles) {
+    this(
         principal,
         date,
         timeout,
@@ -65,8 +82,10 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
             salt,
             curry,
             calcChecksum((Object[]) permissions),
-            PEPPER));
-    this.permissions = permissions;
+            calcChecksum((Object[]) roles),
+            PEPPER),
+        permissions,
+        roles);
   }
 
   private static final long serialVersionUID = -1072740888045267613L;
@@ -75,6 +94,10 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
     return this.permissions;
   }
 
+  public String[] getRoles() {
+    return this.roles;
+  }
+
   @Override
   public String calcChecksum() {
     return calcChecksum(
@@ -85,6 +108,7 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
         this.salt,
         this.curry,
         calcChecksum((Object[]) this.permissions),
+        calcChecksum((Object[]) this.roles),
         PEPPER);
   }
 
@@ -98,6 +122,7 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
           this.timeout,
           this.salt,
           this.permissions,
+          this.roles,
           this.checksum
         });
   }
@@ -117,19 +142,148 @@ public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToke
     final long timeout = (Long) array[3];
     final String salt = (String) array[4];
     final String[] permissions = toStringArray((Object[]) array[5]);
-    final String checksum = (String) array[6];
+    final String[] roles = toStringArray((Object[]) array[6]);
+    final String checksum = (String) array[7];
     return new OneTimeAuthenticationToken(
-        principal, date, timeout, salt, curry, checksum, permissions);
+        principal, date, timeout, salt, curry, checksum, permissions, roles);
   }
 
   public static OneTimeAuthenticationToken generate(
-      final Principal principal, final String curry, final String... permissions) {
+      final Principal principal, final String curry, final String[] permissions, String[] roles) {
     return new OneTimeAuthenticationToken(
         principal,
         System.currentTimeMillis(),
         Long.parseLong(CaosDBServer.getServerProperty(ServerProperties.KEY_ACTIVATION_TIMEOUT_MS)),
         UUID.randomUUID().toString(),
         curry,
-        permissions);
+        permissions,
+        roles);
+  }
+  
+  
+  public static List<Config> loadConfig(InputStream input) throws Exception {
+    List<Config> results = new LinkedList<>();
+    ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
+      ObjectReader reader = mapper.readerFor(Config.class);
+      Iterator<Config> configs = reader.readValues(input);
+      configs.forEachRemaining(results::add);
+    return results;
+  }
+  
+  public static Map<String, Config> getPurposeMap(List<Config> configs) throws Exception {
+    Map<String, Config> result = new HashMap<>();
+    for(Config config : configs) {
+      if(config.getPurpose() != null && !config.getPurpose().isEmpty()) {
+        if(result.containsKey(config.getPurpose())) {
+          throw new Exception("OneTimeAuthToken configuration contains duplicate values for the 'purpose' property.");
+        }
+        result.put(config.getPurpose(), config);
+      }
+    }
+    return result;
+  }
+
+  public static OneTimeAuthenticationToken generate(Config c) {
+    return generate(c, new Principal("anonymous", "anonymous"), null);
+  }
+
+  public static OneTimeAuthenticationToken generateForPurpose(String purpose, Subject user, String curry) {
+    Config c = purposes.get(purpose);
+    if (c != null) {
+      Principal principal = (Principal) user.getPrincipal();
+      return generate(c, principal, curry);
+    }
+    return null;
+  }
+
+  public static OneTimeAuthenticationToken generate(Config c, Principal principal, String curry) {
+    return generate(principal, curry, c.getPermissions(), c.getRoles());
+  }
+
+  static Map<String, Config> purposes;
+
+  public static class Output {
+    private String file = null;
+    
+    public Output() {
+    }
+
+    public void output(OneTimeAuthenticationToken t) throws IOException {
+      try (PrintWriter writer = new PrintWriter(file, "utf-8")) {
+        writer.println(t.toString());
+      }
+    }
+
+    public String getFile() {
+      return file;
+    }
+
+    public void setFile(String file) {
+      this.file = file;
+    }
   }
-}
+  
+  public static class Config {
+    private String[] permissions = {};
+    private String[] roles = {};
+    private String purpose = null;
+    private Output output = null;
+    
+    public Config() {
+    }
+
+    public String[] getPermissions() {
+      return permissions;
+    }
+    public String getPurpose() {
+      return purpose;
+    }
+    public void setPermissions(String[] permissions) {
+      this.permissions = permissions;
+    }
+    public String[] getRoles() {
+      return roles;
+    }
+    public void setRoles(String[] roles) {
+      this.roles = roles;
+    }
+    public void setPurpose(String purpose) {
+      this.purpose = purpose;
+    }
+
+    public Output getOutput() {
+      return output;
+    }
+
+    public void setOutput(Output output) {
+      this.output = output;
+    }
+
+  }
+  
+  public static Map<String, Config> getPurposeMap() {
+    return purposes;
+  }
+  
+  
+  public static void init(InputStream yamlConfig) throws Exception {
+    List<Config> configs = loadConfig(yamlConfig);
+    output(configs);
+    purposes = getPurposeMap(configs);
+  }
+
+  private static void output(List<Config> configs) throws IOException {
+    for (Config config : configs) {
+      if(config.getOutput() != null) {
+        config.getOutput().output(generate(config));
+      }
+    }
+  }
+
+  public static void init() throws Exception {
+    try(FileInputStream f = new FileInputStream("conf/ext/authtoken.yaml")) {
+      init(f);
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/src/main/java/caosdb/server/accessControl/OneTimeTokenRealm.java b/src/main/java/caosdb/server/accessControl/OneTimeTokenRealm.java
index 468a8d8d..1c16bf0c 100644
--- a/src/main/java/caosdb/server/accessControl/OneTimeTokenRealm.java
+++ b/src/main/java/caosdb/server/accessControl/OneTimeTokenRealm.java
@@ -22,11 +22,11 @@
  */
 package caosdb.server.accessControl;
 
-import caosdb.server.accessControl.CaosDBAuthorizingRealm.PermissionAuthenticationInfo;
 import org.apache.shiro.authc.AuthenticationException;
 import org.apache.shiro.authc.AuthenticationInfo;
 import org.apache.shiro.authc.AuthenticationToken;
 import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
+import caosdb.server.accessControl.CaosDBAuthorizingRealm.PermissionAuthenticationInfo;
 
 public class OneTimeTokenRealm extends SessionTokenRealm {
 
diff --git a/src/main/java/caosdb/server/accessControl/SessionToken.java b/src/main/java/caosdb/server/accessControl/SessionToken.java
index 273bd50b..82b11f22 100644
--- a/src/main/java/caosdb/server/accessControl/SessionToken.java
+++ b/src/main/java/caosdb/server/accessControl/SessionToken.java
@@ -22,9 +22,9 @@
  */
 package caosdb.server.accessControl;
 
+import org.eclipse.jetty.util.ajax.JSON;
 import caosdb.server.CaosDBServer;
 import caosdb.server.ServerProperties;
-import org.eclipse.jetty.util.ajax.JSON;
 
 public class SessionToken extends SelfValidatingAuthenticationToken {
 
diff --git a/src/main/java/caosdb/server/resource/ScriptingResource.java b/src/main/java/caosdb/server/resource/ScriptingResource.java
index b23a9593..d967d031 100644
--- a/src/main/java/caosdb/server/resource/ScriptingResource.java
+++ b/src/main/java/caosdb/server/resource/ScriptingResource.java
@@ -24,18 +24,6 @@
 
 package caosdb.server.resource;
 
-import caosdb.server.FileSystem;
-import caosdb.server.accessControl.Principal;
-import caosdb.server.accessControl.SessionToken;
-import caosdb.server.accessControl.UserSources;
-import caosdb.server.entity.FileProperties;
-import caosdb.server.entity.Message;
-import caosdb.server.scripting.CallerSerializer;
-import caosdb.server.scripting.ServerSideScriptingCaller;
-import caosdb.server.utils.Serializer;
-import caosdb.server.utils.ServerMessages;
-import caosdb.server.utils.Utils;
-import com.ibm.icu.text.Collator;
 import java.io.IOException;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
@@ -48,6 +36,7 @@ import org.apache.commons.fileupload.FileItemIterator;
 import org.apache.commons.fileupload.FileItemStream;
 import org.apache.commons.fileupload.FileUploadException;
 import org.apache.commons.fileupload.disk.DiskFileItemFactory;
+import org.apache.shiro.subject.Subject;
 import org.jdom2.Element;
 import org.restlet.data.CharacterSet;
 import org.restlet.data.Form;
@@ -57,6 +46,20 @@ import org.restlet.data.Status;
 import org.restlet.engine.header.ContentType;
 import org.restlet.ext.fileupload.RestletFileUpload;
 import org.restlet.representation.Representation;
+import com.ibm.icu.text.Collator;
+import caosdb.server.FileSystem;
+import caosdb.server.accessControl.OneTimeAuthenticationToken;
+import caosdb.server.accessControl.Principal;
+import caosdb.server.accessControl.SessionToken;
+import caosdb.server.accessControl.UserSources;
+import caosdb.server.entity.FileProperties;
+import caosdb.server.entity.Message;
+import caosdb.server.scripting.CallerSerializer;
+import caosdb.server.scripting.ScriptingPermissions;
+import caosdb.server.scripting.ServerSideScriptingCaller;
+import caosdb.server.utils.Serializer;
+import caosdb.server.utils.ServerMessages;
+import caosdb.server.utils.Utils;
 
 public class ScriptingResource extends AbstractCaosDBServerResource {
 
@@ -83,9 +86,6 @@ public class ScriptingResource extends AbstractCaosDBServerResource {
   @Override
   protected Representation httpPostInChildClass(Representation entity) throws Exception {
 
-    if (isAnonymous()) {
-      return error(ServerMessages.AUTHORIZATION_ERROR, Status.CLIENT_ERROR_FORBIDDEN);
-    }
     MediaType mediaType = entity.getMediaType();
     try {
       if (mediaType.equals(MediaType.MULTIPART_FORM_DATA, true)) {
@@ -193,28 +193,38 @@ public class ScriptingResource extends AbstractCaosDBServerResource {
 
   public int callScript(Form form, List<FileProperties> files) throws Message {
     List<String> commandLine = form2CommandLine(form);
+    String call = commandLine.get(0);
+
+    checkExecutionPermission(getUser(), call);
     Integer timeoutMs = Integer.valueOf(form.getFirstValue("timeout", "-1"));
     return callScript(commandLine, timeoutMs, files);
   }
 
   public int callScript(List<String> commandLine, Integer timeoutMs, List<FileProperties> files)
       throws Message {
-    return callScript(commandLine, timeoutMs, files, generateAuthToken());
+    return callScript(commandLine, timeoutMs, files, generateAuthToken(commandLine.get(0)));
   }
 
-  public Object generateAuthToken() {
+  public boolean isAnonymous() {
+    return getUser().hasRole(UserSources.ANONYMOUS_ROLE);
+  }
+
+  public Object generateAuthToken(String call) {
+    String purpose = "scripting:" + call;
+    Object authtoken = OneTimeAuthenticationToken.generateForPurpose(purpose, getUser(), null);
+    if (authtoken != null || isAnonymous()) {
+      return authtoken;
+    }
     return SessionToken.generate((Principal) getUser().getPrincipal(), null);
   }
 
-  boolean isAnonymous() {
-    boolean ret = getUser().hasRole(UserSources.ANONYMOUS_ROLE);
-    return ret;
+  public void checkExecutionPermission(Subject user, String call) {
+    user.checkPermission(ScriptingPermissions.PERMISSION_EXECUTION(call));
   }
 
   public int callScript(
       List<String> commandLine, Integer timeoutMs, List<FileProperties> files, Object authToken)
       throws Message {
-    // TODO getUser().checkPermission("SCRIPTING:EXECUTE:" + commandLine.get(0));
     caller =
         new ServerSideScriptingCaller(
             commandLine.toArray(new String[commandLine.size()]), timeoutMs, files, authToken);
diff --git a/src/main/java/caosdb/server/scripting/ScriptingPermissions.java b/src/main/java/caosdb/server/scripting/ScriptingPermissions.java
new file mode 100644
index 00000000..b1417035
--- /dev/null
+++ b/src/main/java/caosdb/server/scripting/ScriptingPermissions.java
@@ -0,0 +1,11 @@
+package caosdb.server.scripting;
+
+public class ScriptingPermissions {
+
+  public static final String PERMISSION_EXECUTION(final String call) {
+    StringBuilder ret = new StringBuilder(10 + call.length());
+    ret.append("SCRIPTING:EXECUTE:");
+    ret.append(call.replace("/", ":"));
+    return ret.toString();
+  }
+}
diff --git a/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java
index 6fa5d1a4..fa51d5ff 100644
--- a/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java
+++ b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java
@@ -35,6 +35,7 @@ import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.lang.ProcessBuilder.Redirect;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
@@ -255,7 +256,12 @@ public class ServerSideScriptingCaller {
   }
 
   int callScript() throws IOException, InterruptedException, TimeoutException {
-    String[] effectiveCommandLine = injectAuthToken(getCommandLine());
+    String[] effectiveCommandLine;
+    if (authToken != null) {
+      effectiveCommandLine = injectAuthToken(getCommandLine());
+    } else {
+      effectiveCommandLine = Arrays.copyOf(getCommandLine(), getCommandLine().length);
+    }
     effectiveCommandLine[0] = makeCallAbsolute(effectiveCommandLine[0]);
     final ProcessBuilder pb = new ProcessBuilder(effectiveCommandLine);
 
diff --git a/src/test/java/caosdb/server/authentication/AuthTokenTest.java b/src/test/java/caosdb/server/authentication/AuthTokenTest.java
index 9937dc60..7337d6e3 100644
--- a/src/test/java/caosdb/server/authentication/AuthTokenTest.java
+++ b/src/test/java/caosdb/server/authentication/AuthTokenTest.java
@@ -22,15 +22,23 @@
  */
 package caosdb.server.authentication;
 
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Map;
+import org.apache.commons.io.input.CharSequenceInputStream;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
 import caosdb.server.CaosDBServer;
 import caosdb.server.accessControl.AuthenticationUtils;
 import caosdb.server.accessControl.OneTimeAuthenticationToken;
+import caosdb.server.accessControl.OneTimeAuthenticationToken.Config;
 import caosdb.server.accessControl.Principal;
 import caosdb.server.accessControl.SessionToken;
-import java.io.IOException;
-import org.junit.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
 
 public class AuthTokenTest {
 
@@ -192,7 +200,7 @@ public class AuthTokenTest {
   }
 
   @Test
-  public void testOneTimeToken() {
+  public void testOneTimeTokenSerialization() {
     final String curry = null;
     final OneTimeAuthenticationToken t1 =
         new OneTimeAuthenticationToken(
@@ -201,7 +209,8 @@ public class AuthTokenTest {
             60000L,
             "sdfh37456sd",
             curry,
-            new String[] {""});
+            new String[] {"permissions"},
+            new String[] {"roles"});
     System.err.println(t1.toString());
     Assert.assertFalse(t1.isExpired());
     Assert.assertTrue(t1.isHashValid());
@@ -213,4 +222,93 @@ public class AuthTokenTest {
     Assert.assertTrue(OneTimeAuthenticationToken.parse(t1.toString(), curry).isHashValid());
     Assert.assertTrue(OneTimeAuthenticationToken.parse(t1.toString(), curry).isValid());
   }
+  
+  @Test
+  public void testOneTimeTokenConfigBasic() throws Exception {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("purpose: test purpose 1\n");
+    testYaml.append("roles: [ role1, \"role2\"]\n");
+    testYaml.append("permissions:\n");
+    testYaml.append("  - permission1\n");
+    testYaml.append("  - 'permission2'\n");
+    testYaml.append("  - \"permission3\"\n");
+    testYaml.append("  - \"permission with white space\"\n");
+
+    OneTimeAuthenticationToken.init(new CharSequenceInputStream(testYaml, "utf-8"));
+    Map<String, Config> map = OneTimeAuthenticationToken.getPurposeMap();
+    Assert.assertNotNull("test purpose there", map.get("test purpose 1"));
+    Assert.assertTrue("parsing to Config object", map.get("test purpose 1") instanceof Config);
+    Config config = map.get("test purpose 1");
+
+    Assert.assertArrayEquals("permissions parsed", new String[] {"permission1", "permission2", "permission3", "permission with white space"}, config.getPermissions());
+    Assert.assertArrayEquals("roles parsed", new String[] {"role1", "role2" }, config.getRoles());
+  }
+
+  @Test
+  public void testOneTimeTokenConfigNoRoles() throws Exception {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("purpose: no roles test\n");
+    testYaml.append("permissions:\n");
+    testYaml.append("  - permission1\n");
+    testYaml.append("  - 'permission2'\n");
+    testYaml.append("  - \"permission3\"\n");
+    testYaml.append("  - \"permission with white space\"\n");
+
+    OneTimeAuthenticationToken.init(new CharSequenceInputStream(testYaml, "utf-8"));
+    Map<String, Config> map = OneTimeAuthenticationToken.getPurposeMap();
+    Config config = map.get("no roles test");
+
+    Assert.assertArrayEquals("empty roles array parsed", new String[] {}, config.getRoles());
+  }
+
+  @Test
+  public void testOneTimeTokenConfigNoPurpose() throws Exception {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("permissions:\n");
+    testYaml.append("  - permission1\n");
+    testYaml.append("  - 'permission2'\n");
+    testYaml.append("  - \"permission3\"\n");
+    testYaml.append("  - \"permission with white space\"\n");
+
+    OneTimeAuthenticationToken.init(new CharSequenceInputStream(testYaml, "utf-8"));
+    Map<String, Config> map = OneTimeAuthenticationToken.getPurposeMap();
+    Assert.assertEquals(map.size(), 0);
+  }
+
+  @Test
+  public void testOneTimeTokenConfigMulti() throws Exception {
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("- purpose: purpose 1\n");
+    testYaml.append("- purpose: purpose 2\n");
+    testYaml.append("- purpose: purpose 3\n");
+    testYaml.append("- purpose: purpose 4\n");
+
+    OneTimeAuthenticationToken.init(new CharSequenceInputStream(testYaml, "utf-8"));
+    Map<String, Config> map = OneTimeAuthenticationToken.getPurposeMap();
+    Assert.assertEquals("four items", 4, map.size());
+    Assert.assertTrue(map.get("purpose 2") instanceof Config);
+  }
+  
+  @Test
+  public void testOneTimeTokenConfigOutputFile() throws Exception {
+    File tempFile = File.createTempFile("authtoken", "json");
+    tempFile.deleteOnExit();
+
+    StringBuilder testYaml = new StringBuilder();
+    testYaml.append("- output:\n");
+    testYaml.append("    file: " + tempFile.getAbsolutePath() + "\n");
+    testYaml.append("  permissions: [ permission1 ]\n");
+    
+    // write the token
+    OneTimeAuthenticationToken.init(new CharSequenceInputStream(testYaml, "utf-8"));
+    Assert.assertTrue(tempFile.exists());
+    try ( BufferedReader reader = new BufferedReader(new FileReader(tempFile))) {
+      OneTimeAuthenticationToken token = OneTimeAuthenticationToken.parse(reader.readLine(), null);
+      assertEquals("Token has anonymous username", "anonymous", token.getPrincipal().getUsername());
+      assertEquals("Token has anonymous realm", "anonymous", token.getPrincipal().getRealm());
+      assertArrayEquals("Permissions array has been written and read", new String[] { "permission1" }, token.getPermissions());
+    }
+    
+  }
+
 }
diff --git a/src/test/java/caosdb/server/resource/TestScriptingResource.java b/src/test/java/caosdb/server/resource/TestScriptingResource.java
index 2edfb82b..e1538573 100644
--- a/src/test/java/caosdb/server/resource/TestScriptingResource.java
+++ b/src/test/java/caosdb/server/resource/TestScriptingResource.java
@@ -23,7 +23,6 @@
 package caosdb.server.resource;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
 import caosdb.server.CaosDBServer;
 import caosdb.server.accessControl.AuthenticationUtils;
@@ -177,7 +176,7 @@ public class TestScriptingResource {
         };
 
         @Override
-        public Object generateAuthToken() {
+        public Object generateAuthToken(String purpose) {
           return "";
         }
       };
@@ -201,10 +200,26 @@ public class TestScriptingResource {
   }
 
   @Test
-  public void testAnonymous() {
+  public void testAnonymousWithOutPermission() {
+    Subject user = SecurityUtils.getSubject();
+    user.login(AuthenticationUtils.ANONYMOUS_USER);
+    Representation entity = new StringRepresentation("asdf");
+    entity.setMediaType(MediaType.TEXT_ALL);
+    Request request = new Request(Method.POST, "../test", entity);
+    request.setRootRef(new Reference("bla"));
+    request.getAttributes().put("SRID", "asdf1234");
+    request.setDate(new Date());
+    request.setHostRef("bla");
+    resource.init(null, request, new Response(null));
+
+    resource.handle();
+    assertEquals(Status.CLIENT_ERROR_FORBIDDEN, resource.getResponse().getStatus());
+  }
+
+  @Test
+  public void testAnonymousWithPermission() {
     Subject user = SecurityUtils.getSubject();
     user.login(AuthenticationUtils.ANONYMOUS_USER);
-    assertTrue(resource.isAnonymous());
     Representation entity = new StringRepresentation("asdf");
     entity.setMediaType(MediaType.TEXT_ALL);
     Request request = new Request(Method.POST, "../test", entity);
diff --git a/src/test/java/caosdb/server/utils/WebinterfaceUtilsTest.java b/src/test/java/caosdb/server/utils/WebinterfaceUtilsTest.java
index 4ff5d718..22dc38c7 100644
--- a/src/test/java/caosdb/server/utils/WebinterfaceUtilsTest.java
+++ b/src/test/java/caosdb/server/utils/WebinterfaceUtilsTest.java
@@ -5,6 +5,7 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
 import caosdb.server.CaosDBServer;
+import caosdb.server.ServerProperties;
 import java.io.IOException;
 import org.junit.BeforeClass;
 import org.junit.Rule;
@@ -26,7 +27,11 @@ public class WebinterfaceUtilsTest {
     WebinterfaceUtils utils = new WebinterfaceUtils(new Reference("https://host:2345/some_path"));
     String buildNumber = utils.getBuildNumber();
     String ref = utils.getWebinterfaceURI("sub");
-    assertEquals("https://host:2345/webinterface/" + buildNumber + "/sub", ref);
+    String contextRoot = CaosDBServer.getServerProperty(ServerProperties.KEY_CONTEXT_ROOT);
+    contextRoot =
+        contextRoot != null ? "/" + contextRoot.replaceFirst("^/", "").replaceFirst("/$", "") : "";
+
+    assertEquals("https://host:2345" + contextRoot + "/webinterface/" + buildNumber + "/sub", ref);
   }
 
   @Test
-- 
GitLab