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 27382acd3b6ac4e94224186a6e89664470c20cad..22ce0c27bc196c81f1aa10f132b2d62b50f1cc63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- 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. @@ -30,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated -- +- CaosDBTerminal ### Removed 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/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/server.conf b/conf/core/server.conf index 358f6b5c14106b87d7a676e2262474772ae98512..d30d82b681a185a820783c6c2d8d81a62981eb64 100644 --- a/conf/core/server.conf +++ b/conf/core/server.conf @@ -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 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/pom.xml b/pom.xml index c8f79b3dcc43759cc941244283d13b248d811bb8..0594c0548331f0e834fc0bc10a61fec69ffa9a69 100644 --- a/pom.xml +++ b/pom.xml @@ -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> 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/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..45da4eb0e655f011e06deede49eec2035745fac1 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; @@ -80,11 +81,15 @@ 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 { 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/jobs/extension/AWIBoxLoan.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java deleted file mode 100644 index 30dd77e0c0fc7f97f85db7d45c00cf762badc695..0000000000000000000000000000000000000000 --- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java +++ /dev/null @@ -1,562 +0,0 @@ -package caosdb.server.jobs.extension; - -import static caosdb.server.permissions.Role.ANONYMOUS_ROLE; - -import caosdb.server.accessControl.UserSources; -import caosdb.server.database.exceptions.EntityDoesNotExistException; -import caosdb.server.datatype.SingleValue; -import caosdb.server.entity.EntityInterface; -import caosdb.server.entity.Message; -import caosdb.server.entity.Message.MessageType; -import caosdb.server.entity.wrapper.Property; -import caosdb.server.jobs.JobAnnotation; -import caosdb.server.jobs.core.CheckPropValid; -import caosdb.server.permissions.EntityACL; -import caosdb.server.permissions.EntityACLFactory; -import caosdb.server.permissions.EntityPermission; -import caosdb.server.query.Query; -import caosdb.server.transaction.Delete; -import caosdb.server.transaction.Insert; -import caosdb.server.transaction.Update; -import caosdb.server.utils.EntityStatus; -import caosdb.server.utils.ServerMessages; -import caosdb.server.utils.Utils; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@JobAnnotation(transaction = caosdb.server.transaction.WriteTransaction.class, loadAlways = true) -public class AWIBoxLoan extends AWIBoxLoanModel { - - public Logger logger = LoggerFactory.getLogger(getClass()); - private static final Message UNIQUE_USER = - new Message("The user must have a unique combination of first name and last name!"); - private static final Message BOX_HAS_LOAN = - new Message( - "This box cannot be be requested right now because it appears to have a Loan property attached to it. This usually means, that the box is already requested or borrowed by someone."); - - @Override - protected void run() { - try { - if (isAnonymous() - && (getTransaction() instanceof Delete - || isAcceptBorrowUpdateLoan() - || isConfirmLoanUpdateLoan() - || isRejectReturnUpdateLoan() - || isAcceptReturnUpdateLoan() - || isManualReturnUpdateBox() - || isManualReturnUpdateLoan() - || !(isRequestLoanSetUser() - || isRequestLoanInsertLoan() - || isRequestLoanUpdateBox() - || isRequestReturnSetUser() - || isRequestReturnUpdateLoan()))) { - addError(ServerMessages.AUTHORIZATION_ERROR); - getContainer() - .addMessage( - new Message(MessageType.Info, 0, "Anonymous users have restricted permissions.")); - return; - } - } catch (EntityDoesNotExistException exc) { - addError(ServerMessages.AUTHORIZATION_ERROR); - getContainer() - .addMessage( - new Message(MessageType.Info, 0, "Anonymous users have restricted permissions.")); - return; - } - - try { - if (!(getTransaction() instanceof Delete - || isRequestLoanSetUser() - || isRequestLoanInsertLoan() - || isManualReturnUpdateLoan() - || isManualReturnUpdateBox() - || isRequestLoanUpdateBox() - || isAcceptBorrowUpdateLoan() - || isConfirmLoanUpdateLoan() - || isRequestReturnSetUser() - || isRequestReturnUpdateLoan() - || isRejectReturnUpdateLoan())) { - isAcceptReturnUpdateLoan(); - } - } catch (EntityDoesNotExistException exc) { - // ignore - } - - // special ACL for boxes, loans and persons - try { - if (getTransaction() instanceof Insert) { - for (EntityInterface e : getContainer()) { - if (isBoxRecord(e)) { - e.setEntityACL(EntityACL.combine(e.getEntityACL(), getBoxACL())); - } else if (isLoanRecord(e)) { - e.setEntityACL(EntityACL.combine(e.getEntityACL(), getLoanACL())); - } else if (isPersonRecord(e)) { - e.setEntityACL(EntityACL.combine(e.getEntityACL(), getPersonACL())); - } - } - } - } catch (EntityDoesNotExistException e) { - } - } - - boolean isManualReturnUpdateLoan() { - for (EntityInterface e : getContainer()) { - if (!isLoanRecord(e) || !hasManualReturnLoanProperties(e)) { - logger.trace("isManualReturnUpdateLoan: false"); - return false; - } - } - for (EntityInterface e : getContainer()) { - removeAnonymousPermissions(e); - } - logger.trace("isManualReturnUpdateLoan: true"); - return true; - } - - boolean hasManualReturnLoanProperties(EntityInterface e) { - for (Property p : e.getProperties()) { - if (isReturnedProperty(p)) { - return true; - } - } - return false; - } - - boolean isReturnedProperty(Property p) { - return Objects.equals(p.getId(), getReturnedId()); - } - - void removeAnonymousPermissions(EntityInterface e) { - EntityACLFactory f = new EntityACLFactory(); - f.deny(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_ADD_PROPERTY); - f.deny(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_REMOVE_PROPERTY); - e.setEntityACL(EntityACL.combine(e.getEntityACL(), f.create())); - } - - boolean isManualReturnUpdateBox() { - for (EntityInterface e : getContainer()) { - if (!isBoxRecord(e) || !hasManualReturnBoxProperties(e)) { - logger.trace("isManualReturnUpdateBox: false"); - return false; - } - } - logger.trace("isManualReturnUpdateBox: true"); - return true; - } - - boolean hasManualReturnBoxProperties(EntityInterface e) { - return validBoxHasLoanProperty(e) && boxLoanHasReturnedProperty(e); - } - - boolean boxLoanHasReturnedProperty(EntityInterface e) { - try { - EntityInterface validBox = retrieveValidEntity(e.getId()); - - for (Property p : validBox.getProperties()) { - if (isLoanProperty(p)) { - return validLoanHasReturnedProperty(p.getId()); - } - } - } catch (EntityDoesNotExistException exc) { - return false; - } - return false; - } - - boolean validLoanHasReturnedProperty(Integer id) { - try { - EntityInterface validLoan = retrieveValidEntity(id); - for (Property p : validLoan.getProperties()) { - if (isReturnedProperty(p)) { - return true; - } - } - } catch (EntityDoesNotExistException exc) { - return false; - } - return false; - } - - boolean isLoanProperty(Property p) { - return Objects.equals(p.getId(), getLoanId()); - } - - private boolean isRejectReturnUpdateLoan() { - for (EntityInterface e : getContainer()) { - if (!isLoanRecord(e) || !hasRejectReturnProperties(e)) { - logger.trace("isRejectReturnUpdateLoan: false"); - return false; - } - } - for (EntityInterface e : getContainer()) { - removeDestinationProperty(e); - } - logger.trace("isRejectReturnUpdateLoan: true"); - return true; - } - - void removeDestinationProperty(EntityInterface e) { - Iterator<Property> iterator = e.getProperties().iterator(); - while (iterator.hasNext()) { - Property p = iterator.next(); - if (isDestinationProperty(p)) { - iterator.remove(); - } - } - } - - boolean hasRejectReturnProperties(EntityInterface e) { - boolean found = false; - for (Property p : e.getProperties()) { - if (isDestinationProperty(p)) { - found = true; - } else if (isReturnAcceptedProperty(p) || isReturnRequestProperty(p)) { - logger.trace("hasRejectReturnProperties: false"); - return false; - } - } - logger.trace("hasRejectReturnProperties found: {}", found); - return found; - } - - boolean isDestinationProperty(Property p) { - return Objects.equals(p.getId(), getDestinationId()); - } - - boolean isAcceptReturnUpdateLoan() { - for (EntityInterface e : getContainer()) { - if (!isLoanRecord(e) || !hasAcceptReturnProperties(e)) { - logger.trace("isAcceptReturnUpdateLoan: false"); - return false; - } - } - logger.trace("isAcceptReturnUpdateLoan: true"); - return true; - } - - boolean hasAcceptReturnProperties(EntityInterface e) { - boolean found = false; - for (Property p : e.getProperties()) { - if (isReturnAcceptedProperty(p)) { - found = true; - } - } - return found; - } - - boolean isConfirmLoanUpdateLoan() { - for (EntityInterface e : getContainer()) { - if (!isLoanRecord(e) || !hasConfirmLoanProperties(e)) { - logger.trace("isConfirmLoanUpdateLoan: false"); - return false; - } - } - // switch from destination to location - for (EntityInterface e : getContainer()) { - switchDestinationToLocation(e); - } - logger.trace("isConfirmLoanUpdateLoan: true"); - return true; - } - - void switchDestinationToLocation(EntityInterface e) { - Iterator<Property> iterator = e.getProperties().iterator(); - EntityInterface p = retrieveValidEntity(getLocationId()); - while (iterator.hasNext()) { - Property next = iterator.next(); - if (isDestinationProperty(next)) { - iterator.remove(); - p.setValue(next.getValue()); - e.addProperty(new Property(p)); - break; - } - } - } - - boolean hasConfirmLoanProperties(EntityInterface e) { - boolean found = false; - for (Property p : e.getProperties()) { - if (isLentProperty(p)) { - found = true; - } else if (isReturnAcceptedProperty(p) || isDestinationProperty(p)) { - logger.trace("hasConfirmLoanProperties: false"); - return false; - } - } - logger.trace("hasConfirmLoanProperties found: {}", found); - return found; - } - - boolean isAcceptBorrowUpdateLoan() { - for (EntityInterface e : getContainer()) { - if (!isLoanRecord(e) || !hasLoanAcceptProperties(e)) { - logger.trace("isAcceptBorrowUpdateLoan: false"); - return false; - } - } - logger.trace("isAcceptBorrowUpdateLoan: true"); - return true; - } - - boolean hasLoanAcceptProperties(EntityInterface e) { - boolean found = false; - for (Property p : e.getProperties()) { - if (isLoanAcceptProperty(p)) { - found = true; - } else if (isLentProperty(p) || isReturnAcceptedProperty(p)) { - logger.trace("hasLoanAcceptProperties: false"); - return false; - } - } - logger.trace("hasLoanAcceptProperties found: {}", found); - return found; - } - - boolean isReturnAcceptedProperty(Property p) { - return Objects.equals(p.getId(), getReturnAcceptedId()); - } - - boolean isLentProperty(Property p) { - return Objects.equals(p.getId(), getLentId()); - } - - boolean isLoanAcceptProperty(Property p) { - return Objects.equals(p.getId(), getLoanAcceptedId()); - } - - EntityACL getPersonACL() { - // same as loan acl - property updates are allowed for anonymous. - return getLoanACL(); - } - - EntityACL getLoanACL() { - EntityACLFactory f = new EntityACLFactory(); - f.grant(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_ADD_PROPERTY); - f.grant(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_REMOVE_PROPERTY); - return f.create(); - } - - EntityACL getBoxACL() { - EntityACLFactory f = new EntityACLFactory(); - f.grant(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_ADD_PROPERTY); - f.grant(ANONYMOUS_ROLE, false, EntityPermission.UPDATE_REMOVE_PROPERTY); - return f.create(); - } - - boolean isAnonymous() { - boolean ret = getUser().hasRole(UserSources.ANONYMOUS_ROLE); - logger.trace("is Anonymous: {}", ret); - return ret; - } - - boolean isRequestReturnUpdateLoan() { - // is UPDATE transaction - if (getTransaction() instanceof Update) { - // Container has only loan elements with special properties - for (EntityInterface e : getContainer()) { - if (!isLoanRecord(e) || !hasOnlyAllowedLoanProperties4RequestReturn(e)) { - logger.trace("isRequestReturnUpdateLoan: false"); - return false; - } - setReturnRequestedDate(e); - } - appendJob(AWIBoxLoanRequestReturnCuratorEmail.class); - logger.trace("isRequestReturnUpdateLoan: true"); - return true; - } - logger.trace("isRequestReturnUpdateLoan: false"); - return false; - } - - boolean isRequestReturnSetUser() { - // same as request_loan.set_user - logger.trace("isRequestReturnSetUser: ->"); - return isRequestLoanSetUser(); - } - - boolean isRequestLoanUpdateBox() { - // is UPDATE transaction - if (getTransaction() instanceof Update) { - // Container has only box elements - for (EntityInterface e : getContainer()) { - if (validBoxHasLoanProperty(e)) { - e.addError(BOX_HAS_LOAN); - return false; - } - if (!isBoxRecord(e) || !hasOnlyAllowedBoxProperties4RequestLoan(e)) { - return false; - } - // TODO this breaks the box loan functionality if any other prior changes have been made to - // the box - // appendJob(e, CheckNoAdditionalPropertiesPresent.class); - } - return true; - } - return false; - } - - boolean validBoxHasLoanProperty(EntityInterface e) { - try { - EntityInterface validBox = retrieveValidEntity(e.getId()); - for (Property p : validBox.getProperties()) { - if (isLoanProperty(p)) { - return true; - } - } - } catch (EntityDoesNotExistException exc) { - return false; - } - return false; - } - - /** Has only one new property -> Loan. */ - boolean hasOnlyAllowedBoxProperties4RequestLoan(EntityInterface e) { - int count = 0; - for (Property p : e.getProperties()) { - if (p.getEntityStatus() == EntityStatus.QUALIFIED && Objects.equals(p.getId(), getLoanId())) { - count++; - } - } - - // Box has only one update, a loan property - return count == 1; - } - - boolean isRequestLoanInsertLoan() { - // is INSERT transaction - if (getTransaction() instanceof Insert) { - // Container has only loan elements - for (EntityInterface e : getContainer()) { - if (!isLoanRecord(e)) { - return false; - } - } - for (EntityInterface e : getContainer()) { - if (isAnonymous()) { - setCuratorAsOwner(e); - } - setLoanRequestDate(e); - // TODO this check breaks the box loan functionality if any other changes have been made to - // the box entity - // appendJob(e, CheckNoAdditionalPropertiesPresent.class); - // appendJob(e, CheckNoOverridesPresent.class); - } - appendJob(AWIBoxLoanRequestLoanCuratorEmail.class); - return true; - } - return false; - } - - void setCuratorAsOwner(EntityInterface e) { - e.setEntityACL(EntityACL.getOwnerACLFor(caosdb.server.permissions.Role.create("curator"))); - } - - void setReturnRequestedDate(EntityInterface e) { - // TODO setDateProperty(e, getReturnRequestedId()); - } - - private void setDateProperty(EntityInterface e, Integer propertyId) { - // TODO - EntityInterface p = retrieveValidEntity(propertyId); - p.setValue(getTransaction().getTimestamp()); - e.addProperty(new Property(p)); - } - - void setLoanRequestDate(EntityInterface e) { - // TODO setDateProperty(e, getLoanRequestedId()); - } - - boolean isRequestLoanSetUser() { - // is INSERT/UPDATE transaction - // Container has only one element, user - if ((getTransaction() instanceof Update || getTransaction() instanceof Insert) - && getContainer().size() == 1 - && isPersonRecord(getContainer().get(0)) - && checkUniqueName(getContainer().get(0)) - && checkEmail(getContainer().get(0))) { - // TODO this check breaks the box loan functionality if any other changes have been made to - // the box entity - // appendJob(getContainer().get(0), CheckNoAdditionalPropertiesPresent.class); - // appendJob(getContainer().get(0), CheckNoOverridesPresent.class); - logger.trace("isRequestReturnSetUser: true"); - return true; - } - logger.trace("isRequestReturnSetUser: false"); - return false; - } - - boolean checkEmail(EntityInterface entity) { - runJobFromSchedule(entity, CheckPropValid.class); - for (Property p : entity.getProperties()) { - if (Objects.equals(p.getId(), getEmailID()) && p.getValue() instanceof SingleValue) { - if (!Utils.isRFC822Compliant(((SingleValue) p.getValue()).toDatabaseString())) { - p.addError(ServerMessages.EMAIL_NOT_WELL_FORMED); - } else { - p.setName("email"); // TODO fix webinterface to use lower-case email - p.setNameOverride(false); - } - return true; - } - } - return false; - } - - private boolean checkUniqueName(EntityInterface entity) { - String firstName = null; - String lastName = null; - Query q = - new Query( - "FIND " - + getPersonID().toString() - + " WITH " - + getFirstNameId().toString() - + "='" - + firstName - + "' AND " - + getLastNameId().toString() - + "='" - + lastName - + "'", - getUser()) - .execute(getTransaction().getAccess()); - List<Integer> resultSet = q.getResultSet(); - if (resultSet.isEmpty() - || (resultSet.size() == 1 && Objects.equals(resultSet.get(0), entity.getId()))) { - return true; - } - entity.addError(UNIQUE_USER); - return false; - } - - /** - * Has only 5/6 new/updated properties: content, returnRequested, destination, Borrower, comment - * (optional), location - * - * @throws Message - */ - boolean hasOnlyAllowedLoanProperties4RequestReturn(EntityInterface e) { - runJobFromSchedule(e, CheckPropValid.class); - // appendJob(e, CheckNoOverridesPresent.class); - - boolean foundReturnRequested = false; - for (Property p : e.getProperties()) { - if (p.getEntityStatus() == EntityStatus.QUALIFIED) { // this means update - if (isReturnRequestProperty(p)) { - foundReturnRequested = true; - } else if (isReturnAcceptedProperty(p) || isReturnedProperty(p)) { - logger.trace("hasOnlyAllowedLoanProperties4RequestReturn: false"); - return false; // this is not a returnRequest, return has already been accepted - } - } - } - logger.trace("hasOnlyAllowedLoanProperties4RequestReturn found: {}", foundReturnRequested); - return foundReturnRequested; - } - - boolean isReturnRequestProperty(Property p) { - return Objects.equals(p.getId(), getReturnRequestedId()); - } -} diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanCuratorEmail.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanCuratorEmail.java deleted file mode 100644 index 2bf51e76e7357839ee0c52d28caa18017691849f..0000000000000000000000000000000000000000 --- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanCuratorEmail.java +++ /dev/null @@ -1,92 +0,0 @@ -package caosdb.server.jobs.extension; - -import caosdb.datetime.DateTimeInterface; -import caosdb.server.CaosDBServer; -import caosdb.server.ServerProperties; -import caosdb.server.datatype.ReferenceValue; -import caosdb.server.datatype.SingleValue; -import caosdb.server.datatype.Value; -import caosdb.server.entity.EntityInterface; -import caosdb.server.entity.wrapper.Property; -import caosdb.server.utils.mail.Mail; -import java.util.TimeZone; - -public abstract class AWIBoxLoanCuratorEmail extends AWIBoxLoanModel { - - public static final String KEY_EXT_AWI_CURATOR_EMAIL = "EXT_AWI_BOX_CURATOR_EMAIL"; - protected static final String FROM_EMAIL = - CaosDBServer.getServerProperty(ServerProperties.KEY_NO_REPLY_EMAIL); - protected static final String FROM_NAME = - CaosDBServer.getServerProperty(ServerProperties.KEY_NO_REPLY_NAME); - protected static final String CURATOR_EMAIL = - CaosDBServer.getServerProperty(KEY_EXT_AWI_CURATOR_EMAIL); - - protected String loanToString(EntityInterface e) { - StringBuilder s = new StringBuilder(); - for (Property p : e.getProperties()) { - s.append("\n"); - s.append(p.getName()); - s.append(": "); - if (p.getValue() instanceof ReferenceValue) { - Integer id = ((ReferenceValue) p.getValue()).getId(); - if (isBorrowerProperty(p)) { - s.append(borrowerToString(id)); - } else if (isBoxProperty(p)) { - s.append(boxToString(id)); - } else { - s.append(valueToString(p.getValue())); - } - } else { - s.append(valueToString(p.getValue())); - } - } - return s.toString(); - } - - private String valueToString(Value value) { - if (value == null) { - return ""; - } - if (value instanceof DateTimeInterface) { - return ((DateTimeInterface) value).toDateTimeString(TimeZone.getDefault()); - } - if (value instanceof SingleValue) { - return ((SingleValue) value).toDatabaseString(); - } else { - return value.toString(); - } - } - - private String boxToString(Integer id) { - StringBuilder s = new StringBuilder(); - EntityInterface box = retrieveValidEntity(id); - for (Property sp : box.getProperties()) { - if (isNumberProperty(sp) || isContentProperty(sp) || isLocationProperty(sp)) { - s.append("\n "); - s.append(sp.getName()); - s.append(": "); - s.append(valueToString(sp.getValue())); - } - } - return s.toString(); - } - - private String borrowerToString(Integer id) { - StringBuilder s = new StringBuilder(); - EntityInterface borrower = retrieveValidEntity(id); - for (Property sp : borrower.getProperties()) { - s.append("\n "); - s.append(sp.getName()); - s.append(": "); - s.append(valueToString(sp.getValue())); - } - return s.toString(); - } - - protected void sendCuratorEmail(String body, String subject) { - for (String addr : CURATOR_EMAIL.split(" ")) { - Mail m = new Mail(FROM_NAME, FROM_EMAIL, null, addr, subject, body); - m.send(); - } - } -} diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanModel.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanModel.java deleted file mode 100644 index 92a8a240363cf8659e0d3526dec1c5fcac8ddd11..0000000000000000000000000000000000000000 --- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanModel.java +++ /dev/null @@ -1,154 +0,0 @@ -package caosdb.server.jobs.extension; - -import caosdb.server.CaosDBServer; -import caosdb.server.database.exceptions.EntityDoesNotExistException; -import caosdb.server.entity.EntityInterface; -import caosdb.server.entity.Role; -import caosdb.server.entity.wrapper.Property; -import caosdb.server.jobs.ContainerJob; -import caosdb.server.utils.Utils; -import java.util.Objects; - -public abstract class AWIBoxLoanModel extends ContainerJob { - - /** Is Record and has single user parent. */ - boolean isPersonRecord(EntityInterface entity) { - try { - return entity.getParents().size() == 1 - && Objects.equals( - retrieveValidIDByName(entity.getParents().get(0).getName()), getPersonID()); - } catch (EntityDoesNotExistException exc) { - return false; - } - } - - /** Is Record an has single box parent. */ - boolean isBoxRecord(EntityInterface e) { - return e.getRole() == Role.Record - && e.getParents().size() == 1 - && Objects.equals(e.getParents().get(0).getId(), getBoxId()); - } - - /** Is Record and has single loan parent */ - boolean isLoanRecord(EntityInterface e) { - try { - return e.getRole() == Role.Record - && e.getParents().size() == 1 - && Objects.equals(retrieveValidIDByName(e.getParents().get(0).getName()), getLoanId()); - } catch (EntityDoesNotExistException exc) { - return false; - } - } - - boolean isBoxProperty(Property p) { - return Objects.equals(p.getId(), getBoxId()); - } - - boolean isBorrowerProperty(Property p) { - return Objects.equals(p.getId(), getBorrowerId()); - } - - boolean isLocationProperty(Property p) { - return Objects.equals(p.getId(), getLocationId()); - } - - boolean isContentProperty(Property p) { - return Objects.equals(p.getId(), getContentId()); - } - - boolean isNumberProperty(Property p) { - return Objects.equals(p.getId(), getNumberId()); - } - - Integer getIdOf(String string) { - String id = CaosDBServer.getServerProperty("EXT_AWI_" + string.toUpperCase() + "_ID"); - if (id != null && Utils.isNonNullInteger(id)) { - return Integer.parseInt(id); - } - String name = CaosDBServer.getServerProperty("EXT_AWI_" + string.toUpperCase() + "_NAME"); - if (name == null || name.isEmpty()) { - name = string; - } - return retrieveValidIDByName(name); - } - - Integer getBorrowerId() { - return getIdOf("Borrower"); - } - - Integer getCommentId() { - return getIdOf("comment"); - } - - Integer getLocationId() { - return getIdOf("Location"); - } - - Integer getDestinationId() { - return getIdOf("destination"); - } - - Integer getContentId() { - return getIdOf("Content"); - } - - Integer getBoxId() { - return getIdOf("Box"); - } - - Integer getLoanId() { - return getIdOf("Loan"); - } - - Integer getPersonID() { - return getIdOf("Person"); - } - - Integer getEmailID() { - return getIdOf("email"); - } - - Integer getLoanAcceptedId() { - return getIdOf("loanAccepted"); - } - - Integer getReturnAcceptedId() { - return getIdOf("returnAccepted"); - } - - Integer getLentId() { - return getIdOf("lent"); - } - - Integer getLoanRequestedId() { - return getIdOf("loanRequested"); - } - - Integer getExhaustContentsId() { - return getIdOf("exhaustContents"); - } - - Integer getExpectedReturnId() { - return getIdOf("expectedReturn"); - } - - Integer getReturnRequestedId() { - return getIdOf("returnRequested"); - } - - Integer getLastNameId() { - return getIdOf("lastName"); - } - - Integer getFirstNameId() { - return getIdOf("firstName"); - } - - Integer getNumberId() { - return getIdOf("Number"); - } - - Integer getReturnedId() { - return getIdOf("returned"); - } -} diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestLoanCuratorEmail.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestLoanCuratorEmail.java deleted file mode 100644 index ff490e99a72e93b03b04a0134c81790b1a2a1bec..0000000000000000000000000000000000000000 --- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestLoanCuratorEmail.java +++ /dev/null @@ -1,24 +0,0 @@ -package caosdb.server.jobs.extension; - -import caosdb.server.entity.EntityInterface; -import caosdb.server.jobs.JobAnnotation; -import caosdb.server.jobs.JobExecutionTime; - -@JobAnnotation(time = JobExecutionTime.POST_TRANSACTION) -public class AWIBoxLoanRequestLoanCuratorEmail extends AWIBoxLoanCuratorEmail { - - private static final String SUBJECT = "Box Loan Requests"; - - @Override - protected void run() { - StringBuilder body = new StringBuilder("LOAN REQUEST(S):"); - - if (!getContainer().isEmpty()) { - for (EntityInterface e : getContainer()) { - body.append("\n"); - body.append(loanToString(e)); - } - this.sendCuratorEmail(body.toString(), SUBJECT); - } - } -} diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestReturnCuratorEmail.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestReturnCuratorEmail.java deleted file mode 100644 index 373ce0b8d10ef6db560decb6afcc3e7ab5aa71b3..0000000000000000000000000000000000000000 --- a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoanRequestReturnCuratorEmail.java +++ /dev/null @@ -1,22 +0,0 @@ -package caosdb.server.jobs.extension; - -import caosdb.server.entity.EntityInterface; -import caosdb.server.jobs.JobAnnotation; -import caosdb.server.jobs.JobExecutionTime; - -@JobAnnotation(time = JobExecutionTime.POST_TRANSACTION) -public class AWIBoxLoanRequestReturnCuratorEmail extends AWIBoxLoanCuratorEmail { - private static final String SUBJECT = "Box Return Requests"; - - @Override - protected void run() { - if (!getContainer().isEmpty()) { - StringBuilder body = new StringBuilder("RETURN REQUEST(S):"); - for (EntityInterface e : getContainer()) { - body.append("\n"); - body.append(loanToString(e)); - } - this.sendCuratorEmail(body.toString(), SUBJECT); - } - } -} 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/resource/AbstractCaosDBServerResource.java b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java index ea4e65f0a60d72cb5da6cb03b2cec44848dbc3c6..d7897450b2caaa7a70f20ead02ed1bb0c67e192d 100644 --- a/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java +++ b/src/main/java/caosdb/server/resource/AbstractCaosDBServerResource.java @@ -29,7 +29,6 @@ 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; @@ -210,13 +209,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 +228,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 +245,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 +408,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) { 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/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/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..fa51d5ff49c9693d2216fd5dc168873f850e6e25 100644 --- a/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java +++ b/src/main/java/caosdb/server/scripting/ServerSideScriptingCaller.java @@ -35,6 +35,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.ProcessBuilder.Redirect; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -255,7 +256,12 @@ public class ServerSideScriptingCaller { } int callScript() throws IOException, InterruptedException, TimeoutException { - String[] effectiveCommandLine = injectAuthToken(getCommandLine()); + String[] effectiveCommandLine; + if (authToken != null) { + effectiveCommandLine = injectAuthToken(getCommandLine()); + } else { + effectiveCommandLine = Arrays.copyOf(getCommandLine(), getCommandLine().length); + } effectiveCommandLine[0] = makeCallAbsolute(effectiveCommandLine[0]); final ProcessBuilder pb = new ProcessBuilder(effectiveCommandLine); diff --git a/src/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/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/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..085be51d0929ce76d10ed5e4e8c32696e0b48476 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,9 +44,6 @@ 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(); @@ -185,16 +181,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 +199,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/jobs/extension/TestAWIBoxLoan.java b/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java deleted file mode 100644 index 08a0e412d8562779cb54574f94ddca932ff54777..0000000000000000000000000000000000000000 --- a/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java +++ /dev/null @@ -1,277 +0,0 @@ -package caosdb.server.jobs.extension; - -import static org.junit.Assert.assertEquals; - -import caosdb.server.entity.container.TransactionContainer; -import caosdb.server.jobs.core.Mode; -import caosdb.server.transaction.Transaction; -import caosdb.server.utils.EntityStatus; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.Callable; -import org.apache.shiro.authc.AuthenticationException; -import org.apache.shiro.authc.AuthenticationToken; -import org.apache.shiro.authz.AuthorizationException; -import org.apache.shiro.authz.Permission; -import org.apache.shiro.session.Session; -import org.apache.shiro.subject.ExecutionException; -import org.apache.shiro.subject.PrincipalCollection; -import org.apache.shiro.subject.Subject; -import org.junit.Test; - -public class TestAWIBoxLoan { - - @Test - public void testNonAnonymousUser() { - TransactionContainer container = new TransactionContainer(); - AWIBoxLoan j = - new AWIBoxLoan() { - - @Override - protected Subject getUser() { - return new Subject() { - - @Override - public void runAs(PrincipalCollection principals) - throws NullPointerException, IllegalStateException {} - - @Override - public PrincipalCollection releaseRunAs() { - return null; - } - - @Override - public void logout() {} - - @Override - public void login(AuthenticationToken token) throws AuthenticationException {} - - @Override - public boolean isRunAs() { - return false; - } - - @Override - public boolean isRemembered() { - return false; - } - - @Override - public boolean isPermittedAll(Collection<Permission> permissions) { - return false; - } - - @Override - public boolean isPermittedAll(String... permissions) { - return false; - } - - @Override - public boolean[] isPermitted(List<Permission> permissions) { - return null; - } - - @Override - public boolean[] isPermitted(String... permissions) { - return null; - } - - @Override - public boolean isPermitted(Permission permission) { - - return false; - } - - @Override - public boolean isPermitted(String permission) { - - return false; - } - - @Override - public boolean isAuthenticated() { - - return false; - } - - @Override - public boolean[] hasRoles(List<String> roleIdentifiers) { - - return null; - } - - @Override - public boolean hasRole(String roleIdentifier) { - - return false; - } - - @Override - public boolean hasAllRoles(Collection<String> roleIdentifiers) { - - return false; - } - - @Override - public Session getSession(boolean create) { - return null; - } - - @Override - public Session getSession() { - return null; - } - - @Override - public PrincipalCollection getPrincipals() { - return null; - } - - @Override - public Object getPrincipal() { - return null; - } - - @Override - public PrincipalCollection getPreviousPrincipals() { - return null; - } - - @Override - public void execute(Runnable runnable) {} - - @Override - public <V> V execute(Callable<V> callable) throws ExecutionException { - return null; - } - - @Override - public void checkRoles(String... roleIdentifiers) throws AuthorizationException {} - - @Override - public void checkRoles(Collection<String> roleIdentifiers) - throws AuthorizationException {} - - @Override - public void checkRole(String roleIdentifier) throws AuthorizationException {} - - @Override - public void checkPermissions(Collection<Permission> permissions) - throws AuthorizationException {} - - @Override - public void checkPermissions(String... permissions) throws AuthorizationException {} - - @Override - public void checkPermission(Permission permission) throws AuthorizationException {} - - @Override - public void checkPermission(String permission) throws AuthorizationException {} - - @Override - public Runnable associateWith(Runnable runnable) { - return null; - } - - @Override - public <V> Callable<V> associateWith(Callable<V> callable) { - return null; - } - }; - } - }; - Transaction<TransactionContainer> t = - new Transaction<TransactionContainer>(container, null) { - - @Override - protected void transaction() throws Exception {} - - @Override - protected void preTransaction() throws InterruptedException {} - - @Override - protected void preCheck() throws InterruptedException, Exception {} - - @Override - protected void postTransaction() throws Exception {} - - @Override - protected void postCheck() {} - - @Override - public boolean logHistory() { - return false; - } - - @Override - protected void init() throws Exception {} - - @Override - protected void cleanUp() {} - }; - - j.init(Mode.MUST, null, t); - assertEquals(0, j.getContainer().getMessages().size()); - assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus()); - - // non-anonymous user - j.run(); - assertEquals(0, j.getContainer().getMessages().size()); - assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus()); - } - - @Test - public void testAnonymousUserUnqualified() { - TransactionContainer container = new TransactionContainer(); - AWIBoxLoan j = - new AWIBoxLoan() { - - @Override - protected Subject getUser() { - return null; - } - - @Override - boolean isAnonymous() { - return true; - } - }; - Transaction<TransactionContainer> t = - new Transaction<TransactionContainer>(container, null) { - - @Override - protected void transaction() throws Exception {} - - @Override - protected void preTransaction() throws InterruptedException {} - - @Override - protected void preCheck() throws InterruptedException, Exception {} - - @Override - protected void postTransaction() throws Exception {} - - @Override - protected void postCheck() {} - - @Override - public boolean logHistory() { - return false; - } - - @Override - protected void init() throws Exception {} - - @Override - protected void cleanUp() {} - }; - - j.init(Mode.MUST, null, t); - assertEquals(0, j.getContainer().getMessages().size()); - assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus()); - - j.run(); - assertEquals(2, j.getContainer().getMessages().size()); - assertEquals(EntityStatus.UNQUALIFIED, j.getContainer().getStatus()); - } -} 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/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