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()); + } }