From 2993d78b113102a871e76d81d178c6c95eac48d7 Mon Sep 17 00:00:00 2001
From: Timm Fitschen <t.fitschen@indiscale.com>
Date: Mon, 20 Dec 2021 12:55:32 +0100
Subject: [PATCH] Add Entity Permission Rules to GRPC API

---
 caosdb-proto                                  |   2 +-
 .../server/accessControl/ACMPermissions.java  | 235 ++++++++++++++++--
 .../server/accessControl/AnonymousRealm.java  |   9 +-
 .../server/accessControl/Principal.java       |   2 +-
 .../accessControl/SessionTokenRealm.java      |   2 +-
 .../server/accessControl/UserSources.java     |  18 ++
 .../implementation/MySQL/MySQLHelper.java     |   8 +-
 .../backend/transaction/RetrieveRole.java     |   5 +
 .../AccessControlManagementServiceImpl.java   |  24 +-
 .../caosdb/server/grpc/AuthInterceptor.java   |   8 -
 .../server/grpc/CaosDBToGrpcConverters.java   |  34 ++-
 .../server/grpc/GrpcToCaosDBConverters.java   |  13 +-
 .../server/jobs/core/AccessControl.java       |  44 +++-
 .../jobs/core/CheckStateTransition.java       |  35 ++-
 .../server/jobs/core/EntityStateJob.java      |  24 +-
 .../server/permissions/EntityPermission.java  |   4 +
 .../caosdb/server/permissions/Permission.java |   5 -
 .../java/org/caosdb/server/query/Query.java   |   6 +
 .../scripting/ScriptingPermissions.java       |  24 +-
 .../transaction/AccessControlTransaction.java |   8 +
 .../transaction/DeleteUserTransaction.java    |   9 +-
 .../FileStorageConsistencyCheck.java          |   7 +
 .../InsertLogRecordTransaction.java           |   8 +
 .../transaction/InsertRoleTransaction.java    |  17 +-
 .../transaction/ListRolesTransaction.java     |   2 +-
 .../RetrieveLogRecordTransaction.java         |   8 +
 .../RetrieveUserRolesTransaction.java         |   8 +
 .../transaction/TransactionInterface.java     |   3 +
 .../caosdb/server/transaction/UpdateACL.java  |  37 ++-
 .../transaction/UpdateRoleTransaction.java    |  39 ++-
 .../transaction/UpdateUserTransaction.java    |  36 ++-
 .../server/transaction/WriteTransaction.java  |   3 +-
 .../WriteTransactionInterface.java            |   5 +
 .../java/org/caosdb/server/utils/Info.java    |   8 +
 .../caosdb/server/utils/Initialization.java   |   8 +
 .../caosdb/server/utils/ServerMessages.java   |  12 +
 36 files changed, 604 insertions(+), 116 deletions(-)

diff --git a/caosdb-proto b/caosdb-proto
index a6650844..2f3e4ad1 160000
--- a/caosdb-proto
+++ b/caosdb-proto
@@ -1 +1 @@
-Subproject commit a66508445c9fe9c4bc3b55eb3bdaff147670bd39
+Subproject commit 2f3e4ad1cf515450fcfedb300f66198b82122b7e
diff --git a/src/main/java/org/caosdb/server/accessControl/ACMPermissions.java b/src/main/java/org/caosdb/server/accessControl/ACMPermissions.java
index 84844e89..264af444 100644
--- a/src/main/java/org/caosdb/server/accessControl/ACMPermissions.java
+++ b/src/main/java/org/caosdb/server/accessControl/ACMPermissions.java
@@ -1,9 +1,10 @@
 /*
- * ** 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
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2021 IndiScale GmbH <info@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
@@ -17,79 +18,269 @@
  *
  * 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.accessControl;
 
-public class ACMPermissions {
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import org.caosdb.server.jobs.core.AccessControl;
+import org.caosdb.server.jobs.core.CheckStateTransition;
+import org.caosdb.server.scripting.ScriptingPermissions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ACMPermissions implements Comparable<ACMPermissions> {
+
+  private static Logger LOGGER = LoggerFactory.getLogger(ACMPermissions.class);
+  public static final String USER_PARAMETER = "?USER?";
+  public static final String REALM_PARAMETER = "?REALM?";
+  public static final String ROLE_PARAMETER = "?ROLE?";
+  public static Set<ACMPermissions> ALL = new HashSet<>();
+  public static final ACMPermissions GENERIC_ACM_PERMISSION =
+      new ACMPermissions(
+          "ACM:*",
+          "Permissions to administrate the access controll management system. That includes managing users, roles, and assigning permissions to roles and roles to users.");
+
+  protected final String permission;
+  protected final String description;
+
+  public ACMPermissions(String permission, String description) {
+    if (permission == null) {
+      throw new NullPointerException("Permission must no be null");
+    }
+    this.permission = permission;
+    this.description = description;
+    ALL.add(this);
+  }
+
+  @Override
+  public final boolean equals(Object obj) {
+    if (obj instanceof ACMPermissions) {
+      ACMPermissions that = (ACMPermissions) obj;
+      return this.permission.equals(that.permission);
+    }
+    return false;
+  }
+
+  @Override
+  public final int hashCode() {
+    return permission.hashCode();
+  }
+
+  @Override
+  public final String toString() {
+    return this.permission;
+  }
+
+  public final String getDescription() {
+    return description;
+  }
+
+  public static final class UserPermission extends ACMPermissions {
+
+    public static final ACMPermissions GENERIC_USER_PERMISSION =
+        new ACMPermissions(
+            "ACM:USER:*",
+            "Permissions to manage users, i.e. create, retrieve, update and delete users.");
+
+    public UserPermission(String permission, String description) {
+      super(permission, description);
+    }
+
+    public String toString(String realm) {
+      return toString().replace(REALM_PARAMETER, realm);
+    }
+
+    public String toString(String realm, String user) {
+      return toString(realm).replace(USER_PARAMETER, user);
+    }
+  }
+
+  public static final class RolePermission extends ACMPermissions {
+
+    public static final ACMPermissions GENERIC_ROLE_PERMISSION =
+        new ACMPermissions(
+            "ACM:ROLE:*",
+            "Permissions to manage roles, i.e. create, retrieve, update and delete roles and assign them to users.");
+
+    public RolePermission(String permission, String description) {
+      super(permission, description);
+    }
+
+    public String toString(String role) {
+      return toString().replace(ROLE_PARAMETER, role);
+    }
+  }
+
+  public static final String PERMISSION_ACCESS_SERVER_PROPERTIES =
+      new ACMPermissions("ACCESS_SERVER_PROPERTIES", "Permission to read the server properties.")
+          .toString();
+
+  @Deprecated
+  public static final String PERMISSION_RETRIEVE_SERVERLOGS =
+      new ACMPermissions("SERVERLOGS:RETRIEVE", "Permission to read the server logs. (DEPRECATED)")
+          .toString();
 
-  public static final String PERMISSION_ACCESS_SERVER_PROPERTIES = "ACCESS_SERVER_PROPERTIES";
-  public static final String PERMISSION_RETRIEVE_SERVERLOGS = "SERVERLOGS:RETRIEVE";
+  private static UserPermission retrieve_user_roles =
+      new UserPermission(
+          "ACM:USER:RETRIEVE:ROLES:" + REALM_PARAMETER + ":" + USER_PARAMETER,
+          "Permission to retrieve the roles of a user");
 
   public static final String PERMISSION_RETRIEVE_USER_ROLES(
       final String realm, final String username) {
-    return "ACM:USER:RETRIEVE:ROLES:" + realm + ":" + username;
+    return retrieve_user_roles.toString(realm, username);
   }
 
+  private static UserPermission retrieve_user_info =
+      new UserPermission(
+          "ACM:USER:RETRIEVE:INFO:" + REALM_PARAMETER + ":" + USER_PARAMETER,
+          "Permission to retrieve the user info (email, entity, status)");
+
   public static final String PERMISSION_RETRIEVE_USER_INFO(
       final String realm, final String username) {
-    return "ACM:USER:RETRIEVE:INFO:" + realm + ":" + username;
+    return retrieve_user_info.toString(realm, username);
   }
 
+  private static UserPermission delete_user =
+      new UserPermission(
+          "ACM:USER:DELETE:" + REALM_PARAMETER + ":" + USER_PARAMETER,
+          "Permission to delete a user");
+
   public static String PERMISSION_DELETE_USER(final String realm, final String username) {
-    return "ACM:USER:DELETE:" + realm + ":" + username;
+    return delete_user.toString(realm, username);
   }
 
+  private static final UserPermission insert_user =
+      new UserPermission(
+          "ACM:USER:INSERT:" + REALM_PARAMETER, "Permission to create a user in the given realm");
+
   public static String PERMISSION_INSERT_USER(final String realm) {
-    return "ACM:USER:INSERT:" + realm;
+    return insert_user.toString(realm);
   }
 
+  private static final UserPermission update_user_password =
+      new UserPermission(
+          "ACM:USER:UPDATE_PASSWORD:" + REALM_PARAMETER + ":" + USER_PARAMETER,
+          "Permission to set the password of a user.");
+
   public static String PERMISSION_UPDATE_USER_PASSWORD(final String realm, final String username) {
-    return "ACM:USER:UPDATE_PASSWORD:" + realm + ":" + username;
+    return update_user_password.toString(realm, username);
   }
 
+  private static final UserPermission update_user_email =
+      new UserPermission(
+          "ACM:USER:UPDATE:EMAIL:" + REALM_PARAMETER + ":" + USER_PARAMETER,
+          "Permission to update the email address of a user.");
+
   public static String PERMISSION_UPDATE_USER_EMAIL(final String realm, final String username) {
-    return "ACM:USER:UPDATE:EMAIL:" + realm + ":" + username;
+    return update_user_email.toString(realm, username);
   }
 
+  private static final UserPermission update_user_status =
+      new UserPermission(
+          "ACM:USER:UPDATE:STATUS:" + REALM_PARAMETER + ":" + USER_PARAMETER,
+          "Permission to update the status of a user, i.e. marking them as ACTIVE or INACTIVE.");
+
   public static String PERMISSION_UPDATE_USER_STATUS(final String realm, final String username) {
-    return "ACM:USER:UPDATE:STATUS:" + realm + ":" + username;
+    return update_user_status.toString(realm, username);
   }
 
+  private static final UserPermission update_user_entity =
+      new UserPermission(
+          "ACM:USER:UPDATE:ENTITY:" + REALM_PARAMETER + ":" + USER_PARAMETER,
+          "Permission to set the entity which is associated with a user.");
+
   public static String PERMISSION_UPDATE_USER_ENTITY(final String realm, final String username) {
-    return "ACM:USER:UPDATE:ENTITY:" + realm + ":" + username;
+    return update_user_entity.toString(realm, username);
   }
 
+  private static final UserPermission update_user_roles =
+      new UserPermission(
+          "ACM:USER:UPDATE:ROLES:" + REALM_PARAMETER + ":" + USER_PARAMETER,
+          "Permission to change the roles of a user.");
+
   public static String PERMISSION_UPDATE_USER_ROLES(final String realm, final String username) {
-    return "ACM:USER:UPDATE:ROLES:" + realm + ":" + username;
+    return update_user_roles.toString(realm, username);
   }
 
+  private static final RolePermission insert_role =
+      new RolePermission("ACM:ROLE:INSERT", "Permission to create a new role.");
+
   public static String PERMISSION_INSERT_ROLE() {
-    return "ACM:ROLE:INSERT";
+    return insert_role.toString();
   }
 
+  private static final RolePermission update_role_description =
+      new RolePermission(
+          "ACM:ROLE:UPDATE:DESCRIPTION:" + ROLE_PARAMETER,
+          "Permission to update the description of a role.");
+
   public static String PERMISSION_UPDATE_ROLE_DESCRIPTION(final String role) {
-    return "ACM:ROLE:UPDATE:DESCRIPTION:" + role;
+    return update_role_description.toString(role);
   }
 
+  private static final RolePermission retrieve_role_description =
+      new RolePermission(
+          "ACM:ROLE:RETRIEVE:DESCRIPTION:" + ROLE_PARAMETER,
+          "Permission to retrieve the description of a role.");
+
   public static String PERMISSION_RETRIEVE_ROLE_DESCRIPTION(final String role) {
-    return "ACM:ROLE:RETRIEVE:DESCRIPTION:" + role;
+    return retrieve_role_description.toString(role);
   }
 
+  private static final RolePermission delete_role =
+      new RolePermission("ACM:ROLE:DELETE:" + ROLE_PARAMETER, "Permission to delete a role.");
+
   public static String PERMISSION_DELETE_ROLE(final String role) {
-    return "ACM:ROLE:DELETE:" + role;
+    return delete_role.toString(role);
   }
 
+  private static final RolePermission update_role_permissions =
+      new RolePermission(
+          "ACM:ROLE:UPDATE:PERMISSIONS:" + ROLE_PARAMETER,
+          "Permission to set the permissions of a role.");
+
   public static String PERMISSION_UPDATE_ROLE_PERMISSIONS(final String role) {
-    return "ACM:ROLE:UPDATE:PERMISSIONS:" + role;
+    return update_role_permissions.toString(role);
   }
 
+  private static final RolePermission retrieve_role_permissions =
+      new RolePermission(
+          "ACM:ROLE:RETRIEVE:PERMISSIONS:" + ROLE_PARAMETER,
+          "Permission to read the permissions of a role.");
+
   public static String PERMISSION_RETRIEVE_ROLE_PERMISSIONS(final String role) {
-    return "ACM:ROLE:RETRIEVE:PERMISSIONS:" + role;
+    return retrieve_role_permissions.toString(role);
   }
 
+  private static final RolePermission assign_role =
+      new RolePermission(
+          "ACM:ROLE:ASSIGN:" + ROLE_PARAMETER, "Permission to assign a role (to a user).");
+
   public static String PERMISSION_ASSIGN_ROLE(final String role) {
-    return "ACM:ROLE:ASSIGN:" + role;
+    return assign_role.toString(role);
+  }
+
+  static {
+    // trigger adding all permissions to ALL
+    LOGGER.debug("Register permissions: ", ScriptingPermissions.PERMISSION_EXECUTION("*"));
+    LOGGER.debug("Register permissions: ", CheckStateTransition.STATE_PERMISSIONS.toString());
+    LOGGER.debug(
+        "Register permissions: ", CheckStateTransition.PERMISSION_STATE_FORCE_FINAL.toString());
+    LOGGER.debug("Register permissions: ", AccessControl.TRANSACTION_PERMISSIONS.toString());
+  }
+
+  @Override
+  public int compareTo(ACMPermissions that) {
+    return this.toString().compareToIgnoreCase(that.toString());
+  }
+
+  public static List<ACMPermissions> getAll() {
+    LinkedList<ACMPermissions> result = new LinkedList<>(ALL);
+    Collections.sort(result);
+    return result;
   }
 }
diff --git a/src/main/java/org/caosdb/server/accessControl/AnonymousRealm.java b/src/main/java/org/caosdb/server/accessControl/AnonymousRealm.java
index 006400bf..92b37c0e 100644
--- a/src/main/java/org/caosdb/server/accessControl/AnonymousRealm.java
+++ b/src/main/java/org/caosdb/server/accessControl/AnonymousRealm.java
@@ -27,12 +27,19 @@ import org.apache.shiro.authc.AuthenticationToken;
 import org.apache.shiro.authc.SimpleAuthenticationInfo;
 import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
 import org.apache.shiro.realm.AuthenticatingRealm;
+import org.caosdb.server.CaosDBServer;
+import org.caosdb.server.ServerProperties;
 
 public class AnonymousRealm extends AuthenticatingRealm {
 
   @Override
   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
-    return new SimpleAuthenticationInfo(token.getPrincipal(), null, getName());
+
+    if (CaosDBServer.getServerProperty(ServerProperties.KEY_AUTH_OPTIONAL)
+        .equalsIgnoreCase("true")) {
+      return new SimpleAuthenticationInfo(token.getPrincipal(), null, getName());
+    }
+    return null;
   }
 
   public AnonymousRealm() {
diff --git a/src/main/java/org/caosdb/server/accessControl/Principal.java b/src/main/java/org/caosdb/server/accessControl/Principal.java
index fc96fb99..7d414455 100644
--- a/src/main/java/org/caosdb/server/accessControl/Principal.java
+++ b/src/main/java/org/caosdb/server/accessControl/Principal.java
@@ -88,6 +88,6 @@ public class Principal implements ResponsibleAgent {
 
   @Override
   public String toString() {
-    return "[[" + this.realm + "]]" + this.username;
+    return this.username + REALM_SEPARATOR + this.realm;
   }
 }
diff --git a/src/main/java/org/caosdb/server/accessControl/SessionTokenRealm.java b/src/main/java/org/caosdb/server/accessControl/SessionTokenRealm.java
index d78ffb40..270565be 100644
--- a/src/main/java/org/caosdb/server/accessControl/SessionTokenRealm.java
+++ b/src/main/java/org/caosdb/server/accessControl/SessionTokenRealm.java
@@ -36,7 +36,7 @@ public class SessionTokenRealm extends AuthenticatingRealm {
     final SelfValidatingAuthenticationToken sessionToken =
         (SelfValidatingAuthenticationToken) token;
 
-    if (sessionToken.isValid()) {
+    if (sessionToken.isValid() && UserSources.isActive(sessionToken)) {
       return new SimpleAuthenticationInfo(sessionToken, null, getName());
     }
     return null;
diff --git a/src/main/java/org/caosdb/server/accessControl/UserSources.java b/src/main/java/org/caosdb/server/accessControl/UserSources.java
index 760c3e90..ad617361 100644
--- a/src/main/java/org/caosdb/server/accessControl/UserSources.java
+++ b/src/main/java/org/caosdb/server/accessControl/UserSources.java
@@ -92,6 +92,11 @@ public class UserSources extends HashMap<String, UserSource> {
     if (principal.getRealm().equals(OneTimeAuthenticationToken.REALM_NAME)) {
       return true;
     }
+    if (principal.toString().equals(AnonymousAuthenticationToken.PRINCIPAL.toString())
+        && CaosDBServer.getServerProperty(ServerProperties.KEY_AUTH_OPTIONAL)
+            .equalsIgnoreCase("true")) {
+      return true;
+    }
     UserSource userSource = instance.get(principal.getRealm());
     if (userSource != null) {
       return userSource.isUserExisting(principal.getUsername());
@@ -315,4 +320,17 @@ public class UserSources extends HashMap<String, UserSource> {
       throw new AuthenticationException(e);
     }
   }
+
+  public static boolean isActive(Principal principal) {
+    if (principal.getRealm().equals(OneTimeAuthenticationToken.REALM_NAME)) {
+      return true;
+    }
+    if (principal.getUsername().equals(AnonymousAuthenticationToken.PRINCIPAL.getUsername())
+        && principal.getRealm().equals(AnonymousAuthenticationToken.PRINCIPAL.getRealm())
+        && CaosDBServer.getServerProperty(ServerProperties.KEY_AUTH_OPTIONAL)
+            .equalsIgnoreCase("true")) {
+      return true;
+    }
+    return isActive(principal.getRealm(), principal.getUsername());
+  }
 }
diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java
index b1f445e2..568e0e53 100644
--- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java
+++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java
@@ -35,7 +35,7 @@ import org.caosdb.server.accessControl.Principal;
 import org.caosdb.server.database.misc.DBHelper;
 import org.caosdb.server.transaction.ChecksumUpdater;
 import org.caosdb.server.transaction.TransactionInterface;
-import org.caosdb.server.transaction.WriteTransaction;
+import org.caosdb.server.transaction.WriteTransactionInterface;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -55,7 +55,7 @@ public class MySQLHelper implements DBHelper {
    *
    * <p>In the database, this adds a row to the transaction table with SRID, user and timestamp.
    */
-  public void initTransaction(Connection connection, WriteTransaction transaction)
+  public void initTransaction(Connection connection, WriteTransactionInterface transaction)
       throws SQLException {
     try (CallableStatement call = connection.prepareCall("CALL set_transaction(?,?,?,?,?)")) {
 
@@ -86,10 +86,10 @@ public class MySQLHelper implements DBHelper {
     if (transaction instanceof ChecksumUpdater) {
       connection.setReadOnly(false);
       connection.setAutoCommit(false);
-    } else if (transaction instanceof WriteTransaction) {
+    } else if (transaction instanceof WriteTransactionInterface) {
       connection.setReadOnly(false);
       connection.setAutoCommit(false);
-      initTransaction(connection, (WriteTransaction) transaction);
+      initTransaction(connection, (WriteTransactionInterface) transaction);
     } else {
       connection.setReadOnly(false);
       connection.setAutoCommit(true);
diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveRole.java b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveRole.java
index 1e855663..f4059832 100644
--- a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveRole.java
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveRole.java
@@ -24,6 +24,7 @@
  */
 package org.caosdb.server.database.backend.transaction;
 
+import java.util.Set;
 import org.apache.commons.jcs.access.behavior.ICacheAccess;
 import org.caosdb.server.accessControl.Role;
 import org.caosdb.server.caching.Cache;
@@ -66,4 +67,8 @@ public class RetrieveRole extends CacheableBackendTransaction<String, Role> {
   public static void removeCached(final String name) {
     cache.remove(name);
   }
+
+  public static void removeCached(Set<String> roles) {
+    roles.forEach(RetrieveRole::removeCached);
+  }
 }
diff --git a/src/main/java/org/caosdb/server/grpc/AccessControlManagementServiceImpl.java b/src/main/java/org/caosdb/server/grpc/AccessControlManagementServiceImpl.java
index cd8e1b87..88429828 100644
--- a/src/main/java/org/caosdb/server/grpc/AccessControlManagementServiceImpl.java
+++ b/src/main/java/org/caosdb/server/grpc/AccessControlManagementServiceImpl.java
@@ -230,23 +230,13 @@ public class AccessControlManagementServiceImpl extends AccessControlManagementS
 
   private Iterable<PermissionDescription> listKnownPermissions() {
     List<PermissionDescription> result = new LinkedList<>();
-    result.add(
-        PermissionDescription.newBuilder()
-            .setPermission(ACMPermissions.PERMISSION_INSERT_ROLE())
-            .setDescription("Create a new user role.")
-            .build());
-    result.add(
-        PermissionDescription.newBuilder()
-            .setPermission(ACMPermissions.PERMISSION_INSERT_USER("?REALM?"))
-            .setDescription("Create a new user in the given realm.")
-            .build());
-    result.add(
-        PermissionDescription.newBuilder()
-            .setPermission(ACMPermissions.PERMISSION_ACCESS_SERVER_PROPERTIES)
-            .setDescription(
-                "Read and set server properties on the fly when the server is in debug mode.")
-            .build());
-    // TODO
+    for (ACMPermissions p : ACMPermissions.getAll()) {
+      result.add(
+          PermissionDescription.newBuilder()
+              .setPermission(p.toString())
+              .setDescription(p.getDescription())
+              .build());
+    }
     return result;
   }
 
diff --git a/src/main/java/org/caosdb/server/grpc/AuthInterceptor.java b/src/main/java/org/caosdb/server/grpc/AuthInterceptor.java
index 4bd8f295..ce8a6221 100644
--- a/src/main/java/org/caosdb/server/grpc/AuthInterceptor.java
+++ b/src/main/java/org/caosdb/server/grpc/AuthInterceptor.java
@@ -124,14 +124,6 @@ public class AuthInterceptor implements ServerInterceptor {
       final Metadata headers,
       final ServerCallHandler<ReqT, RespT> next) {
     ThreadContext.remove();
-    Subject user = SecurityUtils.getSubject();
-    System.out.println(
-        "interceptCall: "
-            + Long.toString(Thread.currentThread().getId())
-            + " "
-            + Thread.currentThread().getName()
-            + " subject: "
-            + user.toString());
 
     String authentication = headers.get(AUTHENTICATION_HEADER);
     if (authentication == null) {
diff --git a/src/main/java/org/caosdb/server/grpc/CaosDBToGrpcConverters.java b/src/main/java/org/caosdb/server/grpc/CaosDBToGrpcConverters.java
index 6d1beea5..913f677f 100644
--- a/src/main/java/org/caosdb/server/grpc/CaosDBToGrpcConverters.java
+++ b/src/main/java/org/caosdb/server/grpc/CaosDBToGrpcConverters.java
@@ -31,6 +31,7 @@ import org.caosdb.api.entity.v1.Entity;
 import org.caosdb.api.entity.v1.Entity.Builder;
 import org.caosdb.api.entity.v1.EntityACL;
 import org.caosdb.api.entity.v1.EntityPermissionRule;
+import org.caosdb.api.entity.v1.EntityPermissionRuleCapability;
 import org.caosdb.api.entity.v1.EntityResponse;
 import org.caosdb.api.entity.v1.EntityRole;
 import org.caosdb.api.entity.v1.Importance;
@@ -473,24 +474,45 @@ public class CaosDBToGrpcConverters {
     EntityACL.Builder builder = EntityACL.newBuilder();
     builder.setId(e.getId().toString());
     if (e.hasEntityACL()) {
-      builder.addAllRules(convert(e.getEntityACL()));
+      builder.addAllRules(convert(e.getEntityACL(), true));
     }
-    builder.addAllRules(convert(org.caosdb.server.permissions.EntityACL.GLOBAL_PERMISSIONS));
+    builder.addAllRules(convert(org.caosdb.server.permissions.EntityACL.GLOBAL_PERMISSIONS, false));
+    builder.addAllPermissions(getCurrentACLPermissions(e));
     // TODO errors?
     return builder.build();
   }
 
+  private Iterable<? extends org.caosdb.api.entity.v1.EntityPermission> getCurrentACLPermissions(
+      EntityInterface e) {
+    List<org.caosdb.api.entity.v1.EntityPermission> result = new LinkedList<>();
+    if (e.hasPermission(EntityPermission.EDIT_ACL)) {
+      org.caosdb.api.entity.v1.EntityPermission.Builder builder =
+          org.caosdb.api.entity.v1.EntityPermission.newBuilder();
+      result.add(builder.setName(EntityPermission.EDIT_ACL.getShortName()).build());
+    }
+    if (e.hasPermission(EntityPermission.EDIT_PRIORITY_ACL)) {
+      org.caosdb.api.entity.v1.EntityPermission.Builder builder =
+          org.caosdb.api.entity.v1.EntityPermission.newBuilder();
+      result.add(builder.setName(EntityPermission.EDIT_PRIORITY_ACL.getShortName()).build());
+    }
+    return result;
+  }
+
   private Iterable<? extends EntityPermissionRule> convert(
-      org.caosdb.server.permissions.EntityACL entityACL) {
+      org.caosdb.server.permissions.EntityACL entityACL, boolean deletable) {
     List<EntityPermissionRule> result = new LinkedList<>();
     for (EntityACI aci : entityACL.getRules()) {
-      result.add(
+      EntityPermissionRule.Builder builder =
           EntityPermissionRule.newBuilder()
               .setGrant(aci.isGrant())
               .setPriority(aci.isPriority())
               .setRole(aci.getResponsibleAgent().toString())
-              .addAllPermissions(convert(aci))
-              .build());
+              .addAllPermissions(convert(aci));
+      if (deletable) {
+        builder.addCapabilities(
+            EntityPermissionRuleCapability.ENTITY_PERMISSION_RULE_CAPABILITY_DELETE);
+      }
+      result.add(builder.build());
     }
     return result;
   }
diff --git a/src/main/java/org/caosdb/server/grpc/GrpcToCaosDBConverters.java b/src/main/java/org/caosdb/server/grpc/GrpcToCaosDBConverters.java
index 5ec896fd..5420a8d8 100644
--- a/src/main/java/org/caosdb/server/grpc/GrpcToCaosDBConverters.java
+++ b/src/main/java/org/caosdb/server/grpc/GrpcToCaosDBConverters.java
@@ -333,9 +333,16 @@ public class GrpcToCaosDBConverters {
   }
 
   private EntityInterface convert(EntityACL acl) {
-    UpdateEntity result = new UpdateEntity(Integer.parseInt(acl.getId()), null);
-    result.setEntityACL(convertAcl(acl));
-    return result;
+    try {
+      Integer id = getId(acl.getId());
+      UpdateEntity result = new UpdateEntity(id, null);
+      result.setEntityACL(convertAcl(acl));
+      return result;
+    } catch (NumberFormatException exc) {
+      UpdateEntity result = new UpdateEntity(null, null);
+      result.addError(ServerMessages.ENTITY_DOES_NOT_EXIST);
+      return result;
+    }
   }
 
   private org.caosdb.server.permissions.EntityACL convertAcl(EntityACL acl) {
diff --git a/src/main/java/org/caosdb/server/jobs/core/AccessControl.java b/src/main/java/org/caosdb/server/jobs/core/AccessControl.java
index 408384f4..dc596e50 100644
--- a/src/main/java/org/caosdb/server/jobs/core/AccessControl.java
+++ b/src/main/java/org/caosdb/server/jobs/core/AccessControl.java
@@ -24,6 +24,7 @@ package org.caosdb.server.jobs.core;
 
 import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.subject.Subject;
+import org.caosdb.server.accessControl.ACMPermissions;
 import org.caosdb.server.database.backend.transaction.RetrieveSparseEntity;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.wrapper.Parent;
@@ -37,12 +38,47 @@ import org.caosdb.server.utils.ServerMessages;
 @JobAnnotation(stage = TransactionStage.INIT)
 public class AccessControl extends ContainerJob {
 
+  public static class TransactionPermission extends ACMPermissions {
+
+    public static final String ENTITY_ROLE_PARAMETER = "?ENTITY_ROLE?";
+
+    public TransactionPermission(String permission, String description) {
+      super(permission, description);
+    }
+
+    public final String toString(String entityRole) {
+      return toString().replace(ENTITY_ROLE_PARAMETER, entityRole);
+    }
+
+    public final String toString(String transaction, String entityRole) {
+      return "TRANSACTION:" + transaction + (entityRole != null ? (":" + entityRole) : "");
+    }
+  }
+
+  public static final TransactionPermission TRANSACTION_PERMISSIONS =
+      new TransactionPermission(
+          "TRANSACTION:*",
+          "Permission to execute any writable transaction. This permission only allows to execute these transactions in general. The necessary entities permissions are not implied.");
+  public static final TransactionPermission UPDATE =
+      new TransactionPermission(
+          "TRANSACTION:UPDATE:" + TransactionPermission.ENTITY_ROLE_PARAMETER,
+          "Permission to update entities of a given role (e.g. Record, File, RecordType, or Property).");
+  public static final TransactionPermission DELETE =
+      new TransactionPermission(
+          "TRANSACTION:DELETE:" + TransactionPermission.ENTITY_ROLE_PARAMETER,
+          "Permission to delete entities of a given role (e.g. Record, File, RecordType, or Property).");
+  public static final TransactionPermission INSERT =
+      new TransactionPermission(
+          "TRANSACTION:INSERT:" + TransactionPermission.ENTITY_ROLE_PARAMETER,
+          "Permission to insert entities of a given role (e.g. Record, File, RecordType, or Property).");
+
   @Override
   protected void run() {
     final Subject subject = SecurityUtils.getSubject();
 
     // subject has complete permissions for this kind of transaction
-    if (subject.isPermitted("TRANSACTION:" + getTransaction().getClass().getSimpleName())) {
+    if (subject.isPermitted(
+        TRANSACTION_PERMISSIONS.toString(getTransaction().getClass().getSimpleName(), null))) {
       return;
     }
 
@@ -54,10 +90,8 @@ public class AccessControl extends ContainerJob {
 
       // per role permission
       if (subject.isPermitted(
-          "TRANSACTION:"
-              + getTransaction().getClass().getSimpleName()
-              + ":"
-              + e.getRole().toString())) {
+          TRANSACTION_PERMISSIONS.toString(
+              getTransaction().getClass().getSimpleName(), e.getRole().toString()))) {
         continue;
       }
 
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
index 1a159910..fa21ed3a 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
@@ -22,6 +22,7 @@ package org.caosdb.server.jobs.core;
 
 import java.util.Map;
 import org.apache.shiro.authz.AuthorizationException;
+import org.caosdb.server.accessControl.ACMPermissions;
 import org.caosdb.server.entity.DeleteEntity;
 import org.caosdb.server.entity.Message;
 import org.caosdb.server.entity.Message.MessageType;
@@ -42,9 +43,31 @@ import org.caosdb.server.utils.ServerMessages;
 @JobAnnotation(stage = TransactionStage.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:";
+  public static final class StateModelPermission extends ACMPermissions {
+
+    public static final String STATE_MODEL_PARAMETER = "?STATE_MODEL?";
+
+    public StateModelPermission(String permission, String description) {
+      super(permission, description);
+    }
+
+    public final String toString(String state_model) {
+      return toString().replace(STATE_MODEL_PARAMETER, state_model);
+    }
+  }
+
+  public static final StateModelPermission PERMISSION_STATE_FORCE_FINAL =
+      new StateModelPermission(
+          "STATE:FORCE:FINAL",
+          "Permission to force to leave a state models specified life-cycle even though the currrent state isn't a final state in the that model.");
+  public static final StateModelPermission PERMISSION_STATE_UNASSIGN =
+      new StateModelPermission(
+          "STATE:UNASSIGN:" + StateModelPermission.STATE_MODEL_PARAMETER,
+          "Permission to unassign a state model.");
+  public static final StateModelPermission PERMISSION_STATE_ASSIGN =
+      new StateModelPermission(
+          "STATE:ASSIGN:" + StateModelPermission.STATE_MODEL_PARAMETER,
+          "Permission to assign a state model.");
   private static final Message TRANSITION_NOT_ALLOWED =
       new Message(MessageType.Error, "Transition not allowed.");
   private static final Message INITIAL_STATE_NOT_ALLOWED =
@@ -167,12 +190,12 @@ public class CheckStateTransition extends EntityStateJob {
   private void checkFinalState(State oldState) throws Message {
     if (!oldState.isFinal()) {
       if (isForceFinal()) {
-        getUser().checkPermission(PERMISSION_STATE_FORCE_FINAL);
+        getUser().checkPermission(PERMISSION_STATE_FORCE_FINAL.toString());
       } else {
         throw FINAL_STATE_NOT_ALLOWED;
       }
     }
-    getUser().checkPermission(PERMISSION_STATE_UNASSIGN + oldState.getStateModelName());
+    getUser().checkPermission(PERMISSION_STATE_UNASSIGN.toString(oldState.getStateModelName()));
   }
 
   /**
@@ -185,7 +208,7 @@ public class CheckStateTransition extends EntityStateJob {
     if (!newState.isInitial()) {
       throw INITIAL_STATE_NOT_ALLOWED;
     }
-    getUser().checkPermission(PERMISSION_STATE_ASSIGN + newState.getStateModelName());
+    getUser().checkPermission(PERMISSION_STATE_ASSIGN.toString(newState.getStateModelName()));
   }
 
   private boolean isForceFinal() {
diff --git a/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
index e2f59a88..16fe5936 100644
--- a/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
+++ b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
@@ -33,6 +33,7 @@ import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Set;
 import org.apache.shiro.subject.Subject;
+import org.caosdb.server.accessControl.ACMPermissions;
 import org.caosdb.server.database.exceptions.EntityDoesNotExistException;
 import org.caosdb.server.datatype.AbstractCollectionDatatype;
 import org.caosdb.server.datatype.CollectionValue;
@@ -82,6 +83,19 @@ import org.jdom2.Element;
  */
 public abstract class EntityStateJob extends EntityJob {
 
+  public static final class TransitionPermission extends ACMPermissions {
+
+    public static final String TRANSITION_PARAMETER = "?TRANSITION?";
+
+    public TransitionPermission(String permission, String description) {
+      super(permission, description);
+    }
+
+    public final String toString(String transition) {
+      return toString().replace(TRANSITION_PARAMETER, transition);
+    }
+  }
+
   protected static final String SERVER_PROPERTY_EXT_ENTITY_STATE = "EXT_ENTITY_STATE";
 
   public static final String TO_STATE_PROPERTY_NAME = "to";
@@ -102,7 +116,13 @@ public abstract class EntityStateJob extends EntityJob {
   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 ACMPermissions STATE_PERMISSIONS =
+      new ACMPermissions(
+          "STATE:*", "Permissions to manage state models and the states of entities.");
+  public static final TransitionPermission PERMISSION_STATE_TRANSION =
+      new TransitionPermission(
+          "STATE:TRANSITION:" + TransitionPermission.TRANSITION_PARAMETER,
+          "Permission to initiate a transition.");
 
   public static final Message STATE_MODEL_NOT_FOUND =
       new Message(MessageType.Error, "StateModel not found.");
@@ -253,7 +273,7 @@ public abstract class EntityStateJob extends EntityJob {
     }
 
     public boolean isPermitted(Subject user) {
-      return user.isPermitted(PERMISSION_STATE_TRANSION + this.name);
+      return user.isPermitted(PERMISSION_STATE_TRANSION.toString(this.name));
     }
   }
 
diff --git a/src/main/java/org/caosdb/server/permissions/EntityPermission.java b/src/main/java/org/caosdb/server/permissions/EntityPermission.java
index 1f84dce3..31e9cc47 100644
--- a/src/main/java/org/caosdb/server/permissions/EntityPermission.java
+++ b/src/main/java/org/caosdb/server/permissions/EntityPermission.java
@@ -37,6 +37,10 @@ public class EntityPermission extends Permission {
   private static final long serialVersionUID = -8935713878537140286L;
   private static List<EntityPermission> instances = new ArrayList<>();
   private final int bitNumber;
+  public static final Permission EDIT_PRIORITY_ACL =
+      new Permission(
+          "ADMIN:ENTITY:EDIT:PRIORITY_ACL",
+          "The permission to edit (add/delete) the prioritized rules of an acl of an entity.");
 
   public static ToElementable getAllEntityPermissions() {
     final Element entityPermissionsElement = new Element("EntityPermissions");
diff --git a/src/main/java/org/caosdb/server/permissions/Permission.java b/src/main/java/org/caosdb/server/permissions/Permission.java
index 44eee1c6..e8f33a3a 100644
--- a/src/main/java/org/caosdb/server/permissions/Permission.java
+++ b/src/main/java/org/caosdb/server/permissions/Permission.java
@@ -28,11 +28,6 @@ public class Permission extends WildcardPermission {
 
   private static final long serialVersionUID = -7471830472441416012L;
 
-  public static final org.apache.shiro.authz.Permission EDIT_PRIORITY_ACL =
-      new Permission(
-          "ADMIN:ENTITY:EDIT:PRIORITY_ACL",
-          "The permission to edit (add/delete) the prioritized rules of an acl of an entity.");
-
   private final String description;
 
   private final String shortName;
diff --git a/src/main/java/org/caosdb/server/query/Query.java b/src/main/java/org/caosdb/server/query/Query.java
index 95f853d6..07a165a3 100644
--- a/src/main/java/org/caosdb/server/query/Query.java
+++ b/src/main/java/org/caosdb/server/query/Query.java
@@ -47,6 +47,7 @@ import org.antlr.v4.runtime.CommonTokenStream;
 import org.apache.commons.jcs.access.behavior.ICacheAccess;
 import org.apache.shiro.subject.Subject;
 import org.caosdb.api.entity.v1.MessageCode;
+import org.caosdb.datetime.UTCDateTime;
 import org.caosdb.server.CaosDBServer;
 import org.caosdb.server.ServerProperties;
 import org.caosdb.server.caching.Cache;
@@ -1047,4 +1048,9 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
       return -1;
     }
   }
+
+  @Override
+  public UTCDateTime getTimestamp() {
+    return null;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/scripting/ScriptingPermissions.java b/src/main/java/org/caosdb/server/scripting/ScriptingPermissions.java
index 78ad99d7..ff596a79 100644
--- a/src/main/java/org/caosdb/server/scripting/ScriptingPermissions.java
+++ b/src/main/java/org/caosdb/server/scripting/ScriptingPermissions.java
@@ -1,11 +1,25 @@
 package org.caosdb.server.scripting;
 
-public class ScriptingPermissions {
+import org.caosdb.server.accessControl.ACMPermissions;
+
+public class ScriptingPermissions extends ACMPermissions {
+
+  public static final String PATH_PARAMETER = "?PATH?";
+
+  public ScriptingPermissions(String permission, String description) {
+    super(permission, description);
+  }
+
+  public String toString(String path) {
+    return toString().replace(PATH_PARAMETER, path.replace("/", ":"));
+  }
+
+  private static final ScriptingPermissions execution =
+      new ScriptingPermissions(
+          "SCRIPTING:EXECUTE:" + PATH_PARAMETER,
+          "Permission to execute a server-side script under the given path. Note that, for utilizing the wild cards feature, you have to use ':' as path separator. E.g. 'SCRIPTING:EXECUTE:my_scripts:*' would be the permission to execute all executables below the my_scripts directory.");
 
   public static final String PERMISSION_EXECUTION(final String call) {
-    StringBuilder ret = new StringBuilder(18 + call.length());
-    ret.append("SCRIPTING:EXECUTE:");
-    ret.append(call.replace("/", ":"));
-    return ret.toString();
+    return execution.toString(call);
   }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java b/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java
index 4897cd65..1cf9f8f7 100644
--- a/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java
@@ -22,6 +22,7 @@
  */
 package org.caosdb.server.transaction;
 
+import org.caosdb.datetime.UTCDateTime;
 import org.caosdb.server.database.DatabaseAccessManager;
 import org.caosdb.server.database.access.Access;
 import org.caosdb.server.database.misc.RollBackHandler;
@@ -30,9 +31,11 @@ import org.caosdb.server.entity.Message;
 public abstract class AccessControlTransaction implements TransactionInterface {
 
   private Access access;
+  private UTCDateTime timestamp;
 
   @Override
   public final void execute() throws Exception {
+    this.timestamp = UTCDateTime.SystemMillisToUTCDateTime(System.currentTimeMillis());
     this.access = DatabaseAccessManager.getAccountAccess(this);
 
     try {
@@ -54,4 +57,9 @@ public abstract class AccessControlTransaction implements TransactionInterface {
   }
 
   protected abstract void transaction() throws Exception;
+
+  @Override
+  public UTCDateTime getTimestamp() {
+    return timestamp;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/DeleteUserTransaction.java b/src/main/java/org/caosdb/server/transaction/DeleteUserTransaction.java
index f9e8ab74..24aac16b 100644
--- a/src/main/java/org/caosdb/server/transaction/DeleteUserTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/DeleteUserTransaction.java
@@ -24,8 +24,10 @@
 package org.caosdb.server.transaction;
 
 import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.subject.Subject;
 import org.caosdb.server.accessControl.ACMPermissions;
 import org.caosdb.server.accessControl.CredentialsValidator;
+import org.caosdb.server.accessControl.Principal;
 import org.caosdb.server.accessControl.UserSources;
 import org.caosdb.server.database.backend.transaction.DeletePassword;
 import org.caosdb.server.database.backend.transaction.DeleteUser;
@@ -50,8 +52,11 @@ public class DeleteUserTransaction extends AccessControlTransaction {
 
   @Override
   protected void transaction() throws Exception {
-    SecurityUtils.getSubject()
-        .checkPermission(ACMPermissions.PERMISSION_DELETE_USER(this.realm, this.name));
+    Subject subject = SecurityUtils.getSubject();
+    if (subject.getPrincipal().equals(new Principal(realm, name))) {
+      throw ServerMessages.CANNOT_DELETE_YOURSELF();
+    }
+    subject.checkPermission(ACMPermissions.PERMISSION_DELETE_USER(this.realm, this.name));
 
     final CredentialsValidator<String> validator =
         execute(new RetrievePasswordValidator(this.name), getAccess()).getValidator();
diff --git a/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java b/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java
index e066feb3..1ce1aa56 100644
--- a/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java
+++ b/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java
@@ -53,6 +53,7 @@ public class FileStorageConsistencyCheck extends Thread
   private Runnable finishRunnable = null;
   private final String location;
   private Long ts = null;
+  private UTCDateTime timestamp = null;
 
   public Exception getException() {
     return this.exception;
@@ -60,6 +61,7 @@ public class FileStorageConsistencyCheck extends Thread
 
   public FileStorageConsistencyCheck(final String location) {
     setDaemon(true);
+    this.timestamp = UTCDateTime.SystemMillisToUTCDateTime(System.currentTimeMillis());
     this.location = location.startsWith("/") ? location.replaceFirst("^/", "") : location;
   }
 
@@ -245,4 +247,9 @@ public class FileStorageConsistencyCheck extends Thread
   public void execute() throws Exception {
     run();
   }
+
+  @Override
+  public UTCDateTime getTimestamp() {
+    return timestamp;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/InsertLogRecordTransaction.java b/src/main/java/org/caosdb/server/transaction/InsertLogRecordTransaction.java
index c1128135..950d9b8d 100644
--- a/src/main/java/org/caosdb/server/transaction/InsertLogRecordTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/InsertLogRecordTransaction.java
@@ -24,6 +24,7 @@ package org.caosdb.server.transaction;
 
 import java.util.List;
 import java.util.logging.LogRecord;
+import org.caosdb.datetime.UTCDateTime;
 import org.caosdb.server.database.DatabaseAccessManager;
 import org.caosdb.server.database.access.Access;
 import org.caosdb.server.database.backend.transaction.InsertLogRecord;
@@ -31,8 +32,10 @@ import org.caosdb.server.database.backend.transaction.InsertLogRecord;
 public class InsertLogRecordTransaction implements TransactionInterface {
 
   private final List<LogRecord> toBeFlushed;
+  private UTCDateTime timestamp;
 
   public InsertLogRecordTransaction(final List<LogRecord> toBeFlushed) {
+    this.timestamp = UTCDateTime.SystemMillisToUTCDateTime(System.currentTimeMillis());
     this.toBeFlushed = toBeFlushed;
   }
 
@@ -45,4 +48,9 @@ public class InsertLogRecordTransaction implements TransactionInterface {
       access.release();
     }
   }
+
+  @Override
+  public UTCDateTime getTimestamp() {
+    return timestamp;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/InsertRoleTransaction.java b/src/main/java/org/caosdb/server/transaction/InsertRoleTransaction.java
index b70e2bd7..44288af5 100644
--- a/src/main/java/org/caosdb/server/transaction/InsertRoleTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/InsertRoleTransaction.java
@@ -22,11 +22,14 @@
  */
 package org.caosdb.server.transaction;
 
+import java.util.Set;
 import org.apache.shiro.SecurityUtils;
 import org.caosdb.server.accessControl.ACMPermissions;
 import org.caosdb.server.accessControl.Role;
 import org.caosdb.server.database.backend.transaction.InsertRole;
 import org.caosdb.server.database.backend.transaction.RetrieveRole;
+import org.caosdb.server.database.backend.transaction.SetPermissionRules;
+import org.caosdb.server.entity.Message;
 import org.caosdb.server.utils.ServerMessages;
 
 public class InsertRoleTransaction extends AccessControlTransaction {
@@ -37,14 +40,24 @@ public class InsertRoleTransaction extends AccessControlTransaction {
     this.role = role;
   }
 
-  @Override
-  protected void transaction() throws Exception {
+  private void checkPermissions() throws Message {
     SecurityUtils.getSubject().checkPermission(ACMPermissions.PERMISSION_INSERT_ROLE());
 
     if (execute(new RetrieveRole(this.role.name), getAccess()).getRole() != null) {
       throw ServerMessages.ROLE_NAME_IS_NOT_UNIQUE;
     }
+  }
+
+  @Override
+  protected void transaction() throws Exception {
+    checkPermissions();
 
     execute(new InsertRole(this.role), getAccess());
+    if (this.role.permission_rules != null) {
+      execute(
+          new SetPermissionRules(this.role.name, Set.copyOf(this.role.permission_rules)),
+          getAccess());
+    }
+    RetrieveRole.removeCached(this.role.name);
   }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/ListRolesTransaction.java b/src/main/java/org/caosdb/server/transaction/ListRolesTransaction.java
index d8c962a0..ed3cd3cc 100644
--- a/src/main/java/org/caosdb/server/transaction/ListRolesTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/ListRolesTransaction.java
@@ -48,7 +48,7 @@ public class ListRolesTransaction extends AccessControlTransaction {
                         ACMPermissions.PERMISSION_RETRIEVE_ROLE_DESCRIPTION(role.name)))
             .collect(Collectors.toList());
 
-    // remove known users
+    // remove users. the list will only contain name and description.
     for (Role role : roles) {
       if (role.users != null) {
         Iterator<ProtoUser> iterator = role.users.iterator();
diff --git a/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java b/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java
index 7e6c5278..2f6cafd6 100644
--- a/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java
@@ -26,6 +26,7 @@ import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import org.apache.shiro.SecurityUtils;
+import org.caosdb.datetime.UTCDateTime;
 import org.caosdb.server.accessControl.ACMPermissions;
 import org.caosdb.server.database.DatabaseAccessManager;
 import org.caosdb.server.database.access.Access;
@@ -37,10 +38,12 @@ public class RetrieveLogRecordTransaction implements TransactionInterface {
   private final String logger;
   private final Level level;
   private final String message;
+  private UTCDateTime timestamp;
 
   public RetrieveLogRecordTransaction(
       final String logger, final Level level, final String message) {
     this.level = level;
+    this.timestamp = UTCDateTime.SystemMillisToUTCDateTime(System.currentTimeMillis());
     if (message != null && message.isEmpty()) {
       this.message = null;
     } else if (message != null) {
@@ -73,4 +76,9 @@ public class RetrieveLogRecordTransaction implements TransactionInterface {
   public List<LogRecord> getLogRecords() {
     return this.logRecords;
   }
+
+  @Override
+  public UTCDateTime getTimestamp() {
+    return timestamp;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/RetrieveUserRolesTransaction.java b/src/main/java/org/caosdb/server/transaction/RetrieveUserRolesTransaction.java
index 97e518d0..e26e2983 100644
--- a/src/main/java/org/caosdb/server/transaction/RetrieveUserRolesTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/RetrieveUserRolesTransaction.java
@@ -23,6 +23,7 @@
 package org.caosdb.server.transaction;
 
 import java.util.Set;
+import org.caosdb.datetime.UTCDateTime;
 import org.caosdb.server.accessControl.Principal;
 import org.caosdb.server.accessControl.UserSources;
 import org.caosdb.server.utils.ServerMessages;
@@ -33,8 +34,10 @@ public class RetrieveUserRolesTransaction implements TransactionInterface {
   private final String user;
   private Set<String> roles;
   private final String realm;
+  private UTCDateTime timestamp;
 
   public RetrieveUserRolesTransaction(final String realm, final String user) {
+    this.timestamp = UTCDateTime.SystemMillisToUTCDateTime(System.currentTimeMillis());
     this.realm = realm;
     this.user = user;
   }
@@ -67,4 +70,9 @@ public class RetrieveUserRolesTransaction implements TransactionInterface {
   public Set<String> getRoles() {
     return this.roles;
   }
+
+  @Override
+  public UTCDateTime getTimestamp() {
+    return timestamp;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/TransactionInterface.java b/src/main/java/org/caosdb/server/transaction/TransactionInterface.java
index d56407ca..8e01c7d9 100644
--- a/src/main/java/org/caosdb/server/transaction/TransactionInterface.java
+++ b/src/main/java/org/caosdb/server/transaction/TransactionInterface.java
@@ -22,6 +22,7 @@
  */
 package org.caosdb.server.transaction;
 
+import org.caosdb.datetime.UTCDateTime;
 import org.caosdb.server.database.BackendTransaction;
 import org.caosdb.server.database.access.Access;
 import org.caosdb.server.database.misc.RollBackHandler;
@@ -48,4 +49,6 @@ public interface TransactionInterface {
     t.executeTransaction();
     return t;
   }
+
+  public UTCDateTime getTimestamp();
 }
diff --git a/src/main/java/org/caosdb/server/transaction/UpdateACL.java b/src/main/java/org/caosdb/server/transaction/UpdateACL.java
index 49125a2b..e0b0c5cf 100644
--- a/src/main/java/org/caosdb/server/transaction/UpdateACL.java
+++ b/src/main/java/org/caosdb/server/transaction/UpdateACL.java
@@ -21,6 +21,8 @@
 
 package org.caosdb.server.transaction;
 
+import static org.caosdb.server.query.Query.clearCache;
+
 import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction;
 import org.caosdb.server.database.backend.transaction.UpdateEntityTransaction;
 import org.caosdb.server.entity.EntityInterface;
@@ -67,23 +69,35 @@ public class UpdateACL extends Transaction<TransactionContainer>
     RetrieveFullEntityTransaction t = new RetrieveFullEntityTransaction(oldContainer);
     execute(t, getAccess());
 
+    // the entities in this container only have an id and an ACL. -> Replace
+    // with full entity and move ACL to full entity if permissions are
+    // sufficient, otherwise add an error.
     getContainer()
         .replaceAll(
             (e) -> {
               EntityInterface result = oldContainer.getEntityById(e.getId());
 
               // Check ACL update is permitted (against the old ACL) and set the new ACL afterwards.
-              EntityACL acl = result.getEntityACL();
-              if (acl != null && acl.isPermitted(getTransactor(), EntityPermission.EDIT_ACL)) {
-                if (acl.equals(e.getEntityACL())) {
+              EntityACL oldAcl = result.getEntityACL();
+              EntityACL newAcl = e.getEntityACL();
+              if (oldAcl != null
+                  && oldAcl.isPermitted(getTransactor(), EntityPermission.EDIT_ACL)) {
+                if (oldAcl.equals(newAcl)) {
                   // nothing to be done
                   result.setEntityStatus(EntityStatus.IGNORE);
                 } else {
+                  if (!oldAcl.getPriorityEntityACL().equals(newAcl.getPriorityEntityACL())
+                      && !oldAcl.isPermitted(getTransactor(), EntityPermission.EDIT_PRIORITY_ACL)) {
+                    // the user is now permitted to update the prioriy acl.
+                    result.addError(org.caosdb.server.utils.ServerMessages.AUTHORIZATION_ERROR);
+                  }
+
                   // we're good to go. set new entity acl
-                  result.setEntityACL(e.getEntityACL());
+                  result.setEntityACL(newAcl);
+                  result.setEntityStatus(EntityStatus.QUALIFIED);
                 }
-              } else if (acl != null
-                  && acl.isPermitted(getTransactor(), EntityPermission.RETRIEVE_ENTITY)) {
+              } else if (oldAcl != null
+                  && oldAcl.isPermitted(getTransactor(), EntityPermission.RETRIEVE_ENTITY)) {
                 // the user knows that this entity exists
                 result.addError(org.caosdb.server.utils.ServerMessages.AUTHORIZATION_ERROR);
               } else {
@@ -116,4 +130,15 @@ public class UpdateACL extends Transaction<TransactionContainer>
   protected void cleanUp() {
     getAccess().release();
   }
+
+  @Override
+  protected void commit() throws Exception {
+    getAccess().commit();
+    clearCache();
+  }
+
+  @Override
+  public String getSRID() {
+    return getContainer().getRequestId();
+  }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/UpdateRoleTransaction.java b/src/main/java/org/caosdb/server/transaction/UpdateRoleTransaction.java
index b1988ffa..a62aff5b 100644
--- a/src/main/java/org/caosdb/server/transaction/UpdateRoleTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/UpdateRoleTransaction.java
@@ -25,36 +25,55 @@ package org.caosdb.server.transaction;
 
 import java.util.Set;
 import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.subject.Subject;
 import org.caosdb.server.accessControl.ACMPermissions;
 import org.caosdb.server.accessControl.Role;
-import org.caosdb.server.accessControl.UserSources;
 import org.caosdb.server.database.backend.transaction.InsertRole;
 import org.caosdb.server.database.backend.transaction.RetrieveRole;
 import org.caosdb.server.database.backend.transaction.SetPermissionRules;
+import org.caosdb.server.entity.Message;
+import org.caosdb.server.permissions.PermissionRule;
 import org.caosdb.server.utils.ServerMessages;
 
 public class UpdateRoleTransaction extends AccessControlTransaction {
 
   private final Role role;
+  private Set<PermissionRule> newPermissionRules = null;
 
   public UpdateRoleTransaction(final Role role) {
     this.role = role;
   }
 
-  @Override
-  protected void transaction() throws Exception {
-    SecurityUtils.getSubject()
-        .checkPermission(ACMPermissions.PERMISSION_UPDATE_ROLE_DESCRIPTION(this.role.name));
+  private void checkPermissions() throws Message {
+
+    Subject subject = SecurityUtils.getSubject();
+    subject.checkPermission(ACMPermissions.PERMISSION_UPDATE_ROLE_DESCRIPTION(this.role.name));
 
-    if (!UserSources.isRoleExisting(this.role.name)) {
+    Role oldRole = execute(new RetrieveRole(this.role.name), getAccess()).getRole();
+    if (oldRole == null) {
       throw ServerMessages.ROLE_DOES_NOT_EXIST;
     }
 
-    execute(new InsertRole(this.role), getAccess());
     if (this.role.permission_rules != null) {
-      execute(
-          new SetPermissionRules(this.role.name, Set.copyOf(this.role.permission_rules)),
-          getAccess());
+      Set<PermissionRule> oldPermissions = Set.copyOf(oldRole.permission_rules);
+      Set<PermissionRule> newPermissions = Set.copyOf(role.permission_rules);
+      if (!oldPermissions.equals(newPermissions)) {
+        if (org.caosdb.server.permissions.Role.ADMINISTRATION.toString().equals(this.role.name)) {
+          throw ServerMessages.SPECIAL_ROLE_PERMISSIONS_CANNOT_BE_CHANGED();
+        }
+        subject.checkPermission(ACMPermissions.PERMISSION_UPDATE_ROLE_PERMISSIONS(this.role.name));
+        this.newPermissionRules = newPermissions;
+      }
+    }
+  }
+
+  @Override
+  protected void transaction() throws Exception {
+    checkPermissions();
+
+    execute(new InsertRole(this.role), getAccess());
+    if (this.newPermissionRules != null) {
+      execute(new SetPermissionRules(this.role.name, newPermissionRules), getAccess());
     }
     RetrieveRole.removeCached(this.role.name);
   }
diff --git a/src/main/java/org/caosdb/server/transaction/UpdateUserTransaction.java b/src/main/java/org/caosdb/server/transaction/UpdateUserTransaction.java
index 1fca0525..6e945118 100644
--- a/src/main/java/org/caosdb/server/transaction/UpdateUserTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/UpdateUserTransaction.java
@@ -23,12 +23,15 @@
 
 package org.caosdb.server.transaction;
 
+import java.util.HashSet;
+import java.util.Set;
 import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.subject.Subject;
 import org.caosdb.server.accessControl.ACMPermissions;
 import org.caosdb.server.accessControl.Principal;
 import org.caosdb.server.accessControl.UserSources;
 import org.caosdb.server.accessControl.UserStatus;
+import org.caosdb.server.database.backend.transaction.RetrieveRole;
 import org.caosdb.server.database.backend.transaction.RetrieveUser;
 import org.caosdb.server.database.backend.transaction.SetPassword;
 import org.caosdb.server.database.backend.transaction.UpdateUser;
@@ -36,6 +39,7 @@ import org.caosdb.server.database.backend.transaction.UpdateUserRoles;
 import org.caosdb.server.database.exceptions.TransactionException;
 import org.caosdb.server.database.proto.ProtoUser;
 import org.caosdb.server.entity.Entity;
+import org.caosdb.server.entity.Message;
 import org.caosdb.server.entity.RetrieveEntity;
 import org.caosdb.server.entity.container.RetrieveContainer;
 import org.caosdb.server.utils.EntityStatus;
@@ -50,6 +54,8 @@ public class UpdateUserTransaction extends AccessControlTransaction {
 
   private final String password;
   private final ProtoUser user;
+  private HashSet<String> newRoles;
+  private Set<String> oldRoles;
 
   public UpdateUserTransaction(
       final String realm,
@@ -79,9 +85,10 @@ public class UpdateUserTransaction extends AccessControlTransaction {
     this.password = password;
   }
 
-  @Override
-  protected void transaction() throws Exception {
-    if (!UserSources.isUserExisting(new Principal(this.user.realm, this.user.name))) {
+  private void checkPermissions() throws Message {
+    Principal principal = new Principal(this.user.realm, this.user.name);
+    ProtoUser oldUser = execute(new RetrieveUser(principal), getAccess()).getUser();
+    if (oldUser == null) {
       throw ServerMessages.ACCOUNT_DOES_NOT_EXIST;
     }
 
@@ -111,15 +118,30 @@ public class UpdateUserTransaction extends AccessControlTransaction {
     }
 
     if (this.user.roles != null) {
-      SecurityUtils.getSubject()
-          .checkPermission(
-              ACMPermissions.PERMISSION_UPDATE_USER_ROLES(this.user.realm, this.user.name));
+      Set<String> oldRoles = oldUser.roles;
+      if (!this.user.roles.equals(oldRoles)) {
+        SecurityUtils.getSubject()
+            .checkPermission(
+                ACMPermissions.PERMISSION_UPDATE_USER_ROLES(this.user.realm, this.user.name));
+      }
+      this.oldRoles = oldRoles;
+      this.newRoles = this.user.roles;
     }
+  }
+
+  @Override
+  protected void transaction() throws Exception {
+    checkPermissions();
+
     if (isToBeUpdated()) {
       execute(new UpdateUser(this.user), getAccess());
     }
-    if (this.user.roles != null) {
+    if (this.newRoles != null) {
       execute(new UpdateUserRoles(this.user.realm, this.user.name, this.user.roles), getAccess());
+      RetrieveRole.removeCached(newRoles);
+      if (this.oldRoles != null) {
+        RetrieveRole.removeCached(oldRoles);
+      }
     }
   }
 
diff --git a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java
index 1540ca79..ee9aa81e 100644
--- a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java
@@ -376,7 +376,7 @@ public class WriteTransaction extends Transaction<WritableContainer>
           .getPriorityEntityACL()
           .equals(oldEntity.getEntityACL().getPriorityEntityACL())) {
         // priority acl is to be changed?
-        oldEntity.checkPermission(Permission.EDIT_PRIORITY_ACL);
+        oldEntity.checkPermission(EntityPermission.EDIT_PRIORITY_ACL);
       }
       updatetable = true;
     } else if (!newEntity.hasEntityACL()) {
@@ -568,6 +568,7 @@ public class WriteTransaction extends Transaction<WritableContainer>
     return null;
   }
 
+  @Override
   public String getSRID() {
     return getContainer().getRequestId();
   }
diff --git a/src/main/java/org/caosdb/server/transaction/WriteTransactionInterface.java b/src/main/java/org/caosdb/server/transaction/WriteTransactionInterface.java
index 165acb40..4e2938e1 100644
--- a/src/main/java/org/caosdb/server/transaction/WriteTransactionInterface.java
+++ b/src/main/java/org/caosdb/server/transaction/WriteTransactionInterface.java
@@ -1,8 +1,13 @@
 package org.caosdb.server.transaction;
 
+import org.apache.shiro.subject.Subject;
 import org.caosdb.server.database.access.Access;
 
 public interface WriteTransactionInterface extends TransactionInterface {
 
   public Access getAccess();
+
+  public Subject getTransactor();
+
+  public String getSRID();
 }
diff --git a/src/main/java/org/caosdb/server/utils/Info.java b/src/main/java/org/caosdb/server/utils/Info.java
index 18ee0982..644239fc 100644
--- a/src/main/java/org/caosdb/server/utils/Info.java
+++ b/src/main/java/org/caosdb/server/utils/Info.java
@@ -28,6 +28,7 @@ import java.sql.SQLException;
 import java.util.LinkedList;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
+import org.caosdb.datetime.UTCDateTime;
 import org.caosdb.server.CaosDBServer;
 import org.caosdb.server.FileSystem;
 import org.caosdb.server.database.DatabaseAccessManager;
@@ -46,6 +47,7 @@ public class Info extends AbstractObservable implements Observer, TransactionInt
   public static final String SYNC_DATABASE_EVENT = "SyncDatabaseEvent";
   private final Access access;
   public Logger logger = LogManager.getLogger(getClass());
+  private UTCDateTime timestamp;
 
   @Override
   public boolean notifyObserver(final String e, final Observable o) {
@@ -58,6 +60,7 @@ public class Info extends AbstractObservable implements Observer, TransactionInt
   }
 
   private Info() {
+    this.timestamp = UTCDateTime.SystemMillisToUTCDateTime(System.currentTimeMillis());
     this.access = DatabaseAccessManager.getInfoAccess(this);
     try {
       syncDatabase();
@@ -246,4 +249,9 @@ public class Info extends AbstractObservable implements Observer, TransactionInt
   public void execute() throws Exception {
     syncDatabase();
   }
+
+  @Override
+  public UTCDateTime getTimestamp() {
+    return timestamp;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/utils/Initialization.java b/src/main/java/org/caosdb/server/utils/Initialization.java
index cb1a3630..4f28f206 100644
--- a/src/main/java/org/caosdb/server/utils/Initialization.java
+++ b/src/main/java/org/caosdb/server/utils/Initialization.java
@@ -24,6 +24,7 @@
  */
 package org.caosdb.server.utils;
 
+import org.caosdb.datetime.UTCDateTime;
 import org.caosdb.server.database.DatabaseAccessManager;
 import org.caosdb.server.database.access.Access;
 import org.caosdb.server.transaction.TransactionInterface;
@@ -31,9 +32,11 @@ import org.caosdb.server.transaction.TransactionInterface;
 public final class Initialization implements TransactionInterface, AutoCloseable {
 
   private Access access;
+  private UTCDateTime timestamp;
   private static final Initialization instance = new Initialization();
 
   private Initialization() {
+    this.timestamp = UTCDateTime.SystemMillisToUTCDateTime(System.currentTimeMillis());
     this.access = DatabaseAccessManager.getInitAccess(this);
   }
 
@@ -55,4 +58,9 @@ public final class Initialization implements TransactionInterface, AutoCloseable
       this.access = null;
     }
   }
+
+  @Override
+  public UTCDateTime getTimestamp() {
+    return timestamp;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/utils/ServerMessages.java b/src/main/java/org/caosdb/server/utils/ServerMessages.java
index ea522289..151f55fa 100644
--- a/src/main/java/org/caosdb/server/utils/ServerMessages.java
+++ b/src/main/java/org/caosdb/server/utils/ServerMessages.java
@@ -283,6 +283,13 @@ public class ServerMessages {
           MessageCode.MESSAGE_CODE_UNKNOWN,
           "This special role cannot be deleted. Ever.");
 
+  public static final Message SPECIAL_ROLE_PERMISSIONS_CANNOT_BE_CHANGED() {
+    return new Message(
+        MessageType.Error,
+        MessageCode.MESSAGE_CODE_UNKNOWN,
+        "This special role's permissions cannot be changed. Ever.");
+  }
+
   public static final Message QUERY_EXCEPTION =
       new Message(
           MessageType.Error,
@@ -604,4 +611,9 @@ public class ServerMessages {
         MessageCode.MESSAGE_CODE_UNKNOWN,
         "The user name does not comply with the policies for user names: " + policy);
   }
+
+  public static final Message CANNOT_DELETE_YOURSELF() {
+    return new Message(
+        MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "You cannot delete yourself.");
+  }
 }
-- 
GitLab