diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d3331a3b24145747836c0898ff852a8ade438bb4..c2de130bd802f9bd3be3d9f0e91a53a74401225b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,10 +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) 2019 Henrik tom Wörden
+# Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as
@@ -19,23 +19,45 @@
 # 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
-#
 
 variables:
   DEPLOY_REF: dev
   CI_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/src/caosdb-server/caosdb-server-testenv:latest
+  GIT_SUBMODULE_STRATEGY: normal
+
+  DEPLOY_PIPELINE: https://gitlab.indiscale.com/api/v4/projects/14/trigger/pipeline
+
+  ## FOR DEBUGGING
+  TRIGGERED_BY_REPO: SERVER
+  TRIGGERED_BY_REF: $CI_COMMIT_REF_NAME
+  TRIGGERED_BY_HASH: $CI_COMMIT_SHORT_SHA
 
 image: $CI_REGISTRY_IMAGE
 stages:
+  - info
   - setup
   - test
   - deploy
 
+.env: &env
+  - F_BRANCH="${CI_COMMIT_REF_NAME}"
+
+info:
+  tags: [cached-dind]
+  image: docker:20.10
+  stage: info
+  needs: []
+  script:
+    - *env
+    - echo "Pipeline triggered by $TRIGGERED_BY_REPO@$TRIGGERED_BY_REF ($TRIGGERED_BY_HASH)"
+    - echo "Pipeline will trigger DEPLOY with branch $DEPLOY_REF"
+    - echo "F_BRANCH = $F_BRANCH"
+
+
 # Setup: Build a docker image in which tests for this repository can run
 build-testenv:
   tags: [ cached-dind ]
-  image: docker:19.03
+  image: docker:20.10
   stage: setup
   timeout: 3h
   only:
@@ -61,18 +83,25 @@ test:
     - mvn compile
     - mvn test
 
+
+
 # Deploy: Trigger building of server image and integration tests
 trigger_build:
   tags: [ docker ]
   stage: deploy
+  needs: [ test ]
   script:
+    - *env
+
+    - echo "Triggering pipeline ${DEPLOY_PIPELINE}@${DEPLOY_REF} with F_BRANCH=${F_BRANCH}"
     - /usr/bin/curl -X POST
       -F token=$CI_JOB_TOKEN
-      -F "variables[F_BRANCH]=$CI_COMMIT_REF_NAME"
       -F "variables[SERVER]=$CI_COMMIT_REF_NAME"
-      -F "variables[TriggerdBy]=SERVER"
-      -F "variables[TriggerdByHash]=$CI_COMMIT_SHORT_SHA"
-      -F ref=$DEPLOY_REF https://gitlab.indiscale.com/api/v4/projects/14/trigger/pipeline
+      -F "variables[F_BRANCH]=$F_BRANCH"
+      -F "variables[TRIGGERED_BY_REPO]=$TRIGGERED_BY_REPO"
+      -F "variables[TRIGGERED_BY_REF]=$TRIGGERED_BY_REF"
+      -F "variables[TRIGGERED_BY_HASH]=$TRIGGERED_BY_HASH"
+      -F ref=$DEPLOY_REF $DEPLOY_PIPELINE
 
 # Build the sphinx documentation and make it ready for deployment by Gitlab Pages
 # Special job for serving a static website. See https://docs.gitlab.com/ee/ci/yaml/README.html#pages
@@ -85,7 +114,7 @@ pages_prepare: &pages_prepare
   script:
     - echo "Deploying..."
     - make doc
-    - cp -r build/doc/html public
+    - rm -r public || true ; cp -r build/doc/html public
   artifacts:
     paths:
       - public
diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md
deleted file mode 100644
index ac68afc814247e5a64395315e630d5de44a5f306..0000000000000000000000000000000000000000
--- a/.gitlab/merge_request_templates/Default.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Summary
-
-    Insert a meaningful description for this merge request here.  What is the
-    new/changed behavior? Which bug has been fixed? Are there related Issues?
-
-# Focus
-
-    Point the reviewer to the core of the code change. Where should they start
-    reading? What should they focus on (e.g. security, performance,
-    maintainability, user-friendliness, compliance with the specs, finding more
-    corner cases, concrete questions)?
-
-# Test Environment
-
-    How to set up a test environment for manual testing?
-
-# Check List for the Author
-
-Please, prepare your MR for a review. Be sure to write a summary and a
-focus and create gitlab comments for the reviewer. They should guide the
-reviewer through the changes, explain your changes and also point out open
-questions. For further good practices have a look at [our review
-guidelines](https://gitlab.com/caosdb/caosdb/-/blob/dev/REVIEW_GUIDELINES.md)
-
-- [ ] All automated tests pass
-- [ ] Reference related Issues
-- [ ] Up-to-date CHANGELOG.md
-- [ ] Annotations in code (Gitlab comments)
-  - Intent of new code
-  - Problems with old code
-  - Why this implementation?
-
-
-# Check List for the Reviewer
-
-
-- [ ] I understand the intent of this MR
-- [ ] All automated tests pass
-- [ ] Up-to-date CHANGELOG.md
-- [ ] The test environment setup works and the intended behavior is
-  reproducible in the test environment
-- [ ] In-code documentation and comments are up-to-date.
-- [ ] Check: Are there specifications? Are they satisfied?
-
-For further good practices have a look at [our review guidelines](https://gitlab.com/caosdb/caosdb/-/blob/dev/REVIEW_GUIDELINES.md).
-
-
-/assign me
diff --git a/.gitmodules b/.gitmodules
index 7a232e00813ac832eb23f82ae0e3104738bf9b9d..175f1f1740ad20fa27dd445118ff4285c1aaec25 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -2,3 +2,6 @@
 	path = caosdb-webui
 	url = ../caosdb-webui/
 	branch = dev
+[submodule "caosdb-proto"]
+	path = caosdb-proto
+	url = ../caosdb-proto/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cbdc98008960ef2fdce6402e404d52469cbead41..0777d7740fbf2b5e2ac0c71142405377dfa9af1e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,12 +19,123 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Security
 
+
+## [0.7.2] - 2022-03-25
+(Timm Fitschen)
+
+This is an important security update.
+
+### Added
+
+* Implementation for the ACM GRPC-API (caosdb-proto 0.2)
+* Implementation for the EntityACL GRPC-API (caosdb-proto 0.2)
+
+### Changed
+
+### Deprecated
+
+### Removed
+
+### Fixed
+
+* Wrong serialization of date time values in the GRPC-API (resulting in
+  org.caosdb.server.datatime@12347abcd or similar).
+* Missing serialization of file descriptors in the GRPC-API during retrievals.
+* [caosdb-server#131](https://gitlab.com/caosdb/caosdb-server/-/issues/131)
+  Query: AND does not work with sub-properties
+* Add previously missing `Value.equals` functions
+* [caosdb-server#132](https://gitlab.com/caosdb/caosdb-server/-/issues/132)
+  Query: subproperties should not require parentheses
+* [caosdb-server#174](https://gitlab.indiscale.com/caosdb/src/caosdb-server/-/issues/174)
+  GRPC-API: Server should gracefully handle non-file entities with file-like
+  attributes.
+* [caosdb-server#217](https://gitlab.indiscale.com/caosdb/src/caosdb-server/-/issues/217)
+  Server gets list property datatype wrong if description is updated.
+* [caosdb-server#220](https://gitlab.indiscale.com/caosdb/src/caosdb-server/-/issues/220)
+  Entities can be retrieved via GRPC despite insufficient permissions.
+* [caosdb-server#221](https://gitlab.indiscale.com/caosdb/src/caosdb-server/-/issues/221)
+  Unknown error during update of property leaving out datatype.
+* [caosdb-server#223](https://gitlab.indiscale.com/caosdb/src/caosdb-server/-/issues/223)
+  State is being leaked even though RETRIEVE:ENTITY permission is not granted.
+
+### Security
+
+* Update of logging backend log4j to 2.17.2
+* Fix for [caosdb-server#220](https://gitlab.indiscale.com/caosdb/src/caosdb-server/-/issues/220)
+  Entities can be retrieved via GRPC despite insufficient permissions.
+* Fix for [caosdb-server#223](https://gitlab.indiscale.com/caosdb/src/caosdb-server/-/issues/223)
+  State is being leaked even though RETRIEVE:ENTITY permission is not granted.
+
+## [v0.7.1] - 2021-12-13
+(Timm Fitschen)
+
+This is an important security update.
+
+### Added
+
+### Changed
+
+### Deprecated
+
+### Removed
+
+### Fixed
+
+### Security
+
+* Update of logging backend log4j after a critical security vulnerability
+  [CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228) to v2.15.0.
+* [caosdb-deploy#225](https://gitlab.indiscale.com/caosdb/src/caosdb-deploy/-/issues/225)
+  - Denied Edit permission leads to retrieve permission.
+
+## [v0.6.1] - 2021-11-13 [YANKED]
+(Timm Fitschen)
+
+This version's release was pulled after some problems during the release
+process. It is identical to v0.7.1
+
+
+## [v0.6.0] - 2021-11-17
+(Timm Fitschen)
+
+### Added
+
+* Endpoint for CaosDB GRPC API 0.1 (see https://gitlab.com/caosdb-proto.git for
+  more).
+  Authentication is supported via a Basic scheme, using the well-known
+  "authentication" header.
+  Notable limitations of the current implementation of the API:
+  * It is currently not possible to mix retrievals
+    (caosdb.entity.v1.RetrieveRequest) with any other transaction type - so
+    transaction are either read-only or write-only. The server throws an error
+    if it finds mixed read/write transactions.
+  * It is currently not possible to have more that one query
+    (caosdb.entity.v1.Query) in a single transaction. The server throws an
+    error if it finds more than one query.
+
+
+### Changed
+
+### Deprecated
+
+* Legacy XML/HTTP API (also known as the REST API). The API will not be removed
+  until the web interface (caosdb-webui) and the python client libraries have
+  been updated and freed from any dependencies. However, new clients should not
+  implement this API anymore.
+
+### Removed
+
+### Fixed
+
+### Security
+
 ## [v0.5.0] - 2021-10-19
 
 ### Added
 
 * An openAPI specification of the XML api
-* New server configuration option `SERVER_BIND_ADDRESS`, which is the address to listen to. See [server.conf](conf/core/server.conf).
+* New server configuration option `SERVER_BIND_ADDRESS`, which is the address to listen to. See
+  [server.conf](conf/core/server.conf).
 
 ### Changed
 
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index 6159dcd8f0ed2296e2f3ac992830f03659d1a131..3a388c541301c55ef32dd2e25e22c8ca750348b2 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -1,5 +1,30 @@
-* caosdb-mysqlbackend == 5.0.0
-* Java 11
-* Apache Maven >= 3.6.0
-* make >= 4.2.0
+# Dependencies
+
+## For Building and Running the Server
+
+* `>=caosdb-proto 0.2.0`
+* `>=caosdb-mysqlbackend 5.0.0`
+* `>=Java 11`
+* `>=Apache Maven 3.6.0`
+* `>=Make 4.2`
+* `libpam` (if PAM authentication is required)
 * More dependencies are being pulled and installed automatically by Maven. See the complete list of dependencies in the [pom.xml](pom.xml)
+
+## For Deploying a Web User Interface (optional)
+
+* `>=caosdb-webui 0.5.0`
+
+## For Building the Documentation (optional)
+
+* `>=Python 3.8`
+* `>=pip 21.0.0`
+
+## For Server-side Scripting (optional)
+
+* `>=Python 3.8`
+* `>=pip 21.0.0`
+* `openpyxl` (for XLS/ODS export)
+
+## Recommended Packages
+
+* `openssl` (if a custom TLS certificate is required)
diff --git a/FEATURES.md b/FEATURES.md
new file mode 100644
index 0000000000000000000000000000000000000000..ac273ca11876636a8e688ddc4458ecb48bf98e3d
--- /dev/null
+++ b/FEATURES.md
@@ -0,0 +1,21 @@
+# Features
+
+* The CaosDB Server implements a CaosDB GRPC API Endpoint (v0.2.0)
+  Authentication is supported via a Basic scheme, using the well-known
+  "authentication" header.
+  Notable limitations of the current implementation of the API:
+  * It is currently not possible to mix retrievals
+    (caosdb.entity.v1.RetrieveRequest) with any other transaction type - so
+    transaction are either read-only or write-only. The server throws an error
+    if it finds mixed read/write transactions.
+  * It is currently not possible to have more that one query
+    (caosdb.entity.v1.Query) in a single transaction. The server throws an
+    error if it finds more than one query.
+* Legacy XML/HTTP API (Deprecated)
+* Deployment of caosdb-webui (>=v0.4.1)
+* Server-side Scripting API (v0.1)
+* CaosDB Query Language Processor
+* CaosDB FileSystem
+* User Management, Authentication and Authorization
+  * Internal authentication service
+  * Authentication via an external service.
diff --git a/Makefile b/Makefile
index 7cafdb949cc213d1468383ce7c6a280452ef9904..29b7d5a51cb110e35f999da0a7c6b366b5bd0d05 100644
--- a/Makefile
+++ b/Makefile
@@ -57,7 +57,7 @@ formatting:
 
 # Compile into a standalone jar file
 jar: print-version easy-units
-	mvn package -DskipTests
+	mvn -e package -DskipTests
 	@pushd target ; \
 		ln -s caosdb-server-$(CAOSDB_SERVER_VERSION)-jar-with-dependencies.jar caosdb-server.jar; \
 		popd
diff --git a/README_SETUP.md b/README_SETUP.md
index d738ad13768df8878b55808e4519202fc95dbd86..77106e5aa931ebb064610959fe2efbff043eb04d 100644
--- a/README_SETUP.md
+++ b/README_SETUP.md
@@ -5,26 +5,7 @@ more.
 
 ## Requirements
 
-### CaosDB Packages
-
-* caosdb-webui=0.2.1
-* caosdb-mysqlbackend=3.0
-
-### Third-party Software
-
-* `>=Java 8`
-* `>=Apache Maven 3.0.4`
-* `>=Python 3.4`
-* `>=pip 9.0.1`
-* `>=git 1.9.1`
-* `>=Make 3.81`
-* `>=Screen 4.01`
-* `>=MySQL 5.5` (better `>=5.6`) or `>=MariaDB 10.1`
-* `libpam` (if PAM authentication is required)
-* `unzip`
-* `openpyxl` (for XLS/ODS export)
-* `openssl` (if a custom TLS certificate is required)
-- `easy-units` >= 0.0.1    https://gitlab.com/timm.fitschen/easy-units
+See [DEPENDENCIES.md](DEPENDENCIES.md).
 
 #### Install the requirements on Debian
 
@@ -56,9 +37,9 @@ installed and the pam user tool must be compiled:
 
 - `cd misc/pam_authentication/`
 - `make`
-- If you want, you can run a test now: `./pam_authentication.sh asdf ghjk`
-  should print `[FAILED]` and return with a non-zero exit code.  Unless there is
-  a user `asdf` with password `ghjk` on your system, of course.
+- If you want, you can run a test now: `./pam_authentication.sh asdf` asks for a password for user
+  `asdf`.  If no such user exists or the wrong passowrd is entered, it print `[FAILED]` and return
+  with a non-zero exit code.
 - If you want to run the CaosDB server without root privilege, you need to use
   the setuid bit for the binary. For example, if the user `caosdb` runs the
   server process the permissions of `bin/pam_authentication` should be the
@@ -77,12 +58,14 @@ libpam0g-dev`. Then try again.
 After a fresh clone of the repository, this is what you need to setup the
 server:
 
-1. Compile the server with `make compile`. This may take a while and there
+1. Install the `proto` submodule (and submodules for those extensions you want, see above):
+   `git submodule update --init caosdb-proto`
+2. Compile the server with `make compile`. This may take a while and there
    needs to be an internet connection as packages are downloaded to be
    integrated in the java file.
    1. It is recommended to run the unit tests with `make test`. It may take a
       while.
-2. Create an SSL certificate somewhere with a `Java Key Store` file.  For
+3. Create an SSL certificate somewhere with a `Java Key Store` file.  For
    self-signed certificates (not recommended for production use) you can do:
    - `mkdir certificates; cd certificates`
    - `keytool -genkey -keyalg RSA -alias selfsigned -keystore caosdb.jks -validity 375 -keysize 2048 -ext san=dns:localhost`
@@ -96,11 +79,11 @@ server:
    Alternatively, you can create a keystore from certificate files that you already have:
    - `openssl pkcs12 -export -inkey privkey.pem -in fullchain.pem -out all-certs.pkcs12`
    - `keytool -importkeystore -srckeystore all-certs.pkcs12 -srcstoretype PKCS12  -deststoretype pkcs12 -destkeystore caosdb.jks`
-3. Install/configure the MySQL back-end: see the `README_SETUP.md` of the
+4. Install/configure the MySQL back-end: see the `README_SETUP.md` of the
    `caosdb-mysqlbackend` repository
-4. Create an authtoken config (e.g. copy `conf/core/authtoken.example.yaml` to
+5. Create an authtoken config (e.g. copy `conf/core/authtoken.example.yaml` to
    `conf/ext/authtoken.yml` and change it)
-5. Copy `conf/core/server.conf` to `conf/ext/server.conf` and change it
+6. Copy `conf/core/server.conf` to `conf/ext/server.conf` and change it
    appropriately:
     * Setup for MySQL back-end:
       specify the fields `MYSQL_USER_NAME`, `MYSQL_USER_PASSWORD`,
@@ -113,7 +96,7 @@ server:
       `CERTIFICATES_KEY_STORE_PATH`, and `CERTIFICATES_KEY_STORE_PASSWORD`.
       Make sure that the conf file is not readable by other users because the
       certificate passwords are stored in plaintext.
-    - Set the path to the authtoken config (see step 4)
+    * Set the path to the authtoken config (see step 4)
     * Set the file system paths:
       - `FILE_SYSTEM_ROOT`: The root for all the files managed by CaosDB.
       - `DROP_OFF_BOX`: Files can be put here for insertion into CaosDB.
@@ -131,8 +114,8 @@ server:
       - `INSERT_FILES_IN_DIR_ALLOWED_DIRS`: add mounted filesystems here that
         shall be accessible by CaosDB
     * Maybe set another `SESSION_TIMEOUT_MS`.
-    * See also [CONFIGURATION.rst](src/doc/administration/configuration.rst)
-6. Copy `conf/core/usersources.ini.template` to `conf/ext/usersources.ini`.
+     * See also [CONFIGURATION.rst](src/doc/administration/configuration.rst)
+7. Copy `conf/core/usersources.ini.template` to `conf/ext/usersources.ini`.
     * You can skip this if you do not want to use an external authentication. 
       Local users (CaosDB realm) are always available.
     * Define the users/groups who you want to include/exclude.
@@ -147,7 +130,7 @@ server:
       Especially that there are no `properties` (aka `keys`) without a
       `value`. An emtpy value can be represented by `""`. Comments are
       everything from `#` or `;` to the end of the line.
-7. Possibly install the PAM caller in `misc/pam_authentication/` if you have 
+8. Possibly install the PAM caller in `misc/pam_authentication/` if you have
    not do so already. See above.
    
 Done!
diff --git a/RELEASE_GUIDELINES.md b/RELEASE_GUIDELINES.md
index 5c1fd68d462212e9b81dbc803fc590006df36a3d..4b9185323d3b6fdafbd049e7c68273c909de5a05 100644
--- a/RELEASE_GUIDELINES.md
+++ b/RELEASE_GUIDELINES.md
@@ -31,3 +31,5 @@ guidelines of the CaosDB Project
 
 8. Update the version property in [pom.xml](./pom.xml) for the next
    developlement round (with a `-SNAPSHOT` suffix).
+
+9. Add a gitlab release in the respective repository.
diff --git a/caosdb-proto b/caosdb-proto
new file mode 160000
index 0000000000000000000000000000000000000000..c439aa40a30c9214db315018b84fa50112c9251e
--- /dev/null
+++ b/caosdb-proto
@@ -0,0 +1 @@
+Subproject commit c439aa40a30c9214db315018b84fa50112c9251e
diff --git a/caosdb-webui b/caosdb-webui
index 8e5799db53b853b06a51a3bd250daeb9c779eee5..e6788d0380bdaf02d43498347616e6f3c4195663 160000
--- a/caosdb-webui
+++ b/caosdb-webui
@@ -1 +1 @@
-Subproject commit 8e5799db53b853b06a51a3bd250daeb9c779eee5
+Subproject commit e6788d0380bdaf02d43498347616e6f3c4195663
diff --git a/conf/core/log4j2-default.properties b/conf/core/log4j2-default.properties
index 974ce34df0f8290d763bf6a993554dab0ec7eeb2..5983f8e33d51729c70cd3d685fe299aa13e8f27c 100644
--- a/conf/core/log4j2-default.properties
+++ b/conf/core/log4j2-default.properties
@@ -2,7 +2,7 @@
 # https://logging.apache.org/log4j/2.x/ for more information.
 
 name = base_configuration
-status = TRACE
+status = DEBUG
 verbose = true
 
 property.LOG_DIR = log
diff --git a/conf/core/server.conf b/conf/core/server.conf
index ac6efe9778a34c2ce1337a2eba6889ca521a5141..246be9aa9285e4434803d3a71d18c24412922bf4 100644
--- a/conf/core/server.conf
+++ b/conf/core/server.conf
@@ -95,6 +95,11 @@ INITIAL_CONNECTIONS=1
 MAX_CONNECTIONS=10
 
 
+# HTTPS port of the grpc end-point
+GRPC_SERVER_PORT_HTTPS=8443
+# HTTP port of the grpc end-point
+GRPC_SERVER_PORT_HTTP=
+
 # --------------------------------------------------
 # HTTPS options
 # --------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 2b11d4fd07fa82ffbb67460eea878b6451bfeb04..733a5b4ca3c741fef0d611885327545eee8c0129 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,12 +25,20 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>org.caosdb</groupId>
   <artifactId>caosdb-server</artifactId>
-  <version>0.5.1-SNAPSHOT</version>
+  <version>0.8.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>CaosDB Server</name>
+  <scm>
+    <connection>scm:git:https://gitlab.indiscale.com/caosdb/src/caosdb-server.git</connection>
+  </scm>
   <properties>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <project.build.testSourceDirectory>src/test/java</project.build.testSourceDirectory>
+    <protobuf.version>3.14.0</protobuf.version>
+    <grpc.version>1.42.1</grpc.version>
+    <netty-tcnative.version>2.0.34.Final</netty-tcnative.version>
+    <restlet.version>2.4.3</restlet.version>
+    <log4j.version>2.17.2</log4j.version>
   </properties>
   <repositories>
     <repository>
@@ -40,8 +48,8 @@
     </repository>
     <repository>
       <id>maven-restlet</id>
-      <name>Public online Restlet repository</name>
-      <url>https://maven.restlet.com</url>
+      <name>Restlet repository</name>
+      <url>https://maven.restlet.talend.com</url>
     </repository>
     <repository>
       <id>local-maven-repo</id>
@@ -57,7 +65,7 @@
     <dependency>
       <groupId>org.quartz-scheduler</groupId>
       <artifactId>quartz</artifactId>
-      <version>2.3.2</version> 
+      <version>2.3.2</version>
     </dependency>
     <dependency>
       <groupId>com.fasterxml.jackson.dataformat</groupId>
@@ -67,7 +75,7 @@
     <dependency>
       <groupId>org.apache.shiro</groupId>
       <artifactId>shiro-core</artifactId>
-      <version>1.5.3</version>
+      <version>1.8.0</version>
     </dependency>
     <dependency>
       <groupId>junit</groupId>
@@ -90,17 +98,17 @@
     <dependency>
       <groupId>org.restlet.jse</groupId>
       <artifactId>org.restlet</artifactId>
-      <version>2.3.12</version>
+      <version>${restlet.version}</version>
     </dependency>
     <dependency>
       <groupId>org.restlet.jse</groupId>
       <artifactId>org.restlet.ext.fileupload</artifactId>
-      <version>2.3.12</version>
+      <version>${restlet.version}</version>
     </dependency>
     <dependency>
       <groupId>mysql</groupId>
       <artifactId>mysql-connector-java</artifactId>
-       <version>8.0.19</version>
+      <version>8.0.19</version>
     </dependency>
     <dependency>
       <groupId>org.xerial</groupId>
@@ -125,7 +133,7 @@
     <dependency>
       <groupId>org.restlet.jse</groupId>
       <artifactId>org.restlet.ext.jetty</artifactId>
-      <version>2.3.12</version>
+      <version>${restlet.version}</version>
     </dependency>
     <dependency>
       <groupId>org.apache.commons</groupId>
@@ -160,30 +168,75 @@
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-slf4j-impl</artifactId>
-      <version>2.11.1</version>
+      <version>${log4j.version}</version>
     </dependency>
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
-      <version>1.7.21</version>
+      <version>1.7.32</version>
     </dependency>
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-api</artifactId>
-      <version>2.11.1</version>
+      <version>${log4j.version}</version>
     </dependency>
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-core</artifactId>
-      <version>2.11.1</version>
+      <version>${log4j.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.grpc</groupId>
+      <artifactId>grpc-netty</artifactId>
+      <version>${grpc.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-tcnative</artifactId>
+      <version>${netty-tcnative.version}</version>
+      <classifier>${os.detected.classifier}</classifier>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>io.grpc</groupId>
+      <artifactId>grpc-protobuf</artifactId>
+      <version>${grpc.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>io.grpc</groupId>
+      <artifactId>grpc-stub</artifactId>
+      <version>${grpc.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>javax.annotation</groupId>
+      <artifactId>javax.annotation-api</artifactId>
+      <version>1.3.2</version>
+    </dependency>
+    <dependency>
+      <groupId>com.google.protobuf</groupId>
+      <artifactId>protobuf-java</artifactId>
+      <version>${protobuf.version}</version>
     </dependency>
   </dependencies>
   <build>
+    <resources>
+      <resource>
+        <directory>${basedir}/src/main/</directory>
+        <includes><include>**/*.properties</include></includes>
+      </resource>
+    </resources>
     <sourceDirectory>${basedir}/src/main/java</sourceDirectory>
     <scriptSourceDirectory>${basedir}/src/main/scripts</scriptSourceDirectory>
     <testSourceDirectory>${basedir}/src/test/java</testSourceDirectory>
     <outputDirectory>${basedir}/target/classes</outputDirectory>
     <testOutputDirectory>${basedir}/target/test-classes</testOutputDirectory>
+    <extensions>
+      <extension>
+        <groupId>kr.motd.maven</groupId>
+        <artifactId>os-maven-plugin</artifactId>
+        <version>1.7.0</version>
+      </extension>
+    </extensions>
     <plugins>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
@@ -256,6 +309,28 @@
           <forkCount>0.5C</forkCount>
         </configuration>
       </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>buildnumber-maven-plugin</artifactId>
+        <version>1.4</version>
+        <executions>
+          <execution>
+            <phase>generate-resources</phase>
+            <goals>
+              <goal>create-metadata</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <addOutputDirectoryToResources>true</addOutputDirectoryToResources>
+          <applicationPropertyName>project.name</applicationPropertyName>
+          <revisionPropertyName>project.revision</revisionPropertyName>
+          <versionPropertyName>project.version</versionPropertyName>
+          <timestampPropertyName>build.timestamp</timestampPropertyName>
+          <outputDirectory>${basedir}/target/classes/org/caosdb/server/</outputDirectory>
+          <outputName>build.properties</outputName>
+        </configuration>
+      </plugin>
       <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>exec-maven-plugin</artifactId>
@@ -320,7 +395,40 @@
           <excludeArtifactIds>easy-units</excludeArtifactIds>
         </configuration>
       </plugin>
+      <plugin>
+        <groupId>kr.motd.maven</groupId>
+        <artifactId>os-maven-plugin</artifactId>
+        <version>1.7.0</version>
+        <executions>
+          <execution>
+            <phase>initialize</phase>
+            <goals>
+              <goal>detect</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <!-- code generation protobuf/grpc -->
+      <plugin>
+        <groupId>org.xolstice.maven.plugins</groupId>
+        <artifactId>protobuf-maven-plugin</artifactId>
+        <version>0.6.1</version>
+        <configuration>
+          <protoSourceRoot>${basedir}/caosdb-proto/proto/</protoSourceRoot>
+          <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
+          <pluginId>grpc-java</pluginId>
+          <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>compile</goal>
+              <goal>compile-custom</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
     </plugins>
   </build>
-  <url>bmp.ds.mpg.de</url>
+  <url>caosdb.org</url>
 </project>
diff --git a/src/doc/CHANGELOG.md b/src/doc/CHANGELOG.md
new file mode 120000
index 0000000000000000000000000000000000000000..699cc9e7b7c5bf63c3549abe36e3eecf8efab625
--- /dev/null
+++ b/src/doc/CHANGELOG.md
@@ -0,0 +1 @@
+../../CHANGELOG.md
\ No newline at end of file
diff --git a/src/doc/DEPENDENCIES.md b/src/doc/DEPENDENCIES.md
new file mode 120000
index 0000000000000000000000000000000000000000..28771e4dc45403b9c9baa795f04584e5b0a283ec
--- /dev/null
+++ b/src/doc/DEPENDENCIES.md
@@ -0,0 +1 @@
+../../DEPENDENCIES.md
\ No newline at end of file
diff --git a/src/doc/conf.py b/src/doc/conf.py
index 604797f15d0585f3cc890f2d40e350caa960ed24..079eec42b21b3532e30c21ceaf8afe5a8ea7f35b 100644
--- a/src/doc/conf.py
+++ b/src/doc/conf.py
@@ -25,9 +25,9 @@ copyright = '2020, IndiScale GmbH'
 author = 'Daniel Hornung'
 
 # The short X.Y version
-version = '0.5'
+version = '0.8.0'
 # The full version, including alpha/beta/rc tags
-release = '0.5.1-SNAPSHOT'
+release = '0.8.0-SNAPSHOT'
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/src/doc/index.rst b/src/doc/index.rst
index 21fd891c083e7932d8b342d93992b2d02158364e..b5ce9f3235277b613b357dca5dfc3334d80aeb3f 100644
--- a/src/doc/index.rst
+++ b/src/doc/index.rst
@@ -10,9 +10,12 @@ Welcome to caosdb-server's documentation!
 
    Getting started <README_SETUP>
    Concepts <concepts>
+   tutorials
    Query Language <CaosDB-Query-Language>
    administration
    Development <development/devel>
+   Dependencies <DEPENDENCIES>
+   Changelog <CHANGELOG>
    specification/index.rst
    Glossary
    API documentation<_apidoc/packages>
diff --git a/src/doc/tutorials.rst b/src/doc/tutorials.rst
new file mode 100644
index 0000000000000000000000000000000000000000..27aacfb3eb9a4f04ab261b12f904e652c4daf3d3
--- /dev/null
+++ b/src/doc/tutorials.rst
@@ -0,0 +1,10 @@
+Tutorials
+==============
+
+.. toctree::
+   :maxdepth: 1
+   :glob:
+
+   tutorials/*
+
+
diff --git a/src/doc/tutorials/setup_state_model.py b/src/doc/tutorials/setup_state_model.py
new file mode 100755
index 0000000000000000000000000000000000000000..0a1a7daa3b14d7c3e7a5b8eac093857bddfad330
--- /dev/null
+++ b/src/doc/tutorials/setup_state_model.py
@@ -0,0 +1,379 @@
+#!/usr/bin/env python3
+# encoding: utf-8
+#
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2021 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2021 Henrik tom Wörden <h.tomwoerden@indiscale.com>
+# Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+"""
+This is a utility script to setup a publication process in LinkAhead using
+states.
+
+If you start from scratch you should perform the following actions in that
+order:
+
+1. setup_roles
+2. setup_state_data_model
+4. setup_model_publication_cycle
+"""
+from argparse import ArgumentParser, RawDescriptionHelpFormatter
+
+import caosdb as db
+from caosdb.common.administration import generate_password
+
+
+def teardown(args):
+    """fully clears the database"""
+
+    if "yes" != input(
+        "Are you really sure that you want to delete ALL "
+        "ENTITIES in LinkAhead? [yes/No]"
+    ):
+
+        print("Nothing done.")
+
+        return
+    d = db.execute_query("FIND ENTITY WITH ID > 99")
+
+    if len(d) > 0:
+        d.delete(flags={"forceFinalState": "true"})
+
+
+def soft_teardown(args):
+    """ allows to remove state data only """
+    recs = db.execute_query("FIND Entity WITH State")
+
+    for rec in recs:
+        rec.state = None
+    recs.update(flags={"forceFinalState": "true"})
+    db.execute_query("FIND StateModel").delete()
+    db.execute_query("FIND Transition").delete()
+    db.execute_query("FIND State").delete()
+    db.execute_query(
+        "FIND Property WITH name=from or name=to or name=initial or name=final or name=color").delete()
+
+
+def setup_user(args):
+    """Creates a user with given username and adds the given role.
+
+    If the user exists, it is deleted first. A random password is generated
+    and printed in clear text in the console output.
+
+    """
+
+    username, role = args.username, args.role
+    try:
+        db.administration._delete_user(name=username)
+    except Exception:
+        pass
+
+    password = generate_password(10)
+    print("new password for {}:\n{}".format(username, password))
+    db.administration._insert_user(
+        name=username, password=password, status="ACTIVE")
+    db.administration._set_roles(username=username, roles=[role])
+
+
+def remove_user(args):
+    """deletes the given user"""
+    db.administration._delete_user(name=args.username)
+
+
+def setup_role_permissions():
+    """
+    Adds the appropriate permissions to the 'normal' and 'publisher' role.
+
+    The permissions are such that they suit the publication life cycle.
+    """
+    db.administration._set_permissions(
+        role="normal",
+        permission_rules=[
+            db.administration.PermissionRule("Grant", "TRANSACTION:*"),
+            db.administration.PermissionRule(
+                "Grant", "ACM:USER:UPDATE_PASSWORD:?REALM?:?USERNAME?"
+            ),
+            db.administration.PermissionRule("Grant", "STATE:TRANSITION:Edit"),
+            db.administration.PermissionRule("Grant", "UPDATE:PROPERTY:ADD"),
+            db.administration.PermissionRule(
+                "Grant", "UPDATE:PROPERTY:REMOVE"),
+            db.administration.PermissionRule(
+                "Grant", "STATE:TRANSITION:Start Review"),
+            db.administration.PermissionRule(
+                "Grant", "STATE:ASSIGN:Publish Life-cycle"
+            ),
+        ],
+    )
+
+    db.administration._set_permissions(
+        role="publisher",
+        permission_rules=[
+            db.administration.PermissionRule(
+                "Grant", "ACM:USER:UPDATE_PASSWORD:?REALM?:?USERNAME?"
+            ),
+            db.administration.PermissionRule("Grant", "TRANSACTION:*"),
+            db.administration.PermissionRule("Grant", "UPDATE:PROPERTY:ADD"),
+            db.administration.PermissionRule(
+                "Grant", "UPDATE:PROPERTY:REMOVE"),
+            db.administration.PermissionRule("Grant", "STATE:*"),
+        ],
+    )
+
+
+def setup_roles(args):
+    """Creates 'publisher' and 'normla' roles and assigns appropriate
+    permissions
+
+    If those roles exist they are deleted first.
+    """
+
+    for role in ["publisher", "normal"]:
+        try:
+            db.administration._delete_role(name=role)
+        except Exception:
+            print("Could not delete role {}".format(role))
+
+    for role in ["publisher", "normal"]:
+        db.administration._insert_role(name=role, description="")
+
+    setup_role_permissions()
+
+
+def setup_state_data_model(args):
+    """Creates the data model for using states
+
+    RecordTypes: State, StateModel, Transition
+    Properties: from, to, initial, final, color
+    """
+    cont = db.Container().extend(
+        [
+            db.RecordType("State"),
+            db.RecordType("StateModel"),
+            db.RecordType("Transition"),
+            db.Property(name="from", datatype="State"),
+            db.Property(name="to", datatype="State"),
+            db.Property(name="initial", datatype="State"),
+            db.Property(name="final", datatype="State"),
+            db.Property(name="color", datatype=db.TEXT),
+        ]
+    )
+    cont.insert()
+
+
+def setup_model_publication_cycle(args):
+    """Creates States and Transitions for the Publication Life Cycle"""
+    unpublished_acl = db.ACL()
+    unpublished_acl.grant(role="publisher", permission="*")
+    unpublished_acl.grant(role="normal", permission="UPDATE:*")
+    unpublished_acl.grant(role="normal", permission="RETRIEVE:ENTITY")
+    unpublished_acl = db.State.create_state_acl(unpublished_acl)
+
+    unpublished_state = (
+        db.Record(
+            "Unpublished",
+            description="Unpublished entries are only visible to the team "
+            "and may be edited by any team member.",
+        )
+        .add_parent("State")
+        .add_property("color", "#5bc0de")
+    )
+    unpublished_state.acl = unpublished_acl
+    unpublished_state.insert()
+
+    review_acl = db.ACL()
+    review_acl.grant(role="publisher", permission="*")
+    review_acl.grant(role="normal", permission="RETRIEVE:ENTITY")
+
+    review_state = (
+        db.Record(
+            "Under Review",
+            description="Entries under review are not publicly available yet, "
+            "but they can only be edited by the members of the publisher "
+            "group.",
+        )
+        .add_parent("State")
+        .add_property("color", "#FFCC33")
+    )
+    review_state.acl = db.State.create_state_acl(review_acl)
+    review_state.insert()
+
+    published_acl = db.ACL()
+    published_acl.grant(role="guest", permission="RETRIEVE:ENTITY")
+
+    published_state = (
+        db.Record(
+            "Published",
+            description="Published entries are publicly available and "
+            "cannot be edited unless they are unpublished again.",
+        )
+        .add_parent("State")
+        .add_property("color", "#333333")
+    )
+    published_state.acl = db.State.create_state_acl(published_acl)
+    published_state.insert()
+
+    # 1->2
+    (
+        db.Record(
+            "Start Review",
+            description="This transitions denies the permissions to edit an "
+            "entry for anyone but the members of the publisher group. "
+            "However, the entry is not yet publicly available.",
+        )
+        .add_parent("Transition")
+        .add_property("from", "unpublished")
+        .add_property("to", "under review")
+        .add_property("color", "#FFCC33")
+        .insert()
+    )
+
+    # 2->3
+    (
+        db.Record(
+            "Publish",
+            description="Published entries are visible for the public and "
+            "cannot be changed unless they are unpublished again. Only members"
+            " of the publisher group can publish or unpublish entries.",
+        )
+        .add_parent("Transition")
+        .add_property("from", "under review")
+        .add_property("to", "published")
+        .add_property("color", "red")
+        .insert()
+    )
+
+    # 3->1
+    (
+        db.Record(
+            "Unpublish",
+            description="Unpublish this entry to hide it from "
+            "the public. Unpublished entries can be edited by any team "
+            "member.",
+        )
+        .add_parent("Transition")
+        .add_property("from", "published")
+        .add_property("to", "unpublished")
+        .insert()
+    )
+
+    # 2->1
+    (
+        db.Record(
+            "Reject",
+            description="Reject the publishing of this entity.  Afterwards, "
+            "the entity is editable for any team member again.",
+        )
+        .add_parent("Transition")
+        .add_property("from", "under review")
+        .add_property("to", "unpublished")
+        .insert()
+    )
+
+    # 1->1
+    (
+        db.Record(
+            "Edit",
+            description="Edit this entity. The changes are not publicly "
+            "available until this entity will have been reviewed and "
+            "published.",
+        )
+        .add_parent(
+            "Transition",
+        )
+        .add_property("from", "unpublished")
+        .add_property("to", "unpublished")
+        .insert()
+    )
+
+    (
+        db.Record(
+            "Publish Life-cycle",
+            description="The publish life-cycle is a quality assurance tool. "
+            "Database entries can be edited without being publicly available "
+            "until the changes have been reviewed and explicitely published by"
+            " an eligible user.",
+        )
+        .add_parent("StateModel")
+        .add_property(
+            "Transition",
+            datatype=db.LIST("Transition"),
+            value=[
+                "Edit",
+                "Start Review",
+                "Reject",
+                "Publish",
+                "Unpublish",
+            ],
+        )
+        .add_property("initial", "Unpublished")
+        .add_property("final", "Unpublished")
+        .insert()
+    )
+
+
+def parse_args():
+    parser = ArgumentParser(
+        description=__doc__, formatter_class=RawDescriptionHelpFormatter
+    )
+    subparsers = parser.add_subparsers(
+        title="action",
+        metavar="ACTION",
+        description=(
+            "You can perform the following actions. "
+            "Print the detailed help for each command with "
+            "#> setup_state_model ACTION -h"
+        ),
+    )
+
+    subparser = subparsers.add_parser(
+        "setup_state_data_model", help=setup_state_data_model.__doc__
+    )
+    subparser.set_defaults(call=setup_state_data_model)
+
+    subparser = subparsers.add_parser(
+        "setup_model_publication_cycle", help=setup_model_publication_cycle.__doc__
+    )
+    subparser.set_defaults(call=setup_model_publication_cycle)
+
+    subparser = subparsers.add_parser("setup_roles", help=setup_roles.__doc__)
+    subparser.set_defaults(call=setup_roles)
+
+    subparser = subparsers.add_parser("remove_user", help=remove_user.__doc__)
+    subparser.set_defaults(call=remove_user)
+    subparser.add_argument("username")
+
+    subparser = subparsers.add_parser("setup_user", help=setup_user.__doc__)
+    subparser.set_defaults(call=setup_user)
+    subparser.add_argument("username")
+    subparser.add_argument("role")
+
+    subparser = subparsers.add_parser(
+        "teardown", help="Removes ALL ENTITIES from LinkAhead!"
+    )
+    subparser.set_defaults(call=teardown)
+
+    subparser = subparsers.add_parser(
+        "soft_teardown", help=soft_teardown.__doc__
+    )
+    subparser.set_defaults(call=soft_teardown)
+
+    return parser.parse_args()
+
+
+if __name__ == "__main__":
+    args = parse_args()
+    args.call(args)
diff --git a/src/doc/tutorials/statemachine.rst b/src/doc/tutorials/statemachine.rst
new file mode 100644
index 0000000000000000000000000000000000000000..317620423e6221ccaf29297fc1f71f0c008f78a0
--- /dev/null
+++ b/src/doc/tutorials/statemachine.rst
@@ -0,0 +1,34 @@
+
+State Machine
+=============
+
+Prerequisites
+-------------
+
+In order to use the state machine functionality you have to set the
+corresponding server setting: ``EXT_ENTITY_STATE=ENABLED``.
+
+Also, a few RecordTypes and Properties are required. You can use the
+script `setup_state_model.py <https://gitlab.com/caosdb/caosdb-server/-/blob/dev/src/doc/tutorials/setup_state_model.py>`_
+to create those or you may have a look at it to see what is needed (``setup_state_data_model`` function).
+
+Defining the State Machine
+--------------------------
+
+Now you are setup to create your own state machine. You can define States and Transitions
+and bundle it all to a StateModel. The above mentioned ``setup_state_model.py`` script defines
+a publication cycle with the state "Unpublished", "UnderReview" and "Published".
+Again, the ``setup_state_model.py`` script provides orientation on how this
+can be setup (``setup_model_publication_cycle`` function).
+
+Note, that you can provide ACL to the state definition which will be applied to an entity once
+the state is reached. This is for example useful to change the visibility depending on a state change.
+
+If you assign a state to a RecordType, this state will be the initial state
+of Records that have that parent. For example by executing:
+
+.. code-block:: Python
+
+    rt = db.RecordType("Article").retrieve()
+    rt.state = db.State(name="UnPublished", model="Publish Life-cycle")``
+    rt.update()
diff --git a/src/main/java/org/caosdb/datetime/DateTimeFactory2.java b/src/main/java/org/caosdb/datetime/DateTimeFactory2.java
index 103359c7b4ff6f995996866e2283bab47562699c..14088ea5d1033771d8835352b61e5bf430c4ff5f 100644
--- a/src/main/java/org/caosdb/datetime/DateTimeFactory2.java
+++ b/src/main/java/org/caosdb/datetime/DateTimeFactory2.java
@@ -30,6 +30,11 @@ import org.antlr.v4.runtime.CommonTokenStream;
 import org.caosdb.server.query.CQLParsingErrorListener;
 import org.caosdb.server.query.CQLParsingErrorListener.ParsingError;
 
+/**
+ * Factory which parses string into CaosDB's DATETIME values.
+ *
+ * @author tf
+ */
 public class DateTimeFactory2 implements DateTimeFactoryInterface {
 
   private Long millis = null;
@@ -51,6 +56,14 @@ public class DateTimeFactory2 implements DateTimeFactoryInterface {
   private Integer nanosecond = null;
   private TimeZone timeZone = TimeZone.getDefault();
 
+  public DateTimeFactory2(TimeZone timeZone) {
+    this.timeZone = timeZone;
+  }
+
+  public DateTimeFactory2() {
+    this(TimeZone.getDefault());
+  }
+
   @Override
   public void setDate(final String string) {
     this.date = Integer.valueOf(string);
@@ -220,6 +233,16 @@ public class DateTimeFactory2 implements DateTimeFactoryInterface {
   }
 
   public static DateTimeInterface valueOf(final String str) {
+    return valueOf(str, TimeZone.getDefault());
+  }
+
+  public static DateTimeInterface valueOf(final String str, TimeZone timeZone) {
+    final DateTimeFactory2 dtf = new DateTimeFactory2(timeZone);
+
+    return dtf.parse(str);
+  }
+
+  public DateTimeInterface parse(String str) {
     final DateTimeLexer lexer = new DateTimeLexer(CharStreams.fromString(str));
     final CommonTokenStream tokens = new CommonTokenStream(lexer);
 
@@ -230,9 +253,7 @@ public class DateTimeFactory2 implements DateTimeFactoryInterface {
     final CQLParsingErrorListener el = new CQLParsingErrorListener(DateTimeLexer.UNKNOWN_CHAR);
     parser.addErrorListener(el);
 
-    final DateTimeFactory2 dtf = new DateTimeFactory2();
-
-    parser.datetime(dtf);
+    parser.datetime(this);
 
     if (el.hasErrors()) {
       final StringBuilder sb = new StringBuilder();
@@ -243,7 +264,7 @@ public class DateTimeFactory2 implements DateTimeFactoryInterface {
           "Parsing DateTime finished with errors. \n" + sb.toString());
     }
 
-    return dtf.getDateTime();
+    return getDateTime();
   }
 
   @Override
diff --git a/src/main/java/org/caosdb/datetime/FragmentDateTime.java b/src/main/java/org/caosdb/datetime/FragmentDateTime.java
index 52faf5f2040de56c05bb7ce4b5aee130fadf67c3..38c16a988a0103a121767b99078c0ca41c6263d5 100644
--- a/src/main/java/org/caosdb/datetime/FragmentDateTime.java
+++ b/src/main/java/org/caosdb/datetime/FragmentDateTime.java
@@ -20,9 +20,13 @@
  *
  * ** end header
  */
+
+/** @review Daniel Hornung 2022-03-04 */
 package org.caosdb.datetime;
 
+import java.util.Objects;
 import java.util.TimeZone;
+import org.caosdb.server.datatype.Value;
 
 public abstract class FragmentDateTime implements DateTimeInterface {
 
@@ -53,4 +57,13 @@ public abstract class FragmentDateTime implements DateTimeInterface {
     this.nanosecond = nanosecond;
     this.timeZone = tz;
   }
+
+  @Override
+  public boolean equals(Value val) {
+    if (val instanceof FragmentDateTime) {
+      FragmentDateTime that = (FragmentDateTime) val;
+      return Objects.equals(that.toDatabaseString(), this.toDatabaseString());
+    }
+    return false;
+  }
 }
diff --git a/src/main/java/org/caosdb/datetime/UTCDateTime.java b/src/main/java/org/caosdb/datetime/UTCDateTime.java
index 0ecb3ac60dfe796692c344e590f3935c36fceac3..1dd86cc975d4b92066f2cf44fff51493ade5babe 100644
--- a/src/main/java/org/caosdb/datetime/UTCDateTime.java
+++ b/src/main/java/org/caosdb/datetime/UTCDateTime.java
@@ -20,13 +20,17 @@
  *
  * ** end header
  */
+
+/** @review Daniel Hornung 2022-03-04 */
 package org.caosdb.datetime;
 
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.GregorianCalendar;
+import java.util.Objects;
 import java.util.TimeZone;
 import org.caosdb.server.datatype.AbstractDatatype.Table;
+import org.caosdb.server.datatype.Value;
 import org.jdom2.Element;
 
 public class UTCDateTime implements Interval {
@@ -360,4 +364,13 @@ public class UTCDateTime implements Interval {
   public boolean hasNanoseconds() {
     return this.nanoseconds != null;
   }
+
+  @Override
+  public boolean equals(Value val) {
+    if (val instanceof UTCDateTime) {
+      UTCDateTime that = (UTCDateTime) val;
+      return Objects.equals(that.toDatabaseString(), this.toDatabaseString());
+    }
+    return false;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/CaosDBServer.java b/src/main/java/org/caosdb/server/CaosDBServer.java
index bd053276e08fda4a898950e476814999f9bf4156..fab783b765cb8af3c5ffdd1dda8ab2eb5dd0086b 100644
--- a/src/main/java/org/caosdb/server/CaosDBServer.java
+++ b/src/main/java/org/caosdb/server/CaosDBServer.java
@@ -1,21 +1,23 @@
 /*
- * ** header v3.0 This file is a part of the CaosDB Project.
+ * 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) 2018 Research Group Biomedical Physics,
+ * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2019-2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2019-2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
- * This program is free software: you can redistribute it and/or modify it under the terms of the
- * GNU Affero General Public License as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
- * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Affero General Public License for more details.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
  *
- * You should have received a copy of the GNU Affero General Public License along with this program.
- * If not, see <https://www.gnu.org/licenses/>.
- *
- * ** end header
+ * 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/>.
  */
 package org.caosdb.server;
 
@@ -57,6 +59,9 @@ import org.caosdb.server.datatype.AbstractDatatype;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.Role;
 import org.caosdb.server.entity.container.Container;
+import org.caosdb.server.grpc.GRPCServer;
+import org.caosdb.server.jobs.core.AccessControl;
+import org.caosdb.server.jobs.core.CheckStateTransition;
 import org.caosdb.server.logging.RequestErrorLogMessage;
 import org.caosdb.server.resource.AuthenticationResource;
 import org.caosdb.server.resource.DefaultResource;
@@ -78,6 +83,7 @@ import org.caosdb.server.resource.Webinterface;
 import org.caosdb.server.resource.WebinterfaceBuildNumber;
 import org.caosdb.server.resource.transaction.EntityNamesResource;
 import org.caosdb.server.resource.transaction.EntityResource;
+import org.caosdb.server.scripting.ScriptingPermissions;
 import org.caosdb.server.transaction.ChecksumUpdater;
 import org.caosdb.server.utils.FileUtils;
 import org.caosdb.server.utils.Initialization;
@@ -150,7 +156,7 @@ public class CaosDBServer extends Application {
       initBackend();
       initWebServer();
       initShutDownHook();
-    } catch (Exception e1) {
+    } catch (final Exception e1) {
       logger.error("Could not start the server.", e1);
       System.exit(1);
     }
@@ -203,7 +209,7 @@ public class CaosDBServer extends Application {
    * @throws IOException
    */
   public static void initTimeZone() throws InterruptedException, IOException {
-    String serverProperty = getServerProperty(ServerProperties.KEY_TIMEZONE);
+    final String serverProperty = getServerProperty(ServerProperties.KEY_TIMEZONE);
     if (serverProperty != null && !serverProperty.isEmpty()) {
       logger.info(
           "SET TIMEZONE = "
@@ -216,7 +222,7 @@ public class CaosDBServer extends Application {
       return;
     }
 
-    String prop = System.getProperty("user.timezone");
+    final String prop = System.getProperty("user.timezone");
     if (prop != null && !prop.isEmpty()) {
       logger.info("SET TIMEZONE = " + prop + " from JVM property `user.timezone`.");
       TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of(prop)));
@@ -224,7 +230,7 @@ public class CaosDBServer extends Application {
       return;
     }
 
-    String envVar = System.getenv("TZ");
+    final String envVar = System.getenv("TZ");
     if (envVar != null && !envVar.isEmpty()) {
       logger.info("SET TIMEZONE = " + envVar + " from evironment variable `TZ`.");
       TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of(envVar)));
@@ -232,7 +238,7 @@ public class CaosDBServer extends Application {
       return;
     }
 
-    String fromDate = getTimeZoneFromDate();
+    final String fromDate = getTimeZoneFromDate();
     if (fromDate != null && fromDate.isEmpty()) {
       logger.info("SET TIMEZONE = " + fromDate + " from `date +%Z`.");
       TimeZone.setDefault(TimeZone.getTimeZone(ZoneId.of(fromDate)));
@@ -277,6 +283,16 @@ public class CaosDBServer extends Application {
     // init Shiro (user authentication/authorization and session management)
     final Ini config = getShiroConfig();
     initShiro(config);
+
+    // Init ACMPermissions.ALL -  the whole point is to fill all these
+    // permissions into ACMPermissions.ALL for retrieval by clients. If we don't
+    // do this, every work, but the list of known permissions grows over time
+    // (as soon as these classes are used for the first time)
+    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());
   }
 
   public static Ini getShiroConfig() {
@@ -298,8 +314,8 @@ public class CaosDBServer extends Application {
     return config;
   }
 
-  public static void initShiro(Ini config) {
-    BasicIniEnvironment env = new BasicIniEnvironment(config);
+  public static void initShiro(final Ini config) {
+    final BasicIniEnvironment env = new BasicIniEnvironment(config);
     final SecurityManager securityManager = env.getSecurityManager();
     SecurityUtils.setSecurityManager(securityManager);
   }
@@ -323,6 +339,8 @@ public class CaosDBServer extends Application {
 
         // ChecksumUpdater
         ChecksumUpdater.start();
+
+        ThreadContext.remove();
       }
     } else {
       logger.info("NO BACKEND");
@@ -345,7 +363,7 @@ public class CaosDBServer extends Application {
     try {
       port_redirect_https =
           Integer.parseInt(getServerProperty(ServerProperties.KEY_REDIRECT_HTTP_TO_HTTPS_PORT));
-    } catch (NumberFormatException e) {
+    } catch (final NumberFormatException e) {
       port_redirect_https = port_https;
     }
     final int initialConnections =
@@ -364,6 +382,7 @@ public class CaosDBServer extends Application {
           initialConnections,
           maxTotalConnections);
     }
+    GRPCServer.startServer();
   }
 
   private static void initDatatypes(final Access access) throws Exception {
@@ -562,9 +581,9 @@ public class CaosDBServer extends Application {
               setSessionCookies(response);
 
             } finally {
-              // remove subject from this thread so that we can reuse the
-              // thread.
-              ThreadContext.unbindSubject();
+              // remove subject and all other session data from this thread so
+              // that we can reuse the thread.
+              ThreadContext.remove();
             }
           }
 
@@ -784,10 +803,12 @@ public class CaosDBServer extends Application {
     }
   }
 
+  /** Add a shutdown hook which runs after the Restlet Server has been shut down. */
   public static void addPostShutdownHook(final Thread t) {
     postShutdownHooks.add(t);
   }
 
+  /** Add a shutdown hook which runs before the Restlet Server is being shut down. */
   public static void addPreShutdownHook(final Runnable runnable) {
     preShutdownHooks.add(runnable);
   }
@@ -827,7 +848,7 @@ public class CaosDBServer extends Application {
    * @param key, the server property.
    * @param value, the new value.
    */
-  public static void setProperty(String key, String value) {
+  public static void setProperty(final String key, final String value) {
     SERVER_PROPERTIES.setProperty(key, value);
   }
 
@@ -835,7 +856,8 @@ public class CaosDBServer extends Application {
     return SERVER_PROPERTIES;
   }
 
-  public static void scheduleJob(JobDetail job, Trigger trigger) throws SchedulerException {
+  public static void scheduleJob(final JobDetail job, final Trigger trigger)
+      throws SchedulerException {
     SCHEDULER.scheduleJob(job, trigger);
   }
 }
@@ -865,7 +887,7 @@ class CaosDBComponent extends Component {
    */
   @Override
   public void handle(final Request request, final Response response) {
-    long t1 = System.currentTimeMillis();
+    final long t1 = System.currentTimeMillis();
     // The server request ID is just a long random number
     request.getAttributes().put("SRID", UUID.randomUUID().toString());
     response.setServerInfo(CaosDBServer.getServerInfo());
@@ -873,7 +895,7 @@ class CaosDBComponent extends Component {
     log(request, response, t1);
   }
 
-  private void log(final Request request, final Response response, long t1) {
+  private void log(final Request request, final Response response, final long t1) {
     if (response.getStatus() == Status.SERVER_ERROR_INTERNAL) {
       final Object object = request.getAttributes().get("THROWN");
       Throwable t = null;
diff --git a/src/main/java/org/caosdb/server/FileSystem.java b/src/main/java/org/caosdb/server/FileSystem.java
index 6bff612ca69acc24b699f8807b6969e343c60fb3..67587f3da7930dd2cbf3e76d3ed06f19508efef7 100644
--- a/src/main/java/org/caosdb/server/FileSystem.java
+++ b/src/main/java/org/caosdb/server/FileSystem.java
@@ -174,7 +174,7 @@ public class FileSystem {
       tmpFile.getParentFile().mkdirs();
       if (tmpFile.isDirectory()) {
         // TODO this should generate an error. This means that the
-        // tmpIdentifyers are inconsistent
+        // tmpIdentifiers are inconsistent
       }
       final OutputStream outputStream = new FileOutputStream(tmpFile);
       final MessageDigest md = MessageDigest.getInstance("SHA-512");
diff --git a/src/main/java/org/caosdb/server/ServerProperties.java b/src/main/java/org/caosdb/server/ServerProperties.java
index d1df2c66587fe6377fb8293cb8b16264480e8ef0..34899f7551ccc8ac99e6f2246eeb154c37171e53 100644
--- a/src/main/java/org/caosdb/server/ServerProperties.java
+++ b/src/main/java/org/caosdb/server/ServerProperties.java
@@ -1,11 +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) 2019 IndiScale GmbH
- * Copyright (C) 2019 Timm Fitschen (t.fitschen@indiscale.com)
+ * Copyright (C) 2019-2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2019-2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -19,8 +18,6 @@
  *
  * 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;
 
@@ -28,6 +25,7 @@ import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -66,6 +64,8 @@ public class ServerProperties extends Properties {
   public static final String KEY_SERVER_PORT_HTTPS = "SERVER_PORT_HTTPS";
   public static final String KEY_SERVER_PORT_HTTP = "SERVER_PORT_HTTP";
   public static final String KEY_REDIRECT_HTTP_TO_HTTPS_PORT = "REDIRECT_HTTP_TO_HTTPS_PORT";
+  public static final String KEY_GRPC_SERVER_PORT_HTTPS = "GRPC_SERVER_PORT_HTTPS";
+  public static final String KEY_GRPC_SERVER_PORT_HTTP = "GRPC_SERVER_PORT_HTTP";
 
   public static final String KEY_HTTPS_ENABLED_PROTOCOLS = "HTTPS_ENABLED_PROTOCOLS";
   public static final String KEY_HTTPS_DISABLED_PROTOCOLS = "HTTPS_DISABLED_PROTOCOLS";
@@ -136,6 +136,11 @@ public class ServerProperties extends Properties {
   public static final String KEY_AUTHTOKEN_CONFIG = "AUTHTOKEN_CONFIG";
   public static final String KEY_JOB_RULES_CONFIG = "JOB_RULES_CONFIG";
 
+  public static final String KEY_PROJECT_VERSION = "project.version";
+  public static final String KEY_PROJECT_NAME = "project.name";
+  public static final String KEY_PROJECT_REVISTION = "project.revision";
+  public static final String KEY_BUILD_TIMESTAMP = "build.timestamp";
+
   /**
    * Read the config files and initialize the server properties.
    *
@@ -145,6 +150,9 @@ public class ServerProperties extends Properties {
     final Properties serverProperties = new Properties();
     final String basepath = System.getProperty("user.dir");
 
+    final URL url = CaosDBServer.class.getResource("/build.properties");
+    serverProperties.load(url.openStream());
+
     // load standard config
     loadConfigFile(serverProperties, new File(basepath + "/conf/core/server.conf"));
 
diff --git a/src/main/java/org/caosdb/server/accessControl/ACMPermissions.java b/src/main/java/org/caosdb/server/accessControl/ACMPermissions.java
index 84844e892297e68d2ad22cbf5dad935612d1c4f4..854fc07f9ed93eda28e8aca99c1ea1983c73f4fe 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,257 @@
  *
  * 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.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 static final String PERMISSION_ACCESS_SERVER_PROPERTIES = "ACCESS_SERVER_PROPERTIES";
-  public static final String PERMISSION_RETRIEVE_SERVERLOGS = "SERVERLOGS:RETRIEVE";
+    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();
+
+  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);
+  }
+
+  @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 006400bf649619390d5eaa642642a73b6a94f337..92b37c0ec15b110670a55ea46b6efa1a418bb597 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/AuthenticationUtils.java b/src/main/java/org/caosdb/server/accessControl/AuthenticationUtils.java
index e6907f40b57b5ef44dc9c4772327ddf8920df173..dc40e72e44ffdd6fca21824c6f3dd036f0b6f9ed 100644
--- a/src/main/java/org/caosdb/server/accessControl/AuthenticationUtils.java
+++ b/src/main/java/org/caosdb/server/accessControl/AuthenticationUtils.java
@@ -29,6 +29,7 @@ import static org.caosdb.server.utils.Utils.URLDecodeWithUTF8;
 import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.LinkedList;
+import org.apache.shiro.authz.AuthorizationInfo;
 import org.apache.shiro.subject.Subject;
 import org.caosdb.server.CaosDBServer;
 import org.caosdb.server.ServerProperties;
@@ -199,4 +200,8 @@ public class AuthenticationUtils {
         .getRealm()
         .equals(OneTimeAuthenticationToken.REALM_NAME);
   }
+
+  public static AuthorizationInfo getAuthorizationInfo(Subject user) {
+    return new CaosDBAuthorizingRealm().doGetAuthorizationInfo(user.getPrincipals());
+  }
 }
diff --git a/src/main/java/org/caosdb/server/accessControl/CaosDBAuthorizingRealm.java b/src/main/java/org/caosdb/server/accessControl/CaosDBAuthorizingRealm.java
index e10a0b29b38b2e246bde8c89c43a8b82a9307b7e..52f8d3db87f9748a254f7a6f60fc00100a1bef0e 100644
--- a/src/main/java/org/caosdb/server/accessControl/CaosDBAuthorizingRealm.java
+++ b/src/main/java/org/caosdb/server/accessControl/CaosDBAuthorizingRealm.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,8 +19,8 @@
  * 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;
 
 import java.util.Collection;
@@ -63,7 +64,7 @@ public class CaosDBAuthorizingRealm extends AuthorizingRealm {
 
     // Find all roles which are associated with this principal in this realm.
     final Set<String> principalRoles =
-        UserSources.resolve((Principal) principals.getPrimaryPrincipal());
+        UserSources.resolveRoles((Principal) principals.getPrimaryPrincipal());
     if (principalRoles != null) {
       authzInfo.addRoles(principalRoles);
     }
diff --git a/src/main/java/org/caosdb/server/accessControl/CaosDBRolePermissionResolver.java b/src/main/java/org/caosdb/server/accessControl/CaosDBRolePermissionResolver.java
index 17637b010a1ddcd7ed88e3ba448e3fa68b894b43..5bc711e17b60309f851e55bf249a22beeec56d61 100644
--- a/src/main/java/org/caosdb/server/accessControl/CaosDBRolePermissionResolver.java
+++ b/src/main/java/org/caosdb/server/accessControl/CaosDBRolePermissionResolver.java
@@ -49,6 +49,7 @@ public class CaosDBRolePermissionResolver {
         throw new AuthenticationException(e);
       }
     }
+
     return new CaosPermission(rules);
   }
 }
diff --git a/src/main/java/org/caosdb/server/accessControl/Principal.java b/src/main/java/org/caosdb/server/accessControl/Principal.java
index fc96fb99670b51d0a128710722e2c10effed152a..7d4144557d89ddf90ec99edb190abb66ec0119aa 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/Role.java b/src/main/java/org/caosdb/server/accessControl/Role.java
index 1a34b49974804ae202ceaf4044ff1f620eaa5d12..d1b04918e76ecbe6c94db8b00c6bb50f1c9a351c 100644
--- a/src/main/java/org/caosdb/server/accessControl/Role.java
+++ b/src/main/java/org/caosdb/server/accessControl/Role.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,18 +19,23 @@
  * 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;
 
 import java.io.Serializable;
+import java.util.LinkedList;
+import org.caosdb.server.database.proto.ProtoUser;
+import org.caosdb.server.permissions.PermissionRule;
 import org.jdom2.Element;
 
 public class Role implements Serializable {
 
-  private static final long serialVersionUID = 8968219504349206982L;
+  private static final long serialVersionUID = -243913823L;
   public String name = null;
   public String description = null;
+  public LinkedList<PermissionRule> permission_rules = null;
+  public LinkedList<ProtoUser> users = null;
 
   public Element toElement() {
     final Element ret = new Element("Role");
diff --git a/src/main/java/org/caosdb/server/accessControl/SessionTokenRealm.java b/src/main/java/org/caosdb/server/accessControl/SessionTokenRealm.java
index d78ffb405a8470a005da54736e25fbd69340b7e5..270565be3dd8fc25408a1d7992638ef24bcfaa1c 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/SinglePermissionSubject.java b/src/main/java/org/caosdb/server/accessControl/SinglePermissionSubject.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c3a3479420c5f0642e0d42bf53b717ab0219571
--- /dev/null
+++ b/src/main/java/org/caosdb/server/accessControl/SinglePermissionSubject.java
@@ -0,0 +1,210 @@
+package org.caosdb.server.accessControl;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.Callable;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.authz.Permission;
+import org.apache.shiro.authz.permission.WildcardPermission;
+import org.apache.shiro.session.Session;
+import org.apache.shiro.subject.ExecutionException;
+import org.apache.shiro.subject.PrincipalCollection;
+import org.apache.shiro.subject.Subject;
+
+public class SinglePermissionSubject implements Subject {
+
+  private WildcardPermission permission;
+
+  public SinglePermissionSubject(String permission) {
+    this.permission = new WildcardPermission(permission);
+  }
+
+  @Override
+  public Object getPrincipal() {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public PrincipalCollection getPrincipals() {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public boolean isPermitted(String permission) {
+    return this.permission.implies(new WildcardPermission(permission));
+  }
+
+  @Override
+  public boolean isPermitted(Permission permission) {
+    return this.permission.implies(permission);
+  }
+
+  @Override
+  public boolean[] isPermitted(String... permissions) {
+    boolean[] result = new boolean[permissions.length];
+    for (int i = 0; i < result.length; i++) {
+      result[i] = this.permission.implies(new WildcardPermission(permissions[i]));
+    }
+    return result;
+  }
+
+  @Override
+  public boolean[] isPermitted(List<Permission> permissions) {
+    boolean[] result = new boolean[permissions.size()];
+    for (int i = 0; i < result.length; i++) {
+      result[i] = this.permission.implies(permissions.get(i));
+    }
+    return result;
+  }
+
+  @Override
+  public boolean isPermittedAll(String... permissions) {
+    boolean result = true;
+    for (int i = 0; i < permissions.length; i++) {
+      result &= this.permission.implies(new WildcardPermission(permissions[i]));
+    }
+    return result;
+  }
+
+  @Override
+  public boolean isPermittedAll(Collection<Permission> permissions) {
+    Boolean result = true;
+    Iterator<Permission> iterator = permissions.iterator();
+    while (iterator.hasNext()) {
+      result &= this.permission.implies(iterator.next());
+    }
+    return result;
+  }
+
+  @Override
+  public void checkPermission(String permission) throws AuthorizationException {
+    if (!isPermitted(permission)) {
+      throw new AuthenticationException("Not permitted: " + permission);
+    }
+  }
+
+  @Override
+  public void checkPermission(Permission permission) throws AuthorizationException {
+    if (!isPermitted(permission)) {
+      throw new AuthenticationException("Not permitted: " + permission.toString());
+    }
+  }
+
+  @Override
+  public void checkPermissions(String... permissions) throws AuthorizationException {
+    if (!isPermittedAll(permissions)) {
+      throw new AuthenticationException("Not permitted: " + permissions.toString());
+    }
+  }
+
+  @Override
+  public void checkPermissions(Collection<Permission> permissions) throws AuthorizationException {
+    if (!isPermittedAll(permissions)) {
+      throw new AuthenticationException("Not permitted: " + permissions.toString());
+    }
+  }
+
+  @Override
+  public boolean hasRole(String roleIdentifier) {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public boolean[] hasRoles(List<String> roleIdentifiers) {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public boolean hasAllRoles(Collection<String> roleIdentifiers) {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public void checkRole(String roleIdentifier) throws AuthorizationException {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public void checkRoles(Collection<String> roleIdentifiers) throws AuthorizationException {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public void checkRoles(String... roleIdentifiers) throws AuthorizationException {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public void login(AuthenticationToken token) throws AuthenticationException {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public boolean isAuthenticated() {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public boolean isRemembered() {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public Session getSession() {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public Session getSession(boolean create) {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public void logout() {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public <V> V execute(Callable<V> callable) throws ExecutionException {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public void execute(Runnable runnable) {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public <V> Callable<V> associateWith(Callable<V> callable) {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public Runnable associateWith(Runnable runnable) {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public void runAs(PrincipalCollection principals)
+      throws NullPointerException, IllegalStateException {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public boolean isRunAs() {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public PrincipalCollection getPreviousPrincipals() {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+
+  @Override
+  public PrincipalCollection releaseRunAs() {
+    throw new UnsupportedOperationException("This method should never be called");
+  }
+}
diff --git a/src/main/java/org/caosdb/server/accessControl/UserSources.java b/src/main/java/org/caosdb/server/accessControl/UserSources.java
index a7abd1405f3d566bb672befc86b853dcf2a17357..04c2ecab9fee23c62b042657c69911e2d69846a0 100644
--- a/src/main/java/org/caosdb/server/accessControl/UserSources.java
+++ b/src/main/java/org/caosdb/server/accessControl/UserSources.java
@@ -1,11 +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) 2020 Indiscale GmbH <info@indiscale.com>
- * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2020-2021 Indiscale GmbH <info@indiscale.com>
+ * Copyright (C) 2020-2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -20,22 +19,26 @@
  * 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;
 
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Set;
 import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authz.AuthorizationException;
 import org.apache.shiro.config.Ini;
+import org.apache.shiro.subject.Subject;
 import org.caosdb.server.CaosDBServer;
 import org.caosdb.server.ServerProperties;
 import org.caosdb.server.entity.Message;
 import org.caosdb.server.permissions.Role;
+import org.caosdb.server.transaction.LogUserVisitTransaction;
 import org.caosdb.server.transaction.RetrieveRoleTransaction;
 import org.caosdb.server.transaction.RetrieveUserTransaction;
 import org.caosdb.server.utils.ServerMessages;
@@ -69,6 +72,8 @@ import org.slf4j.LoggerFactory;
  */
 public class UserSources extends HashMap<String, UserSource> {
 
+  public static final String USERNAME_PASSWORD_AUTHENTICATION = "USERNAME_PASSWORD_AUTHENTICATION";
+  private static final Subject transactor = new SinglePermissionSubject("ACM:*:RETRIEVE:*");
   private static final Logger logger = LoggerFactory.getLogger(UserSources.class);
   public static final String KEY_DEFAULT_REALM = "defaultRealm";
   public static final String KEY_REALMS = "realms";
@@ -86,6 +91,14 @@ public class UserSources extends HashMap<String, UserSource> {
    * @return true iff the user identified by the given {@link Principal} exists.
    */
   public static boolean isUserExisting(final Principal principal) {
+    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());
@@ -164,18 +177,23 @@ public class UserSources extends HashMap<String, UserSource> {
    * @return A set of user roles.
    */
   public static Set<String> resolveRoles(String realm, final String username) {
+    if (AnonymousAuthenticationToken.PRINCIPAL.getRealm().equals(realm)
+        && AnonymousAuthenticationToken.PRINCIPAL.getUsername().equals(username)) {
+      return Collections.singleton(Role.ANONYMOUS_ROLE.toString());
+    }
     if (realm == null) {
       realm = guessRealm(username);
     }
 
-    UserSource userSource = instance.get(realm);
-    if (userSource == null) {
-      return null;
+    RetrieveUserTransaction t = new RetrieveUserTransaction(realm, username, transactor);
+    try {
+      t.execute();
+      if (t.getUser() != null) return t.getRoles();
+    } catch (Exception e) {
+      throw new AuthorizationException("Could not resolve roles for " + username + "@" + realm);
     }
-    // find all roles that are associated with this principal in this realm
-    final Set<String> ret = userSource.resolveRolesForUsername(username);
 
-    return ret;
+    return null;
   }
 
   public static String guessRealm(final String username) {
@@ -206,20 +224,28 @@ public class UserSources extends HashMap<String, UserSource> {
     return instance.map.getSectionProperty(Ini.DEFAULT_SECTION_NAME, KEY_DEFAULT_REALM, "CaosDB");
   }
 
-  // @todo Refactor name: resolveRoles(...)?
-  public static Set<String> resolve(final Principal principal) {
+  /**
+   * Return the roles of a given user.
+   *
+   * @param principal
+   * @return A set of role names.
+   */
+  public static Set<String> resolveRoles(final Principal principal) {
     if (AnonymousAuthenticationToken.PRINCIPAL == principal) {
       // anymous has one role
       Set<String> roles = new HashSet<>();
       roles.add(Role.ANONYMOUS_ROLE.toString());
       return roles;
     }
+    if (principal instanceof OneTimeAuthenticationToken) {
+      return new HashSet<>(((OneTimeAuthenticationToken) principal).getRoles());
+    }
 
     return resolveRoles(principal.getRealm(), principal.getUsername());
   }
 
   public static boolean isRoleExisting(final String role) {
-    final RetrieveRoleTransaction t = new RetrieveRoleTransaction(role);
+    final RetrieveRoleTransaction t = new RetrieveRoleTransaction(role, transactor);
     try {
       t.execute();
       return true;
@@ -233,12 +259,20 @@ public class UserSources extends HashMap<String, UserSource> {
     }
   }
 
+  public static UserStatus getDefaultUserStatus(final String realm, String username) {
+    return instance.get(realm).getDefaultUserStatus(username);
+  }
+
   public static UserStatus getDefaultUserStatus(final Principal p) {
-    return instance.get(p.getRealm()).getDefaultUserStatus(p.getUsername());
+    return getDefaultUserStatus(p.getRealm(), p.getUsername());
   }
 
   public static String getDefaultUserEmail(final Principal p) {
-    return instance.get(p.getRealm()).getDefaultUserEmail(p.getUsername());
+    return getDefaultUserEmail(p.getRealm(), p.getUsername());
+  }
+
+  public static String getDefaultUserEmail(String realm, String username) {
+    return instance.get(realm).getDefaultUserEmail(username);
   }
 
   public static UserSource getInternalRealm() {
@@ -251,16 +285,60 @@ public class UserSources extends HashMap<String, UserSource> {
     }
 
     final boolean isValid = instance.get(realm).isValid(username, password);
-    return isValid && isActive(realm, username);
+
+    if (isValid && isActive(realm, username)) {
+      logUserVisit(realm, username, USERNAME_PASSWORD_AUTHENTICATION);
+      return true;
+    }
+    return false;
+  }
+
+  /** Log the current time as the user's last visit. */
+  public static void logUserVisit(String realm, String username, String type) {
+    try {
+      LogUserVisitTransaction t =
+          new LogUserVisitTransaction(System.currentTimeMillis(), realm, username, type);
+      t.execute();
+    } catch (final Exception e) {
+      throw new AuthenticationException(e);
+    }
   }
 
   private static boolean isActive(final String realm, final String username) {
-    final RetrieveUserTransaction t = new RetrieveUserTransaction(realm, username);
+    final RetrieveUserTransaction t = new RetrieveUserTransaction(realm, username, transactor);
     try {
       t.execute();
-      return t.isActive();
+      if (t.getUser() != null) {
+        return t.getUser().status == UserStatus.ACTIVE;
+      } else {
+        return instance.get(realm).getDefaultUserStatus(username) == UserStatus.ACTIVE;
+      }
     } catch (final Exception e) {
       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());
+  }
+
+  public static Set<String> getDefaultRoles(String realm, String username) {
+    UserSource userSource = instance.get(realm);
+    if (userSource == null) {
+      return null;
+    }
+    // find all roles that are associated with this principal in this realm
+    final Set<String> ret = userSource.resolveRolesForUsername(username);
+
+    return ret;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/database/BackendTransaction.java b/src/main/java/org/caosdb/server/database/BackendTransaction.java
index 7c26db1ed05e76c30f9d30f0c3d9965ed0c59b6b..5addf3f3f3e34a8e3e6a9152b9511046fe4b6a23 100644
--- a/src/main/java/org/caosdb/server/database/BackendTransaction.java
+++ b/src/main/java/org/caosdb/server/database/BackendTransaction.java
@@ -44,10 +44,14 @@ import org.caosdb.server.database.backend.implementation.MySQL.MySQLInsertRole;
 import org.caosdb.server.database.backend.implementation.MySQL.MySQLInsertSparseEntity;
 import org.caosdb.server.database.backend.implementation.MySQL.MySQLInsertTransactionHistory;
 import org.caosdb.server.database.backend.implementation.MySQL.MySQLIsSubType;
+import org.caosdb.server.database.backend.implementation.MySQL.MySQLListRoles;
+import org.caosdb.server.database.backend.implementation.MySQL.MySQLListUsers;
+import org.caosdb.server.database.backend.implementation.MySQL.MySQLLogUserVisit;
 import org.caosdb.server.database.backend.implementation.MySQL.MySQLRegisterSubDomain;
 import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveAll;
 import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveAllUncheckedFiles;
 import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveDatatypes;
+import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveEntityACL;
 import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveLogRecord;
 import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveParents;
 import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrievePasswordValidator;
@@ -99,10 +103,14 @@ import org.caosdb.server.database.backend.interfaces.InsertRoleImpl;
 import org.caosdb.server.database.backend.interfaces.InsertSparseEntityImpl;
 import org.caosdb.server.database.backend.interfaces.InsertTransactionHistoryImpl;
 import org.caosdb.server.database.backend.interfaces.IsSubTypeImpl;
+import org.caosdb.server.database.backend.interfaces.ListRolesImpl;
+import org.caosdb.server.database.backend.interfaces.ListUsersImpl;
+import org.caosdb.server.database.backend.interfaces.LogUserVisitImpl;
 import org.caosdb.server.database.backend.interfaces.RegisterSubDomainImpl;
 import org.caosdb.server.database.backend.interfaces.RetrieveAllImpl;
 import org.caosdb.server.database.backend.interfaces.RetrieveAllUncheckedFilesImpl;
 import org.caosdb.server.database.backend.interfaces.RetrieveDatatypesImpl;
+import org.caosdb.server.database.backend.interfaces.RetrieveEntityACLImpl;
 import org.caosdb.server.database.backend.interfaces.RetrieveLogRecordImpl;
 import org.caosdb.server.database.backend.interfaces.RetrieveParentsImpl;
 import org.caosdb.server.database.backend.interfaces.RetrievePasswordValidatorImpl;
@@ -192,6 +200,7 @@ public abstract class BackendTransaction implements Undoable {
       setImpl(FileCheckSize.class, UnixFileSystemCheckSize.class);
       setImpl(InsertRoleImpl.class, MySQLInsertRole.class);
       setImpl(RetrieveRoleImpl.class, MySQLRetrieveRole.class);
+      setImpl(ListRolesImpl.class, MySQLListRoles.class);
       setImpl(DeleteRoleImpl.class, MySQLDeleteRole.class);
       setImpl(SetPermissionRulesImpl.class, MySQLSetPermissionRules.class);
       setImpl(RetrievePermissionRulesImpl.class, MySQLRetrievePermissionRules.class);
@@ -204,6 +213,9 @@ public abstract class BackendTransaction implements Undoable {
       setImpl(InsertEntityDatatypeImpl.class, MySQLInsertEntityDatatype.class);
       setImpl(RetrieveVersionHistoryImpl.class, MySQLRetrieveVersionHistory.class);
       setImpl(SetFileChecksumImpl.class, MySQLSetFileChecksum.class);
+      setImpl(ListUsersImpl.class, MySQLListUsers.class);
+      setImpl(LogUserVisitImpl.class, MySQLLogUserVisit.class);
+      setImpl(RetrieveEntityACLImpl.class, MySQLRetrieveEntityACL.class);
     }
   }
 
diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLDeleteRole.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLDeleteRole.java
index 8fd8d35fda6b99f7d79b245a10ace4ed9521e0ce..b0faa45f96e04e6957ff1d288d034ca9e96cb6d7 100644
--- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLDeleteRole.java
+++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLDeleteRole.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,15 +19,17 @@
  * 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.database.backend.implementation.MySQL;
 
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
+import java.sql.SQLIntegrityConstraintViolationException;
 import org.caosdb.server.database.access.Access;
 import org.caosdb.server.database.backend.interfaces.DeleteRoleImpl;
 import org.caosdb.server.database.exceptions.TransactionException;
+import org.caosdb.server.utils.ServerMessages;
 
 public class MySQLDeleteRole extends MySQLTransaction implements DeleteRoleImpl {
 
@@ -42,6 +45,8 @@ public class MySQLDeleteRole extends MySQLTransaction implements DeleteRoleImpl
       final PreparedStatement stmt = prepareStatement(STMT_DELETE_ROLE);
       stmt.setString(1, role);
       stmt.execute();
+    } catch (final SQLIntegrityConstraintViolationException e) {
+      throw new TransactionException(ServerMessages.ROLE_CANNOT_BE_DELETED);
     } catch (final SQLException e) {
       throw new TransactionException(e);
     } catch (final ConnectionException e) {
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 b1f445e2b8ad2d79b756e1dd662296d8e6c9a49c..568e0e53a7b80a76c533229e13c6700c4c4ff47f 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/implementation/MySQL/MySQLListRoles.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLListRoles.java
new file mode 100644
index 0000000000000000000000000000000000000000..69ac524eae823d06fdbd4346056979ea15307437
--- /dev/null
+++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLListRoles.java
@@ -0,0 +1,65 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.database.backend.implementation.MySQL;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.LinkedList;
+import java.util.List;
+import org.caosdb.server.accessControl.Role;
+import org.caosdb.server.database.access.Access;
+import org.caosdb.server.database.backend.interfaces.ListRolesImpl;
+import org.caosdb.server.database.exceptions.TransactionException;
+
+public class MySQLListRoles extends MySQLTransaction implements ListRolesImpl {
+
+  public MySQLListRoles(Access access) {
+    super(access);
+  }
+
+  public static final String STMT_LIST_ROLES = "SELECT name, description FROM roles";
+
+  @Override
+  public List<Role> execute() {
+    List<Role> roles = new LinkedList<>();
+    try {
+      final PreparedStatement stmt = prepareStatement(STMT_LIST_ROLES);
+      final ResultSet rs = stmt.executeQuery();
+      try {
+        while (rs.next()) {
+          final Role role = new Role();
+          role.name = rs.getString("name");
+          role.description = rs.getString("description");
+          roles.add(role);
+        }
+      } finally {
+        rs.close();
+      }
+    } catch (final SQLException e) {
+      throw new TransactionException(e);
+    } catch (final ConnectionException e) {
+      throw new TransactionException(e);
+    }
+    return roles;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLListUsers.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLListUsers.java
new file mode 100644
index 0000000000000000000000000000000000000000..83ebffe679e6625da720fd2933a5d8defb7264c3
--- /dev/null
+++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLListUsers.java
@@ -0,0 +1,73 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.database.backend.implementation.MySQL;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.LinkedList;
+import java.util.List;
+import org.caosdb.server.accessControl.UserStatus;
+import org.caosdb.server.database.access.Access;
+import org.caosdb.server.database.backend.interfaces.ListUsersImpl;
+import org.caosdb.server.database.exceptions.TransactionException;
+import org.caosdb.server.database.proto.ProtoUser;
+
+public class MySQLListUsers extends MySQLTransaction implements ListUsersImpl {
+
+  public MySQLListUsers(Access access) {
+    super(access);
+  }
+
+  public static final String STMT_LIST_USERS =
+      "SELECT status, realm, name, email, entity FROM user_info";
+
+  @Override
+  public List<ProtoUser> execute() {
+    List<ProtoUser> users = new LinkedList<>();
+    try {
+      final PreparedStatement stmt = prepareStatement(STMT_LIST_USERS);
+      final ResultSet rs = stmt.executeQuery();
+      try {
+        while (rs.next()) {
+          final ProtoUser user = new ProtoUser();
+          user.name = rs.getString("name");
+          user.realm = rs.getString("realm");
+          user.email = rs.getString("email");
+          user.entity = rs.getInt("entity");
+          if (user.entity == 0) {
+            user.entity = null;
+          }
+          user.status = UserStatus.valueOf(rs.getString("status"));
+          users.add(user);
+        }
+      } finally {
+        rs.close();
+      }
+    } catch (final SQLException e) {
+      throw new TransactionException(e);
+    } catch (final ConnectionException e) {
+      throw new TransactionException(e);
+    }
+    return users;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLLogUserVisit.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLLogUserVisit.java
new file mode 100644
index 0000000000000000000000000000000000000000..23050da87e324026be1eefa02690959d0a0590cc
--- /dev/null
+++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLLogUserVisit.java
@@ -0,0 +1,59 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.database.backend.implementation.MySQL;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.caosdb.server.database.access.Access;
+import org.caosdb.server.database.backend.interfaces.LogUserVisitImpl;
+import org.caosdb.server.database.exceptions.TransactionException;
+
+public class MySQLLogUserVisit extends MySQLTransaction implements LogUserVisitImpl {
+
+  public MySQLLogUserVisit(Access access) {
+    super(access);
+  }
+
+  public static final String LOG_USER_VISIT =
+      "SELECT status FROM user_info WHERE realm = ? AND name = ?";
+  // TODO Replace by "UPDATE user_info SET last_seen = ?";
+
+  /** Return true if this is not the first visit of this user. */
+  @Override
+  public boolean logUserReturnIsKnown(long timestamp, String realm, String username, String type) {
+    try (final PreparedStatement stmt = prepareStatement(LOG_USER_VISIT)) {
+      stmt.setString(1, realm);
+      stmt.setString(2, username);
+      ResultSet executeQuery = stmt.executeQuery();
+      if (!executeQuery.next()) {
+        // first login of this user
+        return false;
+      }
+    } catch (final SQLException e) {
+      throw new TransactionException(e);
+    } catch (final ConnectionException e) {
+      throw new TransactionException(e);
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveEntityACL.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveEntityACL.java
new file mode 100644
index 0000000000000000000000000000000000000000..5db8ed7597b1c0d357273848fb7c36febdcfed13
--- /dev/null
+++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveEntityACL.java
@@ -0,0 +1,57 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.database.backend.implementation.MySQL;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.caosdb.server.database.access.Access;
+import org.caosdb.server.database.backend.interfaces.RetrieveEntityACLImpl;
+import org.caosdb.server.database.exceptions.TransactionException;
+import org.caosdb.server.database.proto.VerySparseEntity;
+
+public class MySQLRetrieveEntityACL extends MySQLTransaction implements RetrieveEntityACLImpl {
+
+  public MySQLRetrieveEntityACL(Access access) {
+    super(access);
+  }
+
+  public static final String STMT =
+      "SELECT a.acl FROM entities AS e LEFT JOIN entity_acl AS a ON (a.id = e.acl) WHERE e.id = ? LIMIT 1";
+
+  @Override
+  public VerySparseEntity execute(Integer id) {
+    try (PreparedStatement stmt = prepareStatement(STMT)) {
+      stmt.setInt(1, id);
+      ResultSet rs = stmt.executeQuery();
+      if (rs.next()) {
+        VerySparseEntity result = new VerySparseEntity();
+        result.id = id;
+        result.acl = rs.getString(1);
+        return result;
+      }
+    } catch (SQLException | ConnectionException e) {
+      throw new TransactionException(e);
+    }
+    return null;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveRole.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveRole.java
index f0219040c47bc09fe2052d8e95a3ea83637e6aae..e9ee37c9a55bc07c79abc8b73d8f9bef2a5cde8c 100644
--- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveRole.java
+++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveRole.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,17 +19,22 @@
  * 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.database.backend.implementation.MySQL;
 
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.LinkedList;
+import java.util.Map;
 import org.caosdb.server.accessControl.Role;
 import org.caosdb.server.database.access.Access;
 import org.caosdb.server.database.backend.interfaces.RetrieveRoleImpl;
 import org.caosdb.server.database.exceptions.TransactionException;
+import org.caosdb.server.database.proto.ProtoUser;
+import org.caosdb.server.permissions.PermissionRule;
+import org.eclipse.jetty.util.ajax.JSON;
 
 public class MySQLRetrieveRole extends MySQLTransaction implements RetrieveRoleImpl {
 
@@ -36,30 +42,66 @@ public class MySQLRetrieveRole extends MySQLTransaction implements RetrieveRoleI
     super(access);
   }
 
-  public static final String STMT_RETRIEVE_ROLE = "SELECT description FROM roles WHERE name=?";
+  public static final String STMT_RETRIEVE_ROLE =
+      "SELECT r.description AS description, p.permissions AS permissions FROM roles AS r LEFT JOIN permissions AS p ON (r.name = p.role) WHERE r.name=?";
+  public static final String STMT_RETRIEVE_USERS =
+      "SELECT u.realm, u.user FROM user_roles AS u WHERE u.role = ?";
 
   @Override
   public Role retrieve(final String role) throws TransactionException {
-    try {
-      final PreparedStatement stmt = prepareStatement(STMT_RETRIEVE_ROLE);
+    Role ret = null;
+    try (final PreparedStatement stmt = prepareStatement(STMT_RETRIEVE_ROLE)) {
       stmt.setString(1, role);
       final ResultSet rs = stmt.executeQuery();
-      try {
-        if (rs.next()) {
-          final Role ret = new Role();
-          ret.name = role;
-          ret.description = rs.getString("description");
-          return ret;
-        } else {
-          return null;
+      if (rs.next()) {
+        ret = new Role();
+        ret.name = role;
+        ret.description = rs.getString("description");
+        ret.permission_rules = parse(rs.getString("permissions"));
+      } else {
+        return null;
+      }
+    } catch (final SQLException e) {
+      throw new TransactionException(e);
+    } catch (final ConnectionException e) {
+      throw new TransactionException(e);
+    }
+
+    try (final PreparedStatement stmt = prepareStatement(STMT_RETRIEVE_USERS)) {
+      stmt.setString(1, role);
+      final ResultSet rs = stmt.executeQuery();
+      if (rs.next()) {
+        ret.users = new LinkedList<>();
+
+        ret.users.add(nextUser(rs));
+        while (rs.next()) {
+          ret.users.add(nextUser(rs));
         }
-      } finally {
-        rs.close();
       }
     } catch (final SQLException e) {
       throw new TransactionException(e);
     } catch (final ConnectionException e) {
       throw new TransactionException(e);
     }
+    return ret;
+  }
+
+  private ProtoUser nextUser(ResultSet rs) throws SQLException {
+    ProtoUser nextUser = new ProtoUser();
+    nextUser.realm = rs.getString("realm");
+    nextUser.name = rs.getString("user");
+    return nextUser;
+  }
+
+  @SuppressWarnings("unchecked")
+  private LinkedList<PermissionRule> parse(String string) {
+    if (string == null) return null;
+    final Object[] maps = (Object[]) JSON.parse(string);
+    final LinkedList<PermissionRule> ret = new LinkedList<>();
+    for (final Object map : maps) {
+      ret.add(PermissionRule.parse((Map<String, String>) map));
+    }
+
+    return ret;
   }
 }
diff --git a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLUpdateUserRoles.java b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLUpdateUserRoles.java
index 62cb68bf58fc8269cc365fa167929ff79726499b..6a52ab9d0e7d606d5f92038239be0ccacd03bda6 100644
--- a/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLUpdateUserRoles.java
+++ b/src/main/java/org/caosdb/server/database/backend/implementation/MySQL/MySQLUpdateUserRoles.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,13 +19,12 @@
  * 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.database.backend.implementation.MySQL;
 
 import java.sql.PreparedStatement;
 import java.sql.SQLException;
-import java.util.HashSet;
+import java.util.Set;
 import org.caosdb.server.database.access.Access;
 import org.caosdb.server.database.backend.interfaces.UpdateUserRolesImpl;
 import org.caosdb.server.database.exceptions.TransactionException;
@@ -41,7 +41,7 @@ public class MySQLUpdateUserRoles extends MySQLTransaction implements UpdateUser
       "INSERT INTO user_roles (realm, user, role) VALUES (?,?,?);";
 
   @Override
-  public void updateUserRoles(final String realm, final String user, final HashSet<String> roles)
+  public void updateUserRoles(final String realm, final String user, final Set<String> roles)
       throws TransactionException {
     try {
       final PreparedStatement delete_stmt = prepareStatement(STMT_DELETE_USER_ROLES);
diff --git a/src/main/java/org/caosdb/server/database/backend/interfaces/ListRolesImpl.java b/src/main/java/org/caosdb/server/database/backend/interfaces/ListRolesImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..aab9041ae0f4e02d4b601e97d042dff591fbbf85
--- /dev/null
+++ b/src/main/java/org/caosdb/server/database/backend/interfaces/ListRolesImpl.java
@@ -0,0 +1,30 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.database.backend.interfaces;
+
+import java.util.List;
+import org.caosdb.server.accessControl.Role;
+
+public interface ListRolesImpl extends BackendTransactionImpl {
+
+  public List<Role> execute();
+}
diff --git a/src/main/java/org/caosdb/server/database/backend/interfaces/ListUsersImpl.java b/src/main/java/org/caosdb/server/database/backend/interfaces/ListUsersImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..6bdfda92f2b5a78b8b2dfc8b6f20aac3b68c08a9
--- /dev/null
+++ b/src/main/java/org/caosdb/server/database/backend/interfaces/ListUsersImpl.java
@@ -0,0 +1,30 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.database.backend.interfaces;
+
+import java.util.List;
+import org.caosdb.server.database.proto.ProtoUser;
+
+public interface ListUsersImpl extends BackendTransactionImpl {
+
+  List<ProtoUser> execute();
+}
diff --git a/src/main/java/org/caosdb/server/database/backend/interfaces/LogUserVisitImpl.java b/src/main/java/org/caosdb/server/database/backend/interfaces/LogUserVisitImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..38c253e2e76075c83a7874d581f8b97d25a382f5
--- /dev/null
+++ b/src/main/java/org/caosdb/server/database/backend/interfaces/LogUserVisitImpl.java
@@ -0,0 +1,26 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.caosdb.server.database.backend.interfaces;
+
+public interface LogUserVisitImpl extends BackendTransactionImpl {
+
+  /** Return true if this is not the first visit of this user. */
+  boolean logUserReturnIsKnown(long timestamp, String realm, String username, String type);
+}
diff --git a/src/main/java/org/caosdb/server/database/backend/interfaces/RetrieveEntityACLImpl.java b/src/main/java/org/caosdb/server/database/backend/interfaces/RetrieveEntityACLImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..230d4d34103603d5499532a7dc1a941cf82068e7
--- /dev/null
+++ b/src/main/java/org/caosdb/server/database/backend/interfaces/RetrieveEntityACLImpl.java
@@ -0,0 +1,29 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.database.backend.interfaces;
+
+import org.caosdb.server.database.proto.VerySparseEntity;
+
+public interface RetrieveEntityACLImpl extends BackendTransactionImpl {
+
+  public VerySparseEntity execute(Integer id);
+}
diff --git a/src/main/java/org/caosdb/server/database/backend/interfaces/UpdateUserRolesImpl.java b/src/main/java/org/caosdb/server/database/backend/interfaces/UpdateUserRolesImpl.java
index f2abfbe3cb7846f16e21d8f9a719ad708884841c..891df301e992f42c38f7c52d867353a65cddd1b2 100644
--- a/src/main/java/org/caosdb/server/database/backend/interfaces/UpdateUserRolesImpl.java
+++ b/src/main/java/org/caosdb/server/database/backend/interfaces/UpdateUserRolesImpl.java
@@ -22,11 +22,11 @@
  */
 package org.caosdb.server.database.backend.interfaces;
 
-import java.util.HashSet;
+import java.util.Set;
 import org.caosdb.server.database.exceptions.TransactionException;
 
 public interface UpdateUserRolesImpl extends BackendTransactionImpl {
 
-  public void updateUserRoles(String realm, String user, HashSet<String> roles)
+  public void updateUserRoles(String realm, String user, Set<String> roles)
       throws TransactionException;
 }
diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntityProperties.java b/src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntityProperties.java
index 7b02fbf003e49c18bc14a602f7e2f781b495c682..4072c1b2877cd9231424a98948fad32747870537 100644
--- a/src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntityProperties.java
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/DeleteEntityProperties.java
@@ -22,13 +22,12 @@
  */
 package org.caosdb.server.database.backend.transaction;
 
-import static org.caosdb.server.transaction.Transaction.ERROR_INTEGRITY_VIOLATION;
-
 import org.caosdb.server.database.BackendTransaction;
 import org.caosdb.server.database.backend.interfaces.DeleteEntityPropertiesImpl;
 import org.caosdb.server.database.exceptions.IntegrityException;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.utils.EntityStatus;
+import org.caosdb.server.utils.ServerMessages;
 
 public class DeleteEntityProperties extends BackendTransaction {
 
@@ -50,7 +49,7 @@ public class DeleteEntityProperties extends BackendTransaction {
       try {
         ret.execute(this.entity.getId());
       } catch (final IntegrityException exc) {
-        this.entity.addError(ERROR_INTEGRITY_VIOLATION);
+        this.entity.addError(ServerMessages.ERROR_INTEGRITY_VIOLATION);
         throw exc;
       }
     }
diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/DeleteSparseEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/DeleteSparseEntity.java
index 55989d219ad738d5a866e0ef6f322368c8fc559b..e371cfe1f9caf3d4438e8801d4a6cee1119322f8 100644
--- a/src/main/java/org/caosdb/server/database/backend/transaction/DeleteSparseEntity.java
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/DeleteSparseEntity.java
@@ -24,13 +24,12 @@
  */
 package org.caosdb.server.database.backend.transaction;
 
-import static org.caosdb.server.transaction.Transaction.ERROR_INTEGRITY_VIOLATION;
-
 import org.caosdb.server.database.BackendTransaction;
 import org.caosdb.server.database.backend.interfaces.DeleteSparseEntityImpl;
 import org.caosdb.server.database.exceptions.IntegrityException;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.utils.EntityStatus;
+import org.caosdb.server.utils.ServerMessages;
 
 public class DeleteSparseEntity extends BackendTransaction {
 
@@ -54,7 +53,7 @@ public class DeleteSparseEntity extends BackendTransaction {
         ret.execute(this.entity.getId());
       }
     } catch (final IntegrityException exc) {
-      this.entity.addError(ERROR_INTEGRITY_VIOLATION);
+      this.entity.addError(ServerMessages.ERROR_INTEGRITY_VIOLATION);
       throw exc;
     }
   }
diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityDatatype.java b/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityDatatype.java
index 182874d1f1e30ed8f5249e609dbbc42a724d94af..aa139e7db565ae82073d6983915c0c53e9cd8b51 100644
--- a/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityDatatype.java
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/InsertEntityDatatype.java
@@ -1,12 +1,11 @@
 package org.caosdb.server.database.backend.transaction;
 
-import static org.caosdb.server.transaction.Transaction.ERROR_INTEGRITY_VIOLATION;
-
 import org.caosdb.server.database.BackendTransaction;
 import org.caosdb.server.database.backend.interfaces.InsertEntityDatatypeImpl;
 import org.caosdb.server.database.exceptions.IntegrityException;
 import org.caosdb.server.database.proto.SparseEntity;
 import org.caosdb.server.entity.EntityInterface;
+import org.caosdb.server.utils.ServerMessages;
 
 public class InsertEntityDatatype extends BackendTransaction {
 
@@ -25,7 +24,7 @@ public class InsertEntityDatatype extends BackendTransaction {
     try {
       t.execute(e);
     } catch (final IntegrityException exc) {
-      this.entity.addError(ERROR_INTEGRITY_VIOLATION);
+      this.entity.addError(ServerMessages.ERROR_INTEGRITY_VIOLATION);
       throw exc;
     }
 
diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/InsertSparseEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/InsertSparseEntity.java
index 22720f836e5cbd4912f3aedccd25398e80a19324..213c7529766dcac4cc002f81380bf8d141cd8065 100644
--- a/src/main/java/org/caosdb/server/database/backend/transaction/InsertSparseEntity.java
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/InsertSparseEntity.java
@@ -22,8 +22,6 @@
  */
 package org.caosdb.server.database.backend.transaction;
 
-import static org.caosdb.server.transaction.Transaction.ERROR_INTEGRITY_VIOLATION;
-
 import org.caosdb.server.database.BackendTransaction;
 import org.caosdb.server.database.backend.interfaces.InsertSparseEntityImpl;
 import org.caosdb.server.database.exceptions.IntegrityException;
@@ -31,6 +29,7 @@ import org.caosdb.server.database.exceptions.TransactionException;
 import org.caosdb.server.database.proto.SparseEntity;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.Version;
+import org.caosdb.server.utils.ServerMessages;
 import org.caosdb.server.utils.Undoable;
 
 public class InsertSparseEntity extends BackendTransaction {
@@ -52,7 +51,7 @@ public class InsertSparseEntity extends BackendTransaction {
     try {
       t.execute(e);
     } catch (final IntegrityException exc) {
-      this.entity.addError(ERROR_INTEGRITY_VIOLATION);
+      this.entity.addError(ServerMessages.ERROR_INTEGRITY_VIOLATION);
       throw exc;
     }
 
diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/ListRoles.java b/src/main/java/org/caosdb/server/database/backend/transaction/ListRoles.java
new file mode 100644
index 0000000000000000000000000000000000000000..75792475c4c5a6ae9091f89c8de1b4ef157aa11f
--- /dev/null
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/ListRoles.java
@@ -0,0 +1,42 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.database.backend.transaction;
+
+import java.util.List;
+import org.caosdb.server.accessControl.Role;
+import org.caosdb.server.database.BackendTransaction;
+import org.caosdb.server.database.backend.interfaces.ListRolesImpl;
+
+public class ListRoles extends BackendTransaction {
+
+  private List<Role> roles;
+
+  @Override
+  protected void execute() {
+    ListRolesImpl t = getImplementation(ListRolesImpl.class);
+    roles = t.execute();
+  }
+
+  public List<Role> getRoles() {
+    return roles;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/ListUsers.java b/src/main/java/org/caosdb/server/database/backend/transaction/ListUsers.java
new file mode 100644
index 0000000000000000000000000000000000000000..9f29a3c5888104140eb6e2c29b83370fc1c56401
--- /dev/null
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/ListUsers.java
@@ -0,0 +1,42 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.database.backend.transaction;
+
+import java.util.List;
+import org.caosdb.server.database.BackendTransaction;
+import org.caosdb.server.database.backend.interfaces.ListUsersImpl;
+import org.caosdb.server.database.proto.ProtoUser;
+
+public class ListUsers extends BackendTransaction {
+
+  private List<ProtoUser> users;
+
+  @Override
+  protected void execute() {
+    ListUsersImpl t = getImplementation(ListUsersImpl.class);
+    users = t.execute();
+  }
+
+  public List<ProtoUser> getUsers() {
+    return users;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/LogUserVisit.java b/src/main/java/org/caosdb/server/database/backend/transaction/LogUserVisit.java
new file mode 100644
index 0000000000000000000000000000000000000000..43fd87495c4e7489e7f15ec8674ce01cb674dd90
--- /dev/null
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/LogUserVisit.java
@@ -0,0 +1,66 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.database.backend.transaction;
+
+import java.util.HashSet;
+import org.caosdb.server.accessControl.UserSources;
+import org.caosdb.server.database.BackendTransaction;
+import org.caosdb.server.database.backend.interfaces.LogUserVisitImpl;
+import org.caosdb.server.database.backend.interfaces.UpdateUserImpl;
+import org.caosdb.server.database.backend.interfaces.UpdateUserRolesImpl;
+import org.caosdb.server.database.exceptions.TransactionException;
+import org.caosdb.server.database.proto.ProtoUser;
+
+public class LogUserVisit extends BackendTransaction {
+
+  private String realm;
+  private String username;
+  private String type;
+  private long timestamp;
+
+  public LogUserVisit(long timestamp, String realm, String username, String type) {
+    this.timestamp = timestamp;
+    this.realm = realm;
+    this.username = username;
+    this.type = type;
+  }
+
+  @Override
+  protected void execute() throws TransactionException {
+    final LogUserVisitImpl t = getImplementation(LogUserVisitImpl.class);
+    if (!t.logUserReturnIsKnown(timestamp, realm, username, type)) {
+      // User is unknown. Make it known
+      ProtoUser user = new ProtoUser();
+      user.realm = realm;
+      user.name = username;
+      user.email = UserSources.getDefaultUserEmail(realm, username);
+      user.status = UserSources.getDefaultUserStatus(realm, username);
+      user.roles = new HashSet<>(UserSources.getDefaultRoles(realm, username));
+
+      UpdateUserImpl insertUser = getImplementation(UpdateUserImpl.class);
+      insertUser.execute(user);
+      UpdateUserRolesImpl setRoles = getImplementation(UpdateUserRolesImpl.class);
+      setRoles.updateUserRoles(user.realm, user.name, user.roles);
+      t.logUserReturnIsKnown(timestamp, realm, username, type);
+    }
+  }
+}
diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveEntityACLTransaction.java b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveEntityACLTransaction.java
new file mode 100644
index 0000000000000000000000000000000000000000..9cb96112532fe249fd81c3e8d10d85ca900293d9
--- /dev/null
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/RetrieveEntityACLTransaction.java
@@ -0,0 +1,71 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.database.backend.transaction;
+
+import org.caosdb.server.database.CacheableBackendTransaction;
+import org.caosdb.server.database.backend.interfaces.RetrieveEntityACLImpl;
+import org.caosdb.server.database.exceptions.TransactionException;
+import org.caosdb.server.database.proto.VerySparseEntity;
+import org.caosdb.server.permissions.EntityACL;
+
+public class RetrieveEntityACLTransaction
+    extends CacheableBackendTransaction<Integer, VerySparseEntity> {
+
+  private Integer id;
+  private EntityACL entityAcl;
+
+  public RetrieveEntityACLTransaction(Integer id) {
+    // TODO
+    super(null);
+    this.id = id;
+  }
+
+  @Override
+  public VerySparseEntity executeNoCache() throws TransactionException {
+    RetrieveEntityACLImpl t = getImplementation(RetrieveEntityACLImpl.class);
+    return t.execute(getKey());
+  }
+
+  @Override
+  protected void process(VerySparseEntity t) throws TransactionException {
+    this.entityAcl = EntityACL.fromJSON(t.acl);
+  }
+
+  @Override
+  protected Integer getKey() {
+    return id;
+  }
+
+  public EntityACL getEntityAcl() {
+    return entityAcl;
+  }
+
+  public RetrieveEntityACLTransaction reuse(Integer id) {
+    this.id = id;
+    this.entityAcl = null;
+    return this;
+  }
+
+  public static void removeCached(Integer id) {
+    // TODO
+  }
+}
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 1e855663590d85610860fcee246a4f1ed78a31b2..f4059832095acc38d3c65b828412c09cc58b3194 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/database/backend/transaction/UpdateSparseEntity.java b/src/main/java/org/caosdb/server/database/backend/transaction/UpdateSparseEntity.java
index 8548fb0477de9d8b52b18b8132af15295dbc9353..837f64b32ca4cd6f035b74d13ee116205c7e82bc 100644
--- a/src/main/java/org/caosdb/server/database/backend/transaction/UpdateSparseEntity.java
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/UpdateSparseEntity.java
@@ -4,8 +4,8 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
- * Copyright (C) 2019 IndiScale GmbH
- * Copyright (C) 2019 Timm Fitschen (t.fitschen@indiscale.com)
+ * Copyright (C) 2019, 2021 IndiScale GmbH
+ * Copyright (C) 2019, 2021 Timm Fitschen (t.fitschen@indiscale.com)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -41,6 +41,7 @@ public class UpdateSparseEntity extends BackendTransaction {
 
   @Override
   public void execute() throws TransactionException {
+    RetrieveEntityACLTransaction.removeCached(this.entity.getId());
     RetrieveSparseEntity.removeCached(this.entity);
     if (entity.hasFileProperties()) {
       GetFileRecordByPath.removeCached(this.entity.getFileProperties().getPath());
diff --git a/src/main/java/org/caosdb/server/database/backend/transaction/UpdateUserRoles.java b/src/main/java/org/caosdb/server/database/backend/transaction/UpdateUserRoles.java
index 66b3cb8e6f03c3abde0ccaa051a1228057d973f7..bbd23aa95d3eb1bed4f2bafc78fa12d112a02169 100644
--- a/src/main/java/org/caosdb/server/database/backend/transaction/UpdateUserRoles.java
+++ b/src/main/java/org/caosdb/server/database/backend/transaction/UpdateUserRoles.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,11 +19,11 @@
  * 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.database.backend.transaction;
 
-import java.util.HashSet;
+import java.util.Set;
 import org.caosdb.server.accessControl.Principal;
 import org.caosdb.server.database.BackendTransaction;
 import org.caosdb.server.database.backend.interfaces.UpdateUserRolesImpl;
@@ -31,10 +32,10 @@ import org.caosdb.server.database.exceptions.TransactionException;
 public class UpdateUserRoles extends BackendTransaction {
 
   private final String user;
-  private final HashSet<String> roles;
+  private final Set<String> roles;
   private final String realm;
 
-  public UpdateUserRoles(final String realm, final String user, final HashSet<String> roles) {
+  public UpdateUserRoles(final String realm, final String user, final Set<String> roles) {
     this.realm = realm;
     this.user = user;
     this.roles = roles;
diff --git a/src/main/java/org/caosdb/server/database/proto/ProtoUser.java b/src/main/java/org/caosdb/server/database/proto/ProtoUser.java
index cafa32d2580c8a09a97e1cd3ee48c2ec59a454cf..643f4250216c7829c8218113a8842ff139b313ec 100644
--- a/src/main/java/org/caosdb/server/database/proto/ProtoUser.java
+++ b/src/main/java/org/caosdb/server/database/proto/ProtoUser.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,24 +19,24 @@
  * 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.database.proto;
 
 import java.io.Serializable;
-import java.util.Set;
+import java.util.HashSet;
 import org.caosdb.server.accessControl.UserStatus;
 
 public class ProtoUser implements Serializable {
 
   public ProtoUser() {}
 
-  private static final long serialVersionUID = -2704114543883567439L;
+  private static final long serialVersionUID = 5381234723597234L;
 
   public UserStatus status = null;
   public String name = null;
   public String email = null;
   public Integer entity = null;
   public String realm = null;
-  public Set<String> roles = null;
+  public HashSet<String> roles = null;
 }
diff --git a/src/main/java/org/caosdb/server/datatype/AbstractEnumValue.java b/src/main/java/org/caosdb/server/datatype/AbstractEnumValue.java
index 06216c822c705fa6fbaef949386966af15ff6b13..be88002543155771d2937e216c64d52931051c25 100644
--- a/src/main/java/org/caosdb/server/datatype/AbstractEnumValue.java
+++ b/src/main/java/org/caosdb/server/datatype/AbstractEnumValue.java
@@ -1,10 +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
  *
+ *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
  * published by the Free Software Foundation, either version 3 of the
@@ -17,9 +17,9 @@
  *
  * 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
  */
+
+/** @review Daniel Hornung 2022-03-04 */
 package org.caosdb.server.datatype;
 
 import com.google.common.base.Objects;
@@ -52,7 +52,15 @@ public abstract class AbstractEnumValue implements SingleValue {
   @Override
   public boolean equals(final Object obj) {
     if (obj instanceof AbstractEnumValue) {
-      final AbstractEnumValue that = (AbstractEnumValue) obj;
+      return equals((AbstractEnumValue) obj);
+    }
+    return false;
+  }
+
+  @Override
+  public boolean equals(Value val) {
+    if (val instanceof AbstractEnumValue) {
+      final AbstractEnumValue that = (AbstractEnumValue) val;
       return Objects.equal(that.value, this.value);
     }
     return false;
diff --git a/src/main/java/org/caosdb/server/datatype/BooleanValue.java b/src/main/java/org/caosdb/server/datatype/BooleanValue.java
index 0abfa0c917f21d246854cabcfe437fe48cb3d59e..2bcbb48bcaf52ec0ef9a83a6d81d51190d1249ef 100644
--- a/src/main/java/org/caosdb/server/datatype/BooleanValue.java
+++ b/src/main/java/org/caosdb/server/datatype/BooleanValue.java
@@ -37,4 +37,8 @@ public class BooleanValue extends AbstractEnumValue {
   public static BooleanValue valueOf(final boolean b) {
     return valueOf(Boolean.toString(b));
   }
+
+  public boolean getValue() {
+    return toDatabaseString().equals("TRUE");
+  }
 }
diff --git a/src/main/java/org/caosdb/server/datatype/CaosEnum.java b/src/main/java/org/caosdb/server/datatype/CaosEnum.java
index 1040ab9a8114544dfd8a0d5a0a75660be4de5f07..00d8cb167a158a40417e7fddcb9fbe1cf21e7cdb 100644
--- a/src/main/java/org/caosdb/server/datatype/CaosEnum.java
+++ b/src/main/java/org/caosdb/server/datatype/CaosEnum.java
@@ -61,17 +61,17 @@ class EnumElement implements Comparable<EnumElement> {
 
 public class CaosEnum {
 
-  final boolean cs;
+  final boolean case_sensitive; // for string operations
 
   public CaosEnum(final String... values) {
     this(false, values);
   }
 
-  public CaosEnum(final boolean cs, final String... values) {
-    this.cs = cs;
+  public CaosEnum(final boolean case_sensitive, final String... values) {
+    this.case_sensitive = case_sensitive;
     int index = 0;
     for (String v : values) {
-      if (!cs) {
+      if (!case_sensitive) {
         v = v.toUpperCase();
       }
       this.values.add(new EnumElement(index++, v));
@@ -82,7 +82,7 @@ public class CaosEnum {
 
   public EnumElement valueOf(final String s) {
     int hash;
-    if (!this.cs) {
+    if (!this.case_sensitive) {
       hash = s.toUpperCase().hashCode();
     } else {
       hash = s.hashCode();
diff --git a/src/main/java/org/caosdb/server/datatype/CollectionValue.java b/src/main/java/org/caosdb/server/datatype/CollectionValue.java
index 55cbf0489c3f965e19784c80439d291930d89841..fc94f660490284e560a34d1c407e2d74c51f6358 100644
--- a/src/main/java/org/caosdb/server/datatype/CollectionValue.java
+++ b/src/main/java/org/caosdb/server/datatype/CollectionValue.java
@@ -20,6 +20,8 @@
  *
  * ** end header
  */
+
+/** @review Daniel Hornung 2022-03-04 */
 package org.caosdb.server.datatype;
 
 import java.util.ArrayList;
@@ -80,4 +82,24 @@ public class CollectionValue implements Value, Iterable<IndexedSingleValue> {
   public int size() {
     return list.size();
   }
+
+  /** Compares if the content is equal, regardless of the order. */
+  @Override
+  public boolean equals(Value val) {
+    if (val instanceof CollectionValue) {
+      CollectionValue that = (CollectionValue) val;
+      sort();
+      that.sort();
+      return this.list.equals(that.list);
+    }
+    return false;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof Value) {
+      return this.equals((Value) obj);
+    }
+    return false;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/datatype/GenericValue.java b/src/main/java/org/caosdb/server/datatype/GenericValue.java
index e21841ee782a2ff86d2d993bbb0b9901a7835667..a4b1b0b1508b9debde1f34f1dd58786bdae840c8 100644
--- a/src/main/java/org/caosdb/server/datatype/GenericValue.java
+++ b/src/main/java/org/caosdb/server/datatype/GenericValue.java
@@ -58,10 +58,14 @@ public class GenericValue implements SingleValue {
     this.table = Table.text_data;
   }
 
+  public GenericValue(final long value) {
+    this((Integer) Math.toIntExact(value));
+  }
+
   @Override
   public void addToElement(final Element e) {
     if (this.value instanceof String && ((String) this.value).isEmpty()) {
-      Element empty = new Element("EmptyString");
+      final Element empty = new Element("EmptyString");
       e.addContent(empty);
     } else {
       e.addContent(this.value.toString());
@@ -85,10 +89,22 @@ public class GenericValue implements SingleValue {
 
   @Override
   public boolean equals(final Object obj) {
-    if (obj instanceof GenericValue) {
-      final GenericValue that = (GenericValue) obj;
+    if (obj instanceof Value) {
+      return equals((Value) obj);
+    }
+    return false;
+  }
+
+  @Override
+  public boolean equals(Value val) {
+    if (val instanceof GenericValue) {
+      final GenericValue that = (GenericValue) val;
       return Objects.equal(that.value, this.value);
     }
     return false;
   }
+
+  public Object getValue() {
+    return this.value;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/datatype/IndexedSingleValue.java b/src/main/java/org/caosdb/server/datatype/IndexedSingleValue.java
index a9ed584b3ee858e6de2bbb84ad4a43da820cedb0..ec9280c3ab0cbdf6e577feec4d40a01f6402dbfe 100644
--- a/src/main/java/org/caosdb/server/datatype/IndexedSingleValue.java
+++ b/src/main/java/org/caosdb/server/datatype/IndexedSingleValue.java
@@ -22,6 +22,7 @@
  */
 package org.caosdb.server.datatype;
 
+import java.util.Objects;
 import org.caosdb.server.datatype.AbstractDatatype.Table;
 import org.jdom2.Element;
 
@@ -78,4 +79,21 @@ public class IndexedSingleValue implements SingleValue, Comparable<IndexedSingle
   public SingleValue getWrapped() {
     return this.wrapped;
   }
+
+  @Override
+  public boolean equals(Value val) {
+    if (val instanceof IndexedSingleValue) {
+      IndexedSingleValue that = (IndexedSingleValue) val;
+      return Objects.equals(that.wrapped, this.wrapped);
+    }
+    return false;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof Value) {
+      return this.equals((Value) obj);
+    }
+    return false;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/datatype/IntegerDatatype.java b/src/main/java/org/caosdb/server/datatype/IntegerDatatype.java
index b5ac2d971c7edd1eaf3947eacf1d1f94f967d808..0b1116ed5cbc135f846b5c7fbc0e1d497576380f 100644
--- a/src/main/java/org/caosdb/server/datatype/IntegerDatatype.java
+++ b/src/main/java/org/caosdb/server/datatype/IntegerDatatype.java
@@ -32,10 +32,13 @@ public class IntegerDatatype extends AbstractDatatype {
   public SingleValue parseValue(final Object value) throws Message {
     try {
       if (value instanceof GenericValue) {
-        return new GenericValue(Integer.parseInt(((GenericValue) value).toDatabaseString()));
+        return new GenericValue(Long.parseLong(((GenericValue) value).toDatabaseString()));
       } else {
-        return new GenericValue(Integer.parseInt(value.toString()));
+        return new GenericValue(Long.parseLong(value.toString()));
       }
+
+    } catch (final ArithmeticException e) {
+      throw ServerMessages.INTEGER_OUT_OF_RANGE;
     } catch (final NumberFormatException e) {
       throw ServerMessages.CANNOT_PARSE_INT_VALUE;
     }
diff --git a/src/main/java/org/caosdb/server/datatype/ReferenceDatatype.java b/src/main/java/org/caosdb/server/datatype/ReferenceDatatype.java
index 42f60cebc116d23137d359c11015a2978233234e..769d3ebdd109e22dcbc41f2ae7bd478b709f0482 100644
--- a/src/main/java/org/caosdb/server/datatype/ReferenceDatatype.java
+++ b/src/main/java/org/caosdb/server/datatype/ReferenceDatatype.java
@@ -27,9 +27,6 @@ import org.caosdb.server.entity.Message;
 @DatatypeDefinition(name = "Reference")
 public class ReferenceDatatype extends AbstractDatatype {
 
-  public static final Message REFERENCE_ID_NOT_PARSABLE =
-      new Message(217, "The reference is not parsable. It must be an integer.");
-
   @Override
   public ReferenceValue parseValue(final Object value) throws Message {
     return ReferenceValue.parseReference(value);
diff --git a/src/main/java/org/caosdb/server/datatype/ReferenceValue.java b/src/main/java/org/caosdb/server/datatype/ReferenceValue.java
index 89601b50a46cbf838a995a222b0d4c43150f8a19..525d3c43160adc4b47925c19cafe2acd2b6b2246 100644
--- a/src/main/java/org/caosdb/server/datatype/ReferenceValue.java
+++ b/src/main/java/org/caosdb/server/datatype/ReferenceValue.java
@@ -4,8 +4,9 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  *   Max-Planck-Institute for Dynamics and Self-Organization Göttingen
- * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2022 IndiScale GmbH <info@indiscale.com>
  * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2022 Daniel Hornung <d.hornung@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
@@ -22,6 +23,8 @@
  *
  * ** end header
  */
+
+/** @review Daniel Hornung 2022-03-04 */
 package org.caosdb.server.datatype;
 
 import java.util.Objects;
@@ -199,13 +202,29 @@ public class ReferenceValue implements SingleValue {
 
   @Override
   public boolean equals(final Object obj) {
-    if (obj instanceof ReferenceValue) {
-      final ReferenceValue that = (ReferenceValue) obj;
+    if (obj instanceof Value) {
+      return equals((Value) obj);
+    }
+    return false;
+  }
+
+  /**
+   * Test if this is equal to the other object.
+   *
+   * <p>Two references are equal, if 1) they both have IDs and their content is equal or 2) at least
+   * one does not have an ID, but their names are equal. Otherwise they are considered unequal.
+   *
+   * @param val The other object.
+   */
+  @Override
+  public boolean equals(Value val) {
+    if (val instanceof ReferenceValue) {
+      final ReferenceValue that = (ReferenceValue) val;
       if (that.getId() != null && getId() != null) {
         return that.getId().equals(getId())
             && Objects.deepEquals(that.getVersion(), this.getVersion());
       } else if (that.getName() != null && getName() != null) {
-        return that.getName().equals(getName());
+        return Objects.equals(that.getName(), this.getName());
       }
     }
     return false;
diff --git a/src/main/java/org/caosdb/server/datatype/Value.java b/src/main/java/org/caosdb/server/datatype/Value.java
index 540e6bc52b72744c4d7c1c651c40924d1da57c6d..a2612fb37dae11953b6083f3be8132b2dd61f77f 100644
--- a/src/main/java/org/caosdb/server/datatype/Value.java
+++ b/src/main/java/org/caosdb/server/datatype/Value.java
@@ -26,4 +26,6 @@ import org.jdom2.Element;
 
 public interface Value {
   public void addToElement(Element e);
+
+  public abstract boolean equals(Value val);
 }
diff --git a/src/main/java/org/caosdb/server/entity/Entity.java b/src/main/java/org/caosdb/server/entity/Entity.java
index 0a60f9af59f731098a0e194e4848b578094d4e68..07c451a328b8a1a6d52810f25f78276699fce797 100644
--- a/src/main/java/org/caosdb/server/entity/Entity.java
+++ b/src/main/java/org/caosdb/server/entity/Entity.java
@@ -51,7 +51,7 @@ import org.caosdb.server.entity.wrapper.Domain;
 import org.caosdb.server.entity.wrapper.Parent;
 import org.caosdb.server.entity.wrapper.Property;
 import org.caosdb.server.entity.xml.EntityToElementStrategy;
-import org.caosdb.server.entity.xml.SetFieldStrategy;
+import org.caosdb.server.entity.xml.SerializeFieldStrategy;
 import org.caosdb.server.entity.xml.ToElementStrategy;
 import org.caosdb.server.entity.xml.ToElementable;
 import org.caosdb.server.permissions.EntityACL;
@@ -537,27 +537,26 @@ public class Entity extends AbstractObservable implements EntityInterface {
 
   @Override
   public final Element toElement() {
-    return getToElementStrategy().toElement(this, new SetFieldStrategy(getSelections()));
+    return getToElementStrategy().toElement(this, getSerializeFieldStrategy());
   }
 
   @Override
-  public final void addToElement(final Element element) {
-    addToElement(element, new SetFieldStrategy(getSelections()));
+  public SerializeFieldStrategy getSerializeFieldStrategy() {
+    // @review Florian Spreckelsen 2022-03-22
+    if (this.serializeFieldStrategy == null) {
+      this.serializeFieldStrategy = new SerializeFieldStrategy(getSelections());
+    }
+    return this.serializeFieldStrategy;
   }
 
   @Override
-  public void addToElement(Element element, SetFieldStrategy strategy) {
-    getToElementStrategy().addToElement(this, element, strategy);
+  public final void addToElement(final Element element) {
+    addToElement(element, getSerializeFieldStrategy());
   }
 
-  /**
-   * Print this entity to the standard outputs. Just for debugging.
-   *
-   * @throws CaosDBException
-   */
   @Override
-  public void print() {
-    print("");
+  public void addToElement(Element element, SerializeFieldStrategy strategy) {
+    getToElementStrategy().addToElement(this, element, strategy);
   }
 
   @Override
@@ -565,64 +564,6 @@ public class Entity extends AbstractObservable implements EntityInterface {
     return 0;
   }
 
-  @Override
-  public void print(final String indent) {
-    System.out.println(
-        indent
-            + "+---| "
-            + this.getClass().getSimpleName()
-            + " |----------------------------------");
-    if (getDomain() != 0) {
-      System.out.println(indent + "|      Domain: " + Integer.toString(getDomain()));
-    }
-    if (hasId()) {
-      System.out.println(indent + "|          ID: " + Integer.toString(getId()));
-    }
-    if (hasCuid()) {
-      System.out.println(indent + "|        Cuid: " + getCuid());
-    }
-    if (hasName()) {
-      System.out.println(indent + "|        Name: " + getName());
-    }
-    if (hasDescription()) {
-      System.out.println(indent + "| Description: " + getDescription());
-    }
-    if (hasRole()) {
-      System.out.println(indent + "|        Role: " + getRole());
-    }
-    if (hasStatementStatus()) {
-      System.out.println(indent + "|   Statement: " + getStatementStatus().toString());
-    }
-    if (hasDatatype()) {
-      System.out.println(indent + "|    Datatype: " + getDatatype().toString());
-    }
-    if (hasValue()) {
-      System.out.println(indent + "|       Value: " + getValue().toString());
-    }
-    if (hasEntityStatus()) {
-      System.out.println(indent + "|      Entity: " + getEntityStatus().toString());
-    }
-    if (hasFileProperties()) {
-      getFileProperties().print(indent);
-    }
-    System.out.println(indent + "+-----------------------------------");
-    for (final ToElementable m : getMessages()) {
-      if (m instanceof Message) {
-        ((Message) m).print(indent + "|   ");
-      }
-    }
-    for (final EntityInterface p : getParents()) {
-      // p.print(indent + "| ");
-      System.out.println(indent + "|      Parent: " + p.getName());
-    }
-    for (final EntityInterface s : getProperties()) {
-      s.print(indent + "|   ");
-    }
-    if (indent.equals("")) {
-      System.out.println(indent + "+------------------------------------");
-    }
-  }
-
   /** Errors, Warnings and Info messages for this entity. */
   private final Set<ToElementable> messages = new HashSet<>();
 
@@ -662,18 +603,6 @@ public class Entity extends AbstractObservable implements EntityInterface {
     return ret;
   }
 
-  @Override
-  public final Message getMessage(final String type, final Integer code) {
-    for (final ToElementable m : this.messages) {
-      if (m instanceof Message
-          && ((Message) m).getType().equalsIgnoreCase(type)
-          && ((Message) m).getCode() == code) {
-        return (Message) m;
-      }
-    }
-    return null;
-  }
-
   @Override
   public final void addMessage(final ToElementable m) {
     this.messages.add(m);
@@ -691,7 +620,7 @@ public class Entity extends AbstractObservable implements EntityInterface {
 
   @Override
   public void addInfo(final String description) {
-    final Message m = new Message(MessageType.Info, 0, description);
+    final Message m = new Message(description);
     addMessage(m);
   }
 
@@ -712,7 +641,6 @@ public class Entity extends AbstractObservable implements EntityInterface {
     if (!this.isParsed) {
       this.isParsed = true;
       setValue(getDatatype().parseValue(getValue()));
-      this.isParsed = true;
     }
   }
 
@@ -886,23 +814,23 @@ public class Entity extends AbstractObservable implements EntityInterface {
     }
 
     // Parse TMPIDENTIFYER.
-    String tmpIdentifyer = null;
+    String tmpIdentifier = null;
     boolean pickup = false;
     if (element.getAttribute("pickup") != null && !element.getAttributeValue("pickup").equals("")) {
-      tmpIdentifyer = element.getAttributeValue("pickup");
+      tmpIdentifier = element.getAttributeValue("pickup");
       pickup = true;
     } else if (element.getAttribute("upload") != null
         && !element.getAttributeValue("upload").equals("")) {
-      tmpIdentifyer = element.getAttributeValue("upload");
+      tmpIdentifier = element.getAttributeValue("upload");
     }
-    if (tmpIdentifyer != null && tmpIdentifyer.endsWith("/")) {
-      tmpIdentifyer = tmpIdentifyer.substring(0, tmpIdentifyer.length() - 1);
+    if (tmpIdentifier != null && tmpIdentifier.endsWith("/")) {
+      tmpIdentifier = tmpIdentifier.substring(0, tmpIdentifier.length() - 1);
     }
 
     // Store PATH, HASH, SIZE, TMPIDENTIFYER
-    if (tmpIdentifyer != null || checksum != null || path != null || size != null) {
+    if (tmpIdentifier != null || checksum != null || path != null || size != null) {
       setFileProperties(
-          new FileProperties(checksum, path, size, tmpIdentifyer).setPickupable(pickup));
+          new FileProperties(checksum, path, size, tmpIdentifier).setPickupable(pickup));
     }
 
     // Parse flags
@@ -1011,6 +939,7 @@ public class Entity extends AbstractObservable implements EntityInterface {
 
   private boolean datatypeOverride = false;
   private Version version = new Version();
+  private SerializeFieldStrategy serializeFieldStrategy = null;
 
   @Override
   public EntityInterface setDatatypeOverride(final boolean b) {
@@ -1148,4 +1077,10 @@ public class Entity extends AbstractObservable implements EntityInterface {
         && this.getDatatype() instanceof AbstractCollectionDatatype
         && ((AbstractCollectionDatatype) getDatatype()).getDatatype() instanceof ReferenceDatatype;
   }
+
+  @Override
+  public void setSerializeFieldStrategy(SerializeFieldStrategy s) {
+    // @review Florian Spreckelsen 2022-03-22
+    this.serializeFieldStrategy = s;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/entity/EntityInterface.java b/src/main/java/org/caosdb/server/entity/EntityInterface.java
index 954bcc47ca5b6117d3164ba3c9c585dbd373d4f8..cfb4c28050f871684044c6216081101759a1cf17 100644
--- a/src/main/java/org/caosdb/server/entity/EntityInterface.java
+++ b/src/main/java/org/caosdb/server/entity/EntityInterface.java
@@ -34,7 +34,7 @@ import org.caosdb.server.entity.container.PropertyContainer;
 import org.caosdb.server.entity.wrapper.Domain;
 import org.caosdb.server.entity.wrapper.Parent;
 import org.caosdb.server.entity.wrapper.Property;
-import org.caosdb.server.entity.xml.SetFieldStrategy;
+import org.caosdb.server.entity.xml.SerializeFieldStrategy;
 import org.caosdb.server.entity.xml.ToElementable;
 import org.caosdb.server.jobs.JobTarget;
 import org.caosdb.server.permissions.EntityACL;
@@ -112,10 +112,6 @@ public interface EntityInterface
 
   public abstract boolean hasDatatype();
 
-  public abstract void print();
-
-  public abstract void print(String indent);
-
   public abstract FileProperties getFileProperties();
 
   public abstract void setFileProperties(FileProperties fileProperties);
@@ -193,7 +189,7 @@ public interface EntityInterface
 
   public abstract void setVersion(Version version);
 
-  public abstract void addToElement(Element element, SetFieldStrategy strategy);
+  public abstract void addToElement(Element element, SerializeFieldStrategy strategy);
 
   /** Return true iff the data type is present and is an instance of ReferenceDatatype. */
   public abstract boolean isReference();
@@ -203,4 +199,6 @@ public interface EntityInterface
    * AbstractCollectionDatatype's elements' data type is an instance of ReferenceDatatype.
    */
   public abstract boolean isReferenceList();
+
+  public abstract SerializeFieldStrategy getSerializeFieldStrategy();
 }
diff --git a/src/main/java/org/caosdb/server/entity/FileProperties.java b/src/main/java/org/caosdb/server/entity/FileProperties.java
index c2c2c10fae44d4d84af56a33a052f5ed589debed..eaf4246f6c16a0f7cb38964e0ffb12757a4f0373 100644
--- a/src/main/java/org/caosdb/server/entity/FileProperties.java
+++ b/src/main/java/org/caosdb/server/entity/FileProperties.java
@@ -38,7 +38,7 @@ public class FileProperties {
   private String checksum = null;
   private String path = null;
   private Long size = null;
-  private String tmpIdentifyer = null;
+  private String tmpIdentifier = null;
 
   public FileProperties setChecksum(final String checksum) {
     this.checksum = checksum;
@@ -96,26 +96,11 @@ public class FileProperties {
   }
 
   public FileProperties(
-      final String checksum, final String path, final Long size, final String tmpIdentifyer) {
+      final String checksum, final String path, final Long size, final String tmpIdentifier) {
     this.checksum = checksum;
     this.path = (path == null ? null : path.replaceFirst("^/", ""));
     this.size = size;
-    this.tmpIdentifyer = tmpIdentifyer;
-  }
-
-  public void print(final String indent) {
-    if (hasChecksum()) {
-      System.out.println(indent + "|    Checksum: " + this.checksum);
-    }
-    if (hasPath()) {
-      System.out.println(indent + "|        Path: " + "/" + this.path);
-    }
-    if (hasSize()) {
-      System.out.println(indent + "|        Size: " + Long.toString(this.size));
-    }
-    if (getFile() != null) {
-      System.out.println(indent + "|        File: " + getFile().getAbsolutePath());
-    }
+    this.tmpIdentifier = tmpIdentifier;
   }
 
   public FileProperties setFile(final File file) {
@@ -127,13 +112,13 @@ public class FileProperties {
     return this.file;
   }
 
-  public FileProperties setTmpIdentifyer(final String tmpIdentifyer) {
-    this.tmpIdentifyer = tmpIdentifyer;
+  public FileProperties setTmpIdentifier(final String tmpIdentifier) {
+    this.tmpIdentifier = tmpIdentifier;
     return this;
   }
 
-  public String getTmpIdentifyer() {
-    return this.tmpIdentifyer;
+  public String getTmpIdentifier() {
+    return this.tmpIdentifier;
   }
 
   private String getThumbnailPath(final File target) {
@@ -291,6 +276,6 @@ public class FileProperties {
   }
 
   public boolean hasTmpIdentifier() {
-    return this.tmpIdentifyer != null;
+    return this.tmpIdentifier != null;
   }
 }
diff --git a/src/main/java/org/caosdb/server/entity/MagicTypes.java b/src/main/java/org/caosdb/server/entity/MagicTypes.java
index cffdb0723588d3aaec31e24a39895e304aa2e18b..f17d448ca934901dc491f1d737f9122e2a67862a 100644
--- a/src/main/java/org/caosdb/server/entity/MagicTypes.java
+++ b/src/main/java/org/caosdb/server/entity/MagicTypes.java
@@ -26,6 +26,7 @@ import java.util.HashMap;
 import org.caosdb.server.entity.container.RetrieveContainer;
 import org.caosdb.server.transaction.Retrieve;
 
+/** Some types correspond to entities in the database with magic IDs. */
 public enum MagicTypes {
   UNIT,
   NAME,
diff --git a/src/main/java/org/caosdb/server/entity/Message.java b/src/main/java/org/caosdb/server/entity/Message.java
index 91720aed492a8b1875bb5613df2aaace7df3bef5..fca6024561ede97c1b5520bc6a07f08f30f7a76a 100644
--- a/src/main/java/org/caosdb/server/entity/Message.java
+++ b/src/main/java/org/caosdb/server/entity/Message.java
@@ -19,19 +19,49 @@
  * 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/>.
  */
+
 package org.caosdb.server.entity;
 
+import java.util.HashMap;
+import java.util.Map;
+import org.caosdb.api.entity.v1.MessageCode;
 import org.caosdb.server.entity.xml.ToElementable;
+import org.caosdb.server.utils.ServerMessages;
 import org.jdom2.Element;
 
 public class Message extends Exception implements Comparable<Message>, ToElementable {
 
   protected final String type;
-  private final Integer code;
+  private final MessageCode code;
   private final String description;
   private final String body;
 
-  private static final long serialVersionUID = -3005017964769041935L;
+  @Override
+  public String getMessage() {
+    return description;
+  }
+
+  @Deprecated private static final Map<String, String> legacy_codes_mapping = new HashMap<>();
+
+  static void init() {
+    legacy_codes_mapping.put(
+        ServerMessages.ENTITY_HAS_BEEN_DELETED_SUCCESSFULLY.coreToString(), "10");
+    legacy_codes_mapping.put(ServerMessages.ATOMICITY_ERROR.coreToString(), "12");
+    legacy_codes_mapping.put(ServerMessages.ENTITY_DOES_NOT_EXIST.coreToString(), "101");
+    legacy_codes_mapping.put(ServerMessages.PROPERTY_HAS_NO_DATATYPE.coreToString(), "110");
+    legacy_codes_mapping.put(
+        ServerMessages.ENTITY_HAS_UNQUALIFIED_PROPERTIES.coreToString(), "114");
+    legacy_codes_mapping.put(ServerMessages.ENTITY_HAS_UNQUALIFIED_PARENTS.coreToString(), "116");
+    legacy_codes_mapping.put(ServerMessages.WARNING_OCCURED.coreToString(), "128");
+    legacy_codes_mapping.put(ServerMessages.ENTITY_NAME_IS_NOT_UNIQUE.coreToString(), "152");
+    legacy_codes_mapping.put(ServerMessages.REQUIRED_BY_UNQUALIFIED.coreToString(), "192");
+    legacy_codes_mapping.put(ServerMessages.REFERENCED_ENTITY_DOES_NOT_EXIST.coreToString(), "235");
+    legacy_codes_mapping.put(ServerMessages.AUTHORIZATION_ERROR.coreToString(), "403");
+    legacy_codes_mapping.put(ServerMessages.ROLE_DOES_NOT_EXIST.coreToString(), "1104");
+    legacy_codes_mapping.put(ServerMessages.ENTITY_NAME_DUPLICATES.coreToString(), "0");
+  }
+
+  private static final long serialVersionUID = 1837110172902899264L;
 
   public enum MessageType {
     Warning,
@@ -44,15 +74,18 @@ public class Message extends Exception implements Comparable<Message>, ToElement
     return toString().hashCode();
   }
 
-  @Override
-  public String toString() {
-    return this.type.toString()
-        + " ("
-        + (this.code != null ? Integer.toString(this.code) : "")
+  private String coreToString() {
+    return " ("
+        + (this.code != null ? this.code.toString() : "0")
         + ") - "
         + (this.description != null ? this.description : "");
   }
 
+  @Override
+  public String toString() {
+    return this.type.toString() + coreToString();
+  }
+
   @Override
   public boolean equals(final Object obj) {
     return obj.toString().equals(toString());
@@ -74,33 +107,33 @@ public class Message extends Exception implements Comparable<Message>, ToElement
     this(type, null, description, body);
   }
 
-  public Message(final String type, final Integer code) {
+  public Message(final String type, final MessageCode code) {
     this(type, code, null, null);
   }
 
-  public Message(Integer code, String description) {
+  public Message(final MessageCode code, final String description) {
     this(MessageType.Info, code, description);
   }
 
-  public Message(final MessageType type, final Integer code, final String description) {
+  public Message(final MessageType type, final MessageCode code, final String description) {
     this(type.toString(), code, description, null);
   }
 
   public Message(
-      final MessageType type, final Integer code, final String description, final String body) {
+      final MessageType type, final MessageCode code, final String description, final String body) {
     this(type.toString(), code, description, body);
   }
 
-  public Message(final String type, final Integer code, final String description) {
+  public Message(final String type, final MessageCode code, final String description) {
     this(type, code, description, null);
   }
 
-  public Message(MessageType type, String description) {
-    this(type.toString(), 0, description);
+  public Message(final MessageType type, final String description) {
+    this(type.toString(), MessageCode.MESSAGE_CODE_UNKNOWN, description);
   }
 
   public Message(
-      final String type, final Integer code, final String description, final String body) {
+      final String type, final MessageCode code, final String description, final String body) {
     this.code = code;
     this.description = description;
     this.body = body;
@@ -108,7 +141,7 @@ public class Message extends Exception implements Comparable<Message>, ToElement
   }
 
   public Message(
-      final Integer code, final String description, final String body, final MessageType type) {
+      final MessageCode code, final String description, final String body, final MessageType type) {
     this.code = code;
     this.description = description;
     this.body = body;
@@ -116,20 +149,29 @@ public class Message extends Exception implements Comparable<Message>, ToElement
   }
 
   public Message(final String string) {
-    this.code = null;
+    this.code = MessageCode.MESSAGE_CODE_UNKNOWN;
     this.description = string;
     this.body = null;
     this.type = MessageType.Info.toString();
   }
 
-  public Integer getCode() {
+  public MessageCode getCode() {
     return this.code;
   }
 
   public Element toElement() {
+    if (legacy_codes_mapping.isEmpty()) {
+      init();
+    }
     final Element e = new Element(this.type);
     if (this.code != null) {
-      e.setAttribute("code", Integer.toString(this.code));
+      if (legacy_codes_mapping.containsKey(this.coreToString())) {
+        e.setAttribute("code", legacy_codes_mapping.get(this.coreToString()));
+      } else if (this.code == MessageCode.MESSAGE_CODE_UNKNOWN) {
+        e.setAttribute("code", "0");
+      } else {
+        e.setAttribute("code", Integer.toString(this.code.getNumber()));
+      }
     }
     if (this.description != null) {
       e.setAttribute("description", this.description);
@@ -146,20 +188,6 @@ public class Message extends Exception implements Comparable<Message>, ToElement
     parent.addContent(e);
   }
 
-  /** Print this entity to the standard outputs. Just for debugging. */
-  public final void print() {
-    print("");
-  }
-
-  public final void print(final String indent) {
-    System.out.println(indent + "+---| " + this.type + " |------------------------ ");
-    System.out.println(
-        indent + "|        Code: " + (this.code != null ? Integer.toString(this.code) : "null"));
-    System.out.println(indent + "| Description: " + this.description);
-    System.out.println(indent + "|        Body: " + this.body);
-    System.out.println(indent + "+------------------------------------------------------ ");
-  }
-
   @Override
   public int compareTo(final Message o) {
     final int tc = this.type.compareToIgnoreCase(o.type);
diff --git a/src/main/java/org/caosdb/server/entity/TransactionEntity.java b/src/main/java/org/caosdb/server/entity/TransactionEntity.java
index 2883c076f437520581037219a724020b47893424..976106223800802cd3e885baaad5d0113d9726da 100644
--- a/src/main/java/org/caosdb/server/entity/TransactionEntity.java
+++ b/src/main/java/org/caosdb/server/entity/TransactionEntity.java
@@ -25,6 +25,7 @@ package org.caosdb.server.entity;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.caosdb.server.entity.xml.SerializeFieldStrategy;
 import org.caosdb.server.entity.xml.ToElementStrategy;
 import org.caosdb.server.entity.xml.ToElementable;
 import org.caosdb.server.query.Query.Selection;
@@ -47,6 +48,8 @@ public interface TransactionEntity {
 
   public abstract void setToElementStragegy(ToElementStrategy s);
 
+  public abstract void setSerializeFieldStrategy(SerializeFieldStrategy s);
+
   public abstract Element toElement();
 
   public abstract Set<ToElementable> getMessages();
@@ -59,8 +62,6 @@ public interface TransactionEntity {
 
   public abstract List<Message> getMessages(String type);
 
-  public abstract Message getMessage(String type, Integer code);
-
   public abstract void addMessage(ToElementable m);
 
   public abstract void addError(Message m);
diff --git a/src/main/java/org/caosdb/server/entity/UpdateEntity.java b/src/main/java/org/caosdb/server/entity/UpdateEntity.java
index 221888b3da57694e5eabb64d0955cd6fca5e0b0e..c5727e8257e4826b25408f35577eee4bc588e6f5 100644
--- a/src/main/java/org/caosdb/server/entity/UpdateEntity.java
+++ b/src/main/java/org/caosdb/server/entity/UpdateEntity.java
@@ -40,12 +40,16 @@ public class UpdateEntity extends WritableEntity {
     super(element);
   }
 
+  public UpdateEntity(final Integer id, final Role role) {
+    super(id, role);
+  }
+
   @Override
   public boolean skipJob() {
     return getEntityStatus() != EntityStatus.QUALIFIED;
   }
 
-  public void setOriginal(EntityInterface original) {
+  public void setOriginal(final EntityInterface original) {
     this.original = original;
   }
 
diff --git a/src/main/java/org/caosdb/server/entity/WritableEntity.java b/src/main/java/org/caosdb/server/entity/WritableEntity.java
index 22ffd6e869e871cf568e87363e5ead762e444400..147feb0c43de65e9fa204207b29ae27df4251721 100644
--- a/src/main/java/org/caosdb/server/entity/WritableEntity.java
+++ b/src/main/java/org/caosdb/server/entity/WritableEntity.java
@@ -29,7 +29,11 @@ public class WritableEntity extends Entity {
     super(element);
   }
 
-  public WritableEntity(String name, Role role) {
+  public WritableEntity(final String name, final Role role) {
     super(name, role);
   }
+
+  public WritableEntity(final Integer id, final Role role) {
+    super(id, role);
+  }
 }
diff --git a/src/main/java/org/caosdb/server/entity/container/ParentContainer.java b/src/main/java/org/caosdb/server/entity/container/ParentContainer.java
index 90c6078a47471712ac51448ac4aac3d05794f5a6..896a355c5f2df67a542ec17b0710777c543313ef 100644
--- a/src/main/java/org/caosdb/server/entity/container/ParentContainer.java
+++ b/src/main/java/org/caosdb/server/entity/container/ParentContainer.java
@@ -26,7 +26,7 @@ import org.caosdb.server.entity.Entity;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.wrapper.Parent;
 import org.caosdb.server.entity.xml.ParentToElementStrategy;
-import org.caosdb.server.entity.xml.SetFieldStrategy;
+import org.caosdb.server.entity.xml.SerializeFieldStrategy;
 import org.caosdb.server.entity.xml.ToElementStrategy;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.Observable;
@@ -51,10 +51,10 @@ public class ParentContainer extends Container<Parent> {
   }
 
   public Element addToElement(final Element element) {
-    final SetFieldStrategy setFieldStrategy =
-        new SetFieldStrategy(this.child.getSelections()).forProperty("parent");
+    final SerializeFieldStrategy serializeFieldStrategy =
+        new SerializeFieldStrategy(this.child.getSelections()).forProperty("parent");
     for (final EntityInterface entity : this) {
-      s.addToElement(entity, element, setFieldStrategy);
+      s.addToElement(entity, element, serializeFieldStrategy);
     }
     return element;
   }
diff --git a/src/main/java/org/caosdb/server/entity/container/PropertyContainer.java b/src/main/java/org/caosdb/server/entity/container/PropertyContainer.java
index f5cce963fa4611bbdebfacdb11f98497c2a68544..2a6593d1e801cdd795756d58cc38e66b8f3b919b 100644
--- a/src/main/java/org/caosdb/server/entity/container/PropertyContainer.java
+++ b/src/main/java/org/caosdb/server/entity/container/PropertyContainer.java
@@ -31,7 +31,7 @@ import org.caosdb.server.entity.Entity;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.wrapper.Property;
 import org.caosdb.server.entity.xml.PropertyToElementStrategy;
-import org.caosdb.server.entity.xml.SetFieldStrategy;
+import org.caosdb.server.entity.xml.SerializeFieldStrategy;
 import org.caosdb.server.entity.xml.ToElementStrategy;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.Observable;
@@ -72,12 +72,12 @@ public class PropertyContainer extends Container<Property> {
    *
    * @param property
    * @param element
-   * @param setFieldStrategy
+   * @param serializeFieldStrategy
    */
   public void addToElement(
-      EntityInterface property, Element element, SetFieldStrategy setFieldStrategy) {
-    if (setFieldStrategy.isToBeSet(property.getName())) {
-      SetFieldStrategy strategy = setFieldStrategy.forProperty(property.getName());
+      EntityInterface property, Element element, SerializeFieldStrategy serializeFieldStrategy) {
+    if (serializeFieldStrategy.isToBeSet(property.getName())) {
+      SerializeFieldStrategy strategy = serializeFieldStrategy.forProperty(property.getName());
       this.s.addToElement(property, element, strategy);
     }
   }
@@ -86,12 +86,13 @@ public class PropertyContainer extends Container<Property> {
    * Add all properties to the element using the given setFieldStrategy.
    *
    * @param element
-   * @param setFieldStrategy
+   * @param serializeFieldStrategy
    */
-  public void addToElement(final Element element, final SetFieldStrategy setFieldStrategy) {
+  public void addToElement(
+      final Element element, final SerializeFieldStrategy serializeFieldStrategy) {
     sort();
     for (final EntityInterface property : this) {
-      addToElement(property, element, setFieldStrategy);
+      addToElement(property, element, serializeFieldStrategy);
     }
   }
 
diff --git a/src/main/java/org/caosdb/server/entity/container/TransactionContainer.java b/src/main/java/org/caosdb/server/entity/container/TransactionContainer.java
index 18a7837df27b39efa92abb2b16943a3476dca9ec..7e9a2ba36bc1fe7474e8dc57a9fe3e2d5e649c6f 100644
--- a/src/main/java/org/caosdb/server/entity/container/TransactionContainer.java
+++ b/src/main/java/org/caosdb/server/entity/container/TransactionContainer.java
@@ -114,15 +114,6 @@ public class TransactionContainer extends Container<EntityInterface>
     }
   }
 
-  public void print() {
-    System.out.println("*******************************************************************");
-    System.out.println("*******************************************************************");
-    for (final EntityInterface e : this) {
-      e.print("*");
-    }
-    System.out.println("*******************************************************************\n\n");
-  }
-
   /** The files that have been uploaded. */
   private HashMap<String, FileProperties> files = new HashMap<String, FileProperties>();
 
diff --git a/src/main/java/org/caosdb/server/entity/wrapper/EntityWrapper.java b/src/main/java/org/caosdb/server/entity/wrapper/EntityWrapper.java
index c83cb67883f5a616109268fb6d62bae40cd73e6b..2028bee793d1f81525d3e1a86ebd22b7e5d46963 100644
--- a/src/main/java/org/caosdb/server/entity/wrapper/EntityWrapper.java
+++ b/src/main/java/org/caosdb/server/entity/wrapper/EntityWrapper.java
@@ -41,7 +41,7 @@ import org.caosdb.server.entity.StatementStatus;
 import org.caosdb.server.entity.Version;
 import org.caosdb.server.entity.container.ParentContainer;
 import org.caosdb.server.entity.container.PropertyContainer;
-import org.caosdb.server.entity.xml.SetFieldStrategy;
+import org.caosdb.server.entity.xml.SerializeFieldStrategy;
 import org.caosdb.server.entity.xml.ToElementStrategy;
 import org.caosdb.server.entity.xml.ToElementable;
 import org.caosdb.server.permissions.EntityACL;
@@ -290,16 +290,6 @@ public class EntityWrapper implements EntityInterface {
     this.entity.addToElement(element);
   }
 
-  @Override
-  public void print() {
-    this.entity.print();
-  }
-
-  @Override
-  public void print(final String indent) {
-    this.entity.print(indent);
-  }
-
   @Override
   public FileProperties getFileProperties() {
     return this.entity.getFileProperties();
@@ -340,11 +330,6 @@ public class EntityWrapper implements EntityInterface {
     return this.entity.getMessages(type);
   }
 
-  @Override
-  public Message getMessage(final String type, final Integer code) {
-    return this.entity.getMessage(type, code);
-  }
-
   @Override
   public void addMessage(final ToElementable m) {
     this.entity.addMessage(m);
@@ -577,7 +562,7 @@ public class EntityWrapper implements EntityInterface {
   }
 
   @Override
-  public void addToElement(Element element, SetFieldStrategy strategy) {
+  public void addToElement(Element element, SerializeFieldStrategy strategy) {
     this.entity.addToElement(element, strategy);
   }
 
@@ -590,4 +575,16 @@ public class EntityWrapper implements EntityInterface {
   public boolean isReferenceList() {
     return this.entity.isReferenceList();
   }
+
+  @Override
+  public void setSerializeFieldStrategy(SerializeFieldStrategy s) {
+    // @review Florian Spreckelsen 2022-03-22
+    this.entity.setSerializeFieldStrategy(s);
+  }
+
+  @Override
+  public SerializeFieldStrategy getSerializeFieldStrategy() {
+    // @review Florian Spreckelsen 2022-03-22
+    return this.entity.getSerializeFieldStrategy();
+  }
 }
diff --git a/src/main/java/org/caosdb/server/entity/wrapper/Property.java b/src/main/java/org/caosdb/server/entity/wrapper/Property.java
index b3ce45b56527e40382d3971a61b43f9bc054d21f..064e2bd31fd329477d712c723ecd6462e94d3933 100644
--- a/src/main/java/org/caosdb/server/entity/wrapper/Property.java
+++ b/src/main/java/org/caosdb/server/entity/wrapper/Property.java
@@ -50,6 +50,7 @@ public class Property extends EntityWrapper {
     super(new Entity());
   }
 
+  /** Return the Property Index, the index of a property with respect to a containing Entity. */
   public int getPIdx() {
     return this.pIdx;
   }
@@ -58,6 +59,7 @@ public class Property extends EntityWrapper {
   private EntityInterface domain = null;
   private boolean isName;
 
+  /** Set the Property Index. */
   public void setPIdx(final int i) {
     this.pIdx = i;
   }
diff --git a/src/main/java/org/caosdb/server/entity/xml/DomainToElementStrategy.java b/src/main/java/org/caosdb/server/entity/xml/DomainToElementStrategy.java
index a8b600793a6fdaf8d15ec499112cc06771f66fc3..9480119221aec4fa31c44b812539928327d97ccd 100644
--- a/src/main/java/org/caosdb/server/entity/xml/DomainToElementStrategy.java
+++ b/src/main/java/org/caosdb/server/entity/xml/DomainToElementStrategy.java
@@ -39,9 +39,10 @@ public class DomainToElementStrategy extends EntityToElementStrategy {
   }
 
   @Override
-  public Element toElement(final EntityInterface entity, final SetFieldStrategy setFieldStrategy) {
+  public Element toElement(
+      final EntityInterface entity, final SerializeFieldStrategy serializeFieldStrategy) {
     Element element = new Element(tagName);
-    sparseEntityToElement(element, entity, setFieldStrategy);
+    sparseEntityToElement(element, entity, serializeFieldStrategy);
     return element;
   }
 
@@ -49,8 +50,8 @@ public class DomainToElementStrategy extends EntityToElementStrategy {
   public Element addToElement(
       final EntityInterface entity,
       final Element element,
-      final SetFieldStrategy setFieldStrategy) {
-    element.addContent(toElement(entity, setFieldStrategy));
+      final SerializeFieldStrategy serializeFieldStrategy) {
+    element.addContent(toElement(entity, serializeFieldStrategy));
     return element;
   }
 }
diff --git a/src/main/java/org/caosdb/server/entity/xml/EntityToElementStrategy.java b/src/main/java/org/caosdb/server/entity/xml/EntityToElementStrategy.java
index 89420c596894da9be35e33a507821c96d10f5b7d..a274d269b2281ebed59530728c767cb1bca14f4a 100644
--- a/src/main/java/org/caosdb/server/entity/xml/EntityToElementStrategy.java
+++ b/src/main/java/org/caosdb/server/entity/xml/EntityToElementStrategy.java
@@ -76,41 +76,59 @@ public class EntityToElementStrategy implements ToElementStrategy {
    *
    * @param element
    * @param entity
-   * @param setFieldStrategy
+   * @param serializeFieldStrategy
    */
   public void sparseEntityToElement(
       final Element element,
       final EntityInterface entity,
-      final SetFieldStrategy setFieldStrategy) {
+      final SerializeFieldStrategy serializeFieldStrategy) {
+
+    // @review Florian Spreckelsen 2022-03-22
 
     if (entity.getEntityACL() != null) {
       element.addContent(entity.getEntityACL().getPermissionsFor(SecurityUtils.getSubject()));
     }
-    if (setFieldStrategy.isToBeSet("id") && entity.hasId()) {
+    if (serializeFieldStrategy.isToBeSet("id") && entity.hasId()) {
       element.setAttribute("id", Integer.toString(entity.getId()));
     }
-    if (entity.hasVersion()) {
+    if (serializeFieldStrategy.isToBeSet("version") && entity.hasVersion()) {
       Element v = new VersionXMLSerializer().toElement(entity.getVersion());
       element.addContent(v);
     }
-    if (setFieldStrategy.isToBeSet("cuid") && entity.hasCuid()) {
+    if (serializeFieldStrategy.isToBeSet("cuid") && entity.hasCuid()) {
       element.setAttribute("cuid", entity.getCuid());
     }
-    if (setFieldStrategy.isToBeSet("name") && entity.hasName()) {
+    if (serializeFieldStrategy.isToBeSet("name") && entity.hasName()) {
       element.setAttribute("name", entity.getName());
     }
-    if (setFieldStrategy.isToBeSet("description") && entity.hasDescription()) {
+    if (serializeFieldStrategy.isToBeSet("description") && entity.hasDescription()) {
       element.setAttribute("description", entity.getDescription());
     }
-    if (setFieldStrategy.isToBeSet("datatype") && entity.hasDatatype()) {
+    if (serializeFieldStrategy.isToBeSet("datatype") && entity.hasDatatype()) {
       setDatatype(entity, element);
     }
-    if (setFieldStrategy.isToBeSet("message") && entity.hasMessages()) {
+    if (serializeFieldStrategy.isToBeSet("message") && entity.hasMessages()) {
       for (final ToElementable m : entity.getMessages()) {
         m.addToElement(element);
       }
+    } else {
+      if (serializeFieldStrategy.isToBeSet("error")) {
+        for (ToElementable m : entity.getMessages("error")) {
+          m.addToElement(element);
+        }
+      }
+      if (serializeFieldStrategy.isToBeSet("warning")) {
+        for (ToElementable m : entity.getMessages("warning")) {
+          m.addToElement(element);
+        }
+      }
+      if (serializeFieldStrategy.isToBeSet("info")) {
+        for (ToElementable m : entity.getMessages("info")) {
+          m.addToElement(element);
+        }
+      }
     }
-    if (setFieldStrategy.isToBeSet("query") && entity.getQueryTemplateDefinition() != null) {
+    if (serializeFieldStrategy.isToBeSet("query") && entity.getQueryTemplateDefinition() != null) {
       final Element q = new Element("Query");
       q.setText(entity.getQueryTemplateDefinition());
       element.addContent(q);
@@ -127,9 +145,10 @@ public class EntityToElementStrategy implements ToElementStrategy {
    *
    * @param entity
    * @param element
-   * @param setFieldStrategy
+   * @param serializeFieldStrategy
    */
-  public void setValue(EntityInterface entity, Element element, SetFieldStrategy setFieldStrategy) {
+  public void setValue(
+      EntityInterface entity, Element element, SerializeFieldStrategy serializeFieldStrategy) {
     if (entity.hasValue()) {
       try {
         entity.parseValue();
@@ -139,7 +158,7 @@ public class EntityToElementStrategy implements ToElementStrategy {
         // CheckValueParsable job.
       }
 
-      if (entity.isReference() && setFieldStrategy.isToBeSet("_referenced")) {
+      if (entity.isReference() && serializeFieldStrategy.isToBeSet("_referenced")) {
         // Append the complete entity. This needs to be done when we are
         // processing SELECT Queries.
         EntityInterface ref = ((ReferenceValue) entity.getValue()).getEntity();
@@ -147,12 +166,12 @@ public class EntityToElementStrategy implements ToElementStrategy {
           if (entity.hasDatatype()) {
             setDatatype(entity, element);
           }
-          ref.addToElement(element, setFieldStrategy);
+          ref.addToElement(element, serializeFieldStrategy);
           // the referenced entity has been appended. Return here to suppress
           // adding the reference id as well.
           return;
         }
-      } else if (entity.isReferenceList() && setFieldStrategy.isToBeSet("_referenced")) {
+      } else if (entity.isReferenceList() && serializeFieldStrategy.isToBeSet("_referenced")) {
         // Append the all referenced entities. This needs to be done when we are
         // processing SELECT Queries.
         boolean skipValue = false;
@@ -163,7 +182,7 @@ public class EntityToElementStrategy implements ToElementStrategy {
               setDatatype(entity, element);
             }
             Element valueElem = new Element("Value");
-            ref.addToElement(valueElem, setFieldStrategy);
+            ref.addToElement(valueElem, serializeFieldStrategy);
             element.addContent(valueElem);
             skipValue = true;
           }
@@ -174,7 +193,7 @@ public class EntityToElementStrategy implements ToElementStrategy {
           return;
       }
 
-      if (setFieldStrategy.isToBeSet("value")) {
+      if (serializeFieldStrategy.isToBeSet("value")) {
         if (entity.hasDatatype()) {
           setDatatype(entity, element);
         }
@@ -184,24 +203,25 @@ public class EntityToElementStrategy implements ToElementStrategy {
   }
 
   @Override
-  public Element toElement(final EntityInterface entity, final SetFieldStrategy setFieldStrategy) {
+  public Element toElement(
+      final EntityInterface entity, final SerializeFieldStrategy serializeFieldStrategy) {
     final Element element = new Element(tagName);
 
     // always have the values at the beginning of the children
-    setValue(entity, element, setFieldStrategy);
+    setValue(entity, element, serializeFieldStrategy);
 
-    sparseEntityToElement(element, entity, setFieldStrategy);
+    sparseEntityToElement(element, entity, serializeFieldStrategy);
 
-    if (entity.hasStatementStatus() && setFieldStrategy.isToBeSet("importance")) {
+    if (entity.hasStatementStatus() && serializeFieldStrategy.isToBeSet("importance")) {
       element.setAttribute("importance", entity.getStatementStatus().toString());
     }
-    if (entity.hasParents() && setFieldStrategy.isToBeSet("parent")) {
+    if (entity.hasParents() && serializeFieldStrategy.isToBeSet("parent")) {
       entity.getParents().addToElement(element);
     }
     if (entity.hasProperties()) {
-      entity.getProperties().addToElement(element, setFieldStrategy);
+      entity.getProperties().addToElement(element, serializeFieldStrategy);
     }
-    if (entity.hasTransactionLogMessages() && setFieldStrategy.isToBeSet("history")) {
+    if (entity.hasTransactionLogMessages() && serializeFieldStrategy.isToBeSet("history")) {
       for (final TransactionLogMessage t : entity.getTransactionLogMessages()) {
         t.xmlAppendTo(element);
       }
@@ -211,9 +231,11 @@ public class EntityToElementStrategy implements ToElementStrategy {
 
   @Override
   public Element addToElement(
-      final EntityInterface entity, final Element parent, final SetFieldStrategy setFieldStrategy) {
+      final EntityInterface entity,
+      final Element parent,
+      final SerializeFieldStrategy serializeFieldStrategy) {
     if (entity.getEntityStatus() != EntityStatus.IGNORE) {
-      parent.addContent(toElement(entity, setFieldStrategy));
+      parent.addContent(toElement(entity, serializeFieldStrategy));
     }
     return parent;
   }
diff --git a/src/main/java/org/caosdb/server/entity/xml/FileToElementStrategy.java b/src/main/java/org/caosdb/server/entity/xml/FileToElementStrategy.java
index 7ac394a2e3f226afa6f4723daadc95c63db69b25..288159809057d08f57c71dc7cd6decf8adc3be73 100644
--- a/src/main/java/org/caosdb/server/entity/xml/FileToElementStrategy.java
+++ b/src/main/java/org/caosdb/server/entity/xml/FileToElementStrategy.java
@@ -33,17 +33,19 @@ public class FileToElementStrategy extends EntityToElementStrategy {
   }
 
   @Override
-  public Element toElement(final EntityInterface entity, final SetFieldStrategy setFieldStrategy) {
-    final Element element = super.toElement(entity, setFieldStrategy);
+  public Element toElement(
+      final EntityInterface entity, final SerializeFieldStrategy serializeFieldStrategy) {
+    final Element element = super.toElement(entity, serializeFieldStrategy);
 
     if (entity.hasFileProperties()) {
-      if (setFieldStrategy.isToBeSet("checksum") && entity.getFileProperties().hasChecksum()) {
+      if (serializeFieldStrategy.isToBeSet("checksum")
+          && entity.getFileProperties().hasChecksum()) {
         element.setAttribute(new Attribute("checksum", entity.getFileProperties().getChecksum()));
       }
-      if (setFieldStrategy.isToBeSet("path") && entity.getFileProperties().hasPath()) {
+      if (serializeFieldStrategy.isToBeSet("path") && entity.getFileProperties().hasPath()) {
         element.setAttribute(new Attribute("path", "/" + entity.getFileProperties().getPath()));
       }
-      if (setFieldStrategy.isToBeSet("size") && entity.getFileProperties().hasSize()) {
+      if (serializeFieldStrategy.isToBeSet("size") && entity.getFileProperties().hasSize()) {
         element.setAttribute(
             new Attribute("size", Long.toString(entity.getFileProperties().getSize())));
       }
diff --git a/src/main/java/org/caosdb/server/entity/xml/IdAndServerMessagesOnlyStrategy.java b/src/main/java/org/caosdb/server/entity/xml/IdAndServerMessagesOnlyStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..42f7eef9536ae9525875064e7eb60ec855a3952d
--- /dev/null
+++ b/src/main/java/org/caosdb/server/entity/xml/IdAndServerMessagesOnlyStrategy.java
@@ -0,0 +1,21 @@
+package org.caosdb.server.entity.xml;
+
+/**
+ * Special purpose subclass of {@link SerializeFieldStrategy} which only serializes the id and the
+ * server messages (error, warning, info).
+ *
+ * <p>This strategy is used when the client doesn't have the permission to retrieve and entity.
+ *
+ * @author Timm Fitschen <t.fitschen@indiscale.com>
+ */
+public class IdAndServerMessagesOnlyStrategy extends SerializeFieldStrategy {
+
+  // @review Florian Spreckelsen 2022-03-22
+  @Override
+  public boolean isToBeSet(String field) {
+    return "id".equals(field)
+        || "error".equals(field)
+        || "warning".equals(field)
+        || "info".equals(field);
+  }
+}
diff --git a/src/main/java/org/caosdb/server/entity/xml/ParentToElementStrategy.java b/src/main/java/org/caosdb/server/entity/xml/ParentToElementStrategy.java
index 29c163dc5ca55b731aa44a85224821c41356e35a..4f7906829427a2305f0f1b2addf72d2b69112a9e 100644
--- a/src/main/java/org/caosdb/server/entity/xml/ParentToElementStrategy.java
+++ b/src/main/java/org/caosdb/server/entity/xml/ParentToElementStrategy.java
@@ -41,7 +41,8 @@ public class ParentToElementStrategy extends EntityToElementStrategy {
   }
 
   @Override
-  public Element toElement(final EntityInterface entity, final SetFieldStrategy setFieldStrategy) {
+  public Element toElement(
+      final EntityInterface entity, final SerializeFieldStrategy setFieldStrategy) {
     final Element element = new Element(this.tagName);
     sparseEntityToElement(element, entity, setFieldStrategy);
     final Parent parent = (Parent) entity;
@@ -55,7 +56,7 @@ public class ParentToElementStrategy extends EntityToElementStrategy {
   public Element addToElement(
       final EntityInterface entity,
       final Element element,
-      final SetFieldStrategy setFieldStrategy) {
+      final SerializeFieldStrategy setFieldStrategy) {
     if (entity.getEntityStatus() != EntityStatus.IGNORE) {
       element.addContent(toElement(entity, setFieldStrategy));
     }
diff --git a/src/main/java/org/caosdb/server/entity/xml/PropertyToElementStrategy.java b/src/main/java/org/caosdb/server/entity/xml/PropertyToElementStrategy.java
index 60014d82b18da4e8dfdd5fe0bcf042171c2bdda7..23fd8a553e66df23349c20e8539e3154f3e2ac0a 100644
--- a/src/main/java/org/caosdb/server/entity/xml/PropertyToElementStrategy.java
+++ b/src/main/java/org/caosdb/server/entity/xml/PropertyToElementStrategy.java
@@ -39,7 +39,7 @@ public class PropertyToElementStrategy extends EntityToElementStrategy {
   public Element addToElement(
       final EntityInterface entity,
       final Element element,
-      final SetFieldStrategy setFieldStrategy) {
+      final SerializeFieldStrategy setFieldStrategy) {
     try {
       final Value v = entity.getValue();
       if (entity.hasId()) {
diff --git a/src/main/java/org/caosdb/server/entity/xml/SetFieldStrategy.java b/src/main/java/org/caosdb/server/entity/xml/SerializeFieldStrategy.java
similarity index 86%
rename from src/main/java/org/caosdb/server/entity/xml/SetFieldStrategy.java
rename to src/main/java/org/caosdb/server/entity/xml/SerializeFieldStrategy.java
index 1d9f219086e5ddd921d9519d3391e1cca9fbffc9..30210118c0fe8c7b88f0f56532560c49c3782cc0 100644
--- a/src/main/java/org/caosdb/server/entity/xml/SetFieldStrategy.java
+++ b/src/main/java/org/caosdb/server/entity/xml/SerializeFieldStrategy.java
@@ -39,7 +39,7 @@ import org.caosdb.server.query.Query.Selection;
  *
  * @author Timm Fitschen <t.fitschen@indiscale.com>
  */
-public class SetFieldStrategy {
+public class SerializeFieldStrategy {
 
   private final List<Selection> selections = new LinkedList<Selection>();
   private HashMap<String, Boolean> cache = null;
@@ -48,25 +48,25 @@ public class SetFieldStrategy {
    * The default is: Any field should be included into the serialization, unless it is a referenced
    * entity.
    */
-  private static final SetFieldStrategy defaultSelections =
-      new SetFieldStrategy(null) {
+  private static final SerializeFieldStrategy defaultSelections =
+      new SerializeFieldStrategy(null) {
         @Override
         public boolean isToBeSet(final String field) {
           return field == null || !field.equalsIgnoreCase("_referenced");
         }
       };
 
-  public SetFieldStrategy(final List<Selection> selections) {
+  public SerializeFieldStrategy(final List<Selection> selections) {
     if (selections != null) {
       this.selections.addAll(selections);
     }
   }
 
-  public SetFieldStrategy() {
+  public SerializeFieldStrategy() {
     this(null);
   }
 
-  public SetFieldStrategy addSelection(final Selection selection) {
+  public SerializeFieldStrategy addSelection(final Selection selection) {
     // ignore null
     if (selection == null) {
       return this;
@@ -80,15 +80,15 @@ public class SetFieldStrategy {
     return this;
   }
 
-  public SetFieldStrategy forProperty(final EntityInterface property) {
+  public SerializeFieldStrategy forProperty(final EntityInterface property) {
     return forProperty(property.getName());
   }
 
   /** Return the strategy for a property. */
-  public SetFieldStrategy forProperty(final String name) {
+  public SerializeFieldStrategy forProperty(final String name) {
     // if property is to be omitted: always-false-strategy
     if (!isToBeSet(name)) {
-      return new SetFieldStrategy() {
+      return new SerializeFieldStrategy() {
         @Override
         public boolean isToBeSet(final String field) {
           return false;
@@ -111,7 +111,7 @@ public class SetFieldStrategy {
        * <p>This is the case when the selections are deeply nested but only the very last segments
        * are actually used, e.g ["a.b.c.d1", "a.b.c.d2"].
        */
-      return new SetFieldStrategy() {
+      return new SerializeFieldStrategy() {
         // Return true for everything except version fields.
         @Override
         public boolean isToBeSet(String field) {
@@ -119,7 +119,7 @@ public class SetFieldStrategy {
         }
       };
     }
-    return new SetFieldStrategy(subselections);
+    return new SerializeFieldStrategy(subselections);
   }
 
   public boolean isToBeSet(final String field) {
@@ -135,9 +135,11 @@ public class SetFieldStrategy {
 
     if (this.cache == null) {
       this.cache = new HashMap<String, Boolean>();
-      // always include the id and the name
+      // always include the id, version, role and the name
       this.cache.put("id", true);
+      this.cache.put("version", true);
       this.cache.put("name", true);
+      this.cache.put("role", true);
 
       // ... and the referenced entity.
       this.cache.put("_referenced", true);
diff --git a/src/main/java/org/caosdb/server/entity/xml/ToElementStrategy.java b/src/main/java/org/caosdb/server/entity/xml/ToElementStrategy.java
index 81a7074c90df84f88ff3104c84662cb9a7a56433..01e114a25253865e164ca02d6cf42690c57cca0b 100644
--- a/src/main/java/org/caosdb/server/entity/xml/ToElementStrategy.java
+++ b/src/main/java/org/caosdb/server/entity/xml/ToElementStrategy.java
@@ -27,8 +27,8 @@ import org.jdom2.Element;
 
 public interface ToElementStrategy {
 
-  public Element toElement(EntityInterface entity, SetFieldStrategy setFieldStrategy);
+  public Element toElement(EntityInterface entity, SerializeFieldStrategy setFieldStrategy);
 
   public Element addToElement(
-      EntityInterface entity, Element parent, SetFieldStrategy setFieldStrategy);
+      EntityInterface entity, Element parent, SerializeFieldStrategy setFieldStrategy);
 }
diff --git a/src/main/java/org/caosdb/server/grpc/AccessControlManagementServiceImpl.java b/src/main/java/org/caosdb/server/grpc/AccessControlManagementServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..88429828f27ac843903cfcfc801847c35eedb607
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/AccessControlManagementServiceImpl.java
@@ -0,0 +1,602 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.grpc;
+
+import io.grpc.Status;
+import io.grpc.StatusException;
+import io.grpc.stub.StreamObserver;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authz.UnauthorizedException;
+import org.apache.shiro.subject.Subject;
+import org.caosdb.api.acm.v1alpha1.AccessControlManagementServiceGrpc.AccessControlManagementServiceImplBase;
+import org.caosdb.api.acm.v1alpha1.CreateSingleRoleRequest;
+import org.caosdb.api.acm.v1alpha1.CreateSingleRoleResponse;
+import org.caosdb.api.acm.v1alpha1.CreateSingleUserRequest;
+import org.caosdb.api.acm.v1alpha1.CreateSingleUserResponse;
+import org.caosdb.api.acm.v1alpha1.DeleteSingleRoleRequest;
+import org.caosdb.api.acm.v1alpha1.DeleteSingleRoleResponse;
+import org.caosdb.api.acm.v1alpha1.DeleteSingleUserRequest;
+import org.caosdb.api.acm.v1alpha1.DeleteSingleUserResponse;
+import org.caosdb.api.acm.v1alpha1.EmailSetting;
+import org.caosdb.api.acm.v1alpha1.EntitySetting;
+import org.caosdb.api.acm.v1alpha1.ListKnownPermissionsRequest;
+import org.caosdb.api.acm.v1alpha1.ListKnownPermissionsResponse;
+import org.caosdb.api.acm.v1alpha1.ListRoleItem;
+import org.caosdb.api.acm.v1alpha1.ListRolesRequest;
+import org.caosdb.api.acm.v1alpha1.ListRolesResponse;
+import org.caosdb.api.acm.v1alpha1.ListUsersRequest;
+import org.caosdb.api.acm.v1alpha1.ListUsersResponse;
+import org.caosdb.api.acm.v1alpha1.PermissionDescription;
+import org.caosdb.api.acm.v1alpha1.PermissionRule;
+import org.caosdb.api.acm.v1alpha1.RetrieveSingleRoleRequest;
+import org.caosdb.api.acm.v1alpha1.RetrieveSingleRoleResponse;
+import org.caosdb.api.acm.v1alpha1.RetrieveSingleUserRequest;
+import org.caosdb.api.acm.v1alpha1.RetrieveSingleUserResponse;
+import org.caosdb.api.acm.v1alpha1.RoleCapabilities;
+import org.caosdb.api.acm.v1alpha1.RolePermissions;
+import org.caosdb.api.acm.v1alpha1.UpdateSingleRoleRequest;
+import org.caosdb.api.acm.v1alpha1.UpdateSingleRoleResponse;
+import org.caosdb.api.acm.v1alpha1.UpdateSingleUserRequest;
+import org.caosdb.api.acm.v1alpha1.UpdateSingleUserResponse;
+import org.caosdb.api.acm.v1alpha1.User;
+import org.caosdb.api.acm.v1alpha1.UserCapabilities;
+import org.caosdb.api.acm.v1alpha1.UserPermissions;
+import org.caosdb.api.acm.v1alpha1.UserStatus;
+import org.caosdb.server.accessControl.ACMPermissions;
+import org.caosdb.server.accessControl.AuthenticationUtils;
+import org.caosdb.server.accessControl.Role;
+import org.caosdb.server.accessControl.UserSources;
+import org.caosdb.server.database.proto.ProtoUser;
+import org.caosdb.server.transaction.DeleteRoleTransaction;
+import org.caosdb.server.transaction.DeleteUserTransaction;
+import org.caosdb.server.transaction.InsertRoleTransaction;
+import org.caosdb.server.transaction.InsertUserTransaction;
+import org.caosdb.server.transaction.ListRolesTransaction;
+import org.caosdb.server.transaction.ListUsersTransaction;
+import org.caosdb.server.transaction.RetrieveRoleTransaction;
+import org.caosdb.server.transaction.RetrieveUserTransaction;
+import org.caosdb.server.transaction.UpdateRoleTransaction;
+import org.caosdb.server.transaction.UpdateUserTransaction;
+import org.caosdb.server.utils.ServerMessages;
+
+public class AccessControlManagementServiceImpl extends AccessControlManagementServiceImplBase {
+
+  /////////////////////////////////// CONVERTERS
+
+  private ProtoUser convert(User user) {
+    ProtoUser result = new ProtoUser();
+    result.realm = user.getRealm();
+    result.name = user.getName();
+    result.email = user.hasEmailSetting() ? user.getEmailSetting().getEmail() : null;
+    result.status = convert(user.getStatus());
+    result.roles = new HashSet<String>();
+    if (user.getRolesCount() >= 0) {
+      user.getRolesList().forEach(result.roles::add);
+    }
+    return result;
+  }
+
+  private org.caosdb.server.accessControl.UserStatus convert(UserStatus status) {
+    switch (status) {
+      case USER_STATUS_ACTIVE:
+        return org.caosdb.server.accessControl.UserStatus.ACTIVE;
+      case USER_STATUS_INACTIVE:
+        return org.caosdb.server.accessControl.UserStatus.INACTIVE;
+      default:
+        break;
+    }
+    return org.caosdb.server.accessControl.UserStatus.INACTIVE;
+  }
+
+  private ListUsersResponse convertUsers(List<ProtoUser> users) {
+    ListUsersResponse.Builder response = ListUsersResponse.newBuilder();
+    users.forEach(
+        user -> {
+          response.addUsers(convert(user));
+        });
+
+    return response.build();
+  }
+
+  private User.Builder convert(ProtoUser user) {
+    User.Builder result = User.newBuilder();
+    result.setRealm(user.realm);
+    result.setName(user.name);
+    if (user.status != null) result.setStatus(convert(user.status));
+    if (user.email != null) {
+      result.setEmailSetting(EmailSetting.newBuilder().setEmail(user.email));
+    }
+    if (user.entity != null) {
+      result.setEntitySetting(
+          EntitySetting.newBuilder().setEntityId(Integer.toString(user.entity)));
+    }
+    if (user.roles != null && !user.roles.isEmpty()) {
+      result.addAllRoles(user.roles);
+    }
+    return result;
+  }
+
+  private UserStatus convert(org.caosdb.server.accessControl.UserStatus status) {
+    switch (status) {
+      case ACTIVE:
+        return UserStatus.USER_STATUS_ACTIVE;
+      case INACTIVE:
+        return UserStatus.USER_STATUS_INACTIVE;
+      default:
+        return UserStatus.USER_STATUS_UNSPECIFIED;
+    }
+  }
+
+  private Role convert(org.caosdb.api.acm.v1alpha1.Role role) {
+    Role result = new Role();
+
+    result.name = role.getName();
+    result.description = role.getDescription();
+    result.permission_rules = convertPermissionRules(role.getPermissionRulesList());
+    return result;
+  }
+
+  private LinkedList<org.caosdb.server.permissions.PermissionRule> convertPermissionRules(
+      List<PermissionRule> permissionRulesList) {
+    LinkedList<org.caosdb.server.permissions.PermissionRule> result = new LinkedList<>();
+    permissionRulesList.forEach((r) -> result.add(convert(r)));
+    return result;
+  }
+
+  private org.caosdb.server.permissions.PermissionRule convert(PermissionRule r) {
+    boolean grant = r.getGrant();
+    boolean priority = r.getPriority();
+    String permission = r.getPermission();
+    return new org.caosdb.server.permissions.PermissionRule(grant, priority, permission);
+  }
+
+  private ListRolesResponse convert(List<Role> roles) {
+    ListRolesResponse.Builder response = ListRolesResponse.newBuilder();
+    roles.forEach(
+        role -> {
+          response.addRoles(convert(role, getRolePermissions(role), getRoleCapabilities(role)));
+        });
+
+    return response.build();
+  }
+
+  private ListRoleItem convert(
+      Role role,
+      Iterable<? extends RolePermissions> rolePermissions,
+      Iterable<? extends RoleCapabilities> roleCapabilities) {
+    return ListRoleItem.newBuilder()
+        .setRole(convert(role))
+        .addAllCapabilities(roleCapabilities)
+        .addAllPermissions(rolePermissions)
+        .build();
+  }
+
+  private org.caosdb.api.acm.v1alpha1.Role.Builder convert(Role role) {
+    org.caosdb.api.acm.v1alpha1.Role.Builder result = org.caosdb.api.acm.v1alpha1.Role.newBuilder();
+    result.setDescription(role.description);
+    result.setName(role.name);
+    if (role.permission_rules != null) result.addAllPermissionRules(convert(role.permission_rules));
+    return result;
+  }
+
+  private PermissionRule convert(org.caosdb.server.permissions.PermissionRule rule) {
+    PermissionRule.Builder result = PermissionRule.newBuilder();
+    result.setGrant(rule.isGrant());
+    result.setPriority(rule.isPriority());
+    result.setPermission(rule.getPermission());
+    return result.build();
+  }
+
+  private Iterable<PermissionRule> convert(
+      LinkedList<org.caosdb.server.permissions.PermissionRule> permission_rules) {
+
+    List<PermissionRule> result = new LinkedList<>();
+    permission_rules.forEach(
+        (rule) -> {
+          result.add(convert(rule));
+        });
+    return result;
+  }
+
+  ////////////////////////////////////// RPC Methods (Implementation)
+
+  private ListKnownPermissionsResponse listKnownPermissions(ListKnownPermissionsRequest request) {
+    ListKnownPermissionsResponse.Builder builder = ListKnownPermissionsResponse.newBuilder();
+    builder.addAllPermissions(listKnownPermissions());
+    return builder.build();
+  }
+
+  private Iterable<PermissionDescription> listKnownPermissions() {
+    List<PermissionDescription> result = new LinkedList<>();
+    for (ACMPermissions p : ACMPermissions.getAll()) {
+      result.add(
+          PermissionDescription.newBuilder()
+              .setPermission(p.toString())
+              .setDescription(p.getDescription())
+              .build());
+    }
+    return result;
+  }
+
+  ////////////////// ... for roles
+
+  private ListRolesResponse listRolesTransaction(ListRolesRequest request) throws Exception {
+    ListRolesTransaction transaction = new ListRolesTransaction();
+    transaction.execute();
+    List<Role> roles = transaction.getRoles();
+
+    return convert(roles);
+  }
+
+  private CreateSingleRoleResponse createSingleRoleTransaction(CreateSingleRoleRequest request)
+      throws Exception {
+    Role role = convert(request.getRole());
+    InsertRoleTransaction transaction = new InsertRoleTransaction(role);
+    transaction.execute();
+
+    return CreateSingleRoleResponse.newBuilder().build();
+  }
+
+  private RetrieveSingleRoleResponse retrieveSingleRoleTransaction(
+      RetrieveSingleRoleRequest request) throws Exception {
+    RetrieveRoleTransaction transaction = new RetrieveRoleTransaction(request.getName());
+    transaction.execute();
+
+    Role role = transaction.getRole();
+    RetrieveSingleRoleResponse.Builder builder =
+        RetrieveSingleRoleResponse.newBuilder().setRole(convert(transaction.getRole()));
+    if (role.users != null && !role.users.isEmpty())
+      role.users.forEach(
+          (u) -> {
+            builder.addUsers(convert(u));
+          });
+    return builder
+        .addAllPermissions(getRolePermissions(role))
+        .addAllCapabilities(getRoleCapabilities(role))
+        .build();
+  }
+
+  /** What can be done with this role. */
+  private Iterable<? extends RoleCapabilities> getRoleCapabilities(Role role) {
+    List<RoleCapabilities> result = new LinkedList<>();
+    if (org.caosdb.server.permissions.Role.ADMINISTRATION.toString().equals(role.name)) {
+      // administration cannot be deleted and the permissions cannot be changed (from *)
+      result.add(RoleCapabilities.ROLE_CAPABILITIES_ASSIGN);
+    } else if (org.caosdb.server.permissions.Role.ANONYMOUS_ROLE.toString().equals(role.name)) {
+      // anonymous cannot be deleted or assigned to any user
+      result.add(RoleCapabilities.ROLE_CAPABILITIES_UPDATE_PERMISSION_RULES);
+    } else {
+      result.add(RoleCapabilities.ROLE_CAPABILITIES_ASSIGN);
+      result.add(RoleCapabilities.ROLE_CAPABILITIES_DELETE);
+      result.add(RoleCapabilities.ROLE_CAPABILITIES_UPDATE_PERMISSION_RULES);
+    }
+    return result;
+  }
+
+  /** The permissions of the current user w.r.t. this role. */
+  private Iterable<? extends RolePermissions> getRolePermissions(Role role) {
+    List<RolePermissions> result = new LinkedList<>();
+    Subject current_user = SecurityUtils.getSubject();
+    if (current_user.isPermitted(ACMPermissions.PERMISSION_DELETE_ROLE(role.name))) {
+      result.add(RolePermissions.ROLE_PERMISSIONS_DELETE);
+    }
+    if (current_user.isPermitted(ACMPermissions.PERMISSION_UPDATE_ROLE_DESCRIPTION(role.name))) {
+      result.add(RolePermissions.ROLE_PERMISSIONS_UPDATE_DESCRIPTION);
+    }
+    if (current_user.isPermitted(ACMPermissions.PERMISSION_UPDATE_ROLE_PERMISSIONS(role.name))) {
+      result.add(RolePermissions.ROLE_PERMISSIONS_UPDATE_PERMISSION_RULES);
+    }
+    if (current_user.isPermitted(ACMPermissions.PERMISSION_ASSIGN_ROLE(role.name))) {
+      result.add(RolePermissions.ROLE_PERMISSIONS_ASSIGN);
+    }
+    return result;
+  }
+
+  private DeleteSingleRoleResponse deleteSingleRoleTransaction(DeleteSingleRoleRequest request)
+      throws Exception {
+    DeleteRoleTransaction transaction = new DeleteRoleTransaction(request.getName());
+    transaction.execute();
+
+    return DeleteSingleRoleResponse.newBuilder().build();
+  }
+
+  private UpdateSingleRoleResponse updateSingleRoleTransaction(UpdateSingleRoleRequest request)
+      throws Exception {
+    Role role = convert(request.getRole());
+    UpdateRoleTransaction transaction = new UpdateRoleTransaction(role);
+    transaction.execute();
+    return UpdateSingleRoleResponse.newBuilder().build();
+  }
+
+  ////////////////// ... for users
+
+  private ListUsersResponse listUsersTransaction(ListUsersRequest request) throws Exception {
+    ListUsersTransaction transaction = new ListUsersTransaction();
+    transaction.execute();
+    List<ProtoUser> users = transaction.getUsers();
+    return convertUsers(users);
+  }
+
+  private CreateSingleUserResponse createSingleUserTransaction(CreateSingleUserRequest request)
+      throws Exception {
+    ProtoUser user = convert(request.getUser());
+    InsertUserTransaction transaction =
+        new InsertUserTransaction(
+            user, request.hasPasswordSetting() ? request.getPasswordSetting().getPassword() : null);
+    transaction.execute();
+
+    return CreateSingleUserResponse.newBuilder().build();
+  }
+
+  private UpdateSingleUserResponse updateSingleUserTransaction(UpdateSingleUserRequest request)
+      throws Exception {
+    ProtoUser user = convert(request.getUser());
+    UpdateUserTransaction transaction =
+        new UpdateUserTransaction(
+            user, request.hasPasswordSetting() ? request.getPasswordSetting().getPassword() : null);
+    transaction.execute();
+
+    return UpdateSingleUserResponse.newBuilder().build();
+  }
+
+  private RetrieveSingleUserResponse retrieveSingleUserTransaction(
+      RetrieveSingleUserRequest request) throws Exception {
+    RetrieveUserTransaction transaction =
+        new RetrieveUserTransaction(request.getRealm(), request.getName());
+    transaction.execute();
+    ProtoUser user = transaction.getUser();
+
+    return RetrieveSingleUserResponse.newBuilder()
+        .setUser(convert(user))
+        .addAllPermissions(getUserPermissions(user.realm, user.name))
+        .addAllCapabilities(getUserCapabilities(user))
+        .build();
+  }
+
+  private Iterable<? extends UserCapabilities> getUserCapabilities(ProtoUser user) {
+    LinkedList<UserCapabilities> result = new LinkedList<>();
+    if (user.realm.equals(UserSources.getInternalRealm().getName())) {
+      result.add(UserCapabilities.USER_CAPABILITIES_DELETE);
+      result.add(UserCapabilities.USER_CAPABILITIES_UPDATE_PASSWORD);
+    }
+    return result;
+  }
+
+  private Iterable<? extends UserPermissions> getUserPermissions(String realm, String name) {
+    LinkedList<UserPermissions> result = new LinkedList<>();
+    Subject current_user = SecurityUtils.getSubject();
+    if (current_user.isPermitted(ACMPermissions.PERMISSION_DELETE_USER(realm, name))) {
+      result.add(UserPermissions.USER_PERMISSIONS_DELETE);
+    }
+    if (current_user.isPermitted(ACMPermissions.PERMISSION_UPDATE_USER_EMAIL(realm, name))) {
+      result.add(UserPermissions.USER_PERMISSIONS_UPDATE_EMAIL);
+    }
+    if (current_user.isPermitted(ACMPermissions.PERMISSION_UPDATE_USER_STATUS(realm, name))) {
+      result.add(UserPermissions.USER_PERMISSIONS_UPDATE_STATUS);
+    }
+    if (current_user.isPermitted(ACMPermissions.PERMISSION_UPDATE_USER_ROLES(realm, name))) {
+      result.add(UserPermissions.USER_PERMISSIONS_UPDATE_ROLES);
+    }
+    if (current_user.isPermitted(ACMPermissions.PERMISSION_UPDATE_USER_PASSWORD(realm, name))) {
+      result.add(UserPermissions.USER_PERMISSIONS_UPDATE_PASSWORD);
+    }
+    return result;
+  }
+
+  private DeleteSingleUserResponse deleteSingleUserTransaction(DeleteSingleUserRequest request)
+      throws Exception {
+    DeleteUserTransaction transaction =
+        new DeleteUserTransaction(request.getRealm(), request.getName());
+    transaction.execute();
+
+    return DeleteSingleUserResponse.newBuilder().build();
+  }
+
+  ///////////////////////////////////// RPC Methods (API)
+
+  @Override
+  public void listUsers(
+      ListUsersRequest request, StreamObserver<ListUsersResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final ListUsersResponse response = listUsersTransaction(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      handleException(responseObserver, e);
+    }
+  }
+
+  @Override
+  public void listRoles(
+      ListRolesRequest request, StreamObserver<ListRolesResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final ListRolesResponse response = listRolesTransaction(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      handleException(responseObserver, e);
+    }
+  }
+
+  @Override
+  public void createSingleRole(
+      CreateSingleRoleRequest request, StreamObserver<CreateSingleRoleResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final CreateSingleRoleResponse response = createSingleRoleTransaction(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      handleException(responseObserver, e);
+    }
+  }
+
+  @Override
+  public void retrieveSingleRole(
+      RetrieveSingleRoleRequest request,
+      StreamObserver<RetrieveSingleRoleResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final RetrieveSingleRoleResponse response = retrieveSingleRoleTransaction(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      handleException(responseObserver, e);
+    }
+  }
+
+  @Override
+  public void createSingleUser(
+      CreateSingleUserRequest request, StreamObserver<CreateSingleUserResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final CreateSingleUserResponse response = createSingleUserTransaction(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      handleException(responseObserver, e);
+    }
+  }
+
+  @Override
+  public void updateSingleUser(
+      UpdateSingleUserRequest request, StreamObserver<UpdateSingleUserResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final UpdateSingleUserResponse response = updateSingleUserTransaction(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      handleException(responseObserver, e);
+    }
+  }
+
+  @Override
+  public void retrieveSingleUser(
+      RetrieveSingleUserRequest request,
+      StreamObserver<RetrieveSingleUserResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final RetrieveSingleUserResponse response = retrieveSingleUserTransaction(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      handleException(responseObserver, e);
+    }
+  }
+
+  @Override
+  public void deleteSingleUser(
+      DeleteSingleUserRequest request, StreamObserver<DeleteSingleUserResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final DeleteSingleUserResponse response = deleteSingleUserTransaction(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      handleException(responseObserver, e);
+    }
+  }
+
+  @Override
+  public void deleteSingleRole(
+      DeleteSingleRoleRequest request, StreamObserver<DeleteSingleRoleResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final DeleteSingleRoleResponse response = deleteSingleRoleTransaction(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      handleException(responseObserver, e);
+    }
+  }
+
+  @Override
+  public void updateSingleRole(
+      UpdateSingleRoleRequest request, StreamObserver<UpdateSingleRoleResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final UpdateSingleRoleResponse response = updateSingleRoleTransaction(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      handleException(responseObserver, e);
+    }
+  }
+
+  @Override
+  public void listKnownPermissions(
+      ListKnownPermissionsRequest request,
+      StreamObserver<ListKnownPermissionsResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final ListKnownPermissionsResponse response = listKnownPermissions(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      handleException(responseObserver, e);
+    }
+  }
+
+  public static void handleException(StreamObserver<?> responseObserver, Exception e) {
+    String description = e.getMessage();
+    if (description == null || description.isBlank()) {
+      description = "Unknown Error. Please Report!";
+    }
+    if (e instanceof UnauthorizedException) {
+      Subject subject = SecurityUtils.getSubject();
+      if (AuthenticationUtils.isAnonymous(subject)) {
+        responseObserver.onError(new StatusException(AuthInterceptor.PLEASE_LOG_IN));
+        return;
+      } else {
+        responseObserver.onError(
+            new StatusException(
+                Status.PERMISSION_DENIED.withCause(e).withDescription(description)));
+        return;
+      }
+    } else if (e == ServerMessages.ROLE_DOES_NOT_EXIST
+        || e == ServerMessages.ACCOUNT_DOES_NOT_EXIST) {
+      responseObserver.onError(
+          new StatusException(Status.NOT_FOUND.withDescription(description).withCause(e)));
+      return;
+    }
+    e.printStackTrace();
+    responseObserver.onError(
+        new StatusException(Status.UNKNOWN.withDescription(description).withCause(e)));
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/AuthInterceptor.java b/src/main/java/org/caosdb/server/grpc/AuthInterceptor.java
new file mode 100644
index 0000000000000000000000000000000000000000..ce8a62210d3c8e849397600fc112cd3103f15df4
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/AuthInterceptor.java
@@ -0,0 +1,313 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.grpc;
+
+import static org.caosdb.server.utils.Utils.URLDecodeWithUTF8;
+
+import io.grpc.Context;
+import io.grpc.Contexts;
+import io.grpc.ForwardingServerCall;
+import io.grpc.Metadata;
+import io.grpc.Metadata.Key;
+import io.grpc.ServerCall;
+import io.grpc.ServerCall.Listener;
+import io.grpc.ServerCallHandler;
+import io.grpc.ServerInterceptor;
+import io.grpc.Status;
+import java.util.Base64;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.util.ThreadContext;
+import org.caosdb.server.CaosDBServer;
+import org.caosdb.server.ServerProperties;
+import org.caosdb.server.accessControl.AnonymousAuthenticationToken;
+import org.caosdb.server.accessControl.AuthenticationUtils;
+import org.caosdb.server.accessControl.RealmUsernamePasswordToken;
+import org.caosdb.server.accessControl.SelfValidatingAuthenticationToken;
+import org.caosdb.server.accessControl.SessionToken;
+import org.caosdb.server.accessControl.UserSources;
+import org.caosdb.server.utils.Utils;
+import org.restlet.data.CookieSetting;
+
+/**
+ * ServerInterceptor for Authentication. If the authentication succeeds or if the caller is
+ * anonymous, the {@link Context} of the {@link ServerCall} is updated with a {@link Subject}
+ * instance. If the request does not succeed the call is closed with {@link Status#UNAUTHENTICATED}.
+ *
+ * @author Timm Fitschen <t.fitschen@indiscale.com>
+ */
+public class AuthInterceptor implements ServerInterceptor {
+
+  public static final Status PLEASE_LOG_IN =
+      Status.UNAUTHENTICATED.withDescription("Please log in!");
+  public static final Key<String> AUTHENTICATION_HEADER =
+      Key.of("authentication", Metadata.ASCII_STRING_MARSHALLER);
+  public static final Key<String> AUTHORIZATION_HEADER =
+      Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
+  public static final Key<String> COOKIE_HEADER =
+      Key.of("Cookie", Metadata.ASCII_STRING_MARSHALLER);
+  public static final Context.Key<Subject> SUBJECT_KEY = Context.key("subject");
+  public static final String BASIC_SCHEME_PREFIX = "Basic ";
+  public static final Pattern SESSION_TOKEN_COOKIE_PREFIX_PATTERN =
+      Pattern.compile("^\\s*" + AuthenticationUtils.SESSION_TOKEN_COOKIE + "\\s*=\\s*");
+  public static final Predicate<String> SESSION_TOKEN_COOKIE_PREFIX_PREDICATE =
+      SESSION_TOKEN_COOKIE_PREFIX_PATTERN.asPredicate();
+
+  @SuppressWarnings("unused")
+  public static Subject bindSubject() {
+    Subject subject = (Subject) SUBJECT_KEY.get();
+    ThreadContext.bind(subject);
+    return subject;
+  }
+
+  public final Metadata expiredSessionMetadata() {
+    Metadata metadata = new Metadata();
+    metadata.put(CookieSetter.SET_COOKIE, CookieSetter.EXPIRED_SESSION_COOKIE);
+    return metadata;
+  }
+  /**
+   * A no-op listener. This class is used for failed authentications. We couldn't return a null
+   * instead because the documentation of the {@link ServerInterceptor} explicitely forbids it.
+   *
+   * @author Timm Fitschen <t.fitschen@indiscale.com>
+   */
+  static class NoOpListener<ReqT> extends Listener<ReqT> {}
+
+  /** Whether the anonymous login is possible or not. */
+  private final boolean isAuthOptional() {
+    return CaosDBServer.getServerProperty(ServerProperties.KEY_AUTH_OPTIONAL)
+        .equalsIgnoreCase("true");
+  }
+
+  /**
+   * Login via username and password with the basic authentication scheme and return the logged-in
+   * subject.
+   */
+  private Subject basicAuth(final String base64) {
+    final String plain = new String(Base64.getDecoder().decode(base64));
+    final String[] split = plain.split(":", 2);
+    final String username = split[0];
+    final String password = split[1];
+    final RealmUsernamePasswordToken token =
+        new RealmUsernamePasswordToken(
+            UserSources.guessRealm(username, UserSources.getDefaultRealm()), username, password);
+    final Subject subject = SecurityUtils.getSubject();
+    subject.login(token);
+    return subject;
+  }
+
+  @Override
+  public <ReqT, RespT> Listener<ReqT> interceptCall(
+      final ServerCall<ReqT, RespT> call,
+      final Metadata headers,
+      final ServerCallHandler<ReqT, RespT> next) {
+    ThreadContext.remove();
+
+    String authentication = headers.get(AUTHENTICATION_HEADER);
+    if (authentication == null) {
+      authentication = headers.get(AUTHORIZATION_HEADER);
+    }
+    if (authentication == null) {
+      authentication = getSessionToken(headers.get(COOKIE_HEADER));
+    }
+    Status status =
+        Status.UNKNOWN.withDescription(
+            "An unknown error occured during authentication. Please report a bug.");
+    if (authentication == null && isAuthOptional()) {
+      return anonymous(call, headers, next);
+    } else if (authentication == null) {
+      status = PLEASE_LOG_IN;
+    } else if (authentication.startsWith(BASIC_SCHEME_PREFIX)) {
+      return basicAuth(authentication.substring(BASIC_SCHEME_PREFIX.length()), call, headers, next);
+    } else if (SESSION_TOKEN_COOKIE_PREFIX_PREDICATE.test(authentication)) {
+      return sessionTokenAuth(
+          SESSION_TOKEN_COOKIE_PREFIX_PATTERN.split(authentication, 2)[1], call, headers, next);
+    } else {
+      status = Status.UNAUTHENTICATED.withDescription("Unsupported authentication scheme.");
+    }
+    call.close(status, expiredSessionMetadata());
+    return new NoOpListener<ReqT>();
+  }
+
+  private String getSessionToken(String cookies) {
+    if (cookies != null)
+      for (String cookie : cookies.split("\\s*;\\s*")) {
+        if (SESSION_TOKEN_COOKIE_PREFIX_PREDICATE.test(cookie)) {
+          return cookie;
+        }
+      }
+    return null;
+  }
+
+  /**
+   * Login via AuthenticationToken and add the resulting subject to the call context.
+   *
+   * @see #updateContext(Subject, ServerCall, Metadata, ServerCallHandler) for more information.
+   */
+  private <ReqT, RespT> Listener<ReqT> sessionTokenAuth(
+      String sessionTokenCookie,
+      ServerCall<ReqT, RespT> call,
+      Metadata headers,
+      ServerCallHandler<ReqT, RespT> next) {
+    try {
+      final String tokenString = URLDecodeWithUTF8(sessionTokenCookie.split(";")[0]);
+
+      final Subject subject = sessionTokenAuth(tokenString);
+      return updateContext(subject, call, headers, next, "sessionToken: " + tokenString);
+    } catch (final AuthenticationException e) {
+      final Status status =
+          Status.UNAUTHENTICATED.withDescription(
+              "Authentication failed. SessionToken was invalid.");
+      call.close(status, expiredSessionMetadata());
+      return new NoOpListener<ReqT>();
+    }
+  }
+
+  /** Login via AuthenticationToken and return the logged-in subject. */
+  private Subject sessionTokenAuth(String tokenString) {
+    Subject subject = SecurityUtils.getSubject();
+    subject.login(SelfValidatingAuthenticationToken.parse(tokenString));
+    return subject;
+  }
+
+  /**
+   * Login via username and password with the basic authentication scheme and add the resulting
+   * subject to the call context.
+   *
+   * @see #updateContext(Subject, ServerCall, Metadata, ServerCallHandler) for more information.
+   */
+  private <ReqT, RespT> Listener<ReqT> basicAuth(
+      final String base64,
+      final ServerCall<ReqT, RespT> call,
+      final Metadata headers,
+      final ServerCallHandler<ReqT, RespT> next) {
+    try {
+      final Subject subject = basicAuth(base64);
+      return updateContext(
+          subject,
+          call,
+          headers,
+          next,
+          "basic: " + base64 + " thread: " + Thread.currentThread().getName());
+    } catch (final AuthenticationException e) {
+      final Status status =
+          Status.UNAUTHENTICATED.withDescription(
+              "Authentication failed. Username or password wrong.");
+      call.close(status, expiredSessionMetadata());
+      return new NoOpListener<ReqT>();
+    }
+  }
+
+  /**
+   * Login as anonymous and add the anonymous subject to the call context.
+   *
+   * @see #updateContext(Subject, ServerCall, Metadata, ServerCallHandler) for more information.
+   */
+  private <ReqT, RespT> Listener<ReqT> anonymous(
+      final ServerCall<ReqT, RespT> call,
+      final Metadata headers,
+      final ServerCallHandler<ReqT, RespT> next) {
+    final Subject subject = anonymous();
+    return updateContext(subject, call, headers, next, "anonymous");
+  }
+
+  /** Login as anonymous. */
+  private Subject anonymous() {
+    final Subject anonymous = SecurityUtils.getSubject();
+    anonymous.login(AnonymousAuthenticationToken.getInstance());
+    return anonymous;
+  }
+
+  /**
+   * Add the subject to the call context. This is done the grpcic way by returning a listener which
+   * does exactly that.
+   */
+  private <ReqT, RespT> Listener<ReqT> updateContext(
+      final Subject subject,
+      final ServerCall<ReqT, RespT> call,
+      final Metadata headers,
+      final ServerCallHandler<ReqT, RespT> next,
+      final String tag) {
+    final Context context = Context.current().withValue(SUBJECT_KEY, subject);
+    ServerCall<ReqT, RespT> cookieSetter = new CookieSetter<>(call, subject, tag);
+    return Contexts.interceptCall(context, cookieSetter, headers, next);
+  }
+}
+
+final class CookieSetter<ReqT, RespT>
+    extends ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT> {
+  public static final String EXPIRED_SESSION_COOKIE =
+      AuthenticationUtils.SESSION_TOKEN_COOKIE
+          + "=expired; Path=/; HttpOnly; SameSite=Strict; Max-Age=0";
+  public static final Key<String> SET_COOKIE =
+      Key.of("Set-Cookie", Metadata.ASCII_STRING_MARSHALLER);
+  private Subject subject;
+
+  protected CookieSetter(ServerCall<ReqT, RespT> delegate, Subject subject, String tag) {
+    super(delegate);
+    this.subject = subject;
+  }
+
+  String getSessionTimeoutSeconds() {
+    int ms =
+        Integer.parseInt(CaosDBServer.getServerProperty(ServerProperties.KEY_SESSION_TIMEOUT_MS));
+    int seconds = (int) Math.floor(ms / 1000);
+    return Integer.toString(seconds);
+  }
+
+  @Override
+  public void sendHeaders(Metadata headers) {
+    setSessionCookies(headers);
+    super.sendHeaders(headers);
+  };
+
+  private void setSessionCookies(Metadata headers) {
+    // if authenticated as a normal user: generate and set session cookie.
+    if (subject.isAuthenticated()
+        && !AnonymousAuthenticationToken.PRINCIPAL.equals(subject.getPrincipal())) {
+      final SessionToken sessionToken = SessionToken.generate(subject);
+      if (sessionToken != null && sessionToken.isValid()) {
+
+        final CookieSetting sessionTokenCookie =
+            AuthenticationUtils.createSessionTokenCookie(sessionToken);
+        if (sessionTokenCookie != null) {
+          // TODO add "Secure;" to cookie setting
+          headers.put(
+              SET_COOKIE,
+              AuthenticationUtils.SESSION_TOKEN_COOKIE
+                  + "="
+                  + Utils.URLEncodeWithUTF8(sessionToken.toString())
+                  + "; Path=/; HttpOnly; SameSite=Strict; Max-Age="
+                  + getSessionTimeoutSeconds());
+        }
+      }
+    } else if (AnonymousAuthenticationToken.PRINCIPAL.equals(subject.getPrincipal())) {
+      // this is anonymous, do nothing
+      headers.toString();
+    } else {
+      headers.put(SET_COOKIE, EXPIRED_SESSION_COOKIE);
+    }
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/CaosDBToGrpcConverters.java b/src/main/java/org/caosdb/server/grpc/CaosDBToGrpcConverters.java
new file mode 100644
index 0000000000000000000000000000000000000000..40fda9d8af48aa0b5736c328d7d2f2c3353c9a1d
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/CaosDBToGrpcConverters.java
@@ -0,0 +1,591 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.grpc;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.TimeZone;
+import org.caosdb.api.entity.v1.AtomicDataType;
+import org.caosdb.api.entity.v1.CollectionValues;
+import org.caosdb.api.entity.v1.DataType;
+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.EntityAclPermission;
+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.FileDescriptor;
+import org.caosdb.api.entity.v1.Importance;
+import org.caosdb.api.entity.v1.ListDataType;
+import org.caosdb.api.entity.v1.MessageCode;
+import org.caosdb.api.entity.v1.Parent;
+import org.caosdb.api.entity.v1.ReferenceDataType;
+import org.caosdb.api.entity.v1.ScalarValue;
+import org.caosdb.api.entity.v1.SpecialValue;
+import org.caosdb.api.entity.v1.Version;
+import org.caosdb.datetime.DateTimeInterface;
+import org.caosdb.server.datatype.AbstractCollectionDatatype;
+import org.caosdb.server.datatype.AbstractDatatype;
+import org.caosdb.server.datatype.BooleanDatatype;
+import org.caosdb.server.datatype.BooleanValue;
+import org.caosdb.server.datatype.CollectionValue;
+import org.caosdb.server.datatype.DateTimeDatatype;
+import org.caosdb.server.datatype.DoubleDatatype;
+import org.caosdb.server.datatype.FileDatatype;
+import org.caosdb.server.datatype.GenericValue;
+import org.caosdb.server.datatype.IndexedSingleValue;
+import org.caosdb.server.datatype.IntegerDatatype;
+import org.caosdb.server.datatype.ReferenceDatatype;
+import org.caosdb.server.datatype.ReferenceDatatype2;
+import org.caosdb.server.datatype.ReferenceValue;
+import org.caosdb.server.datatype.TextDatatype;
+import org.caosdb.server.datatype.Value;
+import org.caosdb.server.entity.EntityInterface;
+import org.caosdb.server.entity.FileProperties;
+import org.caosdb.server.entity.MagicTypes;
+import org.caosdb.server.entity.Message;
+import org.caosdb.server.entity.Role;
+import org.caosdb.server.entity.StatementStatus;
+import org.caosdb.server.entity.container.ParentContainer;
+import org.caosdb.server.entity.wrapper.Property;
+import org.caosdb.server.entity.xml.SerializeFieldStrategy;
+import org.caosdb.server.permissions.EntityACI;
+import org.caosdb.server.permissions.EntityPermission;
+
+public class CaosDBToGrpcConverters {
+
+  private TimeZone timeZone;
+
+  public CaosDBToGrpcConverters(TimeZone timeZone) {
+    this.timeZone = timeZone;
+  }
+
+  /** Get the unit as string. */
+  public String getStringUnit(final EntityInterface entity) {
+    final Iterator<Property> iterator = entity.getProperties().iterator();
+    while (iterator.hasNext()) {
+      final Property p = iterator.next();
+      if (Objects.equals(MagicTypes.UNIT.getId(), p.getId())) {
+        iterator.remove();
+        return p.getValue().toString();
+      }
+    }
+    return null;
+  }
+
+  public EntityResponse.Builder convert(final EntityInterface from) {
+
+    // @review Florian Spreckelsen 2022-03-22
+
+    SerializeFieldStrategy s = from.getSerializeFieldStrategy();
+    final Builder entityBuilder = Entity.newBuilder();
+
+    if (from.hasId() && s.isToBeSet("id")) {
+      entityBuilder.setId(Integer.toString(from.getId()));
+    }
+    if (from.getRole() != null && s.isToBeSet("role")) {
+      entityBuilder.setRole(convert(from.getRole()));
+    }
+    if (from.hasName() && s.isToBeSet("name")) {
+      entityBuilder.setName(from.getName());
+    }
+    if (from.hasDescription() && s.isToBeSet("description")) {
+      entityBuilder.setDescription(from.getDescription());
+    }
+    if (from.hasDatatype() && s.isToBeSet("datatype")) {
+      entityBuilder.setDataType(convert(from.getDatatype()));
+    }
+    if (from.hasValue() && s.isToBeSet("value")) {
+      try {
+        from.parseValue();
+      } catch (final Message e) {
+        // ignore. This problem should be handled elsewhere because this is
+        // only for the serialization of the data and not for the validation.
+        // In any case, the string representation can be used.
+      }
+      entityBuilder.setValue(convert(from.getValue()));
+    }
+    final String unit = getStringUnit(from);
+    if (unit != null && s.isToBeSet("unit")) {
+      entityBuilder.setUnit(unit);
+    }
+    if (from.hasProperties()) {
+      entityBuilder.addAllProperties(convertProperties(from));
+    }
+    if (from.hasParents() && s.isToBeSet("parent")) {
+      entityBuilder.addAllParents(convert(from.getParents()));
+    }
+    if (from.hasFileProperties()) {
+      FileDescriptor.Builder fileDescriptor = convert(s, from.getFileProperties());
+      if (fileDescriptor != null) {
+        entityBuilder.setFileDescriptor(fileDescriptor);
+      }
+    }
+
+    final EntityResponse.Builder responseBuilder = EntityResponse.newBuilder();
+    responseBuilder.setEntity(entityBuilder);
+
+    appendMessages(from, responseBuilder);
+
+    return responseBuilder;
+  }
+
+  private FileDescriptor.Builder convert(SerializeFieldStrategy s, FileProperties fileProperties) {
+    // @review Florian Spreckelsen 2022-03-22
+    FileDescriptor.Builder result = null;
+    if (s.isToBeSet("path")) {
+      result = FileDescriptor.newBuilder();
+      result.setPath(fileProperties.getPath());
+    }
+    if (s.isToBeSet("size")) {
+      if (result == null) {
+        result = FileDescriptor.newBuilder();
+      }
+      result.setSize(fileProperties.getSize());
+    }
+    return result;
+  }
+
+  private EntityRole convert(final Role role) {
+    switch (role) {
+      case RecordType:
+        return EntityRole.ENTITY_ROLE_RECORD_TYPE;
+      case Record:
+        return EntityRole.ENTITY_ROLE_RECORD;
+      case Property:
+        return EntityRole.ENTITY_ROLE_PROPERTY;
+      case File:
+        return EntityRole.ENTITY_ROLE_FILE;
+      default:
+        return EntityRole.ENTITY_ROLE_UNSPECIFIED;
+    }
+  }
+
+  public Iterable<? extends org.caosdb.api.entity.v1.Message> convert(
+      final List<Message> messages) {
+    final List<org.caosdb.api.entity.v1.Message> result = new LinkedList<>();
+    for (final Message m : messages) {
+      result.add(convert(m));
+    }
+    return result;
+  }
+
+  public org.caosdb.api.entity.v1.Message convert(final Message m) {
+    final org.caosdb.api.entity.v1.Message.Builder builder =
+        org.caosdb.api.entity.v1.Message.newBuilder();
+    final MessageCode code = getMessageCode(m);
+    builder.setCode(code.getNumber());
+    builder.setDescription(m.getDescription());
+    return builder.build();
+  }
+
+  public static MessageCode getMessageCode(final Message m) {
+    return m.getCode();
+  }
+
+  public Version convert(final org.caosdb.server.entity.Version from) {
+    final org.caosdb.api.entity.v1.Version.Builder builder = Version.newBuilder();
+
+    builder.setId(from.getId());
+    return builder.build();
+  }
+
+  public Parent convert(final org.caosdb.server.entity.wrapper.Parent from) {
+    final org.caosdb.api.entity.v1.Parent.Builder builder = Parent.newBuilder();
+    if (from.hasId()) {
+      builder.setId(from.getId().toString());
+    }
+    if (from.hasName()) {
+      builder.setName(from.getName());
+    }
+    if (from.hasDescription()) {
+      builder.setDescription(from.getDescription());
+    }
+
+    return builder.build();
+  }
+
+  public org.caosdb.api.entity.v1.Property convert(final Property from) {
+    // @review Florian Spreckelsen 2022-03-22
+    final org.caosdb.api.entity.v1.Property.Builder builder =
+        org.caosdb.api.entity.v1.Property.newBuilder();
+
+    SerializeFieldStrategy s = from.getSerializeFieldStrategy();
+    if (from.hasId() && s.isToBeSet("id")) {
+      builder.setId(from.getId().toString());
+    }
+    if (from.hasName() && s.isToBeSet("name")) {
+      builder.setName(from.getName());
+    }
+    if (from.hasDescription() && s.isToBeSet("description")) {
+      builder.setDescription(from.getDescription());
+    }
+    if (from.hasDatatype() && s.isToBeSet("datatype")) {
+      builder.setDataType(convert(from.getDatatype()));
+    }
+    final String unit = getStringUnit(from);
+    if (unit != null && s.isToBeSet("unit")) {
+      builder.setUnit(unit);
+    }
+    if (from.hasValue() && s.isToBeSet("value")) {
+      try {
+        from.parseValue();
+      } catch (final Message e) {
+        // ignore. This problem should be handled elsewhere because this is
+        // only for the serialization of the data and not for the validation.
+        // In any case, the string representation can be used.
+      }
+      builder.setValue(convert(from.getValue()));
+    }
+    if (s.isToBeSet("importance")) {
+      builder.setImportance(convert(from.getStatementStatus()));
+    }
+    return builder.build();
+  }
+
+  private org.caosdb.api.entity.v1.Value.Builder convert(final Value value) {
+    if (value instanceof CollectionValue) {
+      return convertCollectionValue((CollectionValue) value);
+    }
+    final org.caosdb.api.entity.v1.Value.Builder builder =
+        org.caosdb.api.entity.v1.Value.newBuilder();
+    builder.setScalarValue(convertScalarValue(value));
+    return builder;
+  }
+
+  protected ScalarValue.Builder convertScalarValue(final Value value) {
+
+    if (value instanceof BooleanValue) {
+      return convertBooleanValue((BooleanValue) value);
+
+    } else if (value instanceof ReferenceValue) {
+      return convertReferenceValue((ReferenceValue) value);
+
+    } else if (value instanceof DateTimeInterface) {
+      return convertDateTimeInterface((DateTimeInterface) value);
+
+    } else if (value instanceof GenericValue) {
+      return convertGenericValue((GenericValue) value);
+    }
+    return null;
+  }
+
+  private ScalarValue.Builder convertGenericValue(final GenericValue value) {
+    final Object wrappedValue = value.getValue();
+    if (wrappedValue instanceof Double) {
+      return ScalarValue.newBuilder().setDoubleValue((Double) wrappedValue);
+    } else if (wrappedValue instanceof Integer) {
+      return ScalarValue.newBuilder().setIntegerValue((Integer) wrappedValue);
+    } else {
+      return convertStringValue(value.toString());
+    }
+  }
+
+  private org.caosdb.api.entity.v1.ScalarValue.Builder convertStringValue(final String value) {
+    if (value.isEmpty()) {
+      return ScalarValue.newBuilder().setSpecialValue(SpecialValue.SPECIAL_VALUE_EMPTY_STRING);
+    }
+    return ScalarValue.newBuilder().setStringValue(value);
+  }
+
+  private org.caosdb.api.entity.v1.ScalarValue.Builder convertDateTimeInterface(
+      final DateTimeInterface value) {
+    return convertStringValue(value.toDateTimeString(timeZone));
+  }
+
+  private org.caosdb.api.entity.v1.ScalarValue.Builder convertBooleanValue(
+      final BooleanValue value) {
+    return ScalarValue.newBuilder().setBooleanValue(value.getValue());
+  }
+
+  private ScalarValue.Builder convertReferenceValue(final ReferenceValue value) {
+    return convertStringValue(value.toString());
+  }
+
+  private org.caosdb.api.entity.v1.Value.Builder convertCollectionValue(
+      final CollectionValue value) {
+
+    final org.caosdb.api.entity.v1.Value.Builder builder =
+        org.caosdb.api.entity.v1.Value.newBuilder();
+    final List<ScalarValue> values = new LinkedList<>();
+    value.forEach(
+        (v) -> {
+          values.add(convertScalarValue(v));
+        });
+    builder.setListValues(CollectionValues.newBuilder().addAllValues(values));
+    return builder;
+  }
+
+  private ScalarValue convertScalarValue(final IndexedSingleValue v) {
+    if (v == null || v.getWrapped() == null) {
+      return ScalarValue.newBuilder()
+          .setSpecialValue(SpecialValue.SPECIAL_VALUE_UNSPECIFIED)
+          .build();
+    }
+    return convertScalarValue(v.getWrapped()).build();
+  }
+
+  private Importance convert(final StatementStatus statementStatus) {
+    switch (statementStatus) {
+      case FIX:
+        return Importance.IMPORTANCE_FIX;
+      case OBLIGATORY:
+        return Importance.IMPORTANCE_OBLIGATORY;
+      case RECOMMENDED:
+        return Importance.IMPORTANCE_RECOMMENDED;
+      case SUGGESTED:
+        return Importance.IMPORTANCE_SUGGESTED;
+      default:
+        return null;
+    }
+  }
+
+  private org.caosdb.api.entity.v1.DataType.Builder convert(final AbstractDatatype datatype) {
+    if (datatype instanceof ReferenceDatatype2) {
+      return DataType.newBuilder()
+          .setReferenceDataType(convertReferenceDatatype((ReferenceDatatype2) datatype));
+    } else if (datatype instanceof FileDatatype) {
+      return DataType.newBuilder()
+          .setReferenceDataType(convertReferenceDatatype((FileDatatype) datatype));
+    } else if (datatype instanceof ReferenceDatatype) {
+      return DataType.newBuilder()
+          .setReferenceDataType(convertReferenceDatatype((ReferenceDatatype) datatype));
+    } else if (datatype instanceof AbstractCollectionDatatype) {
+      return DataType.newBuilder()
+          .setListDataType(
+              convertAbstractCollectionDatatype((AbstractCollectionDatatype) datatype));
+    } else if (datatype instanceof BooleanDatatype) {
+      return DataType.newBuilder()
+          .setAtomicDataType(convertBooleanDatatype((BooleanDatatype) datatype));
+
+    } else if (datatype instanceof DateTimeDatatype) {
+      return DataType.newBuilder()
+          .setAtomicDataType(convertDateTimeDatatype((DateTimeDatatype) datatype));
+
+    } else if (datatype instanceof DoubleDatatype) {
+      return DataType.newBuilder()
+          .setAtomicDataType(convertDoubleDatatype((DoubleDatatype) datatype));
+
+    } else if (datatype instanceof IntegerDatatype) {
+      return DataType.newBuilder()
+          .setAtomicDataType(convertIntegerDatatype((IntegerDatatype) datatype));
+
+    } else if (datatype instanceof TextDatatype) {
+      return DataType.newBuilder().setAtomicDataType(convertTextDatatype((TextDatatype) datatype));
+    }
+    return null;
+  }
+
+  private AtomicDataType convertTextDatatype(final TextDatatype datatype) {
+    return AtomicDataType.ATOMIC_DATA_TYPE_TEXT;
+  }
+
+  private AtomicDataType convertIntegerDatatype(final IntegerDatatype datatype) {
+    return AtomicDataType.ATOMIC_DATA_TYPE_INTEGER;
+  }
+
+  private AtomicDataType convertDoubleDatatype(final DoubleDatatype datatype) {
+    return AtomicDataType.ATOMIC_DATA_TYPE_DOUBLE;
+  }
+
+  private AtomicDataType convertDateTimeDatatype(final DateTimeDatatype datatype) {
+    return AtomicDataType.ATOMIC_DATA_TYPE_DATETIME;
+  }
+
+  private AtomicDataType convertBooleanDatatype(final BooleanDatatype datatype) {
+    return AtomicDataType.ATOMIC_DATA_TYPE_BOOLEAN;
+  }
+
+  private org.caosdb.api.entity.v1.ListDataType.Builder convertAbstractCollectionDatatype(
+      final AbstractCollectionDatatype collectionDatatype) {
+
+    final org.caosdb.api.entity.v1.ListDataType.Builder listBuilder = ListDataType.newBuilder();
+    final AbstractDatatype datatype = collectionDatatype.getDatatype();
+    if (datatype instanceof ReferenceDatatype) {
+      listBuilder.setReferenceDataType(convertReferenceDatatype((ReferenceDatatype) datatype));
+    } else if (datatype instanceof BooleanDatatype) {
+      return listBuilder.setAtomicDataType(convertBooleanDatatype((BooleanDatatype) datatype));
+
+    } else if (datatype instanceof DateTimeDatatype) {
+      return listBuilder.setAtomicDataType(convertDateTimeDatatype((DateTimeDatatype) datatype));
+
+    } else if (datatype instanceof DoubleDatatype) {
+      return listBuilder.setAtomicDataType(convertDoubleDatatype((DoubleDatatype) datatype));
+
+    } else if (datatype instanceof IntegerDatatype) {
+      return listBuilder.setAtomicDataType(convertIntegerDatatype((IntegerDatatype) datatype));
+
+    } else if (datatype instanceof TextDatatype) {
+      return listBuilder.setAtomicDataType(convertTextDatatype((TextDatatype) datatype));
+    }
+    return listBuilder;
+  }
+
+  private org.caosdb.api.entity.v1.ReferenceDataType.Builder convertReferenceDatatype(
+      final ReferenceDatatype datatype) {
+    return ReferenceDataType.newBuilder().setName(datatype.getName());
+  }
+
+  public Iterable<? extends org.caosdb.api.entity.v1.Property> convertProperties(
+      final EntityInterface from) {
+    // @review Florian Spreckelsen 2022-03-22
+    final Iterator<org.caosdb.server.entity.wrapper.Property> iterator =
+        from.getProperties().iterator();
+    return () ->
+        new Iterator<>() {
+
+          private Property property;
+
+          @Override
+          public boolean hasNext() {
+            while (iterator.hasNext()) {
+              this.property = iterator.next();
+              if (from.getSerializeFieldStrategy().isToBeSet(this.property.getName())) {
+                this.property.setSerializeFieldStrategy(
+                    from.getSerializeFieldStrategy().forProperty(this.property));
+                return true;
+              }
+            }
+            return false;
+          }
+
+          @Override
+          public org.caosdb.api.entity.v1.Property next() {
+            if (this.property == null) {
+              // trigger this.property to be non-null
+              if (!hasNext()) {
+                throw new NoSuchElementException("The iterator has no more elements.");
+              }
+            }
+
+            Property next_property = this.property;
+            this.property = null;
+
+            return convert(next_property);
+          }
+        };
+  }
+
+  public Iterable<? extends Parent> convert(final ParentContainer from) {
+    final Iterator<org.caosdb.server.entity.wrapper.Parent> iterator = from.iterator();
+    return () ->
+        new Iterator<>() {
+
+          @Override
+          public boolean hasNext() {
+            return iterator.hasNext();
+          }
+
+          @Override
+          public Parent next() {
+            return convert(iterator.next());
+          }
+        };
+  }
+
+  public void appendMessages(
+      final EntityInterface from, final org.caosdb.api.entity.v1.EntityResponse.Builder builder) {
+    // @review Florian Spreckelsen 2022-03-22
+    SerializeFieldStrategy s = from.getSerializeFieldStrategy();
+    if (from.hasMessage(Message.MessageType.Error.toString()) && s.isToBeSet("error")) {
+      builder.addAllErrors(convert(from.getMessages(Message.MessageType.Error.toString())));
+    }
+    if (from.hasMessage(Message.MessageType.Warning.toString()) && s.isToBeSet("warning")) {
+      builder.addAllWarnings(convert(from.getMessages(Message.MessageType.Warning.toString())));
+    }
+    if (from.hasMessage(Message.MessageType.Info.toString()) && s.isToBeSet("info")) {
+      builder.addAllInfos(convert(from.getMessages(Message.MessageType.Info.toString())));
+    }
+  }
+
+  public void appendMessages(
+      final EntityInterface from, final org.caosdb.api.entity.v1.IdResponse.Builder builder) {
+    // @review Florian Spreckelsen 2022-03-22
+    SerializeFieldStrategy s = from.getSerializeFieldStrategy();
+    if (from.hasMessage(Message.MessageType.Error.toString()) && s.isToBeSet("error")) {
+      builder.addAllErrors(convert(from.getMessages(Message.MessageType.Error.toString())));
+    }
+    if (from.hasMessage(Message.MessageType.Warning.toString()) && s.isToBeSet("warning")) {
+      builder.addAllWarnings(convert(from.getMessages(Message.MessageType.Warning.toString())));
+    }
+    if (from.hasMessage(Message.MessageType.Info.toString()) && s.isToBeSet("info")) {
+      builder.addAllInfos(convert(from.getMessages(Message.MessageType.Info.toString())));
+    }
+  }
+
+  public EntityACL convertACL(EntityInterface e) {
+    EntityACL.Builder builder = EntityACL.newBuilder();
+    builder.setId(e.getId().toString());
+    if (e.hasEntityACL()) {
+      builder.addAllRules(convert(e.getEntityACL(), true));
+    }
+    builder.addAllRules(convert(org.caosdb.server.permissions.EntityACL.GLOBAL_PERMISSIONS, false));
+    EntityAclPermission entityAclPermission = getCurrentACLPermission(e);
+    if (entityAclPermission != null) {
+      builder.setPermission(entityAclPermission);
+    }
+    return builder.build();
+  }
+
+  private org.caosdb.api.entity.v1.EntityAclPermission getCurrentACLPermission(EntityInterface e) {
+    if (e.hasPermission(EntityPermission.EDIT_PRIORITY_ACL)) {
+      return EntityAclPermission.ENTITY_ACL_PERMISSION_EDIT_PRIORITY_ACL;
+    }
+    if (e.hasPermission(EntityPermission.EDIT_ACL)) {
+      return EntityAclPermission.ENTITY_ACL_PERMISSION_EDIT_ACL;
+    }
+    return null;
+  }
+
+  private Iterable<? extends EntityPermissionRule> convert(
+      org.caosdb.server.permissions.EntityACL entityACL, boolean deletable) {
+    List<EntityPermissionRule> result = new LinkedList<>();
+    for (EntityACI aci : entityACL.getRules()) {
+      EntityPermissionRule.Builder builder =
+          EntityPermissionRule.newBuilder()
+              .setGrant(aci.isGrant())
+              .setPriority(aci.isPriority())
+              .setRole(aci.getResponsibleAgent().toString())
+              .addAllPermissions(convert(aci));
+      if (deletable) {
+        builder.addCapabilities(
+            EntityPermissionRuleCapability.ENTITY_PERMISSION_RULE_CAPABILITY_DELETE);
+      }
+      result.add(builder.build());
+    }
+    return result;
+  }
+
+  private Iterable<org.caosdb.api.entity.v1.EntityPermission> convert(EntityACI aci) {
+    List<org.caosdb.api.entity.v1.EntityPermission> result = new LinkedList<>();
+
+    for (EntityPermission p : aci.getPermission()) {
+      result.add(p.getMapping());
+    }
+    return result;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/DownloadBuffer.java b/src/main/java/org/caosdb/server/grpc/DownloadBuffer.java
new file mode 100644
index 0000000000000000000000000000000000000000..06c387624c887f5ceeb73efb3ee7b042519a1a6f
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/DownloadBuffer.java
@@ -0,0 +1,72 @@
+package org.caosdb.server.grpc;
+
+import com.google.protobuf.ByteString;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileChannel.MapMode;
+import org.caosdb.api.entity.v1.FileChunk;
+import org.caosdb.api.entity.v1.FileDownloadResponse;
+import org.caosdb.api.entity.v1.TransmissionStatus;
+import org.caosdb.server.entity.FileProperties;
+
+public class DownloadBuffer {
+
+  private final FileProperties file_properties;
+  private FileInputStream fileInputStream;
+  private FileChannel fileChannel;
+
+  public DownloadBuffer(final FileProperties file_properties) {
+    this.file_properties = file_properties;
+    this.fileInputStream = null;
+  }
+
+  public FileProperties getFileProperties() {
+    return file_properties;
+  }
+
+  public FileDownloadResponse getNextChunk() throws FileNotFoundException, IOException {
+    if (fileChannel == null) {
+      fileInputStream = new FileInputStream(file_properties.getFile());
+      fileChannel = fileInputStream.getChannel();
+    }
+    final long position = fileChannel.position();
+    final long unread_bytes = fileChannel.size() - position;
+    final long next_chunk_size = Math.min(unread_bytes, getChunkSize());
+
+    final MappedByteBuffer map = fileChannel.map(MapMode.READ_ONLY, position, next_chunk_size);
+    fileChannel.position(position + next_chunk_size);
+
+    final FileChunk.Builder builder = FileChunk.newBuilder();
+    builder.setData(ByteString.copyFrom(map));
+
+    final TransmissionStatus status;
+    if (fileInputStream.available() > 0) {
+      status = TransmissionStatus.TRANSMISSION_STATUS_GO_ON;
+    } else {
+      status = TransmissionStatus.TRANSMISSION_STATUS_SUCCESS;
+      cleanUp();
+    }
+    return FileDownloadResponse.newBuilder().setChunk(builder).setStatus(status).build();
+  }
+
+  public void cleanUp() {
+    try {
+      if (fileChannel != null && fileChannel.isOpen()) {
+        fileChannel.close();
+      }
+      if (fileInputStream != null) {
+        fileInputStream.close();
+      }
+    } catch (final IOException e) {
+      e.printStackTrace();
+    }
+  }
+
+  private int getChunkSize() {
+    // 16kB
+    return 16384;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/EntityTransactionServiceImpl.java b/src/main/java/org/caosdb/server/grpc/EntityTransactionServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..50845c5456c98ee59a1b4d1a05bae0e84d712774
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/EntityTransactionServiceImpl.java
@@ -0,0 +1,465 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.grpc;
+
+import io.grpc.stub.StreamObserver;
+import java.util.HashMap;
+import java.util.TimeZone;
+import java.util.UUID;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authc.AuthenticationException;
+import org.caosdb.api.entity.v1.DeleteRequest;
+import org.caosdb.api.entity.v1.DeleteResponse;
+import org.caosdb.api.entity.v1.Entity;
+import org.caosdb.api.entity.v1.EntityRequest;
+import org.caosdb.api.entity.v1.EntityResponse;
+import org.caosdb.api.entity.v1.EntityTransactionServiceGrpc.EntityTransactionServiceImplBase;
+import org.caosdb.api.entity.v1.IdResponse;
+import org.caosdb.api.entity.v1.InsertRequest;
+import org.caosdb.api.entity.v1.InsertResponse;
+import org.caosdb.api.entity.v1.MultiRetrieveEntityACLRequest;
+import org.caosdb.api.entity.v1.MultiRetrieveEntityACLResponse;
+import org.caosdb.api.entity.v1.MultiTransactionRequest;
+import org.caosdb.api.entity.v1.MultiTransactionResponse;
+import org.caosdb.api.entity.v1.MultiUpdateEntityACLRequest;
+import org.caosdb.api.entity.v1.MultiUpdateEntityACLResponse;
+import org.caosdb.api.entity.v1.RetrieveResponse;
+import org.caosdb.api.entity.v1.TransactionRequest;
+import org.caosdb.api.entity.v1.TransactionRequest.WrappedRequestsCase;
+import org.caosdb.api.entity.v1.TransactionResponse;
+import org.caosdb.api.entity.v1.UpdateRequest;
+import org.caosdb.api.entity.v1.UpdateResponse;
+import org.caosdb.server.CaosDBException;
+import org.caosdb.server.entity.DeleteEntity;
+import org.caosdb.server.entity.EntityInterface;
+import org.caosdb.server.entity.FileProperties;
+import org.caosdb.server.entity.InsertEntity;
+import org.caosdb.server.entity.Message;
+import org.caosdb.server.entity.RetrieveEntity;
+import org.caosdb.server.entity.UpdateEntity;
+import org.caosdb.server.entity.container.RetrieveContainer;
+import org.caosdb.server.entity.container.WritableContainer;
+import org.caosdb.server.permissions.EntityPermission;
+import org.caosdb.server.transaction.Retrieve;
+import org.caosdb.server.transaction.RetrieveACL;
+import org.caosdb.server.transaction.UpdateACL;
+import org.caosdb.server.transaction.WriteTransaction;
+import org.caosdb.server.utils.ServerMessages;
+
+public class EntityTransactionServiceImpl extends EntityTransactionServiceImplBase {
+
+  // TODO(tf) let the clients define the time zone of the date time values which are being returned.
+  private final CaosDBToGrpcConverters caosdbToGrpc =
+      new CaosDBToGrpcConverters(TimeZone.getDefault());
+  private final GrpcToCaosDBConverters grpcToCaosdb = new GrpcToCaosDBConverters();
+  private final FileTransmissionServiceImpl fileTransmissionService;
+
+  public EntityTransactionServiceImpl(final FileTransmissionServiceImpl fileTransmissionService) {
+    this.fileTransmissionService = fileTransmissionService;
+  }
+
+  /**
+   * Handle read-only transactions. Of these only one may be a query at the moment, the others must
+   * be ID retrieves.
+   *
+   * @param request
+   * @return
+   * @throws Exception
+   */
+  public MultiTransactionResponse retrieve(final MultiTransactionRequest request) throws Exception {
+    // @review Florian Spreckelsen 2022-03-22
+    final MultiTransactionResponse.Builder builder = MultiTransactionResponse.newBuilder();
+    final RetrieveContainer container =
+        new RetrieveContainer(
+            SecurityUtils.getSubject(), getTimestamp(), getSRID(), new HashMap<>());
+    FileDownload fileDownload = null;
+
+    for (final TransactionRequest sub_request : request.getRequestsList()) {
+      if (sub_request.getWrappedRequestsCase() != WrappedRequestsCase.RETRIEVE_REQUEST) {
+        throw new CaosDBException(
+            "Cannot process a "
+                + sub_request.getWrappedRequestsCase().name()
+                + " in a read-only request.");
+      }
+      final boolean isFileDownload = sub_request.getRetrieveRequest().getRegisterFileDownload();
+      if (sub_request.getRetrieveRequest().hasQuery() // Retrieves are either queries...
+          && !sub_request.getRetrieveRequest().getQuery().getQuery().isBlank()) {
+        final String query = sub_request.getRetrieveRequest().getQuery().getQuery();
+        if (container.getFlags().containsKey("query")) { // Check for more than one query request.
+          throw new CaosDBException("Cannot process more than one query request.");
+        }
+        container.getFlags().put("query", query);
+        if (isFileDownload) {
+          container.getFlags().put("download_files", "true");
+        }
+      } else { // or ID retrieves.
+        final String id = sub_request.getRetrieveRequest().getId();
+        if (!id.isBlank()) {
+          try {
+            final RetrieveEntity entity = new RetrieveEntity(grpcToCaosdb.getId(id));
+            if (isFileDownload) {
+              entity.setFlag("download_files", "true");
+            }
+            container.add(entity);
+          } catch (final NumberFormatException e) {
+            // We handle this after the retrieval
+          }
+        }
+      }
+    }
+
+    final Retrieve transaction = new Retrieve(container);
+    transaction.execute();
+    if (container.getFlags().containsKey("query_count_result")) {
+      final int count = Integer.parseInt(container.getFlags().get("query_count_result"));
+      builder
+          .addResponsesBuilder()
+          .setRetrieveResponse(RetrieveResponse.newBuilder().setCountResult(count));
+    } else {
+      final boolean download_files_container = container.getFlags().containsKey("download_files");
+      for (final EntityInterface entity : container) {
+        final EntityResponse.Builder entityResponse = caosdbToGrpc.convert(entity);
+        if ((download_files_container || entity.getFlags().containsKey("download_files"))
+            && entity.hasFileProperties()) {
+          try {
+            entity.checkPermission(EntityPermission.RETRIEVE_FILE);
+            if (fileDownload == null) {
+              fileDownload = fileTransmissionService.registerFileDownload(null);
+            }
+            entity.getFileProperties().retrieveFromFileSystem();
+            entityResponse.setDownloadId(
+                fileTransmissionService.registerFileDownload(
+                    fileDownload.getId(), entity.getFileProperties()));
+          } catch (AuthenticationException exc) {
+            entityResponse.addErrors(caosdbToGrpc.convert(ServerMessages.AUTHORIZATION_ERROR));
+            entityResponse.addInfos(caosdbToGrpc.convert(new Message(exc.getMessage())));
+          }
+        }
+        builder
+            .addResponsesBuilder()
+            .setRetrieveResponse(RetrieveResponse.newBuilder().setEntityResponse(entityResponse));
+      }
+    }
+
+    // Add those entities which have not been retrieved because they have a string id
+    for (final TransactionRequest sub_request : request.getRequestsList()) {
+      final String id = sub_request.getRetrieveRequest().getId();
+      if (!id.isBlank()) {
+        try {
+          grpcToCaosdb.getId(id);
+        } catch (final NumberFormatException e) {
+          // ID wasn't an integer - the server doesn't support string ids yet, so that entity
+          // cannot exist.
+          builder.addResponses(
+              TransactionResponse.newBuilder()
+                  .setRetrieveResponse(
+                      RetrieveResponse.newBuilder().setEntityResponse(entityDoesNotExist(id))));
+        }
+      }
+    }
+    return builder.build();
+  }
+
+  private EntityResponse entityDoesNotExist(final String id) {
+    return EntityResponse.newBuilder()
+        .addErrors(caosdbToGrpc.convert(ServerMessages.ENTITY_DOES_NOT_EXIST))
+        .setEntity(Entity.newBuilder().setId(id))
+        .build();
+  }
+
+  private String getSRID() {
+    return UUID.randomUUID().toString();
+  }
+
+  private Long getTimestamp() {
+    return System.currentTimeMillis();
+  }
+
+  /**
+   * Handle all entity transactions.
+   *
+   * <p>Currently either all requests must be read-only/retrieve requests, or none of the requests.
+   *
+   * @param request
+   * @return
+   * @throws Exception
+   */
+  public MultiTransactionResponse transaction(final MultiTransactionRequest request)
+      throws Exception {
+    if (request.getRequestsCount() > 0) {
+      // We only test the first request and raise errors when subsequent sub-transactions do not fit
+      // into the retrieve context.  Currently this means that either all or none of the requests
+      // must be retrieve requests.
+      final WrappedRequestsCase requestCase = request.getRequests(0).getWrappedRequestsCase();
+      switch (requestCase) {
+        case RETRIEVE_REQUEST:
+          // Handle read-only transactions.
+          return retrieve(request);
+        default:
+          // Handle mixed-writed transactions.
+          return write(request);
+      }
+    } else {
+      // empty request, empty response.
+      return MultiTransactionResponse.newBuilder().build();
+    }
+  }
+
+  /**
+   * Handle mixed-write transactions.
+   *
+   * <p>The current implementation fails fast, without attempts to execute a single request, if
+   * there are requests with non-integer IDs. This will change in the near future, once string IDs
+   * are supported by the server.
+   *
+   * @param requests
+   * @return
+   * @throws Exception
+   */
+  private MultiTransactionResponse write(final MultiTransactionRequest requests) throws Exception {
+    final MultiTransactionResponse.Builder builder = MultiTransactionResponse.newBuilder();
+    final WritableContainer container =
+        new WritableContainer(
+            SecurityUtils.getSubject(), getTimestamp(), getSRID(), new HashMap<String, String>());
+
+    // put entities into the transaction object
+    for (final TransactionRequest subRequest : requests.getRequestsList()) {
+      switch (subRequest.getWrappedRequestsCase()) {
+        case INSERT_REQUEST:
+          {
+            final InsertRequest insertRequest = subRequest.getInsertRequest();
+            final Entity insertEntity = insertRequest.getEntityRequest().getEntity();
+
+            final InsertEntity entity =
+                new InsertEntity(
+                    insertEntity.getName().isEmpty() ? null : insertEntity.getName(),
+                    grpcToCaosdb.convert(insertEntity.getRole()));
+            grpcToCaosdb.convert(insertEntity, entity);
+            addFileUpload(container, entity, insertRequest.getEntityRequest());
+            container.add(entity);
+          }
+          break;
+        case UPDATE_REQUEST:
+          final UpdateRequest updateRequest = subRequest.getUpdateRequest();
+          final Entity updateEntity = updateRequest.getEntityRequest().getEntity();
+
+          try {
+            final UpdateEntity entity =
+                new UpdateEntity(
+                    grpcToCaosdb.getId(updateEntity.getId()), // ID is not handled by grpc convert
+                    grpcToCaosdb.convert(updateEntity.getRole()));
+            grpcToCaosdb.convert(updateEntity, entity);
+            addFileUpload(container, entity, updateRequest.getEntityRequest());
+            container.add(entity);
+          } catch (final NumberFormatException e) {
+            // ID wasn't an integer
+            return failedWriteDueToStringId(requests);
+          }
+          break;
+        case DELETE_REQUEST:
+          final DeleteRequest deleteRequest = subRequest.getDeleteRequest();
+          try {
+            final DeleteEntity entity = new DeleteEntity(grpcToCaosdb.getId(deleteRequest.getId()));
+            container.add(entity);
+
+          } catch (final NumberFormatException e) {
+            // ID wasn't an integer
+            return failedWriteDueToStringId(requests);
+          }
+          break;
+        default:
+          throw new CaosDBException(
+              "Cannot process a "
+                  + subRequest.getWrappedRequestsCase().name()
+                  + " in a write request.");
+      }
+    }
+
+    // execute the transaction
+    final WriteTransaction transaction = new WriteTransaction(container);
+    transaction.setNoIdIsError(false);
+    transaction.execute();
+
+    // put inserted/updated/deleted entities back into the response
+    for (final EntityInterface entity : container) {
+      final IdResponse.Builder idResponse = IdResponse.newBuilder();
+      if (entity.getId() != null) {
+        idResponse.setId(entity.getId().toString());
+      }
+      caosdbToGrpc.appendMessages(entity, idResponse);
+
+      if (entity instanceof InsertEntity) {
+        builder
+            .addResponsesBuilder()
+            .setInsertResponse(InsertResponse.newBuilder().setIdResponse(idResponse));
+      } else if (entity instanceof UpdateEntity) {
+        builder
+            .addResponsesBuilder()
+            .setUpdateResponse(UpdateResponse.newBuilder().setIdResponse(idResponse));
+      } else {
+        builder
+            .addResponsesBuilder()
+            .setDeleteResponse(DeleteResponse.newBuilder().setIdResponse(idResponse));
+      }
+    }
+    return builder.build();
+  }
+
+  /**
+   * Handle a request which contains string id (which cannot be converted to integer ids) and return
+   * a response which has the "ENTITY_DOES_NOT_EXIST" error for all entities with affected ids.
+   *
+   * <p>This does not attempt to execute a single request.
+   *
+   * @param request
+   * @return
+   */
+  private MultiTransactionResponse failedWriteDueToStringId(final MultiTransactionRequest request) {
+    final org.caosdb.api.entity.v1.MultiTransactionResponse.Builder builder =
+        MultiTransactionResponse.newBuilder();
+    for (final TransactionRequest subRequest : request.getRequestsList()) {
+      final IdResponse.Builder idResponse = IdResponse.newBuilder();
+      switch (subRequest.getWrappedRequestsCase()) {
+        case INSERT_REQUEST:
+          builder
+              .addResponsesBuilder()
+              .setInsertResponse(InsertResponse.newBuilder().setIdResponse(idResponse));
+
+          break;
+        case UPDATE_REQUEST:
+          final UpdateRequest updateRequest = subRequest.getUpdateRequest();
+          final Entity updateEntity = updateRequest.getEntityRequest().getEntity();
+
+          idResponse.setId(updateEntity.getId());
+          try {
+            grpcToCaosdb.getId(updateEntity.getId());
+          } catch (final NumberFormatException e) {
+            // ID wasn't an integer
+            idResponse.addErrors(caosdbToGrpc.convert(ServerMessages.ENTITY_DOES_NOT_EXIST));
+          }
+          builder
+              .addResponsesBuilder()
+              .setUpdateResponse(UpdateResponse.newBuilder().setIdResponse(idResponse));
+          break;
+        case DELETE_REQUEST:
+          final DeleteRequest deleteRequest = subRequest.getDeleteRequest();
+          idResponse.setId(deleteRequest.getId());
+          try {
+            grpcToCaosdb.getId(deleteRequest.getId());
+          } catch (final NumberFormatException e) {
+            // ID wasn't an integer
+            idResponse.addErrors(caosdbToGrpc.convert(ServerMessages.ENTITY_DOES_NOT_EXIST));
+          }
+          builder
+              .addResponsesBuilder()
+              .setDeleteResponse(DeleteResponse.newBuilder().setIdResponse(idResponse));
+          break;
+        default:
+          throw new CaosDBException(
+              "Cannot process a "
+                  + subRequest.getWrappedRequestsCase().name()
+                  + " in a write request.");
+      }
+    }
+    return builder.build();
+  }
+
+  private void addFileUpload(
+      final WritableContainer container,
+      final EntityInterface entity,
+      final EntityRequest entityRequest) {
+    if (entityRequest.hasUploadId()) {
+      final FileProperties uploadFile =
+          fileTransmissionService.getUploadFile(entityRequest.getUploadId());
+      if (uploadFile == null) {
+        entity.addError(ServerMessages.FILE_HAS_NOT_BEEN_UPLOAED);
+      } else {
+        container.addFile(uploadFile.getTmpIdentifier(), uploadFile);
+        entity.getFileProperties().setTmpIdentifier(uploadFile.getTmpIdentifier());
+      }
+    }
+  }
+
+  private MultiRetrieveEntityACLResponse multiRetrieveEntityACL(
+      MultiRetrieveEntityACLRequest request) throws Exception {
+    MultiRetrieveEntityACLResponse.Builder builder = MultiRetrieveEntityACLResponse.newBuilder();
+    RetrieveACL transaction = new RetrieveACL(request.getIdList());
+    transaction.execute();
+    for (EntityInterface e : transaction.getContainer()) {
+      builder.addAcls(caosdbToGrpc.convertACL(e));
+    }
+    return builder.build();
+  }
+
+  private MultiUpdateEntityACLResponse multiUpdateEntityACL(MultiUpdateEntityACLRequest request)
+      throws Exception {
+    MultiUpdateEntityACLResponse.Builder builder = MultiUpdateEntityACLResponse.newBuilder();
+    UpdateACL transaction = new UpdateACL(grpcToCaosdb.convertAcls(request.getAclsList()));
+    transaction.execute();
+    return builder.build();
+  }
+
+  @Override
+  public void multiTransaction(
+      final MultiTransactionRequest request,
+      final StreamObserver<MultiTransactionResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final MultiTransactionResponse response = transaction(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      AccessControlManagementServiceImpl.handleException(responseObserver, e);
+    }
+  }
+
+  @Override
+  public void multiRetrieveEntityACL(
+      MultiRetrieveEntityACLRequest request,
+      StreamObserver<MultiRetrieveEntityACLResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final MultiRetrieveEntityACLResponse response = multiRetrieveEntityACL(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      AccessControlManagementServiceImpl.handleException(responseObserver, e);
+    }
+  }
+
+  @Override
+  public void multiUpdateEntityACL(
+      MultiUpdateEntityACLRequest request,
+      StreamObserver<MultiUpdateEntityACLResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final MultiUpdateEntityACLResponse response = multiUpdateEntityACL(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      AccessControlManagementServiceImpl.handleException(responseObserver, e);
+    }
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/FileDownload.java b/src/main/java/org/caosdb/server/grpc/FileDownload.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f3c79899dac73174c65b14e01dbc08c71f1a243
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/FileDownload.java
@@ -0,0 +1,59 @@
+package org.caosdb.server.grpc;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import org.caosdb.api.entity.v1.FileDownloadResponse;
+import org.caosdb.api.entity.v1.FileTransmissionSettings;
+import org.caosdb.server.entity.FileProperties;
+import org.caosdb.server.utils.Utils;
+
+public class FileDownload extends FileTransmission {
+
+  Map<String, DownloadBuffer> buffers = new HashMap<>();
+  private final FileTransmissionSettings settings;
+
+  public FileDownload(final FileTransmissionSettings settings, final String id) throws Exception {
+    super(id);
+    this.settings = settings;
+  }
+
+  @Override
+  public void cleanUp() {
+    buffers.forEach(
+        (id, buffer) -> {
+          buffer.cleanUp();
+        });
+  }
+
+  @Override
+  public FileProperties getFile(final String fileId) {
+    synchronized (lock) {
+      touch();
+      return buffers.get(fileId).getFileProperties();
+    }
+  }
+
+  @Override
+  public FileTransmissionSettings getTransmissionSettings() {
+    return settings;
+  }
+
+  public String append(final FileProperties fp) {
+    synchronized (lock) {
+      touch();
+      final String id = Utils.getUID();
+      buffers.put(id, new DownloadBuffer(fp));
+      return id;
+    }
+  }
+
+  public FileDownloadResponse getNextChunk(final String fileId)
+      throws FileNotFoundException, IOException {
+    synchronized (lock) {
+      touch();
+      return buffers.get(fileId).getNextChunk();
+    }
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/FileDownloadRegistration.java b/src/main/java/org/caosdb/server/grpc/FileDownloadRegistration.java
new file mode 100644
index 0000000000000000000000000000000000000000..5130404024e52ec0ac36745452a609b01764dc0d
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/FileDownloadRegistration.java
@@ -0,0 +1,71 @@
+package org.caosdb.server.grpc;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.caosdb.api.entity.v1.FileDownloadResponse;
+import org.caosdb.api.entity.v1.FileTransmissionId;
+import org.caosdb.api.entity.v1.FileTransmissionSettings;
+import org.caosdb.server.entity.FileProperties;
+import org.caosdb.server.utils.Utils;
+
+public class FileDownloadRegistration {
+
+  private final Map<String, FileDownload> registeredDownloads = new HashMap<>();
+
+  public FileTransmissionId registerFileDownload(
+      final String registration_id, final FileProperties fp) {
+    synchronized (registeredDownloads) {
+      final String file_id = registeredDownloads.get(registration_id).append(fp);
+      return FileTransmissionId.newBuilder()
+          .setRegistrationId(registration_id)
+          .setFileId(file_id)
+          .build();
+    }
+  }
+
+  public FileDownload registerFileDownload(final FileTransmissionSettings settings)
+      throws Exception {
+    final FileDownload result = new FileDownload(settings, Utils.getUID());
+    register(result);
+    return result;
+  }
+
+  private void register(final FileDownload fileTransmission) {
+    synchronized (registeredDownloads) {
+      registeredDownloads.put(fileTransmission.getId(), fileTransmission);
+    }
+  }
+
+  public FileDownloadResponse downloadNextChunk(final FileTransmissionId fileTransmissionId)
+      throws FileNotFoundException, IOException {
+    FileDownload next;
+    synchronized (registeredDownloads) {
+      next = registeredDownloads.get(fileTransmissionId.getRegistrationId());
+    }
+    return next.getNextChunk(fileTransmissionId.getFileId());
+  }
+
+  public void cleanUp(final boolean all) {
+    synchronized (registeredDownloads) {
+      final List<String> cleanUp = new LinkedList<>();
+      for (final Entry<String, FileDownload> entry : registeredDownloads.entrySet()) {
+        if (all || entry.getValue().isExpired()) {
+          cleanUp.add(entry.getKey());
+        }
+      }
+      for (final String key : cleanUp) {
+        registeredDownloads.get(key).cleanUp();
+        registeredDownloads.remove(key);
+      }
+    }
+  }
+
+  public void cleanUp() {
+    cleanUp(false);
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/FileTransmission.java b/src/main/java/org/caosdb/server/grpc/FileTransmission.java
new file mode 100644
index 0000000000000000000000000000000000000000..67eca23a8f3deb557e0d773e6de7ff0ee38821a2
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/FileTransmission.java
@@ -0,0 +1,62 @@
+package org.caosdb.server.grpc;
+
+import java.util.concurrent.locks.ReentrantLock;
+import org.caosdb.api.entity.v1.FileTransmissionSettings;
+import org.caosdb.api.entity.v1.RegistrationStatus;
+import org.caosdb.server.entity.FileProperties;
+
+public abstract class FileTransmission {
+
+  protected ReentrantLock lock = new ReentrantLock();
+  protected final String id;
+  protected final long createdTimestamp;
+  protected long touchedTimestamp;
+  protected final RegistrationStatus status;
+
+  public FileTransmission(final String id) {
+    this.id = id;
+    this.status = RegistrationStatus.REGISTRATION_STATUS_ACCEPTED;
+    this.createdTimestamp = System.currentTimeMillis();
+    this.touchedTimestamp = createdTimestamp;
+  }
+
+  public abstract void cleanUp();
+
+  public long getCreatedTimestamp() {
+    return createdTimestamp;
+  }
+
+  public long getTouchedTimestamp() {
+    return this.touchedTimestamp;
+  }
+
+  public void touch() {
+    this.touchedTimestamp = System.currentTimeMillis();
+  }
+
+  public String getId() {
+    return this.id;
+  }
+
+  public RegistrationStatus getRegistrationStatus() {
+    return status;
+  }
+
+  public long getMaxChunkSize() {
+    // 2^24, 16.78 MB
+    return 16777216;
+  }
+
+  public long getMaxFileSize() {
+    // 2^30, 1.074 GB
+    return 1073741824;
+  }
+
+  public abstract FileProperties getFile(final String fileId);
+
+  public abstract FileTransmissionSettings getTransmissionSettings();
+
+  public boolean isExpired() {
+    return System.currentTimeMillis() - touchedTimestamp > 10 * 60 * 1000; // older than 10 min
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/FileTransmissionServiceImpl.java b/src/main/java/org/caosdb/server/grpc/FileTransmissionServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..1f549b37a33f26c2f4ecdc5efc1258a3bb602d50
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/FileTransmissionServiceImpl.java
@@ -0,0 +1,153 @@
+package org.caosdb.server.grpc;
+
+import io.grpc.stub.StreamObserver;
+import java.io.IOException;
+import org.caosdb.api.entity.v1.FileChunk;
+import org.caosdb.api.entity.v1.FileDownloadRequest;
+import org.caosdb.api.entity.v1.FileDownloadResponse;
+import org.caosdb.api.entity.v1.FileTransmissionId;
+import org.caosdb.api.entity.v1.FileTransmissionServiceGrpc.FileTransmissionServiceImplBase;
+import org.caosdb.api.entity.v1.FileTransmissionSettings;
+import org.caosdb.api.entity.v1.FileUploadRequest;
+import org.caosdb.api.entity.v1.FileUploadResponse;
+import org.caosdb.api.entity.v1.RegisterFileUploadRequest;
+import org.caosdb.api.entity.v1.RegisterFileUploadResponse;
+import org.caosdb.api.entity.v1.RegisterFileUploadResponse.Builder;
+import org.caosdb.api.entity.v1.RegistrationStatus;
+import org.caosdb.api.entity.v1.TransmissionStatus;
+import org.caosdb.server.CaosDBServer;
+import org.caosdb.server.entity.FileProperties;
+import org.caosdb.server.utils.CronJob;
+import org.caosdb.server.utils.Utils;
+
+public class FileTransmissionServiceImpl extends FileTransmissionServiceImplBase {
+
+  public class FileUploadStreamObserver implements StreamObserver<FileUploadRequest> {
+
+    private FileUpload fileUpload = null;
+    private FileTransmissionId fileTransmissionId = null;
+    private final StreamObserver<FileUploadResponse> outputStreamObserver;
+    private TransmissionStatus status;
+
+    public FileUploadStreamObserver(final StreamObserver<FileUploadResponse> outputStreamObserver) {
+      this.outputStreamObserver = outputStreamObserver;
+    }
+
+    @Override
+    public void onError(final Throwable throwable) {}
+
+    @Override
+    public void onCompleted() {
+      final FileUploadResponse response = FileUploadResponse.newBuilder().setStatus(status).build();
+      outputStreamObserver.onNext(response);
+      outputStreamObserver.onCompleted();
+    }
+
+    @Override
+    public void onNext(final FileUploadRequest request) {
+      AuthInterceptor.bindSubject();
+      final FileChunk chunk = request.getChunk();
+      if (chunk.hasFileTransmissionId()) {
+        fileUpload =
+            fileUploadRegistration.getFileUpload(chunk.getFileTransmissionId().getRegistrationId());
+        fileTransmissionId = chunk.getFileTransmissionId();
+        if (fileTransmissionId.getFileId().isBlank()) {
+          fileTransmissionId =
+              FileTransmissionId.newBuilder(fileTransmissionId).setFileId(Utils.getUID()).build();
+        }
+      } else {
+
+        try {
+          status = fileUpload.uploadChunk(fileTransmissionId.getFileId(), chunk);
+        } catch (final IOException e) {
+          status = TransmissionStatus.TRANSMISSION_STATUS_ERROR;
+          e.printStackTrace();
+        }
+      }
+    }
+  }
+
+  public FileTransmissionServiceImpl() {
+    CaosDBServer.addPreShutdownHook(
+        () -> {
+          fileDownloadRegistration.cleanUp(true);
+          fileUploadRegistration.cleanUp(true);
+        });
+  }
+
+  FileUploadRegistration fileUploadRegistration = new FileUploadRegistration();
+  FileDownloadRegistration fileDownloadRegistration = new FileDownloadRegistration();
+  CronJob cleanUp =
+      new CronJob(
+          "FileTransmissionCleanUp",
+          () -> {
+            fileUploadRegistration.cleanUp();
+            fileDownloadRegistration.cleanUp();
+          },
+          60,
+          false);
+
+  FileTransmissionId registerFileDownload(final String registration_id, final FileProperties fp)
+      throws Exception {
+    return fileDownloadRegistration.registerFileDownload(registration_id, fp);
+  }
+
+  FileDownload registerFileDownload(final FileTransmissionSettings settings) throws Exception {
+    return fileDownloadRegistration.registerFileDownload(settings);
+  }
+
+  public FileProperties getUploadFile(final FileTransmissionId uploadId) {
+    return fileUploadRegistration.getUploadFile(uploadId);
+  }
+
+  @Override
+  public void registerFileUpload(
+      final RegisterFileUploadRequest request,
+      final StreamObserver<RegisterFileUploadResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      final FileTransmission result = fileUploadRegistration.registerFileUpload();
+      final Builder builder = RegisterFileUploadResponse.newBuilder();
+      builder.setStatus(result.getRegistrationStatus());
+      if (result.getRegistrationStatus() == RegistrationStatus.REGISTRATION_STATUS_ACCEPTED) {
+        builder.setRegistrationId(result.getId());
+        builder.setUploadSettings(result.getTransmissionSettings());
+      }
+
+      final RegisterFileUploadResponse response = builder.build();
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      e.printStackTrace();
+      responseObserver.onError(e);
+    }
+  }
+
+  @Override
+  public StreamObserver<FileUploadRequest> fileUpload(
+      final StreamObserver<FileUploadResponse> responseObserver) {
+    return new FileUploadStreamObserver(responseObserver);
+  }
+
+  @Override
+  public void fileDownload(
+      final FileDownloadRequest request,
+      final StreamObserver<FileDownloadResponse> responseObserver) {
+    try {
+      AuthInterceptor.bindSubject();
+      FileDownloadResponse response =
+          fileDownloadRegistration.downloadNextChunk(request.getFileTransmissionId());
+      responseObserver.onNext(response);
+
+      while (response.getStatus() == TransmissionStatus.TRANSMISSION_STATUS_GO_ON) {
+        response = fileDownloadRegistration.downloadNextChunk(request.getFileTransmissionId());
+        responseObserver.onNext(response);
+      }
+      responseObserver.onCompleted();
+    } catch (final Exception e) {
+      e.printStackTrace();
+      responseObserver.onError(e);
+    }
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/FileUpload.java b/src/main/java/org/caosdb/server/grpc/FileUpload.java
new file mode 100644
index 0000000000000000000000000000000000000000..2ec4ca3de3d5d7ad5b9c44ec16deade8725d9145
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/FileUpload.java
@@ -0,0 +1,79 @@
+package org.caosdb.server.grpc;
+
+import com.google.protobuf.ByteString;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import org.caosdb.api.entity.v1.FileChunk;
+import org.caosdb.api.entity.v1.FileTransmissionSettings;
+import org.caosdb.api.entity.v1.FileTransmissionSettings.Builder;
+import org.caosdb.api.entity.v1.TransmissionStatus;
+import org.caosdb.server.FileSystem;
+import org.caosdb.server.entity.FileProperties;
+
+public class FileUpload extends FileTransmission {
+
+  Map<String, UploadBuffer> buffers = new HashMap<>();
+
+  File tmpDir;
+
+  public FileUpload(final String id) {
+    super(id);
+    this.tmpDir = null;
+  }
+
+  @Override
+  public void cleanUp() {
+    if (tmpDir != null) {
+      org.apache.commons.io.FileUtils.deleteQuietly(tmpDir);
+    }
+  }
+
+  File getTmpDir() {
+    if (tmpDir == null) {
+      tmpDir = new File(FileSystem.getTmp() + id + "/");
+      tmpDir.mkdirs();
+    }
+    return tmpDir;
+  }
+
+  public TransmissionStatus upload(final String fileId, final ByteString data) throws IOException {
+    synchronized (lock) {
+      touch();
+      return getFileBuffer(fileId).write(data);
+    }
+  }
+
+  protected UploadBuffer getFileBuffer(final String fileId) {
+    touch();
+    if (!buffers.containsKey(fileId)) {
+      buffers.put(fileId, new UploadBuffer(getTmpDir().toPath().resolve(fileId).toFile()));
+    }
+    return buffers.get(fileId);
+  }
+
+  @Override
+  public FileProperties getFile(final String fileId) {
+    synchronized (lock) {
+      touch();
+      if (buffers.containsKey(fileId)) {
+        return buffers.get(fileId).toFileProperties(fileId);
+      }
+      return null;
+    }
+  }
+
+  @Override
+  public FileTransmissionSettings getTransmissionSettings() {
+    final Builder builder = FileTransmissionSettings.newBuilder();
+    builder.setMaxChunkSize(getMaxChunkSize());
+    builder.setMaxFileSize(getMaxFileSize());
+    return builder.build();
+  }
+
+  public TransmissionStatus uploadChunk(final String fileId, final FileChunk chunk)
+      throws IOException {
+    return upload(fileId, chunk.getData());
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/FileUploadRegistration.java b/src/main/java/org/caosdb/server/grpc/FileUploadRegistration.java
new file mode 100644
index 0000000000000000000000000000000000000000..bfb25be2ec91cda450be282b89f08ace26a16409
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/FileUploadRegistration.java
@@ -0,0 +1,64 @@
+package org.caosdb.server.grpc;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.caosdb.api.entity.v1.FileTransmissionId;
+import org.caosdb.server.entity.FileProperties;
+import org.caosdb.server.utils.Utils;
+
+public class FileUploadRegistration {
+
+  private final Map<String, FileUpload> registeredUploads = new HashMap<>();
+
+  public FileTransmission registerFileUpload() {
+    final FileUpload result = new FileUpload(Utils.getUID());
+    register(result);
+    return result;
+  }
+
+  private void register(final FileUpload fileTransmission) {
+
+    synchronized (registeredUploads) {
+      registeredUploads.put(fileTransmission.getId(), fileTransmission);
+    }
+  }
+
+  public FileProperties getUploadFile(final FileTransmissionId uploadId) {
+    final String fileId = uploadId.getFileId();
+    final String registrationId = uploadId.getRegistrationId();
+    final FileTransmission fileTransmission;
+
+    synchronized (registeredUploads) {
+      fileTransmission = registeredUploads.get(registrationId);
+    }
+    return fileTransmission.getFile(fileId);
+  }
+
+  public FileUpload getFileUpload(final String registrationId) {
+    synchronized (registeredUploads) {
+      return registeredUploads.get(registrationId);
+    }
+  }
+
+  public void cleanUp(final boolean all) {
+    synchronized (registeredUploads) {
+      final List<String> cleanUp = new LinkedList<>();
+      for (final Entry<String, FileUpload> entry : registeredUploads.entrySet()) {
+        if (all || entry.getValue().isExpired()) {
+          cleanUp.add(entry.getKey());
+        }
+      }
+      for (final String key : cleanUp) {
+        registeredUploads.get(key).cleanUp();
+        registeredUploads.remove(key);
+      }
+    }
+  }
+
+  public void cleanUp() {
+    cleanUp(false);
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/GRPCServer.java b/src/main/java/org/caosdb/server/grpc/GRPCServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..73356140b85fb09af93bdca034a69685ce8cd4eb
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/GRPCServer.java
@@ -0,0 +1,236 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.caosdb.server.grpc;
+
+import io.grpc.Server;
+import io.grpc.ServerInterceptors;
+import io.grpc.ServerServiceDefinition;
+import io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.NettyServerBuilder;
+import io.netty.handler.ssl.ApplicationProtocolConfig;
+import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
+import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
+import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import java.io.File;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.KeyManagerFactory;
+import org.caosdb.server.CaosDBServer;
+import org.caosdb.server.ServerProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is the main class of the gRPC end-point.
+ *
+ * <p>Here, the http and https servers are startet.
+ *
+ * @author Timm Fitschen <t.fitschen@indiscale.com>
+ */
+public class GRPCServer {
+  private static GRPCServer instance = new GRPCServer();
+
+  private static String getServerProperty(final String key) {
+    return CaosDBServer.getServerProperty(key);
+  }
+
+  private static final Logger logger = LoggerFactory.getLogger(GRPCServer.class.getName());
+
+  private final AuthInterceptor authInterceptor = new AuthInterceptor();
+  private final LoggingInterceptor loggingInterceptor = new LoggingInterceptor();
+
+  /**
+   * Create an ssl context.
+   *
+   * <p>Read the server certificate from the Java Key Store. Also, use the server properties for
+   * enabling desired TLS protocols and cipher suites.
+   *
+   * @return An SslContext for a https grpc end-point.
+   * @throws NoSuchAlgorithmException
+   * @throws UnrecoverableKeyException
+   * @throws KeyStoreException
+   * @throws CertificateException
+   * @throws IOException
+   */
+  private SslContext buildSslContext()
+      throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException,
+          CertificateException, IOException {
+    final KeyManagerFactory kmf =
+        KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+    final char[] password =
+        getServerProperty(ServerProperties.KEY_CERTIFICATES_KEY_STORE_PASSWORD).toCharArray();
+    kmf.init(
+        KeyStore.getInstance(
+            new File(getServerProperty(ServerProperties.KEY_CERTIFICATES_KEY_STORE_PATH)),
+            password),
+        password);
+
+    final String[] protocols =
+        getServerProperty(ServerProperties.KEY_HTTPS_ENABLED_PROTOCOLS).split("\\s*,\\s*|\\s+");
+    final List<String> ciphers =
+        Arrays.asList(
+            getServerProperty(ServerProperties.KEY_HTTPS_ENABLED_CIPHER_SUITES)
+                .split("\\s*,\\s*|\\s+"));
+    final ApplicationProtocolConfig config =
+        new ApplicationProtocolConfig(
+            Protocol.NPN_AND_ALPN,
+            SelectorFailureBehavior.FATAL_ALERT,
+            SelectedListenerFailureBehavior.FATAL_ALERT,
+            protocols);
+    final SslContextBuilder builder =
+        GrpcSslContexts.configure(
+            SslContextBuilder.forServer(kmf).applicationProtocolConfig(config).ciphers(ciphers));
+
+    return builder.build();
+  }
+
+  /** @return A list of services which should be added to the gRPC end-point. */
+  private List<ServerServiceDefinition> getEnabledServices() {
+    final List<ServerServiceDefinition> services = new LinkedList<>();
+
+    final AccessControlManagementServiceImpl accessControlManagementService =
+        new AccessControlManagementServiceImpl();
+    services.add(
+        ServerInterceptors.intercept(
+            accessControlManagementService, loggingInterceptor, authInterceptor));
+
+    final GeneralInfoServiceImpl generalInfoService = new GeneralInfoServiceImpl();
+    services.add(
+        ServerInterceptors.intercept(generalInfoService, loggingInterceptor, authInterceptor));
+
+    final FileTransmissionServiceImpl fileTransmissionService = new FileTransmissionServiceImpl();
+    services.add(
+        ServerInterceptors.intercept(fileTransmissionService, loggingInterceptor, authInterceptor));
+
+    final EntityTransactionServiceImpl entityTransactionService =
+        new EntityTransactionServiceImpl(fileTransmissionService);
+    services.add(
+        ServerInterceptors.intercept(
+            entityTransactionService, loggingInterceptor, authInterceptor));
+
+    return services;
+  }
+
+  /**
+   * Create a server listening on the specified port. If `tls` is true, the server is a HTTPS
+   * server. Otherwise, HTTP.
+   *
+   * @param port
+   * @param tls indicate whether the server uses tls or not
+   * @return A new, unstarted server instance.
+   * @throws UnrecoverableKeyException
+   * @throws NoSuchAlgorithmException
+   * @throws KeyStoreException
+   * @throws CertificateException
+   * @throws IOException
+   */
+  private Server buildServer(final int port, final boolean tls)
+      throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException,
+          CertificateException, IOException {
+    final NettyServerBuilder builder = NettyServerBuilder.forPort(port);
+
+    if (tls) {
+      final SslContext sslContext = buildSslContext();
+      builder.sslContext(sslContext);
+    }
+    for (final ServerServiceDefinition service : getEnabledServices()) {
+      builder.addService(service);
+    }
+
+    return builder.build();
+  }
+
+  /**
+   * Main method of the gRPC end-point which starts the server(s) with HTTP or HTTPS. Whether a
+   * server is started or not depends on the server properites {@link
+   * ServerProperties#KEY_GRPC_SERVER_PORT_HTTP} and {@link
+   * ServerProperties#KEY_GRPC_SERVER_PORT_HTTPS}.
+   *
+   * @throws IOException
+   * @throws InterruptedException
+   * @throws KeyStoreException
+   * @throws NoSuchAlgorithmException
+   * @throws CertificateException
+   * @throws UnrecoverableKeyException
+   */
+  public static void startServer()
+      throws IOException, InterruptedException, KeyStoreException, NoSuchAlgorithmException,
+          CertificateException, UnrecoverableKeyException {
+
+    boolean started = false;
+    final String port_https_str = getServerProperty(ServerProperties.KEY_GRPC_SERVER_PORT_HTTPS);
+    if (port_https_str != null && !port_https_str.isEmpty()) {
+      final Integer port_https = Integer.parseInt(port_https_str);
+      final Server server = instance.buildServer(port_https, true);
+      CaosDBServer.addPreShutdownHook(new ServerStopper(server));
+      server.start();
+      started = true;
+      logger.info("Started GRPC (HTTPS) on port {}", port_https);
+    }
+
+    final String port_http_str = getServerProperty(ServerProperties.KEY_GRPC_SERVER_PORT_HTTP);
+    if (port_http_str != null && !port_http_str.isEmpty()) {
+      final Integer port_http = Integer.parseInt(port_http_str);
+
+      final Server server = instance.buildServer(port_http, false);
+      CaosDBServer.addPreShutdownHook(new ServerStopper(server));
+      server.start();
+      logger.info("Started GRPC (HTTP) on port {}", port_http);
+
+    } else if (!started) {
+      logger.warn(
+          "No GRPC Server has been started. Please configure {} or {} to do so.",
+          ServerProperties.KEY_GRPC_SERVER_PORT_HTTP,
+          ServerProperties.KEY_GRPC_SERVER_PORT_HTTPS);
+    }
+  }
+
+  private static class ServerStopper implements Runnable {
+
+    private final Server server;
+
+    public ServerStopper(final Server server) {
+      this.server = server;
+    }
+
+    @Override
+    public void run() {
+      try {
+        if (!server.isShutdown()) {
+          server.shutdown();
+          server.awaitTermination(60, TimeUnit.SECONDS);
+        }
+      } catch (final InterruptedException e) {
+        logger.warn("Could not shutdown the GRPC server on port {}", server.getPort());
+      }
+    }
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/GeneralInfoServiceImpl.java b/src/main/java/org/caosdb/server/grpc/GeneralInfoServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..e47bda9c1783fd9a82703626ad8017c26dbea1fe
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/GeneralInfoServiceImpl.java
@@ -0,0 +1,141 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package org.caosdb.server.grpc;
+
+import io.grpc.stub.StreamObserver;
+import java.util.Collection;
+import java.util.LinkedList;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authz.AuthorizationInfo;
+import org.apache.shiro.authz.Permission;
+import org.apache.shiro.subject.Subject;
+import org.caosdb.api.info.v1.GeneralInfoServiceGrpc.GeneralInfoServiceImplBase;
+import org.caosdb.api.info.v1.GetSessionInfoRequest;
+import org.caosdb.api.info.v1.GetSessionInfoResponse;
+import org.caosdb.api.info.v1.GetVersionInfoRequest;
+import org.caosdb.api.info.v1.GetVersionInfoResponse;
+import org.caosdb.api.info.v1.VersionInfo;
+import org.caosdb.server.CaosDBServer;
+import org.caosdb.server.ServerProperties;
+import org.caosdb.server.accessControl.AuthenticationUtils;
+import org.caosdb.server.accessControl.Principal;
+import org.caosdb.server.permissions.CaosPermission;
+
+/**
+ * Implementation of the GeneralInfoService.
+ *
+ * <p>Currently, the only functionality is {@link #getVersionInfo(GetVersionInfoRequest,
+ * StreamObserver)} which returns the current version and build metadata of this server instance to
+ * the client.
+ *
+ * @author Timm Fitschen <t.fitschen@indiscale.com>
+ */
+public class GeneralInfoServiceImpl extends GeneralInfoServiceImplBase {
+
+  private GetVersionInfoResponse getVersionInfo(GetVersionInfoRequest request) {
+
+    final String version[] =
+        CaosDBServer.getServerProperty(ServerProperties.KEY_PROJECT_VERSION).split("[\\.-]", 4);
+    final Integer major = Integer.parseInt(version[0]);
+    final Integer minor = Integer.parseInt(version[1]);
+    final Integer patch = Integer.parseInt(version[2]);
+    final String pre_release = version.length > 3 ? version[3] : "";
+    final String build = CaosDBServer.getServerProperty(ServerProperties.KEY_PROJECT_REVISTION);
+
+    final VersionInfo versionInfo =
+        VersionInfo.newBuilder()
+            .setMajor(major)
+            .setMinor(minor)
+            .setPatch(patch)
+            .setPreRelease(pre_release)
+            .setBuild(build)
+            .build();
+    return GetVersionInfoResponse.newBuilder().setVersionInfo(versionInfo).build();
+  }
+
+  @Override
+  public void getVersionInfo(
+      final GetVersionInfoRequest request,
+      final StreamObserver<GetVersionInfoResponse> responseObserver) {
+
+    try {
+      AuthInterceptor.bindSubject();
+      GetVersionInfoResponse response = getVersionInfo(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      AccessControlManagementServiceImpl.handleException(responseObserver, e);
+    }
+  }
+
+  private GetSessionInfoResponse getSessionInfo(GetSessionInfoRequest request) {
+    Subject subject = SecurityUtils.getSubject();
+    GetSessionInfoResponse.Builder response = GetSessionInfoResponse.newBuilder();
+
+    Principal principal = (Principal) subject.getPrincipal();
+
+    response.setUsername(principal.getUsername());
+    response.setRealm(principal.getRealm());
+
+    AuthorizationInfo authorizationInfo = AuthenticationUtils.getAuthorizationInfo(subject);
+    Collection<String> roles = authorizationInfo.getRoles();
+    if (roles != null && !roles.isEmpty()) {
+      response.addAllRoles(roles);
+    }
+
+    Collection<String> permissions = new LinkedList<>();
+
+    Collection<String> stringPermissions = authorizationInfo.getStringPermissions();
+    if (stringPermissions != null && !stringPermissions.isEmpty()) {
+      permissions.addAll(stringPermissions);
+    }
+
+    for (Permission p : authorizationInfo.getObjectPermissions()) {
+      if (p instanceof CaosPermission) {
+        permissions.addAll(((CaosPermission) p).getStringPermissions(subject));
+      } else {
+        permissions.add(p.toString());
+      }
+    }
+
+    if (permissions != null && !permissions.isEmpty()) {
+      response.addAllPermissions(permissions);
+    }
+
+    return response.build();
+  }
+
+  @Override
+  public void getSessionInfo(
+      GetSessionInfoRequest request, StreamObserver<GetSessionInfoResponse> responseObserver) {
+
+    try {
+      AuthInterceptor.bindSubject();
+      final GetSessionInfoResponse response = getSessionInfo(request);
+      responseObserver.onNext(response);
+      responseObserver.onCompleted();
+
+    } catch (final Exception e) {
+      AccessControlManagementServiceImpl.handleException(responseObserver, e);
+    }
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/GrpcToCaosDBConverters.java b/src/main/java/org/caosdb/server/grpc/GrpcToCaosDBConverters.java
new file mode 100644
index 0000000000000000000000000000000000000000..d7c515a433cfa82f0bea046a1856dd9fb309b915
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/GrpcToCaosDBConverters.java
@@ -0,0 +1,376 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.grpc;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+import org.apache.shiro.SecurityUtils;
+import org.caosdb.api.entity.v1.AtomicDataType;
+import org.caosdb.api.entity.v1.CollectionValues;
+import org.caosdb.api.entity.v1.DataType;
+import org.caosdb.api.entity.v1.Entity;
+import org.caosdb.api.entity.v1.EntityACL;
+import org.caosdb.api.entity.v1.EntityPermission;
+import org.caosdb.api.entity.v1.EntityPermissionRule;
+import org.caosdb.api.entity.v1.EntityRole;
+import org.caosdb.api.entity.v1.FileDescriptor;
+import org.caosdb.api.entity.v1.Importance;
+import org.caosdb.api.entity.v1.ListDataType;
+import org.caosdb.api.entity.v1.Parent;
+import org.caosdb.api.entity.v1.ReferenceDataType;
+import org.caosdb.api.entity.v1.SpecialValue;
+import org.caosdb.server.datatype.AbstractDatatype;
+import org.caosdb.server.datatype.BooleanValue;
+import org.caosdb.server.datatype.CollectionValue;
+import org.caosdb.server.datatype.FileDatatype;
+import org.caosdb.server.datatype.GenericValue;
+import org.caosdb.server.datatype.ListDatatype;
+import org.caosdb.server.datatype.ReferenceDatatype;
+import org.caosdb.server.datatype.ReferenceDatatype2;
+import org.caosdb.server.datatype.Value;
+import org.caosdb.server.entity.EntityInterface;
+import org.caosdb.server.entity.FileProperties;
+import org.caosdb.server.entity.MagicTypes;
+import org.caosdb.server.entity.Role;
+import org.caosdb.server.entity.StatementStatus;
+import org.caosdb.server.entity.UpdateEntity;
+import org.caosdb.server.entity.container.TransactionContainer;
+import org.caosdb.server.entity.wrapper.Property;
+import org.caosdb.server.permissions.EntityACLFactory;
+import org.caosdb.server.utils.EntityStatus;
+import org.caosdb.server.utils.ServerMessages;
+
+public class GrpcToCaosDBConverters {
+
+  public Integer getId(final String id) {
+    return Integer.parseInt(id);
+  }
+
+  public Role convert(final EntityRole role) {
+    switch (role) {
+      case ENTITY_ROLE_FILE:
+        return Role.File;
+      case ENTITY_ROLE_PROPERTY:
+        return Role.Property;
+      case ENTITY_ROLE_RECORD:
+        return Role.Record;
+      case ENTITY_ROLE_RECORD_TYPE:
+        return Role.RecordType;
+      default:
+        return null;
+    }
+  }
+
+  public Property getUnit(final String unitStr) {
+    final EntityInterface magicUnit = MagicTypes.UNIT.getEntity();
+    final Property unit = new Property();
+    unit.setDescription(magicUnit.getDescription());
+    unit.setName(magicUnit.getName());
+    unit.setId(magicUnit.getId());
+    unit.setDatatype(magicUnit.getDatatype());
+    unit.setStatementStatus(StatementStatus.FIX);
+    unit.setValue(new GenericValue(unitStr));
+    unit.setEntityStatus(EntityStatus.QUALIFIED);
+    return unit;
+  }
+
+  public Value getValue(final String valString) {
+    return new GenericValue(valString);
+  }
+
+  /**
+   * Set the content of {@code entity} to that of the grpc message object {@code from}. Also return
+   * {@code entity} at the end.
+   */
+  public EntityInterface convert(final Entity from, final EntityInterface entity) {
+    entity.setName(from.getName().isEmpty() ? null : from.getName());
+    entity.setDescription(from.getDescription().isBlank() ? null : from.getDescription());
+    if (!from.getUnit().isBlank()) {
+      entity.addProperty(getUnit(from.getUnit()));
+    }
+    if (from.hasDataType()) {
+      entity.setDatatype(convert(from.getDataType()));
+    }
+    if (from.hasValue()) {
+      entity.setValue(convert(from.getValue()));
+    }
+
+    if (from.getPropertiesCount() > 0) {
+      final StatementStatus defaultImportance =
+          entity.getRole() == Role.RecordType ? StatementStatus.RECOMMENDED : StatementStatus.FIX;
+      entity.getProperties().addAll(convertProperties(from.getPropertiesList(), defaultImportance));
+    }
+    if (from.getParentsCount() > 0) {
+      entity.getParents().addAll(convertParents(from.getParentsList()));
+    }
+    if (entity.getRole() == Role.File && from.hasFileDescriptor()) {
+      entity.setFileProperties(convert(from.getFileDescriptor()));
+    }
+    return entity;
+  }
+
+  private Value convert(final org.caosdb.api.entity.v1.Value value) {
+    switch (value.getValueCase()) {
+      case LIST_VALUES:
+        return convertListValue(value.getListValues());
+      case SCALAR_VALUE:
+        return convertScalarValue(value.getScalarValue());
+      default:
+        break;
+    }
+    return null;
+  }
+
+  private CollectionValue convertListValue(final CollectionValues collectionValues) {
+    final CollectionValue result = new CollectionValue();
+    collectionValues
+        .getValuesList()
+        .forEach(
+            (v) -> {
+              result.add(convertScalarValue(v));
+            });
+    return result;
+  }
+
+  private Value convertScalarValue(final org.caosdb.api.entity.v1.ScalarValue value) {
+    switch (value.getScalarValueCase()) {
+      case BOOLEAN_VALUE:
+        return BooleanValue.valueOf(value.getBooleanValue());
+      case DOUBLE_VALUE:
+        return new GenericValue(value.getDoubleValue());
+      case INTEGER_VALUE:
+        return new GenericValue(Long.toString(value.getIntegerValue()));
+      case SPECIAL_VALUE:
+        return convertSpecial(value.getSpecialValue());
+      case STRING_VALUE:
+        return new GenericValue(value.getStringValue());
+      default:
+        break;
+    }
+    return null;
+  }
+
+  private Value convertSpecial(final SpecialValue specialValue) {
+    if (specialValue == SpecialValue.SPECIAL_VALUE_EMPTY_STRING) {
+      return new GenericValue("");
+    }
+    return null;
+  }
+
+  private AbstractDatatype convert(final DataType dataType) {
+    switch (dataType.getDataTypeCase()) {
+      case ATOMIC_DATA_TYPE:
+        return convertAtomicType(dataType.getAtomicDataType());
+      case LIST_DATA_TYPE:
+        return convertListDataType(dataType.getListDataType());
+      case REFERENCE_DATA_TYPE:
+        return convertReferenceDataType(dataType.getReferenceDataType());
+      default:
+        break;
+    }
+    return null;
+  }
+
+  private ReferenceDatatype convertReferenceDataType(final ReferenceDataType referenceDataType) {
+    final String name = referenceDataType.getName();
+    if (name.equalsIgnoreCase("REFERENCE")) {
+      return new ReferenceDatatype();
+    } else if (name.equalsIgnoreCase("FILE")) {
+      return new FileDatatype();
+    }
+    return new ReferenceDatatype2(name);
+  }
+
+  private AbstractDatatype convertAtomicType(final AtomicDataType dataType) {
+    switch (dataType) {
+      case ATOMIC_DATA_TYPE_BOOLEAN:
+        return AbstractDatatype.datatypeFactory("BOOLEAN");
+      case ATOMIC_DATA_TYPE_DATETIME:
+        return AbstractDatatype.datatypeFactory("DATETIME");
+      case ATOMIC_DATA_TYPE_DOUBLE:
+        return AbstractDatatype.datatypeFactory("DOUBLE");
+      case ATOMIC_DATA_TYPE_INTEGER:
+        return AbstractDatatype.datatypeFactory("INTEGER");
+      case ATOMIC_DATA_TYPE_TEXT:
+        return AbstractDatatype.datatypeFactory("TEXT");
+      default:
+        return null;
+    }
+  }
+
+  private AbstractDatatype convertListDataType(final ListDataType dataType) {
+    switch (dataType.getListDataTypeCase()) {
+      case ATOMIC_DATA_TYPE:
+        return new ListDatatype(convertAtomicType(dataType.getAtomicDataType()));
+      case REFERENCE_DATA_TYPE:
+        return new ListDatatype(convertReferenceDataType(dataType.getReferenceDataType()));
+      default:
+        return null;
+    }
+  }
+
+  private FileProperties convert(final FileDescriptor fileDescriptor) {
+    return new FileProperties(
+        null,
+        fileDescriptor.getPath(),
+        fileDescriptor.getSize() == 0 ? null : fileDescriptor.getSize());
+  }
+
+  private Collection<Property> convertProperties(
+      final List<org.caosdb.api.entity.v1.Property> propertiesList,
+      final StatementStatus defaultImportance) {
+    final Collection<Property> result = new LinkedList<>();
+    propertiesList.forEach(
+        prop -> {
+          result.add(convert(prop, defaultImportance));
+        });
+    return result;
+  }
+
+  private Property convert(
+      final org.caosdb.api.entity.v1.Property e, final StatementStatus defaultImportance) {
+    final Property result = new Property();
+
+    try {
+      result.setId(e.getId().isBlank() ? null : getId(e.getId()));
+    } catch (final NumberFormatException exc) {
+      result.addError(ServerMessages.ENTITY_DOES_NOT_EXIST);
+    }
+    result.setName(e.getName().isBlank() ? null : e.getName());
+    result.setDescription(e.getDescription().isBlank() ? null : e.getDescription());
+    if (!e.getUnit().isBlank()) {
+      result.addProperty(getUnit(e.getUnit()));
+    }
+    if (e.hasDataType()) {
+      result.setDatatype(convert(e.getDataType()));
+    }
+    if (e.hasValue()) {
+      result.setValue(convert(e.getValue()));
+    }
+    if (e.getImportance() != Importance.IMPORTANCE_UNSPECIFIED) {
+      result.setStatementStatus(convert(e.getImportance()));
+    } else {
+      result.setStatementStatus(defaultImportance);
+    }
+    // TODO remove this hard-coded setting when the API supports flags
+    if (result.getFlag("inheritance") == null) {
+      result.setFlag("inheritance", "fix");
+    }
+
+    return result;
+  }
+
+  private StatementStatus convert(final Importance importance) {
+    switch (importance) {
+      case IMPORTANCE_FIX:
+        return StatementStatus.FIX;
+      case IMPORTANCE_OBLIGATORY:
+        return StatementStatus.OBLIGATORY;
+      case IMPORTANCE_RECOMMENDED:
+        return StatementStatus.RECOMMENDED;
+      case IMPORTANCE_SUGGESTED:
+        return StatementStatus.SUGGESTED;
+      default:
+        return null;
+    }
+  }
+
+  private Collection<org.caosdb.server.entity.wrapper.Parent> convertParents(
+      final List<Parent> parentsList) {
+    final Collection<org.caosdb.server.entity.wrapper.Parent> result = new LinkedList<>();
+    parentsList.forEach(
+        e -> {
+          result.add(convert(e));
+        });
+    return result;
+  }
+
+  private org.caosdb.server.entity.wrapper.Parent convert(final Parent e) {
+    final org.caosdb.server.entity.wrapper.Parent result =
+        new org.caosdb.server.entity.wrapper.Parent();
+
+    try {
+      result.setId(e.getId().isBlank() ? null : getId(e.getId()));
+    } catch (final NumberFormatException exc) {
+      result.addError(ServerMessages.ENTITY_DOES_NOT_EXIST);
+    }
+    result.setName(e.getName().isBlank() ? null : e.getName());
+    return result;
+  }
+
+  public TransactionContainer convertAcls(List<EntityACL> aclsList) {
+    TransactionContainer result =
+        new TransactionContainer(
+            SecurityUtils.getSubject(), System.currentTimeMillis(), UUID.randomUUID().toString());
+
+    aclsList.forEach(
+        acl -> {
+          result.add(convert(acl));
+        });
+    return result;
+  }
+
+  private EntityInterface convert(EntityACL acl) {
+    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) {
+    EntityACLFactory fac = new EntityACLFactory();
+    for (EntityPermissionRule rule : acl.getRulesList()) {
+      if (rule.getGrant()) {
+        fac.grant(
+            org.caosdb.server.permissions.Role.create(rule.getRole()),
+            rule.getPriority(),
+            convert(rule.getPermissionsList()));
+      } else {
+        fac.deny(
+            org.caosdb.server.permissions.Role.create(rule.getRole()),
+            rule.getPriority(),
+            convert(rule.getPermissionsList()));
+      }
+    }
+    return fac.remove(org.caosdb.server.permissions.EntityACL.GLOBAL_PERMISSIONS).create();
+  }
+
+  private org.caosdb.server.permissions.EntityPermission[] convert(
+      List<EntityPermission> permissionsList) {
+    ArrayList<org.caosdb.server.permissions.EntityPermission> result =
+        new ArrayList<>(permissionsList.size());
+    permissionsList.forEach(
+        (p) -> {
+          result.add(org.caosdb.server.permissions.EntityPermission.getEntityPermission(p));
+        });
+    return result.toArray(new org.caosdb.server.permissions.EntityPermission[0]);
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/LoggingInterceptor.java b/src/main/java/org/caosdb/server/grpc/LoggingInterceptor.java
new file mode 100644
index 0000000000000000000000000000000000000000..9edd35090659e908ecf7f3fb502d34c36ac7e829
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/LoggingInterceptor.java
@@ -0,0 +1,24 @@
+package org.caosdb.server.grpc;
+
+import io.grpc.Context;
+import io.grpc.Contexts;
+import io.grpc.Metadata;
+import io.grpc.ServerCall;
+import io.grpc.ServerCall.Listener;
+import io.grpc.ServerCallHandler;
+import io.grpc.ServerInterceptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LoggingInterceptor implements ServerInterceptor {
+
+  private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class.getName());
+
+  @Override
+  public <ReqT, RespT> Listener<ReqT> interceptCall(
+      ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
+
+    logger.info(call.getMethodDescriptor().getFullMethodName() + " - " + call.getAttributes());
+    return Contexts.interceptCall(Context.current(), call, headers, next);
+  }
+}
diff --git a/src/main/java/org/caosdb/server/grpc/UploadBuffer.java b/src/main/java/org/caosdb/server/grpc/UploadBuffer.java
new file mode 100644
index 0000000000000000000000000000000000000000..b341c33cbe9e0ca911c35aad144492cc2631da63
--- /dev/null
+++ b/src/main/java/org/caosdb/server/grpc/UploadBuffer.java
@@ -0,0 +1,52 @@
+package org.caosdb.server.grpc;
+
+import com.google.protobuf.ByteString;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import org.caosdb.api.entity.v1.TransmissionStatus;
+import org.caosdb.server.entity.FileProperties;
+
+public class UploadBuffer {
+
+  TransmissionStatus status = TransmissionStatus.TRANSMISSION_STATUS_UNSPECIFIED;
+  private final File tmpFile;
+
+  public UploadBuffer(final File tmpFile) {
+    this.tmpFile = tmpFile;
+  }
+
+  public TransmissionStatus write(final ByteString data) throws IOException {
+    switch (status) {
+      case TRANSMISSION_STATUS_UNSPECIFIED:
+        status = TransmissionStatus.TRANSMISSION_STATUS_GO_ON;
+        break;
+      case TRANSMISSION_STATUS_GO_ON:
+        break;
+      default:
+        throw new RuntimeException("Wrong transmission state.");
+    }
+    try (final FileOutputStream fileOutputStream =
+        new FileOutputStream(tmpFile, tmpFile.exists())) {
+      data.writeTo(fileOutputStream);
+    }
+    return status;
+  }
+
+  public FileProperties toFileProperties(final String fileId) {
+    switch (status) {
+      case TRANSMISSION_STATUS_SUCCESS:
+        break;
+      case TRANSMISSION_STATUS_GO_ON:
+        status = TransmissionStatus.TRANSMISSION_STATUS_SUCCESS;
+        break;
+      default:
+        throw new RuntimeException("Wrong transmission state.");
+    }
+    final FileProperties result =
+        new FileProperties(null, tmpFile.getAbsolutePath(), tmpFile.length());
+    result.setFile(tmpFile);
+    result.setTmpIdentifier(fileId);
+    return result;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/jobs/Job.java b/src/main/java/org/caosdb/server/jobs/Job.java
index 2de6b08537d82504bb989a4cf661f89ff473e4c2..675686aa797cd2a00cf7a921a29e81ce144db179 100644
--- a/src/main/java/org/caosdb/server/jobs/Job.java
+++ b/src/main/java/org/caosdb/server/jobs/Job.java
@@ -27,8 +27,6 @@ import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
-import org.apache.shiro.SecurityUtils;
-import org.apache.shiro.authz.Permission;
 import org.apache.shiro.subject.Subject;
 import org.caosdb.server.CaosDBException;
 import org.caosdb.server.database.BackendTransaction;
@@ -300,13 +298,6 @@ public abstract class Job {
     return entity;
   }
 
-  protected final void checkPermission(final EntityInterface entity, final Permission permission)
-      throws Message {
-    if (!entity.getEntityACL().isPermitted(SecurityUtils.getSubject(), permission)) {
-      throw ServerMessages.AUTHORIZATION_ERROR;
-    }
-  }
-
   /**
    * Create a Job object with the given parameters.
    *
diff --git a/src/main/java/org/caosdb/server/jobs/Schedule.java b/src/main/java/org/caosdb/server/jobs/Schedule.java
index f33400099a951aa8fbcf1975208988085813a8b8..57474da515418745ed0457962596dfce233bff48 100644
--- a/src/main/java/org/caosdb/server/jobs/Schedule.java
+++ b/src/main/java/org/caosdb/server/jobs/Schedule.java
@@ -97,8 +97,8 @@ public class Schedule {
             ? this.jobLists.get(jobclass.getAnnotation(JobAnnotation.class).stage().ordinal())
             : this.jobLists.get(TransactionStage.CHECK.ordinal());
     for (final ScheduledJob scheduledJob : jobs) {
-      if (jobclass.isInstance(scheduledJob.job)) {
-        if (scheduledJob.job.getEntity() == entity) {
+      if (jobclass.isInstance(scheduledJob.getJob())) {
+        if (scheduledJob.getJob().getEntity() == entity) {
           runJob(scheduledJob);
         }
       }
diff --git a/src/main/java/org/caosdb/server/jobs/ScheduledJob.java b/src/main/java/org/caosdb/server/jobs/ScheduledJob.java
index 3affdfd21421961edc4721c25d80f392d914d1bb..0ef0ac143428eaddaf857f063b7953a620f47ee6 100644
--- a/src/main/java/org/caosdb/server/jobs/ScheduledJob.java
+++ b/src/main/java/org/caosdb/server/jobs/ScheduledJob.java
@@ -31,11 +31,14 @@ package org.caosdb.server.jobs;
 public class ScheduledJob {
 
   long runtime = 0;
-  final Job job;
+  private final Job job;
   private long startTime = -1;
 
-  ScheduledJob(final Job j) {
-    this.job = j;
+  ScheduledJob(final Job job) {
+    if (job == null) {
+      throw new NullPointerException("job was null.");
+    }
+    this.job = job;
   }
 
   void run() {
@@ -85,4 +88,8 @@ public class ScheduledJob {
   public boolean skip() {
     return this.job.getTarget().skipJob();
   }
+
+  public Job getJob() {
+    return job;
+  }
 }
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 408384f416e1249501d5383ea1028ced56914ded..724dd89844097dad5bd1a6fbf2102c8f2c4ceb8c 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,51 @@ 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 String init() {
+      return TransactionPermission.class.getSimpleName();
+    }
+  }
+
+  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 +94,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/CheckDatatypePresent.java b/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java
index f9c7704c8bd0ff00214b41d5793c8fdb15bcc61a..dd87d5298365a4b95abc52c66c17823c5c52687a 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckDatatypePresent.java
@@ -23,12 +23,14 @@
 package org.caosdb.server.jobs.core;
 
 import java.util.List;
+import org.apache.shiro.authz.AuthorizationException;
 import org.caosdb.server.database.exceptions.EntityDoesNotExistException;
 import org.caosdb.server.database.exceptions.EntityWasNotUniqueException;
 import org.caosdb.server.datatype.AbstractCollectionDatatype;
 import org.caosdb.server.datatype.AbstractDatatype;
 import org.caosdb.server.datatype.ReferenceDatatype2;
 import org.caosdb.server.entity.EntityInterface;
+import org.caosdb.server.entity.InsertEntity;
 import org.caosdb.server.entity.Message;
 import org.caosdb.server.entity.Role;
 import org.caosdb.server.jobs.EntityJob;
@@ -52,7 +54,9 @@ public final class CheckDatatypePresent extends EntityJob {
 
       // inherit datatype
       if (!getEntity().hasDatatype()) {
-        resolveId(getEntity());
+        if (!(getEntity() instanceof InsertEntity)) {
+          resolveId(getEntity());
+        }
 
         inheritDatatypeFromAbstractEntity();
 
@@ -92,15 +96,17 @@ public final class CheckDatatypePresent extends EntityJob {
       } else {
 
         // finally, no data type
-        throw ServerMessages.NO_DATATYPE;
+        throw ServerMessages.PROPERTY_HAS_NO_DATATYPE;
       }
-
     } catch (final Message m) {
       if (m == ServerMessages.ENTITY_DOES_NOT_EXIST) {
         getEntity().addError(ServerMessages.UNKNOWN_DATATYPE);
       } else {
         getEntity().addError(m);
       }
+    } catch (AuthorizationException exc) {
+      getEntity().addError(ServerMessages.AUTHORIZATION_ERROR);
+      getEntity().addInfo(exc.getMessage());
     } catch (final EntityDoesNotExistException exc) {
       getEntity().addError(ServerMessages.UNKNOWN_DATATYPE);
     } catch (final EntityWasNotUniqueException exc) {
@@ -149,8 +155,8 @@ public final class CheckDatatypePresent extends EntityJob {
     }
   }
 
-  private void assertAllowedToUse(final EntityInterface datatype) throws Message {
-    checkPermission(datatype, EntityPermission.USE_AS_DATA_TYPE);
+  private void assertAllowedToUse(final EntityInterface datatype) {
+    datatype.checkPermission(EntityPermission.USE_AS_DATA_TYPE);
   }
 
   private void checkIfOverride() throws Message {
@@ -211,7 +217,7 @@ public final class CheckDatatypePresent extends EntityJob {
       } catch (final EntityDoesNotExistException exc) {
         entity.addError(ServerMessages.ENTITY_DOES_NOT_EXIST);
       } catch (final EntityWasNotUniqueException exc) {
-        entity.addError(ServerMessages.CANNOT_IDENTIFY_ENTITY_UNIQUELY);
+        entity.addError(ServerMessages.ENTITY_NAME_DUPLICATES);
       }
     }
   }
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckFileStorageConsistency.java b/src/main/java/org/caosdb/server/jobs/core/CheckFileStorageConsistency.java
index 8dc6fc0c60bc234d65f576377a89244d0a7a673e..e425649bb1bc43a9745120ed078c653f55af2e38 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckFileStorageConsistency.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckFileStorageConsistency.java
@@ -127,8 +127,6 @@ public class CheckFileStorageConsistency extends FlagJob {
       getContainer()
           .addMessage(
               new Message(
-                  "Info",
-                  0,
                   "Test took too long. The results will be written to './ConsistencyTest.xml'"));
     } else {
       // add info/warning/error
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckParOblPropPresent.java b/src/main/java/org/caosdb/server/jobs/core/CheckParOblPropPresent.java
index 9588a3056e15f7fb3847db37468bb6a628741203..e6ba9da430e6218e5c40fba968c309ed94152bf6 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckParOblPropPresent.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckParOblPropPresent.java
@@ -39,9 +39,6 @@ import org.caosdb.server.utils.ServerMessages;
 public class CheckParOblPropPresent extends EntityJob {
 
   public static final String OBL_IMPORTANCE_FLAG_KEY = "force-missing-obligatory";
-  public static final Message ENTITY_NOT_UNIQUE =
-      new Message(
-          MessageType.Error, 0, "Could not check importance. Parent was not uniquely resolvable.");
   public static final Message ILLEGAL_FLAG_VALUE =
       new Message(MessageType.Warning, "Illegal value for flag 'force-missing-obligatory'.");
 
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java
index e663874189ee0301a5b30452468391711624b606..d3b28bd5f158b767469e6d7fcc3cebe9fa75edb8 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckParValid.java
@@ -23,6 +23,7 @@
 package org.caosdb.server.jobs.core;
 
 import com.google.common.base.Objects;
+import org.apache.shiro.authz.AuthorizationException;
 import org.caosdb.server.database.exceptions.EntityDoesNotExistException;
 import org.caosdb.server.database.exceptions.EntityWasNotUniqueException;
 import org.caosdb.server.entity.Affiliation;
@@ -60,7 +61,7 @@ public class CheckParValid extends EntityJob {
             // The parent has neither an id nor a name.
             // Therefore it cannot be identified.
 
-            throw ServerMessages.ENTITY_HAS_NO_NAME_AND_NO_ID;
+            throw ServerMessages.ENTITY_HAS_NO_NAME_OR_ID;
           }
 
           if (parent.hasId()) {
@@ -118,13 +119,16 @@ public class CheckParValid extends EntityJob {
             }
           }
 
-          addError(parent, ServerMessages.ENTITY_DOES_NOT_EXIST);
+          parent.addError(ServerMessages.ENTITY_DOES_NOT_EXIST);
         } catch (final Message m) {
-          addError(parent, m);
+          parent.addError(m);
+        } catch (AuthorizationException e) {
+          parent.addError(ServerMessages.AUTHORIZATION_ERROR);
+          parent.addInfo(e.getMessage());
         } catch (final EntityDoesNotExistException exc) {
-          addError(parent, ServerMessages.ENTITY_DOES_NOT_EXIST);
+          parent.addError(ServerMessages.ENTITY_DOES_NOT_EXIST);
         } catch (final EntityWasNotUniqueException exc) {
-          addError(parent, ServerMessages.NAME_DUPLICATES);
+          parent.addError(ServerMessages.ENTITY_NAME_DUPLICATES);
         }
       }
     }
@@ -139,8 +143,8 @@ public class CheckParValid extends EntityJob {
       if (par.getEntityStatus() != EntityStatus.IGNORE) {
         for (final Parent par2 : getEntity().getParents()) {
           if (par != par2 && par2.getEntityStatus() != EntityStatus.IGNORE) {
-            if ((par.hasId() && par2.hasId() && par.getId().equals(par2.getId()))
-                || (par.hasName() && par2.hasName() && par.getName().equals(par2.getName()))) {
+            if (par.hasId() && par2.hasId() && par.getId().equals(par2.getId())
+                || par.hasName() && par2.hasName() && par.getName().equals(par2.getName())) {
               if (!Objects.equal(par.getFlag("inheritance"), par2.getFlag("inheritance"))) {
                 getEntity().addError(ServerMessages.PARENT_DUPLICATES_ERROR);
                 getEntity().setEntityStatus(EntityStatus.UNQUALIFIED);
@@ -191,12 +195,7 @@ public class CheckParValid extends EntityJob {
     throw ServerMessages.AFFILIATION_ERROR;
   }
 
-  private void assertAllowedToUse(final EntityInterface entity) throws Message {
-    checkPermission(entity, EntityPermission.USE_AS_PARENT);
-  }
-
-  private void addError(final EntityInterface parent, final Message m) {
-    parent.addError(m);
-    parent.setEntityStatus(EntityStatus.UNQUALIFIED);
+  private void assertAllowedToUse(final EntityInterface entity) {
+    entity.checkPermission(EntityPermission.USE_AS_PARENT);
   }
 }
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java
index 390deedde211c0931eca1c3677ac5ff9c8ee9d8f..eeea52b85b9c1629b426660020c7e65f0e4b96ef 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckPropValid.java
@@ -25,6 +25,7 @@ package org.caosdb.server.jobs.core;
 import static org.caosdb.server.utils.ServerMessages.ENTITY_DOES_NOT_EXIST;
 
 import com.google.common.base.Objects;
+import org.apache.shiro.authz.AuthorizationException;
 import org.caosdb.server.database.exceptions.EntityDoesNotExistException;
 import org.caosdb.server.database.exceptions.EntityWasNotUniqueException;
 import org.caosdb.server.entity.EntityInterface;
@@ -124,32 +125,23 @@ public class CheckPropValid extends EntityJob {
           }
         }
       } catch (final Message m) {
-        addError(property, m);
+        property.addError(m);
+      } catch (AuthorizationException e) {
+        property.addError(ServerMessages.AUTHORIZATION_ERROR);
+        property.addInfo(e.getMessage());
       } catch (final EntityDoesNotExistException e) {
-        addError(property, ENTITY_DOES_NOT_EXIST);
+        property.addError(ENTITY_DOES_NOT_EXIST);
       } catch (final EntityWasNotUniqueException e) {
-        addError(property, ServerMessages.ENTITY_NAME_DUPLICATES);
+        property.addError(ServerMessages.ENTITY_NAME_DUPLICATES);
       }
     }
 
     // process names
     appendJob(ProcessNameProperties.class);
-    // final ProcessNameProperties processNameProperties = new
-    // ProcessNameProperties();
-    // processNameProperties.init(getMode(), getEntity(), getContainer(),
-    // getTransaction());
-    // getTransaction().getSchedule().add(processNameProperties);
-    // getTransaction().getSchedule().runJob(processNameProperties);
-
-  }
-
-  private void assertAllowedToUse(final EntityInterface property) throws Message {
-    checkPermission(property, EntityPermission.USE_AS_PROPERTY);
   }
 
-  private void addError(final EntityInterface property, final Message m) {
-    property.addError(m);
-    property.setEntityStatus(EntityStatus.UNQUALIFIED);
+  private void assertAllowedToUse(final EntityInterface property) {
+    property.checkPermission(EntityPermission.USE_AS_PROPERTY);
   }
 
   private static void deriveOverrideStatus(final Property child, final EntityInterface parent) {
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java b/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java
index b9f804f519948b605a9fa8820c91495618463fba..e739ffde01a38fb1539c97166014350e9f9c61d7 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckRefidIsaParRefid.java
@@ -31,7 +31,6 @@ import org.caosdb.server.datatype.ReferenceValue;
 import org.caosdb.server.entity.Entity;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.Message;
-import org.caosdb.server.entity.Message.MessageType;
 import org.caosdb.server.entity.Role;
 import org.caosdb.server.entity.wrapper.Parent;
 import org.caosdb.server.jobs.EntityJob;
@@ -143,15 +142,13 @@ public class CheckRefidIsaParRefid extends EntityJob implements Observer {
           getEntity()
               .addInfo(
                   new Message(
-                      MessageType.Info,
-                      0,
                       "Could not resolve all parents of the entity with id "
                           + child.toString()
                           + ". Problematic parent: "
                           + (par.hasName()
                               ? par.getName()
                               : (par.hasCuid() ? par.getCuid() : (par.toString())))));
-          throw ServerMessages.ENTITY_HAS_INVALID_REFERENCE;
+          throw ServerMessages.ENTITY_HAS_UNQUALIFIED_REFERENCE;
         }
         if (isSubType(par.getId(), parent)) {
           return true;
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java
index 8563ea716072602684dc1a78870392d573dc3a22..645f87d0b2eb0bfc045eb3d4237a40b702c044a3 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckRefidValid.java
@@ -24,6 +24,7 @@
  */
 package org.caosdb.server.jobs.core;
 
+import org.apache.shiro.authz.AuthorizationException;
 import org.caosdb.server.database.exceptions.EntityDoesNotExistException;
 import org.caosdb.server.database.exceptions.EntityWasNotUniqueException;
 import org.caosdb.server.datatype.CollectionValue;
@@ -71,13 +72,13 @@ public class CheckRefidValid extends EntityJob implements Observer {
       }
     } catch (final Message m) {
       getEntity().addError(m);
-      getEntity().setEntityStatus(EntityStatus.UNQUALIFIED);
+    } catch (AuthorizationException exc) {
+      getEntity().addError(ServerMessages.AUTHORIZATION_ERROR);
+      getEntity().addInfo(exc.getMessage());
     } catch (final EntityDoesNotExistException e) {
       getEntity().addError(ServerMessages.REFERENCED_ENTITY_DOES_NOT_EXIST);
-      getEntity().setEntityStatus(EntityStatus.UNQUALIFIED);
     } catch (final EntityWasNotUniqueException e) {
       getEntity().addError(ServerMessages.REFERENCE_NAME_DUPLICATES);
-      getEntity().setEntityStatus(EntityStatus.UNQUALIFIED);
     }
   }
 
@@ -139,8 +140,8 @@ public class CheckRefidValid extends EntityJob implements Observer {
     }
   }
 
-  private void assertAllowedToUse(final EntityInterface referencedEntity) throws Message {
-    checkPermission(referencedEntity, EntityPermission.USE_AS_REFERENCE);
+  private void assertAllowedToUse(final EntityInterface referencedEntity) {
+    referencedEntity.checkPermission(EntityPermission.USE_AS_REFERENCE);
   }
 
   @Override
@@ -157,7 +158,7 @@ public class CheckRefidValid extends EntityJob implements Observer {
     if (ref.getEntity().hasEntityStatus()) {
       switch (ref.getEntity().getEntityStatus()) {
         case UNQUALIFIED:
-          getEntity().addError(ServerMessages.ENTITY_HAS_INVALID_REFERENCE);
+          getEntity().addError(ServerMessages.ENTITY_HAS_UNQUALIFIED_REFERENCE);
           getEntity().setEntityStatus(EntityStatus.UNQUALIFIED);
           return false;
         case DELETED:
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 1a159910044a079baba3b64c62176883ad31b8ef..a43face9ae14f3fac57890ccb8617e5637c95c16 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,35 @@ 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 String init() {
+      return StateModelPermission.class.getSimpleName();
+    }
+  }
+
+  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 +194,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 +212,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/CheckTargetPathValid.java b/src/main/java/org/caosdb/server/jobs/core/CheckTargetPathValid.java
index 7d06498f5d9e4fbc382e7dde025983ceac6a7fdc..28b914e61d33847e7beb46881c0dac4d9b17f9f0 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckTargetPathValid.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckTargetPathValid.java
@@ -37,11 +37,6 @@ import org.caosdb.server.utils.ServerMessages;
  */
 public class CheckTargetPathValid extends EntityJob {
 
-  public static final Message TARGET_PATH_NOT_IN_USER_FOLDER =
-      new Message(
-          0,
-          "According to the server's configuration, your file has to be stored to ýour user folder");
-
   @Override
   public final void run() {
     if (getEntity().hasFileProperties()) {
@@ -50,7 +45,7 @@ public class CheckTargetPathValid extends EntityJob {
       if (!file.hasPath()) {
         // the file doesn't have a path property at all
         getEntity().setEntityStatus(EntityStatus.UNQUALIFIED);
-        getEntity().addError(ServerMessages.NO_TARGET_PATH);
+        getEntity().addError(ServerMessages.FILE_HAS_NO_TARGET_PATH);
         return;
       }
 
diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckUnitPresent.java b/src/main/java/org/caosdb/server/jobs/core/CheckUnitPresent.java
index 5e3e6120fadef8f4b519ec694ebbf7588d7c2488..85eb3259dcc17cfdfd2a03aeb3d3360fb04f3908 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckUnitPresent.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckUnitPresent.java
@@ -42,11 +42,11 @@ public class CheckUnitPresent extends EntityJob {
     if (!hasUnit(getEntity())) {
       switch (getFailureSeverity()) {
         case ERROR:
-          getEntity().addError(ServerMessages.ENTITY_HAS_NO_UNIT);
+          getEntity().addError(ServerMessages.PROPERTY_HAS_NO_UNIT);
           getEntity().setEntityStatus(EntityStatus.UNQUALIFIED);
           break;
         case WARN:
-          getEntity().addWarning(ServerMessages.ENTITY_HAS_NO_UNIT);
+          getEntity().addWarning(ServerMessages.PROPERTY_HAS_NO_UNIT);
         default:
           break;
       }
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 e2f59a8827a682df508a26176a45a9fc1a874b4e..16fe593654aad760edc2b8efc2e6889222bfa78e 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/jobs/core/ExecuteQuery.java b/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java
index 399039a072d2fd7b5f54566b2f448791169a356b..0a19977b199195610d96f887c4c538a75b491511 100644
--- a/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java
+++ b/src/main/java/org/caosdb/server/jobs/core/ExecuteQuery.java
@@ -22,13 +22,16 @@
  */
 package org.caosdb.server.jobs.core;
 
+import org.caosdb.api.entity.v1.MessageCode;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.Message;
+import org.caosdb.server.entity.Message.MessageType;
 import org.caosdb.server.jobs.FlagJob;
 import org.caosdb.server.jobs.JobAnnotation;
 import org.caosdb.server.jobs.TransactionStage;
 import org.caosdb.server.query.Query;
 import org.caosdb.server.query.Query.ParsingException;
+import org.caosdb.server.query.Query.Type;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.ServerMessages;
 
@@ -49,10 +52,15 @@ public class ExecuteQuery extends FlagJob {
     } catch (final UnsupportedOperationException e) {
       getContainer().addMessage(ServerMessages.QUERY_EXCEPTION);
       getContainer().setStatus(EntityStatus.UNQUALIFIED);
-      getContainer().addMessage(new Message(e.getMessage()));
+      getContainer().addMessage(new Message(MessageType.Info, (MessageCode) null, e.getMessage()));
     }
     getContainer().addMessage(queryInstance);
-    for (EntityInterface entity : getContainer()) {
+    if (queryInstance.getQuery().getType() == Type.COUNT) {
+      getContainer()
+          .getFlags()
+          .put("query_count_result", Integer.toString(queryInstance.getCount()));
+    }
+    for (final EntityInterface entity : getContainer()) {
       getTransaction().getSchedule().addAll(loadJobs(entity, getTransaction()));
     }
   }
diff --git a/src/main/java/org/caosdb/server/jobs/core/Inheritance.java b/src/main/java/org/caosdb/server/jobs/core/Inheritance.java
index 56ac72fab9f980581cf755c5018dad48ad841f96..78e89a53593e3f112650ed81e292d0c62f071e24 100644
--- a/src/main/java/org/caosdb/server/jobs/core/Inheritance.java
+++ b/src/main/java/org/caosdb/server/jobs/core/Inheritance.java
@@ -26,6 +26,7 @@ package org.caosdb.server.jobs.core;
 
 import java.util.ArrayList;
 import java.util.List;
+import org.caosdb.api.entity.v1.MessageCode;
 import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction;
 import org.caosdb.server.entity.Entity;
 import org.caosdb.server.entity.EntityInterface;
@@ -61,7 +62,7 @@ public class Inheritance extends EntityJob {
   public static final Message ILLEGAL_INHERITANCE_MODE =
       new Message(
           MessageType.Warning,
-          0,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
           "Unknown value for flag \"inheritance\". None of the parent's properties have been transfered to the child.");
 
   @Override
diff --git a/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java b/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java
index 8133e4659d55caf29faa56eeb80ced194cd26cfc..5b5418286a81d442822aed0e7252fa4557fb21ff 100644
--- a/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java
+++ b/src/main/java/org/caosdb/server/jobs/core/InsertFilesInDir.java
@@ -30,6 +30,7 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import org.caosdb.api.entity.v1.MessageCode;
 import org.caosdb.server.CaosDBException;
 import org.caosdb.server.CaosDBServer;
 import org.caosdb.server.FileSystem;
@@ -132,8 +133,6 @@ public class InsertFilesInDir extends FlagJob {
           getContainer()
               .addMessage(
                   new Message(
-                      MessageType.Info,
-                      0,
                       "Files count in "
                           + dir.getName()
                           + "/: "
@@ -147,7 +146,12 @@ public class InsertFilesInDir extends FlagJob {
         throw new TransactionException(e);
       }
     } else {
-      getContainer().addMessage(new Message(MessageType.Error, 0, "No such directory: " + dirStr));
+      getContainer()
+          .addMessage(
+              new Message(
+                  MessageType.Error,
+                  MessageCode.MESSAGE_CODE_UNKNOWN,
+                  "No such directory: " + dirStr));
       return;
     }
   }
@@ -289,7 +293,7 @@ public class InsertFilesInDir extends FlagJob {
             .addMessage(
                 new Message(
                     MessageType.Warning,
-                    1,
+                    MessageCode.MESSAGE_CODE_UNKNOWN,
                     "Not explicitly included file: " + sub.getCanonicalPath()));
         return false;
       }
@@ -297,7 +301,9 @@ public class InsertFilesInDir extends FlagJob {
         getContainer()
             .addMessage(
                 new Message(
-                    MessageType.Warning, 2, "Explicitly excluded file: " + sub.getCanonicalPath()));
+                    MessageType.Warning,
+                    MessageCode.MESSAGE_CODE_ENTITY_DOES_NOT_EXIST,
+                    "Explicitly excluded file: " + sub.getCanonicalPath()));
         return false;
       }
     }
@@ -305,14 +311,18 @@ public class InsertFilesInDir extends FlagJob {
       getContainer()
           .addMessage(
               new Message(
-                  MessageType.Warning, 3, "Hidden directory or file: " + sub.getCanonicalPath()));
+                  MessageType.Warning,
+                  MessageCode.MESSAGE_CODE_UNKNOWN,
+                  "Hidden directory or file: " + sub.getCanonicalPath()));
       return false;
     }
     if (sub.isDirectory() && !sub.canExecute()) {
       getContainer()
           .addMessage(
               new Message(
-                  MessageType.Warning, 4, "Unaccessible directory: " + sub.getCanonicalPath()));
+                  MessageType.Warning,
+                  MessageCode.MESSAGE_CODE_UNKNOWN,
+                  "Unaccessible directory: " + sub.getCanonicalPath()));
       return false;
     }
     if (!sub.canRead()) {
@@ -320,7 +330,7 @@ public class InsertFilesInDir extends FlagJob {
           .addMessage(
               new Message(
                   MessageType.Warning,
-                  5,
+                  MessageCode.MESSAGE_CODE_UNKNOWN,
                   "Unreadable directory or file: " + sub.getCanonicalPath()));
       return false;
     }
@@ -329,7 +339,7 @@ public class InsertFilesInDir extends FlagJob {
           .addMessage(
               new Message(
                   MessageType.Warning,
-                  6,
+                  MessageCode.MESSAGE_CODE_ENTITY_HAS_UNQUALIFIED_PARENTS,
                   "Directory or file is symbolic link: " + sub.getAbsolutePath()));
       if (!this.forceSymLinks) {
         return false;
@@ -355,11 +365,15 @@ public class InsertFilesInDir extends FlagJob {
     // overlaps the directory to be inserted.
 
     if (!dir.isDirectory()) {
-      throw new Message(MessageType.Error, 0, "Dir is not a directory.");
+      throw new Message(
+          MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "Dir is not a directory.");
     }
 
     if (!dir.canRead() || !dir.canExecute()) {
-      throw new Message(MessageType.Error, 0, "Cannot read or enter the desired directory.");
+      throw new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "Cannot read or enter the desired directory.");
     }
 
     final File base = new File(FileSystem.getBasepath());
@@ -375,7 +389,10 @@ public class InsertFilesInDir extends FlagJob {
         || isSubDir(tmp, dir)
         || isSubDir(dir, root)
         || isSubDir(root, dir)) {
-      throw new Message(MessageType.Error, 0, "Dir is not allowed: " + dir.toString());
+      throw new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "Dir is not allowed: " + dir.toString());
     }
 
     for (final File f : getAllowedFolders()) {
@@ -386,7 +403,7 @@ public class InsertFilesInDir extends FlagJob {
     }
     throw new Message(
         MessageType.Error,
-        1,
+        MessageCode.MESSAGE_CODE_UNKNOWN,
         "Dir is not allowed: "
             + dir.toString()
             + " Allowed directories: "
diff --git a/src/main/java/org/caosdb/server/jobs/core/MatchFileProp.java b/src/main/java/org/caosdb/server/jobs/core/MatchFileProp.java
index cef8b2b8ae9764ceb1e4bd2636c2b532d7e9415b..249fc0b17ebe908a9a7ab9d13bfdb00d869ec630 100644
--- a/src/main/java/org/caosdb/server/jobs/core/MatchFileProp.java
+++ b/src/main/java/org/caosdb/server/jobs/core/MatchFileProp.java
@@ -46,7 +46,7 @@ public class MatchFileProp extends FilesJob {
       // descriptiveFileProperties is how the copied/uploaded/... file
       // ACTUALLY looks like. (path, checksum, size)
       final FileProperties descriptiveFileProperties =
-          getFile(normativeFileProperties.getTmpIdentifyer());
+          getFile(normativeFileProperties.getTmpIdentifier());
 
       if (descriptiveFileProperties != null) {
         checkChecksum(normativeFileProperties, descriptiveFileProperties);
diff --git a/src/main/java/org/caosdb/server/jobs/core/PickUp.java b/src/main/java/org/caosdb/server/jobs/core/PickUp.java
index 94a80836c863ea226a88e035201a2e2fb9fd1039..b0e04529fa059be33b5af99c47286a3d2a469cdb 100644
--- a/src/main/java/org/caosdb/server/jobs/core/PickUp.java
+++ b/src/main/java/org/caosdb/server/jobs/core/PickUp.java
@@ -46,12 +46,12 @@ public class PickUp extends EntityJob implements Observer {
       if (normativeFileProperties.isPickupable()) {
         try {
           entity.acceptObserver(this);
-          this.dropOffBoxPath = normativeFileProperties.getTmpIdentifyer();
+          this.dropOffBoxPath = normativeFileProperties.getTmpIdentifier();
           final FileProperties descriptiveFileProperties =
               FileSystem.pickUp(this.dropOffBoxPath, getRequestId());
           normativeFileProperties.setFile(descriptiveFileProperties.getFile());
           normativeFileProperties.setThumbnail(descriptiveFileProperties.getThumbnail());
-          normativeFileProperties.setTmpIdentifyer(null);
+          normativeFileProperties.setTmpIdentifier(null);
           if (!normativeFileProperties.hasSize()) {
             normativeFileProperties.setSize(normativeFileProperties.getFile().length());
           }
diff --git a/src/main/java/org/caosdb/server/jobs/core/TestMail.java b/src/main/java/org/caosdb/server/jobs/core/TestMail.java
index fb3e87097fae44f3f1f609dd7e347c27c6d971c2..793e6fe211f18c2d576501597c1189dac2444d46 100644
--- a/src/main/java/org/caosdb/server/jobs/core/TestMail.java
+++ b/src/main/java/org/caosdb/server/jobs/core/TestMail.java
@@ -25,7 +25,6 @@ package org.caosdb.server.jobs.core;
 import org.caosdb.server.CaosDBServer;
 import org.caosdb.server.ServerProperties;
 import org.caosdb.server.entity.Message;
-import org.caosdb.server.entity.Message.MessageType;
 import org.caosdb.server.jobs.FlagJob;
 import org.caosdb.server.jobs.JobAnnotation;
 import org.caosdb.server.utils.ServerMessages;
@@ -44,13 +43,12 @@ public class TestMail extends FlagJob {
       CaosDBServer.getServerProperty(ServerProperties.KEY_NO_REPLY_EMAIL);
 
   @Override
-  protected void job(final String value) {
-    if (CaosDBServer.isDebugMode() && value != null) {
-      if (Utils.isRFC822Compliant(value)) {
-        final Mail m = new Mail(NAME, EMAIL, null, value, "Test mail", "This is a test mail.");
+  protected void job(final String recipient) {
+    if (CaosDBServer.isDebugMode() && recipient != null) {
+      if (Utils.isRFC822Compliant(recipient)) {
+        final Mail m = new Mail(NAME, EMAIL, null, recipient, "Test mail", "This is a test mail.");
         m.send();
-        getContainer()
-            .addMessage(new Message(MessageType.Info, 0, "A mail has been send to " + value));
+        getContainer().addMessage(new Message("A mail has been sent to " + recipient));
       } else {
         getContainer().addMessage(ServerMessages.EMAIL_NOT_WELL_FORMED);
       }
diff --git a/src/main/java/org/caosdb/server/jobs/core/UniqueName.java b/src/main/java/org/caosdb/server/jobs/core/UniqueName.java
index 2836ccf742ebbd1bbda5a8ed3ae95ac0d69c2fbb..0ef9792556e2b8b6e3981f66cb697ced5659c7a1 100644
--- a/src/main/java/org/caosdb/server/jobs/core/UniqueName.java
+++ b/src/main/java/org/caosdb/server/jobs/core/UniqueName.java
@@ -46,7 +46,7 @@ public class UniqueName extends FlagJob {
       } catch (final EntityDoesNotExistException e) {
         // ok
       } catch (final EntityWasNotUniqueException e) {
-        entity.addError(ServerMessages.NAME_IS_NOT_UNIQUE);
+        entity.addError(ServerMessages.ENTITY_NAME_IS_NOT_UNIQUE);
         entity.setEntityStatus(EntityStatus.UNQUALIFIED);
         return;
       }
@@ -55,7 +55,7 @@ public class UniqueName extends FlagJob {
       for (final EntityInterface e : getContainer()) {
         if (entity != e && e.hasName() && e.getName().equals(entity.getName())) {
           entity.setEntityStatus(EntityStatus.UNQUALIFIED);
-          entity.addError(ServerMessages.NAME_IS_NOT_UNIQUE);
+          entity.addError(ServerMessages.ENTITY_NAME_IS_NOT_UNIQUE);
           return;
         }
       }
diff --git a/src/main/java/org/caosdb/server/permissions/CaosPermission.java b/src/main/java/org/caosdb/server/permissions/CaosPermission.java
index bfbb7eb2e8515f71bb2d611d4fd75916cfae50e7..615b4d1a9c26c1addb955506ab1debddb729db9c 100644
--- a/src/main/java/org/caosdb/server/permissions/CaosPermission.java
+++ b/src/main/java/org/caosdb/server/permissions/CaosPermission.java
@@ -22,12 +22,11 @@
  */
 package org.caosdb.server.permissions;
 
+import java.util.Collection;
 import java.util.HashSet;
-import java.util.Map;
 import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.authz.Permission;
 import org.apache.shiro.subject.Subject;
-import org.eclipse.jetty.util.ajax.JSON;
 
 public class CaosPermission extends HashSet<PermissionRule> implements Permission {
 
@@ -35,16 +34,33 @@ public class CaosPermission extends HashSet<PermissionRule> implements Permissio
     super(rules);
   }
 
-  public CaosPermission() {}
+  public Collection<String> getStringPermissions(Subject subject) {
+    HashSet<String> grant = new HashSet<>();
+    HashSet<String> prio_grant = new HashSet<>();
+    HashSet<String> deny = new HashSet<>();
+    HashSet<String> prio_deny = new HashSet<>();
 
-  public static CaosPermission parseJSON(final String json) {
-    final CaosPermission ret = new CaosPermission();
-    @SuppressWarnings("unchecked")
-    final Map<String, String>[] rules = (Map<String, String>[]) JSON.parse(json);
-    for (final Map<String, String> rule : rules) {
-      ret.add(PermissionRule.parse(rule));
+    for (PermissionRule r : this) {
+      String p = subject == null ? r.getPermission() : r.getPermission(subject).toString();
+      if (r.isGrant()) {
+        if (r.isPriority()) {
+          prio_grant.add(p);
+        } else {
+          grant.add(p);
+        }
+      } else {
+        if (r.isPriority()) {
+          prio_deny.add(p);
+        } else {
+          deny.add(p);
+        }
+      }
     }
-    return ret;
+
+    grant.removeAll(deny);
+    grant.addAll(prio_grant);
+    grant.removeAll(prio_deny);
+    return grant;
   }
 
   private static final long serialVersionUID = 2136265443788256009L;
diff --git a/src/main/java/org/caosdb/server/permissions/EntityACI.java b/src/main/java/org/caosdb/server/permissions/EntityACI.java
index ccc889decafc941484432c99bf48908c21dff209..34d713eb69179cf7e88103cca2dd901077ffc092 100644
--- a/src/main/java/org/caosdb/server/permissions/EntityACI.java
+++ b/src/main/java/org/caosdb/server/permissions/EntityACI.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,11 +19,12 @@
  * 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.permissions;
 
 import java.util.HashMap;
+import java.util.Set;
 
 public final class EntityACI {
 
@@ -72,4 +74,16 @@ public final class EntityACI {
     map.put("bitSet", getBitSet());
     return map;
   }
+
+  public boolean isGrant() {
+    return EntityACL.isAllowance(bitSet);
+  }
+
+  public boolean isPriority() {
+    return EntityACL.isPriorityBitSet(bitSet);
+  }
+
+  public Set<EntityPermission> getPermission() {
+    return EntityACL.getPermissionsFromBitSet(bitSet);
+  }
 }
diff --git a/src/main/java/org/caosdb/server/permissions/EntityACL.java b/src/main/java/org/caosdb/server/permissions/EntityACL.java
index cfa436d59ae25971a08d4314a7f668a70cf75bbf..f959ba89bd08cd5996af18540a484292a5046f94 100644
--- a/src/main/java/org/caosdb/server/permissions/EntityACL.java
+++ b/src/main/java/org/caosdb/server/permissions/EntityACL.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,8 +19,8 @@
  * 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.permissions;
 
 import static org.caosdb.server.permissions.Role.OTHER_ROLE;
@@ -152,6 +153,7 @@ public class EntityACL {
       }
     }
     if (acl.isEmpty()) {
+      // There haven't been any rules which apply to this subject. Thus the rules for ?OTHER? apply.
       acl.addAll(forOthers);
     }
     return getPermissionsFromBitSet(getResultingACL(acl));
@@ -191,7 +193,9 @@ public class EntityACL {
   public static final List<ResponsibleAgent> getOwners(final Collection<EntityACI> acl) {
     final List<ResponsibleAgent> owners = new ArrayList<>();
     for (final EntityACI aci : acl) {
-      if (isOwnerBitSet(aci.getBitSet()) && !aci.getResponsibleAgent().equals(OWNER_ROLE)) {
+      if (aci.isGrant()
+          && isOwnerBitSet(aci.getBitSet())
+          && !aci.getResponsibleAgent().equals(OWNER_ROLE)) {
         owners.add(aci.getResponsibleAgent());
       }
     }
@@ -358,10 +362,6 @@ public class EntityACL {
     return new EntityACL(newACL);
   }
 
-  public static final void normalize(final Collection<EntityACI> acl) {
-    // every priority denial removes all other items
-  }
-
   public EntityACL getPriorityEntityACL() {
     return getPriorityEntityACL(this);
   }
diff --git a/src/main/java/org/caosdb/server/permissions/EntityPermission.java b/src/main/java/org/caosdb/server/permissions/EntityPermission.java
index 1f84dce34a611f3b25bea5ddaab0d40c9723a280..f8f21a354ef68403f852a1f426e28afed857278d 100644
--- a/src/main/java/org/caosdb/server/permissions/EntityPermission.java
+++ b/src/main/java/org/caosdb/server/permissions/EntityPermission.java
@@ -37,6 +37,11 @@ public class EntityPermission extends Permission {
   private static final long serialVersionUID = -8935713878537140286L;
   private static List<EntityPermission> instances = new ArrayList<>();
   private final int bitNumber;
+  private final org.caosdb.api.entity.v1.EntityPermission mapping;
+  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");
@@ -57,9 +62,14 @@ public class EntityPermission extends Permission {
     };
   }
 
-  private EntityPermission(final String shortName, final String description, final int bitNumber) {
+  private EntityPermission(
+      final String shortName,
+      final String description,
+      final int bitNumber,
+      org.caosdb.api.entity.v1.EntityPermission mapping) {
     super(shortName, description);
     this.bitNumber = bitNumber;
+    this.mapping = mapping;
     if (bitNumber > 61) {
       throw new CaosDBException(
           "This bitNumber is too big. This implementation only handles bitNumbers up to 61.");
@@ -102,6 +112,16 @@ public class EntityPermission extends Permission {
     throw new IllegalArgumentException("Permission is not defined.");
   }
 
+  public static EntityPermission getEntityPermission(
+      final org.caosdb.api.entity.v1.EntityPermission permission) {
+    for (final EntityPermission p : instances) {
+      if (p.getMapping() == permission) {
+        return p;
+      }
+    }
+    throw new IllegalArgumentException("Permission not found." + permission.name());
+  }
+
   public long getBitSet() {
     return (long) Math.pow(2, getBitNumber());
   }
@@ -116,78 +136,153 @@ public class EntityPermission extends Permission {
     return ret;
   }
 
+  public org.caosdb.api.entity.v1.EntityPermission getMapping() {
+    return mapping;
+  }
+
   public static final EntityPermission RETRIEVE_ENTITY =
       new EntityPermission(
           "RETRIEVE:ENTITY",
           "Permission to retrieve the full entity (name, description, data type, ...) with all parents and properties (unless prohibited by another rule on the property level).",
-          4);
+          4,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_RETRIEVE_ENTITY);
   public static final EntityPermission RETRIEVE_ACL =
       new EntityPermission(
-          "RETRIEVE:ACL", "Permission to retrieve the full and final ACL of this entity.", 5);
+          "RETRIEVE:ACL",
+          "Permission to retrieve the full and final ACL of this entity.",
+          5,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_RETRIEVE_ACL);
   public static final EntityPermission RETRIEVE_HISTORY =
       new EntityPermission(
-          "RETRIEVE:HISTORY", "Permission to retrieve the history of this entity.", 6);
+          "RETRIEVE:HISTORY",
+          "Permission to retrieve the history of this entity.",
+          6,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_RETRIEVE_HISTORY);
   public static final EntityPermission RETRIEVE_OWNER =
       new EntityPermission(
-          "RETRIEVE:OWNER", "Permission to retrieve the owner(s) of this entity.", 9);
+          "RETRIEVE:OWNER",
+          "Permission to retrieve the owner(s) of this entity.",
+          9,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_RETRIEVE_OWNER);
   public static final EntityPermission RETRIEVE_FILE =
       new EntityPermission(
-          "RETRIEVE:FILE", "Permission to download the file belonging to this entity.", 10);
+          "RETRIEVE:FILE",
+          "Permission to download the file belonging to this entity.",
+          10,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_RETRIEVE_FILE);
   public static final EntityPermission DELETE =
-      new EntityPermission("DELETE", "Permission to delete an entity.", 1);
+      new EntityPermission(
+          "DELETE",
+          "Permission to delete an entity.",
+          1,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_DELETE);
   public static final EntityPermission EDIT_ACL =
       new EntityPermission(
           "EDIT:ACL",
           "Permission to change the user-specified part of this entity's ACL. Roles with this Permission are called 'Owners'.",
-          0);
+          0,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_EDIT_ACL);
   public static final EntityPermission UPDATE_DESCRIPTION =
       new EntityPermission(
-          "UPDATE:DESCRIPTION", "Permission to change the value of this entity.", 11);
+          "UPDATE:DESCRIPTION",
+          "Permission to change the value of this entity.",
+          11,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_DESCRIPTION);
   public static final EntityPermission UPDATE_VALUE =
-      new EntityPermission("UPDATE:VALUE", "Permission to change the value of this entity.", 12);
+      new EntityPermission(
+          "UPDATE:VALUE",
+          "Permission to change the value of this entity.",
+          12,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_VALUE);
   public static final EntityPermission UPDATE_ROLE =
-      new EntityPermission("UPDATE:ROLE", "Permission to change the role of this entity.", 13);
+      new EntityPermission(
+          "UPDATE:ROLE",
+          "Permission to change the role of this entity.",
+          13,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_ROLE);
   public static final EntityPermission UPDATE_REMOVE_PARENT =
       new EntityPermission(
-          "UPDATE:PARENT:REMOVE", "Permission  to remove parents from this entity.", 14);
+          "UPDATE:PARENT:REMOVE",
+          "Permission  to remove parents from this entity.",
+          14,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_REMOVE_PARENT);
   public static final EntityPermission UPDATE_ADD_PARENT =
-      new EntityPermission("UPDATE:PARENT:ADD", "Permission to add a parent to this entity.", 15);
+      new EntityPermission(
+          "UPDATE:PARENT:ADD",
+          "Permission to add a parent to this entity.",
+          15,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_ADD_PARENT);
   public static final EntityPermission UPDATE_REMOVE_PROPERTY =
       new EntityPermission(
-          "UPDATE:PROPERTY:REMOVE", "Permission to remove properties from this entity.", 16);
+          "UPDATE:PROPERTY:REMOVE",
+          "Permission to remove properties from this entity.",
+          16,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_REMOVE_PROPERTY);
   public static final EntityPermission UPDATE_ADD_PROPERTY =
       new EntityPermission(
-          "UPDATE:PROPERTY:ADD", "Permission to add a property to this entity.", 17);
+          "UPDATE:PROPERTY:ADD",
+          "Permission to add a property to this entity.",
+          17,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_ADD_PROPERTY);
   public static final EntityPermission UPDATE_NAME =
-      new EntityPermission("UPDATE:NAME", "Permission to change the name of this entity.", 19);
+      new EntityPermission(
+          "UPDATE:NAME",
+          "Permission to change the name of this entity.",
+          19,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_NAME);
   public static final EntityPermission UPDATE_DATA_TYPE =
       new EntityPermission(
-          "UPDATE:DATA_TYPE", "Permission to change the data type of this entity.", 20);
+          "UPDATE:DATA_TYPE",
+          "Permission to change the data type of this entity.",
+          20,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_DATA_TYPE);
   public static final EntityPermission UPDATE_REMOVE_FILE =
       new EntityPermission(
-          "UPDATE:FILE:REMOVE", "Permission to delete the file of this entity.", 21);
+          "UPDATE:FILE:REMOVE",
+          "Permission to delete the file of this entity.",
+          21,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_REMOVE_FILE);
   public static final EntityPermission UPDATE_ADD_FILE =
-      new EntityPermission("UPDATE:FILE:ADD", "Permission to set a file for this entity.", 22);
+      new EntityPermission(
+          "UPDATE:FILE:ADD",
+          "Permission to set a file for this entity.",
+          22,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_ADD_FILE);
   public static final EntityPermission UPDATE_MOVE_FILE =
       new EntityPermission(
-          "UPDATE:FILE:MOVE", "Permission to move an existing file to a new location.", 23);
+          "UPDATE:FILE:MOVE",
+          "Permission to move an existing file to a new location.",
+          23,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_MOVE_FILE);
   public static final EntityPermission USE_AS_REFERENCE =
       new EntityPermission(
-          "USE:AS_REFERENCE", "Permission to refer to this entity via a reference property.", 24);
+          "USE:AS_REFERENCE",
+          "Permission to refer to this entity via a reference property.",
+          24,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_USE_AS_REFERENCE);
   public static final EntityPermission USE_AS_PROPERTY =
       new EntityPermission(
-          "USE:AS_PROPERTY", "Permission to implement this entity as a property.", 25);
+          "USE:AS_PROPERTY",
+          "Permission to implement this entity as a property.",
+          25,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_USE_AS_PROPERTY);
   public static final EntityPermission USE_AS_PARENT =
       new EntityPermission(
-          "USE:AS_PARENT", "Permission to use this entity as a super type for other entities.", 26);
+          "USE:AS_PARENT",
+          "Permission to use this entity as a super type for other entities.",
+          26,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_USE_AS_PARENT);
   public static final EntityPermission USE_AS_DATA_TYPE =
       new EntityPermission(
           "USE:AS_DATA_TYPE",
           "Permission to use this entity as a data type for reference properties.",
-          27);
+          27,
+          org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_USE_AS_DATA_TYPE);
   public static final EntityPermission UPDATE_QUERY_TEMPLATE_DEFINITION =
       new EntityPermission(
           "UPDATE:QUERY_TEMPLATE_DEFINITION",
           "Permission to update the query template definition of this QueryTemplate",
-          28);
+          28,
+          org.caosdb.api.entity.v1.EntityPermission
+              .ENTITY_PERMISSION_UPDATE_QUERY_TEMPLATE_DEFINITION);
 }
diff --git a/src/main/java/org/caosdb/server/permissions/Permission.java b/src/main/java/org/caosdb/server/permissions/Permission.java
index 44eee1c68a073c405233195aee491bcf10fcd38c..e8f33a3ae3f9eb0956bae6bc7c7eceefdc5663b7 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/permissions/PermissionRule.java b/src/main/java/org/caosdb/server/permissions/PermissionRule.java
index 85d3b62834a67a4fc46ea9b3c38d19e4b8261d74..2e8b6984509761d649acb27b5482c8df7bff9933 100644
--- a/src/main/java/org/caosdb/server/permissions/PermissionRule.java
+++ b/src/main/java/org/caosdb/server/permissions/PermissionRule.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,8 +19,8 @@
  * 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.permissions;
 
 import java.util.HashMap;
@@ -43,6 +44,9 @@ public class PermissionRule {
   public PermissionRule(final boolean grant, final boolean priority, final String permission) {
     this.grant = grant;
     this.priority = priority;
+    if (permission == null) {
+      throw new NullPointerException("permission cannot be null");
+    }
     this.permission = permission;
   }
 
@@ -54,6 +58,10 @@ public class PermissionRule {
     return this.priority;
   }
 
+  public String getPermission() {
+    return permission;
+  }
+
   public Permission getPermission(String realm, String username) {
     return new WildcardPermission(
         permission.replaceAll("\\?REALM\\?", realm).replaceAll("\\?USERNAME\\?", username));
@@ -72,6 +80,31 @@ public class PermissionRule {
     return ret;
   }
 
+  @Override
+  public String toString() {
+    return "PermissionRule("
+        + (priority ? "[P]" : "")
+        + (grant ? "GRANT" : "DENY")
+        + permission
+        + ")";
+  }
+
+  @Override
+  public int hashCode() {
+    return permission.hashCode() + (grant ? 3 : 0) - (priority ? 5 : 0);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof PermissionRule) {
+      PermissionRule that = (PermissionRule) obj;
+      return this.grant == that.grant
+          && this.priority == that.priority
+          && this.permission.equals(that.permission);
+    }
+    return false;
+  }
+
   public static PermissionRule parse(final Element e) {
     return new PermissionRule(
         e.getName().equalsIgnoreCase("Grant"),
diff --git a/src/main/java/org/caosdb/server/permissions/Role.java b/src/main/java/org/caosdb/server/permissions/Role.java
index 54afefc664c330dfc4465285347a1463d9df0d5e..f7f4e55e195d72ba3a42a8b909d2c87944bcbf81 100644
--- a/src/main/java/org/caosdb/server/permissions/Role.java
+++ b/src/main/java/org/caosdb/server/permissions/Role.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,8 +19,8 @@
  * 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.permissions;
 
 import java.util.HashMap;
@@ -31,6 +32,7 @@ public class Role implements ResponsibleAgent {
   public static final Role OWNER_ROLE = new Role("?OWNER?");
   public static final Role OTHER_ROLE = new Role("?OTHER?");
   public static final Role ANONYMOUS_ROLE = new Role("anonymous");
+  public static final Role ADMINISTRATION = new Role("administration");
 
   private final String role;
 
diff --git a/src/main/java/org/caosdb/server/query/CQLParser.g4 b/src/main/java/org/caosdb/server/query/CQLParser.g4
index c4311ac1cbe03f490b79456e091cc0e73b94c506..f8c2a3b5ea4e7c34f3cc8fd07cdc77c912a5cc7b 100644
--- a/src/main/java/org/caosdb/server/query/CQLParser.g4
+++ b/src/main/java/org/caosdb/server/query/CQLParser.g4
@@ -287,15 +287,36 @@ pov returns [POV filter] locals [Query.Pattern p, String o, String v, String a]
 ;
 
 
-subproperty returns [SubProperty subp] locals [String p]
+subproperty returns [SubProperty subp]
 @init{
-    $p = null;
     $subp = null;
 }
 :
-    entity_filter {$subp = new SubProperty($entity_filter.filter);}
+	subproperty_filter {$subp = new SubProperty($subproperty_filter.filter);}
 ;
 
+subproperty_filter returns [EntityFilterInterface filter]
+    @init{
+        $filter = null;
+    }
+:
+    which_exp
+    (
+        (
+            LPAREN WHITE_SPACE?
+            (
+                filter_expression {$filter = $filter_expression.efi;}
+                | conjunction {$filter = $conjunction.c;}
+                | disjunction {$filter = $disjunction.d;}
+            )
+            RPAREN
+        ) | (
+            filter_expression {$filter = $filter_expression.efi;}
+        )
+    )?
+;
+
+
 backreference returns [Backreference ref] locals [Query.Pattern e, Query.Pattern p]
     @init{
         $e = null;
@@ -328,7 +349,7 @@ storedat returns [StoredAt filter] locals [String loc]
     WHITE_SPACE?
 ;
 
-conjunction returns [Conjunction c] locals [Conjunction dummy]
+conjunction returns [Conjunction c]
     @init{
         $c = new Conjunction();
     }
@@ -493,7 +514,7 @@ number_with_unit
 
 unit
 :
-    (~(WHITE_SPACE | WHICH | HAS_A | WITH | WHERE | DOT | AND | OR ))
+    (~(WHITE_SPACE | WHICH | HAS_A | WITH | WHERE | DOT | AND | OR | RPAREN ))
     (~(WHITE_SPACE))*
     |
     NUM SLASH (~(WHITE_SPACE))+
@@ -510,7 +531,7 @@ atom returns [Query.Pattern ep]
 :
     double_quoted {$ep = $double_quoted.ep;}
     | single_quoted {$ep = $single_quoted.ep;}
-    | (~(WHITE_SPACE | DOT ))+  {$ep = new Query.Pattern($text, Query.Pattern.TYPE_NORMAL);}
+    | (~(WHITE_SPACE | DOT | RPAREN | LPAREN ))+ {$ep = new Query.Pattern($text, Query.Pattern.TYPE_NORMAL);}
 ;
 
 single_quoted returns [Query.Pattern ep] locals [StringBuffer sb, int patternType]
diff --git a/src/main/java/org/caosdb/server/query/Query.java b/src/main/java/org/caosdb/server/query/Query.java
index d57189c368d46ae8edf7fbf70b75fff0ffb278f3..3915243565182aa4b680dec253ef65812d77cd30 100644
--- a/src/main/java/org/caosdb/server/query/Query.java
+++ b/src/main/java/org/caosdb/server/query/Query.java
@@ -46,6 +46,8 @@ import org.antlr.v4.runtime.CharStreams;
 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;
@@ -216,7 +218,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
   }
 
   public static class IdVersionPair {
-    public IdVersionPair(Integer id, String version) {
+    public IdVersionPair(final Integer id, final String version) {
       this.id = id;
       this.version = version;
     }
@@ -233,9 +235,9 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
     }
 
     @Override
-    public boolean equals(Object obj) {
+    public boolean equals(final Object obj) {
       if (obj instanceof IdVersionPair) {
-        IdVersionPair that = (IdVersionPair) obj;
+        final IdVersionPair that = (IdVersionPair) obj;
         return this.id == that.id && this.version == that.version;
       }
       return false;
@@ -247,12 +249,12 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
     }
   }
 
-  private boolean filterEntitiesWithoutRetrievePermisions =
+  private final boolean filterEntitiesWithoutRetrievePermisions =
       !CaosDBServer.getServerProperty(
               ServerProperties.KEY_QUERY_FILTER_ENTITIES_WITHOUT_RETRIEVE_PERMISSIONS)
           .equalsIgnoreCase("FALSE");
 
-  private Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
+  private final Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
   List<IdVersionPair> resultSet = null;
   private final String query;
   private Pattern entity = null;
@@ -503,8 +505,8 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
     }
   }
 
-  private String initQuery(boolean versioned) throws QueryException {
-    String sql = "call initQuery(" + versioned + ")";
+  private String initQuery(final boolean versioned) throws QueryException {
+    final String sql = "call initQuery(" + versioned + ")";
     try (final CallableStatement callInitQuery = getConnection().prepareCall(sql)) {
       ResultSet initQueryResult = null;
       initQueryResult = callInitQuery.executeQuery();
@@ -523,7 +525,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
    * @param optimize whether to run optimize() immediately.
    * @throws ParsingException
    */
-  public void parse(boolean optimize) throws ParsingException {
+  public void parse(final boolean optimize) throws ParsingException {
     final long t1 = System.currentTimeMillis();
     CQLLexer lexer;
     lexer = new CQLLexer(CharStreams.fromString(this.query));
@@ -586,7 +588,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
     }
   }
 
-  private String executeStrategy(boolean versioned) throws QueryException {
+  private String executeStrategy(final boolean versioned) throws QueryException {
     if (this.entity != null) {
       return sourceStrategy(initQuery(versioned));
     } else if (this.role == Role.ENTITY && this.filter == null) {
@@ -609,7 +611,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
    * @throws QueryException
    */
   private String generateSelectStatementForResultSet(
-      final String resultSetTableName, boolean versioned) {
+      final String resultSetTableName, final boolean versioned) {
     if (resultSetTableName.equals("entities")) {
       return "SELECT entity_id AS id"
           + (versioned ? ", version AS version" : "")
@@ -634,7 +636,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
    * @return list of results of this query.
    * @throws QueryException
    */
-  private List<IdVersionPair> getResultSet(final String resultSetTableName, boolean versioned)
+  private List<IdVersionPair> getResultSet(final String resultSetTableName, final boolean versioned)
       throws QueryException {
     ResultSet finishResultSet = null;
     try {
@@ -730,7 +732,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
    * @param key
    * @param resultSet
    */
-  private void setCache(String key, List<IdVersionPair> resultSet) {
+  private void setCache(final String key, final List<IdVersionPair> resultSet) {
     synchronized (cache) {
       if (resultSet instanceof Serializable) {
         cache.put(key, (Serializable) resultSet);
@@ -747,11 +749,11 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
    * @return
    */
   @SuppressWarnings("unchecked")
-  private List<IdVersionPair> getCached(String key) {
+  private List<IdVersionPair> getCached(final String key) {
     return (List<IdVersionPair>) cache.get(key);
   }
 
-  protected void executeNoCache(Access access) {
+  protected void executeNoCache(final Access access) {
     try {
       this.resultSet = getResultSet(executeStrategy(this.versioned), this.versioned);
     } finally {
@@ -760,7 +762,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
   }
 
   private void addWarning(final String w) {
-    this.messages.add(new Message(MessageType.Warning, 0, w));
+    this.messages.add(new Message(MessageType.Warning, MessageCode.MESSAGE_CODE_UNKNOWN, w));
   }
 
   private void cleanUp() {
@@ -841,7 +843,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
       return entities;
     }
 
-    List<IdVersionPair> result = new ArrayList<>();
+    final List<IdVersionPair> result = new ArrayList<>();
     final Iterator<IdVersionPair> iterator = entities.iterator();
     while (iterator.hasNext()) {
       final long t1 = System.currentTimeMillis();
@@ -908,11 +910,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
       return;
     }
     ret.setAttribute("string", this.query);
-    if (this.resultSet != null) {
-      ret.setAttribute("results", Integer.toString(this.resultSet.size()));
-    } else {
-      ret.setAttribute("results", "0");
-    }
+    ret.setAttribute("results", Integer.toString(getCount()));
     ret.setAttribute("cached", Boolean.toString(this.cached));
     ret.setAttribute("etag", cacheETag);
 
@@ -1013,11 +1011,19 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
    * @return A Cache key.
    */
   String getCacheKey() {
-    StringBuilder sb = new StringBuilder();
-    if (this.versioned) sb.append("versioned");
-    if (this.role != null) sb.append(this.role.toString());
-    if (this.entity != null) sb.append(this.entity.toString());
-    if (this.filter != null) sb.append(this.filter.getCacheKey());
+    final StringBuilder sb = new StringBuilder();
+    if (this.versioned) {
+      sb.append("versioned");
+    }
+    if (this.role != null) {
+      sb.append(this.role.toString());
+    }
+    if (this.entity != null) {
+      sb.append(this.entity.toString());
+    }
+    if (this.filter != null) {
+      sb.append(this.filter.getCacheKey());
+    }
     return sb.toString();
   }
 
@@ -1040,4 +1046,17 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac
   public static String getETag() {
     return cacheETag;
   }
+
+  public int getCount() {
+    if (this.resultSet != null) {
+      return this.resultSet.size();
+    } else {
+      return -1;
+    }
+  }
+
+  @Override
+  public UTCDateTime getTimestamp() {
+    return null;
+  }
 }
diff --git a/src/main/java/org/caosdb/server/resource/AbstractCaosDBServerResource.java b/src/main/java/org/caosdb/server/resource/AbstractCaosDBServerResource.java
index 876b43a160f673b96a7220a746c9a7dcd4951388..44ebfa22d08c975031f5bf5bfca325d6b706af24 100644
--- a/src/main/java/org/caosdb/server/resource/AbstractCaosDBServerResource.java
+++ b/src/main/java/org/caosdb/server/resource/AbstractCaosDBServerResource.java
@@ -251,6 +251,7 @@ public abstract class AbstractCaosDBServerResource extends ServerResource {
     try {
       return httpPostInChildClass(entity);
     } catch (final Throwable t) {
+      t.printStackTrace();
       return handleThrowable(t);
     }
   }
@@ -388,6 +389,7 @@ public abstract class AbstractCaosDBServerResource extends ServerResource {
   public Representation handleThrowable(final Throwable t) {
     try {
       getRequest().getAttributes().put("THROWN", t);
+      t.printStackTrace();
       throw t;
     } catch (final AuthenticationException e) {
       return error(ServerMessages.UNAUTHENTICATED, Status.CLIENT_ERROR_UNAUTHORIZED);
diff --git a/src/main/java/org/caosdb/server/resource/RolesResource.java b/src/main/java/org/caosdb/server/resource/RolesResource.java
index a89a731950d09a7edbce0a5f67a3e81287c07b2a..ad1544da3a326f1c16e0b01238985d10e93b0581 100644
--- a/src/main/java/org/caosdb/server/resource/RolesResource.java
+++ b/src/main/java/org/caosdb/server/resource/RolesResource.java
@@ -26,7 +26,6 @@ import java.io.IOException;
 import java.security.NoSuchAlgorithmException;
 import java.sql.SQLException;
 import org.caosdb.server.CaosDBException;
-import org.caosdb.server.accessControl.ACMPermissions;
 import org.caosdb.server.accessControl.Role;
 import org.caosdb.server.database.backend.implementation.MySQL.ConnectionException;
 import org.caosdb.server.entity.Message;
@@ -55,7 +54,6 @@ public class RolesResource extends AbstractCaosDBServerResource {
     if (getRequestedItems().length > 0) {
       final String name = getRequestedItems()[0];
       if (name != null) {
-        getUser().checkPermission(ACMPermissions.PERMISSION_RETRIEVE_ROLE_DESCRIPTION(name));
         final RetrieveRoleTransaction t = new RetrieveRoleTransaction(name);
         try {
           t.execute();
diff --git a/src/main/java/org/caosdb/server/resource/ScriptingResource.java b/src/main/java/org/caosdb/server/resource/ScriptingResource.java
index 35885bb4376b26fc04977389f4dbf1cd7c40decd..6e7cf721cb11bf95df23396bc79d5691335708a6 100644
--- a/src/main/java/org/caosdb/server/resource/ScriptingResource.java
+++ b/src/main/java/org/caosdb/server/resource/ScriptingResource.java
@@ -96,7 +96,17 @@ public class ScriptingResource extends AbstractCaosDBServerResource {
         return null;
       }
     } catch (Message m) {
-      return error(m, Status.valueOf(m.getCode()));
+      if (m == ServerMessages.SERVER_SIDE_SCRIPT_DOES_NOT_EXIST) {
+        return error(m, Status.CLIENT_ERROR_NOT_FOUND);
+      } else if (m == ServerMessages.SERVER_SIDE_SCRIPT_MISSING_CALL) {
+        return error(m, Status.CLIENT_ERROR_BAD_REQUEST);
+      } else if (m == ServerMessages.SERVER_SIDE_SCRIPT_NOT_EXECUTABLE) {
+        return error(m, Status.CLIENT_ERROR_BAD_REQUEST);
+      } else if (m == ServerMessages.SERVER_SIDE_SCRIPT_TIMEOUT) {
+        return error(m, Status.CLIENT_ERROR_BAD_REQUEST);
+      }
+
+      return error(m, Status.SERVER_ERROR_INTERNAL);
     } finally {
       deleteTmpFiles();
     }
@@ -142,7 +152,7 @@ public class ScriptingResource extends AbstractCaosDBServerResource {
         final FileProperties file = FileSystem.upload(item, this.getSRID());
         deleteTmpFileAfterTermination(file);
         file.setPath(item.getName());
-        file.setTmpIdentifyer(item.getFieldName());
+        file.setTmpIdentifier(item.getFieldName());
         files.add(file);
         form.add(
             new Parameter(
diff --git a/src/main/java/org/caosdb/server/resource/Webinterface.java b/src/main/java/org/caosdb/server/resource/Webinterface.java
index adba714a96e5bbb4bc3ea13b8d72ec69a88cb33b..95901260937a5364a68ed3e9e6849210bd67d6b3 100644
--- a/src/main/java/org/caosdb/server/resource/Webinterface.java
+++ b/src/main/java/org/caosdb/server/resource/Webinterface.java
@@ -81,7 +81,9 @@ public class Webinterface extends ServerResource {
                             ? MediaType.IMAGE_PNG
                             : path.endsWith(".html")
                                 ? MediaType.TEXT_HTML
-                                : path.endsWith(".yaml") ? MediaType.TEXT_YAML : MediaType.TEXT_XML;
+                                : path.endsWith(".yaml")
+                                    ? MediaType.TEXT_YAML
+                                    : path.endsWith(".xml") ? MediaType.TEXT_XML : MediaType.ALL;
 
     final FileRepresentation ret = new FileRepresentation(file, mt);
 
diff --git a/src/main/java/org/caosdb/server/scripting/ScriptingPermissions.java b/src/main/java/org/caosdb/server/scripting/ScriptingPermissions.java
index 78ad99d77d06b681838e455ad391c62e7ce85ca6..0af49e9faf6ec8ad9a4107d72554a44744184661 100644
--- a/src/main/java/org/caosdb/server/scripting/ScriptingPermissions.java
+++ b/src/main/java/org/caosdb/server/scripting/ScriptingPermissions.java
@@ -1,11 +1,29 @@
 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);
+  }
+
+  public static String init() {
+    return ScriptingPermissions.class.getSimpleName();
   }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java b/src/main/java/org/caosdb/server/transaction/AccessControlTransaction.java
index 4897cd65620577e36a98f2db5e5530a42b3f43d0..1cf9f8f70df2eaa384e4074cc3b80a4816a88142 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/DeleteRoleTransaction.java b/src/main/java/org/caosdb/server/transaction/DeleteRoleTransaction.java
index ad9a9925f8bed2ef03a1bcf9cd6b8c79adca6345..2e7e5b2a5ad4354b517dc7020f44f5ece459cc35 100644
--- a/src/main/java/org/caosdb/server/transaction/DeleteRoleTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/DeleteRoleTransaction.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,8 +19,8 @@
  * 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.transaction;
 
 import org.apache.shiro.SecurityUtils;
@@ -27,6 +28,8 @@ import org.caosdb.server.accessControl.ACMPermissions;
 import org.caosdb.server.database.backend.transaction.DeleteRole;
 import org.caosdb.server.database.backend.transaction.RetrieveRole;
 import org.caosdb.server.database.backend.transaction.SetPermissionRules;
+import org.caosdb.server.database.exceptions.TransactionException;
+import org.caosdb.server.entity.Message;
 import org.caosdb.server.utils.ServerMessages;
 
 public class DeleteRoleTransaction extends AccessControlTransaction {
@@ -41,10 +44,19 @@ public class DeleteRoleTransaction extends AccessControlTransaction {
   protected void transaction() throws Exception {
     SecurityUtils.getSubject().checkPermission(ACMPermissions.PERMISSION_DELETE_ROLE(this.name));
 
+    if (this.name == "administration" || this.name == "anonymous") {
+      throw ServerMessages.SPECIAL_ROLE_CANNOT_BE_DELETED;
+    }
     if (execute(new RetrieveRole(this.name), getAccess()).getRole() == null) {
       throw ServerMessages.ROLE_DOES_NOT_EXIST;
     }
     execute(new SetPermissionRules(this.name, null), getAccess());
-    execute(new DeleteRole(this.name), getAccess());
+    try {
+      execute(new DeleteRole(this.name), getAccess());
+    } catch (TransactionException e) {
+      if (e.getCause() == ServerMessages.ROLE_CANNOT_BE_DELETED) {
+        throw (Message) e.getCause();
+      }
+    }
   }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/DeleteUserTransaction.java b/src/main/java/org/caosdb/server/transaction/DeleteUserTransaction.java
index 0f2f5b7fb676e436b131ca69678214c2e34b974c..24aac16b9c529130520518822e6bf3a6a3e8b72d 100644
--- a/src/main/java/org/caosdb/server/transaction/DeleteUserTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/DeleteUserTransaction.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,53 +19,61 @@
  * 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.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;
 import org.caosdb.server.database.backend.transaction.RetrievePasswordValidator;
 import org.caosdb.server.entity.Message;
-import org.caosdb.server.entity.Message.MessageType;
 import org.caosdb.server.utils.ServerMessages;
 import org.jdom2.Element;
 
 public class DeleteUserTransaction extends AccessControlTransaction {
 
-  private final String user;
+  private final String name;
   private final String realm;
 
-  public DeleteUserTransaction(final String user) {
-    this.realm = UserSources.getInternalRealm().getName();
-    this.user = user;
+  public DeleteUserTransaction(final String name) {
+    this(UserSources.getInternalRealm().getName(), name);
+  }
+
+  public DeleteUserTransaction(final String realm, final String name) {
+    this.realm = realm;
+    this.name = name;
   }
 
   @Override
   protected void transaction() throws Exception {
-    SecurityUtils.getSubject()
-        .checkPermission(ACMPermissions.PERMISSION_DELETE_USER(this.realm, this.user));
+    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.user), getAccess()).getValidator();
+        execute(new RetrievePasswordValidator(this.name), getAccess()).getValidator();
 
     if (validator == null) {
       throw ServerMessages.ACCOUNT_DOES_NOT_EXIST;
     }
 
-    execute(new DeletePassword(this.user), getAccess());
-    execute(new DeleteUser(this.realm, this.user), getAccess());
+    execute(new DeletePassword(this.name), getAccess());
+    execute(new DeleteUser(this.realm, this.name), getAccess());
   }
 
   public Element getUserElement() {
     final Element ret = new Element("User");
     ret.setAttribute("realm", this.realm);
-    ret.setAttribute("name", this.user);
-    ret.addContent(new Message(MessageType.Info, 0, "This user has been deleted.").toElement());
+    ret.setAttribute("name", this.name);
+    ret.addContent(new Message("This user has been deleted.").toElement());
     return ret;
   }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java b/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java
index ed93812d22eaad6bf448fa1dede2d92cea1d50a6..1ce1aa566ad28d048a36f91d50bb297b1a709d14 100644
--- a/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java
+++ b/src/main/java/org/caosdb/server/transaction/FileStorageConsistencyCheck.java
@@ -28,6 +28,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map.Entry;
 import java.util.TimeZone;
+import org.caosdb.api.entity.v1.MessageCode;
 import org.caosdb.datetime.UTCDateTime;
 import org.caosdb.server.database.DatabaseAccessManager;
 import org.caosdb.server.database.access.Access;
@@ -52,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;
@@ -59,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;
   }
 
@@ -176,7 +179,13 @@ public class FileStorageConsistencyCheck extends Thread
         sb.append('\n').append(t.toString());
       }
 
-      e.addContent(new Message("Error", 0, "An exception was thrown.", sb.toString()).toElement());
+      e.addContent(
+          new Message(
+                  "Error",
+                  MessageCode.MESSAGE_CODE_UNKNOWN,
+                  "An exception was thrown.",
+                  sb.toString())
+              .toElement());
     }
 
     final List<Message> results2Messages = results2Messages(getResults(), this.location);
@@ -196,24 +205,36 @@ public class FileStorageConsistencyCheck extends Thread
     final ArrayList<Message> ret = new ArrayList<Message>();
     if (results.isEmpty()) {
       if (location.length() > 0) {
-        ret.add(new Message("Info", 0, "File system below " + location + " is consistent."));
+        ret.add(new Message("File system below " + location + " is consistent."));
       } else {
-        ret.add(new Message("Info", 0, "File system is consistent."));
+        ret.add(new Message("File system is consistent."));
       }
     }
     for (final Entry<String, Integer> r : results.entrySet()) {
       switch (r.getValue()) {
         case FileConsistencyCheck.FILE_DOES_NOT_EXIST:
-          ret.add(new Message("Error", 0, r.getKey() + ": File does not exist."));
+          ret.add(
+              new Message(
+                  "Error",
+                  MessageCode.MESSAGE_CODE_UNKNOWN,
+                  r.getKey() + ": File does not exist."));
           break;
         case FileConsistencyCheck.FILE_MODIFIED:
-          ret.add(new Message("Error", 0, r.getKey() + ": File was modified."));
+          ret.add(
+              new Message(
+                  "Error", MessageCode.MESSAGE_CODE_UNKNOWN, r.getKey() + ": File was modified."));
           break;
         case FileConsistencyCheck.UNKNOWN_FILE:
-          ret.add(new Message("Warning", 0, r.getKey() + ": Unknown file."));
+          ret.add(
+              new Message(
+                  "Warning", MessageCode.MESSAGE_CODE_UNKNOWN, r.getKey() + ": Unknown file."));
           break;
         case FileConsistencyCheck.NONE:
-          ret.add(new Message("Warning", 0, r.getKey() + ": Test result not available."));
+          ret.add(
+              new Message(
+                  "Warning",
+                  MessageCode.MESSAGE_CODE_UNKNOWN,
+                  r.getKey() + ": Test result not available."));
           break;
         default:
           break;
@@ -226,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 c1128135b8620ebfe08328cd4901e9c0e6062053..950d9b8d25688e47833b60c01335f8dd525a0630 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 b70e2bd71259645a8f619cb25d36998579e3dca7..44288af5888c2dacbf9e97c235a76e0d9992a969 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/InsertUserTransaction.java b/src/main/java/org/caosdb/server/transaction/InsertUserTransaction.java
index b91b1aa6b31911822ed9df4c1eaffa87374ede82..09d59e8d84659037da66e11f7b4c0da556d8aa50 100644
--- a/src/main/java/org/caosdb/server/transaction/InsertUserTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/InsertUserTransaction.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,25 +19,27 @@
  * 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.transaction;
 
 import org.apache.shiro.SecurityUtils;
 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.RetrievePasswordValidator;
 import org.caosdb.server.database.backend.transaction.SetPassword;
 import org.caosdb.server.database.backend.transaction.UpdateUser;
+import org.caosdb.server.database.backend.transaction.UpdateUserRoles;
 import org.caosdb.server.database.proto.ProtoUser;
+import org.caosdb.server.entity.Message;
 import org.caosdb.server.utils.ServerMessages;
 import org.caosdb.server.utils.Utils;
 import org.jdom2.Element;
 
 public class InsertUserTransaction extends AccessControlTransaction {
 
-  ProtoUser user = new ProtoUser();
+  private final ProtoUser user;
   private final String password;
 
   public InsertUserTransaction(
@@ -45,11 +48,16 @@ public class InsertUserTransaction extends AccessControlTransaction {
       final String email,
       final UserStatus status,
       final Integer entity) {
+    this(new ProtoUser(), password);
     this.user.realm = UserSources.getInternalRealm().getName();
     this.user.name = username;
     this.user.email = email;
     this.user.status = status;
     this.user.entity = entity;
+  }
+
+  public InsertUserTransaction(ProtoUser user, String password) {
+    this.user = user;
     this.password = password;
   }
 
@@ -58,6 +66,8 @@ public class InsertUserTransaction extends AccessControlTransaction {
     SecurityUtils.getSubject()
         .checkPermission(ACMPermissions.PERMISSION_INSERT_USER(this.user.realm));
 
+    checkUserName(this.user.name);
+
     if (this.user.email != null && !Utils.isRFC822Compliant(this.user.email)) {
       throw ServerMessages.EMAIL_NOT_WELL_FORMED;
     }
@@ -66,15 +76,31 @@ public class InsertUserTransaction extends AccessControlTransaction {
       UpdateUserTransaction.checkEntityExists(this.user.entity);
     }
 
-    if (execute(new RetrievePasswordValidator(this.user.name), getAccess()).getValidator()
-        == null) {
-      if (this.password != null) {
-        Utils.checkPasswordStrength(this.password);
-      }
+    if (this.password != null) {
+      Utils.checkPasswordStrength(this.password);
+    }
+
+    execute(new SetPassword(this.user.name, this.password), getAccess());
+    execute(new UpdateUser(this.user), getAccess());
+    execute(new UpdateUserRoles(this.user.realm, this.user.name, this.user.roles), getAccess());
+  }
+
+  /*
+   * Names should have at least a length of 1, a maximum length of 32 and match
+   * ^[a-zA-Z_][a-zA-Z0-9_-]*$.
+   */
+  private void checkUserName(String name) throws Message {
+    // Make this configurable?
+    final boolean length = name.length() >= 1 && name.length() <= 32;
+    final boolean match =
+        name.matches("^[\\p{Lower}\\p{Upper}_][\\p{Lower}\\p{Upper}\\p{Digit}_-]*$");
+
+    if (!(length && match)) {
+      throw ServerMessages.INVALID_USER_NAME(
+          "User names must have a length from 1 to 32 characters, begin with a latin letter a-z (upper case or lower case) or an underscore (_), and all other characters must be latin letters, arabic numbers, hyphens (-) or undescores (_).");
+    }
 
-      execute(new SetPassword(this.user.name, this.password), getAccess());
-      execute(new UpdateUser(this.user), getAccess());
-    } else {
+    if (UserSources.isUserExisting(new Principal(this.user.realm, this.user.name))) {
       throw ServerMessages.ACCOUNT_NAME_NOT_UNIQUE;
     }
   }
diff --git a/src/main/java/org/caosdb/server/transaction/ListRolesTransaction.java b/src/main/java/org/caosdb/server/transaction/ListRolesTransaction.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed3cd3cc5a154e82946e76f9934ee8608fe6980c
--- /dev/null
+++ b/src/main/java/org/caosdb/server/transaction/ListRolesTransaction.java
@@ -0,0 +1,69 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.transaction;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+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.database.backend.transaction.ListRoles;
+import org.caosdb.server.database.proto.ProtoUser;
+
+public class ListRolesTransaction extends AccessControlTransaction {
+
+  private List<Role> roles = null;
+
+  @Override
+  protected void transaction() throws Exception {
+    Subject currentUser = SecurityUtils.getSubject();
+    roles =
+        execute(new ListRoles(), getAccess())
+            .getRoles()
+            .stream()
+            .filter(
+                role ->
+                    currentUser.isPermitted(
+                        ACMPermissions.PERMISSION_RETRIEVE_ROLE_DESCRIPTION(role.name)))
+            .collect(Collectors.toList());
+
+    // remove users. the list will only contain name and description.
+    for (Role role : roles) {
+      if (role.users != null) {
+        Iterator<ProtoUser> iterator = role.users.iterator();
+        while (iterator.hasNext()) {
+          ProtoUser user = iterator.next();
+          if (!currentUser.isPermitted(
+              ACMPermissions.PERMISSION_RETRIEVE_USER_ROLES(user.realm, user.name))) {
+            iterator.remove();
+          }
+        }
+      }
+    }
+  }
+
+  public List<Role> getRoles() {
+    return roles;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/transaction/ListUsersTransaction.java b/src/main/java/org/caosdb/server/transaction/ListUsersTransaction.java
new file mode 100644
index 0000000000000000000000000000000000000000..2ade4e8595f159d1fc6996c1e04913f4195ecd97
--- /dev/null
+++ b/src/main/java/org/caosdb/server/transaction/ListUsersTransaction.java
@@ -0,0 +1,63 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.transaction;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.subject.Subject;
+import org.caosdb.server.accessControl.ACMPermissions;
+import org.caosdb.server.database.backend.transaction.ListUsers;
+import org.caosdb.server.database.proto.ProtoUser;
+
+public class ListUsersTransaction extends AccessControlTransaction {
+
+  private List<ProtoUser> users = null;
+
+  @Override
+  protected void transaction() throws Exception {
+    Subject currentUser = SecurityUtils.getSubject();
+    users =
+        execute(new ListUsers(), getAccess())
+            .getUsers()
+            .stream()
+            .filter(
+                user ->
+                    currentUser.isPermitted(
+                        ACMPermissions.PERMISSION_RETRIEVE_USER_INFO(user.realm, user.name)))
+            .collect(Collectors.toList());
+
+    // remove roles
+    for (ProtoUser user : users) {
+      if (user.roles != null) {
+        if (!currentUser.isPermitted(
+            ACMPermissions.PERMISSION_RETRIEVE_USER_ROLES(user.realm, user.name))) {
+          user.roles = null;
+        }
+      }
+    }
+  }
+
+  public List<ProtoUser> getUsers() {
+    return users;
+  }
+}
diff --git a/src/main/java/org/caosdb/server/transaction/LogUserVisitTransaction.java b/src/main/java/org/caosdb/server/transaction/LogUserVisitTransaction.java
new file mode 100644
index 0000000000000000000000000000000000000000..dff0d34fed5bab445bfd761817aecc474def6025
--- /dev/null
+++ b/src/main/java/org/caosdb/server/transaction/LogUserVisitTransaction.java
@@ -0,0 +1,42 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.caosdb.server.transaction;
+
+import org.caosdb.server.database.backend.transaction.LogUserVisit;
+
+public class LogUserVisitTransaction extends AccessControlTransaction {
+
+  private String realm;
+  private String username;
+  private String type;
+  private long timestamp;
+
+  public LogUserVisitTransaction(long timestamp, String realm, String username, String type) {
+    this.timestamp = timestamp;
+    this.realm = realm;
+    this.username = username;
+    this.type = type;
+  }
+
+  @Override
+  protected void transaction() throws Exception {
+    execute(new LogUserVisit(timestamp, realm, username, type), getAccess());
+  }
+}
diff --git a/src/main/java/org/caosdb/server/transaction/Retrieve.java b/src/main/java/org/caosdb/server/transaction/Retrieve.java
index 250df0423869ae97a766da2fc54131e04d5257ca..86b7672cf9ebc0ad8e74f3974ee89174532d9f73 100644
--- a/src/main/java/org/caosdb/server/transaction/Retrieve.java
+++ b/src/main/java/org/caosdb/server/transaction/Retrieve.java
@@ -27,9 +27,7 @@ import org.caosdb.server.database.access.Access;
 import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.container.RetrieveContainer;
-import org.caosdb.server.entity.xml.SetFieldStrategy;
-import org.caosdb.server.entity.xml.ToElementStrategy;
-import org.caosdb.server.entity.xml.ToElementable;
+import org.caosdb.server.entity.xml.IdAndServerMessagesOnlyStrategy;
 import org.caosdb.server.jobs.ScheduledJob;
 import org.caosdb.server.jobs.core.JobFailureSeverity;
 import org.caosdb.server.jobs.core.RemoveDuplicates;
@@ -37,7 +35,6 @@ import org.caosdb.server.jobs.core.ResolveNames;
 import org.caosdb.server.permissions.EntityPermission;
 import org.caosdb.server.utils.EntityStatus;
 import org.caosdb.server.utils.ServerMessages;
-import org.jdom2.Element;
 
 public class Retrieve extends Transaction<RetrieveContainer> {
 
@@ -54,14 +51,14 @@ public class Retrieve extends Transaction<RetrieveContainer> {
     {
       final ResolveNames r = new ResolveNames();
       r.init(JobFailureSeverity.WARN, null, this);
-      ScheduledJob scheduledJob = getSchedule().add(r);
+      final ScheduledJob scheduledJob = getSchedule().add(r);
       getSchedule().runJob(scheduledJob);
     }
 
     {
       final RemoveDuplicates job = new RemoveDuplicates();
       job.init(JobFailureSeverity.ERROR, null, this);
-      ScheduledJob scheduledJob = getSchedule().add(job);
+      final ScheduledJob scheduledJob = getSchedule().add(job);
       getSchedule().runJob(scheduledJob);
     }
 
@@ -80,40 +77,15 @@ public class Retrieve extends Transaction<RetrieveContainer> {
 
   @Override
   protected void postTransaction() {
+    // @review Florian Spreckelsen 2022-03-22
+
     // generate Error for missing RETRIEVE:ENTITY Permission.
     for (final EntityInterface e : getContainer()) {
-      if (e.getEntityACL() != null) {
+      if (e.hasEntityACL()) {
         try {
           e.checkPermission(EntityPermission.RETRIEVE_ENTITY);
         } catch (final AuthorizationException exc) {
-          e.setToElementStragegy(
-              new ToElementStrategy() {
-
-                @Override
-                public Element toElement(
-                    final EntityInterface entity, final SetFieldStrategy setFieldStrategy) {
-                  Element ret;
-                  if (entity.hasRole()) {
-                    ret = new Element(entity.getRole().toString());
-                  } else {
-                    ret = new Element("Entity");
-                  }
-                  ret.setAttribute("id", entity.getId().toString());
-                  for (final ToElementable m : entity.getMessages()) {
-                    m.addToElement(ret);
-                  }
-                  return ret;
-                }
-
-                @Override
-                public Element addToElement(
-                    final EntityInterface entity,
-                    final Element parent,
-                    final SetFieldStrategy setFieldStrategy) {
-                  parent.addContent(toElement(entity, setFieldStrategy));
-                  return parent;
-                }
-              });
+          e.setSerializeFieldStrategy(new IdAndServerMessagesOnlyStrategy());
           e.setEntityStatus(EntityStatus.UNQUALIFIED);
           e.addError(ServerMessages.AUTHORIZATION_ERROR);
           e.addInfo(exc.getMessage());
diff --git a/src/main/java/org/caosdb/server/transaction/RetrieveACL.java b/src/main/java/org/caosdb/server/transaction/RetrieveACL.java
new file mode 100644
index 0000000000000000000000000000000000000000..cd9c5c0841557da579c3581f876a1180ecd06f8d
--- /dev/null
+++ b/src/main/java/org/caosdb/server/transaction/RetrieveACL.java
@@ -0,0 +1,89 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.transaction;
+
+import com.google.protobuf.ProtocolStringList;
+import java.util.UUID;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authz.AuthorizationException;
+import org.caosdb.server.database.backend.transaction.RetrieveEntityACLTransaction;
+import org.caosdb.server.entity.Entity;
+import org.caosdb.server.entity.EntityInterface;
+import org.caosdb.server.entity.container.TransactionContainer;
+import org.caosdb.server.permissions.EntityACL;
+import org.caosdb.server.permissions.EntityPermission;
+
+public class RetrieveACL extends Transaction<TransactionContainer> {
+
+  public RetrieveACL(ProtocolStringList idList) {
+    super(
+        new TransactionContainer(
+            SecurityUtils.getSubject(), System.currentTimeMillis(), UUID.randomUUID().toString()));
+    for (String strId : idList) {
+      getContainer().add(new Entity(Integer.parseInt(strId)));
+    }
+  }
+
+  @Override
+  public boolean logHistory() {
+    return false;
+  }
+
+  @Override
+  protected void init() throws Exception {
+    // acquire weak access
+    setAccess(getAccessManager().acquireReadAccess(this));
+  }
+
+  @Override
+  protected void preCheck() throws InterruptedException, Exception {}
+
+  @Override
+  protected void postCheck() {}
+
+  @Override
+  protected void preTransaction() throws InterruptedException {}
+
+  @Override
+  protected void transaction() throws Exception {
+    RetrieveEntityACLTransaction t = new RetrieveEntityACLTransaction(null);
+    for (EntityInterface e : getContainer()) {
+      EntityACL acl = execute(t.reuse(e.getId()), getAccess()).getEntityAcl();
+      if (acl != null && acl.isPermitted(getTransactor(), EntityPermission.RETRIEVE_ACL)) {
+        e.setEntityACL(acl);
+      } else if (acl != null
+          && acl.isPermitted(getTransactor(), EntityPermission.RETRIEVE_ENTITY)) {
+        throw new AuthorizationException("You are not permitted to update this entity's ACL.");
+      } else {
+        e.addError(org.caosdb.server.utils.ServerMessages.ENTITY_DOES_NOT_EXIST);
+      }
+    }
+  }
+
+  @Override
+  protected void postTransaction() throws Exception {}
+
+  @Override
+  protected void cleanUp() {
+    getAccess().release();
+  }
+}
diff --git a/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java b/src/main/java/org/caosdb/server/transaction/RetrieveLogRecordTransaction.java
index 7e6c527865566795c397377409b1fc63020ca1dd..2f6cafd6e4e55ab868f69f1d3414a2196f0a4fea 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/RetrieveRoleTransaction.java b/src/main/java/org/caosdb/server/transaction/RetrieveRoleTransaction.java
index c7c251ba9847fe29f84589297f846f436f2a1309..8e92a347f869b2a6a3e6f2be58d9124e6a9442d7 100644
--- a/src/main/java/org/caosdb/server/transaction/RetrieveRoleTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/RetrieveRoleTransaction.java
@@ -22,25 +22,50 @@
  */
 package org.caosdb.server.transaction;
 
+import java.util.Iterator;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.subject.Subject;
+import org.caosdb.server.accessControl.ACMPermissions;
 import org.caosdb.server.accessControl.Role;
 import org.caosdb.server.database.backend.transaction.RetrieveRole;
+import org.caosdb.server.database.proto.ProtoUser;
 import org.caosdb.server.utils.ServerMessages;
 
 public class RetrieveRoleTransaction extends AccessControlTransaction {
 
   private final String name;
   private Role role;
+  private Subject transactor;
 
-  public RetrieveRoleTransaction(final String name) {
+  public RetrieveRoleTransaction(final String name, Subject transactor) {
+    this.transactor = transactor;
     this.name = name;
   }
 
+  public RetrieveRoleTransaction(final String name) {
+    this(name, SecurityUtils.getSubject());
+  }
+
   @Override
   protected void transaction() throws Exception {
+    if (!transactor.isPermitted(ACMPermissions.PERMISSION_RETRIEVE_ROLE_DESCRIPTION(this.name))) {
+      throw new AuthorizationException("You are not permitted to retrieve this role");
+    }
     this.role = execute(new RetrieveRole(this.name), getAccess()).getRole();
     if (this.role == null) {
       throw ServerMessages.ROLE_DOES_NOT_EXIST;
     }
+    if (this.role.users != null) {
+      Iterator<ProtoUser> iterator = this.role.users.iterator();
+      while (iterator.hasNext()) {
+        ProtoUser user = iterator.next();
+        if (!transactor.isPermitted(
+            ACMPermissions.PERMISSION_RETRIEVE_USER_ROLES(user.realm, user.name))) {
+          iterator.remove();
+        }
+      }
+    }
   }
 
   public Role getRole() {
diff --git a/src/main/java/org/caosdb/server/transaction/RetrieveUserRolesTransaction.java b/src/main/java/org/caosdb/server/transaction/RetrieveUserRolesTransaction.java
index 97e518d06ac9ad5766b3801be8b1f1403cf8af3f..e26e29830a6e3c5f135b467f4622febda195da89 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/RetrieveUserTransaction.java b/src/main/java/org/caosdb/server/transaction/RetrieveUserTransaction.java
index 228b1585233ae71d3b7130196f68c3b9f83f5ed2..eb24fb24db4f1b6a50a279cfcd9e158003a70948 100644
--- a/src/main/java/org/caosdb/server/transaction/RetrieveUserTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/RetrieveUserTransaction.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,14 +19,16 @@
  * 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.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.Principal;
 import org.caosdb.server.accessControl.UserSources;
-import org.caosdb.server.accessControl.UserStatus;
 import org.caosdb.server.database.backend.transaction.RetrieveUser;
 import org.caosdb.server.database.proto.ProtoUser;
 import org.caosdb.server.utils.ServerMessages;
@@ -35,28 +38,32 @@ public class RetrieveUserTransaction extends AccessControlTransaction {
 
   private final Principal principal;
   private ProtoUser user;
+  private final Subject currentUser;
 
   public RetrieveUserTransaction(final String realm, final String name) {
-    this.principal = new Principal(realm, name);
+    this(realm, name, SecurityUtils.getSubject());
+  }
+
+  public RetrieveUserTransaction(String realm, String username, Subject transactor) {
+    currentUser = transactor;
+    this.principal = new Principal(realm, username);
   }
 
   @Override
   protected void transaction() throws Exception {
-    if (!UserSources.isUserExisting(this.principal)) {
+    if (!UserSources.isUserExisting(this.principal)
+        || !currentUser.isPermitted(
+            ACMPermissions.PERMISSION_RETRIEVE_USER_INFO(
+                this.principal.getRealm(), this.principal.getUsername()))) {
       throw ServerMessages.ACCOUNT_DOES_NOT_EXIST;
     }
     this.user = execute(new RetrieveUser(this.principal), getAccess()).getUser();
 
-    if (this.user == null) {
-      this.user = new ProtoUser();
-      this.user.name = this.principal.getUsername();
-      this.user.realm = this.principal.getRealm();
-    }
-    if (this.user.status == null) {
-      this.user.status = UserSources.getDefaultUserStatus(this.principal);
-    }
-    if (this.user.email == null) {
-      this.user.email = UserSources.getDefaultUserEmail(this.principal);
+    if (user != null && user.roles != null) {
+      if (!currentUser.isPermitted(
+          ACMPermissions.PERMISSION_RETRIEVE_USER_ROLES(user.realm, user.name))) {
+        user.roles = null;
+      }
     }
   }
 
@@ -86,7 +93,7 @@ public class RetrieveUserTransaction extends AccessControlTransaction {
     return this.user.roles;
   }
 
-  public boolean isActive() {
-    return this.user.status == UserStatus.ACTIVE;
+  public ProtoUser getUser() {
+    return user;
   }
 }
diff --git a/src/main/java/org/caosdb/server/transaction/Transaction.java b/src/main/java/org/caosdb/server/transaction/Transaction.java
index fe0b63f333e0151aed161ce3fa3914794e5f3d52..b4d63fbb293e0119cefe90db34b9cfb28f4e0ce1 100644
--- a/src/main/java/org/caosdb/server/transaction/Transaction.java
+++ b/src/main/java/org/caosdb/server/transaction/Transaction.java
@@ -33,7 +33,6 @@ import org.caosdb.server.database.exceptions.TransactionException;
 import org.caosdb.server.database.misc.TransactionBenchmark;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.Message;
-import org.caosdb.server.entity.Message.MessageType;
 import org.caosdb.server.entity.container.TransactionContainer;
 import org.caosdb.server.jobs.Job;
 import org.caosdb.server.jobs.Schedule;
@@ -52,9 +51,6 @@ import org.caosdb.server.utils.Observer;
 public abstract class Transaction<C extends TransactionContainer> extends AbstractObservable
     implements TransactionInterface {
 
-  public static final Message ERROR_INTEGRITY_VIOLATION =
-      new Message(MessageType.Error, 0, "This entity caused an unexpected integrity violation.");
-
   @Override
   public TransactionBenchmark getTransactionBenchmark() {
     return getContainer().getTransactionBenchmark();
@@ -91,7 +87,7 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra
    *
    * <p>E.g. in {@link Retrieve} and {@link WriteTransaction}.
    */
-  protected void makeSchedule() throws Exception {
+  protected void makeSchedule() {
     // load flag jobs
     final Job loadContainerFlags =
         Job.getJob("LoadContainerFlagJobs", JobFailureSeverity.ERROR, null, this);
diff --git a/src/main/java/org/caosdb/server/transaction/TransactionInterface.java b/src/main/java/org/caosdb/server/transaction/TransactionInterface.java
index d56407ca854c756a956fe11d9bc98a72f05a4b50..8e01c7d9b407927ebdb6a0528f7dcbc6d5bea40d 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
new file mode 100644
index 0000000000000000000000000000000000000000..84c73080550d8899f4f1a5156bebb2a44c39df6a
--- /dev/null
+++ b/src/main/java/org/caosdb/server/transaction/UpdateACL.java
@@ -0,0 +1,146 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+package org.caosdb.server.transaction;
+
+import static org.caosdb.server.query.Query.clearCache;
+
+import org.apache.shiro.authz.AuthorizationException;
+import org.caosdb.server.database.backend.transaction.RetrieveFullEntityTransaction;
+import org.caosdb.server.database.backend.transaction.UpdateEntityTransaction;
+import org.caosdb.server.entity.EntityInterface;
+import org.caosdb.server.entity.UpdateEntity;
+import org.caosdb.server.entity.container.TransactionContainer;
+import org.caosdb.server.jobs.Job;
+import org.caosdb.server.jobs.core.CheckEntityACLRoles;
+import org.caosdb.server.jobs.core.JobFailureSeverity;
+import org.caosdb.server.permissions.EntityACL;
+import org.caosdb.server.permissions.EntityPermission;
+import org.caosdb.server.utils.EntityStatus;
+
+public class UpdateACL extends Transaction<TransactionContainer>
+    implements WriteTransactionInterface {
+
+  public UpdateACL(TransactionContainer t) {
+    super(t);
+  }
+
+  @Override
+  public boolean logHistory() {
+    return false;
+  }
+
+  @Override
+  protected void init() throws Exception {
+    getSchedule()
+        .add(
+            Job.getJob(
+                CheckEntityACLRoles.class.getSimpleName(), JobFailureSeverity.ERROR, null, this));
+    // reserve write access. Other thread may read until the write access is
+    // actually acquired.
+    setAccess(getAccessManager().reserveWriteAccess(this));
+  }
+
+  @Override
+  protected void preCheck() throws InterruptedException, Exception {
+    TransactionContainer oldContainer = new TransactionContainer();
+
+    for (EntityInterface e : getContainer()) {
+      oldContainer.add(new UpdateEntity(e.getId(), null));
+    }
+
+    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 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)) {
+                    throw new AuthorizationException(
+                        "You are not permitted to change prioritized permission rules of this entity.");
+                  }
+
+                  // we're good to go. set new entity acl
+                  result.setEntityACL(newAcl);
+                  result.setEntityStatus(EntityStatus.QUALIFIED);
+                }
+              } else if (oldAcl != null
+                  && oldAcl.isPermitted(getTransactor(), EntityPermission.RETRIEVE_ENTITY)) {
+                // the user knows that this entity exists
+                throw new AuthorizationException(
+                    "You are not permitted to change permission rules of this entity.");
+              } else {
+                // we pretend this entity doesn't exist
+                result.addError(org.caosdb.server.utils.ServerMessages.ENTITY_DOES_NOT_EXIST);
+              }
+              return result;
+            });
+  }
+
+  @Override
+  protected void postCheck() {}
+
+  @Override
+  protected void preTransaction() throws InterruptedException {
+
+    setAccess(getAccessManager().acquireWriteAccess(this));
+  }
+
+  @Override
+  protected void transaction() throws Exception {
+    UpdateEntityTransaction t = new UpdateEntityTransaction(getContainer());
+    execute(t, getAccess());
+  }
+
+  @Override
+  protected void postTransaction() throws Exception {}
+
+  @Override
+  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 7e1632f8a0a9252c6e400d352bb63e133f6ae9e6..a62aff5bb6e90120e6cfd6651acde231cf3519b1 100644
--- a/src/main/java/org/caosdb/server/transaction/UpdateRoleTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/UpdateRoleTransaction.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,36 +19,62 @@
  * 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.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 {
 
-    if (!UserSources.isRoleExisting(this.role.name)) {
+    Subject subject = SecurityUtils.getSubject();
+    subject.checkPermission(ACMPermissions.PERMISSION_UPDATE_ROLE_DESCRIPTION(this.role.name));
+
+    Role oldRole = execute(new RetrieveRole(this.role.name), getAccess()).getRole();
+    if (oldRole == null) {
       throw ServerMessages.ROLE_DOES_NOT_EXIST;
     }
 
+    if (this.role.permission_rules != null) {
+      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 330ce5907f601b4b4345c26b1d483029f7d049b1..6e945118c9b0eef7460f41b5062ccb308b0fd956 100644
--- a/src/main/java/org/caosdb/server/transaction/UpdateUserTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/UpdateUserTransaction.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,21 +19,27 @@
  * 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.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;
+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;
@@ -40,10 +47,15 @@ import org.caosdb.server.utils.ServerMessages;
 import org.caosdb.server.utils.Utils;
 import org.jdom2.Element;
 
+/**
+ * This transaction also checks if the current user has sufficient permissions to make the update.
+ */
 public class UpdateUserTransaction extends AccessControlTransaction {
 
   private final String password;
-  private final ProtoUser user = new ProtoUser();
+  private final ProtoUser user;
+  private HashSet<String> newRoles;
+  private Set<String> oldRoles;
 
   public UpdateUserTransaction(
       final String realm,
@@ -52,6 +64,7 @@ public class UpdateUserTransaction extends AccessControlTransaction {
       final String email,
       final Integer entity,
       final String password) {
+    this.user = new ProtoUser();
     this.user.realm =
         (realm == null
             ? UserSources.guessRealm(username, UserSources.getInternalRealm().getName())
@@ -63,9 +76,19 @@ 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))) {
+  public UpdateUserTransaction(ProtoUser user, String password) {
+    this.user = user;
+    if (this.user.realm == null) {
+      this.user.realm =
+          UserSources.guessRealm(this.user.name, UserSources.getInternalRealm().getName());
+    }
+    this.password = password;
+  }
+
+  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;
     }
 
@@ -94,29 +117,51 @@ public class UpdateUserTransaction extends AccessControlTransaction {
       }
     }
 
+    if (this.user.roles != null) {
+      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.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);
+      }
+    }
   }
 
   private boolean isToBeUpdated() throws Exception {
+    Subject current_user = SecurityUtils.getSubject();
     boolean isToBeUpdated = false;
     final ProtoUser validUser =
         execute(new RetrieveUser(new Principal(this.user.realm, this.user.name)), getAccess())
             .getUser();
     if (this.user.status != null && (validUser == null || this.user.status != validUser.status)) {
-      SecurityUtils.getSubject()
-          .checkPermission(
-              ACMPermissions.PERMISSION_UPDATE_USER_STATUS(this.user.realm, this.user.name));
+      current_user.checkPermission(
+          ACMPermissions.PERMISSION_UPDATE_USER_STATUS(this.user.realm, this.user.name));
       isToBeUpdated = true;
     } else if (validUser != null) {
       this.user.status = validUser.status;
     }
     if (this.user.email != null
         && (validUser == null || !this.user.email.equals(validUser.email))) {
-      SecurityUtils.getSubject()
-          .checkPermission(
-              ACMPermissions.PERMISSION_UPDATE_USER_EMAIL(this.user.realm, this.user.name));
+      current_user.checkPermission(
+          ACMPermissions.PERMISSION_UPDATE_USER_EMAIL(this.user.realm, this.user.name));
       if (!Utils.isRFC822Compliant(this.user.email)) {
         throw ServerMessages.EMAIL_NOT_WELL_FORMED;
       }
@@ -127,9 +172,8 @@ public class UpdateUserTransaction extends AccessControlTransaction {
     }
     if (this.user.entity != null
         && (validUser == null || !this.user.entity.equals(validUser.entity))) {
-      SecurityUtils.getSubject()
-          .checkPermission(
-              ACMPermissions.PERMISSION_UPDATE_USER_ENTITY(this.user.realm, this.user.name));
+      current_user.checkPermission(
+          ACMPermissions.PERMISSION_UPDATE_USER_ENTITY(this.user.realm, this.user.name));
       isToBeUpdated = true;
 
       if (this.user.entity.equals(0)) {
diff --git a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java
index 62a9beea1921f34dd3646bcc78ed9086d05e6098..3497f912c33a69716a701f6f0d15af732d63870f 100644
--- a/src/main/java/org/caosdb/server/transaction/WriteTransaction.java
+++ b/src/main/java/org/caosdb/server/transaction/WriteTransaction.java
@@ -3,8 +3,9 @@
  *
  * Copyright (C) 2018 Research Group Biomedical Physics,
  * Max-Planck-Institute for Dynamics and Self-Organization Göttingen
- * Copyright (C) 2019-2021 IndiScale GmbH <info@indiscale.com>
- * Copyright (C) 2019-2021 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2019-2022 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2019-2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2022 Daniel Hornung <d.hornung@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
@@ -24,6 +25,7 @@ package org.caosdb.server.transaction;
 import java.io.IOException;
 import java.security.NoSuchAlgorithmException;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import org.apache.shiro.SecurityUtils;
@@ -40,6 +42,7 @@ import org.caosdb.server.entity.DeleteEntity;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.FileProperties;
 import org.caosdb.server.entity.InsertEntity;
+import org.caosdb.server.entity.Message;
 import org.caosdb.server.entity.RetrieveEntity;
 import org.caosdb.server.entity.UpdateEntity;
 import org.caosdb.server.entity.container.TransactionContainer;
@@ -67,10 +70,17 @@ import org.caosdb.server.utils.ServerMessages;
 public class WriteTransaction extends Transaction<WritableContainer>
     implements WriteTransactionInterface {
 
+  private boolean noIdIsError = true;
+
   public WriteTransaction(final WritableContainer container) {
     super(container);
   }
 
+  /** Set if it is an error, if an Entity has no ID but just a name upon update. */
+  public void setNoIdIsError(final boolean noIdIsError) {
+    this.noIdIsError = noIdIsError;
+  }
+
   @Override
   protected final void preTransaction() throws InterruptedException {
     // Acquire strong access. No other thread can have access until this strong access is released.
@@ -174,7 +184,7 @@ public class WriteTransaction extends Transaction<WritableContainer>
 
                 // get file by tmpIdentifier
                 final FileProperties f =
-                    getContainer().getFiles().get(entity.getFileProperties().getTmpIdentifyer());
+                    getContainer().getFiles().get(entity.getFileProperties().getTmpIdentifier());
 
                 // is it there?
                 if (f != null) {
@@ -187,7 +197,7 @@ public class WriteTransaction extends Transaction<WritableContainer>
                     final FileProperties thumbnail =
                         getContainer()
                             .getFiles()
-                            .get(entity.getFileProperties().getTmpIdentifyer() + ".thumbnail");
+                            .get(entity.getFileProperties().getTmpIdentifier() + ".thumbnail");
                     if (thumbnail != null) {
                       entity.getFileProperties().setThumbnail(thumbnail.getFile());
                     } else {
@@ -243,7 +253,7 @@ public class WriteTransaction extends Transaction<WritableContainer>
           && !entity.getFileProperties().isPickupable()) {
         // dereference files (file upload only)
         final FileProperties f =
-            getContainer().getFiles().get(entity.getFileProperties().getTmpIdentifyer());
+            getContainer().getFiles().get(entity.getFileProperties().getTmpIdentifier());
         if (f != null) {
           entity.getFileProperties().setFile(f.getFile());
           if (f.getThumbnail() != null) {
@@ -252,7 +262,7 @@ public class WriteTransaction extends Transaction<WritableContainer>
             final FileProperties thumbnail =
                 getContainer()
                     .getFiles()
-                    .get(entity.getFileProperties().getTmpIdentifyer() + ".thumbnail");
+                    .get(entity.getFileProperties().getTmpIdentifier() + ".thumbnail");
             if (thumbnail != null) {
               entity.getFileProperties().setThumbnail(thumbnail.getFile());
             } else {
@@ -288,7 +298,7 @@ public class WriteTransaction extends Transaction<WritableContainer>
         entity.setEntityStatus(EntityStatus.UNQUALIFIED);
         entity.addError(ServerMessages.AUTHORIZATION_ERROR);
         entity.addInfo(exc.getMessage());
-      } catch (ClassCastException exc) {
+      } catch (final ClassCastException exc) {
         // not an update entity. ignore.
       }
 
@@ -308,10 +318,10 @@ public class WriteTransaction extends Transaction<WritableContainer>
 
       // split up the TransactionContainer into three containers, one for each
       // type of writing transaction.
-      TransactionContainer inserts = new TransactionContainer();
-      TransactionContainer updates = new TransactionContainer();
-      TransactionContainer deletes = new TransactionContainer();
-      for (EntityInterface entity : getContainer()) {
+      final TransactionContainer inserts = new TransactionContainer();
+      final TransactionContainer updates = new TransactionContainer();
+      final TransactionContainer deletes = new TransactionContainer();
+      for (final EntityInterface entity : getContainer()) {
         if (entity instanceof InsertEntity) {
           inserts.add(entity);
         } else if (entity instanceof UpdateEntity) {
@@ -354,12 +364,14 @@ public class WriteTransaction extends Transaction<WritableContainer>
    * @throws IOException
    * @throws NoSuchAlgorithmException
    */
-  public static HashSet<Permission> deriveUpdate(
+  public HashSet<Permission> deriveUpdate(
       final EntityInterface newEntity, final EntityInterface oldEntity)
       throws NoSuchAlgorithmException, IOException, CaosDBException {
     final HashSet<Permission> needPermissions = new HashSet<>();
     boolean updatetable = false;
 
+    // @review Florian Spreckelsen 2022-03-15
+
     // new acl?
     if (newEntity.hasEntityACL() && !newEntity.getEntityACL().equals(oldEntity.getEntityACL())) {
       oldEntity.checkPermission(EntityPermission.EDIT_ACL);
@@ -368,7 +380,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()) {
@@ -389,10 +401,13 @@ public class WriteTransaction extends Transaction<WritableContainer>
         || newEntity.hasDatatype() ^ oldEntity.hasDatatype()) {
       needPermissions.add(EntityPermission.UPDATE_DATA_TYPE);
       updatetable = true;
+    } else {
+      newEntity.setDatatypeOverride(oldEntity.isDatatypeOverride());
     }
 
     // entity role
-    if (newEntity.hasRole()
+    if (!(newEntity instanceof Property && oldEntity instanceof Property)
+            && newEntity.hasRole()
             && oldEntity.hasRole()
             && !newEntity.getRole().equals(oldEntity.getRole())
         || newEntity.hasRole() ^ oldEntity.hasRole()) {
@@ -401,10 +416,18 @@ public class WriteTransaction extends Transaction<WritableContainer>
     }
 
     // entity value
-    if (newEntity.hasValue()
-            && oldEntity.hasValue()
-            && !newEntity.getValue().equals(oldEntity.getValue())
-        || newEntity.hasValue() ^ oldEntity.hasValue()) {
+    if (newEntity.hasValue() && oldEntity.hasValue()) {
+      try {
+        newEntity.parseValue();
+        oldEntity.parseValue();
+      } catch (NullPointerException | Message m) {
+        // ignore, parsing is handled elsewhere
+      }
+      if (!newEntity.getValue().equals(oldEntity.getValue())) {
+        needPermissions.add(EntityPermission.UPDATE_VALUE);
+        updatetable = true;
+      }
+    } else if (newEntity.hasValue() ^ oldEntity.hasValue()) {
       needPermissions.add(EntityPermission.UPDATE_VALUE);
       updatetable = true;
     }
@@ -416,6 +439,8 @@ public class WriteTransaction extends Transaction<WritableContainer>
         || newEntity.hasName() ^ oldEntity.hasName()) {
       needPermissions.add(EntityPermission.UPDATE_NAME);
       updatetable = true;
+    } else {
+      newEntity.setNameOverride(oldEntity.isNameOverride());
     }
 
     // entity description
@@ -425,6 +450,8 @@ public class WriteTransaction extends Transaction<WritableContainer>
         || newEntity.hasDescription() ^ oldEntity.hasDescription()) {
       needPermissions.add(EntityPermission.UPDATE_DESCRIPTION);
       updatetable = true;
+    } else {
+      newEntity.setDescOverride(oldEntity.isDescOverride());
     }
 
     // file properties
@@ -461,46 +488,34 @@ public class WriteTransaction extends Transaction<WritableContainer>
     }
 
     // properties
-    outerLoop:
-    for (final EntityInterface newProperty : newEntity.getProperties()) {
-
-      // find corresponding oldProperty for this new property and make a
-      // diff.
-      if (newProperty.hasId()) {
-        for (final EntityInterface oldProperty : oldEntity.getProperties()) {
-          if (newProperty.getId().equals(oldProperty.getId())) {
-            // do not check again.
-            oldEntity.getProperties().remove(oldProperty);
-
-            if (((Property) oldProperty).getPIdx() != ((Property) newProperty).getPIdx()) {
-              // change order of properties
-              needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY);
-              needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY);
-              updatetable = true;
-            }
-
-            deriveUpdate(newProperty, oldProperty);
-            if (newProperty.getEntityStatus() == EntityStatus.QUALIFIED) {
-              needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY);
-              needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY);
-              updatetable = true;
-            }
+    for (final Property newProperty : newEntity.getProperties()) {
+
+      // find corresponding oldProperty for this new property and make a diff (existing property,
+      // same property index in this entity, equal content?).
+      final Property oldProperty = findOldEntity(newProperty, oldEntity.getProperties());
+      if (oldProperty != null) {
+        // do not check again.
+        oldEntity.getProperties().remove(oldProperty);
+
+        if (oldProperty.getPIdx() != newProperty.getPIdx()) {
+          // change order of properties
+          needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY);
+          needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY);
+          updatetable = true;
+        }
 
-            continue outerLoop;
-          }
+        deriveUpdate(newProperty, oldProperty);
+        if (newProperty.getEntityStatus() == EntityStatus.QUALIFIED) {
+          needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY);
+          needPermissions.add(EntityPermission.UPDATE_REMOVE_PROPERTY);
+          updatetable = true;
         }
+
       } else {
-        newProperty.setEntityStatus(EntityStatus.UNQUALIFIED);
-        newProperty.addError(ServerMessages.ENTITY_HAS_NO_ID);
-        newProperty.addInfo("On updates, allways specify the id not just the name.");
-        newEntity.addError(ServerMessages.ENTITY_HAS_UNQUALIFIED_PROPERTIES);
-        newEntity.setEntityStatus(EntityStatus.UNQUALIFIED);
-        return needPermissions;
+        // no corresponding property found -> this property is new.
+        needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY);
+        updatetable = true;
       }
-
-      // no corresponding property found -> this property is new.
-      needPermissions.add(EntityPermission.UPDATE_ADD_PROPERTY);
-      updatetable = true;
     }
 
     // some old properties left (and not matched with new ones) -> there are
@@ -511,30 +526,20 @@ public class WriteTransaction extends Transaction<WritableContainer>
     }
 
     // update parents
-    outerLoop:
     for (final Parent newParent : newEntity.getParents()) {
 
       // find corresponding oldParent
-      if (newParent.hasId()) {
-        for (final Parent oldParent : oldEntity.getParents()) {
-          if (oldParent.getId().equals(newParent.getId())) {
-            // still there! do not check this one again
-            oldEntity.getParents().remove(oldParent);
-            continue outerLoop;
-          }
-        }
+      final Parent oldProperty = findOldEntity(newParent, oldEntity.getParents());
+
+      if (oldProperty != null) {
+        // do not check again.
+        oldEntity.getParents().remove(oldProperty);
+
       } else {
-        newParent.setEntityStatus(EntityStatus.UNQUALIFIED);
-        newParent.addError(ServerMessages.ENTITY_HAS_NO_ID);
-        newParent.addInfo("On updates, allways specify the id not just the name.");
-        newEntity.addError(ServerMessages.ENTITY_HAS_UNQUALIFIED_PROPERTIES);
-        newEntity.setEntityStatus(EntityStatus.UNQUALIFIED);
-        return needPermissions;
+        // no corresponding parent found -> this parent is new.
+        needPermissions.add(EntityPermission.UPDATE_ADD_PARENT);
+        updatetable = true;
       }
-
-      // no corresponding parent found -> this parent is new.
-      needPermissions.add(EntityPermission.UPDATE_ADD_PARENT);
-      updatetable = true;
     }
 
     // some old parents left (and not matched with new ones) -> there are
@@ -553,6 +558,36 @@ public class WriteTransaction extends Transaction<WritableContainer>
     return needPermissions;
   }
 
+  /**
+   * Attempt to find a (sparse) entity among a list of entities.
+   *
+   * <p>If no match by ID can be found, matching by name is attempted next, but only if noIdIsError
+   * is false.
+   */
+  private <T extends EntityInterface> T findOldEntity(
+      final EntityInterface newEntity, final List<T> oldEntities) {
+    if (newEntity.hasId()) {
+      for (final T oldEntity : oldEntities) {
+        if (Objects.equals(oldEntity.getId(), newEntity.getId())) {
+          return oldEntity;
+        }
+      }
+    } else if (noIdIsError) {
+      newEntity.addError(ServerMessages.ENTITY_HAS_NO_ID);
+      newEntity.addInfo("On updates, always specify the id not just the name.");
+    } else if (newEntity.hasName()) {
+      for (final T oldEntity : oldEntities) {
+        if (oldEntity.getName().equals(newEntity.getName())) {
+          return oldEntity;
+        }
+      }
+    } else {
+      newEntity.addError(ServerMessages.ENTITY_HAS_NO_NAME_OR_ID);
+    }
+    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 165acb408776a720c6887d31b550cf57e3d6fa0c..4e2938e18d061cbd1a7575baa59322356731859a 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 18ee09828006c0394dec315ca77483f8298ba86b..644239fc662c1b6a05dc1aac37af1bfe26e7972e 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 cb1a36307928a72b9da95b48737196e569c43346..4f28f206bd5f272887fb78b661c6dc46d5cff6bc 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 2801218aa47a18ec1494ee8ef76607eb9b962130..151f55face7edbfab793606474f183a65f0468be 100644
--- a/src/main/java/org/caosdb/server/utils/ServerMessages.java
+++ b/src/main/java/org/caosdb/server/utils/ServerMessages.java
@@ -1,24 +1,29 @@
 /*
- * ** header v3.0 This file is a part of the CaosDB Project.
+ * 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) 2018 Research Group Biomedical Physics, Max-Planck-Institute
+ * for Dynamics and Self-Organization Göttingen
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
- * This program is free software: you can redistribute it and/or modify it under the terms of the
- * GNU Affero General Public License as published by the Free Software Foundation, either version 3
- * of the License, or (at your option) any later version.
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
  *
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
- * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Affero General Public License for more details.
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
  *
- * You should have received a copy of the GNU Affero General Public License along with this program.
- * If not, see <https://www.gnu.org/licenses/>.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
  *
- * ** end header
  */
+
 package org.caosdb.server.utils;
 
+import org.caosdb.api.entity.v1.MessageCode;
 import org.caosdb.server.CaosDBServer;
 import org.caosdb.server.ServerProperties;
 import org.caosdb.server.entity.Message;
@@ -27,325 +32,438 @@ import org.caosdb.server.entity.Message.MessageType;
 public class ServerMessages {
 
   public static final Message ENTITY_HAS_BEEN_DELETED_SUCCESSFULLY =
-      new Message(MessageType.Info, 10, "This entity has been deleted successfully.");
+      new Message(
+          MessageType.Info,
+          MessageCode.MESSAGE_CODE_ENTITY_HAS_BEEN_DELETED_SUCCESSFULLY,
+          "This entity has been deleted successfully.");
 
   public static final Message ENTITY_DOES_NOT_EXIST =
-      new Message(MessageType.Error, 101, "Entity does not exist.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_ENTITY_DOES_NOT_EXIST,
+          "Entity does not exist.");
 
   public static final Message ENTITY_HAS_UNQUALIFIED_PROPERTIES =
-      new Message(MessageType.Error, 114, "Entity has unqualified properties.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_ENTITY_HAS_UNQUALIFIED_PROPERTIES,
+          "Entity has unqualified properties.");
 
   public static final Message ENTITY_HAS_UNQUALIFIED_PARENTS =
-      new Message(MessageType.Error, 116, "Entity has unqualified parents.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_ENTITY_HAS_UNQUALIFIED_PARENTS,
+          "Entity has unqualified parents.");
 
-  public static final Message PARSING_FAILED = new Message(MessageType.Error, 2, "Parsing failed.");
+  public static final Message PARSING_FAILED =
+      new Message(MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "Parsing failed.");
 
   public static final Message UNKNOWN_DATATYPE =
-      new Message(MessageType.Error, 0, "Unknown datatype.");
+      new Message(MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "Unknown data type.");
 
   public static final Message UNKNOWN_IMPORTANCE =
-      new Message(MessageType.Error, 0, "Unknown importance.");
+      new Message(MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "Unknown importance.");
 
   public static final Message ENTITY_HAS_NO_ID =
-      new Message(MessageType.Error, 0, "Entity has no ID.");
+      new Message(
+          MessageType.Error, MessageCode.MESSAGE_CODE_ENTITY_HAS_NO_ID, "Entity has no ID.");
 
   public static final Message REQUIRED_BY_PERSISTENT_ENTITY =
       new Message(
           MessageType.Error,
-          0,
+          MessageCode.MESSAGE_CODE_REQUIRED_BY_PERSISTENT_ENTITY,
           "Entity is required by other entities which are not to be deleted.");
 
-  public static final Message NO_DATATYPE =
-      new Message(MessageType.Error, 110, "Property has no datatype.");
+  public static final Message PROPERTY_HAS_NO_DATATYPE =
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_PROPERTY_HAS_NO_DATA_TYPE,
+          "Property has no data type.");
 
   public static final Message ENTITY_HAS_NO_DESCRIPTION =
-      new Message(MessageType.Error, 0, "Entity has no description.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_ENTITY_HAS_NO_DESCRIPTION,
+          "Entity has no description.");
 
   public static final Message ENTITY_HAS_NO_NAME =
-      new Message(MessageType.Error, 0, "Entity has no name.");
+      new Message(
+          MessageType.Error, MessageCode.MESSAGE_CODE_ENTITY_HAS_NO_NAME, "Entity has no name.");
 
   public static final Message OBLIGATORY_PROPERTY_MISSING =
-      new Message(MessageType.Error, 0, "An obligatory property is missing.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_OBLIGATORY_PROPERTY_MISSING,
+          "An obligatory property is missing.");
 
   public static final Message ENTITY_HAS_NO_PARENTS =
-      new Message(MessageType.Error, 0, "Entity has no parents.");
-
-  public static final Message NAME_DUPLICATES =
-      new Message(MessageType.Error, 0, "Entity can not be identified due to name duplicates.");
-
-  public static final Message ENTITY_HAS_NO_NAME_AND_NO_ID =
-      new Message(MessageType.Error, 0, "Entity has no name and no ID.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_ENTITY_HAS_NO_PARENTS,
+          "Entity has no parents.");
 
   public static final Message ENTITY_HAS_NO_PROPERTIES =
-      new Message(MessageType.Error, 0, "Entity has no properties.");
-
-  public static final Message REFERENCE_HAS_NO_REFID =
-      new Message(MessageType.Error, 0, "Reference property has no refid.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_ENTITY_HAS_NO_PROPERTIES,
+          "Entity has no properties.");
 
-  public static final Message NO_TARGET_PATH =
-      new Message(MessageType.Error, 0, "No target path specified.");
+  public static final Message FILE_HAS_NO_TARGET_PATH =
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_FILE_HAS_NO_TARGET_PATH,
+          "No target path specified.");
 
   public static final Message TARGET_PATH_NOT_ALLOWED =
-      new Message(MessageType.Error, 0, "This target path is not allowed.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_TARGET_PATH_NOT_ALLOWED,
+          "This target path is not allowed.");
 
   public static final Message TARGET_PATH_EXISTS =
-      new Message(MessageType.Error, 0, "This target path does already exist.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_TARGET_PATH_EXISTS,
+          "This target path does already exist.");
 
-  public static final Message ENTITY_HAS_NO_UNIT =
-      new Message(MessageType.Error, 0, "Entity has no unit.");
+  public static final Message PROPERTY_HAS_NO_UNIT =
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_PROPERTY_HAS_NO_UNIT,
+          "Property has no unit.");
 
   public static final Message CANNOT_PARSE_VALUE =
-      new Message(MessageType.Error, 0, "Cannot parse value.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_CANNOT_PARSE_VALUE,
+          "Cannot parse the value.");
 
   public static final Message CHECKSUM_TEST_FAILED =
-      new Message(MessageType.Error, 0, "Checksum test failed. File is corrupted.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_CHECKSUM_TEST_FAILED,
+          "Checksum test failed. File is corrupted.");
 
   public static final Message SIZE_TEST_FAILED =
-      new Message(MessageType.Error, 0, "Size test failed. File is corrupted.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_SIZE_TEST_FAILED,
+          "Size test failed. File is corrupted.");
 
   public static final Message CANNOT_CREATE_PARENT_FOLDER =
-      new Message(MessageType.Error, 0, "Could not create parent folder in the file system.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_CANNOT_CREATE_PARENT_FOLDER,
+          "Could not create parent folder in the file system.");
 
   public static final Message FILE_HAS_NOT_BEEN_UPLOAED =
-      new Message(MessageType.Error, 0, "File has not been uploaded.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_FILE_HAS_NOT_BEEN_UPLOAED,
+          "File has not been uploaded.");
 
   public static final Message THUMBNAIL_HAS_NOT_BEEN_UPLOAED =
-      new Message(MessageType.Error, 0, "Thumbnail has not been uploaded.");
+      new Message(
+          MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "Thumbnail has not been uploaded.");
 
   public static final Message CANNOT_MOVE_FILE_TO_TARGET_PATH =
-      new Message(MessageType.Error, 0, "Could not move file to it's target folder.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_CANNOT_MOVE_FILE_TO_TARGET_PATH,
+          "Could not move file to its target folder.");
 
   public static final Message CANNOT_PARSE_DATETIME_VALUE =
       new Message(
           MessageType.Error,
-          0,
+          MessageCode.MESSAGE_CODE_CANNOT_PARSE_DATETIME_VALUE,
           "Cannot parse value to datetime format (yyyy-mm-dd'T'hh:mm:ss[.fffffffff][TimeZone]).");
 
   public static final Message CANNOT_PARSE_DOUBLE_VALUE =
-      new Message(MessageType.Error, 0, "Cannot parse value to double.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_CANNOT_PARSE_DOUBLE_VALUE,
+          "Cannot parse value to double.");
 
   public static final Message CANNOT_PARSE_INT_VALUE =
-      new Message(MessageType.Error, 0, "Cannot parse value to integer.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_CANNOT_PARSE_INT_VALUE,
+          "Cannot parse value to integer.");
 
   public static final Message CANNOT_PARSE_BOOL_VALUE =
       new Message(
           MessageType.Error,
-          0,
-          "Cannot parse value to boolean (either 'true' or 'false', ignoring case).");
+          MessageCode.MESSAGE_CODE_CANNOT_PARSE_BOOL_VALUE,
+          "Cannot parse value to boolean (either 'true' or 'false', case insensitive).");
 
   public static final Message CANNOT_CONNECT_TO_DATABASE =
-      new Message(MessageType.Error, 0, "Could not connect to MySQL server.");
-
-  public static final Message REQUEST_BODY_NOT_WELLFORMED =
-      new Message(MessageType.Error, 0, "Request Body was not a well-formed xml.");
-
-  public static final Message REQUEST_BODY_EMPTY =
-      new Message(MessageType.Error, 0, "Request body was empty.");
-
-  public static final Message MYSQL_PROCEDURE_EXCEPTION =
       new Message(
           MessageType.Error,
-          0,
-          "Please check if your MySQL has all required procedures installed.");
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "Could not connect to MySQL server.");
 
-  public static final Message REQUEST_HAS_WRONG_ENCODING =
+  public static final Message REQUEST_BODY_NOT_WELLFORMED =
       new Message(
           MessageType.Error,
-          0,
-          "This error occurred while parsing the xml. Probably it has a wrong encoding.");
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "Request body was not a well-formed xml.");
+
+  public static final Message REQUEST_BODY_EMPTY =
+      new Message(MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "Request body was empty.");
 
   public static final Message REQUEST_DOESNT_CONTAIN_EXPECTED_ELEMENTS =
-      new Message(MessageType.Error, 0, "Request body didn't contain the expected Elements.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "Request body didn't contain the expected elements.");
 
   public static final Message FILE_NOT_IN_DROPOFFBOX =
-      new Message(MessageType.Error, 0, "File is not in drop-off box.");
+      new Message(
+          MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "File is not in drop-off box.");
 
   public static final Message FILE_NOT_FOUND =
-      new Message(MessageType.Error, 0, "File could not be found.");
+      new Message(
+          MessageType.Error, MessageCode.MESSAGE_CODE_FILE_NOT_FOUND, "File could not be found.");
 
   public static final Message CANNOT_MOVE_FILE_TO_TMP =
-      new Message(MessageType.Error, 0, "Could not move file to tmp folder.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "Could not move file to tmp folder.");
 
   public static final Message CANNOT_READ_FILE =
       new Message(
           MessageType.Error,
-          0,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
           "Insufficient read permission for this file. Please make it readable.");
 
   public static final Message CANNOT_READ_THUMBNAIL =
       new Message(
           MessageType.Error,
-          0,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
           "Insufficient read permission for this file's thumbnail. Please make it readable.");
 
   public static final Message NO_FILE_REPRESENTATION_SUBMITTED =
-      new Message(MessageType.Error, 0, "No file representation submitted.");
+      new Message(
+          MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "No file representation submitted.");
 
   public static final Message FORM_CONTAINS_UNSUPPORTED_CONTENT =
-      new Message(MessageType.Error, 0, "The form contains unsupported content");
-
-  public static final Message FILE_IS_EMPTY = new Message(MessageType.Error, 0, "File is empty.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "The form contains unsupported content");
 
   public static final Message WARNING_OCCURED =
       new Message(
           MessageType.Error,
-          128,
+          MessageCode.MESSAGE_CODE_WARNING_OCCURED,
           "A warning occured while processing an entity with the strict flag.");
 
-  public static final Message UNKNOWN_JOB = new Message(MessageType.Warning, 0, "Unknown job.");
-
-  public static final Message NAME_IS_NOT_UNIQUE =
+  public static final Message ENTITY_NAME_IS_NOT_UNIQUE =
       new Message(
           MessageType.Error,
-          152,
+          MessageCode.MESSAGE_CODE_ENTITY_NAME_IS_NOT_UNIQUE,
           "Name is already in use. Choose a different name or reuse an existing entity.");
 
   public static final Message ROLE_NAME_IS_NOT_UNIQUE =
-      new Message(MessageType.Error, 1152, "Role name is already in use. Choose a different name.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "Role name is already in use. Choose a different name.");
+
+  public static final Message ROLE_CANNOT_BE_DELETED =
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "Role cannot be deleted because there are still users with this role.");
 
-  public static final Message CANNOT_IDENTIFY_ENTITY_UNIQUELY =
+  public static final Message SPECIAL_ROLE_CANNOT_BE_DELETED =
       new Message(
-          MessageType.Error, 0, "This entity cannot be identified uniquely due to name dublicates");
+          MessageType.Error,
+          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, 13, "This query finished with errors.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_QUERY_EXCEPTION,
+          "This query finished with errors.");
 
   public static final Message LOGOUT_INFO =
-      new Message(MessageType.Info, 201, "You have successfully logged out.");
+      new Message(
+          MessageType.Info, MessageCode.MESSAGE_CODE_UNKNOWN, "You have successfully logged out.");
 
   public static final Message ENTITY_IS_EMPTY =
-      new Message(MessageType.Error, 0, "This entity is empty.");
+      new Message(MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "This entity is empty.");
 
   public static final Message TRANSACTION_ROLL_BACK =
       new Message(
           MessageType.Error,
-          0,
+          MessageCode.MESSAGE_CODE_TRANSACTION_ROLL_BACK,
           "An unknown error occured during the transaction and it was rolled back.");
 
   public static final Message FILE_UPLOAD_FAILED =
-      new Message(MessageType.Error, 0, "The file upload failed for an unknown reason.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "The file upload failed for an unknown reason.");
 
   public static final Message ACCOUNT_CANNOT_BE_ACTIVATED =
-      new Message(MessageType.Error, 0, "This account cannot be activated.");
+      new Message(
+          MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "This account cannot be activated.");
 
   public static final Message ACCOUNT_DOES_NOT_EXIST =
-      new Message(MessageType.Error, 0, "This account does not exist.");
+      new Message(
+          MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "This account does not exist.");
 
   public static final Message ACCOUNT_CANNOT_BE_RESET =
-      new Message(MessageType.Error, 0, "This account cannot be reset.");
+      new Message(
+          MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "This account cannot be reset.");
 
   public static final Message UNKNOWN_UNIT =
       new Message(
           MessageType.Error,
-          0,
+          MessageCode.MESSAGE_CODE_UNKNOWN_UNIT,
           "Unknown unit. Values with this unit cannot be converted to other units when used in search queries.");
 
   public static final Message ACCOUNT_NAME_NOT_UNIQUE =
-      new Message(MessageType.Error, 0, "This user name is yet in use. Please choose another one.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "This user name is yet in use. Please choose another one.");
 
   public static final Message ACCOUNT_HAS_BEEN_DELETED =
-      new Message(MessageType.Info, 10, "This user has been deleted successfully.");
+      new Message(
+          MessageType.Info,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "This user has been deleted successfully.");
 
   public static final Message PLEASE_SET_A_PASSWORD =
-      new Message(MessageType.Info, 0, "Please set a password.");
+      new Message(MessageType.Info, MessageCode.MESSAGE_CODE_UNKNOWN, "Please set a password.");
 
   public static final Message USER_HAS_BEEN_ACTIVATED =
       new Message(
           MessageType.Info,
-          0,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
           "User has been activated. You can now log in with your username and password.");
 
   public static final Message UNAUTHENTICATED =
-      new Message(MessageType.Error, 401, "Sign in, please.");
+      new Message(MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "Sign in, please.");
 
   public static final Message AUTHORIZATION_ERROR =
-      new Message(MessageType.Error, 403, "You are not allowed to do this.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_AUTHORIZATION_ERROR,
+          "You are not allowed to do this.");
 
   public static final Message REFERENCE_IS_NOT_ALLOWED_BY_DATATYPE =
       new Message(
           MessageType.Error,
-          0,
+          MessageCode.MESSAGE_CODE_REFERENCE_IS_NOT_ALLOWED_BY_DATA_TYPE,
           "Reference not qualified. The value of this Reference Property is to be a child of its data type.");
 
   public static final Message CANNOT_PARSE_ENTITY_ACL =
-      new Message(MessageType.Error, 0, "Cannot parse EntityACL.");
+      new Message(MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "Cannot parse EntityACL.");
 
   public static final Message ROLE_DOES_NOT_EXIST =
-      new Message(MessageType.Error, 1104, "User Role does not exist.");
+      new Message(MessageType.Error, MessageCode.MESSAGE_CODE_UNKNOWN, "User role does not exist.");
 
   public static final Message ENTITY_NAME_DUPLICATES =
-      new Message(MessageType.Error, 0, "This entity cannot be identified due to name duplicates.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_ENTITY_NAME_DUPLICATES,
+          "Entity can not be identified due to name duplicates.");
 
   public static final Message DATA_TYPE_NAME_DUPLICATES =
       new Message(
-          MessageType.Error, 0, "This data type cannot be identified due to name duplicates.");
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_DATA_TYPE_NAME_DUPLICATES,
+          "This data type cannot be identified due to name duplicates.");
 
   public static final Message ENTITY_HAS_NO_NAME_OR_ID =
       new Message(
           MessageType.Error,
-          0,
+          MessageCode.MESSAGE_CODE_ENTITY_HAS_NO_NAME_OR_ID,
           "This entity cannot be identified as it didn't come with a name or id.");
 
   public static final Message EMAIL_NOT_WELL_FORMED =
-      new Message(MessageType.Error, 0, "This email address is not RFC822 compliant.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "This email address is not RFC822 compliant.");
 
   public static final Message PASSWORD_TOO_WEAK =
       new Message(
           MessageType.Error,
-          0,
-          "This password is too weak. It should be longer than 8 characters and sufficiently random. ");
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "This password is too weak. It should be longer than 8 characters and sufficiently random.");
 
   public static final Message AFFILIATION_ERROR =
       new Message(
-          MessageType.Error, 0, "Affiliation is not defined for this child-parent constellation.");
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_AFFILIATION_ERROR,
+          "Affiliation is not defined for this child-parent constellation.");
 
   public static final Message QUERY_TEMPLATE_HAS_NO_QUERY_DEFINITION =
-      new Message(MessageType.Error, 0, "This QueryTemplate has no query definition.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "This QueryTemplate has no query definition.");
 
   public static final Message QUERY_TEMPLATE_WITH_COUNT =
       new Message(
           MessageType.Error,
-          0,
-          "QueryTemplates may not be defined by 'COUNT...' queries for consistency reasons.");
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "QueryTemplates may not be defined by 'COUNT' queries for consistency reasons.");
 
   public static final Message QUERY_TEMPLATE_WITH_SELECT =
       new Message(
           MessageType.Error,
-          0,
-          "QueryTemplates may not be defined by 'SELECT... FROM...' queries for consistency reasons.");
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "QueryTemplates may not be defined by 'SELECT ... FROM ...' queries for consistency reasons.");
 
   public static final Message QUERY_PARSING_ERROR =
       new Message(
           MessageType.Error,
-          0,
-          "An error occured during the parsing of this query. Maybe you use a wrong syntax?");
+          MessageCode.MESSAGE_CODE_QUERY_PARSING_ERROR,
+          "An error occured during the parsing of this query. Maybe you were using a wrong syntax?");
 
   public static final Message NAME_PROPERTIES_MUST_BE_TEXT =
       new Message(
           MessageType.Error,
-          0,
+          MessageCode.MESSAGE_CODE_NAME_PROPERTIES_MUST_BE_TEXT,
           "A property which has 'name' as its parent must have a TEXT data type.");
 
   public static final Message PARENT_DUPLICATES_WARNING =
       new Message(
           MessageType.Warning,
-          0,
-          "This entity had parent duplicates. That is meaningless and only one parent had been inserted.");
+          MessageCode.MESSAGE_CODE_PARENT_DUPLICATES_WARNING,
+          "This entity had parent duplicates. That is meaningless and only one parent has been inserted.");
 
   public static final Message PARENT_DUPLICATES_ERROR =
       new Message(
           MessageType.Error,
-          0,
+          MessageCode.MESSAGE_CODE_PARENT_DUPLICATES_ERROR,
           "This entity had parent duplicates. Parent duplicates are meaningless and would be ignored (and inserted only once). But these parents had diverging inheritance instructions which cannot be processed.");
 
   public static final Message ATOMICITY_ERROR =
       new Message(
           MessageType.Error,
-          12,
+          MessageCode.MESSAGE_CODE_ATOMICITY_ERROR,
           "One or more entities are not qualified. None of them have been inserted/updated/deleted.");
 
   public static final Message NO_SUCH_ENTITY_ROLE(final String role) {
-    return new Message(MessageType.Error, 0, "There is no such role '" + role + "'.");
+    return new Message(
+        MessageType.Error,
+        MessageCode.MESSAGE_CODE_NO_SUCH_ENTITY_ROLE,
+        "There is no such role '" + role + "'.");
   }
 
   public static Message UNKNOWN_ERROR(final String requestID) {
@@ -366,72 +484,136 @@ public class ServerMessages {
               + " and include the SRID into your report.";
     }
 
-    return new Message(MessageType.Error, 500, description, (body.isEmpty() ? null : body));
+    return new Message(
+        MessageType.Error,
+        MessageCode.MESSAGE_CODE_UNKNOWN,
+        description,
+        body.isEmpty() ? null : body);
   }
 
   public static final Message REQUIRED_BY_UNQUALIFIED =
       new Message(
-          MessageType.Error, 192, "This entity cannot be deleted due to dependency problems");
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_REQUIRED_BY_UNQUALIFIED,
+          "This entity cannot be deleted due to dependency problems");
 
-  public static final Message ENTITY_HAS_INVALID_REFERENCE =
-      new Message(MessageType.Error, 0, "This entity has an invalid reference.");
+  public static final Message ENTITY_HAS_UNQUALIFIED_REFERENCE =
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_ENTITY_HAS_UNQUALIFIED_REFERENCE,
+          "This entity has an unqualified reference.");
 
   public static final Message REFERENCED_ENTITY_DOES_NOT_EXIST =
-      new Message(MessageType.Error, 235, "Referenced entity does not exist.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_REFERENCED_ENTITY_DOES_NOT_EXIST,
+          "Referenced entity does not exist.");
 
   public static final Message REFERENCE_NAME_DUPLICATES =
       new Message(
-          MessageType.Error, 0, "This reference cannot be identified due to name duplicates.");
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_REFERENCE_NAME_DUPLICATES,
+          "This reference cannot be identified due to name duplicates.");
 
   public static final Message DATATYPE_INHERITANCE_AMBIGUOUS =
       new Message(
           MessageType.Error,
-          0,
-          "The datatype which is to be inherited could not be detected due to divergent datatypes of at least two parents.");
+          MessageCode.MESSAGE_CODE_DATA_TYPE_INHERITANCE_AMBIGUOUS,
+          "The data type which is to be inherited could not be detected due to divergent data types of at least two parents.");
 
   public static final Message DATA_TYPE_DOES_NOT_ACCEPT_COLLECTION_VALUES =
       new Message(
           MessageType.Error,
-          0,
-          "This datatype does not accept collections of values (e.g. Lists).");
+          MessageCode.MESSAGE_CODE_DATA_TYPE_DOES_NOT_ACCEPT_COLLECTION_VALUES,
+          "This data type does not accept collections of values (e.g. Lists).");
 
   public static final Message CANNOT_PARSE_UNIT =
-      new Message(MessageType.Error, 0, "This unit cannot be parsed.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_CANNOT_PARSE_UNIT,
+          "This unit cannot be parsed.");
 
   public static final Message SERVER_SIDE_SCRIPT_DOES_NOT_EXIST =
       new Message(
-          MessageType.Error, 404, "This server-side script does not exist. Did you install it?");
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "This server-side script does not exist. Did you install it?");
 
   public static final Message SERVER_SIDE_SCRIPT_NOT_EXECUTABLE =
-      new Message(MessageType.Error, 400, "This server-side script is not executable.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "This server-side script is not executable.");
 
   public static final Message SERVER_SIDE_SCRIPT_ERROR =
-      new Message(MessageType.Error, 500, "The invocation of this server-side script failed.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "The invocation of this server-side script failed.");
 
   public static final Message SERVER_SIDE_SCRIPT_SETUP_ERROR =
       new Message(
           MessageType.Error,
-          500,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
           "The setup routine for the server-side script failed. This might indicate a misconfiguration of the server. Please contact the administrator.");
 
   public static final Message SERVER_SIDE_SCRIPT_TIMEOUT =
-      new Message(MessageType.Error, 400, "This server-side script did not finish in time.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "This server-side script did not finish in time.");
 
   public static final Message SERVER_SIDE_SCRIPT_MISSING_CALL =
-      new Message(MessageType.Error, 400, "You must specify the `call` field.");
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_UNKNOWN,
+          "You must specify the `call` field.");
 
   public static final Message ADDITIONAL_PROPERTY =
       new Message(
           MessageType.Warning,
-          0,
+          MessageCode.MESSAGE_CODE_ADDITIONAL_PROPERTY,
           "This property is an additional property which has no corresponding property among the properties of the parents.");
 
   public static final Message PROPERTY_WITH_DATATYPE_OVERRIDE =
-      new Message(MessageType.Warning, 0, "This property overrides the datatype.");
+      new Message(
+          MessageType.Warning,
+          MessageCode.MESSAGE_CODE_PROPERTY_WITH_DATA_TYPE_OVERRIDE,
+          "This property overrides the data type.");
 
   public static final Message PROPERTY_WITH_DESC_OVERRIDE =
-      new Message(MessageType.Warning, 0, "This property overrides the description.");
+      new Message(
+          MessageType.Warning,
+          MessageCode.MESSAGE_CODE_PROPERTY_WITH_DESCRIPTION_OVERRIDE,
+          "This property overrides the description.");
 
   public static final Message PROPERTY_WITH_NAME_OVERRIDE =
-      new Message(MessageType.Warning, 0, "This property overrides the name.");
+      new Message(
+          MessageType.Warning,
+          MessageCode.MESSAGE_CODE_PROPERTY_WITH_NAME_OVERRIDE,
+          "This property overrides the name.");
+
+  public static final Message INTEGER_OUT_OF_RANGE =
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_INTEGER_VALUE_OUT_OF_RANGE,
+          "The integer value is out of range. This server only supports signed 32 bit integers.");
+
+  public static final Message ERROR_INTEGRITY_VIOLATION =
+      new Message(
+          MessageType.Error,
+          MessageCode.MESSAGE_CODE_INTEGRITY_VIOLATION,
+          "This entity caused an unexpected integrity violation. This is a strong indicator for a server bug. Please report.");
+
+  public static final Message INVALID_USER_NAME(String policy) {
+    return new Message(
+        MessageType.Error,
+        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.");
+  }
 }
diff --git a/src/test/java/org/caosdb/server/authentication/AuthTokenTest.java b/src/test/java/org/caosdb/server/authentication/AuthTokenTest.java
index ca19603230bf6aa989c495fbadabd0989c2a9790..0b19b414517769ee1d6c3882658e870d50193225 100644
--- a/src/test/java/org/caosdb/server/authentication/AuthTokenTest.java
+++ b/src/test/java/org/caosdb/server/authentication/AuthTokenTest.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,12 +19,13 @@
  * 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.authentication;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectReader;
@@ -53,10 +55,11 @@ import org.caosdb.server.database.backend.interfaces.RetrievePasswordValidatorIm
 import org.caosdb.server.database.backend.interfaces.RetrievePermissionRulesImpl;
 import org.caosdb.server.database.backend.interfaces.RetrieveRoleImpl;
 import org.caosdb.server.database.backend.interfaces.RetrieveUserImpl;
+import org.caosdb.server.grpc.AuthInterceptor;
 import org.caosdb.server.resource.TestScriptingResource.RetrievePasswordValidator;
 import org.caosdb.server.resource.TestScriptingResource.RetrievePermissionRules;
-import org.caosdb.server.resource.TestScriptingResource.RetrieveRole;
-import org.caosdb.server.resource.TestScriptingResource.RetrieveUser;
+import org.caosdb.server.resource.TestScriptingResource.RetrieveRoleMockup;
+import org.caosdb.server.resource.TestScriptingResource.RetrieveUserMockUp;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -71,9 +74,9 @@ public class AuthTokenTest {
 
   @BeforeClass
   public static void setupShiro() throws IOException {
-    BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRole.class);
+    BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRoleMockup.class);
     BackendTransaction.setImpl(RetrievePermissionRulesImpl.class, RetrievePermissionRules.class);
-    BackendTransaction.setImpl(RetrieveUserImpl.class, RetrieveUser.class);
+    BackendTransaction.setImpl(RetrieveUserImpl.class, RetrieveUserMockUp.class);
     BackendTransaction.setImpl(
         RetrievePasswordValidatorImpl.class, RetrievePasswordValidator.class);
 
@@ -393,6 +396,7 @@ public class AuthTokenTest {
     OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8"));
 
     Subject anonymous = SecurityUtils.getSubject();
+    CaosDBServer.setProperty(ServerProperties.KEY_AUTH_OPTIONAL, "true");
     anonymous.login(AnonymousAuthenticationToken.getInstance());
 
     OneTimeAuthenticationToken token =
@@ -448,4 +452,10 @@ public class AuthTokenTest {
     assertEquals(9223372036854775000L, config.getExpiresAfter());
     assertEquals(922337203685477000L, config.getReplayTimeout());
   }
+
+  @Test
+  public void testSessionTokenCookiePattern() {
+    String cookie = "SessionToken=%5B%22S%22%22%5D";
+    assertTrue(AuthInterceptor.SESSION_TOKEN_COOKIE_PREFIX_PATTERN.matcher(cookie).find());
+  }
 }
diff --git a/src/test/java/org/caosdb/server/database/InsertTest.java b/src/test/java/org/caosdb/server/database/InsertTest.java
index 7b00cacfe7c3454539cb6823d62bc3af9b4d7ce7..b6b535c77700e55bfba67523d6250025b6ce3360 100644
--- a/src/test/java/org/caosdb/server/database/InsertTest.java
+++ b/src/test/java/org/caosdb/server/database/InsertTest.java
@@ -224,8 +224,6 @@ public class InsertTest {
     subp.setStatementStatus(StatementStatus.FIX);
     p2.addProperty(subp);
 
-    r.print();
-
     final LinkedList<EntityInterface> stage1Inserts = new LinkedList<EntityInterface>();
     final LinkedList<EntityInterface> stage2Inserts = new LinkedList<EntityInterface>();
 
@@ -254,15 +252,6 @@ public class InsertTest {
     assertEquals((Integer) 2, stage2Inserts.get(0).getId());
     assertEquals("V2", ((SingleValue) stage2Inserts.get(0).getValue()).toDatabaseString());
     assertFalse(stage2Inserts.get(0).hasReplacement());
-
-    System.out.println("######### stage 1 #########");
-    for (EntityInterface e : stage1Inserts) {
-      e.print();
-    }
-    System.out.println("######### stage 2 #########");
-    for (EntityInterface e : stage2Inserts) {
-      e.print();
-    }
   }
 
   /**
diff --git a/src/test/java/org/caosdb/server/entity/SelectionTest.java b/src/test/java/org/caosdb/server/entity/SelectionTest.java
index 0ede3a970be6504ef0a89868aed40e4cadb141d0..26fbf413d42e66ccb5c65759a65dbb08a9d94df5 100644
--- a/src/test/java/org/caosdb/server/entity/SelectionTest.java
+++ b/src/test/java/org/caosdb/server/entity/SelectionTest.java
@@ -30,7 +30,7 @@ import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
 import org.caosdb.server.CaosDBServer;
-import org.caosdb.server.entity.xml.SetFieldStrategy;
+import org.caosdb.server.entity.xml.SerializeFieldStrategy;
 import org.caosdb.server.query.Query;
 import org.caosdb.server.query.Query.Selection;
 import org.junit.Assert;
@@ -46,7 +46,7 @@ public class SelectionTest {
 
   @Test
   public void testEmpty1() {
-    final SetFieldStrategy setFieldStrategy = new SetFieldStrategy();
+    final SerializeFieldStrategy setFieldStrategy = new SerializeFieldStrategy();
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("id"));
     Assert.assertTrue(setFieldStrategy.isToBeSet("name"));
@@ -64,7 +64,8 @@ public class SelectionTest {
   @Test
   public void testName1() {
     final Selection selection = new Selection("name");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("name"));
   }
@@ -72,7 +73,8 @@ public class SelectionTest {
   @Test
   public void testName2() {
     final Selection selection = new Selection("id");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("name"));
   }
@@ -80,7 +82,8 @@ public class SelectionTest {
   @Test
   public void testName3() {
     final Selection selection = new Selection("value");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("name"));
   }
@@ -88,7 +91,8 @@ public class SelectionTest {
   @Test
   public void testName4() {
     final Selection selection = new Selection("description");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("name"));
   }
@@ -96,7 +100,8 @@ public class SelectionTest {
   @Test
   public void testName5() {
     final Selection selection = new Selection("datatype");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("name"));
   }
@@ -104,7 +109,8 @@ public class SelectionTest {
   @Test
   public void testName6() {
     final Selection selection = new Selection("datatype");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("name"));
   }
@@ -112,7 +118,8 @@ public class SelectionTest {
   @Test
   public void testName7() {
     final Selection selection = new Selection("blabla");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("name"));
   }
@@ -120,7 +127,8 @@ public class SelectionTest {
   @Test
   public void testId1() {
     final Selection selection = new Selection("id");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("id"));
   }
@@ -128,7 +136,8 @@ public class SelectionTest {
   @Test
   public void testId2() {
     final Selection selection = new Selection("name");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("id"));
   }
@@ -136,7 +145,8 @@ public class SelectionTest {
   @Test
   public void testId3() {
     final Selection selection = new Selection("description");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("id"));
   }
@@ -144,7 +154,8 @@ public class SelectionTest {
   @Test
   public void testId4() {
     final Selection selection = new Selection("blablabla");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("id"));
   }
@@ -152,7 +163,8 @@ public class SelectionTest {
   @Test
   public void testDesc1() {
     final Selection selection = new Selection("description");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("description"));
   }
@@ -160,7 +172,8 @@ public class SelectionTest {
   @Test
   public void testDesc2() {
     final Selection selection = new Selection("name");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertFalse(setFieldStrategy.isToBeSet("description"));
   }
@@ -168,7 +181,8 @@ public class SelectionTest {
   @Test
   public void testDesc3() {
     final Selection selection = new Selection("id");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertFalse(setFieldStrategy.isToBeSet("description"));
   }
@@ -176,15 +190,16 @@ public class SelectionTest {
   @Test
   public void testDesc4() {
     final Selection selection = new Selection("blablaba");
-    final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection);
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(selection);
 
     Assert.assertFalse(setFieldStrategy.isToBeSet("description"));
   }
 
   @Test
   public void testMulti1() {
-    final SetFieldStrategy setFieldStrategy =
-        (new SetFieldStrategy())
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy())
             .addSelection(new Selection("id"))
             .addSelection(new Selection("name"));
 
@@ -196,8 +211,8 @@ public class SelectionTest {
 
   @Test
   public void testMulti2() {
-    final SetFieldStrategy setFieldStrategy =
-        (new SetFieldStrategy())
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy())
             .addSelection(new Selection("id"))
             .addSelection(new Selection("description"));
 
@@ -210,8 +225,8 @@ public class SelectionTest {
 
   @Test
   public void testMulti3() {
-    final SetFieldStrategy setFieldStrategy =
-        (new SetFieldStrategy())
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy())
             .addSelection(new Selection("datatype"))
             .addSelection(new Selection("description"));
 
@@ -224,8 +239,8 @@ public class SelectionTest {
 
   @Test
   public void testMulti4() {
-    final SetFieldStrategy setFieldStrategy =
-        (new SetFieldStrategy())
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy())
             .addSelection(new Selection("datatype"))
             .addSelection(new Selection("value"));
 
@@ -238,15 +253,15 @@ public class SelectionTest {
 
   @Test
   public void testComposition1() {
-    final SetFieldStrategy setFieldStrategy =
-        (new SetFieldStrategy()).addSelection(new Selection("blabla"));
+    final SerializeFieldStrategy setFieldStrategy =
+        (new SerializeFieldStrategy()).addSelection(new Selection("blabla"));
 
     Assert.assertTrue(setFieldStrategy.isToBeSet("blabla"));
     Assert.assertTrue(setFieldStrategy.isToBeSet("id"));
     Assert.assertTrue(setFieldStrategy.isToBeSet("name"));
     Assert.assertFalse(setFieldStrategy.isToBeSet("bleb"));
 
-    final SetFieldStrategy forProperty = setFieldStrategy.forProperty("blabla");
+    final SerializeFieldStrategy forProperty = setFieldStrategy.forProperty("blabla");
     Assert.assertTrue(forProperty.isToBeSet("id"));
     Assert.assertTrue(forProperty.isToBeSet("name"));
     Assert.assertTrue(forProperty.isToBeSet("blub"));
@@ -254,8 +269,8 @@ public class SelectionTest {
 
   @Test
   public void testComposition2() {
-    final SetFieldStrategy setFieldStrategy =
-        new SetFieldStrategy()
+    final SerializeFieldStrategy setFieldStrategy =
+        new SerializeFieldStrategy()
             .addSelection(new Selection("blabla"))
             .addSelection(new Selection("blabla.name"));
 
@@ -264,7 +279,7 @@ public class SelectionTest {
     Assert.assertTrue(setFieldStrategy.isToBeSet("name"));
     Assert.assertFalse(setFieldStrategy.isToBeSet("bleb"));
 
-    final SetFieldStrategy forProperty = setFieldStrategy.forProperty("blabla");
+    final SerializeFieldStrategy forProperty = setFieldStrategy.forProperty("blabla");
     Assert.assertTrue(forProperty.isToBeSet("id"));
     Assert.assertTrue(forProperty.isToBeSet("name"));
     Assert.assertTrue(forProperty.isToBeSet("blub"));
@@ -272,8 +287,8 @@ public class SelectionTest {
 
   @Test
   public void testComposition3() {
-    final SetFieldStrategy setFieldStrategy =
-        new SetFieldStrategy()
+    final SerializeFieldStrategy setFieldStrategy =
+        new SerializeFieldStrategy()
             .addSelection(new Selection("blabla").setSubSelection(new Selection("value")))
             .addSelection(new Selection("blabla").setSubSelection(new Selection("description")));
 
@@ -282,7 +297,7 @@ public class SelectionTest {
     assertTrue(setFieldStrategy.isToBeSet("name"));
     assertFalse(setFieldStrategy.isToBeSet("bleb"));
 
-    final SetFieldStrategy forProperty = setFieldStrategy.forProperty("blabla");
+    final SerializeFieldStrategy forProperty = setFieldStrategy.forProperty("blabla");
     assertTrue(forProperty.isToBeSet("id"));
     assertTrue(forProperty.isToBeSet("name"));
     assertTrue(forProperty.isToBeSet("description"));
@@ -312,7 +327,7 @@ public class SelectionTest {
 
     assertEquals(s.toString(), "property.subproperty.subsubproperty");
 
-    SetFieldStrategy setFieldStrategy = new SetFieldStrategy().addSelection(s);
+    SerializeFieldStrategy setFieldStrategy = new SerializeFieldStrategy().addSelection(s);
     assertTrue(setFieldStrategy.isToBeSet("property"));
     assertFalse(setFieldStrategy.forProperty("property").isToBeSet("sadf"));
     //    assertFalse(setFieldStrategy.forProperty("property").isToBeSet("name"));
diff --git a/src/test/java/org/caosdb/server/entity/container/PropertyContainerTest.java b/src/test/java/org/caosdb/server/entity/container/PropertyContainerTest.java
index 0428af146f7b4a191fa6be679e01af45aff315f2..a65d2bccb61c787ee6fed4663f97b0b84e1d98c0 100644
--- a/src/test/java/org/caosdb/server/entity/container/PropertyContainerTest.java
+++ b/src/test/java/org/caosdb/server/entity/container/PropertyContainerTest.java
@@ -30,7 +30,7 @@ import org.caosdb.server.entity.Entity;
 import org.caosdb.server.entity.Role;
 import org.caosdb.server.entity.wrapper.Property;
 import org.caosdb.server.entity.xml.PropertyToElementStrategyTest;
-import org.caosdb.server.entity.xml.SetFieldStrategy;
+import org.caosdb.server.entity.xml.SerializeFieldStrategy;
 import org.jdom2.Element;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -70,8 +70,9 @@ public class PropertyContainerTest {
   public void test() {
     PropertyContainer container = new PropertyContainer(new Entity());
     Element element = new Element("Record");
-    SetFieldStrategy setFieldStrategy =
-        new SetFieldStrategy().addSelection(PropertyToElementStrategyTest.parse("window.height"));
+    SerializeFieldStrategy setFieldStrategy =
+        new SerializeFieldStrategy()
+            .addSelection(PropertyToElementStrategyTest.parse("window.height"));
 
     container.addToElement(windowProperty, element, setFieldStrategy);
 
diff --git a/src/test/java/org/caosdb/server/entity/xml/PropertyToElementStrategyTest.java b/src/test/java/org/caosdb/server/entity/xml/PropertyToElementStrategyTest.java
index 436278ad640663ba42f71b824924b2f1293a5132..fa8469dba9d95b70d77dcf0428e4426ff1672548 100644
--- a/src/test/java/org/caosdb/server/entity/xml/PropertyToElementStrategyTest.java
+++ b/src/test/java/org/caosdb/server/entity/xml/PropertyToElementStrategyTest.java
@@ -88,7 +88,8 @@ public class PropertyToElementStrategyTest {
   @Test
   public void test() {
     PropertyToElementStrategy strategy = new PropertyToElementStrategy();
-    SetFieldStrategy setFieldStrategy = new SetFieldStrategy().addSelection(parse("height"));
+    SerializeFieldStrategy setFieldStrategy =
+        new SerializeFieldStrategy().addSelection(parse("height"));
     EntityInterface property = windowProperty;
     ((ReferenceValue) property.getValue()).setEntity(window, true);
     Element element = strategy.toElement(property, setFieldStrategy);
diff --git a/src/test/java/org/caosdb/server/grpc/CaosDBToGrpcConvertersTest.java b/src/test/java/org/caosdb/server/grpc/CaosDBToGrpcConvertersTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..84baba26fbadb920320b8d22a789509e855f9693
--- /dev/null
+++ b/src/test/java/org/caosdb/server/grpc/CaosDBToGrpcConvertersTest.java
@@ -0,0 +1,103 @@
+package org.caosdb.server.grpc;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.TimeZone;
+import org.caosdb.datetime.DateTimeFactory2;
+import org.caosdb.server.datatype.GenericValue;
+import org.caosdb.server.datatype.Value;
+import org.caosdb.server.entity.FileProperties;
+import org.caosdb.server.entity.Message;
+import org.caosdb.server.entity.RetrieveEntity;
+import org.caosdb.server.entity.Role;
+import org.caosdb.server.entity.StatementStatus;
+import org.caosdb.server.entity.wrapper.Parent;
+import org.caosdb.server.entity.wrapper.Property;
+import org.caosdb.server.entity.xml.IdAndServerMessagesOnlyStrategy;
+import org.junit.Test;
+
+public class CaosDBToGrpcConvertersTest {
+
+  @Test
+  public void testConvertScalarValue_Datetime() {
+    TimeZone timeZone = TimeZone.getTimeZone("UTC");
+    DateTimeFactory2 factory = new DateTimeFactory2(timeZone);
+    CaosDBToGrpcConverters converters = new CaosDBToGrpcConverters(timeZone);
+    Value value = null;
+    assertNull(converters.convertScalarValue(value));
+    value = factory.parse("2022");
+    assertEquals(converters.convertScalarValue(value).toString(), "string_value: \"2022\"\n");
+    value = factory.parse("2022-12");
+    assertEquals(converters.convertScalarValue(value).toString(), "string_value: \"2022-12\"\n");
+    value = factory.parse("2022-12-24");
+    assertEquals(converters.convertScalarValue(value).toString(), "string_value: \"2022-12-24\"\n");
+    value = factory.parse("2022-12-24T18:15:00");
+    assertEquals(
+        converters.convertScalarValue(value).toString(),
+        "string_value: \"2022-12-24T18:15:00+0000\"\n");
+    value = factory.parse("2022-12-24T18:15:00.999999");
+    assertEquals(
+        converters.convertScalarValue(value).toString(),
+        "string_value: \"2022-12-24T18:15:00.999999+0000\"\n");
+    value = factory.parse("2022-12-24T18:15:00.999999UTC");
+    assertEquals(
+        converters.convertScalarValue(value).toString(),
+        "string_value: \"2022-12-24T18:15:00.999999+0000\"\n");
+    value = factory.parse("2022-12-24T18:15:00.999999+0200");
+    assertEquals(
+        converters.convertScalarValue(value).toString(),
+        "string_value: \"2022-12-24T16:15:00.999999+0000\"\n");
+  }
+
+  @Test
+  public void testConvertEntity_FileDescriptor() {
+    RetrieveEntity entity = new RetrieveEntity(null);
+    CaosDBToGrpcConverters converters = new CaosDBToGrpcConverters(null);
+    assertEquals(converters.convert(entity).toString(), "entity {\n}\n");
+    entity.setFileProperties(new FileProperties("checksum1234", "the/path", 1024L));
+    assertEquals(
+        converters.convert(entity).toString(),
+        "entity {\n  file_descriptor {\n    path: \"the/path\"\n    size: 1024\n  }\n}\n");
+  }
+
+  @Test
+  public void testIdServerMessagesOnlyStrategy() {
+    // @review Florian Spreckelsen 2022-03-22
+    RetrieveEntity entity = new RetrieveEntity(null);
+
+    // must be printed
+    entity.setId(1234);
+    entity.addInfo("info");
+    entity.addWarning(new Message("warning"));
+    entity.addError(new Message("error"));
+
+    // must not be printed
+    Parent par = new Parent();
+    par.setName("dont print parent");
+    entity.addParent(par);
+    entity.setName("dont print");
+    entity.setDescription("dont print");
+    entity.setRole(Role.File);
+    entity.setFileProperties(new FileProperties("dont print checksum", "dont print path", 1234L));
+    Property p = new Property();
+    p.setStatementStatus(StatementStatus.FIX);
+    p.setName("dont print property");
+    p.setDatatype("TEXT");
+    p.setValue(new GenericValue("don print"));
+    entity.addProperty(p);
+
+    CaosDBToGrpcConverters converters = new CaosDBToGrpcConverters(null);
+
+    // first test the normal SerializeFieldStrategy instead
+    entity.setSerializeFieldStrategy(null);
+    assertTrue(converters.convert(entity).toString().contains("dont print"));
+
+    // now suppress all fields but id and server messages.
+    entity.setSerializeFieldStrategy(new IdAndServerMessagesOnlyStrategy());
+    assertEquals(
+        converters.convert(entity).toString(),
+        "entity {\n  id: \"1234\"\n}\nerrors {\n  code: 1\n  description: \"error\"\n}\nwarnings {\n  code: 1\n  description: \"warning\"\n}\ninfos {\n  code: 1\n  description: \"info\"\n}\n");
+  }
+}
diff --git a/src/test/java/org/caosdb/server/permissions/EntityACLTest.java b/src/test/java/org/caosdb/server/permissions/EntityACLTest.java
index 1787c902f48124d692f8c53e4a73ed04564dfe8f..30c0cd992573c0c083d863a6d60750dd77d6e035 100644
--- a/src/test/java/org/caosdb/server/permissions/EntityACLTest.java
+++ b/src/test/java/org/caosdb/server/permissions/EntityACLTest.java
@@ -23,6 +23,7 @@
 package org.caosdb.server.permissions;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
@@ -30,20 +31,27 @@ import java.io.IOException;
 import java.util.BitSet;
 import java.util.HashSet;
 import java.util.LinkedList;
+import java.util.Set;
 import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.subject.Subject;
 import org.caosdb.server.CaosDBServer;
+import org.caosdb.server.ServerProperties;
 import org.caosdb.server.accessControl.AnonymousAuthenticationToken;
 import org.caosdb.server.accessControl.AuthenticationUtils;
 import org.caosdb.server.accessControl.Config;
+import org.caosdb.server.accessControl.CredentialsValidator;
 import org.caosdb.server.accessControl.OneTimeAuthenticationToken;
+import org.caosdb.server.accessControl.Principal;
 import org.caosdb.server.accessControl.Role;
 import org.caosdb.server.database.BackendTransaction;
 import org.caosdb.server.database.access.Access;
+import org.caosdb.server.database.backend.interfaces.RetrievePasswordValidatorImpl;
 import org.caosdb.server.database.backend.interfaces.RetrievePermissionRulesImpl;
 import org.caosdb.server.database.backend.interfaces.RetrieveRoleImpl;
+import org.caosdb.server.database.backend.interfaces.RetrieveUserImpl;
 import org.caosdb.server.database.exceptions.TransactionException;
 import org.caosdb.server.database.misc.TransactionBenchmark;
+import org.caosdb.server.database.proto.ProtoUser;
 import org.caosdb.server.resource.AbstractCaosDBServerResource;
 import org.caosdb.server.resource.AbstractCaosDBServerResource.XMLParser;
 import org.caosdb.server.utils.Utils;
@@ -101,6 +109,54 @@ public class EntityACLTest {
     }
   }
 
+  public static class RetrievePasswordValidatorMockup implements RetrievePasswordValidatorImpl {
+
+    public RetrievePasswordValidatorMockup(Access a) {}
+
+    @Override
+    public void setTransactionBenchmark(TransactionBenchmark b) {}
+
+    @Override
+    public TransactionBenchmark getBenchmark() {
+      return null;
+    }
+
+    @Override
+    public CredentialsValidator<String> execute(String name) throws TransactionException {
+      if (name.equals("anonymous")) {
+        return new CredentialsValidator<String>() {
+
+          @Override
+          public boolean isValid(String credential) {
+            return false;
+          }
+        };
+      }
+      return null;
+    }
+  }
+
+  public static class RetrieveUserMockup implements RetrieveUserImpl {
+
+    public RetrieveUserMockup(Access a) {}
+
+    @Override
+    public void setTransactionBenchmark(TransactionBenchmark b) {}
+
+    @Override
+    public TransactionBenchmark getBenchmark() {
+      return null;
+    }
+
+    @Override
+    public ProtoUser execute(Principal principal) throws TransactionException {
+      if (principal.getUsername().equals("anonymous")) {
+        return new ProtoUser();
+      }
+      return null;
+    }
+  }
+
   @BeforeClass
   public static void init() throws IOException {
     CaosDBServer.initServerProperties();
@@ -110,6 +166,9 @@ public class EntityACLTest {
     BackendTransaction.setImpl(
         RetrievePermissionRulesImpl.class, RetrievePermissionRulesMockup.class);
     BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRoleMockup.class);
+    BackendTransaction.setImpl(
+        RetrievePasswordValidatorImpl.class, RetrievePasswordValidatorMockup.class);
+    BackendTransaction.setImpl(RetrieveUserImpl.class, RetrieveUserMockup.class);
   }
 
   @Test
@@ -270,6 +329,7 @@ public class EntityACLTest {
   @Test
   public void testEntityACLForAnonymous() {
     Subject anonymous = SecurityUtils.getSubject();
+    CaosDBServer.setProperty(ServerProperties.KEY_AUTH_OPTIONAL, "true");
     anonymous.login(AnonymousAuthenticationToken.getInstance());
     assertTrue(AuthenticationUtils.isAnonymous(anonymous));
     EntityACL acl = EntityACL.getOwnerACLFor(anonymous);
@@ -277,35 +337,6 @@ public class EntityACLTest {
     assertTrue(acl.getOwners().isEmpty());
   }
 
-  //   @Test
-  //   public void testParseFromElement() throws JDOMException, IOException {
-  //   Assert.assertEquals("[]",
-  //   EntityACL.serialize(EntityACL.parseFromElement(stringToJdom("<ACL></ACL>"))));
-  //   Assert.assertEquals("[]", EntityACL.serialize(EntityACL
-  //   .parseFromElement(stringToJdom("<ACL><Grant></Grant></ACL>"))));
-  //   Assert.assertEquals("[]", EntityACL.serialize(EntityACL
-  //   .parseFromElement(stringToJdom("<ACL><Deny></Deny></ACL>"))));
-  //   Assert.assertEquals("[]", EntityACL.serialize(EntityACL
-  //   .parseFromElement(stringToJdom("<ACL><Grant role='bla'></Grant></ACL>"))));
-  //   Assert.assertEquals("[]", EntityACL.serialize(EntityACL
-  //   .parseFromElement(stringToJdom("<ACL><Deny role='bla'></Deny></ACL>"))));
-  //   Assert.assertEquals(
-  //   "{bla:2;}",
-  //   EntityACL.serialize(EntityACL
-  //   .parseFromElement(stringToJdom("<ACL><Grant role='bla'><Permission
-  // name='DELETE'/></Grant></ACL>"))));
-  //   Assert.assertEquals(
-  //   "{bla:" + (Long.MIN_VALUE + 2) + ";}",
-  //   EntityACL.serialize(EntityACL
-  //   .parseFromElement(stringToJdom("<ACL><Deny role='bla'><Permission name='DELETE'
-  // /></Deny></ACL>"))));
-  //   Assert.assertEquals(
-  //   "{bla:32;}",
-  //   EntityACL.serialize(EntityACL
-  //   .parseFromElement(stringToJdom("<ACL><Grant role='bla'><Permission name='RETRIEVE:ACL'
-  // /></Grant></ACL>"))));
-  //   }
-
   @Test
   public void testFactory() {
     final AbstractEntityACLFactory<EntityACL> f = new EntityACLFactory();
@@ -395,4 +426,38 @@ public class EntityACLTest {
       assertTrue(EntityACL.isPriorityBitSet(aci.getBitSet()));
     }
   }
+
+  @Test
+  public void testOwnership() {
+    EntityACLFactory f = new EntityACLFactory();
+    f.grant(
+        org.caosdb.server.permissions.Role.create("the_owner"), false, EntityPermission.EDIT_ACL);
+    f.deny(
+        org.caosdb.server.permissions.Role.create("someone_else"),
+        false,
+        EntityPermission.EDIT_ACL);
+    EntityACL acl = f.create();
+    assertEquals(1, acl.getOwners().size());
+    assertEquals("the_owner", acl.getOwners().get(0).toString());
+  }
+
+  @Test
+  public void testPermissionsFor() {
+    EntityACLFactory f = new EntityACLFactory();
+    f.deny(org.caosdb.server.permissions.Role.ANONYMOUS_ROLE, false, EntityPermission.EDIT_ACL);
+    f.grant(org.caosdb.server.permissions.Role.OWNER_ROLE, false, "*");
+    EntityACL acl = f.create();
+
+    Subject anonymous = SecurityUtils.getSubject();
+    CaosDBServer.setProperty(ServerProperties.KEY_AUTH_OPTIONAL, "true");
+    anonymous.login(AnonymousAuthenticationToken.getInstance());
+    assertTrue(AuthenticationUtils.isAnonymous(anonymous));
+
+    assertNotNull(acl);
+    assertTrue(acl.getOwners().isEmpty());
+    final Set<EntityPermission> permissionsFor =
+        EntityACL.getPermissionsFor(anonymous, acl.getRules());
+
+    assertFalse(permissionsFor.contains(EntityPermission.RETRIEVE_ENTITY));
+  }
 }
diff --git a/src/test/java/org/caosdb/server/permissions/EntityPermissionTest.java b/src/test/java/org/caosdb/server/permissions/EntityPermissionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..fd7c3e8e7e9a66a8b99ac09487bfa02767497e22
--- /dev/null
+++ b/src/test/java/org/caosdb/server/permissions/EntityPermissionTest.java
@@ -0,0 +1,92 @@
+package org.caosdb.server.permissions;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class EntityPermissionTest {
+
+  @Test
+  public void testGRPCMapping() {
+    assertEquals(
+        EntityPermission.EDIT_ACL.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_EDIT_ACL);
+
+    assertEquals(
+        EntityPermission.DELETE.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_DELETE);
+
+    assertEquals(
+        EntityPermission.USE_AS_DATA_TYPE.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_USE_AS_DATA_TYPE);
+    assertEquals(
+        EntityPermission.USE_AS_PARENT.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_USE_AS_PARENT);
+    assertEquals(
+        EntityPermission.USE_AS_PROPERTY.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_USE_AS_PROPERTY);
+    assertEquals(
+        EntityPermission.USE_AS_REFERENCE.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_USE_AS_REFERENCE);
+
+    assertEquals(
+        EntityPermission.RETRIEVE_ENTITY.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_RETRIEVE_ENTITY);
+    assertEquals(
+        EntityPermission.RETRIEVE_ACL.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_RETRIEVE_ACL);
+    assertEquals(
+        EntityPermission.RETRIEVE_OWNER.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_RETRIEVE_OWNER);
+    assertEquals(
+        EntityPermission.RETRIEVE_HISTORY.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_RETRIEVE_HISTORY);
+    assertEquals(
+        EntityPermission.RETRIEVE_FILE.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_RETRIEVE_FILE);
+
+    assertEquals(
+        EntityPermission.UPDATE_VALUE.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_VALUE);
+    assertEquals(
+        EntityPermission.UPDATE_DESCRIPTION.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_DESCRIPTION);
+    assertEquals(
+        EntityPermission.UPDATE_DATA_TYPE.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_DATA_TYPE);
+    assertEquals(
+        EntityPermission.UPDATE_NAME.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_NAME);
+    assertEquals(
+        EntityPermission.UPDATE_QUERY_TEMPLATE_DEFINITION.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission
+            .ENTITY_PERMISSION_UPDATE_QUERY_TEMPLATE_DEFINITION);
+    assertEquals(
+        EntityPermission.UPDATE_ROLE.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_ROLE);
+
+    assertEquals(
+        EntityPermission.UPDATE_ADD_PARENT.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_ADD_PARENT);
+    assertEquals(
+        EntityPermission.UPDATE_REMOVE_PARENT.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_REMOVE_PARENT);
+
+    assertEquals(
+        EntityPermission.UPDATE_ADD_PROPERTY.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_ADD_PROPERTY);
+    assertEquals(
+        EntityPermission.UPDATE_REMOVE_PROPERTY.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_REMOVE_PROPERTY);
+
+    assertEquals(
+        EntityPermission.UPDATE_ADD_FILE.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_ADD_FILE);
+    assertEquals(
+        EntityPermission.UPDATE_REMOVE_FILE.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_REMOVE_FILE);
+    assertEquals(
+        EntityPermission.UPDATE_MOVE_FILE.getMapping(),
+        org.caosdb.api.entity.v1.EntityPermission.ENTITY_PERMISSION_UPDATE_MOVE_FILE);
+  }
+}
diff --git a/src/test/java/org/caosdb/server/query/TestCQL.java b/src/test/java/org/caosdb/server/query/TestCQL.java
index cf54bf71b69ea5f539744bd4bbe30fd1c37edcf4..ff1be776b041490aac7c434acdee73c96a9e88f9 100644
--- a/src/test/java/org/caosdb/server/query/TestCQL.java
+++ b/src/test/java/org/caosdb/server/query/TestCQL.java
@@ -264,6 +264,13 @@ public class TestCQL {
   String queryMR56 = "FIND ENTITY WITH ((p0 = v0 OR p1=v1) AND p2=v2)";
 
   String versionedQuery1 = "FIND ANY VERSION OF ENTITY e1";
+  // https://gitlab.com/caosdb/caosdb-server/-/issues/131
+  String issue131a = "FIND ename WITH pname1.x AND pname2";
+  String issue131b = "FIND ename WITH (pname1.x < 10) AND (pname1.x)";
+  String issue131c = "FIND ename WITH pname2 AND pname1.x ";
+  String issue131d = "FIND ename WITH (pname1.x) AND pname2";
+  String issue131e = "FIND ename WITH (pname1.pname2 > 30) AND (pname1.pname2 < 40)";
+  String issue131f = "FIND ename WITH (pname1.pname2 > 30) AND pname1.pname2 < 40";
 
   @Test
   public void testQuery1()
@@ -4341,7 +4348,7 @@ public class TestCQL {
     assertEquals("THE GREATEST ID", conjunction.getChild(3).getText());
   }
 
-  /** String query31 = "FIND PROPERTIES WHICH ARE INSERTED TODAY"; */
+  /** String query31 = "FIND PROPERTIES WHICH WERE INSERTED TODAY"; */
   @Test
   public void testQuery31() {
     CQLLexer lexer;
@@ -6740,4 +6747,163 @@ public class TestCQL {
     // must not throw ParsingException
     new Query(this.queryIssue131).parse();
   }
+
+  /** String issue131a = "FIND ename WITH pname1.x AND pname2"; */
+  @Test
+  public void testIssue131a() {
+    CQLLexer lexer;
+    lexer = new CQLLexer(CharStreams.fromString(this.issue131a));
+    final CommonTokenStream tokens = new CommonTokenStream(lexer);
+
+    final CQLParser parser = new CQLParser(tokens);
+    final CqContext sfq = parser.cq();
+
+    System.out.println(sfq.toStringTree(parser));
+
+    assertTrue(sfq.filter instanceof Conjunction);
+    LinkedList<EntityFilterInterface> filters = ((Conjunction) sfq.filter).getFilters();
+    assertEquals(filters.size(), 2);
+    assertTrue(filters.get(0) instanceof POV);
+    assertTrue(filters.get(1) instanceof POV);
+    POV pov1 = ((POV) filters.get(0));
+    POV pov2 = ((POV) filters.get(1));
+    assertEquals("POV(pname1,null,null)", pov1.toString());
+    assertEquals("POV(pname2,null,null)", pov2.toString());
+    assertTrue(pov1.hasSubProperty());
+    assertFalse(pov2.hasSubProperty());
+    assertEquals("POV(x,null,null)", pov1.getSubProperty().getFilter().toString());
+  }
+
+  /** String issue131b = "FIND ename WITH (pname1.x < 10) AND (pname1.x)"; */
+  @Test
+  public void testIssue131b() {
+    CQLLexer lexer;
+    lexer = new CQLLexer(CharStreams.fromString(this.issue131b));
+    final CommonTokenStream tokens = new CommonTokenStream(lexer);
+
+    final CQLParser parser = new CQLParser(tokens);
+    final CqContext sfq = parser.cq();
+
+    System.out.println(sfq.toStringTree(parser));
+
+    assertTrue(sfq.filter instanceof Conjunction);
+    LinkedList<EntityFilterInterface> filters = ((Conjunction) sfq.filter).getFilters();
+    assertEquals(filters.size(), 2);
+    assertTrue(filters.get(0) instanceof POV);
+    assertTrue(filters.get(1) instanceof POV);
+    POV pov1 = ((POV) filters.get(0));
+    POV pov2 = ((POV) filters.get(1));
+    assertEquals("POV(pname1,null,null)", pov1.toString());
+    assertEquals("POV(pname1,null,null)", pov2.toString());
+    assertTrue(pov1.hasSubProperty());
+    assertTrue(pov2.hasSubProperty());
+    assertEquals("POV(x,<,10)", pov1.getSubProperty().getFilter().toString());
+    assertEquals("POV(x,null,null)", pov2.getSubProperty().getFilter().toString());
+  }
+
+  /** String issue131c = "FIND ename WITH pname2 AND pname1.x "; */
+  @Test
+  public void testIssue131c() {
+    CQLLexer lexer;
+    lexer = new CQLLexer(CharStreams.fromString(this.issue131c));
+    final CommonTokenStream tokens = new CommonTokenStream(lexer);
+
+    final CQLParser parser = new CQLParser(tokens);
+    final CqContext sfq = parser.cq();
+
+    System.out.println(sfq.toStringTree(parser));
+
+    assertTrue(sfq.filter instanceof Conjunction);
+    LinkedList<EntityFilterInterface> filters = ((Conjunction) sfq.filter).getFilters();
+    assertEquals(filters.size(), 2);
+    assertTrue(filters.get(0) instanceof POV);
+    assertTrue(filters.get(1) instanceof POV);
+    POV pov1 = ((POV) filters.get(0));
+    POV pov2 = ((POV) filters.get(1));
+    assertEquals("POV(pname2,null,null)", pov1.toString());
+    assertEquals("POV(pname1,null,null)", pov2.toString());
+    assertFalse(pov1.hasSubProperty());
+    assertTrue(pov2.hasSubProperty());
+    assertEquals("POV(x,null,null)", pov2.getSubProperty().getFilter().toString());
+  }
+
+  /** String issue131d = "FIND ename WITH (pname1.x) AND pname2"; */
+  @Test
+  public void testIssue131d() {
+    CQLLexer lexer;
+    lexer = new CQLLexer(CharStreams.fromString(this.issue131d));
+    final CommonTokenStream tokens = new CommonTokenStream(lexer);
+
+    final CQLParser parser = new CQLParser(tokens);
+    final CqContext sfq = parser.cq();
+
+    System.out.println(sfq.toStringTree(parser));
+
+    assertTrue(sfq.filter instanceof Conjunction);
+    LinkedList<EntityFilterInterface> filters = ((Conjunction) sfq.filter).getFilters();
+    assertEquals(filters.size(), 2);
+    assertTrue(filters.get(0) instanceof POV);
+    assertTrue(filters.get(1) instanceof POV);
+    POV pov1 = ((POV) filters.get(0));
+    POV pov2 = ((POV) filters.get(1));
+    assertEquals("POV(pname1,null,null)", pov1.toString());
+    assertEquals("POV(pname2,null,null)", pov2.toString());
+    assertTrue(pov1.hasSubProperty());
+    assertFalse(pov2.hasSubProperty());
+    assertEquals("POV(x,null,null)", pov1.getSubProperty().getFilter().toString());
+  }
+
+  /** String issue131e = "FIND ename WITH (pname1.pname2 > 30) AND (pname1.pname2 < 40)"; */
+  @Test
+  public void testIssue131e() {
+    CQLLexer lexer;
+    lexer = new CQLLexer(CharStreams.fromString(this.issue131e));
+    final CommonTokenStream tokens = new CommonTokenStream(lexer);
+
+    final CQLParser parser = new CQLParser(tokens);
+    final CqContext sfq = parser.cq();
+
+    System.out.println(sfq.toStringTree(parser));
+
+    assertTrue(sfq.filter instanceof Conjunction);
+    LinkedList<EntityFilterInterface> filters = ((Conjunction) sfq.filter).getFilters();
+    assertEquals(filters.size(), 2);
+    assertTrue(filters.get(0) instanceof POV);
+    assertTrue(filters.get(1) instanceof POV);
+    POV pov1 = ((POV) filters.get(0));
+    POV pov2 = ((POV) filters.get(1));
+    assertEquals("POV(pname1,null,null)", pov1.toString());
+    assertEquals("POV(pname1,null,null)", pov2.toString());
+    assertTrue(pov1.hasSubProperty());
+    assertTrue(pov2.hasSubProperty());
+    assertEquals("POV(pname2,>,30)", pov1.getSubProperty().getFilter().toString());
+    assertEquals("POV(pname2,<,40)", pov2.getSubProperty().getFilter().toString());
+  }
+
+  /** String issue131f = "FIND ename WITH (pname1.pname2 > 30) AND pname1.pname2 < 40"; */
+  @Test
+  public void testIssue131f() {
+    CQLLexer lexer;
+    lexer = new CQLLexer(CharStreams.fromString(this.issue131f));
+    final CommonTokenStream tokens = new CommonTokenStream(lexer);
+
+    final CQLParser parser = new CQLParser(tokens);
+    final CqContext sfq = parser.cq();
+
+    System.out.println(sfq.toStringTree(parser));
+
+    assertTrue(sfq.filter instanceof Conjunction);
+    LinkedList<EntityFilterInterface> filters = ((Conjunction) sfq.filter).getFilters();
+    assertEquals(filters.size(), 2);
+    assertTrue(filters.get(0) instanceof POV);
+    assertTrue(filters.get(1) instanceof POV);
+    POV pov1 = ((POV) filters.get(0));
+    POV pov2 = ((POV) filters.get(1));
+    assertEquals("POV(pname1,null,null)", pov1.toString());
+    assertEquals("POV(pname1,null,null)", pov2.toString());
+    assertTrue(pov1.hasSubProperty());
+    assertTrue(pov2.hasSubProperty());
+    assertEquals("POV(pname2,>,30)", pov1.getSubProperty().getFilter().toString());
+    assertEquals("POV(pname2,<,40)", pov2.getSubProperty().getFilter().toString());
+  }
 }
diff --git a/src/test/java/org/caosdb/server/resource/TestAbstractCaosDBServerResource.java b/src/test/java/org/caosdb/server/resource/TestAbstractCaosDBServerResource.java
index 5d81ca738d507bcfd75dbe137756135cede3989b..226c9aaac077a56fa96535480418762d30bbb6e9 100644
--- a/src/test/java/org/caosdb/server/resource/TestAbstractCaosDBServerResource.java
+++ b/src/test/java/org/caosdb/server/resource/TestAbstractCaosDBServerResource.java
@@ -89,6 +89,7 @@ public class TestAbstractCaosDBServerResource {
   @Test
   public void testReponseRootElement() throws IOException {
     final Subject user = new DelegatingSubject(new DefaultSecurityManager(new AnonymousRealm()));
+    CaosDBServer.setProperty(ServerProperties.KEY_AUTH_OPTIONAL, "true");
     user.login(AnonymousAuthenticationToken.getInstance());
     AbstractCaosDBServerResource s =
         new AbstractCaosDBServerResource() {
diff --git a/src/test/java/org/caosdb/server/resource/TestScriptingResource.java b/src/test/java/org/caosdb/server/resource/TestScriptingResource.java
index 7f7434528678dbd2e1886fade2116d0c9f766740..566c8a09ee301f628bfe9232dde75ad5a66c0207 100644
--- a/src/test/java/org/caosdb/server/resource/TestScriptingResource.java
+++ b/src/test/java/org/caosdb/server/resource/TestScriptingResource.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 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -18,8 +19,8 @@
  * 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.resource;
 
 import static org.junit.Assert.assertEquals;
@@ -31,6 +32,7 @@ import java.util.List;
 import org.apache.shiro.SecurityUtils;
 import org.apache.shiro.subject.Subject;
 import org.caosdb.server.CaosDBServer;
+import org.caosdb.server.ServerProperties;
 import org.caosdb.server.accessControl.AnonymousAuthenticationToken;
 import org.caosdb.server.accessControl.CredentialsValidator;
 import org.caosdb.server.accessControl.Principal;
@@ -64,9 +66,9 @@ import org.restlet.representation.StringRepresentation;
 
 public class TestScriptingResource {
 
-  public static class RetrieveRole implements RetrieveRoleImpl {
+  public static class RetrieveRoleMockup implements RetrieveRoleImpl {
 
-    public RetrieveRole(Access a) {}
+    public RetrieveRoleMockup(Access a) {}
 
     @Override
     public Role retrieve(String role) throws TransactionException {
@@ -107,9 +109,9 @@ public class TestScriptingResource {
     public void setTransactionBenchmark(TransactionBenchmark b) {}
   }
 
-  public static class RetrieveUser implements RetrieveUserImpl {
+  public static class RetrieveUserMockUp implements RetrieveUserImpl {
 
-    public RetrieveUser(Access a) {}
+    public RetrieveUserMockUp(Access a) {}
 
     @Override
     public ProtoUser execute(Principal principal) throws TransactionException {
@@ -154,9 +156,9 @@ public class TestScriptingResource {
     CaosDBServer.initServerProperties();
     CaosDBServer.initShiro();
 
-    BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRole.class);
+    BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRoleMockup.class);
     BackendTransaction.setImpl(RetrievePermissionRulesImpl.class, RetrievePermissionRules.class);
-    BackendTransaction.setImpl(RetrieveUserImpl.class, RetrieveUser.class);
+    BackendTransaction.setImpl(RetrieveUserImpl.class, RetrieveUserMockUp.class);
     BackendTransaction.setImpl(
         RetrievePasswordValidatorImpl.class, RetrievePasswordValidator.class);
 
@@ -204,6 +206,7 @@ public class TestScriptingResource {
   @Test
   public void testAnonymousWithOutPermission() {
     Subject user = SecurityUtils.getSubject();
+    CaosDBServer.setProperty(ServerProperties.KEY_AUTH_OPTIONAL, "true");
     user.login(AnonymousAuthenticationToken.getInstance());
     Form form = new Form("call=anonymous_no_permission");
     Representation entity = form.getWebRepresentation();
@@ -221,6 +224,7 @@ public class TestScriptingResource {
   @Test
   public void testAnonymousWithPermission() {
     Subject user = SecurityUtils.getSubject();
+    CaosDBServer.setProperty(ServerProperties.KEY_AUTH_OPTIONAL, "true");
     user.login(AnonymousAuthenticationToken.getInstance());
     Form form = new Form("call=anonymous_ok");
     Representation entity = form.getWebRepresentation();
@@ -253,6 +257,7 @@ public class TestScriptingResource {
   @Test
   public void testHandleForm() throws Message, IOException {
     Subject user = SecurityUtils.getSubject();
+    CaosDBServer.setProperty(ServerProperties.KEY_AUTH_OPTIONAL, "true");
     user.login(AnonymousAuthenticationToken.getInstance());
     Form form = new Form("call=anonymous_ok");
     assertEquals(0, resource.handleForm(form));
diff --git a/src/test/java/org/caosdb/server/resource/TestSharedFileResource.java b/src/test/java/org/caosdb/server/resource/TestSharedFileResource.java
index 8dbbd5b1ec25843c3a5b28d46a2fa784f169330d..bff03a45e908419bbaef3a8988df7d9d382a0e8a 100644
--- a/src/test/java/org/caosdb/server/resource/TestSharedFileResource.java
+++ b/src/test/java/org/caosdb/server/resource/TestSharedFileResource.java
@@ -122,16 +122,10 @@ public class TestSharedFileResource {
 
     provideUserSourcesFile();
     final Subject user = new DelegatingSubject(new DefaultSecurityManager(new AnonymousRealm()));
+    CaosDBServer.setProperty(ServerProperties.KEY_AUTH_OPTIONAL, "true");
     user.login(AnonymousAuthenticationToken.getInstance());
     SharedFileResource resource =
         new SharedFileResource() {
-          // @Override
-          // protected Representation httpGetInChildClass()
-          //     throws ConnectionException, IOException, SQLException, CaosDBException,
-          //         NoSuchAlgorithmException, Exception {
-          //   // TODO Auto-generated method stub
-          //   return super.httpGetInChildClass();
-          // }
 
           @Override
           public String getSRID() {
diff --git a/src/test/java/org/caosdb/server/scripting/TestServerSideScriptingCaller.java b/src/test/java/org/caosdb/server/scripting/TestServerSideScriptingCaller.java
index 61be0c5c3a06319dbeb826a79ddf1c6e43a0d672..45b8e7f4408ca7186fbc2589b176d3c5eba113ba 100644
--- a/src/test/java/org/caosdb/server/scripting/TestServerSideScriptingCaller.java
+++ b/src/test/java/org/caosdb/server/scripting/TestServerSideScriptingCaller.java
@@ -186,7 +186,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
   }
 
   /**
-   * Throw {@link CaosDBException} because tmpIdentifyer is null or empty.
+   * Throw {@link CaosDBException} because tmpIdentifier is null or empty.
    *
    * @throws FileNotFoundException
    * @throws CaosDBException
@@ -222,7 +222,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
 
     final ArrayList<FileProperties> files = new ArrayList<>();
     final FileProperties f = new FileProperties(null, null, null);
-    f.setTmpIdentifyer("a2s3d4f5");
+    f.setTmpIdentifier("a2s3d4f5");
     f.setPath("testfile");
     files.add(f);
 
@@ -247,7 +247,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
 
     final ArrayList<FileProperties> files = new ArrayList<>();
     final FileProperties f = new FileProperties(null, null, null);
-    f.setTmpIdentifyer("a2s3d4f5");
+    f.setTmpIdentifier("a2s3d4f5");
     f.setFile(new File("blablabla_non_existing"));
     f.setPath("bla");
     files.add(f);
@@ -282,7 +282,7 @@ public class TestServerSideScriptingCaller extends CaosDBTestClass {
 
     final ArrayList<FileProperties> files = new ArrayList<>();
     final FileProperties f = new FileProperties(null, null, null);
-    f.setTmpIdentifyer("a2s3d4f5");
+    f.setTmpIdentifier("a2s3d4f5");
     f.setFile(testFile);
     f.setPath("testfile");
     files.add(f);
diff --git a/src/test/java/org/caosdb/server/transaction/RetrieveTest.java b/src/test/java/org/caosdb/server/transaction/RetrieveTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..08c41ba4a2346058d9d2da587dd7c26930ad2896
--- /dev/null
+++ b/src/test/java/org/caosdb/server/transaction/RetrieveTest.java
@@ -0,0 +1,79 @@
+package org.caosdb.server.transaction;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.subject.Subject;
+import org.caosdb.server.CaosDBServer;
+import org.caosdb.server.ServerProperties;
+import org.caosdb.server.accessControl.AnonymousAuthenticationToken;
+import org.caosdb.server.accessControl.Role;
+import org.caosdb.server.database.BackendTransaction;
+import org.caosdb.server.database.access.Access;
+import org.caosdb.server.database.backend.interfaces.RetrieveRoleImpl;
+import org.caosdb.server.database.exceptions.TransactionException;
+import org.caosdb.server.database.misc.TransactionBenchmark;
+import org.caosdb.server.entity.EntityInterface;
+import org.caosdb.server.entity.RetrieveEntity;
+import org.caosdb.server.entity.container.RetrieveContainer;
+import org.caosdb.server.entity.xml.IdAndServerMessagesOnlyStrategy;
+import org.caosdb.server.permissions.EntityACLFactory;
+import org.caosdb.server.utils.EntityStatus;
+import org.caosdb.server.utils.ServerMessages;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class RetrieveTest {
+
+  // @review Florian Spreckelsen 2022-03-22
+  @BeforeClass
+  public static void setup() throws IOException {
+    CaosDBServer.initServerProperties();
+    CaosDBServer.setProperty(ServerProperties.KEY_AUTH_OPTIONAL, "TRUE");
+    CaosDBServer.initShiro();
+
+    BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRoleMockup.class);
+  }
+
+  /** a mock-up which returns null */
+  public static class RetrieveRoleMockup implements RetrieveRoleImpl {
+
+    public RetrieveRoleMockup(Access a) {}
+
+    @Override
+    public void setTransactionBenchmark(TransactionBenchmark b) {}
+
+    @Override
+    public TransactionBenchmark getBenchmark() {
+      return null;
+    }
+
+    @Override
+    public Role retrieve(String role) throws TransactionException {
+      return null;
+    }
+  }
+
+  @Test
+  public void testMissingRetrievePermission() {
+    Subject subject = SecurityUtils.getSubject();
+    subject.login(AnonymousAuthenticationToken.getInstance());
+    EntityInterface entity = new RetrieveEntity(1234);
+    EntityACLFactory fac = new EntityACLFactory();
+    fac.deny(AnonymousAuthenticationToken.PRINCIPAL, "RETRIEVE:ENTITY");
+    entity.setEntityACL(fac.create());
+    RetrieveContainer container = new RetrieveContainer(null, null, null, null);
+    assertTrue(entity.getMessages().isEmpty());
+    assertEquals(entity.getEntityStatus(), EntityStatus.QUALIFIED);
+    container.add(entity);
+    Retrieve retrieve = new Retrieve(container);
+    retrieve.postTransaction();
+    assertFalse(entity.getMessages().isEmpty());
+    assertEquals(entity.getMessages("error").get(0), ServerMessages.AUTHORIZATION_ERROR);
+    assertEquals(entity.getEntityStatus(), EntityStatus.UNQUALIFIED);
+    assertTrue(entity.getSerializeFieldStrategy() instanceof IdAndServerMessagesOnlyStrategy);
+  }
+}
diff --git a/src/test/java/org/caosdb/server/transaction/UpdateTest.java b/src/test/java/org/caosdb/server/transaction/UpdateTest.java
index d22fbea8982b1f8d88c9ccdf069a0c8a816add25..9ae999bbf4be5ed4cfdf3e4ad03e2504cc311b33 100644
--- a/src/test/java/org/caosdb/server/transaction/UpdateTest.java
+++ b/src/test/java/org/caosdb/server/transaction/UpdateTest.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) 2022 Indiscale GmbH <info@indiscale.com>
+ * Copyright (C) 2022 Daniel Hornung <d.hornung@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,34 +18,45 @@
  *
  * 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.transaction;
 
 import static org.caosdb.server.utils.EntityStatus.QUALIFIED;
 import static org.caosdb.server.utils.EntityStatus.VALID;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
 import java.security.NoSuchAlgorithmException;
+import java.util.HashSet;
 import org.caosdb.server.CaosDBException;
+import org.caosdb.server.CaosDBServer;
+import org.caosdb.server.datatype.CollectionValue;
 import org.caosdb.server.datatype.GenericValue;
+import org.caosdb.server.datatype.ReferenceValue;
 import org.caosdb.server.entity.Entity;
 import org.caosdb.server.entity.EntityInterface;
 import org.caosdb.server.entity.StatementStatus;
 import org.caosdb.server.entity.wrapper.Property;
+import org.caosdb.server.permissions.EntityPermission;
+import org.caosdb.server.permissions.Permission;
 import org.caosdb.server.utils.EntityStatus;
+import org.junit.BeforeClass;
 import org.junit.Test;
 
 public class UpdateTest {
 
+  @BeforeClass
+  public static void setup() throws IOException {
+    CaosDBServer.initServerProperties();
+  }
+
   @Test
   public void testDeriveUpdate_SameName()
       throws NoSuchAlgorithmException, IOException, CaosDBException {
     final Entity newEntity = new Entity("Name");
     final Entity oldEntity = new Entity("Name");
-    WriteTransaction.deriveUpdate(newEntity, oldEntity);
+    new WriteTransaction(null).deriveUpdate(newEntity, oldEntity);
     assertEquals(newEntity.getEntityStatus(), EntityStatus.VALID);
   }
 
@@ -53,7 +65,7 @@ public class UpdateTest {
       throws NoSuchAlgorithmException, IOException, CaosDBException {
     final Entity newEntity = new Entity("NewName");
     final Entity oldEntity = new Entity("OldName");
-    WriteTransaction.deriveUpdate(newEntity, oldEntity);
+    new WriteTransaction(null).deriveUpdate(newEntity, oldEntity);
     assertEquals(newEntity.getEntityStatus(), EntityStatus.QUALIFIED);
   }
 
@@ -67,7 +79,7 @@ public class UpdateTest {
     final Entity oldEntity = new Entity();
     oldEntity.addProperty(oldProperty);
 
-    WriteTransaction.deriveUpdate(newEntity, oldEntity);
+    new WriteTransaction(null).deriveUpdate(newEntity, oldEntity);
     assertEquals(newEntity.getEntityStatus(), VALID);
   }
 
@@ -83,7 +95,7 @@ public class UpdateTest {
     final Entity oldEntity = new Entity();
     oldEntity.addProperty(oldProperty);
 
-    WriteTransaction.deriveUpdate(newEntity, oldEntity);
+    new WriteTransaction(null).deriveUpdate(newEntity, oldEntity);
     assertEquals(newEntity.getEntityStatus(), QUALIFIED);
     assertEquals(newProperty.getEntityStatus(), VALID);
     assertEquals(newProperty2.getEntityStatus(), QUALIFIED);
@@ -124,7 +136,7 @@ public class UpdateTest {
 
     oldEntity.addProperty(oldProperty);
 
-    WriteTransaction.deriveUpdate(newEntity, oldEntity);
+    new WriteTransaction(null).deriveUpdate(newEntity, oldEntity);
     assertEquals(newUnit.getEntityStatus(), VALID);
     assertEquals(newProperty.getEntityStatus(), VALID);
     assertEquals(newEntity.getEntityStatus(), VALID);
@@ -165,8 +177,107 @@ public class UpdateTest {
 
     oldEntity.addProperty(oldProperty);
 
-    WriteTransaction.deriveUpdate(newEntity, oldEntity);
+    new WriteTransaction(null).deriveUpdate(newEntity, oldEntity);
+    assertEquals(newEntity.getEntityStatus(), QUALIFIED);
+    assertEquals(newProperty.getEntityStatus(), QUALIFIED);
+  }
+
+  @Test
+  public void testDeriveUpdate_Collections()
+      throws NoSuchAlgorithmException, CaosDBException, IOException {
+
+    final Entity newEntity = new Entity();
+    final Property newProperty = new Property(1);
+    newProperty.setDatatype("List<Person>");
+    CollectionValue newValue = new CollectionValue();
+    newValue.add(new ReferenceValue(1234));
+    newValue.add(null);
+    newValue.add(new GenericValue(2345));
+    newValue.add(new GenericValue(3465));
+    newProperty.setValue(newValue);
+    newEntity.addProperty(newProperty);
+    newEntity.setEntityStatus(QUALIFIED);
+    newProperty.setEntityStatus(QUALIFIED);
+
+    // old entity represents the stored entity.
+    final Entity oldEntity = new Entity();
+    final Property oldProperty = new Property(1);
+    oldProperty.setDatatype("List<Person>");
+    CollectionValue oldValue = new CollectionValue();
+    // Values are shuffled but have the correct index
+    oldValue.add(1, null);
+    oldValue.add(3, new GenericValue(3465));
+    oldValue.add(2, new ReferenceValue(2345));
+    oldValue.add(0, new ReferenceValue(1234));
+    oldProperty.setValue(oldValue);
+    oldEntity.addProperty(oldProperty);
+
+    HashSet<Permission> permissions = new WriteTransaction(null).deriveUpdate(newEntity, oldEntity);
+    // Both have been identified as equals
+    assertTrue(permissions.isEmpty());
+    assertEquals(newEntity.getEntityStatus(), VALID);
+    assertEquals(newProperty.getEntityStatus(), VALID);
+
+    // NEW TEST CASE
+    newValue.add(null); // newValue has another null
+    newProperty.setValue(newValue);
+    oldEntity.addProperty(oldProperty); // Add again, because deriveUpdate throws it away
+    newEntity.setEntityStatus(QUALIFIED);
+    newProperty.setEntityStatus(QUALIFIED);
+
+    HashSet<Permission> permissions2 =
+        new WriteTransaction(null).deriveUpdate(newEntity, oldEntity);
+    HashSet<Permission> expected = new HashSet<Permission>();
+    expected.add(EntityPermission.UPDATE_ADD_PROPERTY);
+    expected.add(EntityPermission.UPDATE_REMOVE_PROPERTY);
+    assertEquals(expected, permissions2);
+    assertEquals(newEntity.getEntityStatus(), QUALIFIED);
+    assertEquals(newProperty.getEntityStatus(), QUALIFIED);
+
+    // NEW TEST CASE
+    // now change the order of oldValue
+    CollectionValue oldValue2 = new CollectionValue();
+    // Values are shuffled but have the correct index
+    oldValue2.add(0, null);
+    oldValue2.add(1, new GenericValue(3465));
+    oldValue2.add(2, new ReferenceValue(2345));
+    oldValue2.add(3, new ReferenceValue(1234));
+    oldValue2.add(4, null);
+    oldProperty.setValue(oldValue2);
+
+    oldEntity.addProperty(oldProperty); // Add again, because deriveUpdate throws it away
+    newEntity.setEntityStatus(QUALIFIED);
+    newProperty.setEntityStatus(QUALIFIED);
+
+    HashSet<Permission> permissions3 =
+        new WriteTransaction(null).deriveUpdate(newEntity, oldEntity);
+    assertEquals(expected, permissions3);
     assertEquals(newEntity.getEntityStatus(), QUALIFIED);
     assertEquals(newProperty.getEntityStatus(), QUALIFIED);
   }
+
+  /** For issue #217: Server gets list property datatype wrong if description is updated. */
+  @Test
+  public void testDeriveUpdate_UpdateList()
+      throws NoSuchAlgorithmException, CaosDBException, IOException {
+    // @review Florian Spreckelsen 2022-03-15
+    final Property oldProperty = new Property(1);
+    final Property newProperty = new Property(1);
+    oldProperty.setDatatype("List<Person>");
+    newProperty.setDatatype("List<Person>");
+    final Entity oldEntity = new Entity();
+    final Entity newEntity = new Entity();
+
+    oldProperty.setRole("Record");
+    newProperty.setRole("Property");
+    oldEntity.addProperty(oldProperty);
+    newEntity.addProperty(newProperty);
+
+    // The only difference between old and new
+    newEntity.setDescription("New description.");
+
+    new WriteTransaction(null).deriveUpdate(newEntity, oldEntity);
+    // check if newEntity's Property VALID.
+    assertEquals(VALID, newEntity.getProperties().get(0).getEntityStatus());
+  }
 }