diff --git a/.gitignore b/.gitignore index 06c8c148f1e9f45493f574a11c6789398246defd..ab11f8441c1690ede967897c0e64745f9ad30f86 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ log/ OUTBOX ConsistencyTest.xml testlog/ +authtoken/ # python __pycache__ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8587b4072e1d7485406cd432aff9034e6c2e6b84..c1afaf3f2a5274ff9e9a3083dbfd788e629709c0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -60,7 +60,6 @@ test: - make easy-units - mvn antlr4:antlr4 - mvn compile - - echo "defaultRealm = CaosDB" > conf/ext/usersources.ini - mvn test # Deploy: Trigger building of server image and integration tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 88ac0b5297a401ebcc3edda414cce13d3142d3fd..71e19edb1f1069328a768ca05b9d487388f3c4e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Support for deeply nested selectors in SELECT queries. +- One-time Authentication Tokens for login without credentials and login with + particular permissions and roles for the course of the session. - `Entity/names` resource for retrieving all known entity names. - Scripting is simplified by adding a `home` directory, of which a copy is created for each called script and set as the `HOME` environment variable. @@ -19,6 +22,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Script for moving files (change their path) in the internal file system based on a two-column tsv file (with columns "from" and "to"). See [README.md](misc/move_files/README.md). +- LDAP server may now be given and may be different from LDAP domain. See + `misc/pam_authentication/ldap.conf` +- #47 - Sub-properties can now be queried, such as in + `SELECT window.width FROM house`. +- Added support for versioning, if it is enabled on the backend. + ### Changed @@ -28,7 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated -- +- CaosDBTerminal ### Removed @@ -46,6 +55,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 returned without ID but with a notice now. - #11 - pam_authentication leaks the password to unprivileged processes on the same machine. +- #39 - quotes around datetimes in queries +- #99 - Checksum updating resulted in infinite loop on server. ### Security (in case of vulnerabilities) diff --git a/README_CONFIGURATION.md b/README_CONFIGURATION.md index 55b47175762f15266e66bcca8d3b17143baaf578..27045eea4523c5009e246fe506ec6319965d8e9f 100644 --- a/README_CONFIGURATION.md +++ b/README_CONFIGURATION.md @@ -13,3 +13,9 @@ The default configuration can be overriden by in this order. +# One-time Authentication Tokens + +One-time Authentication Tokens can be configure to be issued for special purposes (e.g. a call of a server-side script) or to be written to a file on a regular basis. + +An example of a configuration is located at `./conf/core/authtoken.example.yaml`. + diff --git a/README_SETUP.md b/README_SETUP.md index 19a62bda4144550d18f2cd40ea43bbc8431ca903..554cd5dacbdffb637227e6be2b18ed1dad7877dc 100644 --- a/README_SETUP.md +++ b/README_SETUP.md @@ -12,6 +12,7 @@ * libpam (if PAM authentication is required) * unzip * openpyxl (for XLS/ODS export) +* openssl (if a custom TLS certificate is required) ### Install the requirements on Debian On Debian, the required packages can be installed with: diff --git a/caosdb-webui b/caosdb-webui index 136582641fb1b675d9630b4eacea54fbf7765eea..66026626089e2b514538510a1a6744868f46b661 160000 --- a/caosdb-webui +++ b/caosdb-webui @@ -1 +1 @@ -Subproject commit 136582641fb1b675d9630b4eacea54fbf7765eea +Subproject commit 66026626089e2b514538510a1a6744868f46b661 diff --git a/conf/core/authtoken.example.yaml b/conf/core/authtoken.example.yaml new file mode 100644 index 0000000000000000000000000000000000000000..453630a852642db19cfc824bf660b004e26af5b2 --- /dev/null +++ b/conf/core/authtoken.example.yaml @@ -0,0 +1,94 @@ +# OneTimeAuthenticationToken Config +# +# One-Time Authentication (OTA) Tokens are a means to authenticate a client +# without a password. +# +# This example config file illustrates several use cases of One-Time +# Authentication Tokens. +# +# This yaml file contains an array of configuration objects which may have the +# properties that are defined by the caosdb.server.accessControl.Config class. +# +# These properties are: +# +# - expiresAfter:: An integer timespan in milliseconds after the OTA Token was +# generated by the server when the token expires. So the token will not be +# valid after <creationDate> + <expiresAfter>. +# - expiresAfterSeconds:: A convenient option which has the same meaning as +# expiresAfter but measured in seconds instead of milliseconds. +# - roles:: A list of strings which are the user roles that the client has when +# it authenticates with this token. This is a way to give a client a set of +# roles which it wouldn't have otherwise. Consequently, the client also has +# all permissions of its roles. +# - permissions:: A list of string which are the permissions that the client has +# when it authenticates with this token. This is another way to give a +# client permissions which it wouldn't have otherwise. +# - maxReplays:: Normally a One-Time token can be used exactly once and +# invalidates the moment when the server recognizes that a client uses it. +# However, when a network connection is unreliable, it should be possible to +# attempt another login with the same token for a few times. And in another +# use case, it might be practical to authenticate several clients at the +# same time with the same authentication token. The maxReplays value is the +# number of times that a server accepts an OTA token as valid. The default +# is, of course, 1. +# - replayTimeout:: An integer timespan in milliseconds. Because replays are a +# possible attack vector, the time span in which the same token may be used +# is limited by this value. The default value is configured by the server +# property 'ONE_TIME_TOKEN_REPLAYS_TIMEOUT_MS' which has a default of 30000, +# which is 30 seconds. +# - replayTimeoutSeconds:: A convenient option which has the same meaning as +# replayTimeout but measured in seconds instead of milliseconds. +# - output:: Defines how the OTA token is output by the server. If this property +# is not present the OTA is not output in any way but only used internally +# (see purpose). The 'output' object has the following properties: +# - file:: An absolute or relative path in the server's file system. This +# property means that the OTA is written to that file on server start. +# - schedule:: A string formatted according to the Quartz[1] library +# indicating when the OTA is renewed and the file is being overridden +# with the new OTA. +# - purpose:: A string which is used (only internally so far) to generate an OTA +# for a special purpose, e.g. the execution of a server-side script with +# particular permissions. This way, an otherwise unprivileged client may +# execute a server-side script with all necessary permissions, if the client +# has the "SCRIPTING:EXECUTE:<script-path>" permission and this config file +# has a configuration with "purpose: SCRIPTING:EXECUTE:<script-path>" +# (case-sensitive). +# +# [1] http://www.quartz-scheduler.org/api/2.3.0/org/quartz/CronExpression.html +# +# +# Examples: +# +# 1. Every client with the SCRIPTING:EXECUTE:administration/diagnostics.py +# permission can execute the administration/diagnostics.py script with the +# administration role (and not the client's roles). +- purpose: SCRIPTING:EXECUTE:administration/diagnostics.py + roles: + - administration +# 2. The server writes an OTA token with the administration role to +# "authtoken/admin_token_crud.txt" and refreshes that token every 10 seconds. +- roles: + - administration + output: + file: "authtoken/admin_token_crud.txt" + schedule: "0/10 * * ? * * *" + +# 3. The server writes an OTA token with the administration role to +# "authtoken/admin_token_3_attempts.txt" which can be replayed 3 times in 10 +# seconds. The OTA token is refreshed every 10 seconds. +- roles: + - administration + output: + file: "authtoken/admin_token_3_attempts.txt" + schedule: "0/10 * * ? * * *" + maxReplays: 3 + replayTimeout: 10000 + +# 4. The server writes an OTA token with the administration role to +# "authtoken/admin_token_expired.txt" which expires immediately. Of course +# this is only useful for testing. +- roles: + - administration + output: + file: "authtoken/admin_token_expired.txt" + expiresAfterSeconds: 0 diff --git a/conf/core/cache.ccf b/conf/core/cache.ccf index 821e5d7862efb21e0aa13f8410886c6c14b10a7c..b4e1f93596a6170ef04ec373dc7a8d70e51fedcc 100644 --- a/conf/core/cache.ccf +++ b/conf/core/cache.ccf @@ -28,6 +28,8 @@ jcs.region.BACKEND_JobRules.cacheattributes.MaxObjects=103 jcs.region.BACKEND_SparseEntities jcs.region.BACKEND_SparseEntities.cacheattributes.MaxObjects=1002 +jcs.region.BACKEND_RetrieveFullVersionInfo +jcs.region.BACKEND_RetrieveFullVersionInfo.cacheattributes.MaxObjects=1006 # PAM UserSource Caching: Cached Items expire after 60 seconds if they are not requested (idle) and after 600 seconds max. # PAM_UnixUserGroups diff --git a/conf/core/server.conf b/conf/core/server.conf index 358f6b5c14106b87d7a676e2262474772ae98512..3507886b0475341b06a8d676845e9280c67b991b 100644 --- a/conf/core/server.conf +++ b/conf/core/server.conf @@ -67,7 +67,7 @@ MYSQL_USER_NAME=caosdb # Password for the user MYSQL_USER_PASSWORD=caosdb # Schema of mysql procedures and tables which is required by this CaosDB instance -MYSQL_SCHEMA_VERSION=v3.0.0-rc1 +MYSQL_SCHEMA_VERSION=v3.0.0-rc2 # -------------------------------------------------- @@ -114,10 +114,21 @@ CERTIFICATES_KEY_STORE_PASSWORD= # 10 min SESSION_TIMEOUT_MS=600000 -# Time after which activation tokens for the activation of new users (internal -# user sources) expire. +# Time after which one-time tokens expire. +# This is only a default value. The actual timeout of tokens can be +# configured otherwise, for example in authtoken.yml. # 7days -ACTIVATION_TIMEOUT_MS=604800000 +ONE_TIME_TOKEN_EXPIRES_MS=604800000 + +# Path to config file for one time tokens, for example authtoken.yml. +AUTHTOKEN_CONFIG= + +# Timeout after which a one-time token expires once it has been first consumed, +# regardless of the maximum of replays that are allowed for that token. This is +# only a default value. The actual timeout of tokens can be configured +# otherwise. +# 30 s +ONE_TIME_TOKEN_REPLAYS_TIMEOUT_MS=30000 # The value for the HTTP cache directive "max-age" WEBUI_HTTP_HEADER_CACHE_MAX_AGE=28800 @@ -170,3 +181,4 @@ CHECK_ENTITY_ACL_ROLES_MODE=MUST # part of any Entity ACL. GLOBAL_ENTITY_PERMISSIONS_FILE=./conf/core/global_entity_permissions.xml +ENTITY_VERSIONING_ENABLED=true diff --git a/makefile b/makefile index 55631222cbb0613e6ccd634a609b594a50ff0951..e900742bededb1651c9e64963efe6db01014903e 100644 --- a/makefile +++ b/makefile @@ -47,6 +47,7 @@ run-single: formatting: mvn fmt:format + autopep8 -ari scripting/ # Compile into a standalone jar file jar: easy-units diff --git a/misc/pam_authentication/ldap.conf b/misc/pam_authentication/ldap.conf index 117d1074915e3cacd9a97b83b5a2e83e3c50d451..664dd7c97524242fdb1ea7015bbc0e26c087b062 100644 --- a/misc/pam_authentication/ldap.conf +++ b/misc/pam_authentication/ldap.conf @@ -1,7 +1,8 @@ # This file is sourced by the LDAP authentication script - -# Set the ldap server here. This is also used to generate a fully qualified -# user name: <USER>@$LDAP_SERVER - +# Set the ldap server here. # LDAP_SERVER="example.com" + +# Set the ldap domain here. This is used to generate a fully qualified +# user name: <USER>@$LDAP_DOMAIN +# LDAP_DOMAIN="example.com" diff --git a/misc/pam_authentication/ldap_authentication.sh b/misc/pam_authentication/ldap_authentication.sh index f887bf99f47c827fd712d2189a5ca89ec2981e6c..1b86b8e1783399e2c43b92981a43789accb21e7d 100755 --- a/misc/pam_authentication/ldap_authentication.sh +++ b/misc/pam_authentication/ldap_authentication.sh @@ -35,7 +35,7 @@ exe_dir=$(dirname $0) # If the second argument is empty or "-", take password from stdin, else use the argument as a file. testpw() { - username="${1}@${LDAP_SERVER}" + username="${1}@${LDAP_DOMAIN}" pwfile="$2" pwargs=("-w" "$pwfile") if [[ $pwfile == "-" ]] ; then diff --git a/pom.xml b/pom.xml index c8f79b3dcc43759cc941244283d13b248d811bb8..4e71f97d2b8d89206e148adba67d259b24ade2a6 100644 --- a/pom.xml +++ b/pom.xml @@ -35,13 +35,13 @@ <repositories> <repository> <id>maven-central</id> - <url>http://central.maven.org/maven2/</url> + <url>https://repo1.maven.org/maven2/</url> <name>Maven Central</name> </repository> <repository> <id>maven-restlet</id> <name>Public online Restlet repository</name> - <url>http://maven.restlet.com</url> + <url>https://maven.restlet.com</url> </repository> <repository> <id>local-maven-repo</id> @@ -54,10 +54,20 @@ <artifactId>easy-units</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> + <dependency> + <groupId>org.quartz-scheduler</groupId> + <artifactId>quartz</artifactId> + <version>2.3.2</version> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.dataformat</groupId> + <artifactId>jackson-dataformat-yaml</artifactId> + <version>2.11.0</version> + </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> - <version>1.4.1</version> + <version>1.5.3</version> </dependency> <dependency> <groupId>junit</groupId> @@ -90,7 +100,7 @@ <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> - <version>6.0.6</version> + <version>8.0.19</version> </dependency> <dependency> <groupId>org.xerial</groupId> @@ -140,7 +150,7 @@ <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-jcs-core</artifactId> - <version>2.1</version> + <version>2.2.1</version> </dependency> <dependency> <groupId>org.kohsuke</groupId> diff --git a/scripting/bin/administration/diagnostics.py b/scripting/bin/administration/diagnostics.py new file mode 100755 index 0000000000000000000000000000000000000000..ccecbd583ffdaf5f941a2df873ac10982560af31 --- /dev/null +++ b/scripting/bin/administration/diagnostics.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# ** 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 Timm Fitschen <t.fitschen@indiscale.com> +# Copyright (C) 2020 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 +# 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/>. +# +# ** end header +# +"""diagnostics.py + +A script which returns a json representation of various parameters which might +be interesting for debugging the server-side scripting functionality and which +should not be executable for non-admin users. +""" + +import sys + +TEST_MODULES = [ + "caosdb", + "numpy", + "pandas", + "validate_email" +] + + +def get_files(): + from os import walk + from os.path import join + result = [] + for p, dirs, files in walk("."): + for f in files: + result.append(join(p, f)) + for d in dirs: + result.append(join(p, d)) + return result + + +def get_option(name, default=None): + for arg in sys.argv: + if arg.startswith("--{}=".format(name)): + index = len(name) + 3 + return arg[index:] + return default + + +def get_exit_code(): + return int(get_option("exit", 0)) + + +def get_auth_token(): + return get_option("auth-token") + + +def get_query(): + return get_option("query") + + +def get_caosdb_info(auth_token): + import caosdb as db + result = dict() + result["version"] = db.version.version + + try: + db.configure_connection( + auth_token=auth_token, + password_method="auth_token") + + info = db.Info() + + result["info"] = str(info) + result["username"] = info.user_info.name + result["realm"] = info.user_info.realm + result["roles"] = info.user_info.roles + + # execute a query and return the results + query = get_query() + if query is not None: + query_result = db.execute_query(query) + result["query"] = (query, str(query_result)) + + except Exception as e: + result["exception"] = str(e) + return result + + +def test_imports(modules): + result = dict() + for m in modules: + try: + i = __import__(m) + if hasattr(i, "__version__") and i.__version__ is not None: + v = i.__version__ + else: + v = "unknown version" + result[m] = (True, v) + except ImportError as e: + result[m] = (False, str(e)) + return result + + +def main(): + try: + import json + except ImportError: + print('{{"python_version":"{v}",' + '"python_path":["{p}"]}}'.format(v=sys.version, + p='","'.join(sys.path))) + raise + + try: + diagnostics = dict() + diagnostics["python_version"] = sys.version + diagnostics["python_path"] = sys.path + diagnostics["call"] = sys.argv + diagnostics["import"] = test_imports(TEST_MODULES) + diagnostics["files"] = get_files() + + auth_token = get_auth_token() + diagnostics["auth_token"] = auth_token + + if diagnostics["import"]["caosdb"][0] is True: + diagnostics["caosdb"] = get_caosdb_info(auth_token) + + finally: + json.dump(diagnostics, sys.stdout) + + sys.exit(get_exit_code()) + + +if __name__ == "__main__": + main() diff --git a/scripting/bin/xls_from_csv.py b/scripting/bin/xls_from_csv.py index 38dfbc1c9392ce60d338b102f9b9fd05309919d3..73bb6b0ac23fc2df093bd6797e864aad1f2d5592 100755 --- a/scripting/bin/xls_from_csv.py +++ b/scripting/bin/xls_from_csv.py @@ -93,7 +93,8 @@ def _parse_arguments(): parser.add_argument('-t', '--tempdir', required=False, default=tempdir, help="Temporary dir for saving the result.") parser.add_argument('-a', '--auth-token', required=False, - help="An authentication token (not needed, only for compatibility).") + help=("An authentication token (not needed, only for " + "compatibility).")) parser.add_argument('tsv', help="The tsv file.") return parser.parse_args() @@ -104,5 +105,6 @@ def main(): filename = _write_xls(dataframe, directory=args.tempdir) print(filename) + if __name__ == "__main__": main() diff --git a/src/main/java/caosdb/datetime/UTCDateTime.java b/src/main/java/caosdb/datetime/UTCDateTime.java index 215de67befe403aa6a202ed178244b3de898d23e..db66ef0ad08abf0d52a8f3b1b97c6a20cc613c8a 100644 --- a/src/main/java/caosdb/datetime/UTCDateTime.java +++ b/src/main/java/caosdb/datetime/UTCDateTime.java @@ -296,7 +296,7 @@ public class UTCDateTime implements Interval { throw new NullPointerException("toString method!!!"); } - public static UTCDateTime UTCSeconds(final Long utcseconds, final Integer nanosecond) { + public static UTCDateTime UTCSeconds(final Long utcseconds, final Integer nanoseconds) { if (LEAP_SECONDS.isEmpty()) { initLeapSeconds(); } @@ -310,10 +310,10 @@ public class UTCDateTime implements Interval { if (leapSeconds2 != leapSeconds && LEAP_SECONDS.contains(systemSeconds)) { gc.add(Calendar.SECOND, -1); return new UTCDateTime( - systemSeconds, leapSeconds, nanosecond, new LeapSecondDateTimeStringStrategy(gc, 1)); + systemSeconds, leapSeconds, nanoseconds, new LeapSecondDateTimeStringStrategy(gc, 1)); } else { return new UTCDateTime( - systemSeconds, leapSeconds, nanosecond, new GregorianCalendarDateTimeStringStrategy(gc)); + systemSeconds, leapSeconds, nanoseconds, new GregorianCalendarDateTimeStringStrategy(gc)); } } diff --git a/src/main/java/caosdb/server/CaosAuthenticator.java b/src/main/java/caosdb/server/CaosAuthenticator.java index 25f112aa52050a0b872b2ff83e95400ad4bd925e..8fefee3b453dabc0c613228e2495acacb19deaa0 100644 --- a/src/main/java/caosdb/server/CaosAuthenticator.java +++ b/src/main/java/caosdb/server/CaosAuthenticator.java @@ -22,12 +22,13 @@ */ package caosdb.server; +import caosdb.server.accessControl.AnonymousAuthenticationToken; import caosdb.server.accessControl.AuthenticationUtils; -import caosdb.server.accessControl.OneTimeAuthenticationToken; -import caosdb.server.accessControl.SessionToken; import caosdb.server.resource.DefaultResource; +import caosdb.server.utils.ServerMessages; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.subject.Subject; import org.restlet.Context; import org.restlet.Request; @@ -48,50 +49,26 @@ public class CaosAuthenticator extends Authenticator { protected boolean authenticate(final Request request, final Response response) { final Subject subject = SecurityUtils.getSubject(); - return attemptOneTimeTokenLogin(subject, request) || attemptSessionValidation(subject, request); + return attemptSessionValidation(subject, request); } private static boolean attemptSessionValidation(final Subject subject, final Request request) { try { - final SessionToken sessionToken = + final AuthenticationToken sessionToken = AuthenticationUtils.parseSessionTokenCookie( - request.getCookies().getFirst(AuthenticationUtils.SESSION_TOKEN_COOKIE), null); + request.getCookies().getFirst(AuthenticationUtils.SESSION_TOKEN_COOKIE)); if (sessionToken != null) { subject.login(sessionToken); } - } catch (AuthenticationException e) { - logger.info("LOGIN_FAILED", e); - } - // anonymous users - if (!subject.isAuthenticated() - && CaosDBServer.getServerProperty(ServerProperties.KEY_AUTH_OPTIONAL) - .equalsIgnoreCase("TRUE")) { - subject.login(AuthenticationUtils.ANONYMOUS_USER); - } - return subject.isAuthenticated(); - } - private static boolean attemptOneTimeTokenLogin(final Subject subject, final Request request) { - try { - OneTimeAuthenticationToken oneTimeToken = null; - - // try and parse from the query segment of the uri - oneTimeToken = - AuthenticationUtils.parseOneTimeTokenQuerySegment( - request.getResourceRef().getQueryAsForm().getFirstValue("AuthToken"), null); - - // try and parse from cookie - if (oneTimeToken == null) { - oneTimeToken = - AuthenticationUtils.parseOneTimeTokenCookie( - request.getCookies().getFirst(AuthenticationUtils.ONE_TIME_TOKEN_COOKIE), null); + // anonymous users + if (!subject.isAuthenticated() + && CaosDBServer.getServerProperty(ServerProperties.KEY_AUTH_OPTIONAL) + .equalsIgnoreCase("TRUE")) { + subject.login(AnonymousAuthenticationToken.getInstance()); } - - if (oneTimeToken != null) { - subject.login(oneTimeToken); - } - } catch (final AuthenticationException e) { + } catch (AuthenticationException e) { logger.info("LOGIN_FAILED", e); } return subject.isAuthenticated(); @@ -100,7 +77,7 @@ public class CaosAuthenticator extends Authenticator { @Override protected int unauthenticated(final Request request, final Response response) { final DefaultResource defaultResource = - new DefaultResource(AuthenticationUtils.UNAUTHENTICATED.toElement()); + new DefaultResource(ServerMessages.UNAUTHENTICATED.toElement()); defaultResource.init(getContext(), request, response); defaultResource.handle(); response.setStatus(org.restlet.data.Status.CLIENT_ERROR_UNAUTHORIZED); diff --git a/src/main/java/caosdb/server/CaosDBServer.java b/src/main/java/caosdb/server/CaosDBServer.java index 34bfe3e2911955fa724ea5e6ac7258d80f9cc120..cdee262f08836071d19e5a75038f4291a917b1ff 100644 --- a/src/main/java/caosdb/server/CaosDBServer.java +++ b/src/main/java/caosdb/server/CaosDBServer.java @@ -19,12 +19,13 @@ */ package caosdb.server; +import caosdb.server.accessControl.AnonymousAuthenticationToken; import caosdb.server.accessControl.AnonymousRealm; import caosdb.server.accessControl.AuthenticationUtils; import caosdb.server.accessControl.CaosDBAuthorizingRealm; import caosdb.server.accessControl.CaosDBDefaultRealm; -import caosdb.server.accessControl.OneTimeTokenRealm; -import caosdb.server.accessControl.Principal; +import caosdb.server.accessControl.ConsumedInfoCleanupJob; +import caosdb.server.accessControl.OneTimeAuthenticationToken; import caosdb.server.accessControl.SessionToken; import caosdb.server.accessControl.SessionTokenRealm; import caosdb.server.database.BackendTransaction; @@ -63,7 +64,6 @@ import caosdb.server.transaction.ChecksumUpdater; import caosdb.server.utils.FileUtils; import caosdb.server.utils.Initialization; import caosdb.server.utils.NullPrintStream; -import caosdb.server.utils.Utils; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; @@ -74,17 +74,22 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Properties; import java.util.TimeZone; +import java.util.UUID; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import org.apache.shiro.SecurityUtils; import org.apache.shiro.config.Ini; import org.apache.shiro.config.Ini.Section; -import org.apache.shiro.config.IniSecurityManagerFactory; +import org.apache.shiro.env.BasicIniEnvironment; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; -import org.apache.shiro.util.Factory; import org.apache.shiro.util.ThreadContext; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.impl.StdSchedulerFactory; import org.restlet.Application; import org.restlet.Component; import org.restlet.Context; @@ -118,11 +123,68 @@ public class CaosDBServer extends Application { private static ArrayList<Runnable> preShutdownHooks = new ArrayList<Runnable>(); private static boolean START_BACKEND = true; private static boolean INSECURE = false; + public static final String REQUEST_TIME_LOGGER = "REQUEST_TIME_LOGGER"; + public static final String REQUEST_ERRORS_LOGGER = "REQUEST_ERRORS_LOGGER"; + private static Scheduler SCHEDULER; public static String getServerProperty(final String key) { return getServerProperties().getProperty(key); } + /** + * This main method starts up a web application that will listen on a port defined in the config + * file. + * + * @param args One option temporarily (for testing) available: silent: If present: disable + * System.out-stream (stream to a NullPrintStream). This makes the response of the database + * amazingly faster. + * @throws IOException + * @throws FileNotFoundException + * @throws SecurityException + * @throws Exception If problems occur. + */ + public static void main(final String[] args) + throws SecurityException, FileNotFoundException, IOException { + try { + init(args); + initScheduler(); + initServerProperties(); + initTimeZone(); + initOneTimeTokens(); + initShiro(); + initBackend(); + initWebServer(); + initShutDownHook(); + } catch (Exception e1) { + logger.error("Could not start the server.", e1); + System.exit(1); + } + } + + private static void init(final String[] args) { + // Important change: + // Make silent the default option + START_GUI = false; + for (final String s : args) { + if (s.equals("silent")) { + START_GUI = false; + } else if (s.equals("gui")) { + START_GUI = true; + } else if (s.equals("nobackend")) { + START_BACKEND = false; + } else if (s.equals("insecure")) { + INSECURE = true; + } + } + INSECURE = INSECURE && isDebugMode(); // only allow insecure in debug mode + START_BACKEND = START_BACKEND || !isDebugMode(); // always start backend if not in debug mode + } + + private static void initScheduler() throws SchedulerException { + SCHEDULER = StdSchedulerFactory.getDefaultScheduler(); + SCHEDULER.start(); + } + public static void initServerProperties() throws IOException { SERVER_PROPERTIES = ServerProperties.initServerProperties(); } @@ -206,21 +268,15 @@ public class CaosDBServer extends Application { } } - private static void init(final String[] args) { - // Important change: - // Make silent the default option - START_GUI = false; - for (final String s : args) { - if (s.equals("silent")) { - START_GUI = false; - } else if (s.equals("gui")) { - START_GUI = true; - } else if (s.equals("nobackend")) { - START_BACKEND = false; - } else if (s.equals("insecure")) { - INSECURE = true; - } - } + public static void initOneTimeTokens() throws Exception { + OneTimeAuthenticationToken.initConfig(); + ConsumedInfoCleanupJob.scheduleDaily(); + } + + public static void initShiro() { + // init Shiro (user authentication/authorization and session management) + final Ini config = getShiroConfig(); + initShiro(config); } public static Ini getShiroConfig() { @@ -228,12 +284,11 @@ public class CaosDBServer extends Application { final Section mainSec = config.addSection("main"); mainSec.put("CaosDB", CaosDBDefaultRealm.class.getCanonicalName()); mainSec.put("SessionTokenValidator", SessionTokenRealm.class.getCanonicalName()); - mainSec.put("OneTimeTokenValidator", OneTimeTokenRealm.class.getCanonicalName()); mainSec.put("CaosDBAuthorizingRealm", CaosDBAuthorizingRealm.class.getCanonicalName()); mainSec.put("AnonymousRealm", AnonymousRealm.class.getCanonicalName()); mainSec.put( "securityManager.realms", - "$CaosDB, $SessionTokenValidator, $OneTimeTokenValidator, $CaosDBAuthorizingRealm, $AnonymousRealm"); + "$CaosDB, $SessionTokenValidator, $CaosDBAuthorizingRealm, $AnonymousRealm"); // disable shiro's default session management. We have quasi-stateless // sessions @@ -243,43 +298,15 @@ public class CaosDBServer extends Application { return config; } - /** - * This main method starts up a web application that will listen on a port defined in the config - * file. - * - * @param args One option temporarily (for testing) available: silent: If present: disable - * System.out-stream (stream to a NullPrintStream). This makes the response of the database - * amazingly faster. - * @throws IOException - * @throws FileNotFoundException - * @throws SecurityException - * @throws Exception If problems occur. - */ - public static void main(final String[] args) - throws SecurityException, FileNotFoundException, IOException { - try { - init(args); - initServerProperties(); - initTimeZone(); - } catch (IOException | InterruptedException e1) { - logger.error("Could not configure the server.", e1); - System.exit(1); - } - - INSECURE = INSECURE && isDebugMode(); // only allow insecure in debug mode - START_BACKEND = START_BACKEND || !isDebugMode(); // always start backend if - // not in debug mode - - // init Shiro (user authentication/authorization and session management) - final Ini config = getShiroConfig(); - final Factory<SecurityManager> factory = new IniSecurityManagerFactory(config); - final SecurityManager securityManager = factory.getInstance(); + public static void initShiro(Ini config) { + BasicIniEnvironment env = new BasicIniEnvironment(config); + final SecurityManager securityManager = env.getSecurityManager(); SecurityUtils.setSecurityManager(securityManager); + } - final Initialization init = Initialization.setUp(); - try { - // init backend - if (START_BACKEND) { + public static void initBackend() throws Exception { + if (START_BACKEND) { + try (final Initialization init = Initialization.setUp()) { BackendTransaction.init(); // init benchmark @@ -296,65 +323,60 @@ public class CaosDBServer extends Application { // ChecksumUpdater ChecksumUpdater.start(); - } else { - logger.info("NO BACKEND"); } + } else { + logger.info("NO BACKEND"); + } + } - // GUI - if (START_GUI) { - final CaosDBTerminal caosDBTerminal = new CaosDBTerminal(); - caosDBTerminal.setName("CaosDBTerminal"); - caosDBTerminal.start(); + private static void initWebServer() throws Exception { + final int port_https = + Integer.parseInt(getServerProperty(ServerProperties.KEY_SERVER_PORT_HTTPS)); + final int port_http = + Integer.parseInt(getServerProperty(ServerProperties.KEY_SERVER_PORT_HTTP)); + int port_redirect_https; + try { + port_redirect_https = + Integer.parseInt(getServerProperty(ServerProperties.KEY_REDIRECT_HTTP_TO_HTTPS_PORT)); + } catch (NumberFormatException e) { + port_redirect_https = port_https; + } + final int initialConnections = + Integer.parseInt(getServerProperty(ServerProperties.KEY_INITIAL_CONNECTIONS)); + final int maxTotalConnections = + Integer.parseInt(getServerProperty(ServerProperties.KEY_MAX_CONNECTIONS)); - addPreShutdownHook( - new Runnable() { + if (INSECURE) { + runHTTPServer(port_http, initialConnections, maxTotalConnections); + } else { + runHTTPSServer( + port_https, port_http, port_redirect_https, initialConnections, maxTotalConnections); + } + } - @Override - public void run() { - caosDBTerminal.shutDown(); - SystemErrPanel.close(); - } - }); - // wait until the terminal is initialized. - Thread.sleep(1000); - - // add Benchmark - StatsPanel.addStat("TransactionBenchmark", TransactionBenchmark.getRootInstance()); - } else { - logger.info("NO GUI"); - System.setOut(new NullPrintStream()); - } + public static void initGUI() throws InterruptedException { + if (START_GUI) { + final CaosDBTerminal caosDBTerminal = new CaosDBTerminal(); + caosDBTerminal.setName("CaosDBTerminal"); + caosDBTerminal.start(); - // Web server properties - final int port_https = - Integer.parseInt(getServerProperty(ServerProperties.KEY_SERVER_PORT_HTTPS)); - final int port_http = - Integer.parseInt(getServerProperty(ServerProperties.KEY_SERVER_PORT_HTTP)); - int port_redirect_https; - try { - port_redirect_https = - Integer.parseInt(getServerProperty(ServerProperties.KEY_REDIRECT_HTTP_TO_HTTPS_PORT)); - } catch (NumberFormatException e) { - port_redirect_https = port_https; - } - final int initialConnections = - Integer.parseInt(getServerProperty(ServerProperties.KEY_INITIAL_CONNECTIONS)); - final int maxTotalConnections = - Integer.parseInt(getServerProperty(ServerProperties.KEY_MAX_CONNECTIONS)); - - init.release(); - - if (INSECURE) { - runHTTPServer(port_http, initialConnections, maxTotalConnections); - } else { - runHTTPSServer( - port_https, port_http, port_redirect_https, initialConnections, maxTotalConnections); - } - initShutDownHook(); - } catch (final Exception e) { - logger.error("Server start failed.", e); - init.release(); - System.exit(1); + addPreShutdownHook( + new Runnable() { + + @Override + public void run() { + caosDBTerminal.shutDown(); + SystemErrPanel.close(); + } + }); + // wait until the terminal is initialized. + Thread.sleep(1000); + + // add Benchmark + StatsPanel.addStat("TransactionBenchmark", TransactionBenchmark.getRootInstance()); + } else { + logger.info("NO GUI"); + System.setOut(new NullPrintStream()); } } @@ -508,9 +530,6 @@ public class CaosDBServer extends Application { } } - public static final String REQUEST_TIME_LOGGER = "REQUEST_TIME_LOGGER"; - public static final String REQUEST_ERRORS_LOGGER = "REQUEST_ERRORS_LOGGER"; - /** * Specify the dispatching restlet that maps URIs to their associated resources for processing. * @@ -560,10 +579,10 @@ public class CaosDBServer extends Application { private void setSessionCookies(final Response response) { final Subject subject = SecurityUtils.getSubject(); + // if authenticated as a normal user: generate and set session cookie. if (subject.isAuthenticated() - && subject.getPrincipal() != AuthenticationUtils.ANONYMOUS_USER.getPrincipal()) { - final SessionToken sessionToken = - SessionToken.generate((Principal) subject.getPrincipal(), null); + && subject.getPrincipal() != AnonymousAuthenticationToken.PRINCIPAL) { + final SessionToken sessionToken = SessionToken.generate(subject); // set session token cookie (httpOnly, secure cookie which // is used to recognize a user session) @@ -823,6 +842,10 @@ public class CaosDBServer extends Application { public static Properties getServerProperties() { return SERVER_PROPERTIES; } + + public static void scheduleJob(JobDetail job, Trigger trigger) throws SchedulerException { + SCHEDULER.scheduleJob(job, trigger); + } } class CaosDBComponent extends Component { @@ -852,7 +875,7 @@ class CaosDBComponent extends Component { public void handle(final Request request, final Response response) { long t1 = System.currentTimeMillis(); // The server request ID is just a long random number - request.getAttributes().put("SRID", Utils.getUID()); + request.getAttributes().put("SRID", UUID.randomUUID().toString()); response.setServerInfo(CaosDBServer.getServerInfo()); super.handle(request, response); log(request, response, t1); diff --git a/src/main/java/caosdb/server/ServerProperties.java b/src/main/java/caosdb/server/ServerProperties.java index c1dd59f3902601ea90c021a4a7b723723d1b0670..29ba470ddbcad0e79db210917234562c4b58620a 100644 --- a/src/main/java/caosdb/server/ServerProperties.java +++ b/src/main/java/caosdb/server/ServerProperties.java @@ -88,7 +88,9 @@ public class ServerProperties extends Properties { public static final String KEY_BUGTRACKER_URI = "BUGTRACKER_URI"; public static final String KEY_SESSION_TIMEOUT_MS = "SESSION_TIMEOUT_MS"; - public static final String KEY_ACTIVATION_TIMEOUT_MS = "ACTIVATION_TIMEOUT_MS"; + public static final String KEY_ONE_TIME_TOKEN_EXPIRES_MS = "ONE_TIME_TOKEN_EXPIRES_MS"; + public static final String KEY_ONE_TIME_TOKEN_REPLAYS_TIMEOUT_MS = + "ONE_TIME_TOKEN_REPLAYS_TIMEOUT_MS"; public static final String KEY_CACHE_CONF_LOC = "CACHE_CONF_LOC"; public static final String KEY_CACHE_DISABLE = "CACHE_DISABLE"; @@ -129,6 +131,7 @@ public class ServerProperties extends Properties { public static final String KEY_TIMEZONE = "TIMEZONE"; public static final String KEY_WEBUI_HTTP_HEADER_CACHE_MAX_AGE = "WEBUI_HTTP_HEADER_CACHE_MAX_AGE"; + public static final String KEY_AUTHTOKEN_CONFIG = "AUTHTOKEN_CONFIG"; /** * Read the config files and initialize the server properties. diff --git a/src/main/java/caosdb/server/accessControl/AnonymousAuthenticationToken.java b/src/main/java/caosdb/server/accessControl/AnonymousAuthenticationToken.java index cd3f86f61eb66759b3eb7d0c91c29dc23637000b..f3f62af2af319d342977a7fe2524b353f8e7adab 100644 --- a/src/main/java/caosdb/server/accessControl/AnonymousAuthenticationToken.java +++ b/src/main/java/caosdb/server/accessControl/AnonymousAuthenticationToken.java @@ -28,7 +28,7 @@ public class AnonymousAuthenticationToken implements AuthenticationToken { private static final long serialVersionUID = 1424325396819592888L; private static final AnonymousAuthenticationToken INSTANCE = new AnonymousAuthenticationToken(); - public static final Object PRINCIPAL = new Object(); + public static final Principal PRINCIPAL = new Principal("anonymous", "anonymous"); private AnonymousAuthenticationToken() {} @@ -37,7 +37,7 @@ public class AnonymousAuthenticationToken implements AuthenticationToken { } @Override - public Object getPrincipal() { + public Principal getPrincipal() { return PRINCIPAL; } diff --git a/src/main/java/caosdb/server/accessControl/AuthenticationUtils.java b/src/main/java/caosdb/server/accessControl/AuthenticationUtils.java index 6e0fd5370ffcc2435067d68b3f2f810819ae9fbb..c3576031da2d9a598bd74a07fe12df62ed7de90d 100644 --- a/src/main/java/caosdb/server/accessControl/AuthenticationUtils.java +++ b/src/main/java/caosdb/server/accessControl/AuthenticationUtils.java @@ -4,6 +4,8 @@ * * 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> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -26,19 +28,15 @@ import static caosdb.server.utils.Utils.URLDecodeWithUTF8; import caosdb.server.CaosDBServer; import caosdb.server.ServerProperties; -import caosdb.server.entity.Message; -import caosdb.server.entity.Message.MessageType; import caosdb.server.permissions.ResponsibleAgent; import caosdb.server.permissions.Role; import caosdb.server.utils.Utils; import java.sql.Timestamp; import java.util.Collection; import java.util.LinkedList; -import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.subject.Subject; import org.restlet.data.Cookie; import org.restlet.data.CookieSetting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Useful static methods, mainly for parsing and serializing SessionTokens by the means of web @@ -48,16 +46,16 @@ import org.slf4j.LoggerFactory; */ public class AuthenticationUtils { - private static final Logger logger = LoggerFactory.getLogger(AuthenticationUtils.class); - - public static final String ONE_TIME_TOKEN_COOKIE = "OneTimeToken"; - public static final Message UNAUTHENTICATED = - new Message(MessageType.Error, 401, "Sign up, please!"); public static final String SESSION_TOKEN_COOKIE = "SessionToken"; public static final String SESSION_TIMEOUT_COOKIE = "SessionTimeOut"; - public static final AuthenticationToken ANONYMOUS_USER = - AnonymousAuthenticationToken.getInstance(); + public static boolean isAnonymous(Subject user) { + return AnonymousAuthenticationToken.PRINCIPAL.equals(user.getPrincipal()); + } + + public static boolean isAnonymous(Principal principal) { + return AnonymousAuthenticationToken.PRINCIPAL.equals(principal); + } /** * Create a cookie for a {@link SelfValidatingAuthenticationToken}. Returns null if the parameter @@ -87,11 +85,8 @@ public class AuthenticationUtils { return null; } - public static CookieSetting createOneTimeTokenCookie(final OneTimeAuthenticationToken token) { - return createTokenCookie(AuthenticationUtils.ONE_TIME_TOKEN_COOKIE, token); - } - - public static CookieSetting createSessionTokenCookie(final SessionToken token) { + public static CookieSetting createSessionTokenCookie( + final SelfValidatingAuthenticationToken token) { return createTokenCookie(AuthenticationUtils.SESSION_TOKEN_COOKIE, token); } @@ -103,48 +98,16 @@ public class AuthenticationUtils { * @return A new SessionToken * @see {@link AuthenticationUtils#createSessionTokenCookie(SessionToken)}, {@link SessionToken} */ - public static SessionToken parseSessionTokenCookie(final Cookie cookie, final String curry) { + public static SelfValidatingAuthenticationToken parseSessionTokenCookie(final Cookie cookie) { if (cookie != null) { final String tokenString = URLDecodeWithUTF8(cookie.getValue()); if (tokenString != null && !tokenString.equals("")) { - try { - return SessionToken.parse(tokenString, curry); - } catch (final Exception e) { - logger.warn("AUTHTOKEN_PARSING_FAILED", e); - } + return SelfValidatingAuthenticationToken.parse(tokenString); } } return null; } - private static OneTimeAuthenticationToken parseOneTimeToken( - final String tokenString, final String curry) { - if (tokenString != null && !tokenString.equals("")) { - try { - return OneTimeAuthenticationToken.parse(tokenString, curry); - } catch (final Exception e) { - logger.warn("AUTHTOKEN_PARSING_FAILED", e); - } - } - return null; - } - - public static OneTimeAuthenticationToken parseOneTimeTokenQuerySegment( - final String tokenString, final String curry) { - if (tokenString != null) { - return parseOneTimeToken(URLDecodeWithUTF8(tokenString), curry); - } - return null; - } - - public static OneTimeAuthenticationToken parseOneTimeTokenCookie( - final Cookie cookie, final String curry) { - if (cookie != null) { - return parseOneTimeToken(URLDecodeWithUTF8(cookie.getValue()), curry); - } - return null; - } - /** * Create a session timeout cookie. The value is a plain UTC timestamp which tells the user how * long his session will stay active. This cookie will be ignored by the server and carries only @@ -163,9 +126,7 @@ public class AuthenticationUtils { if (token != null && token.isValid()) { t = new Timestamp(token.getExpires()).toString().replaceFirst(" ", "T"); - exp_in_sec = (int) Math.ceil(token.getTimeout() / 1000.0); // new - // expiration - // time. + exp_in_sec = (int) Math.ceil(token.getTimeout() / 1000.0); // new expiration time return new CookieSetting( 0, AuthenticationUtils.SESSION_TIMEOUT_COOKIE, @@ -180,6 +141,7 @@ public class AuthenticationUtils { return null; } + // TODO move public static boolean isResponsibleAgentExistent(final ResponsibleAgent agent) { // 1) check OWNER, OTHER if (Role.OTHER_ROLE.equals(agent) || Role.OWNER_ROLE.equals(agent)) { @@ -227,4 +189,14 @@ public class AuthenticationUtils { false, false); } + + public static Collection<String> getRoles(Subject user) { + return new CaosDBAuthorizingRealm().doGetAuthorizationInfo(user.getPrincipals()).getRoles(); + } + + public static boolean isFromOneTimeTokenRealm(Subject subject) { + return ((Principal) subject.getPrincipal()) + .getRealm() + .equals(OneTimeAuthenticationToken.REALM_NAME); + } } diff --git a/src/main/java/caosdb/server/accessControl/CaosDBAuthorizingRealm.java b/src/main/java/caosdb/server/accessControl/CaosDBAuthorizingRealm.java index 5cfa425ac235405a0c861e54c9d97ae8ffab58f5..abd52d9ed306f43c51d8bf52bdf1c25d86d8454e 100644 --- a/src/main/java/caosdb/server/accessControl/CaosDBAuthorizingRealm.java +++ b/src/main/java/caosdb/server/accessControl/CaosDBAuthorizingRealm.java @@ -22,9 +22,7 @@ */ package caosdb.server.accessControl; -import com.google.common.base.Objects; -import java.util.Arrays; -import java.util.List; +import java.util.Collection; import java.util.Set; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; @@ -32,100 +30,47 @@ import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; -import org.apache.shiro.subject.SimplePrincipalCollection; public class CaosDBAuthorizingRealm extends AuthorizingRealm { - private static class PermissionPrincipalCollection extends SimplePrincipalCollection { - - private final List<String> permissions; - - public PermissionPrincipalCollection( - final PrincipalCollection principals, final String[] permissions) { - super(principals); - this.permissions = Arrays.asList(permissions); - } - - private static final long serialVersionUID = 5585425107072564933L; - - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } else if (obj instanceof PermissionPrincipalCollection) { - final PermissionPrincipalCollection that = (PermissionPrincipalCollection) obj; - return Objects.equal(that.permissions, this.permissions) && super.equals(that); - } else { - return false; - } - } + private static final CaosDBRolePermissionResolver role_permission_resolver = + new CaosDBRolePermissionResolver(); - @Override - public int hashCode() { - return super.hashCode() + 28 * this.permissions.hashCode(); - } + public Collection<String> getSessionRoles(SelfValidatingAuthenticationToken token) { + return token.getRoles(); } - static class PermissionAuthenticationInfo implements AuthenticationInfo { - - private static final long serialVersionUID = -3714484164124767976L; - private final PermissionPrincipalCollection principalCollection; - - public PermissionAuthenticationInfo( - final PrincipalCollection principals, final String... permissions) { - this.principalCollection = new PermissionPrincipalCollection(principals, permissions); - } + public Collection<String> getSessionPermissions(SelfValidatingAuthenticationToken token) { + return token.getPermissions(); + } - @Override - public PrincipalCollection getPrincipals() { - return this.principalCollection; - } + @Override + protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) { + final SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo(); + Object principal = principals.getPrimaryPrincipal(); - @Override - public Object getCredentials() { - return null; - } + // Add explicitly given roles and permissions. + if (principal instanceof SelfValidatingAuthenticationToken) { + Collection<String> sessionPermissions = + getSessionPermissions((SelfValidatingAuthenticationToken) principal); - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } else if (obj instanceof PermissionAuthenticationInfo) { - final PermissionAuthenticationInfo that = (PermissionAuthenticationInfo) obj; - return Objects.equal(that.principalCollection, this.principalCollection); - } else { - return false; - } - } + Collection<String> sessionRoles = + getSessionRoles((SelfValidatingAuthenticationToken) principal); - @Override - public int hashCode() { - return this.principalCollection.hashCode(); + authzInfo.addRoles(sessionRoles); + authzInfo.addStringPermissions(sessionPermissions); } - } - - private static final CaosDBRolePermissionResolver role_permission_resolver = - new CaosDBRolePermissionResolver(); - @Override - protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) { - if (principals instanceof PermissionPrincipalCollection) { - // the PrincialsCollection carries the permissions. - final SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); - info.addStringPermissions(((PermissionPrincipalCollection) principals).permissions); - return info; + // Find all roles which are associated with this principal in this realm. + final Set<String> principalRoles = + UserSources.resolve((Principal) principals.getPrimaryPrincipal()); + if (principalRoles != null) { + authzInfo.addRoles(principalRoles); } - final SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo(); - - // find all roles which are associated with this principal in this - // realm. - final Set<String> roles = UserSources.resolve(principals); - if (roles != null) { - authzInfo.setRoles(roles); - - // find all permissions which are associated with these roles. - authzInfo.addObjectPermission(role_permission_resolver.resolvePermissionsInRole(roles)); + if (authzInfo.getRoles() != null && !authzInfo.getRoles().isEmpty()) { + authzInfo.addObjectPermission( + role_permission_resolver.resolvePermissionsInRole(authzInfo.getRoles())); } return authzInfo; diff --git a/src/main/java/caosdb/server/accessControl/CaosDBRolePermissionResolver.java b/src/main/java/caosdb/server/accessControl/CaosDBRolePermissionResolver.java index 3350a68700f108e8ed5cf7309e9f305ac0bb669b..ae4c38605f07f5222499e076def3a3d48c31a385 100644 --- a/src/main/java/caosdb/server/accessControl/CaosDBRolePermissionResolver.java +++ b/src/main/java/caosdb/server/accessControl/CaosDBRolePermissionResolver.java @@ -33,6 +33,7 @@ import org.apache.shiro.authc.AuthenticationException; public class CaosDBRolePermissionResolver { + /** Return CaosPermission with the rules which are associated with the roles. */ public CaosPermission resolvePermissionsInRole(final Set<String> roles) { final HashSet<PermissionRule> rules = new HashSet<PermissionRule>(); for (final String role : roles) { diff --git a/src/main/java/caosdb/server/accessControl/Config.java b/src/main/java/caosdb/server/accessControl/Config.java new file mode 100644 index 0000000000000000000000000000000000000000..3da7a426a9dcecfb93264cb9e504a157b10659c8 --- /dev/null +++ b/src/main/java/caosdb/server/accessControl/Config.java @@ -0,0 +1,86 @@ +package caosdb.server.accessControl; + +public class Config { + private String[] permissions = {}; + private String[] roles = {}; + private String purpose = null; + private OneTimeTokenToFile output = null; + private int maxReplays = 1; + private long expiresAfter = OneTimeAuthenticationToken.DEFAULT_TIMEOUT_MS; + private long replayTimeout = OneTimeAuthenticationToken.DEFAULT_REPLAYS_TIMEOUT_MS; + private String name = AnonymousAuthenticationToken.PRINCIPAL.getUsername(); + + public Config() {} + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getExpiresAfter() { + return expiresAfter; + } + + public void setExpiresAfter(long timeout) { + this.expiresAfter = timeout; + } + + public void setReplayTimeoutSeconds(long seconds) { + this.setReplayTimeout(seconds * 1000); + } + + public void setExpiresAfterSeconds(long seconds) { + this.setExpiresAfter(seconds * 1000); + } + + public void setMaxReplays(int maxReplays) { + this.maxReplays = maxReplays; + } + + public int getMaxReplays() { + return maxReplays; + } + + public String[] getPermissions() { + return permissions; + } + + public String getPurpose() { + return purpose; + } + + public void setPermissions(String[] permissions) { + this.permissions = permissions; + } + + public String[] getRoles() { + return roles; + } + + public void setRoles(String[] roles) { + this.roles = roles; + } + + public void setPurpose(String purpose) { + this.purpose = purpose; + } + + public OneTimeTokenToFile getOutput() { + return output; + } + + public void setOutput(OneTimeTokenToFile output) { + this.output = output; + } + + public long getReplayTimeout() { + return replayTimeout; + } + + public void setReplayTimeout(long replaysTimeout) { + this.replayTimeout = replaysTimeout; + } +} diff --git a/src/main/java/caosdb/server/accessControl/ConsumedInfoCleanupJob.java b/src/main/java/caosdb/server/accessControl/ConsumedInfoCleanupJob.java new file mode 100644 index 0000000000000000000000000000000000000000..1f62fa0a8fc72604d885c24123c3d0c17cb49a0a --- /dev/null +++ b/src/main/java/caosdb/server/accessControl/ConsumedInfoCleanupJob.java @@ -0,0 +1,29 @@ +package caosdb.server.accessControl; + +import caosdb.server.CaosDBServer; +import org.quartz.CronScheduleBuilder; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; + +public class ConsumedInfoCleanupJob implements Job { + + public static void scheduleDaily() throws SchedulerException { + JobDetail job = JobBuilder.newJob(ConsumedInfoCleanupJob.class).build(); + Trigger trigger = + TriggerBuilder.newTrigger() + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(0, 0)) + .build(); + CaosDBServer.scheduleJob(job, trigger); + } + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + OneTimeTokenConsumedInfo.cleanupConsumedInfo(); + } +} diff --git a/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java b/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java index ba7dff8d50bd5f77ae4dc035e07caef48d6f3426..8dddf77b092cb951695ecaa15191856a3909793f 100644 --- a/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java +++ b/src/main/java/caosdb/server/accessControl/OneTimeAuthenticationToken.java @@ -4,6 +4,8 @@ * * 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> * * 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,112 +26,240 @@ package caosdb.server.accessControl; import caosdb.server.CaosDBServer; import caosdb.server.ServerProperties; -import java.util.UUID; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import org.apache.shiro.subject.Subject; import org.eclipse.jetty.util.ajax.JSON; +import org.quartz.SchedulerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class OneTimeAuthenticationToken extends SelfValidatingAuthenticationToken { - private static final transient String PEPPER = java.util.UUID.randomUUID().toString(); - private final String[] permissions; + public static final long DEFAULT_MAX_REPLAYS = 1L; + public static final int DEFAULT_REPLAYS_TIMEOUT_MS = + Integer.parseInt( + CaosDBServer.getServerProperty(ServerProperties.KEY_ONE_TIME_TOKEN_REPLAYS_TIMEOUT_MS)); + public static final int DEFAULT_TIMEOUT_MS = + Integer.parseInt( + CaosDBServer.getServerProperty(ServerProperties.KEY_ONE_TIME_TOKEN_EXPIRES_MS)); + public static final String REALM_NAME = "OneTimeAuthenticationToken"; // TODO move to UserSources + public static final Logger LOGGER = LoggerFactory.getLogger(OneTimeAuthenticationToken.class); + + private long maxReplays; + private long replaysTimeout; public OneTimeAuthenticationToken( final Principal principal, final long date, final long timeout, final String salt, - final String curry, final String checksum, - final String... permissions) { - super(principal, date, timeout, salt, curry, checksum); - this.permissions = permissions; + final String[] permissions, + final String[] roles, + final long maxReplays, + final long replaysTimeout) { + super(principal, date, timeout, salt, checksum, permissions, roles); + this.replaysTimeout = replaysTimeout; + this.maxReplays = maxReplays; + consume(); + } + + public long getReplaysTimeout() { + return replaysTimeout; + } + + public void consume() { + OneTimeTokenConsumedInfo.consume(this); } public OneTimeAuthenticationToken( final Principal principal, - final long date, final long timeout, - final String salt, - final String curry, - final String... permissions) { + final String[] permissions, + final String[] roles, + final Long maxReplays, + final Long replaysTimeout) { super( principal, - date, timeout, - salt, - curry, - calcChecksum( - principal.getRealm(), - principal.getUsername(), - date, - timeout, - salt, - curry, - calcChecksum((Object[]) permissions), - PEPPER)); - this.permissions = permissions; + permissions, + roles, + defaultIfNull(maxReplays, DEFAULT_MAX_REPLAYS), + defaultIfNull(replaysTimeout, DEFAULT_REPLAYS_TIMEOUT_MS)); } private static final long serialVersionUID = -1072740888045267613L; - public String[] getPermissions() { - return this.permissions; + /** + * Return consumed. + * + * @param array + * @param curry + * @return + */ + public static OneTimeAuthenticationToken parse(final Object[] array) { + final Principal principal = new Principal((String) array[1], (String) array[2]); + final String[] roles = toStringArray((Object[]) array[3]); + final String[] permissions = toStringArray((Object[]) array[4]); + final long date = (Long) array[5]; + final long timeout = (Long) array[6]; + final String salt = (String) array[7]; + final String checksum = (String) array[8]; + final long maxReplays = (Long) array[9]; + final long replaysTimeout = (Long) array[10]; + return new OneTimeAuthenticationToken( + principal, date, timeout, salt, checksum, permissions, roles, maxReplays, replaysTimeout); + } + + private static OneTimeAuthenticationToken generate( + final Principal principal, + final String[] permissions, + final String[] roles, + final long timeout, + final long maxReplays, + final long replaysTimeout) { + + return new OneTimeAuthenticationToken( + principal, timeout, permissions, roles, maxReplays, replaysTimeout); + } + + public static List<Config> loadConfig(InputStream input) throws Exception { + List<Config> results = new LinkedList<>(); + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + ObjectReader reader = mapper.readerFor(Config.class); + Iterator<Config> configs = reader.readValues(input); + configs.forEachRemaining(results::add); + return results; + } + + public static Map<String, Config> getPurposeMap(List<Config> configs) throws Exception { + Map<String, Config> result = new HashMap<>(); + for (Config config : configs) { + if (config.getPurpose() != null && !config.getPurpose().isEmpty()) { + if (result.containsKey(config.getPurpose())) { + throw new Exception( + "OneTimeAuthToken configuration contains duplicate values for the 'purpose' property."); + } + result.put(config.getPurpose(), config); + } + } + return result; + } + + public static OneTimeAuthenticationToken generate(Config c) { + return generate(c, new Principal(REALM_NAME, c.getName())); + } + + public static OneTimeAuthenticationToken generateForPurpose(String purpose, Subject user) { + Config c = purposes.get(purpose); + if (c != null) { + Principal principal = (Principal) user.getPrincipal(); + + return generate(c, principal); + } + return null; + } + + public static OneTimeAuthenticationToken generate(Config c, Principal principal) { + return generate( + principal, + c.getPermissions(), + c.getRoles(), + c.getExpiresAfter(), + c.getMaxReplays(), + c.getReplayTimeout()); + } + + static Map<String, Config> purposes = new HashMap<>(); + + public static Map<String, Config> getPurposeMap() { + return purposes; + } + + public static void initConfig(InputStream yamlConfig) throws Exception { + List<Config> configs = loadConfig(yamlConfig); + initOutput(configs); + purposes.putAll(getPurposeMap(configs)); + } + + private static void initOutput(List<Config> configs) throws IOException, SchedulerException { + for (Config config : configs) { + if (config.getOutput() != null) { + config.getOutput().init(config); + } + } + } + + public static void initConfig() throws Exception { + resetConfig(); + try (FileInputStream f = + new FileInputStream( + CaosDBServer.getServerProperty(ServerProperties.KEY_AUTHTOKEN_CONFIG))) { + initConfig(f); + } catch (IOException e) { + LOGGER.error("Could not load the auth token configuration", e); + } } @Override - public String calcChecksum() { + protected void setFields(Object[] fields) { + if (fields.length == 2) { + this.maxReplays = (long) fields[0]; + this.replaysTimeout = (long) fields[1]; + } else { + throw new InstantiationError("Too few fields."); + } + } + + @Override + public String calcChecksum(String pepper) { return calcChecksum( - this.principal.getRealm(), - this.principal.getUsername(), + "O", + this.getRealm(), + this.getUsername(), this.date, this.timeout, this.salt, - this.curry, calcChecksum((Object[]) this.permissions), - PEPPER); + calcChecksum((Object[]) this.roles), + this.maxReplays, + this.replaysTimeout, + pepper); } @Override public String toString() { return JSON.toString( new Object[] { - this.principal.getRealm(), - this.principal.getUsername(), + "O", + this.getRealm(), + this.getUsername(), + this.roles, + this.permissions, this.date, this.timeout, this.salt, - this.permissions, - this.checksum + this.checksum, + this.maxReplays, + this.replaysTimeout }); } - private static String[] toStringArray(final Object[] array) { - final String[] ret = new String[array.length]; - for (int i = 0; i < ret.length; i++) { - ret[i] = (String) array[i]; - } - return ret; - } - - public static OneTimeAuthenticationToken parse(final String token, final String curry) { - final Object[] array = (Object[]) JSON.parse(token); - final Principal principal = new Principal((String) array[0], (String) array[1]); - final long date = (Long) array[2]; - final long timeout = (Long) array[3]; - final String salt = (String) array[4]; - final String[] permissions = toStringArray((Object[]) array[5]); - final String checksum = (String) array[6]; - return new OneTimeAuthenticationToken( - principal, date, timeout, salt, curry, checksum, permissions); + public long getMaxReplays() { + return maxReplays; } - public static OneTimeAuthenticationToken generate( - final Principal principal, final String curry, final String... permissions) { - return new OneTimeAuthenticationToken( - principal, - System.currentTimeMillis(), - Long.parseLong(CaosDBServer.getServerProperty(ServerProperties.KEY_ACTIVATION_TIMEOUT_MS)), - UUID.randomUUID().toString(), - curry, - permissions); + public static void resetConfig() { + purposes.clear(); } } diff --git a/src/main/java/caosdb/server/accessControl/OneTimeTokenConsumedInfo.java b/src/main/java/caosdb/server/accessControl/OneTimeTokenConsumedInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..a8ebd2c807712d8b06ec68771e7abb8082ce0696 --- /dev/null +++ b/src/main/java/caosdb/server/accessControl/OneTimeTokenConsumedInfo.java @@ -0,0 +1,89 @@ +package caosdb.server.accessControl; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import org.apache.shiro.authc.AuthenticationException; + +/** + * Utility class to manage OTTs: mark as consumed, removed expired OTTs, manage maximum number of + * replays and replay timeout of tokens. + */ +class OneTimeTokenConsumedInfo { + + private static Map<String, OneTimeTokenConsumedInfo> consumedOneTimeTokens = new HashMap<>(); + + public static void cleanupConsumedInfo() { + synchronized (consumedOneTimeTokens) { + for (Iterator<Map.Entry<String, OneTimeTokenConsumedInfo>> it = + consumedOneTimeTokens.entrySet().iterator(); + it.hasNext(); ) { + Map.Entry<String, OneTimeTokenConsumedInfo> next = it.next(); + if (next.getValue().isExpired()) { + it.remove(); + } + } + } + } + + /** If the token is valid, consume it once and store this information. */ + public static void consume(OneTimeAuthenticationToken oneTimeAuthenticationToken) { + if (oneTimeAuthenticationToken.isValid()) { + String key = OneTimeTokenConsumedInfo.getKey(oneTimeAuthenticationToken); + OneTimeTokenConsumedInfo consumedInfo = null; + synchronized (consumedOneTimeTokens) { + consumedInfo = consumedOneTimeTokens.get(key); + if (consumedInfo == null) { + consumedInfo = new OneTimeTokenConsumedInfo(oneTimeAuthenticationToken); + consumedOneTimeTokens.put(key, consumedInfo); + } + } + consumedInfo.consume(); + } + } + + private OneTimeAuthenticationToken oneTimeAuthenticationToken; + private List<Long> replays = new LinkedList<>(); + + public OneTimeTokenConsumedInfo(OneTimeAuthenticationToken oneTimeAuthenticationToken) { + this.oneTimeAuthenticationToken = oneTimeAuthenticationToken; + } + + public static String getKey(OneTimeAuthenticationToken token) { + return token.checksum; + } + + private int getNoOfReplays() { + return replays.size(); + } + + private long getMaxReplays() { + return oneTimeAuthenticationToken.getMaxReplays(); + } + + private long getReplayTimeout() { + if (replays.size() == 0) { + return Long.MAX_VALUE; + } + long firstReplayTime = replays.get(0); + return firstReplayTime + oneTimeAuthenticationToken.getReplaysTimeout(); + } + + /** If there are still replays and time left, increase the replay counter by one. */ + public void consume() { + synchronized (replays) { + if (getNoOfReplays() >= getMaxReplays()) { + throw new AuthenticationException("One-time token was consumed too often."); + } else if (getReplayTimeout() < System.currentTimeMillis()) { + throw new AuthenticationException("One-time token replays timeout expired."); + } + replays.add(System.currentTimeMillis()); + } + } + + public boolean isExpired() { + return oneTimeAuthenticationToken.isExpired(); + } +} diff --git a/src/main/java/caosdb/server/accessControl/OneTimeTokenRealm.java b/src/main/java/caosdb/server/accessControl/OneTimeTokenRealm.java deleted file mode 100644 index 468a8d8d212ca5546f1940a4b3de38c1da50b3d8..0000000000000000000000000000000000000000 --- a/src/main/java/caosdb/server/accessControl/OneTimeTokenRealm.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * ** 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 - * 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/>. - * - * ** end header - */ -package caosdb.server.accessControl; - -import caosdb.server.accessControl.CaosDBAuthorizingRealm.PermissionAuthenticationInfo; -import org.apache.shiro.authc.AuthenticationException; -import org.apache.shiro.authc.AuthenticationInfo; -import org.apache.shiro.authc.AuthenticationToken; -import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher; - -public class OneTimeTokenRealm extends SessionTokenRealm { - - @Override - protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token) - throws AuthenticationException { - - final AuthenticationInfo info = super.doGetAuthenticationInfo(token); - if (info != null) { - return new PermissionAuthenticationInfo( - info.getPrincipals(), ((OneTimeAuthenticationToken) token).getPermissions()); - } - - return null; - } - - public OneTimeTokenRealm() { - setAuthenticationTokenClass(OneTimeAuthenticationToken.class); - setCredentialsMatcher(new AllowAllCredentialsMatcher()); - setCachingEnabled(false); - setAuthenticationCachingEnabled(false); - // setAuthorizationCachingEnabled(false); - } -} diff --git a/src/main/java/caosdb/server/accessControl/OneTimeTokenToFile.java b/src/main/java/caosdb/server/accessControl/OneTimeTokenToFile.java new file mode 100644 index 0000000000000000000000000000000000000000..b4006783a2105a353749edfaa475bde3dae93808 --- /dev/null +++ b/src/main/java/caosdb/server/accessControl/OneTimeTokenToFile.java @@ -0,0 +1,83 @@ +package caosdb.server.accessControl; + +import caosdb.server.CaosDBServer; +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import org.quartz.CronScheduleBuilder; +import org.quartz.Job; +import org.quartz.JobBuilder; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.SchedulerException; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; + +public class OneTimeTokenToFile implements Job { + private String file = null; + private String schedule = null; + + public OneTimeTokenToFile() {} + + public static void output(OneTimeAuthenticationToken t, String file) throws IOException { + output(t, new File(file)); + } + + public static void output(OneTimeAuthenticationToken t, File file) throws IOException { + Files.createParentDirs(file); + try (PrintWriter writer = new PrintWriter(file, "utf-8")) { + writer.print(t.toString()); + } + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + + public String getSchedule() { + return schedule; + } + + public void setSchedule(String schedule) { + this.schedule = schedule; + } + + /** If no schedule was set, immediately write the config to file, else schedule the job. */ + public void init(Config config) throws IOException, SchedulerException { + + if (this.schedule != null) { + OneTimeAuthenticationToken.generate(config); // test config, throw away token + JobDataMap map = new JobDataMap(); + map.put("config", config); + map.put("file", file); + JobDetail outputJob = JobBuilder.newJob(OneTimeTokenToFile.class).setJobData(map).build(); + Trigger trigger = + TriggerBuilder.newTrigger() + .withIdentity(config.toString()) + .withSchedule(CronScheduleBuilder.cronSchedule(this.schedule)) + .build(); + CaosDBServer.scheduleJob(outputJob, trigger); + } else { + output(OneTimeAuthenticationToken.generate(config), file); + } + } + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + Config config = (Config) context.getMergedJobDataMap().get("config"); + String file = context.getMergedJobDataMap().getString("file"); + try { + output(OneTimeAuthenticationToken.generate(config), file); + } catch (IOException e) { + // TODO log + e.printStackTrace(); + } + } +} diff --git a/src/main/java/caosdb/server/accessControl/Principal.java b/src/main/java/caosdb/server/accessControl/Principal.java index 3183d864bd5baecc4429cf22fc0ab3ca75d8ca15..6a95dd79eccd50c9358928909822af67056102db 100644 --- a/src/main/java/caosdb/server/accessControl/Principal.java +++ b/src/main/java/caosdb/server/accessControl/Principal.java @@ -47,6 +47,11 @@ public class Principal implements ResponsibleAgent { this(split[0], split[1]); } + public Principal(Principal principal) { + this.username = principal.username; + this.realm = principal.realm; + } + public String getRealm() { return this.realm; } diff --git a/src/main/java/caosdb/server/accessControl/SelfValidatingAuthenticationToken.java b/src/main/java/caosdb/server/accessControl/SelfValidatingAuthenticationToken.java index 079f592ea7911f974bf4a64c9c5f1674e64a09a5..119a86248b83a472df0d2981db43d9a6cc1962f0 100644 --- a/src/main/java/caosdb/server/accessControl/SelfValidatingAuthenticationToken.java +++ b/src/main/java/caosdb/server/accessControl/SelfValidatingAuthenticationToken.java @@ -4,6 +4,8 @@ * * 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> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,52 +25,139 @@ package caosdb.server.accessControl; import caosdb.server.utils.Utils; +import java.util.Arrays; +import java.util.Collection; +import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; +import org.eclipse.jetty.util.ajax.JSON; -public abstract class SelfValidatingAuthenticationToken implements AuthenticationToken { +/** + * These AuthenticationTokens are characterized by the following properties: + * + * <ul> + * <li>date: The creation time. + * <li>timeout: How long this token is valid after creation. + * <li>checksum: The checksum is calculated from all relevant parts of the authentication token + * (including the salt, timeout, permissions, roles, and date) and most importantly, the + * pepper which serves as a randomized password of the server. The salt makes it hard to guess + * the pepper by creating a rainbow table with plausible values for the other properties. + * <li>salt: Salt for the password checksum, may be used by inheriting classes. + * <li>pepper: A static property, generated when class is loaded and used until the server + * reboots. It servers as randomized password of the server. "In cryptography, a pepper is a + * secret added to an input such as a password prior to being hashed with a cryptographic hash + * function." (from: Pepper (cryptography), + * https://en.wikipedia.org/w/index.php?title=Pepper_(cryptography)&oldid=960047694 (last + * visited July 7, 2020)) In our case, the pepper is added to the token before hashing, but + * not exposed to the public, while the salt is. That also means that the resulting hash + * cannot be generated by any client nor be validated by any client, and that all tokens of + * this kind invalidate when the server reboots. + */ +public abstract class SelfValidatingAuthenticationToken extends Principal + implements AuthenticationToken { + protected static final transient String PEPPER = Utils.getSecureFilename(32); private static final long serialVersionUID = -7212039848895531161L; + // date is the token creation time, in ms since 1970 protected final long date; + // token validity duration protected final long timeout; protected final String checksum; - protected final Principal principal; - protected final String curry; protected final String salt; + protected final String[] permissions; + protected final String[] roles; + + public Collection<String> getPermissions() { + return Arrays.asList(this.permissions); + } + + public Collection<String> getRoles() { + return Arrays.asList(this.roles); + } + + public static final String getFreshSalt() { + // salt should be at least 8 octets long. https://www.ietf.org/rfc/rfc2898.txt + // let's double that + return Utils.getSecureFilename(16); + } + + protected static <T> T defaultIfNull(T val, T def) { + if (val != null) { + return val; + } + return def; + } public SelfValidatingAuthenticationToken( final Principal principal, final long date, final long timeout, final String salt, - final String curry, - final String checksum) { - this.date = date; - this.timeout = timeout; - this.principal = principal; - this.salt = salt; - this.curry = curry; - this.checksum = checksum; + final String checksum, + final String[] permissions, + final String[] roles) { + this(principal, date, timeout, salt, permissions, roles, checksum, false); } - public SelfValidatingAuthenticationToken( + private SelfValidatingAuthenticationToken( final Principal principal, final long date, final long timeout, final String salt, - final String curry) { + final String[] permissions, + final String[] roles, + String checksum, + boolean newChecksum, + Object... fields) { + super(principal); this.date = date; this.timeout = timeout; - this.principal = principal; this.salt = salt; - this.curry = curry; - this.checksum = calcChecksum(); + this.permissions = defaultIfNull(permissions, new String[] {}); + this.roles = defaultIfNull(roles, new String[] {}); + if (fields.length > 0) setFields(fields); + // only calculate a checksum iff none is given and newChecksum is explicitly 'true'. + this.checksum = checksum == null && newChecksum ? calcChecksum() : checksum; } - @Override - public Principal getPrincipal() { - return this.principal; + /** Customizable customization method, will be called with the remaining constructor arguments. */ + protected abstract void setFields(Object[] fields); + + public SelfValidatingAuthenticationToken( + final Principal principal, + final long timeout, + final String[] permissions, + final String[] roles, + Object... fields) { + this( + principal, + System.currentTimeMillis(), + timeout, + getFreshSalt(), + permissions, + roles, + null, + true, + fields); + } + + public final String calcChecksum() { + return calcChecksum(PEPPER); } + @Override + public abstract String toString(); + + /** + * Implementation specific version of a peppered checksum. + * + * <p>For secure operation, implementing classes must make sure that the pepper is actually used + * in calculating the checksum and that the checksum can not be used to infer information about + * the pepper. This can be achieved for example by using the {@link calcChecksum(final Object... + * fields)} method. + */ + public abstract String calcChecksum(String pepper); + + /** No credentials (returns null), since this token is self-validating. */ @Override public Object getCredentials() { return null; @@ -86,6 +175,9 @@ public abstract class SelfValidatingAuthenticationToken implements Authenticatio return System.currentTimeMillis() >= getExpires(); } + /** + * Test if the hash stored in `checksum` is equal to the one calculated using the secret pepper. + */ public boolean isHashValid() { final String other = calcChecksum(); return this.checksum != null && this.checksum.equals(other); @@ -95,8 +187,7 @@ public abstract class SelfValidatingAuthenticationToken implements Authenticatio return !isExpired() && isHashValid(); } - public abstract String calcChecksum(); - + /** Return the hash (SHA512) of the stringified arguments. */ protected static String calcChecksum(final Object... fields) { final StringBuilder sb = new StringBuilder(); for (final Object field : fields) { @@ -107,4 +198,36 @@ public abstract class SelfValidatingAuthenticationToken implements Authenticatio } return Utils.sha512(sb.toString(), null, 1); } + + protected static String[] toStringArray(final Object[] array) { + final String[] ret = new String[array.length]; + for (int i = 0; i < ret.length; i++) { + ret[i] = (String) array[i]; + } + return ret; + } + + /** + * Parse a JSON string and return the generated token. Depending on the first element of the JSON, + * this is either (if it is "O") a OneTimeAuthenticationToken or (if it is "S") a SessionToken. + * + * @throws AuthenticationToken if the string could not be parsed into a token. + */ + public static SelfValidatingAuthenticationToken parse(String token) { + Object[] array = (Object[]) JSON.parse(token); + switch (array[0].toString()) { + case "O": + return OneTimeAuthenticationToken.parse(array); + case "S": + return SessionToken.parse(array); + default: + throw new AuthenticationException("Could not parse the authtoken string (unknown type)."); + } + } + + /** No "other" identity, so this returns itself. */ + @Override + public SelfValidatingAuthenticationToken getPrincipal() { + return this; + } } diff --git a/src/main/java/caosdb/server/accessControl/SessionToken.java b/src/main/java/caosdb/server/accessControl/SessionToken.java index 273bd50ba7c4ef89fecd0e2b9b8817acbf6d9efa..d24b4afeb739d764947092783c544168a6c0cf3f 100644 --- a/src/main/java/caosdb/server/accessControl/SessionToken.java +++ b/src/main/java/caosdb/server/accessControl/SessionToken.java @@ -4,6 +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) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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,86 +27,109 @@ package caosdb.server.accessControl; import caosdb.server.CaosDBServer; import caosdb.server.ServerProperties; +import org.apache.shiro.subject.Subject; import org.eclipse.jetty.util.ajax.JSON; +/** + * Session tokens are formatted as JSON arrays with the following elements: + * + * <ul> + * <li>Anything but "O" (upper-case "o"), preferred is "S". + * <li>Realm + * <li>name within the Realm + * <li>list of roles + * <li>list of permissions + * <li>time of token generation (long, ms since 1970) + * <li>validity duration (long, ms) + * <li>salt + * <li>checksum + */ public class SessionToken extends SelfValidatingAuthenticationToken { - /** - * Cryptographic pepper. Generated when class is loaded and used until the server reboots. Hence - * all SessionTokens invalidate when the server reboots. - */ - private static final transient String PEPPER = java.util.UUID.randomUUID().toString(); - - public static final String SEP = ":"; - public SessionToken( final Principal principal, final long date, final long timeout, final String salt, - final String curry, - final String checksum) { - super(principal, date, timeout, salt, curry, checksum); + final String checksum, + final String[] permissions, + final String[] roles) { + super(principal, date, timeout, salt, checksum, permissions, roles); } public SessionToken( final Principal principal, - final long date, final long timeout, - final String salt, - final String curry) { - super(principal, date, timeout, salt, curry); + final String[] permissions, + final String[] roles) { + super(principal, timeout, permissions, roles); } private static final long serialVersionUID = 5887135104218573761L; + public static SessionToken parse(final Object[] array) { + // array[0] is not used here, it was already consumed to determine the type of token. + final Principal principal = new Principal((String) array[1], (String) array[2]); + final String[] roles = toStringArray((Object[]) array[3]); + final String[] permissions = toStringArray((Object[]) array[4]); + final long date = (Long) array[5]; + final long timeout = (Long) array[6]; + final String salt = (String) array[7]; + final String checksum = (String) array[8]; + return new SessionToken(principal, date, timeout, salt, checksum, permissions, roles); + } + + private static SessionToken generate( + final Principal principal, final String[] permissions, final String[] roles) { + int timeout = + Integer.parseInt(CaosDBServer.getServerProperty(ServerProperties.KEY_SESSION_TIMEOUT_MS)); + + return new SessionToken(principal, timeout, permissions, roles); + } + + public static SessionToken generate(Subject subject) { + String[] permissions = new String[] {}; + String[] roles = new String[] {}; + if (subject.getPrincipal() instanceof SelfValidatingAuthenticationToken) { + SelfValidatingAuthenticationToken p = + (SelfValidatingAuthenticationToken) subject.getPrincipal(); + permissions = p.getPermissions().toArray(permissions); + roles = p.getRoles().toArray(roles); + } + return generate((Principal) subject.getPrincipal(), permissions, roles); + } + + /** Nothing to set in this implemention. */ + @Override + protected void setFields(Object[] fields) {} + @Override - public String calcChecksum() { + public String calcChecksum(String pepper) { return calcChecksum( + "S", + this.getRealm(), + this.getUsername(), this.date, this.timeout, - this.principal.getRealm(), - this.principal.getUsername(), - PEPPER, this.salt, - this.curry); + calcChecksum((Object[]) this.permissions), + calcChecksum((Object[]) this.roles), + pepper); } @Override public String toString() { return JSON.toString( new Object[] { - this.principal.getRealm(), - this.principal.getUsername(), + "S", + this.getRealm(), + this.getUsername(), + this.roles, + this.permissions, this.date, this.timeout, this.salt, this.checksum }); } - - public static SessionToken parse(final String token, final String curry) { - final Object[] array = (Object[]) JSON.parse(token); - final Principal principal = new Principal((String) array[0], (String) array[1]); - final long date = (Long) array[2]; - final long timeout = (Long) array[3]; - final String salt = (String) array[4]; - final String checksum = (String) array[5]; - return new SessionToken(principal, date, timeout, salt, curry, checksum); - } - - public static SessionToken generate(final Principal principal, final String curry) { - final SessionToken ret = - new SessionToken( - principal, - System.currentTimeMillis(), - Long.parseLong( - CaosDBServer.getServerProperty(ServerProperties.KEY_SESSION_TIMEOUT_MS).trim()), - java.util.UUID.randomUUID().toString(), - curry); - if (!ret.isValid()) { - throw new RuntimeException("SessionToken not valid!"); - } - return ret; - } } diff --git a/src/main/java/caosdb/server/accessControl/SessionTokenRealm.java b/src/main/java/caosdb/server/accessControl/SessionTokenRealm.java index 6ee72d0295153051e5ad31a6fd0fa092ab53d6e3..cce2850d6d532ae8880d208682b5677dc0369089 100644 --- a/src/main/java/caosdb/server/accessControl/SessionTokenRealm.java +++ b/src/main/java/caosdb/server/accessControl/SessionTokenRealm.java @@ -37,13 +37,13 @@ public class SessionTokenRealm extends AuthenticatingRealm { (SelfValidatingAuthenticationToken) token; if (sessionToken.isValid()) { - return new SimpleAuthenticationInfo(sessionToken.getPrincipal(), null, getName()); + return new SimpleAuthenticationInfo(sessionToken, null, getName()); } return null; } public SessionTokenRealm() { - setAuthenticationTokenClass(SessionToken.class); + setAuthenticationTokenClass(SelfValidatingAuthenticationToken.class); setCredentialsMatcher(new AllowAllCredentialsMatcher()); setCachingEnabled(false); setAuthenticationCachingEnabled(false); diff --git a/src/main/java/caosdb/server/accessControl/UserSources.java b/src/main/java/caosdb/server/accessControl/UserSources.java index d0f707ebaaec8aa97e9b87d760fc0631cbd2f72a..cdf219c017608c621a59d427b2132630fef0631d 100644 --- a/src/main/java/caosdb/server/accessControl/UserSources.java +++ b/src/main/java/caosdb/server/accessControl/UserSources.java @@ -4,6 +4,8 @@ * * 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> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -25,11 +27,11 @@ package caosdb.server.accessControl; import caosdb.server.CaosDBServer; import caosdb.server.ServerProperties; import caosdb.server.entity.Message; +import caosdb.server.permissions.Role; import caosdb.server.transaction.RetrieveRoleTransaction; import caosdb.server.transaction.RetrieveUserTransaction; import caosdb.server.utils.ServerMessages; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; @@ -37,16 +39,39 @@ import java.util.HashSet; import java.util.Set; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.config.Ini; -import org.apache.shiro.subject.PrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * This singleton class is the primary resource for authenticating users and resolving principals to + * roles. + * + * <p>Key concepts: + * + * <ul> + * <li>User name: A string which identifies a user uniquely across one realm. Why is this so? + * Because it is possible, that two different people from collaborating work groups with + * similar names have the same user name in their group e.g. "mueller@uni1.de" and + * "mueller@uni2.de" or two people from different user groups use the name "admin". In the + * "mueller" example the domain name of the email is the realm of authentication. + * <li>Realm: A string which uniquely identifies "where a user comes from". It guarantees the + * authentication of a user with a particular user name. Currently the possible realms are + * quite limited. Only "CaosDB" (which is controlled by the internal user source) and "PAM" + * which delegates authentication to the host system via PAM (Pluggable Authentication Module) + * are known and extension is not too easy. + * <li>User Source: An instance which provides the access to a realm where users can be + * authenticated. + * <li>Principal: The combination of realm and user name - hence a system-wide unique identifier + * for users and the primary key to identifying who did what and who is allowed to to do what. + * </ul> + * + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ public class UserSources extends HashMap<String, UserSource> { - public static final String ANONYMOUS_ROLE = "anonymous"; private static final Logger logger = LoggerFactory.getLogger(UserSources.class); public static final String KEY_DEFAULT_REALM = "defaultRealm"; - public static final String KEY_REALMS = "defaultRealm"; + public static final String KEY_REALMS = "realms"; public static final String KEY_REALM_CLASS = "class"; private static final long serialVersionUID = 6782744064206400521L; @@ -61,6 +86,11 @@ public class UserSources extends HashMap<String, UserSource> { private UserSources() { initMap(); this.put(getInternalRealm()); + if (this.map.getSection(Ini.DEFAULT_SECTION_NAME) == null + || !this.map.getSection(Ini.DEFAULT_SECTION_NAME).containsKey(KEY_REALMS)) { + // no realms defined + return; + } final String[] realms = this.map .getSectionProperty(Ini.DEFAULT_SECTION_NAME, KEY_REALMS) @@ -88,6 +118,8 @@ public class UserSources extends HashMap<String, UserSource> { } } + private Ini map = null; + public UserSource put(final UserSource src) { if (src.getName() == null) { throw new IllegalArgumentException("A user source's name must not be null."); @@ -103,21 +135,14 @@ public class UserSources extends HashMap<String, UserSource> { return instance.put(src); } - private Ini map = null; - public void initMap() { - this.map = null; - try { - final FileInputStream f = - new FileInputStream( - CaosDBServer.getServerProperty(ServerProperties.KEY_USER_SOURCES_INI_FILE)); - this.map = new Ini(); + this.map = new Ini(); + try (final FileInputStream f = + new FileInputStream( + CaosDBServer.getServerProperty(ServerProperties.KEY_USER_SOURCES_INI_FILE))) { this.map.load(f); - f.close(); - } catch (final FileNotFoundException e) { - e.printStackTrace(); } catch (final IOException e) { - e.printStackTrace(); + logger.debug("could not load usersources.ini", e); } } @@ -126,16 +151,19 @@ public class UserSources extends HashMap<String, UserSource> { * * @param realm * @param username - * @return + * @return A set of user roles. */ - public static Set<String> resolve(String realm, final String username) { - + public static Set<String> resolveRoles(String realm, final String username) { if (realm == null) { realm = guessRealm(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 = instance.get(realm).resolveRolesForUsername(username); + final Set<String> ret = userSource.resolveRolesForUsername(username); return ret; } @@ -165,19 +193,19 @@ public class UserSources extends HashMap<String, UserSource> { } public static String getDefaultRealm() { - return instance.map.getSectionProperty(Ini.DEFAULT_SECTION_NAME, KEY_DEFAULT_REALM); + return instance.map.getSectionProperty(Ini.DEFAULT_SECTION_NAME, KEY_DEFAULT_REALM, "CaosDB"); } - public static Set<String> resolve(final PrincipalCollection principals) { - if (principals.getPrimaryPrincipal() == AuthenticationUtils.ANONYMOUS_USER.getPrincipal()) { + // @todo Refactor name: resolveRoles(...)? + public static Set<String> resolve(final Principal principal) { + if (AnonymousAuthenticationToken.PRINCIPAL == principal) { // anymous has one role Set<String> roles = new HashSet<>(); - roles.add(ANONYMOUS_ROLE); + roles.add(Role.ANONYMOUS_ROLE.toString()); return roles; } - Principal primaryPrincipal = (Principal) principals.getPrimaryPrincipal(); - return resolve(primaryPrincipal.getRealm(), primaryPrincipal.getUsername()); + return resolveRoles(principal.getRealm(), principal.getUsername()); } public static boolean isRoleExisting(final String role) { diff --git a/src/main/java/caosdb/server/caching/JCSCacheHelper.java b/src/main/java/caosdb/server/caching/JCSCacheHelper.java index 1ce455a949bcfb76a36815b3b14bcea89e676a4a..c471ed7ef860d902b56940da52a876a44fd97218 100644 --- a/src/main/java/caosdb/server/caching/JCSCacheHelper.java +++ b/src/main/java/caosdb/server/caching/JCSCacheHelper.java @@ -5,7 +5,7 @@ * 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,2020 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 @@ -88,6 +88,9 @@ public class JCSCacheHelper implements CacheHelper { } logger.info("Configuring JCS Caching with {}", config); } + + // If the JCS config is updated/reset, it has to be shut down before. + JCS.shutdown(); JCS.setConfigProperties(config); } diff --git a/src/main/java/caosdb/server/database/BackendTransaction.java b/src/main/java/caosdb/server/database/BackendTransaction.java index 14e65e86bd9fe05b074c731e27b601fa4ed51f68..c476fb5bb6cc67aee4650fbad79e22e97bf3aec9 100644 --- a/src/main/java/caosdb/server/database/BackendTransaction.java +++ b/src/main/java/caosdb/server/database/BackendTransaction.java @@ -3,7 +3,9 @@ * 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 + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 IndiScale GmbH + * Copyright (C) 2020 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 @@ -58,6 +60,7 @@ import caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveRole; import caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveSparseEntity; import caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveTransactionHistory; import caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveUser; +import caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveVersionHistory; import caosdb.server.database.backend.implementation.MySQL.MySQLRuleLoader; import caosdb.server.database.backend.implementation.MySQL.MySQLSetFileCheckedTimestampImpl; import caosdb.server.database.backend.implementation.MySQL.MySQLSetPassword; @@ -113,8 +116,10 @@ import caosdb.server.database.backend.interfaces.RetrieveRoleImpl; import caosdb.server.database.backend.interfaces.RetrieveSparseEntityImpl; import caosdb.server.database.backend.interfaces.RetrieveTransactionHistoryImpl; import caosdb.server.database.backend.interfaces.RetrieveUserImpl; +import caosdb.server.database.backend.interfaces.RetrieveVersionHistoryImpl; import caosdb.server.database.backend.interfaces.RuleLoaderImpl; import caosdb.server.database.backend.interfaces.SetFileCheckedTimestampImpl; +import caosdb.server.database.backend.interfaces.SetFileChecksumImpl; import caosdb.server.database.backend.interfaces.SetPasswordImpl; import caosdb.server.database.backend.interfaces.SetPermissionRulesImpl; import caosdb.server.database.backend.interfaces.SetQueryTemplateDefinitionImpl; @@ -204,6 +209,8 @@ public abstract class BackendTransaction implements Undoable { setImpl( RetrieveQueryTemplateDefinitionImpl.class, MySQLRetrieveQueryTemplateDefinition.class); setImpl(InsertEntityDatatypeImpl.class, MySQLInsertEntityDatatype.class); + setImpl(RetrieveVersionHistoryImpl.class, MySQLRetrieveVersionHistory.class); + setImpl(SetFileChecksumImpl.class, MySQLSetFileChecksum.class); } } diff --git a/src/main/java/caosdb/server/database/CacheableBackendTransaction.java b/src/main/java/caosdb/server/database/CacheableBackendTransaction.java index fc4c9659d9a8ea80393bebad945bd6badf9580c4..0131b740d81cbd41d1b9263c5f42029e1af4539c 100644 --- a/src/main/java/caosdb/server/database/CacheableBackendTransaction.java +++ b/src/main/java/caosdb/server/database/CacheableBackendTransaction.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,2020 IndiScale GmbH + * Copyright (C) 2019,2020 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 @@ -51,7 +51,7 @@ public abstract class CacheableBackendTransaction<K, V extends Serializable> private final V execute(final K key) throws TransactionException { // get from cache if possible... if (cacheIsEnabled() && key != null) { - final V cached = getCache().get(getKey()); + final V cached = getCache().get(key); if (cached != null) { this.cached = true; return cached; @@ -64,7 +64,7 @@ public abstract class CacheableBackendTransaction<K, V extends Serializable> if (notCached != null) { if (cacheIsEnabled() && key != null) { // now cache if possible - getCache().put(getKey(), notCached); + getCache().put(key, notCached); } } return notCached; diff --git a/src/main/java/caosdb/server/database/DatabaseUtils.java b/src/main/java/caosdb/server/database/DatabaseUtils.java index 84c38fa448b7d8a1035aa28ded7bbfcecc5a88b3..9edc0347abf8b63de0ce76ead06fb1f6883332e1 100644 --- a/src/main/java/caosdb/server/database/DatabaseUtils.java +++ b/src/main/java/caosdb/server/database/DatabaseUtils.java @@ -173,12 +173,7 @@ public class DatabaseUtils { while (rs.next()) { final FlatProperty fp = new FlatProperty(); fp.id = rs.getInt("PropertyID"); - - final String v = bytes2UTF8(rs.getBytes("PropertyValue")); - if (v != null) { - fp.value = v; - } - + fp.value = bytes2UTF8(rs.getBytes("PropertyValue")); fp.status = bytes2UTF8(rs.getBytes("PropertyStatus")); fp.idx = rs.getInt("PropertyIndex"); ret.add(fp); @@ -217,12 +212,14 @@ public class DatabaseUtils { ret.datatype = bytes2UTF8(rs.getBytes("Datatype")); ret.collection = bytes2UTF8(rs.getBytes("Collection")); - final String path = bytes2UTF8(rs.getBytes("FilePath")); - if (!rs.wasNull()) { - ret.filePath = path; - ret.fileSize = rs.getLong("FileSize"); - ret.fileHash = bytes2UTF8(rs.getBytes("FileHash")); - } + ret.filePath = bytes2UTF8(rs.getBytes("FilePath")); + ret.fileSize = rs.getLong("FileSize"); + ret.fileHash = bytes2UTF8(rs.getBytes("FileHash")); + + ret.version = bytes2UTF8(rs.getBytes("Version")); + ret.versionSeconds = rs.getLong("VersionSeconds"); + ret.versionNanos = rs.getInt("VersionNanos"); + return ret; } diff --git a/src/main/java/caosdb/server/database/MySQLSetFileChecksum.java b/src/main/java/caosdb/server/database/MySQLSetFileChecksum.java new file mode 100644 index 0000000000000000000000000000000000000000..a74843b65c12a49c05d77cb5df180b8b3317c3df --- /dev/null +++ b/src/main/java/caosdb/server/database/MySQLSetFileChecksum.java @@ -0,0 +1,31 @@ +package caosdb.server.database; + +import caosdb.server.database.access.Access; +import caosdb.server.database.backend.implementation.MySQL.ConnectionException; +import caosdb.server.database.backend.implementation.MySQL.MySQLTransaction; +import caosdb.server.database.backend.interfaces.SetFileChecksumImpl; +import caosdb.server.database.exceptions.TransactionException; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class MySQLSetFileChecksum extends MySQLTransaction implements SetFileChecksumImpl { + + public MySQLSetFileChecksum(Access access) { + super(access); + } + + public static final String STMT_SET_CHECKSUM = + "UPDATE files SET hash = unhex(?) WHERE file_id = ?"; + + @Override + public void execute(Integer id, String checksum) { + try { + PreparedStatement stmt = prepareStatement(STMT_SET_CHECKSUM); + stmt.setInt(2, id); + stmt.setString(1, checksum); + stmt.execute(); + } catch (SQLException | ConnectionException e) { + throw new TransactionException(e); + } + } +} diff --git a/src/main/java/caosdb/server/database/backend/implementation/MySQL/DatabaseConnectionPool.java b/src/main/java/caosdb/server/database/backend/implementation/MySQL/DatabaseConnectionPool.java index 9700daf3a99b30e42baf8580f863998d7efe67a1..fc17dd1066cfa57660d44dbb6a521d3177e5fc72 100644 --- a/src/main/java/caosdb/server/database/backend/implementation/MySQL/DatabaseConnectionPool.java +++ b/src/main/java/caosdb/server/database/backend/implementation/MySQL/DatabaseConnectionPool.java @@ -85,8 +85,7 @@ class DatabaseConnectionPool { + CaosDBServer.getServerProperty(ServerProperties.KEY_MYSQL_PORT) + "/" + CaosDBServer.getServerProperty(ServerProperties.KEY_MYSQL_DATABASE_NAME) - + "?noAccessToProcedureBodies=true&cacheCallableStmts=true&autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&connectionCollation=utf8_unicode_ci&characterSetResults=utf8&serverTimezone=CET"; - // + "?profileSQL=true&characterSetResults=utf8"; + + "?noAccessToProcedureBodies=true&autoReconnect=true&serverTimezone=UTC&characterEncoding=UTF-8"; final String user = CaosDBServer.getServerProperty(ServerProperties.KEY_MYSQL_USER_NAME); final String pwd = CaosDBServer.getServerProperty(ServerProperties.KEY_MYSQL_USER_PASSWORD); final ConnectionPool pool = new ConnectionPool("MySQL Pool", 2, 5, 0, 0, url, user, pwd); diff --git a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java index 458823167423893b6defb166f013618be3a796d7..b443c54507e415ed4171fcf0ac99c3da762fb506 100644 --- a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java +++ b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLHelper.java @@ -4,6 +4,8 @@ * * 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> * * 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,15 +24,20 @@ */ package caosdb.server.database.backend.implementation.MySQL; +import caosdb.server.accessControl.Principal; import caosdb.server.database.misc.DBHelper; +import caosdb.server.transaction.ChecksumUpdater; import caosdb.server.transaction.TransactionInterface; import caosdb.server.transaction.WriteTransaction; -import caosdb.server.utils.Info; -import caosdb.server.utils.Initialization; +import java.io.UnsupportedEncodingException; +import java.sql.CallableStatement; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.Statement; import java.util.HashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Provides cached statements for a MySQL back-end. @@ -41,23 +48,71 @@ public class MySQLHelper implements DBHelper { private Connection connection = null; + private Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * Initialize a transaction by calling the corresponding SQL procedure. + * + * <p>In the database, this adds a row to the transaction table with SRID, user and timestamp. + */ + public void initTransaction(Connection connection, WriteTransaction<?> transaction) + throws SQLException { + try (CallableStatement call = connection.prepareCall("CALL set_transaction(?,?,?,?,?)")) { + + String username = ((Principal) transaction.getTransactor().getPrincipal()).getUsername(); + String realm = ((Principal) transaction.getTransactor().getPrincipal()).getRealm(); + long seconds = transaction.getTimestamp().getUTCSeconds(); + int nanos = transaction.getTimestamp().getNanoseconds(); + byte[] srid = transaction.getSRID().getBytes("UTF-8"); + + call.setBytes(1, srid); + call.setString(2, username); + call.setString(3, realm); + call.setLong(4, seconds); + call.setInt(5, nanos); + call.execute(); + + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + System.exit(1); + } + } + + public Connection initConnection(TransactionInterface transaction) + throws ConnectionException, SQLException { + Connection connection; + connection = DatabaseConnectionPool.getConnection(); + + if (transaction instanceof ChecksumUpdater) { + connection.setReadOnly(false); + connection.setAutoCommit(false); + } else if (transaction instanceof WriteTransaction) { + connection.setReadOnly(false); + connection.setAutoCommit(false); + initTransaction(connection, (WriteTransaction<?>) transaction); + } else { + connection.setReadOnly(false); + connection.setAutoCommit(true); + } + + return connection; + } + public Connection getConnection() throws SQLException, ConnectionException { if (this.connection == null) { - this.connection = DatabaseConnectionPool.getConnection(); - if (this.transaction instanceof WriteTransaction) { - this.connection.setReadOnly(false); - this.connection.setAutoCommit(false); - } else if (this.transaction instanceof Initialization || this.transaction instanceof Info) { - this.connection.setReadOnly(false); - this.connection.setAutoCommit(true); - } else { - this.connection.setReadOnly(false); - this.connection.setAutoCommit(true); - } + this.connection = initConnection(this.transaction); } return this.connection; } + /** + * Prepare a statement from a string. Reuse prepared statements from the cache if available. + * + * @param statement + * @return + * @throws SQLException + * @throws ConnectionException + */ public PreparedStatement prepareStatement(final String statement) throws SQLException, ConnectionException { if (this.stmtCache.containsKey(statement)) { @@ -81,6 +136,7 @@ public class MySQLHelper implements DBHelper { private TransactionInterface transaction = null; + /** Make all changes permanent. */ @Override public void commit() throws SQLException { if (this.connection != null @@ -90,13 +146,20 @@ public class MySQLHelper implements DBHelper { } } + /** + * Reset SRID variable, close all statements, roll back to last save point and close connection. + */ @Override public void cleanUp() { - // close all statements (if necessary), roll back to last save point (if - // possible) and close connection (if necessary). try { if (this.connection != null && !this.connection.isClosed()) { + try (Statement s = connection.createStatement()) { + s.execute("SET @SRID = NULL"); + } catch (SQLException e) { + logger.error("Exception while resetting the @SRID variable.", e); + } + // close all cached statements (if possible) for (final PreparedStatement stmt : this.stmtCache.values()) { try { @@ -104,7 +167,7 @@ public class MySQLHelper implements DBHelper { stmt.close(); } } catch (final SQLException e) { - e.printStackTrace(); + logger.warn("Exception while closing a prepared statement.", e); } } @@ -113,12 +176,12 @@ public class MySQLHelper implements DBHelper { this.connection.rollback(); } } catch (final SQLException r) { - r.printStackTrace(); + logger.warn("Exception during roll-back attempt.", r); } this.connection.close(); } } catch (final SQLException e) { - e.printStackTrace(); + logger.warn("Exception during clean-up.", e); } // clear everything diff --git a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLInsertSparseEntity.java b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLInsertSparseEntity.java index 4ad9f5b6a3cc3e51c05bc83bbc9332807b79484d..97cfde68b1b9097152aca3ea68705e6cdfddfaad 100644 --- a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLInsertSparseEntity.java +++ b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLInsertSparseEntity.java @@ -22,6 +22,7 @@ */ package caosdb.server.database.backend.implementation.MySQL; +import caosdb.server.database.DatabaseUtils; import caosdb.server.database.access.Access; import caosdb.server.database.backend.interfaces.InsertSparseEntityImpl; import caosdb.server.database.exceptions.IntegrityException; @@ -56,6 +57,7 @@ public class MySQLInsertSparseEntity extends MySQLTransaction implements InsertS try (final ResultSet rs = insertEntityStmt.executeQuery()) { if (rs.next()) { entity.id = rs.getInt("EntityID"); + entity.version = DatabaseUtils.bytes2UTF8(rs.getBytes("Version")); } else { throw new TransactionException("Didn't get new EntityID back."); } diff --git a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveParents.java b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveParents.java index c1c430cfee6e7a6b88391aa02df072e918f8d692..721ba566ed389732456342aab78691c171b33922 100644 --- a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveParents.java +++ b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveParents.java @@ -30,6 +30,7 @@ import caosdb.server.database.proto.VerySparseEntity; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Types; import java.util.ArrayList; public class MySQLRetrieveParents extends MySQLTransaction implements RetrieveParentsImpl { @@ -38,16 +39,22 @@ public class MySQLRetrieveParents extends MySQLTransaction implements RetrievePa super(access); } - private static final String stmtStr = "call retrieveEntityParents(?)"; + private static final String stmtStr = "call retrieveEntityParents(?, ?)"; @Override - public ArrayList<VerySparseEntity> execute(final Integer id) throws TransactionException { + public ArrayList<VerySparseEntity> execute(final Integer id, final String version) + throws TransactionException { try { ResultSet rs = null; try { final PreparedStatement prepareStatement = prepareStatement(stmtStr); prepareStatement.setInt(1, id); + if (version == null) { + prepareStatement.setNull(2, Types.VARBINARY); + } else { + prepareStatement.setString(2, version); + } rs = prepareStatement.executeQuery(); return DatabaseUtils.parseParentResultSet(rs); } finally { diff --git a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveProperties.java b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveProperties.java index 8b9a547bac69cf221dbf7c204a93e95326bb484c..9b6704c4cb3e6395c5b8239a3aefc34bb818e937 100644 --- a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveProperties.java +++ b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveProperties.java @@ -31,6 +31,7 @@ import caosdb.server.database.proto.ProtoProperty; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Types; import java.util.ArrayList; import java.util.List; @@ -40,15 +41,17 @@ public class MySQLRetrieveProperties extends MySQLTransaction implements Retriev super(access); } - private static final String stmtStr = "call retrieveEntityProperties(?,?)"; - private static final String stmtStr2 = "call retrieveOverrides(?,?)"; + private static final String stmtStr = "call retrieveEntityProperties(?,?,?)"; + private static final String stmtStr2 = "call retrieveOverrides(?,?,?)"; @Override - public ArrayList<ProtoProperty> execute(final Integer entity) throws TransactionException { + public ArrayList<ProtoProperty> execute(final Integer entity, final String version) + throws TransactionException { try { final PreparedStatement prepareStatement = prepareStatement(stmtStr); - final List<FlatProperty> props = retrieveFlatPropertiesStage1(0, entity, prepareStatement); + final List<FlatProperty> props = + retrieveFlatPropertiesStage1(0, entity, version, prepareStatement); final ArrayList<ProtoProperty> protos = new ArrayList<ProtoProperty>(); for (final FlatProperty p : props) { @@ -56,7 +59,7 @@ public class MySQLRetrieveProperties extends MySQLTransaction implements Retriev proto.property = p; final List<FlatProperty> subProps = - retrieveFlatPropertiesStage1(entity, p.id, prepareStatement); + retrieveFlatPropertiesStage1(entity, p.id, version, prepareStatement); proto.subProperties = subProps; @@ -71,7 +74,10 @@ public class MySQLRetrieveProperties extends MySQLTransaction implements Retriev } private List<FlatProperty> retrieveFlatPropertiesStage1( - final Integer domain, final Integer entity, final PreparedStatement stmt) + final Integer domain, + final Integer entity, + final String version, + final PreparedStatement stmt) throws SQLException, ConnectionException { ResultSet rs = null; try { @@ -82,6 +88,12 @@ public class MySQLRetrieveProperties extends MySQLTransaction implements Retriev } stmt.setInt(2, entity); + if (version == null) { + stmt.setNull(3, Types.VARBINARY); + } else { + stmt.setString(3, version); + } + long t1 = System.currentTimeMillis(); rs = stmt.executeQuery(); long t2 = System.currentTimeMillis(); @@ -91,7 +103,7 @@ public class MySQLRetrieveProperties extends MySQLTransaction implements Retriev final PreparedStatement stmt2 = prepareStatement(stmtStr2); - retrieveOverrides(domain, entity, stmt2, props); + retrieveOverrides(domain, entity, version, stmt2, props); return props; } finally { @@ -104,6 +116,7 @@ public class MySQLRetrieveProperties extends MySQLTransaction implements Retriev private void retrieveOverrides( final Integer domain, final Integer entity, + final String version, final PreparedStatement stmt2, final List<FlatProperty> props) throws SQLException { @@ -112,6 +125,11 @@ public class MySQLRetrieveProperties extends MySQLTransaction implements Retriev try { stmt2.setInt(1, domain); stmt2.setInt(2, entity); + if (version == null) { + stmt2.setNull(3, Types.VARBINARY); + } else { + stmt2.setString(3, version); + } long t1 = System.currentTimeMillis(); rs = stmt2.executeQuery(); long t2 = System.currentTimeMillis(); diff --git a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveQueryTemplateDefinition.java b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveQueryTemplateDefinition.java index 92f2cd5fe4c26d796112baec5d908b9431187ae7..c6d88ec9f8fca1651318456161ed2c0347a47f21 100644 --- a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveQueryTemplateDefinition.java +++ b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveQueryTemplateDefinition.java @@ -28,6 +28,7 @@ import caosdb.server.database.exceptions.TransactionException; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Types; public class MySQLRetrieveQueryTemplateDefinition extends MySQLTransaction implements RetrieveQueryTemplateDefinitionImpl { @@ -37,14 +38,19 @@ public class MySQLRetrieveQueryTemplateDefinition extends MySQLTransaction } public static final String STMT_RETRIEVE_QUERY_TEMPLATE_DEF = - "SELECT definition FROM query_template_def WHERE id=?"; + "call retrieveQueryTemplateDef(?,?)"; @Override - public String retrieve(final Integer id) { + public String retrieve(final Integer id, final String version) { try { final PreparedStatement stmt = prepareStatement(STMT_RETRIEVE_QUERY_TEMPLATE_DEF); stmt.setInt(1, id); + if (version == null) { + stmt.setNull(2, Types.VARBINARY); + } else { + stmt.setString(2, version); + } final ResultSet rs = stmt.executeQuery(); if (rs.next()) { return rs.getString("definition"); diff --git a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveSparseEntity.java b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveSparseEntity.java index 2aff2aaedda70672eb676597a2e3ba7a1db14352..097961974b0c599480e8186f170665838e7d046e 100644 --- a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveSparseEntity.java +++ b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveSparseEntity.java @@ -30,6 +30,7 @@ import caosdb.server.database.proto.SparseEntity; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Types; /** * Retrieve a single SparseEntity by id. @@ -43,14 +44,19 @@ public class MySQLRetrieveSparseEntity extends MySQLTransaction super(access); } - private static final String stmtStr = "call retrieveEntity(?)"; + private static final String stmtStr = "call retrieveEntity(?,?)"; @Override - public SparseEntity execute(final int id) throws TransactionException { + public SparseEntity execute(final int id, final String version) throws TransactionException { try { final PreparedStatement preparedStatement = prepareStatement(stmtStr); preparedStatement.setInt(1, id); + if (version == null) { + preparedStatement.setNull(2, Types.VARBINARY); + } else { + preparedStatement.setString(2, version); + } try (final ResultSet rs = preparedStatement.executeQuery()) { if (rs.next()) { return DatabaseUtils.parseEntityResultSet(rs); diff --git a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveVersionHistory.java b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveVersionHistory.java new file mode 100644 index 0000000000000000000000000000000000000000..22ac0c2e30c0f48fbc884f40593bac2dc45d7b7d --- /dev/null +++ b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLRetrieveVersionHistory.java @@ -0,0 +1,85 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 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/>. + * + * ** end header + */ +package caosdb.server.database.backend.implementation.MySQL; + +import caosdb.server.database.DatabaseUtils; +import caosdb.server.database.access.Access; +import caosdb.server.database.backend.interfaces.RetrieveVersionHistoryImpl; +import caosdb.server.database.exceptions.TransactionException; +import caosdb.server.database.proto.VersionHistoryItem; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.LinkedList; + +/** + * Transaction to retrieve all versions of an entity. + * + * <p>Creates a mapping ID :: (VersionHistoryItem) + */ +public class MySQLRetrieveVersionHistory extends MySQLTransaction + implements RetrieveVersionHistoryImpl { + + public static final String VERSION_HISTORY_STMT = "CALL get_version_history(?)"; + + public MySQLRetrieveVersionHistory(Access access) { + super(access); + } + + @Override + public HashMap<String, VersionHistoryItem> execute(Integer entityId) { + + HashMap<String, VersionHistoryItem> result = new HashMap<>(); + try { + PreparedStatement s = prepareStatement(VERSION_HISTORY_STMT); + s.setInt(1, entityId); + ResultSet rs = s.executeQuery(); + + while (rs.next()) { + String childId = DatabaseUtils.bytes2UTF8(rs.getBytes("child")); + String parentId = DatabaseUtils.bytes2UTF8(rs.getBytes("parent")); + Long childSeconds = rs.getLong("child_seconds"); + Integer childNanos = rs.getInt("child_nanos"); + VersionHistoryItem v = result.get(childId); + if (v == null) { + v = new VersionHistoryItem(); + v.id = childId; + v.seconds = childSeconds; + v.nanos = childNanos; + result.put(childId, v); + } + + if (parentId != null) { + if (v.parents == null) { + v.parents = new LinkedList<>(); + } + v.parents.add(parentId); + } + } + } catch (SQLException | ConnectionException e) { + throw new TransactionException(e); + } + return result; + } +} diff --git a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLUpdateSparseEntity.java b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLUpdateSparseEntity.java index 4cc59f9d0f0a813bb92294f6fabb1cd51f43c151..73c589fade317586cc1b5bfb4b796578f9e819ba 100644 --- a/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLUpdateSparseEntity.java +++ b/src/main/java/caosdb/server/database/backend/implementation/MySQL/MySQLUpdateSparseEntity.java @@ -22,12 +22,14 @@ */ package caosdb.server.database.backend.implementation.MySQL; +import caosdb.server.database.DatabaseUtils; import caosdb.server.database.access.Access; import caosdb.server.database.backend.interfaces.UpdateSparseEntityImpl; import caosdb.server.database.exceptions.IntegrityException; import caosdb.server.database.exceptions.TransactionException; import caosdb.server.database.proto.SparseEntity; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLIntegrityConstraintViolationException; import java.sql.Types; @@ -39,15 +41,31 @@ public class MySQLUpdateSparseEntity extends MySQLTransaction implements UpdateS } public static final String STMT_UPDATE_ENTITY = "call updateEntity(?,?,?,?,?,?,?)"; - public static final String STMT_UPDATE_FILE_PROPS = - "INSERT INTO files (hash, size, path, file_id) VALUES (unhex(?),?,?,?) ON DUPLICATE KEY UPDATE hash=unhex(?), size=?, path=?;"; + public static final String STMT_UPDATE_FILE_PROPS = "call setFileProperties(?,?,?,?)"; @Override public void execute(final SparseEntity spe) throws TransactionException { try { - final PreparedStatement updateEntityStmt = prepareStatement(STMT_UPDATE_ENTITY); + // file properties; + final PreparedStatement updateFilePropsStmt = prepareStatement(STMT_UPDATE_FILE_PROPS); + updateFilePropsStmt.setInt(1, spe.id); + if (spe.filePath != null) { + updateFilePropsStmt.setString(2, spe.filePath); + updateFilePropsStmt.setLong(3, spe.fileSize); + if (spe.fileHash != null) { + updateFilePropsStmt.setString(4, spe.fileHash); + } else { + updateFilePropsStmt.setNull(4, Types.VARCHAR); + } + } else { + updateFilePropsStmt.setNull(2, Types.VARCHAR); + updateFilePropsStmt.setNull(3, Types.BIGINT); + updateFilePropsStmt.setNull(4, Types.VARCHAR); + } + updateFilePropsStmt.execute(); // very sparse entity + final PreparedStatement updateEntityStmt = prepareStatement(STMT_UPDATE_ENTITY); updateEntityStmt.setInt(1, spe.id); updateEntityStmt.setString(2, spe.name); updateEntityStmt.setString(3, spe.description); @@ -59,28 +77,12 @@ public class MySQLUpdateSparseEntity extends MySQLTransaction implements UpdateS } updateEntityStmt.setString(6, spe.collection); updateEntityStmt.setString(7, spe.acl); - updateEntityStmt.execute(); + ResultSet rs = updateEntityStmt.executeQuery(); - // file properties; - if (spe.filePath != null) { - final PreparedStatement updateFilePropsStmt = prepareStatement(STMT_UPDATE_FILE_PROPS); - if (spe.fileHash != null) { - updateFilePropsStmt.setString(1, spe.fileHash); - updateFilePropsStmt.setString(5, spe.fileHash); - } else { - updateFilePropsStmt.setNull(1, Types.VARCHAR); - updateFilePropsStmt.setNull(5, Types.VARCHAR); - } - updateFilePropsStmt.setLong(2, spe.fileSize); - updateFilePropsStmt.setLong(6, spe.fileSize); - - updateFilePropsStmt.setString(3, spe.filePath); - updateFilePropsStmt.setString(7, spe.filePath); - - updateFilePropsStmt.setInt(4, spe.id); - - updateFilePropsStmt.execute(); + if (rs.next()) { + spe.version = DatabaseUtils.bytes2UTF8(rs.getBytes("Version")); } + } catch (final SQLIntegrityConstraintViolationException e) { throw new IntegrityException(e); } catch (final SQLException e) { diff --git a/src/main/java/caosdb/server/database/backend/interfaces/RetrieveParentsImpl.java b/src/main/java/caosdb/server/database/backend/interfaces/RetrieveParentsImpl.java index 551ee962d8768aa90b9f5aaed313c050f0873079..9dfb8cf57c260c7fe154c25f095886ca6a3c2698 100644 --- a/src/main/java/caosdb/server/database/backend/interfaces/RetrieveParentsImpl.java +++ b/src/main/java/caosdb/server/database/backend/interfaces/RetrieveParentsImpl.java @@ -28,5 +28,6 @@ import java.util.ArrayList; public interface RetrieveParentsImpl extends BackendTransactionImpl { - public ArrayList<VerySparseEntity> execute(Integer id) throws TransactionException; + public ArrayList<VerySparseEntity> execute(Integer id, String version) + throws TransactionException; } diff --git a/src/main/java/caosdb/server/database/backend/interfaces/RetrievePropertiesImpl.java b/src/main/java/caosdb/server/database/backend/interfaces/RetrievePropertiesImpl.java index 3c8449f7fc62581457091ea5ab0ccc5921b89a92..b9a15a526b857b9674432a96733c6f2fd8e5d4ea 100644 --- a/src/main/java/caosdb/server/database/backend/interfaces/RetrievePropertiesImpl.java +++ b/src/main/java/caosdb/server/database/backend/interfaces/RetrievePropertiesImpl.java @@ -28,5 +28,5 @@ import java.util.ArrayList; public interface RetrievePropertiesImpl extends BackendTransactionImpl { - public ArrayList<ProtoProperty> execute(Integer id) throws TransactionException; + public ArrayList<ProtoProperty> execute(Integer id, String version) throws TransactionException; } diff --git a/src/main/java/caosdb/server/database/backend/interfaces/RetrieveQueryTemplateDefinitionImpl.java b/src/main/java/caosdb/server/database/backend/interfaces/RetrieveQueryTemplateDefinitionImpl.java index fee860ca2c0ee4341ae91a4a1736d518cd71a5e8..cda336044de4bd0d37a3d67da140c744f2699f32 100644 --- a/src/main/java/caosdb/server/database/backend/interfaces/RetrieveQueryTemplateDefinitionImpl.java +++ b/src/main/java/caosdb/server/database/backend/interfaces/RetrieveQueryTemplateDefinitionImpl.java @@ -24,5 +24,5 @@ package caosdb.server.database.backend.interfaces; public interface RetrieveQueryTemplateDefinitionImpl extends BackendTransactionImpl { - public String retrieve(final Integer id); + public String retrieve(Integer id, String version); } diff --git a/src/main/java/caosdb/server/database/backend/interfaces/RetrieveSparseEntityImpl.java b/src/main/java/caosdb/server/database/backend/interfaces/RetrieveSparseEntityImpl.java index 82a534c03182d577e7b27eca9b2ff9def1c60677..5b6978f8f456bf978c3ca60ae0da479eaf54ed0a 100644 --- a/src/main/java/caosdb/server/database/backend/interfaces/RetrieveSparseEntityImpl.java +++ b/src/main/java/caosdb/server/database/backend/interfaces/RetrieveSparseEntityImpl.java @@ -27,5 +27,5 @@ import caosdb.server.database.proto.SparseEntity; public interface RetrieveSparseEntityImpl extends BackendTransactionImpl { - public SparseEntity execute(int id) throws TransactionException; + public SparseEntity execute(int id, String version) throws TransactionException; } diff --git a/src/main/java/caosdb/server/database/backend/interfaces/RetrieveVersionHistoryImpl.java b/src/main/java/caosdb/server/database/backend/interfaces/RetrieveVersionHistoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..118e783d816ce45d1a43738bf8959e4a94740d3c --- /dev/null +++ b/src/main/java/caosdb/server/database/backend/interfaces/RetrieveVersionHistoryImpl.java @@ -0,0 +1,9 @@ +package caosdb.server.database.backend.interfaces; + +import caosdb.server.database.proto.VersionHistoryItem; +import java.util.HashMap; + +public interface RetrieveVersionHistoryImpl extends BackendTransactionImpl { + + public HashMap<String, VersionHistoryItem> execute(Integer entityId); +} diff --git a/src/main/java/caosdb/server/database/backend/interfaces/SetFileChecksumImpl.java b/src/main/java/caosdb/server/database/backend/interfaces/SetFileChecksumImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..be73b33ecfa646dc7f19b2db3d38a8a464065f09 --- /dev/null +++ b/src/main/java/caosdb/server/database/backend/interfaces/SetFileChecksumImpl.java @@ -0,0 +1,6 @@ +package caosdb.server.database.backend.interfaces; + +public interface SetFileChecksumImpl extends BackendTransactionImpl { + + void execute(Integer id, String checksum); +} diff --git a/src/main/java/caosdb/server/database/backend/transaction/DeleteEntityProperties.java b/src/main/java/caosdb/server/database/backend/transaction/DeleteEntityProperties.java index 15ace2b9820398cb270a6a1e098e6f642b66ab9d..984ca5eb853e356814aafb69ebd9860bee4fe8a1 100644 --- a/src/main/java/caosdb/server/database/backend/transaction/DeleteEntityProperties.java +++ b/src/main/java/caosdb/server/database/backend/transaction/DeleteEntityProperties.java @@ -40,8 +40,8 @@ public class DeleteEntityProperties extends BackendTransaction { @Override public void execute() { - RetrieveProperties.removeCached(this.entity.getId()); - RetrieveParents.removeCached(this.entity.getId()); + RetrieveProperties.removeCached(this.entity.getIdVersion()); + RetrieveParents.removeCached(this.entity.getIdVersion()); final DeleteEntityPropertiesImpl ret = getImplementation(DeleteEntityPropertiesImpl.class); diff --git a/src/main/java/caosdb/server/database/backend/transaction/DeleteSparseEntity.java b/src/main/java/caosdb/server/database/backend/transaction/DeleteSparseEntity.java index 39deb1416c743f838bfc1b96c29fd845dbcef742..a2e4de6482d78053ab01c70ea3634c3f8af232a3 100644 --- a/src/main/java/caosdb/server/database/backend/transaction/DeleteSparseEntity.java +++ b/src/main/java/caosdb/server/database/backend/transaction/DeleteSparseEntity.java @@ -42,7 +42,7 @@ public class DeleteSparseEntity extends BackendTransaction { @Override protected void execute() { - RetrieveSparseEntity.removeCached(this.entity.getId()); + RetrieveSparseEntity.removeCached(this.entity); if (entity.hasFileProperties()) { GetFileRecordByPath.removeCached(this.entity.getFileProperties().getPath()); } diff --git a/src/main/java/caosdb/server/database/backend/transaction/InsertSparseEntity.java b/src/main/java/caosdb/server/database/backend/transaction/InsertSparseEntity.java index f8711197064a88dd92c408f329d1a36df55c5ed2..c8e20895711e16c40c75c89c9776e8ad55851117 100644 --- a/src/main/java/caosdb/server/database/backend/transaction/InsertSparseEntity.java +++ b/src/main/java/caosdb/server/database/backend/transaction/InsertSparseEntity.java @@ -30,6 +30,7 @@ import caosdb.server.database.exceptions.IntegrityException; import caosdb.server.database.exceptions.TransactionException; import caosdb.server.database.proto.SparseEntity; import caosdb.server.entity.EntityInterface; +import caosdb.server.entity.Version; import caosdb.server.utils.Undoable; public class InsertSparseEntity extends BackendTransaction { @@ -68,5 +69,6 @@ public class InsertSparseEntity extends BackendTransaction { public void cleanUp() {} }); this.entity.setId(e.id); + this.entity.setVersion(new Version(e.version)); } } diff --git a/src/main/java/caosdb/server/database/backend/transaction/RetrieveFullEntity.java b/src/main/java/caosdb/server/database/backend/transaction/RetrieveFullEntity.java index 7ac8c1592e0cb275e60008532cdabb33919369a9..ad0d39e94b56904def08a04a8a42a89627a7b4d6 100644 --- a/src/main/java/caosdb/server/database/backend/transaction/RetrieveFullEntity.java +++ b/src/main/java/caosdb/server/database/backend/transaction/RetrieveFullEntity.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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 @@ -23,12 +25,32 @@ package caosdb.server.database.backend.transaction; import caosdb.server.database.BackendTransaction; +import caosdb.server.datatype.ReferenceDatatype; +import caosdb.server.datatype.ReferenceValue; import caosdb.server.entity.EntityInterface; +import caosdb.server.entity.Message; import caosdb.server.entity.RetrieveEntity; import caosdb.server.entity.Role; import caosdb.server.entity.container.Container; +import caosdb.server.entity.wrapper.Property; +import caosdb.server.query.Query; +import caosdb.server.query.Query.Selection; import caosdb.server.utils.EntityStatus; +import java.util.LinkedList; +import java.util.List; +/** + * Retrieve the full entity from the backend - with all parents, properties, file properties and so + * on. + * + * <p>TODO: This class should rather be called FullEntityRetrieval or FullEntityRetrieveTransaction. + * + * <p>When the entity which is to be retrieved has a defined list of {@link Query.Selection} which + * select properties from referenced entities, the referenced entities are retrieved as well. + * Otherwise, only the referenced id is retrieved and the entity stays rather flat. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ public class RetrieveFullEntity extends BackendTransaction { private final Container<? extends EntityInterface> container; @@ -49,22 +71,107 @@ public class RetrieveFullEntity extends BackendTransaction { @Override public void execute() { - for (final EntityInterface e : this.container) { + retrieveFullEntitiesInContainer(this.container); + } + + /** + * Retrieve the entities in the container. + * + * @param container + */ + public void retrieveFullEntitiesInContainer(Container<? extends EntityInterface> container) { + for (final EntityInterface e : container) { if (e.hasId() && e.getId() > 0 && e.getEntityStatus() == EntityStatus.QUALIFIED) { + retrieveFullEntity(e, e.getSelections()); + } + } + } - execute(new RetrieveSparseEntity(e)); - if (e.getEntityStatus() == EntityStatus.VALID) { - if (e.getRole() == Role.QueryTemplate) { - execute(new RetrieveQueryTemplateDefinition(e)); - } - execute(new RetrieveParents(e)); + /** + * Retrieve a single full entity. + * + * <p>If the selections are not empty, retrieve the referenced entities matching the 'selections' + * as well. + * + * <p>This method is called recursively during the retrieval of the referenced entities. + * + * @param e The entity. + * @param selections + */ + public void retrieveFullEntity(EntityInterface e, List<Selection> selections) { + execute(new RetrieveSparseEntity(e)); + + if (e.getEntityStatus() == EntityStatus.VALID) { + if (e.getRole() == Role.QueryTemplate) { + execute(new RetrieveQueryTemplateDefinition(e)); + } + execute(new RetrieveParents(e)); + execute(new RetrieveProperties(e)); + execute(new RetrieveVersionInfo(e)); - execute(new RetrieveProperties(e)); + // recursion! retrieveSubEntities calls retrieveFull sometimes, but with reduced selectors. + if (selections != null && !selections.isEmpty()) { + retrieveSubEntities(e, selections); + } + } + } + + /** + * Retrieve the Entities which match the selections and are referenced by the Entity 'e'. + * + * @param e + * @param selections + */ + public void retrieveSubEntities(EntityInterface e, List<Selection> selections) { + for (final Selection s : selections) { + if (s.getSubselection() != null) { + + String propertyName = s.getSelector(); + + // Find matching (i.e. referencing) Properties + for (Property p : e.getProperties()) { + // get reference properties by name. + if (propertyName.equalsIgnoreCase(p.getName()) + && p.getDatatype() instanceof ReferenceDatatype) { + if (p.getValue() != null) { + try { + p.parseValue(); + } catch (Message m) { + p.addError(m); + } + + ReferenceValue value = (ReferenceValue) p.getValue(); + RetrieveEntity ref = new RetrieveEntity(value.getId()); + // recursion! (Only for the matching selections) + retrieveFullEntity(ref, getSubSelects(selections, propertyName)); + value.setEntity(ref, true); + } + } } } } } + /** + * Return all non-null subselects of those selections which match the given select String. + * + * <p>Effectively, this reduces the depth of the selections by one (and drops non-matching + * selections). + * + * @param selections + * @param select + * @return A new list of Selections. + */ + public List<Selection> getSubSelects(List<Selection> selections, String select) { + List<Selection> result = new LinkedList<>(); + for (Selection s : selections) { + if (s.getSelector().equalsIgnoreCase(select) && s.getSubselection() != null) { + result.add(s.getSubselection()); + } + } + return result; + } + public Container<? extends EntityInterface> getContainer() { return container; } diff --git a/src/main/java/caosdb/server/database/backend/transaction/RetrieveParents.java b/src/main/java/caosdb/server/database/backend/transaction/RetrieveParents.java index 5c1b0178f51496f83a4fac6a4ce3b2447abdccd5..d33d5e96d978fe86d6eb965113726eb79bb60d40 100644 --- a/src/main/java/caosdb/server/database/backend/transaction/RetrieveParents.java +++ b/src/main/java/caosdb/server/database/backend/transaction/RetrieveParents.java @@ -34,10 +34,22 @@ import caosdb.server.entity.EntityInterface; import java.util.ArrayList; import org.apache.commons.jcs.access.behavior.ICacheAccess; +// TODO Problem with the caching. +// When an old entity version has a parent which is deleted, the name is +// still in the cached VerySparseEntity. This can be resolved by using a +// similar strategy as in RetrieveProperties.java where the name etc. are +// retrieved in a second step. Thus the deletion doesn't slip through +// unnoticed. +// +// Changes are necessary in the backend-api, i.e. mysqlbackend and the +// interfaces as well. +// +// See also a failing test in caosdb-pyinttest: +// tests/test_version.py::test_bug_cached_parent_name_in_old_version public class RetrieveParents - extends CacheableBackendTransaction<Integer, ArrayList<VerySparseEntity>> { + extends CacheableBackendTransaction<String, ArrayList<VerySparseEntity>> { - private static final ICacheAccess<Integer, ArrayList<VerySparseEntity>> cache = + private static final ICacheAccess<String, ArrayList<VerySparseEntity>> cache = Cache.getCache("BACKEND_EntityParents"); /** @@ -45,9 +57,9 @@ public class RetrieveParents * * @param id */ - public static void removeCached(final Integer id) { - if (id != null && cache != null) { - cache.remove(id); + public static void removeCached(final String idVersion) { + if (idVersion != null && cache != null) { + cache.remove(idVersion); } } @@ -61,18 +73,18 @@ public class RetrieveParents @Override public ArrayList<VerySparseEntity> executeNoCache() throws TransactionException { final RetrieveParentsImpl t = getImplementation(RetrieveParentsImpl.class); - final Integer key = getKey(); - return t.execute(key); + return t.execute(this.entity.getId(), this.entity.getVersion().getId()); } @Override protected void process(final ArrayList<VerySparseEntity> t) throws TransactionException { this.entity.getParents().clear(); + DatabaseUtils.parseParentsFromVerySparseEntity(this.entity, t); } @Override - protected Integer getKey() { - return this.entity.getId(); + protected String getKey() { + return this.entity.getIdVersion(); } } diff --git a/src/main/java/caosdb/server/database/backend/transaction/RetrieveProperties.java b/src/main/java/caosdb/server/database/backend/transaction/RetrieveProperties.java index 487feb56cf848e948fcacb8f33f49e4a0bc2267d..822f57fd49e54d7517578592acf09100924fe81b 100644 --- a/src/main/java/caosdb/server/database/backend/transaction/RetrieveProperties.java +++ b/src/main/java/caosdb/server/database/backend/transaction/RetrieveProperties.java @@ -37,11 +37,11 @@ import java.util.ArrayList; import org.apache.commons.jcs.access.behavior.ICacheAccess; public class RetrieveProperties - extends CacheableBackendTransaction<Integer, ArrayList<ProtoProperty>> { + extends CacheableBackendTransaction<String, ArrayList<ProtoProperty>> { private final EntityInterface entity; public static final String CACHE_REGION = "BACKEND_EntityProperties"; - private static final ICacheAccess<Integer, ArrayList<ProtoProperty>> cache = + private static final ICacheAccess<String, ArrayList<ProtoProperty>> cache = Cache.getCache(CACHE_REGION); /** @@ -49,9 +49,9 @@ public class RetrieveProperties * * @param id */ - protected static void removeCached(final Integer id) { - if (id != null && cache != null) { - cache.remove(id); + protected static void removeCached(final String idVersion) { + if (idVersion != null && cache != null) { + cache.remove(idVersion); } } @@ -63,7 +63,7 @@ public class RetrieveProperties @Override public ArrayList<ProtoProperty> executeNoCache() throws TransactionException { final RetrievePropertiesImpl t = getImplementation(RetrievePropertiesImpl.class); - return t.execute(getKey()); + return t.execute(this.entity.getId(), this.entity.getVersion().getId()); } @Override @@ -98,7 +98,7 @@ public class RetrieveProperties } @Override - protected Integer getKey() { - return this.entity.getId(); + protected String getKey() { + return this.entity.getIdVersion(); } } diff --git a/src/main/java/caosdb/server/database/backend/transaction/RetrieveQueryTemplateDefinition.java b/src/main/java/caosdb/server/database/backend/transaction/RetrieveQueryTemplateDefinition.java index 208c7439aa6cf9ff8f32abc8f5cd59ea9a3cba95..0c96c72cb0be41aaf368c22009526914994bc8ec 100644 --- a/src/main/java/caosdb/server/database/backend/transaction/RetrieveQueryTemplateDefinition.java +++ b/src/main/java/caosdb/server/database/backend/transaction/RetrieveQueryTemplateDefinition.java @@ -39,6 +39,7 @@ public class RetrieveQueryTemplateDefinition extends BackendTransaction { protected void execute() throws TransactionException { final RetrieveQueryTemplateDefinitionImpl t = getImplementation(RetrieveQueryTemplateDefinitionImpl.class); - this.entity.setQueryTemplateDefinition(t.retrieve(this.entity.getId())); + this.entity.setQueryTemplateDefinition( + t.retrieve(this.entity.getId(), this.entity.getVersion().getId())); } } diff --git a/src/main/java/caosdb/server/database/backend/transaction/RetrieveSparseEntity.java b/src/main/java/caosdb/server/database/backend/transaction/RetrieveSparseEntity.java index a91b5021f1fa722a8e8831234fc8ebc2032ba115..7843f57957d392c4bbf65f01a60d559cff11c6c8 100644 --- a/src/main/java/caosdb/server/database/backend/transaction/RetrieveSparseEntity.java +++ b/src/main/java/caosdb/server/database/backend/transaction/RetrieveSparseEntity.java @@ -3,9 +3,9 @@ * 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) + * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2019,2020 IndiScale GmbH + * Copyright (C) 2019,2020 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 @@ -35,20 +35,21 @@ import caosdb.server.entity.EntityInterface; import caosdb.server.utils.EntityStatus; import org.apache.commons.jcs.access.behavior.ICacheAccess; -public class RetrieveSparseEntity extends CacheableBackendTransaction<Integer, SparseEntity> { +public class RetrieveSparseEntity extends CacheableBackendTransaction<String, SparseEntity> { private final EntityInterface entity; - private static final ICacheAccess<Integer, SparseEntity> cache = + private static final ICacheAccess<String, SparseEntity> cache = Cache.getCache("BACKEND_SparseEntities"); /** - * To be called by {@link UpdateSparseEntity} and {@link DeleteEntity} on execution. + * To be called by {@link UpdateSparseEntity} and {@link DeleteSparseEntity} on execution. * - * @param id + * @param entity */ - public static void removeCached(final Integer id) { - if (id != null && cache != null) { - cache.remove(id); + public static void removeCached(final EntityInterface entity) { + if (entity != null && cache != null) { + cache.remove(entity.getId().toString()); + cache.remove(entity.getIdVersion()); } } @@ -57,14 +58,15 @@ public class RetrieveSparseEntity extends CacheableBackendTransaction<Integer, S this.entity = entity; } - public RetrieveSparseEntity(final int id) { + public RetrieveSparseEntity(final int id, final String version) { this(new Entity(id)); + this.entity.getVersion().setId(version); } @Override public SparseEntity executeNoCache() throws TransactionException { final RetrieveSparseEntityImpl t = getImplementation(RetrieveSparseEntityImpl.class); - final SparseEntity ret = t.execute(getKey()); + final SparseEntity ret = t.execute(getEntity().getId(), getEntity().getVersion().getId()); if (ret == null) { this.entity.setEntityStatus(EntityStatus.NONEXISTENT); } @@ -78,8 +80,14 @@ public class RetrieveSparseEntity extends CacheableBackendTransaction<Integer, S } @Override - protected Integer getKey() { - return this.entity.getId(); + protected String getKey() { + if ("HEAD".equalsIgnoreCase(entity.getVersion().getId())) { + return this.entity.getId().toString(); + } else if (entity.hasVersion() + && entity.getVersion().getId().toUpperCase().startsWith("HEAD")) { + return null; + } + return this.entity.getIdVersion(); } public EntityInterface getEntity() { diff --git a/src/main/java/caosdb/server/database/backend/transaction/RetrieveVersionHistory.java b/src/main/java/caosdb/server/database/backend/transaction/RetrieveVersionHistory.java new file mode 100644 index 0000000000000000000000000000000000000000..20be2f8fb60ce54877fdb2fd85d27a6944a71d86 --- /dev/null +++ b/src/main/java/caosdb/server/database/backend/transaction/RetrieveVersionHistory.java @@ -0,0 +1,79 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 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/>. + * + * ** end header + */ +package caosdb.server.database.backend.transaction; + +import caosdb.server.database.CacheableBackendTransaction; +import caosdb.server.database.backend.interfaces.RetrieveVersionHistoryImpl; +import caosdb.server.database.exceptions.TransactionException; +import caosdb.server.database.proto.VersionHistoryItem; +import caosdb.server.entity.EntityInterface; +import java.util.Collection; +import java.util.HashMap; + +public abstract class RetrieveVersionHistory + extends CacheableBackendTransaction<Integer, HashMap<String, VersionHistoryItem>> { + + // TODO + // private static final ICacheAccess<String, Version> cache = + // Cache.getCache("BACKEND_RetrieveVersionHistory"); + private EntityInterface entity; + private HashMap<String, VersionHistoryItem> map; + + public static void removeCached(Integer entityId) { + // TODO + } + + public RetrieveVersionHistory(EntityInterface e) { + super(null); // TODO caching + this.entity = e; + } + + @Override + public HashMap<String, VersionHistoryItem> executeNoCache() throws TransactionException { + RetrieveVersionHistoryImpl impl = getImplementation(RetrieveVersionHistoryImpl.class); + return impl.execute(getKey()); + } + + /** After this method call, the version map is available to the object. */ + @Override + protected void process(HashMap<String, VersionHistoryItem> map) throws TransactionException { + this.map = map; + } + + @Override + protected Integer getKey() { + return entity.getId(); + } + + public HashMap<String, VersionHistoryItem> getMap() { + return this.map; + } + + public EntityInterface getEntity() { + return this.entity; + } + + public Collection<VersionHistoryItem> getList() { + return this.map.values(); + } +} diff --git a/src/main/java/caosdb/server/database/backend/transaction/RetrieveVersionInfo.java b/src/main/java/caosdb/server/database/backend/transaction/RetrieveVersionInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..1daab0504cafbd05e9e03dff78b2c230a0438ccd --- /dev/null +++ b/src/main/java/caosdb/server/database/backend/transaction/RetrieveVersionInfo.java @@ -0,0 +1,84 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 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/>. + * + * ** end header + */ +package caosdb.server.database.backend.transaction; + +import caosdb.datetime.UTCDateTime; +import caosdb.server.database.exceptions.TransactionException; +import caosdb.server.database.proto.VersionHistoryItem; +import caosdb.server.entity.EntityInterface; +import caosdb.server.entity.Version; +import java.util.HashMap; +import java.util.LinkedList; + +public class RetrieveVersionInfo extends RetrieveVersionHistory { + + public RetrieveVersionInfo(EntityInterface e) { + super(e); + } + + @Override + protected void process(HashMap<String, VersionHistoryItem> map) throws TransactionException { + super.process(map); // Make the map available to the object. + if (!map.isEmpty()) getVersion(); + } + + public Version getVersion() { + Version v = getEntity().getVersion(); + VersionHistoryItem i = getMap().get(v.getId()); + if (i != null) v.setDate(UTCDateTime.UTCSeconds(i.seconds, i.nanos)); + + v.setPredecessors(getPredecessors(v.getId())); + v.setSuccessors(getSuccessors(v.getId())); + return v; + } + + /** Return a list of direct children. */ + private LinkedList<Version> getSuccessors(String id) { + LinkedList<Version> result = new LinkedList<>(); + + outer: + for (VersionHistoryItem i : getList()) { + if (i.parents != null) + for (String p : i.parents) { + if (id.equals(p)) { + Version successor = new Version(i.id, i.seconds, i.nanos); + result.add(successor); + continue outer; + } + } + } + return result; + } + + /** Return a list of direct parents. */ + private LinkedList<Version> getPredecessors(String id) { + LinkedList<Version> result = new LinkedList<>(); + if (getMap().containsKey(id) && getMap().get(id).parents != null) + for (String p : getMap().get(id).parents) { + VersionHistoryItem i = getMap().get(p); + Version predecessor = new Version(i.id, i.seconds, i.nanos); + result.add(predecessor); + } + return result; + } +} diff --git a/src/main/java/caosdb/server/database/backend/transaction/SetFileChecksum.java b/src/main/java/caosdb/server/database/backend/transaction/SetFileChecksum.java new file mode 100644 index 0000000000000000000000000000000000000000..b19bceabef39d48cdd778835f3d2840bbec29297 --- /dev/null +++ b/src/main/java/caosdb/server/database/backend/transaction/SetFileChecksum.java @@ -0,0 +1,26 @@ +package caosdb.server.database.backend.transaction; + +import caosdb.server.database.BackendTransaction; +import caosdb.server.database.backend.interfaces.SetFileChecksumImpl; +import caosdb.server.entity.EntityInterface; + +public class SetFileChecksum extends BackendTransaction { + + private EntityInterface entity; + + public SetFileChecksum(EntityInterface entity) { + this.entity = entity; + } + + @Override + protected void execute() { + RetrieveSparseEntity.removeCached(this.entity); + if (entity.hasFileProperties()) { + GetFileRecordByPath.removeCached(this.entity.getFileProperties().getPath()); + + final SetFileChecksumImpl t = getImplementation(SetFileChecksumImpl.class); + + t.execute(this.entity.getId(), this.entity.getFileProperties().getChecksum()); + } + } +} diff --git a/src/main/java/caosdb/server/database/backend/transaction/UpdateEntity.java b/src/main/java/caosdb/server/database/backend/transaction/UpdateEntity.java index 6378e8bbbbd8aee87382d226df0691d004e641aa..a7c9ba43f32b1ef97a6a09544041b39a2cd5c71a 100644 --- a/src/main/java/caosdb/server/database/backend/transaction/UpdateEntity.java +++ b/src/main/java/caosdb/server/database/backend/transaction/UpdateEntity.java @@ -53,6 +53,8 @@ public class UpdateEntity extends BackendTransaction { execute(new InsertEntityValue(e)); execute(new InsertEntityProperties(e)); + + execute(new RetrieveVersionInfo(e)); } } } diff --git a/src/main/java/caosdb/server/database/backend/transaction/UpdateSparseEntity.java b/src/main/java/caosdb/server/database/backend/transaction/UpdateSparseEntity.java index c6a1c7683409e0c875484e14553e4aeac2757a71..309ff9cbde0131f4b90cc89301a2338a8b92971f 100644 --- a/src/main/java/caosdb/server/database/backend/transaction/UpdateSparseEntity.java +++ b/src/main/java/caosdb/server/database/backend/transaction/UpdateSparseEntity.java @@ -29,6 +29,7 @@ import caosdb.server.database.backend.interfaces.UpdateSparseEntityImpl; import caosdb.server.database.exceptions.TransactionException; import caosdb.server.database.proto.SparseEntity; import caosdb.server.entity.EntityInterface; +import caosdb.server.entity.Version; public class UpdateSparseEntity extends BackendTransaction { @@ -40,7 +41,7 @@ public class UpdateSparseEntity extends BackendTransaction { @Override public void execute() throws TransactionException { - RetrieveSparseEntity.removeCached(this.entity.getId()); + RetrieveSparseEntity.removeCached(this.entity); if (entity.hasFileProperties()) { GetFileRecordByPath.removeCached(this.entity.getFileProperties().getPath()); } @@ -50,5 +51,7 @@ public class UpdateSparseEntity extends BackendTransaction { final SparseEntity spe = this.entity.getSparseEntity(); t.execute(spe); + + this.entity.setVersion(new Version(spe.version)); } } diff --git a/src/main/java/caosdb/server/database/proto/SparseEntity.java b/src/main/java/caosdb/server/database/proto/SparseEntity.java index 4f5dcc69ce5ed7c6afa636b97237b53abb56ab51..d70d4ae026eb72d8c782b763ae3fc21d03a73064 100644 --- a/src/main/java/caosdb/server/database/proto/SparseEntity.java +++ b/src/main/java/caosdb/server/database/proto/SparseEntity.java @@ -38,6 +38,9 @@ public class SparseEntity extends VerySparseEntity { public String filePath = null; public Long fileSize = null; public Long fileChecked = null; + public String version = null; + public Long versionSeconds = null; + public Integer versionNanos = null; @Override public String toString() { @@ -51,6 +54,9 @@ public class SparseEntity extends VerySparseEntity { .append(this.fileHash) .append(this.filePath) .append(this.fileSize) + .append(this.version) + .append(this.versionSeconds) + .append(this.versionNanos) .toString(); } } diff --git a/src/main/java/caosdb/server/database/proto/VersionHistoryItem.java b/src/main/java/caosdb/server/database/proto/VersionHistoryItem.java new file mode 100644 index 0000000000000000000000000000000000000000..e671950f39f40c0b93069d031f636ad964560508 --- /dev/null +++ b/src/main/java/caosdb/server/database/proto/VersionHistoryItem.java @@ -0,0 +1,13 @@ +package caosdb.server.database.proto; + +import java.io.Serializable; +import java.util.LinkedList; + +public class VersionHistoryItem implements Serializable { + + private static final long serialVersionUID = 7855704308135158698L; + public String id = null; + public LinkedList<String> parents = null; + public Long seconds = null; + public Integer nanos = null; +} diff --git a/src/main/java/caosdb/server/datatype/ReferenceDatatype2.java b/src/main/java/caosdb/server/datatype/ReferenceDatatype2.java index ce92aa022cfe85573049eaaeed4e26653b400679..87c17079b0facdc193ea79dd6005598f7fe151b6 100644 --- a/src/main/java/caosdb/server/datatype/ReferenceDatatype2.java +++ b/src/main/java/caosdb/server/datatype/ReferenceDatatype2.java @@ -47,7 +47,7 @@ public class ReferenceDatatype2 extends ReferenceDatatype { } public void setEntity(final EntityInterface datatypeEntity) { - this.refid.setEntity(datatypeEntity); + this.refid.setEntity(datatypeEntity, false); } @Override diff --git a/src/main/java/caosdb/server/datatype/ReferenceValue.java b/src/main/java/caosdb/server/datatype/ReferenceValue.java index 25fa0b381cb436236dd6e3676970cf7e54c17776..77f7aa8c56d97b8dc5abf914c0b6c88749683d69 100644 --- a/src/main/java/caosdb/server/datatype/ReferenceValue.java +++ b/src/main/java/caosdb/server/datatype/ReferenceValue.java @@ -3,7 +3,9 @@ * 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 + * 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> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -26,27 +28,36 @@ import caosdb.server.datatype.AbstractDatatype.Table; import caosdb.server.entity.EntityInterface; import caosdb.server.entity.Message; import caosdb.server.utils.ServerMessages; +import java.util.Objects; import org.jdom2.Element; +/** + * A ReferenceValue represents the value of a reference property to another entity. + * + * <p>Differently from other properties, they may be versioned, i.e. they may reference to a + * specific version of an entity. + * + * <p>TODO: Ways to specify a reference value, what are the consequences of versioned references? + */ public class ReferenceValue implements SingleValue { private EntityInterface entity = null; private String name = null; private Integer id = null; + private String version = null; + private boolean versioned = false; public static ReferenceValue parseReference(final Object reference) throws Message { if (reference == null) { return null; } if (reference instanceof EntityInterface) { - return new ReferenceValue((EntityInterface) reference); + return new ReferenceValue( + (EntityInterface) reference, ((EntityInterface) reference).hasVersion()); } else if (reference instanceof ReferenceValue) { return (ReferenceValue) reference; } else if (reference instanceof GenericValue) { - try { - return new ReferenceValue(Integer.parseInt(((GenericValue) reference).toDatabaseString())); - } catch (final NumberFormatException e) { - return new ReferenceValue(((GenericValue) reference).toDatabaseString()); - } + String str = ((GenericValue) reference).toDatabaseString(); + return parseFromString(str); } else if (reference instanceof CollectionValue) { throw ServerMessages.DATA_TYPE_DOES_NOT_ACCEPT_COLLECTION_VALUES; } else { @@ -58,24 +69,81 @@ public class ReferenceValue implements SingleValue { } } + /** + * Split a reference string into an entity part and a version part, if there is a version part. + * + * <p>If parsing the entity ID part to an integer fails, a NumberFormatException may be thrown. + */ + public static ReferenceValue parseIdVersion(String str) { + String[] split = str.split("@", 2); + if (split.length == 2) { + return new ReferenceValue(Integer.parseInt(split[0]), split[1]); + } else { + return new ReferenceValue(Integer.parseInt(str)); + } + } + + /** + * Create a ReferenceValue from a string. + * + * <p>If the string looks like a valid "entityID@version" string, the result will have the + * corresponding entity and version parts. + */ + public static ReferenceValue parseFromString(String str) { + try { + return parseIdVersion(str); + } catch (final NumberFormatException e) { + return new ReferenceValue(str); + } + } + + /** + * Produce a nice but short string: + * + * <p>Case 1 "versioned" (reference to an entity without specifying that entity's version): + * Produces a string like "1234" or "Experiment". Note that referencing via name is never + * versioned. + * + * <p>Case 2 "unversioned" (reference to an entity with a specified version): Produces a string + * like "1234@ab987f". + */ @Override public String toString() { - if (this.entity != null) { + if (this.entity != null && versioned) { // Was specified as "versioned", with resolved entity + return this.entity.getIdVersion(); + } else if (this.entity != null) { // resolved, but unversioned return this.entity.getId().toString(); - } else if (this.id == null && this.name != null) { + } else if (this.id == null + && this.name != null) { // Only name is available, no id (and thus no resolved entity) return this.name; } + // Specification via id is the only remaining possibility + return getIdVersion(); // if version is null, returns ID only + } + + public String getIdVersion() { + if (this.version != null) { + return new StringBuilder().append(this.id).append("@").append(this.version).toString(); + } return this.id.toString(); } - public ReferenceValue(final EntityInterface entity) { + public ReferenceValue(final EntityInterface entity, boolean versioned) { + this.versioned = versioned; this.entity = entity; } public ReferenceValue(final Integer id) { + this(id, null); + } + + public ReferenceValue(final Integer id, final String version) { this.id = id; + this.version = version; + this.versioned = version != null; } + /** If the reference is given by name, versioning is not possible (at the moment). */ public ReferenceValue(final String name) { this.name = name; } @@ -84,7 +152,8 @@ public class ReferenceValue implements SingleValue { return this.entity; } - public final void setEntity(final EntityInterface entity) { + public final void setEntity(final EntityInterface entity, boolean versioned) { + this.versioned = versioned; this.entity = entity; } @@ -102,6 +171,13 @@ public class ReferenceValue implements SingleValue { return this.id; } + public final String getVersion() { + if (this.entity != null && versioned && this.entity.hasVersion()) { + return this.entity.getVersion().getId(); + } + return this.version; + } + public final void setId(final Integer id) { this.id = id; } @@ -126,7 +202,8 @@ public class ReferenceValue implements SingleValue { if (obj instanceof ReferenceValue) { final ReferenceValue that = (ReferenceValue) obj; if (that.getId() != null && getId() != null) { - return that.getId().equals(getId()); + return that.getId().equals(getId()) + && Objects.deepEquals(that.getVersion(), this.getVersion()); } else if (that.getName() != null && getName() != null) { return that.getName().equals(getName()); } diff --git a/src/main/java/caosdb/server/entity/BaseEntity.java b/src/main/java/caosdb/server/entity/BaseEntity.java deleted file mode 100644 index 62d4873b54c262eb75920f3f5c990e9988f07e50..0000000000000000000000000000000000000000 --- a/src/main/java/caosdb/server/entity/BaseEntity.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * ** 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 - * 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/>. - * - * ** end header - */ -package caosdb.server.entity; - -class BaseEntity { - - EntityID id = null; -} diff --git a/src/main/java/caosdb/server/entity/DeleteEntity.java b/src/main/java/caosdb/server/entity/DeleteEntity.java index 29b6a3a075b60e811cfb0e66d066278a47d2edcb..b27ab3a7745a9b4909dac642a9fccbaf035f52f7 100644 --- a/src/main/java/caosdb/server/entity/DeleteEntity.java +++ b/src/main/java/caosdb/server/entity/DeleteEntity.java @@ -3,7 +3,9 @@ * 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 + * 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> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -27,4 +29,9 @@ public class DeleteEntity extends Entity { public DeleteEntity(final int id) { super(id); } + + public DeleteEntity(int id, String version) { + super(id); + setVersion(new Version(version)); + } } diff --git a/src/main/java/caosdb/server/entity/Entity.java b/src/main/java/caosdb/server/entity/Entity.java index 5c997d7ff8f9f98191ba768c82569a79ca056cf2..16f4f508e2d97a4c0f58c1775e95db9efbf4ffba 100644 --- a/src/main/java/caosdb/server/entity/Entity.java +++ b/src/main/java/caosdb/server/entity/Entity.java @@ -3,7 +3,9 @@ * 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 + * 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> * * 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 +24,7 @@ */ package caosdb.server.entity; +import caosdb.datetime.UTCDateTime; import caosdb.server.CaosDBException; import caosdb.server.database.proto.SparseEntity; import caosdb.server.database.proto.VerySparseEntity; @@ -521,6 +524,7 @@ public class Entity extends AbstractObservable implements EntityInterface { } } + // Strategy to convert this Entity to an XML element. private ToElementStrategy toElementStrategy = null; @Override @@ -535,7 +539,12 @@ public class Entity extends AbstractObservable implements EntityInterface { @Override public final void addToElement(final Element element) { - getToElementStrategy().addToElement(this, element, new SetFieldStrategy(getSelections())); + addToElement(element, new SetFieldStrategy(getSelections())); + } + + @Override + public void addToElement(Element element, SetFieldStrategy strategy) { + getToElementStrategy().addToElement(this, element, strategy); } /** @@ -794,7 +803,11 @@ public class Entity extends AbstractObservable implements EntityInterface { final CollectionValue vals = new CollectionValue(); int pidx = 0; for (final Element pe : element.getChildren()) { - if (pe.getName().equalsIgnoreCase("EmptyString")) { + if (pe.getName().equalsIgnoreCase("Version")) { + // IGNORE: Once it becomes allowed for clients to set a version id, parsing + // the Version element would be done here. Until this is the case, the + // Version tag is ignored. + } else if (pe.getName().equalsIgnoreCase("EmptyString")) { // special case: empty string which cannot be distinguished from null // values otherwise. setValue(new GenericValue("")); @@ -1020,6 +1033,7 @@ public class Entity extends AbstractObservable implements EntityInterface { } private boolean datatypeOverride = false; + private Version version = new Version(); @Override public EntityInterface setDatatypeOverride(final boolean b) { @@ -1060,10 +1074,15 @@ public class Entity extends AbstractObservable implements EntityInterface { } @Override - public EntityInterface parseSparseEntity(final SparseEntity spe) { + public final EntityInterface parseSparseEntity(final SparseEntity spe) { setId(spe.id); this.setRole(spe.role); setEntityACL(spe.acl); + UTCDateTime versionDate = null; + if (spe.versionSeconds != null) { + versionDate = UTCDateTime.UTCSeconds(spe.versionSeconds, spe.versionNanos); + } + this.version = new Version(spe.version, versionDate); if (!isNameOverride()) { setName(spe.name); @@ -1112,4 +1131,34 @@ public class Entity extends AbstractObservable implements EntityInterface { public boolean skipJob() { return false; } + + @Override + public Version getVersion() { + return this.version; + } + + @Override + public boolean hasVersion() { + return this.version.getId() != null; + } + + @Override + public void setVersion(Version version) { + this.version = version; + } + + /** Return "id@version" if there is versioning information, else only "id". */ + @Override + public String getIdVersion() { + if (!this.hasId()) { + return null; + } else if (this.hasVersion()) { + return new StringBuilder() + .append(getId()) + .append("@") + .append(getVersion().getId()) + .toString(); + } + return getId().toString(); + } } diff --git a/src/main/java/caosdb/server/entity/EntityID.java b/src/main/java/caosdb/server/entity/EntityID.java deleted file mode 100644 index fbfb83c8028f5f2e0d8376c6dc480c19046c0d5b..0000000000000000000000000000000000000000 --- a/src/main/java/caosdb/server/entity/EntityID.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * ** 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 - * 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/>. - * - * ** end header - */ -package caosdb.server.entity; - -public class EntityID {} diff --git a/src/main/java/caosdb/server/entity/EntityInterface.java b/src/main/java/caosdb/server/entity/EntityInterface.java index 21ed5cd85234bd633914302ce2982b2b6f4f9a66..4205111a58c30185d916c1d921acb7470d93f7a9 100644 --- a/src/main/java/caosdb/server/entity/EntityInterface.java +++ b/src/main/java/caosdb/server/entity/EntityInterface.java @@ -31,6 +31,7 @@ import caosdb.server.entity.container.PropertyContainer; import caosdb.server.entity.wrapper.Domain; import caosdb.server.entity.wrapper.Parent; import caosdb.server.entity.wrapper.Property; +import caosdb.server.entity.xml.SetFieldStrategy; import caosdb.server.entity.xml.ToElementable; import caosdb.server.jobs.JobTarget; import caosdb.server.permissions.EntityACL; @@ -40,6 +41,7 @@ import caosdb.unit.Unit; import java.util.List; import org.apache.shiro.authz.Permission; import org.apache.shiro.subject.Subject; +import org.jdom2.Element; public interface EntityInterface extends JobTarget, Observable, ToElementable, WriteEntity, TransactionEntity { @@ -50,6 +52,8 @@ public interface EntityInterface public abstract Integer getId(); + public abstract String getIdVersion(); + public abstract void setId(Integer id); public abstract boolean hasId(); @@ -182,4 +186,12 @@ public interface EntityInterface public abstract String getQueryTemplateDefinition(); public abstract void setQueryTemplateDefinition(String query); + + public abstract Version getVersion(); + + public abstract boolean hasVersion(); + + public abstract void setVersion(Version version); + + public abstract void addToElement(Element element, SetFieldStrategy strategy); } diff --git a/src/main/java/caosdb/server/entity/FileProperties.java b/src/main/java/caosdb/server/entity/FileProperties.java index fbf8031f56566ea5a6df263d97611812bab113d0..c0fcaa468b8d2ff2ff353842d5c79c0fcf904f28 100644 --- a/src/main/java/caosdb/server/entity/FileProperties.java +++ b/src/main/java/caosdb/server/entity/FileProperties.java @@ -220,10 +220,10 @@ public class FileProperties { if (file.getAbsolutePath().startsWith(FileSystem.getBasepath())) { final Undoable d; final File parent = file.getParentFile(); - if (file.getCanonicalPath().startsWith(FileSystem.getBasepath())) { - d = FileUtils.delete(file, file.isDirectory()); - } else if (FileUtils.isSymlink(file)) { + if (FileUtils.isSymlink(file)) { d = FileUtils.unlink(file); + } else if (file.getCanonicalPath().startsWith(FileSystem.getBasepath())) { + d = FileUtils.delete(file, file.isDirectory()); } else { throw new CaosDBException( "File is in Filesystem, but it is neither a normal file nor a symlink."); diff --git a/src/main/java/caosdb/server/entity/NamedEntity.java b/src/main/java/caosdb/server/entity/NamedEntity.java deleted file mode 100644 index 76ba79a317e146ca4b86793545f95d9e05e42480..0000000000000000000000000000000000000000 --- a/src/main/java/caosdb/server/entity/NamedEntity.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * ** 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 - * 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/>. - * - * ** end header - */ -package caosdb.server.entity; - -class NamedEntity extends BaseEntity { - - String name = null; -} diff --git a/src/main/java/caosdb/server/entity/RetrieveEntity.java b/src/main/java/caosdb/server/entity/RetrieveEntity.java index 5b54c2bcd4ce401bc469728f774ad68198ba0986..7035232b5a66df77518c8c1b586b7bcb43b7a22b 100644 --- a/src/main/java/caosdb/server/entity/RetrieveEntity.java +++ b/src/main/java/caosdb/server/entity/RetrieveEntity.java @@ -3,7 +3,9 @@ * 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 + * 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> * * 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,9 +24,6 @@ */ package caosdb.server.entity; -import caosdb.server.database.proto.SparseEntity; -import caosdb.server.datatype.AbstractCollectionDatatype; - public class RetrieveEntity extends Entity { public RetrieveEntity(final int id) { @@ -35,38 +34,13 @@ public class RetrieveEntity extends Entity { super(name); } - public RetrieveEntity parseSparseEntity(final SparseEntity spe) { - setId(spe.id); - this.setRole(spe.role); - setEntityACL(spe.acl); - - if (!isNameOverride()) { - setName(spe.name); - } - if (!isDescOverride()) { - setDescription(spe.description); - } - if (!isDatatypeOverride()) { - final String dt = spe.datatype; - final String col = spe.collection; - - if (dt != null - && !dt.equalsIgnoreCase("null") - && (!hasDatatype() || !dt.equalsIgnoreCase(getDatatype().toString()))) { - if (col != null && !col.equalsIgnoreCase("null")) { - this.setDatatype(AbstractCollectionDatatype.collectionDatatypeFactory(col, dt)); - } else { - this.setDatatype(dt); - } - } - } - - if (spe.filePath != null) { - setFileProperties(new FileProperties(spe.fileHash, spe.filePath, spe.fileSize)); - } else { - setFileProperties(null); - } + public RetrieveEntity(int id, String version) { + super(id); + this.setVersion(new Version(version)); + } - return this; + public RetrieveEntity(String name, String version) { + super(name); + this.setVersion(new Version(version)); } } diff --git a/src/main/java/caosdb/server/entity/Version.java b/src/main/java/caosdb/server/entity/Version.java new file mode 100644 index 0000000000000000000000000000000000000000..07c7387e8c419f962e1174504a28ceceda5c30e3 --- /dev/null +++ b/src/main/java/caosdb/server/entity/Version.java @@ -0,0 +1,84 @@ +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 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 caosdb.server.entity; + +import caosdb.datetime.UTCDateTime; +import java.util.LinkedList; + +/** + * Plain old java object (POJO) for an entity's version. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ +public class Version { + + private String id = null; + private LinkedList<Version> predecessors = null; + private LinkedList<Version> successors = null; + private UTCDateTime date = null; + + public Version(String id, long seconds, int nanos) { + this(id, UTCDateTime.UTCSeconds(seconds, nanos)); + } + + public Version(String id, UTCDateTime date) { + this.id = id; + this.date = date; + } + + public Version(String id) { + this(id, null); + } + + public Version() {} + + public UTCDateTime getDate() { + return date; + } + + public void setDate(UTCDateTime date) { + this.date = date; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public LinkedList<Version> getSuccessors() { + return successors; + } + + public void setSuccessors(LinkedList<Version> successors) { + this.successors = successors; + } + + public LinkedList<Version> getPredecessors() { + return predecessors; + } + + public void setPredecessors(LinkedList<Version> predecessors) { + this.predecessors = predecessors; + } +} diff --git a/src/main/java/caosdb/server/entity/container/Container.java b/src/main/java/caosdb/server/entity/container/Container.java index 9ab0415d8c4ccc8767814eaa46a8cacfe74d594a..de20b6a3965fc4fead82612193d061e075d5217d 100644 --- a/src/main/java/caosdb/server/entity/container/Container.java +++ b/src/main/java/caosdb/server/entity/container/Container.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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 @@ -27,5 +29,17 @@ import java.util.ArrayList; public class Container<T extends EntityInterface> extends ArrayList<T> { - private static final long serialVersionUID = 3476714088253567549L; + private static final long serialVersionUID = 8519435849678175750L; + + /** + * Return the entity with the matching id, if it can be found inside this Container, else null. + */ + public T getEntityById(final Integer id) { + for (final T e : this) { + if (e.hasId() && e.getId().equals(id)) { + return e; + } + } + return null; + } } diff --git a/src/main/java/caosdb/server/entity/container/DeleteContainer.java b/src/main/java/caosdb/server/entity/container/DeleteContainer.java index c219ee5e4399caeee857006f995748435d023963..c3d57bc656a7c9609dd8fb6ca4d031e0e82f63fa 100644 --- a/src/main/java/caosdb/server/entity/container/DeleteContainer.java +++ b/src/main/java/caosdb/server/entity/container/DeleteContainer.java @@ -42,4 +42,9 @@ public class DeleteContainer extends EntityByIdContainer { public void add(final int id) { add(new DeleteEntity(id)); } + + @Override + public void add(int id, String version) { + add(new DeleteEntity(id, version)); + } } diff --git a/src/main/java/caosdb/server/entity/container/EntityByIdContainer.java b/src/main/java/caosdb/server/entity/container/EntityByIdContainer.java index 1bfda2024c4ff11375ad619d64946e1036c714b0..29a0978b8acdc809b75c52de643ee131cd62b129 100644 --- a/src/main/java/caosdb/server/entity/container/EntityByIdContainer.java +++ b/src/main/java/caosdb/server/entity/container/EntityByIdContainer.java @@ -37,4 +37,6 @@ public abstract class EntityByIdContainer extends TransactionContainer { } public abstract void add(int id); + + public abstract void add(int id, String version); } diff --git a/src/main/java/caosdb/server/entity/container/PropertyContainer.java b/src/main/java/caosdb/server/entity/container/PropertyContainer.java index a80e2bfccea9f4b9c5cbe17fc32f9dd20b74c995..80cf816156c0d4d81bca1e5b0e0de19c38fcb032 100644 --- a/src/main/java/caosdb/server/entity/container/PropertyContainer.java +++ b/src/main/java/caosdb/server/entity/container/PropertyContainer.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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 @@ -50,6 +52,7 @@ public class PropertyContainer extends Container<Property> { this.s = new PropertyToElementStrategy(); } + /** Sort the properties by their pidx (Property Index). */ public void sort() { Collections.sort( this, @@ -64,14 +67,32 @@ public class PropertyContainer extends Container<Property> { }); } - public Element addToElement(final Element element, final SetFieldStrategy setFieldStrategy) { + /** + * Add a single property to the element using the given setFieldStrategy. + * + * @param property + * @param element + * @param setFieldStrategy + */ + public void addToElement( + EntityInterface property, Element element, SetFieldStrategy setFieldStrategy) { + if (setFieldStrategy.isToBeSet(property.getName())) { + SetFieldStrategy strategy = setFieldStrategy.forProperty(property.getName()); + this.s.addToElement(property, element, strategy); + } + } + + /** + * Add all properties to the element using the given setFieldStrategy. + * + * @param element + * @param setFieldStrategy + */ + public void addToElement(final Element element, final SetFieldStrategy setFieldStrategy) { sort(); for (final EntityInterface property : this) { - if (setFieldStrategy.isToBeSet(property.getName())) { - this.s.addToElement(property, element, setFieldStrategy.forProperty(property.getName())); - } + addToElement(property, element, setFieldStrategy); } - return element; } @Override diff --git a/src/main/java/caosdb/server/entity/container/RetrieveContainer.java b/src/main/java/caosdb/server/entity/container/RetrieveContainer.java index 9b4f7364d4ecdc63da1be8f1ead292545801a078..5b205bc27f38e3d1a04b2ed3c5a7e874af229489 100644 --- a/src/main/java/caosdb/server/entity/container/RetrieveContainer.java +++ b/src/main/java/caosdb/server/entity/container/RetrieveContainer.java @@ -46,4 +46,13 @@ public class RetrieveContainer extends EntityByIdContainer { public void add(final String name) { add(new RetrieveEntity(name)); } + + public void add(final String name, String version) { + add(new RetrieveEntity(name, version)); + } + + @Override + public void add(int id, String version) { + add(new RetrieveEntity(id, version)); + } } diff --git a/src/main/java/caosdb/server/entity/container/TransactionContainer.java b/src/main/java/caosdb/server/entity/container/TransactionContainer.java index f0ac55fae7b57d24456728508baa7768b83a5711..f3a8a0f395838e6c8f5b2a4d8adf6f5b8627d06c 100644 --- a/src/main/java/caosdb/server/entity/container/TransactionContainer.java +++ b/src/main/java/caosdb/server/entity/container/TransactionContainer.java @@ -145,15 +145,6 @@ public class TransactionContainer extends Container<EntityInterface> } } - public EntityInterface getEntityById(final Integer id) { - for (final EntityInterface e : this) { - if (e.hasId() && e.getId().equals(id)) { - return e; - } - } - return null; - } - public Subject getOwner() { return this.owner; } diff --git a/src/main/java/caosdb/server/entity/wrapper/EntityWrapper.java b/src/main/java/caosdb/server/entity/wrapper/EntityWrapper.java index 4d419bbfc17aee5749db70976f1a510ae468032b..f8d563de1776d155b2f66b913de454c7e5b952fd 100644 --- a/src/main/java/caosdb/server/entity/wrapper/EntityWrapper.java +++ b/src/main/java/caosdb/server/entity/wrapper/EntityWrapper.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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 @@ -31,8 +33,10 @@ import caosdb.server.entity.FileProperties; import caosdb.server.entity.Message; import caosdb.server.entity.Role; import caosdb.server.entity.StatementStatus; +import caosdb.server.entity.Version; import caosdb.server.entity.container.ParentContainer; import caosdb.server.entity.container.PropertyContainer; +import caosdb.server.entity.xml.SetFieldStrategy; import caosdb.server.entity.xml.ToElementStrategy; import caosdb.server.entity.xml.ToElementable; import caosdb.server.permissions.EntityACL; @@ -551,4 +555,29 @@ public class EntityWrapper implements EntityInterface { public boolean hasPermission(Subject subject, Permission permission) { return this.entity.hasPermission(subject, permission); } + + @Override + public Version getVersion() { + return this.entity.getVersion(); + } + + @Override + public boolean hasVersion() { + return this.entity.hasVersion(); + } + + @Override + public void setVersion(Version version) { + this.entity.setVersion(version); + } + + @Override + public String getIdVersion() { + return this.entity.getIdVersion(); + } + + @Override + public void addToElement(Element element, SetFieldStrategy strategy) { + this.entity.addToElement(element, strategy); + } } diff --git a/src/main/java/caosdb/server/entity/wrapper/Parent.java b/src/main/java/caosdb/server/entity/wrapper/Parent.java index c36ffa9c2c1a16062add2eb912ca01132af9872c..f3686b5b802e004e55d572e58076e5e08bb5988d 100644 --- a/src/main/java/caosdb/server/entity/wrapper/Parent.java +++ b/src/main/java/caosdb/server/entity/wrapper/Parent.java @@ -57,4 +57,10 @@ public class Parent extends EntityWrapper { public Affiliation getAffiliation() { return this.affiliation; } + + @Override + public boolean hasVersion() { + // parents are not versioned (yet). + return false; + } } diff --git a/src/main/java/caosdb/server/entity/wrapper/Property.java b/src/main/java/caosdb/server/entity/wrapper/Property.java index f6bb840a75d9216ee3fd56f3d2bf95f701280492..1d1cd2a8ae11489479b9e9191365843182d136ca 100644 --- a/src/main/java/caosdb/server/entity/wrapper/Property.java +++ b/src/main/java/caosdb/server/entity/wrapper/Property.java @@ -134,4 +134,10 @@ public class Property extends EntityWrapper { public EntityInterface getDomainEntity() { return this.domain; } + + @Override + public boolean hasVersion() { + // properties are not versioned (yet). + return false; + } } diff --git a/src/main/java/caosdb/server/entity/xml/DomainToElementStrategy.java b/src/main/java/caosdb/server/entity/xml/DomainToElementStrategy.java index 2559cfd12d7ca03344d03fa025ff2078d1bbcca0..099b51044353f3074ece5cef77a7588a658bab81 100644 --- a/src/main/java/caosdb/server/entity/xml/DomainToElementStrategy.java +++ b/src/main/java/caosdb/server/entity/xml/DomainToElementStrategy.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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 @@ -25,11 +27,22 @@ package caosdb.server.entity.xml; import caosdb.server.entity.EntityInterface; import org.jdom2.Element; -public class DomainToElementStrategy implements ToElementStrategy { +/** + * Generates a JDOM (XML) representation of an entity with role "Domain". + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ +public class DomainToElementStrategy extends EntityToElementStrategy { + + public DomainToElementStrategy() { + super("Domain"); + } @Override public Element toElement(final EntityInterface entity, final SetFieldStrategy setFieldStrategy) { - return EntityToElementStrategy.sparseEntityToElement("Domain", entity, setFieldStrategy); + Element element = new Element(tagName); + sparseEntityToElement(element, entity, setFieldStrategy); + return element; } @Override diff --git a/src/main/java/caosdb/server/entity/xml/EntityToElementStrategy.java b/src/main/java/caosdb/server/entity/xml/EntityToElementStrategy.java index 19bfe80c6b5a35b8cc214ffb6c260294633b8495..6cfe8fa9f3628cc0670ccb3e6f30a263a0bc5cf3 100644 --- a/src/main/java/caosdb/server/entity/xml/EntityToElementStrategy.java +++ b/src/main/java/caosdb/server/entity/xml/EntityToElementStrategy.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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 @@ -22,27 +24,62 @@ */ package caosdb.server.entity.xml; +import caosdb.server.datatype.ReferenceValue; import caosdb.server.entity.EntityInterface; import caosdb.server.entity.Message; import caosdb.server.utils.EntityStatus; import caosdb.server.utils.TransactionLogMessage; -import java.util.Comparator; import org.apache.shiro.SecurityUtils; -import org.jdom2.Content; -import org.jdom2.Content.CType; +import org.jdom2.Attribute; import org.jdom2.Element; +/** + * Base class for the generation of a JDOM (XML) representation for entities. + * + * <p>Record and RecordType entities use this class only. Properties, Parents, Files and other + * entities have specialized sub classes. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ public class EntityToElementStrategy implements ToElementStrategy { - private final String tagName; + protected final String tagName; public EntityToElementStrategy(final String tagName) { this.tagName = tagName; } - public static Element sparseEntityToElement( - final String tagName, final EntityInterface entity, final SetFieldStrategy setFieldStrategy) { - final Element element = new Element(tagName); + /** + * Set the data type of this entity as a JDOM {@link Attribute} of the given element. + * + * <p>If the data type has a name, the name is used, otherwise the id is used. + * + * @param entity + * @param element + */ + public void setDatatype(EntityInterface entity, Element element) { + if (entity.getDatatype().getName() != null) { + element.setAttribute("datatype", entity.getDatatype().getName()); + } else { + element.setAttribute("datatype", entity.getDatatype().getId().toString()); + } + } + + /** + * Set all properties of the entity that are considered to be part of the sparse entity, e.g. + * name, description, etc, as {@link Attribute} of the given element. + * + * <p>The setFieldStrategy decides which attributes are set if present and which are omitted in + * any case. + * + * @param element + * @param entity + * @param setFieldStrategy + */ + public void sparseEntityToElement( + final Element element, + final EntityInterface entity, + final SetFieldStrategy setFieldStrategy) { if (entity.getEntityACL() != null) { element.addContent(entity.getEntityACL().getPermissionsFor(SecurityUtils.getSubject())); @@ -50,6 +87,10 @@ public class EntityToElementStrategy implements ToElementStrategy { if (setFieldStrategy.isToBeSet("id") && entity.hasId()) { element.setAttribute("id", Integer.toString(entity.getId())); } + if (setFieldStrategy.isToBeSet("version") && entity.hasVersion()) { + Element v = new VersionXMLSerializer().toElement(entity.getVersion()); + element.addContent(v); + } if (setFieldStrategy.isToBeSet("cuid") && entity.hasCuid()) { element.setAttribute("cuid", entity.getCuid()); } @@ -60,11 +101,7 @@ public class EntityToElementStrategy implements ToElementStrategy { element.setAttribute("description", entity.getDescription()); } if (setFieldStrategy.isToBeSet("datatype") && entity.hasDatatype()) { - if (entity.getDatatype().getName() != null) { - element.setAttribute("datatype", entity.getDatatype().getName()); - } else { - element.setAttribute("datatype", entity.getDatatype().getId().toString()); - } + setDatatype(entity, element); } if (setFieldStrategy.isToBeSet("message") && entity.hasMessages()) { for (final ToElementable m : entity.getMessages()) { @@ -76,55 +113,78 @@ public class EntityToElementStrategy implements ToElementStrategy { q.setText(entity.getQueryTemplateDefinition()); element.addContent(q); } + } - return element; + /** + * Set the value of the entity. + * + * <p>The setFieldStrategy decides if the value is to be set at all. + * + * <p>If the value is a reference, the setFieldStrategy decides whether the referenced entity is + * added as a deep Element tree (as a whole, so to speak) or just the ID of the referenced entity. + * + * @param entity + * @param element + * @param setFieldStrategy + */ + public void setValue(EntityInterface entity, Element element, SetFieldStrategy setFieldStrategy) { + if (entity.hasValue()) { + try { + entity.parseValue(); + } catch (final Message | NullPointerException e) { + // Ignore. Parsing the value failed. But that does not concern us here, because this is the + // case when a write transaction failed. The error for that has already been handled by the + // CheckValueParsable job. + } + + if (entity.getValue() instanceof ReferenceValue + && setFieldStrategy.isToBeSet("_referenced")) { + // Append the complete entity. This needs to be done when we are + // processing SELECT Queries. + EntityInterface ref = ((ReferenceValue) entity.getValue()).getEntity(); + if (ref != null) { + if (entity.hasDatatype()) { + setDatatype(entity, element); + } + ref.addToElement(element, setFieldStrategy); + // the referenced entity has been appended. Return here to suppress + // adding the reference id as well. + return; + } + } + + if (setFieldStrategy.isToBeSet("value")) { + if (entity.hasDatatype()) { + setDatatype(entity, element); + } + entity.getValue().addToElement(element); + } + } } @Override public Element toElement(final EntityInterface entity, final SetFieldStrategy setFieldStrategy) { - final Element element = sparseEntityToElement(this.tagName, entity, setFieldStrategy); + final Element element = new Element(tagName); + + // always have the values at the beginning of the children + setValue(entity, element, setFieldStrategy); - if (setFieldStrategy.isToBeSet("importance") && entity.hasStatementStatus()) { + sparseEntityToElement(element, entity, setFieldStrategy); + + if (entity.hasStatementStatus() && setFieldStrategy.isToBeSet("importance")) { element.setAttribute("importance", entity.getStatementStatus().toString()); } - if (setFieldStrategy.isToBeSet("parent") && entity.hasParents()) { + if (entity.hasParents() && setFieldStrategy.isToBeSet("parent")) { entity.getParents().addToElement(element); } if (entity.hasProperties()) { entity.getProperties().addToElement(element, setFieldStrategy); } - if (setFieldStrategy.isToBeSet("history") && entity.hasTransactionLogMessages()) { + if (entity.hasTransactionLogMessages() && setFieldStrategy.isToBeSet("history")) { for (final TransactionLogMessage t : entity.getTransactionLogMessages()) { t.xmlAppendTo(element); } } - if (setFieldStrategy.isToBeSet("value") && entity.hasValue()) { - if (entity.hasDatatype()) { - try { - - entity.getDatatype().parseValue(entity.getValue()).addToElement(element); - } catch (final Message e) { - // - } - } else { - entity.getValue().addToElement(element); - } - // put value at first position - element.sortContent( - new Comparator<Content>() { - - @Override - public int compare(final Content o1, final Content o2) { - if (o1.getCType() == CType.CDATA || o1.getCType() == CType.Text) { - return -1; - } - if (o2.getCType() == CType.CDATA || o2.getCType() == CType.Text) { - return 1; - } - return 0; - } - }); - } return element; } diff --git a/src/main/java/caosdb/server/entity/xml/ParentToElementStrategy.java b/src/main/java/caosdb/server/entity/xml/ParentToElementStrategy.java index 0d79951f5554b4c2c346224f3b6e91c1c056a96e..2498e00bfda219eb5e724a7c38bc73cc4ad3468e 100644 --- a/src/main/java/caosdb/server/entity/xml/ParentToElementStrategy.java +++ b/src/main/java/caosdb/server/entity/xml/ParentToElementStrategy.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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 @@ -27,17 +29,26 @@ import caosdb.server.entity.wrapper.Parent; import caosdb.server.utils.EntityStatus; import org.jdom2.Element; -public class ParentToElementStrategy implements ToElementStrategy { +/** + * Generates a JDOM (XML) representation of an entity's parent. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ +public class ParentToElementStrategy extends EntityToElementStrategy { + + public ParentToElementStrategy() { + super("Parent"); + } @Override public Element toElement(final EntityInterface entity, final SetFieldStrategy setFieldStrategy) { - final Element e = - EntityToElementStrategy.sparseEntityToElement("Parent", entity, setFieldStrategy); + final Element element = new Element(this.tagName); + sparseEntityToElement(element, entity, setFieldStrategy); final Parent parent = (Parent) entity; if (parent.getAffiliation() != null) { - e.setAttribute("affiliation", parent.getAffiliation().toString()); + element.setAttribute("affiliation", parent.getAffiliation().toString()); } - return e; + return element; } @Override diff --git a/src/main/java/caosdb/server/entity/xml/SetFieldStrategy.java b/src/main/java/caosdb/server/entity/xml/SetFieldStrategy.java index 7299659900014c4800b3fe0b8864cd7d15409380..7a4a29a16cc545ca5316c7b086a8b1ec46cfcfc3 100644 --- a/src/main/java/caosdb/server/entity/xml/SetFieldStrategy.java +++ b/src/main/java/caosdb/server/entity/xml/SetFieldStrategy.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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 @@ -23,20 +25,34 @@ package caosdb.server.entity.xml; import caosdb.server.entity.EntityInterface; +import caosdb.server.query.Query; import caosdb.server.query.Query.Selection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; +/** + * A class which decides whether the properties, parents, name, etc. of an entity are to be included + * into the serialization or not. + * + * <p>The decision is based on a list of {@link Query.Selection} or smart defaults. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ public class SetFieldStrategy { private final List<Selection> selections = new LinkedList<Selection>(); private HashMap<String, Boolean> cache = null; + + /** + * 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) { @Override public boolean isToBeSet(final String field) { - return true; + return field == null || !field.equalsIgnoreCase("_referenced"); } }; @@ -68,8 +84,9 @@ public class SetFieldStrategy { return forProperty(property.getName()); } + /** Return the strategy for a property. */ public SetFieldStrategy forProperty(final String name) { - // if property is to be omitted. + // if property is to be omitted: always-false-strategy if (!isToBeSet(name)) { return new SetFieldStrategy() { @Override @@ -85,6 +102,23 @@ public class SetFieldStrategy { subselections.add(s.getSubselection()); } } + if (subselections.isEmpty() && this.isToBeSet("_referenced")) { + + /** + * If the super selection decided that the referenced entity is to be included into the + * serialization, while it doesn't specify the subselects, every field is to be included. + * + * <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 true for everything except version fields. + @Override + public boolean isToBeSet(String field) { + return field == null || !field.equalsIgnoreCase("version"); + } + }; + } return new SetFieldStrategy(subselections); } @@ -94,13 +128,23 @@ public class SetFieldStrategy { return defaultSelections.isToBeSet(field); } + // There must be a least one selection defined, a null field won't match anything. + if (field == null) { + return false; + } + if (this.cache == null) { this.cache = new HashMap<String, Boolean>(); - // fill cache + // always include the id and the name this.cache.put("id", true); + this.cache.put("name", true); + + // ... and the referenced entity. + this.cache.put("_referenced", true); for (final Selection selection : this.selections) { - if (!selection.getSelector().equals("id")) { - this.cache.put("name", true); + if (selection.getSelector().equals("value")) { + // if the value is present, the data type is needed as well + this.cache.put("datatype", true); } this.cache.put(selection.getSelector().toLowerCase(), true); } diff --git a/src/main/java/caosdb/server/entity/xml/VersionXMLSerializer.java b/src/main/java/caosdb/server/entity/xml/VersionXMLSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..badd1a95f2a2338bde43a1de23774097ab2c45ed --- /dev/null +++ b/src/main/java/caosdb/server/entity/xml/VersionXMLSerializer.java @@ -0,0 +1,57 @@ +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 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 caosdb.server.entity.xml; + +import caosdb.server.entity.Version; +import java.util.TimeZone; +import org.jdom2.Element; + +/** + * Creates a JDOM Element for a Version instance. + * + * @author Timm Fitschen <t.fitschen@indiscale.com> + */ +class VersionXMLSerializer { + public Element toElement(Version version) { + Element result = new Element("Version"); + result.setAttribute("id", version.getId()); + if (version.getDate() != null) { + result.setAttribute("date", version.getDate().toDateTimeString(TimeZone.getDefault())); + } + if (version.getPredecessors() != null) { + for (Version p : version.getPredecessors()) { + Element predecessor = new Element("Predecessor"); + predecessor.setAttribute("id", p.getId()); + predecessor.setAttribute("date", p.getDate().toDateTimeString(TimeZone.getDefault())); + result.addContent(predecessor); + } + } + if (version.getSuccessors() != null) { + for (Version s : version.getSuccessors()) { + Element successor = new Element("Successor"); + successor.setAttribute("id", s.getId()); + successor.setAttribute("date", s.getDate().toDateTimeString(TimeZone.getDefault())); + result.addContent(successor); + } + } + return result; + } +} diff --git a/src/main/java/caosdb/server/jobs/Job.java b/src/main/java/caosdb/server/jobs/Job.java index 110809e77ae0d40076642d953c15b0d9566eddd6..ff214b977dfd870a9aa56b3e8efa4f12967b2083 100644 --- a/src/main/java/caosdb/server/jobs/Job.java +++ b/src/main/java/caosdb/server/jobs/Job.java @@ -3,7 +3,9 @@ * 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 + * 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> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -171,11 +173,39 @@ public abstract class Job extends AbstractObservable implements Observer { protected final EntityInterface retrieveValidSparseEntityByName(final String name) throws Message { - return retrieveValidSparseEntityById(retrieveValidIDByName(name)); + return retrieveValidSparseEntityById(retrieveValidIDByName(name), null); } - protected final EntityInterface retrieveValidSparseEntityById(final Integer id) throws Message { - final EntityInterface ret = execute(new RetrieveSparseEntity(id)).getEntity(); + protected final EntityInterface retrieveValidSparseEntityById( + final Integer id, final String version) throws Message { + + String resulting_version = version; + if (version == null || version.equals("HEAD")) { + // the targeted entity version is the entity after the transaction or the + // entity without a specific version. Thus we have to fetch the entity + // from the container if possible. + EntityInterface ret = getEntityById(id); + if (ret != null) { + return ret; + } + } else if (version.startsWith("HEAD~")) { + EntityInterface entById = getEntityById(id); + if (entById != null && entById.getEntityStatus() != EntityStatus.VALID) { + // if version is HEAD~{OFFSET} with {OFFSET} > 0 and the targeted entity is is to be + // updated, the actual offset has to be reduced by 1. HEAD always denotes the entity@HEAD + // *after* the successful transaction, so that it is consistent with subsequent retrieves. + int offset = Integer.parseInt(version.substring(5)) - 1; + if (offset == 0) { + // special case HEAD~1 + resulting_version = "HEAD"; + } else { + resulting_version = new StringBuilder().append("HEAD~").append(offset).toString(); + } + } + } + + final EntityInterface ret = + execute(new RetrieveSparseEntity(id, resulting_version)).getEntity(); if (ret.getEntityStatus() == EntityStatus.NONEXISTENT) { throw ServerMessages.ENTITY_DOES_NOT_EXIST; } diff --git a/src/main/java/caosdb/server/jobs/core/CheckDatatypePresent.java b/src/main/java/caosdb/server/jobs/core/CheckDatatypePresent.java index 69d36f42fa2f457b05dcc394659b5c088976bff4..faf481dba5ac16836b4495d6112fc25c887a6f1f 100644 --- a/src/main/java/caosdb/server/jobs/core/CheckDatatypePresent.java +++ b/src/main/java/caosdb/server/jobs/core/CheckDatatypePresent.java @@ -138,7 +138,8 @@ public final class CheckDatatypePresent extends EntityJob { } } else { - final EntityInterface validDatatypeEntity = retrieveValidSparseEntityById(datatype.getId()); + final EntityInterface validDatatypeEntity = + retrieveValidSparseEntityById(datatype.getId(), null); assertAllowedToUse(validDatatypeEntity); datatype.setEntity(validDatatypeEntity); } @@ -151,7 +152,7 @@ public final class CheckDatatypePresent extends EntityJob { private void checkIfOverride() throws Message { if (getEntity().hasId() && getEntity().getId() > 0) { // get data type from database - final EntityInterface foreign = retrieveValidSparseEntityById(getEntity().getId()); + final EntityInterface foreign = retrieveValidSparseEntityById(getEntity().getId(), null); if (foreign.hasDatatype() && !foreign.getDatatype().equals(getEntity().getDatatype())) { // is override! @@ -179,7 +180,7 @@ public final class CheckDatatypePresent extends EntityJob { // the data type of the corresponding abstract property. if (getEntity().hasId() && getEntity().getId() > 0) { // get from data base - final EntityInterface foreign = retrieveValidSparseEntityById(getEntity().getId()); + final EntityInterface foreign = retrieveValidSparseEntityById(getEntity().getId(), null); inheritDatatypeFromForeignEntity(foreign); } else if (getEntity().hasId() && getEntity().getId() < 0) { // get from container @@ -218,7 +219,7 @@ public final class CheckDatatypePresent extends EntityJob { for (final EntityInterface parent : getEntity().getParents()) { EntityInterface parentEntity = null; if (parent.getId() > 0) { - parentEntity = retrieveValidSparseEntityById(parent.getId()); + parentEntity = retrieveValidSparseEntityById(parent.getId(), null); } else { parentEntity = getEntityById(parent.getId()); runJobFromSchedule(parentEntity, CheckDatatypePresent.class); diff --git a/src/main/java/caosdb/server/jobs/core/CheckParValid.java b/src/main/java/caosdb/server/jobs/core/CheckParValid.java index 005053a4a7d59c6c9f1a05171085701c606970b5..560558dce6e494fa4857fefadfa7d22ef79ee5aa 100644 --- a/src/main/java/caosdb/server/jobs/core/CheckParValid.java +++ b/src/main/java/caosdb/server/jobs/core/CheckParValid.java @@ -65,7 +65,7 @@ public class CheckParValid extends EntityJob { if (parent.getId() >= 0) { // id >= 0 (parent is yet in the database) // retrieve parent by id - final EntityInterface foreign = retrieveValidSparseEntityById(parent.getId()); + final EntityInterface foreign = retrieveValidSparseEntityById(parent.getId(), null); // check permissions for this // parentforeign.acceptObserver(o) assertAllowedToUse(foreign); diff --git a/src/main/java/caosdb/server/jobs/core/CheckPropValid.java b/src/main/java/caosdb/server/jobs/core/CheckPropValid.java index 6a7cbb51556dd0b9c634da4c8a56d20204fc9f50..5e199e848984a4d042c3a755d83a8d57ba91987f 100644 --- a/src/main/java/caosdb/server/jobs/core/CheckPropValid.java +++ b/src/main/java/caosdb/server/jobs/core/CheckPropValid.java @@ -55,7 +55,7 @@ public class CheckPropValid extends EntityJob { if (property.getId() >= 0) { final EntityInterface abstractProperty = - retrieveValidSparseEntityById(property.getId()); + retrieveValidSparseEntityById(property.getId(), null); assertAllowedToUse(abstractProperty); diff --git a/src/main/java/caosdb/server/jobs/core/CheckRefidIsaParRefid.java b/src/main/java/caosdb/server/jobs/core/CheckRefidIsaParRefid.java index fa2b21ccaea7ba3c8bd2b733f8420c213e4f03f7..04a1ac8c5bf6aa42bf2dcec803e6bc1b68a3a7f0 100644 --- a/src/main/java/caosdb/server/jobs/core/CheckRefidIsaParRefid.java +++ b/src/main/java/caosdb/server/jobs/core/CheckRefidIsaParRefid.java @@ -94,7 +94,8 @@ public class CheckRefidIsaParRefid extends EntityJob { && getEntityByName(rv.getName()).getRole() == Role.File) { } else if (rv.getId() != null && rv.getId() > 0 - && retrieveValidSparseEntityById(rv.getId()).getRole() == Role.File) { + && retrieveValidSparseEntityById(rv.getId(), rv.getVersion()).getRole() + == Role.File) { } else if (rv.getName() != null && retrieveValidSparseEntityByName(rv.getName()).getRole() == Role.File) { } else { diff --git a/src/main/java/caosdb/server/jobs/core/CheckRefidValid.java b/src/main/java/caosdb/server/jobs/core/CheckRefidValid.java index 14d98962f48c300ae77fe0811922bf3b1d7eb4ae..36591070cd64558114bb56e22860b95f42fc6463 100644 --- a/src/main/java/caosdb/server/jobs/core/CheckRefidValid.java +++ b/src/main/java/caosdb/server/jobs/core/CheckRefidValid.java @@ -3,7 +3,9 @@ * 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 + * 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> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -85,9 +87,12 @@ public class CheckRefidValid extends EntityJob { private void checkRefValue(final ReferenceValue ref) throws Message { if (ref.getId() != null) { if (ref.getId() >= 0) { - final EntityInterface referencedValidEntity = retrieveValidSparseEntityById(ref.getId()); + final EntityInterface referencedValidEntity = + retrieveValidSparseEntityById(ref.getId(), ref.getVersion()); assertAllowedToUse(referencedValidEntity); - ref.setEntity(referencedValidEntity); + + // link the entity as versioned entity iff the reference specified a version + ref.setEntity(referencedValidEntity, ref.getVersion() != null); } else { @@ -100,7 +105,9 @@ public class CheckRefidValid extends EntityJob { final EntityInterface referencedEntity = getEntityById(ref.getId()); if (referencedEntity != null) { assertAllowedToUse(referencedEntity); - ref.setEntity(referencedEntity); + + // link the entity as versioned entity iff the reference specified a version + ref.setEntity(referencedEntity, ref.getVersion() != null); } else { throw ServerMessages.REFERENCED_ENTITY_DOES_NOT_EXIST; } @@ -117,7 +124,9 @@ public class CheckRefidValid extends EntityJob { if (referencedEntity != null) { assertAllowedToUse(referencedEntity); - ref.setEntity(referencedEntity); + + // link the entity as versioned entity iff the reference specified a version + ref.setEntity(referencedEntity, ref.getVersion() != null); if (checkRefEntity(ref)) { ref.getEntity().acceptObserver(this); } @@ -125,7 +134,9 @@ public class CheckRefidValid extends EntityJob { final EntityInterface referencedValidEntity = retrieveValidSparseEntityByName(ref.getName()); assertAllowedToUse(referencedValidEntity); - ref.setEntity(referencedValidEntity); + + // link the entity as versioned entity iff the reference specified a version + ref.setEntity(referencedValidEntity, ref.getVersion() != null); } } } diff --git a/src/main/java/caosdb/server/jobs/core/RemoveDuplicates.java b/src/main/java/caosdb/server/jobs/core/RemoveDuplicates.java index a104c124293cf07ecc3b871c4bc6b31470ec2a15..a09f66533cb3bc3c7cc01b2d6931bc120f87f7a7 100644 --- a/src/main/java/caosdb/server/jobs/core/RemoveDuplicates.java +++ b/src/main/java/caosdb/server/jobs/core/RemoveDuplicates.java @@ -30,17 +30,20 @@ public class RemoveDuplicates extends ContainerJob { @Override protected void run() { - final HashSet<EntityInterface> rm = new HashSet<EntityInterface>(); + // collect duplicates + final HashSet<EntityInterface> duplicates = new HashSet<EntityInterface>(); for (final EntityInterface e : getContainer()) { - if (e.hasId() && !rm.contains(e)) { + if (e.hasId() && !duplicates.contains(e)) { for (final EntityInterface e2 : getContainer()) { - if (e2 != e && e.getId().equals(e2.getId())) { - rm.add(e2); + if (e2 != e && e.getIdVersion().equals(e2.getIdVersion())) { + // this is a duplicate of another entity in this container + duplicates.add(e2); } } } } - for (final EntityInterface e : rm) { + // remove duplicates. + for (final EntityInterface e : duplicates) { getContainer().remove(e); } } diff --git a/src/main/java/caosdb/server/permissions/EntityACL.java b/src/main/java/caosdb/server/permissions/EntityACL.java index 34ef34351826facfa7d342d6c8b06e8d8c27cf74..e0bfcf9243eb436f9af0d6a3bacfabb79984538c 100644 --- a/src/main/java/caosdb/server/permissions/EntityACL.java +++ b/src/main/java/caosdb/server/permissions/EntityACL.java @@ -92,7 +92,11 @@ public class EntityACL { } public static final EntityACL getOwnerACLFor(final Subject subject) { - if (subject.getPrincipal() == AuthenticationUtils.ANONYMOUS_USER.getPrincipal()) { + // TODO handle the case where a valid non-guest user (e.g. me@PAM, + // you@CaosDB) is (just temporarily) authenticated via a + // OneTimeAuthenticationToken + if (AuthenticationUtils.isAnonymous(subject) + || AuthenticationUtils.isFromOneTimeTokenRealm(subject)) { return new EntityACLFactory().create(); } return getOwnerACLFor((Principal) subject.getPrincipal()); diff --git a/src/main/java/caosdb/server/permissions/Role.java b/src/main/java/caosdb/server/permissions/Role.java index 70e1a61f754b4beeffe8f8fe203b42842d49cb6d..87b5d2474c4d2d3f13fd2787aaf5b11a1245a39c 100644 --- a/src/main/java/caosdb/server/permissions/Role.java +++ b/src/main/java/caosdb/server/permissions/Role.java @@ -22,7 +22,6 @@ */ package caosdb.server.permissions; -import caosdb.server.accessControl.UserSources; import java.util.HashMap; import org.jdom2.Attribute; import org.jdom2.Element; @@ -31,7 +30,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(UserSources.ANONYMOUS_ROLE); + public static final Role ANONYMOUS_ROLE = new Role("anonymous"); private final String role; diff --git a/src/main/java/caosdb/server/query/CQLParser.g4 b/src/main/java/caosdb/server/query/CQLParser.g4 index af981d6a2d027892c144070323aa924a5d7674a5..8b5e5a94aaea5d62ba60068a1ad652f7b51f1bd4 100644 --- a/src/main/java/caosdb/server/query/CQLParser.g4 +++ b/src/main/java/caosdb/server/query/CQLParser.g4 @@ -4,6 +4,8 @@ * * 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 Florian Spreckelsen <f.spreckelsen@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 @@ -188,11 +190,11 @@ username returns [Query.Pattern ep] locals [int type] transaction_time returns [String tqp] : - ( - (ON | IN) - (datetime {$tqp = $datetime.text;} - | entity {$tqp = $entity.ep.toString();}) - ) | TODAY {$tqp = TransactionFilter.TODAY;} + ( + (ON | IN) + (value {$tqp = $value.text;} + | entity {$tqp = $entity.ep.toString();}) + ) | TODAY {$tqp = TransactionFilter.TODAY;} ; /* @@ -244,8 +246,8 @@ pov returns [POV filter] locals [Query.Pattern p, String o, String v, String a] ) | IS_NULL {$o = "0";} | IS_NOT_NULL {$o = "!0";} - | IN datetime {$o = "("; $v=$datetime.text;} - | NEGATION IN datetime {$o = "!("; $v=$datetime.text;} + | IN value {$o = "("; $v=$value.str;} + | NEGATION IN value {$o = "!("; $v=$value.str;} )? ) | @@ -443,7 +445,7 @@ minmax returns [String agg] value returns [String str] : number {$str = $text;} - | datetime {$str = $text;} + | datetime {$str = $datetime.text;} | atom {$str = $atom.ep.toString();} ; diff --git a/src/main/java/caosdb/server/query/POV.java b/src/main/java/caosdb/server/query/POV.java index a50be918da30d6cb69b457ef4b7801ea3019d58d..924c1293bfea78ef15f1b6ab10e5d9d91a2a98cc 100644 --- a/src/main/java/caosdb/server/query/POV.java +++ b/src/main/java/caosdb/server/query/POV.java @@ -167,6 +167,9 @@ public class POV implements EntityFilterInterface { } catch (final ClassCastException e) { this.vDatetime = null; } catch (final IllegalArgumentException e) { + if (this.operator.contains("(")) { + throw new Query.ParsingException("the value is expected to be a date time"); + } this.vDatetime = null; } } else { diff --git a/src/main/java/caosdb/server/query/Query.java b/src/main/java/caosdb/server/query/Query.java index 1f85826967cf5020cdbdb6ecbbfe4e9452b9d929..a2d6e48767f5835bd9bf40ce65717bf9e8363f98 100644 --- a/src/main/java/caosdb/server/query/Query.java +++ b/src/main/java/caosdb/server/query/Query.java @@ -66,18 +66,20 @@ import org.jdom2.Element; public class Query implements QueryInterface, ToElementable, TransactionInterface { + /** Class which represents the selection of (sub)properties. */ public static class Selection { private final String selector; private Selection subselection = null; public Selection setSubSelection(final Selection sub) { if (this.subselection != null) { - throw new UnsupportedOperationException("subselection is immutble!"); + throw new UnsupportedOperationException("SubSelection is immutable!"); } this.subselection = sub; return this; } + /** No parsing, just sets the selector string. */ public Selection(final String selector) { this.selector = selector.trim(); } @@ -299,7 +301,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac // ... check for RETRIEVE:ENTITY permission... final EntityInterface e = - execute(new RetrieveSparseEntity(q.getKey()), query.getAccess()).getEntity(); + execute(new RetrieveSparseEntity(q.getKey(), null), query.getAccess()).getEntity(); final EntityACL entityACL = e.getEntityACL(); if (!entityACL.isPermitted(query.getUser(), EntityPermission.RETRIEVE_ENTITY)) { // ... and ignore if not. @@ -553,7 +555,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac while (rs.next()) { final long t1 = System.currentTimeMillis(); final Integer id = rs.getInt("id"); - if (!execute(new RetrieveSparseEntity(id), query.getAccess()) + if (!execute(new RetrieveSparseEntity(id, null), query.getAccess()) .getEntity() .getEntityACL() .isPermitted(query.getUser(), EntityPermission.RETRIEVE_ENTITY)) { @@ -586,7 +588,7 @@ public class Query implements QueryInterface, ToElementable, TransactionInterfac while (iterator.hasNext()) { final long t1 = System.currentTimeMillis(); final Integer id = iterator.next(); - if (!execute(new RetrieveSparseEntity(id), getAccess()) + if (!execute(new RetrieveSparseEntity(id, null), getAccess()) .getEntity() .getEntityACL() .isPermitted(getUser(), EntityPermission.RETRIEVE_ENTITY)) { diff --git a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java index ea4e65f0a60d72cb5da6cb03b2cec44848dbc3c6..97488a6ba2a7984d66bb94332b4d7df0f9478cfb 100644 --- a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java +++ b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java @@ -4,7 +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,2020 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2020 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 @@ -23,13 +24,11 @@ */ package caosdb.server.resource; -import static caosdb.server.utils.Utils.isNonNullInteger; import static java.net.URLDecoder.decode; import caosdb.server.CaosDBException; import caosdb.server.accessControl.AuthenticationUtils; import caosdb.server.accessControl.Principal; -import caosdb.server.accessControl.UserSources; import caosdb.server.database.backend.implementation.MySQL.ConnectionException; import caosdb.server.entity.Message; import caosdb.server.utils.ServerMessages; @@ -39,7 +38,6 @@ import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; import java.sql.SQLException; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; @@ -78,9 +76,7 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { private static final XMLParser xmlparser = new XMLParser(); protected String sRID = null; // Server side request ID private String cRID = null; // Client side request ID - private String[] requestedItems = null; - private ArrayList<Integer> requestedIDs = new ArrayList<Integer>(); - private ArrayList<String> requestedNames = new ArrayList<String>(); + private String[] requestedItems = {}; private WebinterfaceUtils utils; /** Return the {@link WebinterfaceUtils} instance for this resource. */ @@ -142,21 +138,6 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { } this.requestedItems = specifier.split("&"); - for (final String requestedItem : this.requestedItems) { - if (isNonNullInteger(requestedItem)) { - final int id = Integer.parseInt(requestedItem); - if (id > 0) { - getRequestedIDs().add(id); - } - } else if (requestedItem.equalsIgnoreCase("all")) { - getRequestedNames().clear(); - getRequestedIDs().clear(); - getRequestedNames().add("all"); - break; - } else { - getRequestedNames().add(requestedItem); - } - } } // flags @@ -210,13 +191,11 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { if (user != null && user.isAuthenticated()) { Element userInfo = new Element("UserInfo"); - if (!user.getPrincipal().equals(AuthenticationUtils.ANONYMOUS_USER.getPrincipal())) { - // TODO: deprecated - addNameAndRealm(retRoot, user); + // TODO: deprecated, needs refactoring in the webui first + addNameAndRealm(retRoot, user); - // this is the new, correct way - addNameAndRealm(userInfo, user); - } + // this is the new, correct way + addNameAndRealm(userInfo, user); addRoles(userInfo, user); retRoot.addContent(userInfo); @@ -231,7 +210,7 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { * @param user */ private void addRoles(Element userInfo, Subject user) { - Collection<String> roles = UserSources.resolve(user.getPrincipals()); + Collection<String> roles = AuthenticationUtils.getRoles(user); if (roles == null) return; Element rs = new Element("Roles"); for (String role : roles) { @@ -248,8 +227,10 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { * @param userInfo */ private void addNameAndRealm(Element userInfo, Subject user) { - userInfo.setAttribute("username", ((Principal) user.getPrincipal()).getUsername()); - userInfo.setAttribute("realm", ((Principal) user.getPrincipal()).getRealm()); + if (!AuthenticationUtils.isAnonymous((Principal) user.getPrincipal())) { + userInfo.setAttribute("username", ((Principal) user.getPrincipal()).getUsername()); + userInfo.setAttribute("realm", ((Principal) user.getPrincipal()).getRealm()); + } } @Get @@ -409,11 +390,9 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { getRequest().getAttributes().put("THROWN", t); throw t; } catch (final AuthenticationException e) { - getResponse().setStatus(Status.CLIENT_ERROR_FORBIDDEN); - return null; + return error(ServerMessages.UNAUTHENTICATED, Status.CLIENT_ERROR_UNAUTHORIZED); } catch (final AuthorizationException e) { - getResponse().setStatus(Status.CLIENT_ERROR_FORBIDDEN); - return null; + return error(ServerMessages.AUTHORIZATION_ERROR, Status.CLIENT_ERROR_FORBIDDEN); } catch (final Message m) { return error(m, Status.CLIENT_ERROR_BAD_REQUEST); } catch (final FileUploadException e) { @@ -430,16 +409,8 @@ public abstract class AbstractCaosDBServerResource extends ServerResource { } } - public ArrayList<Integer> getRequestedIDs() { - return this.requestedIDs; - } - - public ArrayList<String> getRequestedNames() { - return this.requestedNames; - } - - public void setRequestedNames(final ArrayList<String> requestedNames) { - this.requestedNames = requestedNames; + public String[] getRequestedItems() { + return this.requestedItems; } public HashMap<String, String> getFlags() { diff --git a/src/main/java/caosdb/server/resource/FileSystemResource.java b/src/main/java/caosdb/server/resource/FileSystemResource.java index ecaa97fe93cb59fe6a4c58fb60d99a5dca373080..ef352e19eb178c5cefbdbd70d2e48b3231ac7bd7 100644 --- a/src/main/java/caosdb/server/resource/FileSystemResource.java +++ b/src/main/java/caosdb/server/resource/FileSystemResource.java @@ -135,8 +135,9 @@ public class FileSystemResource extends AbstractCaosDBServerResource { try { getEntity(specifier).checkPermission(EntityPermission.RETRIEVE_FILE); } catch (EntityDoesNotExistException exception) { - // This file in the file system has no corresponding File record. - return error(ServerMessages.NOT_PERMITTED, Status.CLIENT_ERROR_FORBIDDEN); + // This file in the file system has no corresponding File record. It + // shall not be retrieved by anyone. + return error(ServerMessages.AUTHORIZATION_ERROR, Status.CLIENT_ERROR_FORBIDDEN); } final MediaType mt = MediaType.valueOf(FileUtils.getMimeType(file)); diff --git a/src/main/java/caosdb/server/resource/PermissionRulesResource.java b/src/main/java/caosdb/server/resource/PermissionRulesResource.java index 1610ba372def0b758b76d15d6d839b825a9d96c6..dba1797a03287c503e5898c7e9a7dde6c389630c 100644 --- a/src/main/java/caosdb/server/resource/PermissionRulesResource.java +++ b/src/main/java/caosdb/server/resource/PermissionRulesResource.java @@ -46,7 +46,7 @@ public class PermissionRulesResource extends AbstractCaosDBServerResource { protected Representation httpGetInChildClass() throws ConnectionException, IOException, SQLException, CaosDBException, NoSuchAlgorithmException, Exception { - final String role = getRequestedNames().get(0); + final String role = getRequestedItems()[0]; getUser().checkPermission(ACMPermissions.PERMISSION_RETRIEVE_ROLE_PERMISSIONS(role)); @@ -73,7 +73,7 @@ public class PermissionRulesResource extends AbstractCaosDBServerResource { public Representation httpPutInChildClass(final Representation entity) throws Exception { final Element root = parseEntity(entity).getRootElement(); - final String role = getRequestedNames().get(0); + final String role = getRequestedItems()[0]; final HashSet<PermissionRule> rules = new HashSet<PermissionRule>(); for (final Element e : root.getChildren()) { diff --git a/src/main/java/caosdb/server/resource/RolesResource.java b/src/main/java/caosdb/server/resource/RolesResource.java index ef9b30f5b831454fac392317e2c57833657c62d6..0f1b01e3230cf3e73f1c20949f38e180bd8f7e3b 100644 --- a/src/main/java/caosdb/server/resource/RolesResource.java +++ b/src/main/java/caosdb/server/resource/RolesResource.java @@ -52,8 +52,8 @@ public class RolesResource extends AbstractCaosDBServerResource { final Element root = generateRootElement(); final Document document = new Document(); - if (!getRequestedNames().isEmpty()) { - final String name = getRequestedNames().get(0); + 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); @@ -78,8 +78,8 @@ public class RolesResource extends AbstractCaosDBServerResource { protected Representation httpDeleteInChildClass() throws ConnectionException, SQLException, CaosDBException, IOException, NoSuchAlgorithmException, Exception { - if (!getRequestedNames().isEmpty()) { - final String name = getRequestedNames().get(0); + if (getRequestedItems().length > 0) { + final String name = getRequestedItems()[0]; if (name != null) { final DeleteRoleTransaction t = new DeleteRoleTransaction(name); try { @@ -133,26 +133,28 @@ public class RolesResource extends AbstractCaosDBServerResource { protected Representation httpPutInChildClass(final Representation entity) throws ConnectionException, JDOMException, Exception, xmlNotWellFormedException { - final String name = getRequestedNames().get(0); - String description = null; + if (getRequestedItems().length > 0) { + final String name = getRequestedItems()[0]; + String description = null; - final Form f = new Form(entity); - if (!f.isEmpty()) { - description = f.getFirstValue("role_description"); - } + final Form f = new Form(entity); + if (!f.isEmpty()) { + description = f.getFirstValue("role_description"); + } - if (name != null && description != null) { - final Role role = new Role(); - role.name = name; - role.description = description; - final UpdateRoleTransaction t = new UpdateRoleTransaction(role); - try { - t.execute(); - } catch (final Message m) { - if (m == ServerMessages.ROLE_DOES_NOT_EXIST) { - return error(m, Status.CLIENT_ERROR_NOT_FOUND); - } else { - throw m; + if (name != null && description != null) { + final Role role = new Role(); + role.name = name; + role.description = description; + final UpdateRoleTransaction t = new UpdateRoleTransaction(role); + try { + t.execute(); + } catch (final Message m) { + if (m == ServerMessages.ROLE_DOES_NOT_EXIST) { + return error(m, Status.CLIENT_ERROR_NOT_FOUND); + } else { + throw m; + } } } } diff --git a/src/main/java/caosdb/server/resource/ScriptingResource.java b/src/main/java/caosdb/server/resource/ScriptingResource.java index b23a9593918ead75a6b38216838995e09ac67bc7..cb652b862282e2cf804ae4cbffd3b6f9de9d109f 100644 --- a/src/main/java/caosdb/server/resource/ScriptingResource.java +++ b/src/main/java/caosdb/server/resource/ScriptingResource.java @@ -25,12 +25,13 @@ package caosdb.server.resource; import caosdb.server.FileSystem; -import caosdb.server.accessControl.Principal; +import caosdb.server.accessControl.AuthenticationUtils; +import caosdb.server.accessControl.OneTimeAuthenticationToken; import caosdb.server.accessControl.SessionToken; -import caosdb.server.accessControl.UserSources; import caosdb.server.entity.FileProperties; import caosdb.server.entity.Message; import caosdb.server.scripting.CallerSerializer; +import caosdb.server.scripting.ScriptingPermissions; import caosdb.server.scripting.ServerSideScriptingCaller; import caosdb.server.utils.Serializer; import caosdb.server.utils.ServerMessages; @@ -48,6 +49,7 @@ import org.apache.commons.fileupload.FileItemIterator; import org.apache.commons.fileupload.FileItemStream; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.shiro.subject.Subject; import org.jdom2.Element; import org.restlet.data.CharacterSet; import org.restlet.data.Form; @@ -83,9 +85,6 @@ public class ScriptingResource extends AbstractCaosDBServerResource { @Override protected Representation httpPostInChildClass(Representation entity) throws Exception { - if (isAnonymous()) { - return error(ServerMessages.AUTHORIZATION_ERROR, Status.CLIENT_ERROR_FORBIDDEN); - } MediaType mediaType = entity.getMediaType(); try { if (mediaType.equals(MediaType.MULTIPART_FORM_DATA, true)) { @@ -193,28 +192,42 @@ public class ScriptingResource extends AbstractCaosDBServerResource { public int callScript(Form form, List<FileProperties> files) throws Message { List<String> commandLine = form2CommandLine(form); + String call = commandLine.get(0); + + checkExecutionPermission(getUser(), call); Integer timeoutMs = Integer.valueOf(form.getFirstValue("timeout", "-1")); return callScript(commandLine, timeoutMs, files); } public int callScript(List<String> commandLine, Integer timeoutMs, List<FileProperties> files) throws Message { - return callScript(commandLine, timeoutMs, files, generateAuthToken()); + return callScript(commandLine, timeoutMs, files, generateAuthToken(commandLine.get(0))); + } + + public boolean isAnonymous() { + return AuthenticationUtils.isAnonymous(getUser()); } - public Object generateAuthToken() { - return SessionToken.generate((Principal) getUser().getPrincipal(), null); + /** + * Generate and return a token for the purpose of the given call. If the user is not anonymous and + * the call is not configured to be called by everyone, a SessionToken is returned instead. + */ + public Object generateAuthToken(String call) { + String purpose = "SCRIPTING:EXECUTE:" + call; + Object authtoken = OneTimeAuthenticationToken.generateForPurpose(purpose, getUser()); + if (authtoken != null || isAnonymous()) { + return authtoken; + } + return SessionToken.generate(getUser()); } - boolean isAnonymous() { - boolean ret = getUser().hasRole(UserSources.ANONYMOUS_ROLE); - return ret; + public void checkExecutionPermission(Subject user, String call) { + user.checkPermission(ScriptingPermissions.PERMISSION_EXECUTION(call)); } public int callScript( List<String> commandLine, Integer timeoutMs, List<FileProperties> files, Object authToken) throws Message { - // TODO getUser().checkPermission("SCRIPTING:EXECUTE:" + commandLine.get(0)); caller = new ServerSideScriptingCaller( commandLine.toArray(new String[commandLine.size()]), timeoutMs, files, authToken); diff --git a/src/main/java/caosdb/server/resource/SharedFileResource.java b/src/main/java/caosdb/server/resource/SharedFileResource.java index ffdce05baab5098d03080567ee815d286476f281..71cf089ddc4246196b8299502b1f15969183f372 100644 --- a/src/main/java/caosdb/server/resource/SharedFileResource.java +++ b/src/main/java/caosdb/server/resource/SharedFileResource.java @@ -71,7 +71,12 @@ public class SharedFileResource extends AbstractCaosDBServerResource { final MediaType mt = MediaType.valueOf(FileUtils.getMimeType(file)); final FileRepresentation ret = new FileRepresentation(file, mt); - ret.setDisposition(new Disposition(Disposition.TYPE_ATTACHMENT)); + + // HTML files should be opened in the browser. + // Any other media type than HTML is attached for download. + if (!MediaType.TEXT_HTML.includes(mt)) { + ret.setDisposition(new Disposition(Disposition.TYPE_ATTACHMENT)); + } return ret; } diff --git a/src/main/java/caosdb/server/resource/UserResource.java b/src/main/java/caosdb/server/resource/UserResource.java index b77ff8a49035d2be930a3f957e743ed2f0dae067..7dc9111ee332421fd27d1c32632cc458cbf6c800 100644 --- a/src/main/java/caosdb/server/resource/UserResource.java +++ b/src/main/java/caosdb/server/resource/UserResource.java @@ -60,9 +60,9 @@ public class UserResource extends AbstractCaosDBServerResource { final Document doc = new Document(); final Element rootElem = generateRootElement(); - if (!getRequestedNames().isEmpty()) { + if (getRequestedItems().length > 0) { try { - final String username = getRequestedNames().get(0); + final String username = getRequestedItems()[0]; final String realm = (getRequestAttributes().containsKey("realm") ? (String) getRequestAttributes().get("realm") @@ -92,7 +92,7 @@ public class UserResource extends AbstractCaosDBServerResource { try { final Form form = new Form(entity); - final String username = getRequestedNames().get(0); + final String username = getRequestedItems()[0]; final String realm = (getRequestAttributes().containsKey("realm") ? (String) getRequestAttributes().get("realm") @@ -187,7 +187,8 @@ public class UserResource extends AbstractCaosDBServerResource { final Document doc = new Document(); final Element rootElem = generateRootElement(); - final DeleteUserTransaction t = new DeleteUserTransaction(getRequestedNames().get(0)); + final String username = getRequestedItems()[0]; + final DeleteUserTransaction t = new DeleteUserTransaction(username); try { t.execute(); } catch (final Message m) { diff --git a/src/main/java/caosdb/server/resource/UserRolesResource.java b/src/main/java/caosdb/server/resource/UserRolesResource.java index 27e147157675982f036b88981b1dce221abae754..46d51ca4d60bec89b584cefc362e54ab37063503 100644 --- a/src/main/java/caosdb/server/resource/UserRolesResource.java +++ b/src/main/java/caosdb/server/resource/UserRolesResource.java @@ -47,7 +47,7 @@ public class UserRolesResource extends AbstractCaosDBServerResource { protected Representation httpGetInChildClass() throws ConnectionException, IOException, SQLException, CaosDBException, NoSuchAlgorithmException, Exception { - final String user = getRequestedNames().get(0); + final String user = getRequestedItems()[0]; final String realm = (getRequestAttributes().get("realm") != null ? (String) getRequestAttributes().get("realm") @@ -73,7 +73,7 @@ public class UserRolesResource extends AbstractCaosDBServerResource { @Override protected Representation httpPutInChildClass(final Representation entity) throws ConnectionException, JDOMException, Exception, xmlNotWellFormedException { - final String user = getRequestedNames().get(0); + final String user = getRequestedItems()[0]; final String realm = (getRequestAttributes().get("realm") != null ? (String) getRequestAttributes().get("realm") diff --git a/src/main/java/caosdb/server/resource/transaction/EntityResource.java b/src/main/java/caosdb/server/resource/transaction/EntityResource.java index dc25bcff3418b3f46f90e965a6b80cdf3111c07f..70e7bdf73adbf5e950fe0a51e119a8dbbe2e054f 100644 --- a/src/main/java/caosdb/server/resource/transaction/EntityResource.java +++ b/src/main/java/caosdb/server/resource/transaction/EntityResource.java @@ -30,8 +30,8 @@ import caosdb.server.entity.container.RetrieveContainer; import caosdb.server.entity.container.UpdateContainer; import caosdb.server.resource.AbstractCaosDBServerResource; import caosdb.server.resource.transaction.handlers.FileUploadHandler; -import caosdb.server.resource.transaction.handlers.IDHandler; import caosdb.server.resource.transaction.handlers.RequestHandler; +import caosdb.server.resource.transaction.handlers.SimpleDeleteRequestHandler; import caosdb.server.resource.transaction.handlers.SimpleGetRequestHandler; import caosdb.server.resource.transaction.handlers.SimpleWriteHandler; import caosdb.server.transaction.Delete; @@ -72,7 +72,7 @@ public class EntityResource extends AbstractCaosDBServerResource { } protected RequestHandler<DeleteContainer> getDeleteRequestHandler() { - return new IDHandler<DeleteContainer>(); + return new SimpleDeleteRequestHandler(); } protected RequestHandler<InsertContainer> getPostRequestHandler() { diff --git a/src/main/java/caosdb/server/resource/transaction/handlers/IDHandler.java b/src/main/java/caosdb/server/resource/transaction/handlers/IDHandler.java deleted file mode 100644 index e641ed416c86075d23a37d70ad791c26a820a966..0000000000000000000000000000000000000000 --- a/src/main/java/caosdb/server/resource/transaction/handlers/IDHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * ** 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 - * 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/>. - * - * ** end header - */ -package caosdb.server.resource.transaction.handlers; - -import caosdb.server.entity.container.EntityByIdContainer; -import caosdb.server.resource.transaction.EntityResource; - -public class IDHandler<T extends EntityByIdContainer> extends RequestHandler<T> { - - @Override - public void handle(final EntityResource t, final T container) throws Exception { - for (final int id : t.getRequestedIDs()) { - container.add(id); - } - } -} diff --git a/src/main/java/caosdb/server/resource/transaction/handlers/SimpleDeleteRequestHandler.java b/src/main/java/caosdb/server/resource/transaction/handlers/SimpleDeleteRequestHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7fb416451bd8f053df5231eb1002db0c03544f99 --- /dev/null +++ b/src/main/java/caosdb/server/resource/transaction/handlers/SimpleDeleteRequestHandler.java @@ -0,0 +1,55 @@ +/* + * ** 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> + * + * 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/>. + * + * ** end header + */ +package caosdb.server.resource.transaction.handlers; + +import caosdb.server.entity.container.DeleteContainer; +import caosdb.server.resource.transaction.EntityResource; + +public class SimpleDeleteRequestHandler extends RequestHandler<DeleteContainer> { + + @Override + public void handle(final EntityResource t, final DeleteContainer container) throws Exception { + // TODO a lot of code duplication, see SimpleGetRequestHandle#handle. + // However, this is about to be changed again when string ids are + // introduced, anyways. So we just leave it. + for (final String item : t.getRequestedItems()) { + String[] elem = item.split("@", 1); + Integer id = null; + String version = null; + try { + id = Integer.parseInt(elem[0]); + } catch (NumberFormatException e) { + // pass + } + if (elem.length > 1) { + version = elem[1]; + } + + if (id != null) { + container.add(id, version); + } + } + } +} diff --git a/src/main/java/caosdb/server/resource/transaction/handlers/SimpleGetRequestHandler.java b/src/main/java/caosdb/server/resource/transaction/handlers/SimpleGetRequestHandler.java index 8fbb9d25cb33d2d0a75ce8405a164c1e8c9ce0fd..18c89d46966a3e22b1a9066b84dcf2d05efd122c 100644 --- a/src/main/java/caosdb/server/resource/transaction/handlers/SimpleGetRequestHandler.java +++ b/src/main/java/caosdb/server/resource/transaction/handlers/SimpleGetRequestHandler.java @@ -25,13 +25,29 @@ package caosdb.server.resource.transaction.handlers; import caosdb.server.entity.container.RetrieveContainer; import caosdb.server.resource.transaction.EntityResource; -public class SimpleGetRequestHandler extends IDHandler<RetrieveContainer> { +public class SimpleGetRequestHandler extends RequestHandler<RetrieveContainer> { @Override public void handle(final EntityResource t, final RetrieveContainer container) throws Exception { - super.handle(t, container); - for (final String name : t.getRequestedNames()) { - container.add(name); + for (final String item : t.getRequestedItems()) { + String[] elem = item.split("@", 2); + Integer id = null; + String name = null; + String version = null; + try { + id = Integer.parseInt(elem[0]); + } catch (NumberFormatException e) { + name = elem[0]; + } + if (elem.length > 1) { + version = elem[1]; + } + + if (id != null) { + container.add(id, version); + } else { + container.add(name); + } } } } diff --git a/src/main/java/caosdb/server/scripting/ScriptingPermissions.java b/src/main/java/caosdb/server/scripting/ScriptingPermissions.java new file mode 100644 index 0000000000000000000000000000000000000000..9165f133c070c351e7a4ecbb2c510c06b71734a2 --- /dev/null +++ b/src/main/java/caosdb/server/scripting/ScriptingPermissions.java @@ -0,0 +1,11 @@ +package caosdb.server.scripting; + +public class ScriptingPermissions { + + public static final String PERMISSION_EXECUTION(final String call) { + StringBuilder ret = new StringBuilder(18 + call.length()); + ret.append("SCRIPTING:EXECUTE:"); + ret.append(call.replace("/", ":")); + return ret.toString(); + } +} diff --git a/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java index 6fa5d1a479df47c20d9d3a5d3a7134e1c0898ad1..faeb96e02738395647abb75fca3eaafe18040ad3 100644 --- a/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java +++ b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java @@ -35,6 +35,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.ProcessBuilder.Redirect; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -165,8 +166,11 @@ public class ServerSideScriptingCaller { if (f.getPath() == null || f.getPath().isEmpty()) { throw new CaosDBException("The path must not be null or empty!"); } - caosdb.server.utils.FileUtils.createSymlink( - getUploadFilesDir().toPath().resolve(f.getPath()).toFile(), f.getFile()); + File link = getUploadFilesDir().toPath().resolve(f.getPath()).toFile(); + if (!link.getParentFile().exists()) { + link.getParentFile().mkdirs(); + } + caosdb.server.utils.FileUtils.createSymlink(link, f.getFile()); } } @@ -255,7 +259,12 @@ public class ServerSideScriptingCaller { } int callScript() throws IOException, InterruptedException, TimeoutException { - String[] effectiveCommandLine = injectAuthToken(getCommandLine()); + String[] effectiveCommandLine; + if (authToken != null) { + effectiveCommandLine = injectAuthToken(getCommandLine()); + } else { + effectiveCommandLine = Arrays.copyOf(getCommandLine(), getCommandLine().length); + } effectiveCommandLine[0] = makeCallAbsolute(effectiveCommandLine[0]); final ProcessBuilder pb = new ProcessBuilder(effectiveCommandLine); diff --git a/src/main/java/caosdb/server/terminal/CaosDBTerminal.java b/src/main/java/caosdb/server/terminal/CaosDBTerminal.java index 8113d4e1e7c36adb517543b778d03ec9a0970598..c85495539d93d67af26b1b1ef4b62a35dd57ff48 100644 --- a/src/main/java/caosdb/server/terminal/CaosDBTerminal.java +++ b/src/main/java/caosdb/server/terminal/CaosDBTerminal.java @@ -30,6 +30,11 @@ import com.googlecode.lanterna.terminal.Terminal; import com.googlecode.lanterna.terminal.text.UnixTerminal; import java.nio.charset.Charset; +/** + * @deprecated Soon to be removed + * @author Timm Fitschen (t.fitschen@indiscale.com) + */ +@Deprecated public class CaosDBTerminal extends Thread { public CaosDBTerminal() { diff --git a/src/main/java/caosdb/server/transaction/ChecksumUpdater.java b/src/main/java/caosdb/server/transaction/ChecksumUpdater.java index 30313478c263a131658005daa39da3b495c85eb2..ec923bec7bf3368492f28bdafa3f11b5e7bd4ec9 100644 --- a/src/main/java/caosdb/server/transaction/ChecksumUpdater.java +++ b/src/main/java/caosdb/server/transaction/ChecksumUpdater.java @@ -27,7 +27,7 @@ import caosdb.server.database.DatabaseMonitor; import caosdb.server.database.access.Access; import caosdb.server.database.backend.transaction.GetUpdateableChecksums; import caosdb.server.database.backend.transaction.RetrieveSparseEntity; -import caosdb.server.database.backend.transaction.UpdateSparseEntity; +import caosdb.server.database.backend.transaction.SetFileChecksum; import caosdb.server.database.exceptions.TransactionException; import caosdb.server.entity.EntityInterface; import caosdb.server.entity.FileProperties; @@ -46,6 +46,7 @@ import java.security.NoSuchAlgorithmException; public class ChecksumUpdater extends WriteTransaction<TransactionContainer> implements Runnable { private Boolean running = false; + private static final ChecksumUpdater instance = new ChecksumUpdater(); private ChecksumUpdater() { @@ -85,7 +86,7 @@ public class ChecksumUpdater extends WriteTransaction<TransactionContainer> impl DatabaseMonitor.getInstance().acquireStrongAccess(this); // update - execute(new UpdateSparseEntity(fileEntity), strongAccess); + execute(new SetFileChecksum(fileEntity), strongAccess); strongAccess.commit(); } catch (final InterruptedException e) { @@ -141,10 +142,11 @@ public class ChecksumUpdater extends WriteTransaction<TransactionContainer> impl instance.running = false; return null; } - return execute(new RetrieveSparseEntity(id), weakAccess).getEntity(); + return execute(new RetrieveSparseEntity(id, null), weakAccess).getEntity(); } } catch (final Exception e) { e.printStackTrace(); + instance.running = false; return null; } finally { weakAccess.release(); diff --git a/src/main/java/caosdb/server/transaction/Insert.java b/src/main/java/caosdb/server/transaction/Insert.java index 627ada52655c5e7802e98172d1c32f8280a95fb4..c1d53f3051eb65efd9d658179e514e13863c313b 100644 --- a/src/main/java/caosdb/server/transaction/Insert.java +++ b/src/main/java/caosdb/server/transaction/Insert.java @@ -26,6 +26,7 @@ import caosdb.server.database.access.Access; import caosdb.server.database.backend.transaction.InsertEntity; import caosdb.server.entity.EntityInterface; import caosdb.server.entity.FileProperties; +import caosdb.server.entity.Version; import caosdb.server.entity.container.InsertContainer; import caosdb.server.entity.container.TransactionContainer; import caosdb.server.permissions.EntityACL; @@ -103,6 +104,10 @@ public class Insert extends WriteTransaction<InsertContainer> { public void insert(final TransactionContainer container, final Access access) throws Exception { if (container.getStatus().ordinal() >= EntityStatus.QUALIFIED.ordinal()) { execute(new InsertEntity(container), access); + for (EntityInterface e : container) { + // TODO move to InsertEntity transaction + e.setVersion(new Version(e.getVersion().getId(), this.getTimestamp())); + } } } diff --git a/src/main/java/caosdb/server/transaction/RetrieveUserRolesTransaction.java b/src/main/java/caosdb/server/transaction/RetrieveUserRolesTransaction.java index 76740a73c9439e4ea9e45479e57501c4c4e1a0d0..e864c20bcf040faf78dc54e24dbcdefc1a8be795 100644 --- a/src/main/java/caosdb/server/transaction/RetrieveUserRolesTransaction.java +++ b/src/main/java/caosdb/server/transaction/RetrieveUserRolesTransaction.java @@ -58,7 +58,7 @@ public class RetrieveUserRolesTransaction implements TransactionInterface { @Override public void execute() throws Exception { if (UserSources.isUserExisting(new Principal(this.realm, this.user))) { - this.roles = UserSources.resolve(this.realm, this.user); + this.roles = UserSources.resolveRoles(this.realm, this.user); } else { throw ServerMessages.ACCOUNT_DOES_NOT_EXIST; } diff --git a/src/main/java/caosdb/server/transaction/Transaction.java b/src/main/java/caosdb/server/transaction/Transaction.java index 28b8276e2b93699f2315fb754303194208bb37e5..d2c16438b5c071fec72b11b600604b46c4b2da30 100644 --- a/src/main/java/caosdb/server/transaction/Transaction.java +++ b/src/main/java/caosdb/server/transaction/Transaction.java @@ -4,6 +4,8 @@ * * 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> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -23,7 +25,6 @@ package caosdb.server.transaction; import caosdb.datetime.UTCDateTime; -import caosdb.server.accessControl.AuthenticationUtils; import caosdb.server.accessControl.Principal; import caosdb.server.database.DatabaseMonitor; import caosdb.server.database.access.Access; @@ -227,14 +228,8 @@ public abstract class Transaction<C extends TransactionContainer> extends Abstra // TODO move to post-transaction job private void writeHistory() throws TransactionException, Message { if (logHistory()) { - String realm = - getTransactor().getPrincipal() == AuthenticationUtils.ANONYMOUS_USER.getPrincipal() - ? "" - : ((Principal) getTransactor().getPrincipal()).getRealm(); - String username = - getTransactor().getPrincipal() == AuthenticationUtils.ANONYMOUS_USER.getPrincipal() - ? "anonymous" - : ((Principal) getTransactor().getPrincipal()).getUsername(); + String realm = ((Principal) getTransactor().getPrincipal()).getRealm(); + String username = ((Principal) getTransactor().getPrincipal()).getUsername(); execute( new InsertTransactionHistory( getContainer(), this.getClass().getSimpleName(), realm, username, getTimestamp()), diff --git a/src/main/java/caosdb/server/transaction/UpdateUserRolesTransaction.java b/src/main/java/caosdb/server/transaction/UpdateUserRolesTransaction.java index 257e8c06cc9d9c9f24774e541cea611663f5ff0d..f11400c3826170de658fd9e5a634665f00e1cb44 100644 --- a/src/main/java/caosdb/server/transaction/UpdateUserRolesTransaction.java +++ b/src/main/java/caosdb/server/transaction/UpdateUserRolesTransaction.java @@ -68,7 +68,7 @@ public class UpdateUserRolesTransaction extends AccessControlTransaction { } public Element getUserRolesElement() { - final Set<String> resulting_roles = UserSources.resolve(this.realm, this.user); + final Set<String> resulting_roles = UserSources.resolveRoles(this.realm, this.user); final Element rolesElem = RetrieveUserRolesTransaction.getUserRolesElement(resulting_roles); if (!this.roles.equals(resulting_roles) && resulting_roles != null) { final Element warning = new Element("Warning"); diff --git a/src/main/java/caosdb/server/transaction/WriteTransaction.java b/src/main/java/caosdb/server/transaction/WriteTransaction.java index e67ae0600bc573efbdf67b9b341299fe1fe3ca00..ec37d19d6f5a4663c0af0adda186c282215a302b 100644 --- a/src/main/java/caosdb/server/transaction/WriteTransaction.java +++ b/src/main/java/caosdb/server/transaction/WriteTransaction.java @@ -64,4 +64,8 @@ public abstract class WriteTransaction<C extends TransactionContainer> extends T } } } + + public String getSRID() { + return getContainer().getRequestId(); + } } diff --git a/src/main/java/caosdb/server/utils/FileUtils.java b/src/main/java/caosdb/server/utils/FileUtils.java index 824646caab6983863acd7d35a276a1213b393235..2357b7217c0fa9b112dd6c9e45d5be64d36edefb 100644 --- a/src/main/java/caosdb/server/utils/FileUtils.java +++ b/src/main/java/caosdb/server/utils/FileUtils.java @@ -218,6 +218,13 @@ public class FileUtils { } } + /** + * @deprecated Soon to be removed. + * @throws IOException + * @throws InterruptedException + * @throws CaosDBException + */ + @Deprecated public static void testChownScript() throws IOException, InterruptedException, CaosDBException { final String sudopw = CaosDBServer.getServerProperty(ServerProperties.KEY_SUDO_PASSWORD); final Process cmd = diff --git a/src/main/java/caosdb/server/utils/Initialization.java b/src/main/java/caosdb/server/utils/Initialization.java index e77922a417c1a2ef9542cb3dc9b8971ca10b332a..9f77fcf2992c0e7fb8de38ee334bebe8c93ef3d8 100644 --- a/src/main/java/caosdb/server/utils/Initialization.java +++ b/src/main/java/caosdb/server/utils/Initialization.java @@ -4,6 +4,8 @@ * * 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> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -26,7 +28,7 @@ import caosdb.server.database.DatabaseMonitor; import caosdb.server.database.access.Access; import caosdb.server.transaction.TransactionInterface; -public final class Initialization implements TransactionInterface { +public final class Initialization implements TransactionInterface, AutoCloseable { private Access access; private static final Initialization instance = new Initialization(); @@ -43,13 +45,14 @@ public final class Initialization implements TransactionInterface { return this.access; } - public final void release() { + @Override + public void execute() throws Exception {} + + @Override + public void close() throws Exception { if (this.access != null) { this.access.release(); this.access = null; } } - - @Override - public void execute() throws Exception {} } diff --git a/src/main/java/caosdb/server/utils/ServerMessages.java b/src/main/java/caosdb/server/utils/ServerMessages.java index c2f3a302d270a3f16616c00b41ec9118862d4605..29983924246f5bc5987ffdbfeaf949d0946c7f16 100644 --- a/src/main/java/caosdb/server/utils/ServerMessages.java +++ b/src/main/java/caosdb/server/utils/ServerMessages.java @@ -133,8 +133,6 @@ public class ServerMessages { 0, "Cannot parse value to boolean (either 'true' or 'false', ignoring case)."); - public static final Message NOT_PERMITTED = new Message(MessageType.Error, 0, "Not permitted."); - public static final Message CANNOT_CONNECT_TO_DATABASE = new Message(MessageType.Error, 0, "Could not connect to MySQL server."); @@ -257,6 +255,9 @@ public class ServerMessages { 0, "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."); + public static final Message AUTHORIZATION_ERROR = new Message(MessageType.Error, 403, "You are not allowed to do this."); diff --git a/src/main/java/caosdb/server/utils/Utils.java b/src/main/java/caosdb/server/utils/Utils.java index 6c48906a40fd299379e91446516a9c0325200f81..752d828d0e89e790aa7d42e1358b91d3fb5f3c1b 100644 --- a/src/main/java/caosdb/server/utils/Utils.java +++ b/src/main/java/caosdb/server/utils/Utils.java @@ -33,7 +33,6 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.text.DecimalFormat; -import java.util.Random; import java.util.Scanner; import java.util.regex.Pattern; import org.apache.commons.codec.binary.Base32; @@ -45,30 +44,9 @@ import org.jdom2.output.XMLOutputter; /** Utility functions. */ public class Utils { - /** Random number generator initialized with the system time and used for generating UIDs. */ - private static Random rand = new Random(System.currentTimeMillis()); - /** Secure random number generator, for secret random numbers. */ private static final SecureRandom srand = new SecureRandom(); - /** - * Check whether obj is non-null and can be parsed to an integer. - * - * @param obj The object to check. - * @return true if obj is not null and obj.toString() can be parsed as an integer - */ - public static boolean isNonNullInteger(final Object obj) { - if (obj == null) { - return false; - } - try { - Integer.parseInt(obj.toString()); - } catch (final NumberFormatException e) { - return false; - } - return true; - } - /** * Regular expression pattern that checks a mail for RFC822 compliance. * @@ -185,16 +163,14 @@ public class Utils { * @return The UID as a String. */ public static String getUID() { - synchronized (rand) { - return Long.toHexString(rand.nextLong()) + Long.toHexString(rand.nextLong()); - } + return Long.toHexString(srand.nextLong()) + Long.toHexString(srand.nextLong()); } /** * Generate a secure filename (base32 letters and numbers). * - * <p>Very similar to getUID, but uses cryptographic random number instead, also also nicely - * formats the resulting string. + * <p>Very similar to getUID, but uses cryptographic random number instead, also nicely formats + * the resulting string. * * @param byteSize How many bytes of random bits shall be generated. * @return The filename as a String. @@ -205,6 +181,7 @@ public class Utils { srand.nextBytes(bytes); // Encode to nice letters + // TODO use StringBuilder and iterate over filename.charArray String filename = (new Base32()).encodeToString(bytes); filename = filename.replaceAll("=+", ""); diff --git a/src/test/java/caosdb/server/Misc.java b/src/test/java/caosdb/server/Misc.java index bcdc1729f008dbb1393fe238a3eb763c67d69e0f..2988bae6aa178ace45f41e03da5c54f08114e485 100644 --- a/src/test/java/caosdb/server/Misc.java +++ b/src/test/java/caosdb/server/Misc.java @@ -39,11 +39,7 @@ import java.io.ObjectOutputStream; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.shiro.SecurityUtils; -import org.apache.shiro.config.Ini; -import org.apache.shiro.config.IniSecurityManagerFactory; -import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; -import org.apache.shiro.util.Factory; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -293,12 +289,7 @@ public class Misc { @Test public void testShiro() { - Ini config = CaosDBServer.getShiroConfig(); - final Factory<SecurityManager> factory = new IniSecurityManagerFactory(config); - - final SecurityManager securityManager = factory.getInstance(); - - SecurityUtils.setSecurityManager(securityManager); + CaosDBServer.initShiro(); final Subject subject = SecurityUtils.getSubject(); diff --git a/src/test/java/caosdb/server/authentication/AuthTokenTest.java b/src/test/java/caosdb/server/authentication/AuthTokenTest.java index 9937dc60b1a33cc421a74661bed028ae5e24522a..9f344889051d8508125dc5f6417d40839c44bd3d 100644 --- a/src/test/java/caosdb/server/authentication/AuthTokenTest.java +++ b/src/test/java/caosdb/server/authentication/AuthTokenTest.java @@ -22,13 +22,43 @@ */ package caosdb.server.authentication; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + import caosdb.server.CaosDBServer; +import caosdb.server.ServerProperties; +import caosdb.server.accessControl.AnonymousAuthenticationToken; import caosdb.server.accessControl.AuthenticationUtils; +import caosdb.server.accessControl.Config; import caosdb.server.accessControl.OneTimeAuthenticationToken; import caosdb.server.accessControl.Principal; +import caosdb.server.accessControl.SelfValidatingAuthenticationToken; import caosdb.server.accessControl.SessionToken; +import caosdb.server.accessControl.SessionTokenRealm; +import caosdb.server.database.BackendTransaction; +import caosdb.server.database.backend.interfaces.RetrievePasswordValidatorImpl; +import caosdb.server.database.backend.interfaces.RetrievePermissionRulesImpl; +import caosdb.server.database.backend.interfaces.RetrieveRoleImpl; +import caosdb.server.database.backend.interfaces.RetrieveUserImpl; +import caosdb.server.resource.TestScriptingResource.RetrievePasswordValidator; +import caosdb.server.resource.TestScriptingResource.RetrievePermissionRules; +import caosdb.server.resource.TestScriptingResource.RetrieveRole; +import caosdb.server.resource.TestScriptingResource.RetrieveUser; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; import java.io.IOException; +import java.util.List; +import java.util.Map; +import org.apache.commons.io.input.CharSequenceInputStream; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.subject.Subject; import org.junit.Assert; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -39,62 +69,49 @@ public class AuthTokenTest { CaosDBServer.initServerProperties(); } + @BeforeClass + public static void setupShiro() throws IOException { + BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRole.class); + BackendTransaction.setImpl(RetrievePermissionRulesImpl.class, RetrievePermissionRules.class); + BackendTransaction.setImpl(RetrieveUserImpl.class, RetrieveUser.class); + BackendTransaction.setImpl( + RetrievePasswordValidatorImpl.class, RetrievePasswordValidator.class); + + CaosDBServer.initServerProperties(); + CaosDBServer.initShiro(); + } + + @Before + public void reset() { + OneTimeAuthenticationToken.resetConfig(); + } + @Test public void testSessionToken() throws InterruptedException { - final String curry = "somecurry"; + // Token 1 - wrong checksum, not expired final SessionToken t1 = new SessionToken( new Principal("somerealm", "someuser1"), System.currentTimeMillis(), 60000, "345sdf56sdf", - curry, - "wrong checksum"); + "wrong checksum", + null, + null); Assert.assertFalse(t1.isExpired()); Assert.assertFalse(t1.isHashValid()); Assert.assertFalse(t1.isValid()); - final SessionToken t2 = - new SessionToken( - new Principal("somerealm", "someuser1"), - System.currentTimeMillis(), - 60000, - "345sdf56sdf", - null, - "wrong checksum"); - Assert.assertFalse(t2.isExpired()); - Assert.assertFalse(t2.isHashValid()); - Assert.assertFalse(t2.isValid()); - + // Token 3 - correct checksum, not expired final SessionToken t3 = - new SessionToken( - new Principal("somerealm", "someuser2"), - System.currentTimeMillis(), - 60000, - "72723gsdg", - curry); + new SessionToken(new Principal("somerealm", "someuser2"), 60000, null, null); Assert.assertFalse(t3.isExpired()); Assert.assertTrue(t3.isHashValid()); Assert.assertTrue(t3.isValid()); - final SessionToken t4 = - new SessionToken( - new Principal("somerealm", "someuser2"), - System.currentTimeMillis(), - 60000, - "72723gsdg", - null); - Assert.assertFalse(t4.isExpired()); - Assert.assertTrue(t4.isHashValid()); - Assert.assertTrue(t4.isValid()); - + // Token 5 - correct checksum, soon to be expired final SessionToken t5 = - new SessionToken( - new Principal("somerealm", "someuser3"), - System.currentTimeMillis(), - 2000, - "23sdfsg34", - curry); + new SessionToken(new Principal("somerealm", "someuser3"), 2000, null, null); Assert.assertFalse(t5.isExpired()); Assert.assertTrue(t5.isHashValid()); Assert.assertTrue(t5.isValid()); @@ -104,113 +121,331 @@ public class AuthTokenTest { Assert.assertTrue(t5.isHashValid()); Assert.assertFalse(t5.isValid()); + // Token 6 - correct checksum, immediately expired final SessionToken t6 = - new SessionToken( - new Principal("somerealm", "someuser3"), - System.currentTimeMillis(), - 0, - "23sdfsg34", - null); + new SessionToken(new Principal("somerealm", "someuser3"), 0, null, null); Assert.assertTrue(t6.isExpired()); Assert.assertTrue(t6.isHashValid()); Assert.assertFalse(t6.isValid()); - Assert.assertEquals(t1.toString(), SessionToken.parse(t1.toString(), curry).toString()); - Assert.assertEquals(t2.toString(), SessionToken.parse(t2.toString(), curry).toString()); - Assert.assertEquals(t3.toString(), SessionToken.parse(t3.toString(), curry).toString()); - Assert.assertEquals(t4.toString(), SessionToken.parse(t4.toString(), curry).toString()); - Assert.assertEquals(t5.toString(), SessionToken.parse(t5.toString(), curry).toString()); - Assert.assertEquals(t6.toString(), SessionToken.parse(t6.toString(), curry).toString()); - Assert.assertEquals(t1.toString(), SessionToken.parse(t1.toString(), null).toString()); - Assert.assertEquals(t2.toString(), SessionToken.parse(t2.toString(), null).toString()); - Assert.assertEquals(t3.toString(), SessionToken.parse(t3.toString(), null).toString()); - Assert.assertEquals(t4.toString(), SessionToken.parse(t4.toString(), null).toString()); - Assert.assertEquals(t5.toString(), SessionToken.parse(t5.toString(), null).toString()); - Assert.assertEquals(t6.toString(), SessionToken.parse(t6.toString(), null).toString()); - - Assert.assertFalse(SessionToken.parse(t1.toString(), curry).isHashValid()); - Assert.assertFalse(SessionToken.parse(t2.toString(), null).isHashValid()); - Assert.assertTrue(SessionToken.parse(t3.toString(), curry).isHashValid()); - Assert.assertTrue(SessionToken.parse(t4.toString(), null).isHashValid()); - Assert.assertTrue(SessionToken.parse(t5.toString(), curry).isHashValid()); - Assert.assertTrue(SessionToken.parse(t6.toString(), null).isHashValid()); - Assert.assertFalse(SessionToken.parse(t1.toString(), null).isHashValid()); - Assert.assertFalse(SessionToken.parse(t2.toString(), curry).isHashValid()); - Assert.assertFalse(SessionToken.parse(t3.toString(), null).isHashValid()); - Assert.assertFalse(SessionToken.parse(t4.toString(), curry).isHashValid()); - Assert.assertFalse(SessionToken.parse(t5.toString(), null).isHashValid()); - Assert.assertFalse(SessionToken.parse(t6.toString(), curry).isHashValid()); + // All tokens can be successfully parsed back. + final SelfValidatingAuthenticationToken t1p = SessionToken.parse(t1.toString()); + final SelfValidatingAuthenticationToken t3p = SessionToken.parse(t3.toString()); + final SelfValidatingAuthenticationToken t5p = SessionToken.parse(t5.toString()); + final SelfValidatingAuthenticationToken t6p = SessionToken.parse(t6.toString()); + Assert.assertEquals(t1.toString(), t1p.toString()); + Assert.assertEquals(t3.toString(), t3p.toString()); + Assert.assertEquals(t5.toString(), t5p.toString()); + Assert.assertEquals(t6.toString(), t6p.toString()); + + // ... and parsed tokens have the correct hash validation + Assert.assertFalse(t1p.isHashValid()); + Assert.assertTrue(t3p.isHashValid()); + Assert.assertTrue(t5p.isHashValid()); + Assert.assertTrue(t6p.isHashValid()); Assert.assertFalse( AuthenticationUtils.parseSessionTokenCookie( - AuthenticationUtils.createSessionTokenCookie(t4), null) - .isExpired()); - Assert.assertTrue( - AuthenticationUtils.parseSessionTokenCookie( - AuthenticationUtils.createSessionTokenCookie(t4), null) - .isHashValid()); - Assert.assertTrue( - AuthenticationUtils.parseSessionTokenCookie( - AuthenticationUtils.createSessionTokenCookie(t4), null) - .isValid()); - Assert.assertFalse( - AuthenticationUtils.parseSessionTokenCookie( - AuthenticationUtils.createSessionTokenCookie(t4), curry) - .isExpired()); - Assert.assertFalse( - AuthenticationUtils.parseSessionTokenCookie( - AuthenticationUtils.createSessionTokenCookie(t4), curry) - .isHashValid()); - Assert.assertFalse( - AuthenticationUtils.parseSessionTokenCookie( - AuthenticationUtils.createSessionTokenCookie(t4), curry) - .isValid()); - Assert.assertFalse( - AuthenticationUtils.parseSessionTokenCookie( - AuthenticationUtils.createSessionTokenCookie(t3), null) - .isExpired()); - Assert.assertFalse( - AuthenticationUtils.parseSessionTokenCookie( - AuthenticationUtils.createSessionTokenCookie(t3), null) - .isHashValid()); - Assert.assertFalse( - AuthenticationUtils.parseSessionTokenCookie( - AuthenticationUtils.createSessionTokenCookie(t3), null) - .isValid()); - Assert.assertFalse( - AuthenticationUtils.parseSessionTokenCookie( - AuthenticationUtils.createSessionTokenCookie(t3), curry) + AuthenticationUtils.createSessionTokenCookie(t3)) .isExpired()); Assert.assertTrue( AuthenticationUtils.parseSessionTokenCookie( - AuthenticationUtils.createSessionTokenCookie(t3), curry) + AuthenticationUtils.createSessionTokenCookie(t3)) .isHashValid()); Assert.assertTrue( AuthenticationUtils.parseSessionTokenCookie( - AuthenticationUtils.createSessionTokenCookie(t3), curry) + AuthenticationUtils.createSessionTokenCookie(t3)) .isValid()); + + // TODO parse invalid tokens } @Test - public void testOneTimeToken() { - final String curry = null; + public void testOneTimeTokenSerialization() { final OneTimeAuthenticationToken t1 = new OneTimeAuthenticationToken( new Principal("somerealm", "someuser"), - System.currentTimeMillis(), - 60000L, - "sdfh37456sd", - curry, - new String[] {""}); - System.err.println(t1.toString()); + 60000, + new String[] {"permissions"}, + new String[] {"roles"}, + 1L, + 3000L); + Assert.assertEquals(1L, t1.getMaxReplays()); Assert.assertFalse(t1.isExpired()); Assert.assertTrue(t1.isHashValid()); Assert.assertTrue(t1.isValid()); + String serialized = t1.toString(); + OneTimeAuthenticationToken parsed = + (OneTimeAuthenticationToken) OneTimeAuthenticationToken.parse(serialized); + + Assert.assertEquals(t1, parsed); + Assert.assertEquals(serialized, parsed.toString()); + + Assert.assertEquals(1L, parsed.getMaxReplays()); + Assert.assertFalse(parsed.isExpired()); + Assert.assertTrue(parsed.isHashValid()); + Assert.assertTrue(parsed.isValid()); + } + + @Test(expected = AuthenticationException.class) + public void testOneTimeTokenConsume() { + final OneTimeAuthenticationToken t1 = + new OneTimeAuthenticationToken( + new Principal("somerealm", "someuser"), + 60000, + new String[] {"permissions"}, + new String[] {"roles"}, + 3L, + 3000L); + Assert.assertFalse(t1.isExpired()); + Assert.assertTrue(t1.isHashValid()); + Assert.assertTrue(t1.isValid()); + try { + t1.consume(); + t1.consume(); + t1.consume(); + } catch (AuthenticationException e) { + Assert.fail(e.getMessage()); + } + + // throws + t1.consume(); + Assert.fail("4th time consume() should throw"); + } + + @Test(expected = AuthenticationException.class) + public void testOneTimeTokenConsumeByParsing() { + final OneTimeAuthenticationToken t1 = + new OneTimeAuthenticationToken( + new Principal("somerealm", "someuser"), + 60000, + new String[] {"permissions"}, + new String[] {"roles"}, + 3L, + 3000L); + Assert.assertFalse(t1.isExpired()); + Assert.assertTrue(t1.isHashValid()); + Assert.assertTrue(t1.isValid()); + + String serialized = t1.toString(); + try { + SelfValidatingAuthenticationToken parsed1 = OneTimeAuthenticationToken.parse(serialized); + Assert.assertTrue(parsed1.isValid()); + SelfValidatingAuthenticationToken parsed2 = OneTimeAuthenticationToken.parse(serialized); + Assert.assertTrue(parsed2.isValid()); + SelfValidatingAuthenticationToken parsed3 = OneTimeAuthenticationToken.parse(serialized); + Assert.assertTrue(parsed3.isValid()); + } catch (AuthenticationException e) { + Assert.fail(e.getMessage()); + } + + // throws + OneTimeAuthenticationToken.parse(serialized); + Assert.fail("4th parsing should throw"); + } + + @Test + public void testOneTimeTokenConfigEmpty() throws Exception { + StringBuilder testYaml = new StringBuilder(); + testYaml.append("[]"); + + List<Config> configs = + OneTimeAuthenticationToken.loadConfig(new CharSequenceInputStream(testYaml, "utf-8")); + + Assert.assertTrue("empty config", configs.isEmpty()); + } + + @Test + public void testOneTimeTokenConfigDefaults() throws Exception { + StringBuilder testYaml = new StringBuilder(); + testYaml.append("roles: []\n"); + + List<Config> configs = + OneTimeAuthenticationToken.loadConfig(new CharSequenceInputStream(testYaml, "utf-8")); + + Assert.assertEquals(1, configs.size()); + Assert.assertTrue("parsing to Config object", configs.get(0) instanceof Config); + + Config config = configs.get(0); + Assert.assertEquals( - t1.toString(), OneTimeAuthenticationToken.parse(t1.toString(), curry).toString()); - Assert.assertFalse(OneTimeAuthenticationToken.parse(t1.toString(), curry).isExpired()); - Assert.assertTrue(OneTimeAuthenticationToken.parse(t1.toString(), curry).isHashValid()); - Assert.assertTrue(OneTimeAuthenticationToken.parse(t1.toString(), curry).isValid()); + Integer.parseInt( + CaosDBServer.getServerProperty(ServerProperties.KEY_ONE_TIME_TOKEN_EXPIRES_MS)), + config.getExpiresAfter()); + Assert.assertEquals(1, config.getMaxReplays()); + Assert.assertNull("no purpose", config.getPurpose()); + + Assert.assertArrayEquals("no permissions", new String[] {}, config.getPermissions()); + Assert.assertArrayEquals("no roles", new String[] {}, config.getRoles()); + Assert.assertNull("no output", config.getOutput()); + } + + @Test + public void testOneTimeTokenConfigBasic() throws Exception { + StringBuilder testYaml = new StringBuilder(); + testYaml.append("purpose: test purpose 1\n"); + testYaml.append("roles: [ role1, \"role2\"]\n"); + testYaml.append("expiresAfterSeconds: 10\n"); + testYaml.append("maxReplays: 3\n"); + testYaml.append("permissions:\n"); + testYaml.append(" - permission1\n"); + testYaml.append(" - 'permission2'\n"); + testYaml.append(" - \"permission3\"\n"); + testYaml.append(" - \"permission with white space\"\n"); + + OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8")); + Map<String, Config> map = OneTimeAuthenticationToken.getPurposeMap(); + Assert.assertNotNull("test purpose there", map.get("test purpose 1")); + Assert.assertTrue("parsing to Config object", map.get("test purpose 1") instanceof Config); + Config config = map.get("test purpose 1"); + Assert.assertEquals(10000, config.getExpiresAfter()); + Assert.assertEquals(3, config.getMaxReplays()); + + Assert.assertArrayEquals( + "permissions parsed", + new String[] {"permission1", "permission2", "permission3", "permission with white space"}, + config.getPermissions()); + Assert.assertArrayEquals("roles parsed", new String[] {"role1", "role2"}, config.getRoles()); + } + + @Test + public void testOneTimeTokenConfigNoRoles() throws Exception { + StringBuilder testYaml = new StringBuilder(); + testYaml.append("purpose: no roles test\n"); + testYaml.append("permissions:\n"); + testYaml.append(" - permission1\n"); + testYaml.append(" - 'permission2'\n"); + testYaml.append(" - \"permission3\"\n"); + testYaml.append(" - \"permission with white space\"\n"); + + OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8")); + Map<String, Config> map = OneTimeAuthenticationToken.getPurposeMap(); + Config config = map.get("no roles test"); + + Assert.assertArrayEquals("empty roles array parsed", new String[] {}, config.getRoles()); + } + + @Test + public void testOneTimeTokenConfigNoPurpose() throws Exception { + StringBuilder testYaml = new StringBuilder(); + testYaml.append("permissions:\n"); + testYaml.append(" - permission1\n"); + testYaml.append(" - 'permission2'\n"); + testYaml.append(" - \"permission3\"\n"); + testYaml.append(" - \"permission with white space\"\n"); + + OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8")); + Map<String, Config> map = OneTimeAuthenticationToken.getPurposeMap(); + Assert.assertEquals(map.size(), 0); + } + + @Test + public void testOneTimeTokenConfigMulti() throws Exception { + StringBuilder testYaml = new StringBuilder(); + testYaml.append("- purpose: purpose 1\n"); + testYaml.append("- purpose: purpose 2\n"); + testYaml.append("- purpose: purpose 3\n"); + testYaml.append("- purpose: purpose 4\n"); + + OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8")); + Map<String, Config> map = OneTimeAuthenticationToken.getPurposeMap(); + Assert.assertEquals("four items", 4, map.size()); + Assert.assertTrue(map.get("purpose 2") instanceof Config); + } + + @Test + public void testOneTimeTokenConfigOutputFile() throws Exception { + File tempFile = File.createTempFile("authtoken", "json"); + tempFile.deleteOnExit(); + + StringBuilder testYaml = new StringBuilder(); + testYaml.append("- output:\n"); + testYaml.append(" file: " + tempFile.getAbsolutePath() + "\n"); + testYaml.append(" permissions: [ permission1 ]\n"); + + // write the token + OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8")); + Assert.assertTrue(tempFile.exists()); + try (BufferedReader reader = new BufferedReader(new FileReader(tempFile))) { + OneTimeAuthenticationToken token = + (OneTimeAuthenticationToken) SelfValidatingAuthenticationToken.parse(reader.readLine()); + assertEquals("Token has anonymous username", "anonymous", token.getPrincipal().getUsername()); + assertEquals( + "Token has anonymous realm", + OneTimeAuthenticationToken.REALM_NAME, + token.getPrincipal().getRealm()); + assertArrayEquals( + "Permissions array has been written and read", + new String[] {"permission1"}, + token.getPermissions().toArray()); + } + } + + @Test + public void testOneTimeTokenForAnonymous() throws Exception { + StringBuilder testYaml = new StringBuilder(); + testYaml.append("purpose: for anonymous\n"); + testYaml.append("roles: [ role1 ]\n"); + testYaml.append("permissions:\n"); + testYaml.append(" - permission1\n"); + + OneTimeAuthenticationToken.initConfig(new CharSequenceInputStream(testYaml, "utf-8")); + + Subject anonymous = SecurityUtils.getSubject(); + anonymous.login(AnonymousAuthenticationToken.getInstance()); + + OneTimeAuthenticationToken token = + OneTimeAuthenticationToken.generateForPurpose("for anonymous", anonymous); + assertEquals("anonymous", token.getPrincipal().getUsername()); + } + + @Test + public void testSessionTokenRealm() { + Config config = new Config(); + OneTimeAuthenticationToken token = OneTimeAuthenticationToken.generate(config); + + String serialized = token.toString(); + SelfValidatingAuthenticationToken parsed = SelfValidatingAuthenticationToken.parse(serialized); + + SessionTokenRealm sessionTokenRealm = new SessionTokenRealm(); + Assert.assertTrue(sessionTokenRealm.supports(token)); + Assert.assertTrue(sessionTokenRealm.supports(parsed)); + + Assert.assertNotNull(sessionTokenRealm.getAuthenticationInfo(token)); + Assert.assertNotNull(sessionTokenRealm.getAuthenticationInfo(parsed)); + + Subject anonymous = SecurityUtils.getSubject(); + anonymous.login(token); + } + + @Test + public void testIntInConfigYaml() throws IOException { + StringBuilder testYaml = new StringBuilder(); + testYaml.append("- expiresAfterSeconds: 1000\n"); + testYaml.append(" replayTimeout: 1000\n"); + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + ObjectReader reader = mapper.readerFor(Config.class); + Config config = + (Config) reader.readValues(new CharSequenceInputStream(testYaml, "utf-8")).next(); + + assertEquals(1000000, config.getExpiresAfter()); + assertEquals(1000, config.getReplayTimeout()); + } + + @Test + public void testLongInConfigYaml() throws IOException { + StringBuilder testYaml = new StringBuilder(); + testYaml.append("- expiresAfter: 9223372036854775000\n"); + testYaml.append(" replayTimeoutSeconds: 922337203685477\n"); + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + ObjectReader reader = mapper.readerFor(Config.class); + Config config = + (Config) reader.readValues(new CharSequenceInputStream(testYaml, "utf-8")).next(); + + assertEquals(9223372036854775000L, config.getExpiresAfter()); + assertEquals(922337203685477000L, config.getReplayTimeout()); } } diff --git a/src/test/java/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java b/src/test/java/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java new file mode 100644 index 0000000000000000000000000000000000000000..78c2ae8206fbe5aac33ee4d9d9d73b2fae5965ef --- /dev/null +++ b/src/test/java/caosdb/server/database/backend/transaction/RetrieveFullEntityTest.java @@ -0,0 +1,79 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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 + * 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/>. + * + * ** end header + */ +package caosdb.server.database.backend.transaction; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import caosdb.server.datatype.ReferenceValue; +import caosdb.server.entity.Entity; +import caosdb.server.entity.EntityInterface; +import caosdb.server.entity.wrapper.Property; +import caosdb.server.entity.xml.PropertyToElementStrategyTest; +import caosdb.server.query.Query.Selection; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +public class RetrieveFullEntityTest { + + @Test + public void testRetrieveSubEntities() { + RetrieveFullEntity r = + new RetrieveFullEntity(0) { + + /** Mock-up */ + @Override + public void retrieveFullEntity(EntityInterface e, List<Selection> selections) { + // The id of the referenced window + assertEquals(1234, (int) e.getId()); + + // The level of selectors has been reduced by 1 + assertEquals("description", selections.get(0).getSelector()); + + e.setDescription("A heart-shaped window."); + }; + }; + + Property window = new Property(2345); + window.setName("Window"); + window.setDatatype("Window"); + window.setValue(new ReferenceValue(1234)); + + Entity house = new Entity(); + house.addProperty(window); + ReferenceValue value = (ReferenceValue) house.getProperties().getEntityById(2345).getValue(); + assertEquals(1234, (int) value.getId()); + assertNull(value.getEntity()); + + List<Selection> selections = new ArrayList<>(); + selections.add(PropertyToElementStrategyTest.parse("window.description")); + + r.retrieveSubEntities(house, selections); + + assertEquals(1234, (int) value.getId()); + assertNotNull(value.getEntity()); + assertEquals("A heart-shaped window.", value.getEntity().getDescription()); + } +} diff --git a/src/test/java/caosdb/server/entity/SelectionTest.java b/src/test/java/caosdb/server/entity/SelectionTest.java index cc961b9a5cfa47c022cfdf63201ea53613758a32..068a87c48a0257925d120613f83b4741173873d1 100644 --- a/src/test/java/caosdb/server/entity/SelectionTest.java +++ b/src/test/java/caosdb/server/entity/SelectionTest.java @@ -4,6 +4,8 @@ * * Copyright (C) 2018 Research Group Biomedical Physics, * Max-Planck-Institute for Dynamics and Self-Organization Göttingen + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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 @@ -22,13 +24,26 @@ */ package caosdb.server.entity; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import caosdb.server.CaosDBServer; import caosdb.server.entity.xml.SetFieldStrategy; +import caosdb.server.query.Query; import caosdb.server.query.Query.Selection; +import java.io.IOException; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; public class SelectionTest { + @BeforeClass + public static void initServerProperties() throws IOException { + CaosDBServer.initServerProperties(); + } + @Test public void testEmpty1() { final SetFieldStrategy setFieldStrategy = new SetFieldStrategy(); @@ -59,7 +74,7 @@ public class SelectionTest { final Selection selection = new Selection("id"); final SetFieldStrategy setFieldStrategy = (new SetFieldStrategy()).addSelection(selection); - Assert.assertFalse(setFieldStrategy.isToBeSet("name")); + Assert.assertTrue(setFieldStrategy.isToBeSet("name")); } @Test @@ -262,16 +277,50 @@ public class SelectionTest { .addSelection(new Selection("blabla").setSubSelection(new Selection("value"))) .addSelection(new Selection("blabla").setSubSelection(new Selection("description"))); - Assert.assertTrue(setFieldStrategy.isToBeSet("blabla")); - Assert.assertTrue(setFieldStrategy.isToBeSet("id")); - Assert.assertTrue(setFieldStrategy.isToBeSet("name")); - Assert.assertFalse(setFieldStrategy.isToBeSet("bleb")); + assertTrue(setFieldStrategy.isToBeSet("blabla")); + assertTrue(setFieldStrategy.isToBeSet("id")); + assertTrue(setFieldStrategy.isToBeSet("name")); + assertFalse(setFieldStrategy.isToBeSet("bleb")); final SetFieldStrategy forProperty = setFieldStrategy.forProperty("blabla"); - Assert.assertTrue(forProperty.isToBeSet("id")); - Assert.assertTrue(forProperty.isToBeSet("name")); - Assert.assertTrue(forProperty.isToBeSet("description")); - Assert.assertTrue(forProperty.isToBeSet("value")); - Assert.assertFalse(forProperty.isToBeSet("blub")); + assertTrue(forProperty.isToBeSet("id")); + assertTrue(forProperty.isToBeSet("name")); + assertTrue(forProperty.isToBeSet("description")); + assertTrue(forProperty.isToBeSet("value")); + assertFalse(forProperty.isToBeSet("blub")); + } + + @Test + public void testSubSubSelection() { + String query_str = "SELECT property.subproperty.subsubproperty FROM ENTITY"; + Query query = new Query(query_str); + query.parse(); + assertEquals(query.getSelections().size(), 1); + + Selection s = query.getSelections().get(0); + assertEquals(s.toString(), "property.subproperty.subsubproperty"); + assertEquals(s.getSubselection().toString(), "subproperty.subsubproperty"); + assertEquals(s.getSubselection().getSubselection().toString(), "subsubproperty"); + } + + @Test + public void testSubSubProperty() { + Selection s = + new Selection("property") + .setSubSelection( + new Selection("subproperty").setSubSelection(new Selection("subsubproperty"))); + + assertEquals(s.toString(), "property.subproperty.subsubproperty"); + + SetFieldStrategy setFieldStrategy = new SetFieldStrategy().addSelection(s); + assertTrue(setFieldStrategy.isToBeSet("property")); + assertFalse(setFieldStrategy.forProperty("property").isToBeSet("sadf")); + // assertFalse(setFieldStrategy.forProperty("property").isToBeSet("name")); + assertTrue(setFieldStrategy.forProperty("property").isToBeSet("subproperty")); + assertTrue( + setFieldStrategy + .forProperty("property") + .forProperty("subproperty") + .isToBeSet("subsubproperty")); } } diff --git a/src/test/java/caosdb/server/entity/container/PropertyContainerTest.java b/src/test/java/caosdb/server/entity/container/PropertyContainerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0678e83072a4f6f3f93aacb8a9f4196d5a566c2e --- /dev/null +++ b/src/test/java/caosdb/server/entity/container/PropertyContainerTest.java @@ -0,0 +1,80 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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 + * 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/>. + * + * ** end header + */ +package caosdb.server.entity.container; + +import static org.junit.Assert.assertEquals; + +import caosdb.server.datatype.GenericValue; +import caosdb.server.datatype.ReferenceValue; +import caosdb.server.entity.Entity; +import caosdb.server.entity.Role; +import caosdb.server.entity.wrapper.Property; +import caosdb.server.entity.xml.PropertyToElementStrategyTest; +import caosdb.server.entity.xml.SetFieldStrategy; +import org.jdom2.Element; +import org.junit.BeforeClass; +import org.junit.Test; + +public class PropertyContainerTest { + + public static Entity house = null; + public static Property houseHeight = null; + public static Entity window = null; + public static Property windowHeight = null; + public static Entity houseOwner = null; + public static Property windowProperty = null; + + @BeforeClass + public static void setup() { + window = new Entity(1234); + windowHeight = new Property(new Entity("window.height", Role.Property)); + window.addProperty(windowHeight); + windowHeight.setValue(new GenericValue("windowHeight")); + + houseOwner = new Entity("The Queen", Role.Record); + + house = new Entity("Buckingham Palace", Role.Record); + houseHeight = new Property(new Entity("height", Role.Property)); + houseHeight.setValue(new GenericValue("houseHeight")); + house.addProperty(houseHeight); + windowProperty = new Property(2345); + windowProperty.setName("window"); + windowProperty.setValue(new ReferenceValue(window.getId())); + house.addProperty(windowProperty); + + house.addProperty(new Property()); + house.addProperty(new Property(houseHeight)); + } + + @Test + public void test() { + PropertyContainer container = new PropertyContainer(new Entity()); + Element element = new Element("Record"); + SetFieldStrategy setFieldStrategy = + new SetFieldStrategy().addSelection(PropertyToElementStrategyTest.parse("window.height")); + + container.addToElement(windowProperty, element, setFieldStrategy); + + assertEquals(1, element.getChildren().size()); + } +} diff --git a/src/test/java/caosdb/server/entity/xml/PropertyToElementStrategyTest.java b/src/test/java/caosdb/server/entity/xml/PropertyToElementStrategyTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0d60ebd70feebe2e8e8810f296908e8b3a74100e --- /dev/null +++ b/src/test/java/caosdb/server/entity/xml/PropertyToElementStrategyTest.java @@ -0,0 +1,105 @@ +/* + * ** header v3.0 + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com> + * Copyright (C) 2020 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 + * 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/>. + * + * ** end header + */ +package caosdb.server.entity.xml; + +import static org.junit.Assert.assertEquals; + +import caosdb.server.datatype.GenericValue; +import caosdb.server.datatype.ReferenceValue; +import caosdb.server.entity.Entity; +import caosdb.server.entity.EntityInterface; +import caosdb.server.entity.Role; +import caosdb.server.entity.wrapper.Property; +import caosdb.server.query.Query.Selection; +import org.jdom2.Element; +import org.junit.Before; +import org.junit.Test; + +public class PropertyToElementStrategyTest { + + public static Entity house = null; + public static Property houseHeight = null; + public static Entity window = null; + public static Property windowHeight = null; + public static Entity houseOwner = null; + public static Property windowProperty = null; + + /** + * Create a nested selection out of the dot-separated parts of <code>select</code>. + * + * <p>The returned Selection has nested subselections, so that each subselection corresponds to + * the next part and the remainder of the initial <code>select</code> String. + */ + public static Selection parse(String select) { + String[] split = select.split("\\."); + Selection result = new Selection(split[0]); + Selection next = result; + + for (int i = 1; i < split.length; i++) { + next.setSubSelection(new Selection(split[i])); + next = next.getSubselection(); + } + return result; + } + + @Before + public void setup() { + window = new Entity(1234, Role.Record); + windowHeight = new Property(new Entity("height", Role.Property)); + window.addProperty(windowHeight); + windowHeight.setValue(new GenericValue("windowHeight")); + + houseOwner = new Entity("The Queen", Role.Record); + + house = new Entity("Buckingham Palace", Role.Record); + houseHeight = new Property(new Entity("height", Role.Property)); + houseHeight.setValue(new GenericValue("houseHeight")); + house.addProperty(houseHeight); + windowProperty = new Property(2345); + windowProperty.setName("window"); + windowProperty.setValue(new ReferenceValue(window.getId())); + house.addProperty(windowProperty); + + house.addProperty(new Property()); + house.addProperty(new Property(houseHeight)); + } + + @Test + public void test() { + PropertyToElementStrategy strategy = new PropertyToElementStrategy(); + SetFieldStrategy setFieldStrategy = new SetFieldStrategy().addSelection(parse("height")); + EntityInterface property = windowProperty; + ((ReferenceValue) property.getValue()).setEntity(window, true); + Element element = strategy.toElement(property, setFieldStrategy); + assertEquals("Property", element.getName()); + assertEquals("2345", element.getAttributeValue("id")); + assertEquals("window", element.getAttributeValue("name")); + assertEquals(1, element.getChildren().size()); + assertEquals("Record", element.getChildren().get(0).getName()); + + Element recordElement = element.getChild("Record"); + assertEquals("1234", recordElement.getAttributeValue("id")); + assertEquals(1, recordElement.getChildren().size()); + assertEquals("windowHeight", recordElement.getChild("Property").getText()); + } +} diff --git a/src/test/java/caosdb/server/permissions/EntityACLTest.java b/src/test/java/caosdb/server/permissions/EntityACLTest.java index 13ebb6279546606c8e796672db09a7e99ffea588..6aae2257e5623d072c5f60e5991a5cb0248ecf74 100644 --- a/src/test/java/caosdb/server/permissions/EntityACLTest.java +++ b/src/test/java/caosdb/server/permissions/EntityACLTest.java @@ -23,14 +23,29 @@ package caosdb.server.permissions; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import caosdb.server.CaosDBServer; +import caosdb.server.accessControl.AnonymousAuthenticationToken; +import caosdb.server.accessControl.AuthenticationUtils; +import caosdb.server.accessControl.Config; +import caosdb.server.accessControl.OneTimeAuthenticationToken; +import caosdb.server.accessControl.Role; +import caosdb.server.database.BackendTransaction; +import caosdb.server.database.access.Access; +import caosdb.server.database.backend.interfaces.RetrievePermissionRulesImpl; +import caosdb.server.database.backend.interfaces.RetrieveRoleImpl; +import caosdb.server.database.exceptions.TransactionException; +import caosdb.server.database.misc.TransactionBenchmark; import caosdb.server.resource.AbstractCaosDBServerResource; import caosdb.server.resource.AbstractCaosDBServerResource.XMLParser; import caosdb.server.utils.Utils; import java.io.IOException; import java.util.BitSet; +import java.util.HashSet; import java.util.LinkedList; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; import org.jdom2.Element; import org.jdom2.JDOMException; import org.junit.Assert; @@ -47,10 +62,53 @@ public class EntityACLTest { return value; } + /** a no-op mock-up which resolved all rules to an empty set of permissions. */ + public static class RetrievePermissionRulesMockup implements RetrievePermissionRulesImpl { + + public RetrievePermissionRulesMockup(Access a) {} + + @Override + public void setTransactionBenchmark(TransactionBenchmark b) {} + + @Override + public TransactionBenchmark getBenchmark() { + return null; + } + + @Override + public HashSet<PermissionRule> retrievePermissionRule(String role) throws TransactionException { + return new HashSet<>(); + } + } + + /** 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; + } + } + @BeforeClass public static void init() throws IOException { CaosDBServer.initServerProperties(); + CaosDBServer.initShiro(); assertNotNull(EntityACL.GLOBAL_PERMISSIONS); + + BackendTransaction.setImpl( + RetrievePermissionRulesImpl.class, RetrievePermissionRulesMockup.class); + BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRoleMockup.class); } @Test @@ -172,35 +230,31 @@ public class EntityACLTest { Assert.assertEquals(convert(EntityACL.convert(EntityACL.OWNER_BITSET).get(1, 32)), 0); } - // @Test - // public void testDeserialize() { - // Assert.assertTrue(EntityACL.deserialize("{}") instanceof EntityACL); - // Assert.assertTrue(EntityACL.deserialize("{tf:134}") instanceof - // EntityACL); - // Assert.assertTrue(EntityACL.deserialize("{tf:6343;bla:884}") instanceof - // EntityACL); - // Assert.assertTrue(EntityACL.deserialize("{tf:-2835;bla:884}") instanceof - // EntityACL); - // Assert.assertTrue(EntityACL.deserialize("{?OWNER?:526;tahsdh : -235;}") - // instanceof EntityACL); - // Assert.assertTrue(EntityACL.deserialize("{asdf:2345;}") instanceof - // EntityACL); - // Assert.assertTrue(raisesIllegalArguementException("{")); - // Assert.assertTrue(raisesIllegalArguementException("}")); - // Assert.assertTrue(raisesIllegalArguementException("{tf:}")); - // Assert.assertTrue(raisesIllegalArguementException("{tf:;}")); - // Assert.assertTrue(raisesIllegalArguementException("{:234}")); - // Assert.assertTrue(raisesIllegalArguementException("{:234;}")); - // Assert.assertTrue(raisesIllegalArguementException("{tf:tf;}")); - // Assert.assertTrue(raisesIllegalArguementException("{tf: +5259;}")); - // Assert.assertTrue(raisesIllegalArguementException("{tf;}")); - // Assert.assertTrue(raisesIllegalArguementException("{tf:123223727356235782735235;}")); - // } - - public boolean raisesIllegalArguementException(final String input) { + @Test + public void testDeserialize() { + Assert.assertTrue(EntityACL.deserialize("{}") instanceof EntityACL); + Assert.assertTrue(EntityACL.deserialize("{\"tf\":134}") instanceof EntityACL); + Assert.assertTrue(EntityACL.deserialize("{\"tf\":6343,\"bla\":884}") instanceof EntityACL); + Assert.assertTrue(EntityACL.deserialize("{\"tf\":-2835,\"bla\":884}") instanceof EntityACL); + Assert.assertTrue( + EntityACL.deserialize("{\"?OWNER?\":526,\"tahsdh \": -235}") instanceof EntityACL); + Assert.assertTrue(EntityACL.deserialize("{\"asdf\":2345}") instanceof EntityACL); + Assert.assertTrue(raisesIllegalStateException("{")); + Assert.assertTrue(raisesIllegalStateException("}")); + Assert.assertTrue(raisesIllegalStateException("{tf:}")); + Assert.assertTrue(raisesIllegalStateException("{tf:;}")); + Assert.assertTrue(raisesIllegalStateException("{:234}")); + Assert.assertTrue(raisesIllegalStateException("{:234;}")); + Assert.assertTrue(raisesIllegalStateException("{tf:tf;}")); + Assert.assertTrue(raisesIllegalStateException("{tf: +5259;}")); + Assert.assertTrue(raisesIllegalStateException("{tf;}")); + Assert.assertTrue(raisesIllegalStateException("{tf:123223727356235782735235;}")); + } + + public boolean raisesIllegalStateException(final String input) { try { EntityACL.deserialize(input); - } catch (final IllegalArgumentException e) { + } catch (final IllegalStateException e) { return true; } return false; @@ -212,134 +266,143 @@ public class EntityACLTest { return parser.parse(Utils.String2InputStream(s)).getRootElement(); } - // @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' + @Test + public void testEntityACLForAnonymous() { + Subject anonymous = SecurityUtils.getSubject(); + anonymous.login(AnonymousAuthenticationToken.getInstance()); + assertTrue(AuthenticationUtils.isAnonymous(anonymous)); + EntityACL acl = EntityACL.getOwnerACLFor(anonymous); + assertNotNull(acl); + 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' + // Assert.assertEquals( + // "{bla:32;}", + // EntityACL.serialize(EntityACL + // .parseFromElement(stringToJdom("<ACL><Grant role='bla'><Permission name='RETRIEVE:ACL' // /></Grant></ACL>")))); - // } - - // @Test - // public void testFactory() { - // final EntityACLFactory f = new EntityACLFactory(); - // f.grant("user1", "UPDATE:NAME"); - // Assert.assertTrue((f.create().isPermitted("user1", - // EntityPermission.UPDATE_NAME))); - // Assert.assertFalse((f.create().isPermitted("user2", - // EntityPermission.UPDATE_NAME))); - // f.grant("user2", "DELETE"); - // Assert.assertFalse((f.create().isPermitted("user1", - // EntityPermission.DELETE))); - // Assert.assertTrue((f.create().isPermitted("user2", - // EntityPermission.DELETE))); - // f.deny("user2", 1); - // f.deny("user1", 1); - // Assert.assertFalse((f.create().isPermitted("user1", - // EntityPermission.DELETE))); - // Assert.assertFalse((f.create().isPermitted("user2", - // EntityPermission.DELETE))); - // f.grant("user1", true, 1); - // Assert.assertTrue((f.create().isPermitted("user1", - // EntityPermission.DELETE))); - // Assert.assertFalse((f.create().isPermitted("user2", - // EntityPermission.DELETE))); - // f.deny("user2", true, 1); - // Assert.assertTrue((f.create().isPermitted("user1", - // EntityPermission.DELETE))); - // Assert.assertFalse((f.create().isPermitted("user2", - // EntityPermission.DELETE))); - // f.grant("user2", true, 1); - // Assert.assertTrue((f.create().isPermitted("user1", - // EntityPermission.DELETE))); - // Assert.assertFalse((f.create().isPermitted("user2", - // EntityPermission.DELETE))); - // f.deny("user1", true, 1); - // Assert.assertFalse((f.create().isPermitted("user1", - // EntityPermission.DELETE))); - // Assert.assertFalse((f.create().isPermitted("user2", - // EntityPermission.DELETE))); - // Assert.assertTrue((f.create().isPermitted("user1", - // EntityPermission.UPDATE_NAME))); - // Assert.assertFalse((f.create().isPermitted("user2", - // EntityPermission.UPDATE_NAME))); - // } - - // @Test - // public void niceFactoryStuff() { - // final EntityACLFactory f = new EntityACLFactory(); - // f.grant("user1", "*"); - // final EntityACL acl1 = f.create(); - // Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.EDIT_ACL)); - // Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.DELETE)); - // Assert.assertTrue(acl1.isPermitted("user1", - // EntityPermission.RETRIEVE_ENTITY)); - // Assert.assertTrue(acl1.isPermitted("user1", - // EntityPermission.UPDATE_DATA_TYPE)); - // Assert.assertTrue(acl1.isPermitted("user1", - // EntityPermission.USE_AS_PROPERTY)); + // } + + @Test + public void testFactory() { + final EntityACLFactory f = new EntityACLFactory(); + + caosdb.server.permissions.Role role1 = caosdb.server.permissions.Role.create("role1"); + Config config1 = new Config(); + config1.setRoles(new String[] {role1.toString()}); + OneTimeAuthenticationToken token1 = OneTimeAuthenticationToken.generate(config1); + Subject user1 = SecurityUtils.getSecurityManager().createSubject(null); + user1.login(token1); + + caosdb.server.permissions.Role role2 = caosdb.server.permissions.Role.create("role2"); + Config config2 = new Config(); + config2.setRoles(new String[] {role2.toString()}); + OneTimeAuthenticationToken token2 = OneTimeAuthenticationToken.generate(config2); + Subject user2 = SecurityUtils.getSecurityManager().createSubject(null); + user2.login(token2); + + f.grant(role1, "UPDATE:NAME"); + Assert.assertTrue((f.create().isPermitted(user1, EntityPermission.UPDATE_NAME))); + Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.UPDATE_NAME))); + f.grant(role2, "DELETE"); + Assert.assertFalse((f.create().isPermitted(user1, EntityPermission.DELETE))); + Assert.assertTrue((f.create().isPermitted(user2, EntityPermission.DELETE))); + f.deny(role2, 1); + f.deny(role1, 1); + Assert.assertFalse((f.create().isPermitted(user1, EntityPermission.DELETE))); + Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.DELETE))); + f.grant(role1, true, 1); + Assert.assertTrue((f.create().isPermitted(user1, EntityPermission.DELETE))); + Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.DELETE))); + f.deny(role2, true, 1); + Assert.assertTrue((f.create().isPermitted(user1, EntityPermission.DELETE))); + Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.DELETE))); + f.grant(role2, true, 1); + Assert.assertTrue((f.create().isPermitted(user1, EntityPermission.DELETE))); + Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.DELETE))); + f.deny(role1, true, 1); + Assert.assertFalse((f.create().isPermitted(user1, EntityPermission.DELETE))); + Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.DELETE))); + Assert.assertTrue((f.create().isPermitted(user1, EntityPermission.UPDATE_NAME))); + Assert.assertFalse((f.create().isPermitted(user2, EntityPermission.UPDATE_NAME))); + } + + // @Test + // public void niceFactoryStuff() { + // final EntityACLFactory f = new EntityACLFactory(); + // f.grant("user1", "*"); + // final EntityACL acl1 = f.create(); + // Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.EDIT_ACL)); + // Assert.assertTrue(acl1.isPermitted("user1", EntityPermission.DELETE)); + // Assert.assertTrue(acl1.isPermitted("user1", + // EntityPermission.RETRIEVE_ENTITY)); + // Assert.assertTrue(acl1.isPermitted("user1", + // EntityPermission.UPDATE_DATA_TYPE)); + // Assert.assertTrue(acl1.isPermitted("user1", + // EntityPermission.USE_AS_PROPERTY)); + // + // f.grant("?OWNER?", "DELETE", "EDIT:ACL", "RETRIEVE:*", "UPDATE:*", + // "USE:*"); + // f.grant("user2", "EDIT:ACL"); + // final EntityACL acl2 = f.create(); + // Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.EDIT_ACL)); + // Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.DELETE)); + // Assert.assertTrue(acl2.isPermitted("user2", + // EntityPermission.RETRIEVE_ENTITY)); + // Assert.assertTrue(acl2.isPermitted("user2", + // EntityPermission.UPDATE_DATA_TYPE)); + // Assert.assertTrue(acl2.isPermitted("user2", + // EntityPermission.USE_AS_PROPERTY)); // - // f.grant("?OWNER?", "DELETE", "EDIT:ACL", "RETRIEVE:*", "UPDATE:*", - // "USE:*"); - // f.grant("user2", "EDIT:ACL"); - // final EntityACL acl2 = f.create(); - // Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.EDIT_ACL)); - // Assert.assertTrue(acl2.isPermitted("user2", EntityPermission.DELETE)); - // Assert.assertTrue(acl2.isPermitted("user2", - // EntityPermission.RETRIEVE_ENTITY)); - // Assert.assertTrue(acl2.isPermitted("user2", - // EntityPermission.UPDATE_DATA_TYPE)); - // Assert.assertTrue(acl2.isPermitted("user2", - // EntityPermission.USE_AS_PROPERTY)); + // } // - // } - - // @Test - // public void testDeny() { - // EntityACLFactory f = new EntityACLFactory(); - // f.deny("test", "DELETE"); - // Assert.assertFalse(f.create().isPermitted("test", - // EntityPermission.DELETE)); + // @Test + // public void testDeny() { + // EntityACLFactory f = new EntityACLFactory(); + // f.deny("test", "DELETE"); + // Assert.assertFalse(f.create().isPermitted("test", + // EntityPermission.DELETE)); // - // System.out.println(Utils.element2String(f.create().toElement())); + // System.out.println(Utils.element2String(f.create().toElement())); // - // System.out.println(Utils.element2String(EntityACL.GLOBAL_PERMISSIONS.toElement())); + // System.out.println(Utils.element2String(EntityACL.GLOBAL_PERMISSIONS.toElement())); // - // f.grant("test", "USE:*"); - // Assert.assertFalse(f.create().isPermitted("test", - // EntityPermission.DELETE)); + // f.grant("test", "USE:*"); + // Assert.assertFalse(f.create().isPermitted("test", + // EntityPermission.DELETE)); // - // System.out.println(Utils.element2String(f.create().toElement())); + // System.out.println(Utils.element2String(f.create().toElement())); // - // f = new EntityACLFactory(); - // f.grant(EntityACL.OTHER_ROLE, "RETRIEVE:*"); - // f.deny(EntityACL.OTHER_ROLE, "DELETE"); - // final EntityACL a = f.create(); + // f = new EntityACLFactory(); + // f.grant(EntityACL.OTHER_ROLE, "RETRIEVE:*"); + // f.deny(EntityACL.OTHER_ROLE, "DELETE"); + // final EntityACL a = f.create(); // - // System.out.println(Utils.element2String(a.toElement())); + // System.out.println(Utils.element2String(a.toElement())); // - // System.out.println(Utils.element2String(EntityACL.deserialize(a.serialize()).toElement())); - // } + // System.out.println(Utils.element2String(EntityACL.deserialize(a.serialize()).toElement())); + // } } diff --git a/src/test/java/caosdb/server/query/TestCQL.java b/src/test/java/caosdb/server/query/TestCQL.java index 6006297dac0cd029ee451c0349f2b11e7ac2f86e..402cddd2be631291d772d494804cf21bc9dbc6b7 100644 --- a/src/test/java/caosdb/server/query/TestCQL.java +++ b/src/test/java/caosdb/server/query/TestCQL.java @@ -167,8 +167,11 @@ public class TestCQL { String ticket187 = "FIND Entity WITH pname=-1"; String ticket207 = "FIND RECORD WHICH REFERENCES 10594"; String query33 = "FIND ename WITH a date IN 2015"; + String query33a = "FIND ename WITH a date IN \"2015\""; String query34 = "FIND ename WITH a date NOT IN 2015"; + String query34a = "FIND ename WITH a date NOT IN \"2015\""; String query35 = "FIND ename WITH a date IN 2015-01-01"; + String query35a = "FIND ename WITH a date IN \"2015-01-01\""; String query36 = "FIND ename.pname LIKE \"wil*card\""; String query37 = "FIND ename.pname LIKE wil*card"; String query38 = "FIND ename WHICH HAS A pname LIKE \"wil*\""; @@ -4671,6 +4674,45 @@ public class TestCQL { assertEquals("2015", pov.getValue()); } + /** String query33a = "FIND ename WITH a date IN \"2015\""; */ + @Test + public void testQuery33a() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query33a)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + // 4 children: FIND, role, WHICHCLAUSE, EOF + assertEquals(4, sfq.getChildCount()); + assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("ename", sfq.getChild(1).getText()); + assertEquals("WITHadateIN\"2015\"", sfq.getChild(2).getText()); + assertEquals("ename", sfq.e.toString()); + assertNull(sfq.r); + assertEquals("POV", sfq.filter.getClass().getSimpleName()); + + final ParseTree whichclause = sfq.getChild(2); + // 2 children; WHICH, transaction + assertEquals(2, whichclause.getChildCount()); + assertEquals("WITHa", whichclause.getChild(0).getText()); + assertEquals("dateIN\"2015\"", whichclause.getChild(1).getText()); + + final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); + assertEquals(3, transactionFilter.getChildCount()); + assertEquals("date", transactionFilter.getChild(0).getText()); + assertEquals("IN", transactionFilter.getChild(1).getText()); + assertEquals("\"2015\"", transactionFilter.getChild(2).getText()); + + assertTrue(sfq.filter instanceof POV); + final POV pov = (POV) sfq.filter; + assertEquals("(", pov.getOperator()); + assertEquals("2015", pov.getValue()); + } + /** String query34 = "FIND ename WITH a date NOT IN 2015"; */ @Test public void testQuery34() { @@ -4711,6 +4753,46 @@ public class TestCQL { assertEquals("2015", pov.getValue()); } + /** String query34a = "FIND ename WITH a date NOT IN \"2015\""; */ + @Test + public void testQuery34a() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query34a)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + // 4 children: FIND, role, WHICHCLAUSE, EOF + assertEquals(4, sfq.getChildCount()); + assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("ename", sfq.getChild(1).getText()); + assertEquals("WITHadateNOTIN\"2015\"", sfq.getChild(2).getText()); + assertEquals("ename", sfq.e.toString()); + assertNull(sfq.r); + assertEquals("POV", sfq.filter.getClass().getSimpleName()); + + final ParseTree whichclause = sfq.getChild(2); + // 2 children: WHICH, POV + assertEquals(2, whichclause.getChildCount()); + assertEquals("WITHa", whichclause.getChild(0).getText()); + assertEquals("dateNOTIN\"2015\"", whichclause.getChild(1).getText()); + + final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); + assertEquals(4, transactionFilter.getChildCount()); + assertEquals("date", transactionFilter.getChild(0).getText()); + assertEquals("NOT", transactionFilter.getChild(1).getText()); + assertEquals("IN", transactionFilter.getChild(2).getText()); + assertEquals("\"2015\"", transactionFilter.getChild(3).getText()); + + assertTrue(sfq.filter instanceof POV); + final POV pov = (POV) sfq.filter; + assertEquals("!(", pov.getOperator()); + assertEquals("2015", pov.getValue()); + } + /** String query35 = "FIND ename WITH a date IN 2015-01-01"; */ @Test public void testQuery35() { @@ -4750,6 +4832,45 @@ public class TestCQL { assertEquals("2015-01-01", pov.getValue()); } + /** String query35a = "FIND ename WITH a date IN \"2015-01-01\""; */ + @Test + public void testQuery35a() { + CQLLexer lexer; + lexer = new CQLLexer(CharStreams.fromString(this.query35a)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + + final CQLParser parser = new CQLParser(tokens); + final CqContext sfq = parser.cq(); + + System.out.println(sfq.toStringTree(parser)); + + // 4 children: FIND, role, WHICHCLAUSE, EOF + assertEquals(4, sfq.getChildCount()); + assertEquals("FIND", sfq.getChild(0).getText()); + assertEquals("ename", sfq.getChild(1).getText()); + assertEquals("WITHadateIN\"2015-01-01\"", sfq.getChild(2).getText()); + assertEquals("ename", sfq.e.toString()); + assertNull(sfq.r); + assertEquals("POV", sfq.filter.getClass().getSimpleName()); + + final ParseTree whichclause = sfq.getChild(2); + // 2 children: WHICH, POV + assertEquals(2, whichclause.getChildCount()); + assertEquals("WITHa", whichclause.getChild(0).getText()); + assertEquals("dateIN\"2015-01-01\"", whichclause.getChild(1).getText()); + + final ParseTree transactionFilter = whichclause.getChild(1).getChild(0); + assertEquals(3, transactionFilter.getChildCount()); + assertEquals("date", transactionFilter.getChild(0).getText()); + assertEquals("IN", transactionFilter.getChild(1).getText()); + assertEquals("\"2015-01-01\"", transactionFilter.getChild(2).getText()); + + assertTrue(sfq.filter instanceof POV); + final POV pov = (POV) sfq.filter; + assertEquals("(", pov.getOperator()); + assertEquals("2015-01-01", pov.getValue()); + } + /** String query36 = "FIND ename.pname LIKE \"wil*card\""; */ @Test public void testQuery36() { diff --git a/src/test/java/caosdb/server/resource/TestAbstractCaosDBServerResource.java b/src/test/java/caosdb/server/resource/TestAbstractCaosDBServerResource.java index 81baabd4797842f1cbbc61f990d6ea580553de85..59f0a1d5812b1127ea0ff4514520bc30039af5cd 100644 --- a/src/test/java/caosdb/server/resource/TestAbstractCaosDBServerResource.java +++ b/src/test/java/caosdb/server/resource/TestAbstractCaosDBServerResource.java @@ -8,12 +8,21 @@ import caosdb.server.CaosDBServer; import caosdb.server.ServerProperties; import caosdb.server.accessControl.AnonymousAuthenticationToken; import caosdb.server.accessControl.AnonymousRealm; +import caosdb.server.accessControl.Role; +import caosdb.server.database.BackendTransaction; +import caosdb.server.database.access.Access; import caosdb.server.database.backend.implementation.MySQL.ConnectionException; +import caosdb.server.database.backend.interfaces.RetrievePermissionRulesImpl; +import caosdb.server.database.backend.interfaces.RetrieveRoleImpl; +import caosdb.server.database.exceptions.TransactionException; +import caosdb.server.database.misc.TransactionBenchmark; +import caosdb.server.permissions.PermissionRule; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.security.NoSuchAlgorithmException; import java.sql.SQLException; +import java.util.HashSet; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.DelegatingSubject; @@ -29,9 +38,51 @@ public class TestAbstractCaosDBServerResource { @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + /** a no-op mock-up which resolved all rules to an empty set of permissions. */ + public static class RetrievePermissionRulesMockup implements RetrievePermissionRulesImpl { + + public RetrievePermissionRulesMockup(Access a) {} + + @Override + public void setTransactionBenchmark(TransactionBenchmark b) {} + + @Override + public TransactionBenchmark getBenchmark() { + return null; + } + + @Override + public HashSet<PermissionRule> retrievePermissionRule(String role) throws TransactionException { + return new HashSet<>(); + } + } + + /** a no-op 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; + } + } + @BeforeClass public static void initServerProperties() throws IOException { CaosDBServer.initServerProperties(); + + BackendTransaction.setImpl( + RetrievePermissionRulesImpl.class, RetrievePermissionRulesMockup.class); + BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRoleMockup.class); } @Test diff --git a/src/test/java/caosdb/server/resource/TestScriptingResource.java b/src/test/java/caosdb/server/resource/TestScriptingResource.java index 2edfb82b53b56324996293a469fadf807264d86c..3280ae3af5a62174006b504408d7c3bde8a2632e 100644 --- a/src/test/java/caosdb/server/resource/TestScriptingResource.java +++ b/src/test/java/caosdb/server/resource/TestScriptingResource.java @@ -23,14 +23,13 @@ package caosdb.server.resource; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import caosdb.server.CaosDBServer; -import caosdb.server.accessControl.AuthenticationUtils; +import caosdb.server.accessControl.AnonymousAuthenticationToken; import caosdb.server.accessControl.CredentialsValidator; import caosdb.server.accessControl.Principal; import caosdb.server.accessControl.Role; -import caosdb.server.accessControl.SessionToken; +import caosdb.server.accessControl.UserSources; import caosdb.server.database.BackendTransaction; import caosdb.server.database.access.Access; import caosdb.server.database.backend.interfaces.RetrievePasswordValidatorImpl; @@ -42,16 +41,16 @@ import caosdb.server.database.misc.TransactionBenchmark; import caosdb.server.database.proto.ProtoUser; import caosdb.server.entity.Message; import caosdb.server.permissions.PermissionRule; +import caosdb.server.scripting.ScriptingPermissions; +import caosdb.server.scripting.ServerSideScriptingCaller; import java.io.IOException; import java.util.Date; import java.util.HashSet; import java.util.List; import org.apache.shiro.SecurityUtils; -import org.apache.shiro.config.Ini; -import org.apache.shiro.config.IniSecurityManagerFactory; -import org.apache.shiro.mgt.SecurityManager; +import org.apache.shiro.authz.permission.WildcardPermission; import org.apache.shiro.subject.Subject; -import org.apache.shiro.util.Factory; +import org.jdom2.Element; import org.junit.BeforeClass; import org.junit.Test; import org.restlet.Request; @@ -93,7 +92,13 @@ public class TestScriptingResource { @Override public HashSet<PermissionRule> retrievePermissionRule(String role) throws TransactionException { - return new HashSet<>(); + HashSet<PermissionRule> result = new HashSet<>(); + result.add( + new PermissionRule( + true, + false, + new WildcardPermission(ScriptingPermissions.PERMISSION_EXECUTION("anonymous_ok")))); + return result; } @Override @@ -149,19 +154,16 @@ public class TestScriptingResource { @BeforeClass public static void setupShiro() throws IOException { + CaosDBServer.initServerProperties(); + CaosDBServer.initShiro(); + BackendTransaction.setImpl(RetrieveRoleImpl.class, RetrieveRole.class); BackendTransaction.setImpl(RetrievePermissionRulesImpl.class, RetrievePermissionRules.class); BackendTransaction.setImpl(RetrieveUserImpl.class, RetrieveUser.class); BackendTransaction.setImpl( RetrievePasswordValidatorImpl.class, RetrievePasswordValidator.class); - CaosDBServer.initServerProperties(); - Ini config = CaosDBServer.getShiroConfig(); - final Factory<SecurityManager> factory = new IniSecurityManagerFactory(config); - - final SecurityManager securityManager = factory.getInstance(); - - SecurityUtils.setSecurityManager(securityManager); + UserSources.getDefaultRealm(); } ScriptingResource resource = @@ -173,23 +175,25 @@ public class TestScriptingResource { java.util.List<caosdb.server.entity.FileProperties> files, Object authToken) throws Message { + if (invokation.get(0).equals("anonymous_ok")) { + return 0; + } return -1; }; @Override - public Object generateAuthToken() { + public Element generateRootElement(ServerSideScriptingCaller caller) { + return new Element("OK"); + }; + + @Override + public Object generateAuthToken(String purpose) { return ""; } }; @Test public void testUnsupportedMediaType() { - Subject user = SecurityUtils.getSubject(); - if (user.isAuthenticated()) { - user.logout(); - } - SessionToken t = SessionToken.generate(new Principal("CaosDB", "user"), null); - user.login(t); Representation entity = new StringRepresentation("asdf"); entity.setMediaType(MediaType.TEXT_ALL); Request request = new Request(Method.POST, "../test", entity); @@ -201,12 +205,11 @@ public class TestScriptingResource { } @Test - public void testAnonymous() { + public void testAnonymousWithOutPermission() { Subject user = SecurityUtils.getSubject(); - user.login(AuthenticationUtils.ANONYMOUS_USER); - assertTrue(resource.isAnonymous()); - Representation entity = new StringRepresentation("asdf"); - entity.setMediaType(MediaType.TEXT_ALL); + user.login(AnonymousAuthenticationToken.getInstance()); + Form form = new Form("call=anonymous_no_permission"); + Representation entity = form.getWebRepresentation(); Request request = new Request(Method.POST, "../test", entity); request.setRootRef(new Reference("bla")); request.getAttributes().put("SRID", "asdf1234"); @@ -218,6 +221,23 @@ public class TestScriptingResource { assertEquals(Status.CLIENT_ERROR_FORBIDDEN, resource.getResponse().getStatus()); } + @Test + public void testAnonymousWithPermission() { + Subject user = SecurityUtils.getSubject(); + user.login(AnonymousAuthenticationToken.getInstance()); + Form form = new Form("call=anonymous_ok"); + Representation entity = form.getWebRepresentation(); + Request request = new Request(Method.POST, "../test", entity); + request.setRootRef(new Reference("bla")); + request.getAttributes().put("SRID", "asdf1234"); + request.setDate(new Date()); + request.setHostRef("bla"); + resource.init(null, request, new Response(null)); + + resource.handle(); + assertEquals(Status.SUCCESS_OK, resource.getResponse().getStatus()); + } + @Test public void testForm2invocation() throws Message { Form form = @@ -235,8 +255,9 @@ public class TestScriptingResource { @Test public void testHandleForm() throws Message, IOException { - Form form = - new Form("-Ooption=OPTION&call=CALL&-Ooption2=OPTION2&-p=POS1&-p4=POS3&-p2=POS2&IGNORED"); - assertEquals(-1, resource.handleForm(form)); + Subject user = SecurityUtils.getSubject(); + user.login(AnonymousAuthenticationToken.getInstance()); + Form form = new Form("call=anonymous_ok"); + assertEquals(0, resource.handleForm(form)); } } diff --git a/src/test/java/caosdb/server/utils/WebinterfaceUtilsTest.java b/src/test/java/caosdb/server/utils/WebinterfaceUtilsTest.java index 4ff5d7185d43704e4bc97ef435a5c853e58e3eb8..075c8d998acdfc7f897f9429de468034a4dc022e 100644 --- a/src/test/java/caosdb/server/utils/WebinterfaceUtilsTest.java +++ b/src/test/java/caosdb/server/utils/WebinterfaceUtilsTest.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import caosdb.server.CaosDBServer; +import caosdb.server.ServerProperties; import java.io.IOException; import org.junit.BeforeClass; import org.junit.Rule; @@ -26,7 +27,13 @@ public class WebinterfaceUtilsTest { WebinterfaceUtils utils = new WebinterfaceUtils(new Reference("https://host:2345/some_path")); String buildNumber = utils.getBuildNumber(); String ref = utils.getWebinterfaceURI("sub"); - assertEquals("https://host:2345/webinterface/" + buildNumber + "/sub", ref); + String contextRoot = CaosDBServer.getServerProperty(ServerProperties.KEY_CONTEXT_ROOT); + contextRoot = + contextRoot != null && contextRoot.length() > 0 + ? "/" + contextRoot.replaceFirst("^/", "").replaceFirst("/$", "") + : ""; + + assertEquals("https://host:2345" + contextRoot + "/webinterface/" + buildNumber + "/sub", ref); } @Test