diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..a3d20248b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +build +out +radar-auth/build +radar-auth/out +radar-auth/src/test +.idea +.gradle +.git +node_modules +oauth-client-util/build +oauth-client-util/out +oauth-client-util/src/test +src/test/java +src/gatling diff --git a/.editorconfig b/.editorconfig index a03599dd0..8e2bc8b68 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,7 @@ root = true # Change these settings to your own preference indent_style = space indent_size = 4 +continuation_indent_size = 8 # We recommend you to keep these unchanged end_of_line = lf @@ -22,3 +23,4 @@ trim_trailing_whitespace = false [{package,bower}.json] indent_style = space indent_size = 2 + diff --git a/.travis.yml b/.travis.yml index 7305971f5..531810aa4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,6 @@ script: - ./gradlew generateJavaClient - killall java # stop running MP instance - ./src/test/bash/run-prod-e2e.sh - - ./src/test/bash/test-docker-build-on-release-branch.sh - echo "include 'managementportal-client'" >> settings.gradle # make this a sub-project so we can build artifacts and javadoc easily after_script: - ./gradlew sendCoverageToCodacy diff --git a/Dockerfile b/Dockerfile index c87151fc1..9247d13f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,16 +8,25 @@ RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \ # installing the node packages before adding the src directory will allow us to re-use these image layers when only the souce code changes WORKDIR /app -COPY build.gradle gradle.properties gradlew package.json postcss.config.js proxy.conf.json settings.gradle tsconfig-aot.json tsconfig.json tslint.json yarn.lock /app/ +ENV GRADLE_OPTS="-Dorg.gradle.daemon=false -Dorg.gradle.project.prod=true" +COPY gradlew /app/ +COPY gradle/wrapper gradle/wrapper +RUN ./gradlew --version + COPY gradle gradle +COPY build.gradle gradle.properties settings.gradle /app/ +COPY radar-auth/build.gradle radar-auth/ +COPY oauth-client-util/build.gradle oauth-client-util/ +RUN ./gradlew downloadDependencies + +COPY package.json postcss.config.js proxy.conf.json tsconfig-aot.json tsconfig.json tslint.json yarn.lock /app/ COPY webpack webpack -COPY radar-auth radar-auth -COPY oauth-client-util oauth-client-util -RUN ./gradlew --no-daemon -s -Pprod npmInstall +RUN ./gradlew -s npmInstall # now we copy our application source code and build it +COPY radar-auth radar-auth COPY src src -RUN ./gradlew --no-daemon -s -Pprod bootRepackage +RUN ./gradlew -s bootRepackage # Run stage FROM openjdk:8-jre-alpine diff --git a/README.md b/README.md index 53729c6e8..62d4b792e 100644 --- a/README.md +++ b/README.md @@ -94,57 +94,75 @@ for other options on overriding the default configuration. | `SPRING_DATASOURCE_URL` | `jdbc:postgresql://localhost:5432/managementportal` | URL for the database to be used | | `SPRING_DATASOURCE_USERNAME` | `` | Username to access the database | | `SPRING_DATASOURCE_PASSWORD` | `` | Password to access the database | +| `SPRING_APPLICATION_JSON` | None | Generic environment variable for overriding all types of application settings | | `MANAGEMENTPORTAL_FRONTEND_CLIENT_SECRET` | None, you need to override this | OAuth client secret for the frontend | | `MANAGEMENTPORTAL_FRONTEND_ACCESS_TOKEN_VALIDITY_SECONDS` | `14400` | Frontend access token validity period in seconds | | `MANAGEMENTPORTAL_FRONTEND_REFRESH_TOKEN_VALIDITY_SECONDS` | `259200` | Frontend refresh token validity period in seconds | | `MANAGEMENTPORTAL_OAUTH_CLIENTS_FILE` | `/mp-includes/config/oauth_client_details.csv` | Location of the OAuth clients file | | `MANAGEMENTPORTAL_OAUTH_KEY_STORE_PASSWORD` | `radarbase` | Password for the JKS keystore | | `MANAGEMENTPORTAL_OAUTH_SIGNING_KEY_ALIAS` | `radarbase-managementportal-ec` | Alias in the keystore of the keypair to use for signing | -| `MANAGEMENTPORTAL_OAUTH_CHECKING_KEY_ALIASES_0` | None | Alias in the keystore of the public key to use for checking. Define multiple aliases by increasing number suffix (i.e. setting `MANAGEMENTPORTAL_OAUTH_CHECKING_KEY_ALIASES_1`, `MANAGEMENTPORTAL_OAUTH_CHECKING_KEY_ALIASES_2` etc.). If you do not set a list of checking key aliases, the public key of the signing keypair will be used for checking signatures. | | `MANAGEMENTPORTAL_CATALOGUE_SERVER_ENABLE_AUTO_IMPORT` | `false` | Wether to enable or disable auto import of sources from the catalogue server | | `MANAGEMENTPORTAL_CATALOGUE_SERVER_SERVER_URL` | None | URL to the catalogue server | +| `MANAGEMENTPORTAL_COMMON_BASE_URL` | None | Resolvable baseUrl of the hosted platform | +| `MANAGEMENTPORTAL_COMMON_MANAGEMENT_PORTAL_BASE_URL` | None | Resolvable baseUrl of this managementportal instance | +| `MANAGEMENTPORTAL_COMMON_PRIVACY_POLICY_URL` | None | Resolvable URL to the common privacy policy url | +| `MANAGEMENTPORTAL_COMMON_ADMIN_PASSWORD` | None | Admin password | | `JHIPSTER_SLEEP` | `10` | Time in seconds that the application should wait at bootup. Used to allow the database to become ready | | `JAVA_OPTS` | `-Xmx512m` | Options to pass on the JVM | +Lists cannot directly be encoded by environment variables in this version of Spring. So for example the OAuth checking key aliases need to be encoded using the `SPRING_APPLICATION_JSON` variable. For setting two aliases, set it to `{"managementportal":{"oauth":{"checkingKeyAliases":["one","two"]}}}`, for example. If this list is not set, the signing key will also be used as the checking key. + ### OAuth Clients ManagementPortal uses `OAuth2` workflow to provide authentication and authorization. To add new OAuth clients, you can add at runtime through the UI, or you can add them to the OAuth clients file referenced by the `MANAGEMENTPORTAL_OAUTH_CLIENTS_FILE` configuration option. - If your client is supposed to work with the `Pair app` feature, you need to set a key called `dynamic_registration` to `true` like this `{"dynamic_registration": true}` in its `additional_information` map. See the aRMT and pRMT clients for an example. +- If your client is `dynamic_registration` enabled, the QR code generated by `Pair app` feature will contain a short-living URL. By doing a `GET` request on that URL the `refresh-token` and related meta-data can be fetched. - If you want to prevent an OAuth client from being altered through the UI, you can add a key `{"protected": true}` in the `additional_information` map. -If the app is paired via the Pair App dialog, the QR code that will be scanned contains a refresh token. -``` -{ - "refreshToken": "..." -} -``` -The app can use that refresh token to get new access and refresh tokens: +If the app is paired via the Pair App dialog, the QR code that will be scanned contains a short-lived URL, e.g. `https://radar-base-url.org/api/meta-token/bMUkowOmTOci` + +Your app should access the URL, where it will receive an OAuth2 +refresh token as well as the platform's base URL and a URL to the privacy policy. No authorization +is required to access this URL. **Important:** For security reasons, the information at this URL can +only be accessed once. Once it has been accessed it can not be retrieved again. + +The app can use that refresh token to get new access and refresh tokens by doing the following HTTP +request to the base URL, using HTTP basic authentication with your OAuth client ID as username, and +an empty password. ``` -POST MyId:MySecret /oauth/token +POST /oauth/token Content-Type: application/x-www-form-urlencoded -grant_type=refresh_token&refresh_token=... +grant_type=refresh_token&refresh_token= ``` -This will respond with the access token and refresh token: +This will respond with at least the access token and refresh token: ```json { "access_token": "...", "refresh_token": "...", + "expires_in": 14400 } ``` -For the next request, you need to use the `refresh_token` that was returned rather than the one in the QR code, since refresh tokens are valid only once. +Both tokens are valid for a limited time only. When the access token runs out, you will need to +perform another request like the one above, but you need to use the new `refresh_token`, since +refresh tokens are valid only once. The code grant flow for OAuth2 clients can be the following: 1. Ask user confirmation for your app: ``` - GET /oauth/confirm_access?client_id=MyId&client_secret=MySecret&grant_type=code&redirect_uri=https://my.example.com/oauth_redirect + GET /oauth/confirm_access?client_id=MyId&grant_type=code&redirect_uri=https://my.example.com/oauth_redirect ``` - This needs to be done from a interactive web view, either a browser or a web window. If the user approves, this will redirect to `https://my.example.com/oauth_redirect?code=abcdef`. In Android, with [https://appauth.io](AppAuth library), the URL could be `com.example.my://oauth_redirect` for the `com.example.my` app. -2. Request a token for you app: + where you replace `MyId` with your OAuth client id. This needs to be done from a interactive + web view, either a browser or a web window. If the user approves, this will redirect to + `https://my.example.com/oauth_redirect?code=abcdef`. In Android, with [https://appauth.io] + (AppAuth library), the URL could be `com.example.my://oauth_redirect` for the `com.example.my` + app. +2. Request a token for your app by doing a POST, again with HTTP basic authentication with as +username your OAuth client id, and leaving the password empty: ``` - POST MyId:MySecret /oauth/token + POST /oauth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=abcdef&redirect_uri=https://my.example.com/oauth_redirect @@ -230,11 +248,14 @@ will generate few files: create src/main/webapp/app/my-component/my-component.component.ts update src/main/webapp/app/app.module.ts -## Building for production +## On Production +### Building for production To optimize the ManagementPortal application for production, run: ./gradlew -Pprod clean bootRepackage +### Hosting in production +The latest Meta-QR code implementation requires REST resources on `api/meta-token/*` should definitely be rate-limited by upstream servers. This will concatenate and minify the client CSS and JavaScript files. It will also modify `index.html` so it references these new files. To ensure everything worked, run: @@ -337,3 +358,4 @@ The resulting file can be imported into the [Swagger editor], or used with [Swag [OpenAPI]: https://www.openapis.org/ [Swagger editor]: http://editor.swagger.io/ [Swagger codegen]: https://swagger.io/swagger-codegen/ +[OAuth2 spec]: https://tools.ietf.org/html/rfc6749#section-9 diff --git a/build.gradle b/build.gradle index 0ed0792a2..6db39c01f 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ plugins { allprojects { group 'org.radarcns' - version '0.4.1' // project version + version '0.5.0' // project version // The comment on the previous line is only there to identify the project version line easily // with a sed command, to auto-update the version number with the prepare-release-branch.sh @@ -139,6 +139,27 @@ apply plugin: 'com.moowork.node' apply plugin: 'io.spring.dependency-management' defaultTasks 'bootRun' +configurations { + codacy + providedRuntime + compile.exclude module: "spring-boot-starter-tomcat" + cucumberTestCompile.extendsFrom testCompile + cucumberTestRuntime.extendsFrom testRuntime +} + +sourceSets { + cucumberTest { + java { + compileClasspath += main.output + test.output + test.compileClasspath + runtimeClasspath += main.output + test.output + test.compileClasspath + srcDir file('src/cucumberTest/java') + } + resources { + srcDir 'src/cucumberTest/resources' + } + } +} + bootRepackage { mainClass = 'org.radarcns.management.ManagementPortalApp' } @@ -176,20 +197,13 @@ if (OperatingSystem.current().isWindows()) { } } - - -test { - include '**/*UnitTest*' - include '**/*IntTest*' - - // uncomment if the tests reports are not generated - // see https://github.com/jhipster/generator-jhipster/pull/2771 and https://github.com/jhipster/generator-jhipster/pull/4484 - // ignoreFailures true - reports.html.enabled = true -} - task cucumberTest(type: Test) { - include '**/CucumberTest*' + testClassesDirs = sourceSets.cucumberTest.output.classesDirs + classpath = sourceSets.cucumberTest.runtimeClasspath + + testLogging { + events "skipped", "failed", "passed" + } // uncomment if the tests reports are not generated // see https://github.com/jhipster/generator-jhipster/pull/2771 and https://github.com/jhipster/generator-jhipster/pull/4484 @@ -228,11 +242,6 @@ ext.codacyVersion = '2.0.1' description = '' -configurations { - codacy - providedRuntime - compile.exclude module: "spring-boot-starter-tomcat" -} repositories { mavenCentral() @@ -526,3 +535,9 @@ artifactory { artifactoryPublish { publications('ManagementPortalPublication') } + +task downloadDependencies { + description "Pre-downloads dependencies" + configurations.compileClasspath.files + configurations.runtimeClasspath.files +} diff --git a/gradle/profile_prod.gradle b/gradle/profile_prod.gradle index eefb29aac..9e96f973b 100644 --- a/gradle/profile_prod.gradle +++ b/gradle/profile_prod.gradle @@ -8,10 +8,6 @@ ext { swaggerTargetFolder = "managementportal-client" } -dependencies { - -} - def profiles = 'prod' if (project.hasProperty('no-liquibase')) { profiles += ',no-liquibase' @@ -44,7 +40,7 @@ processResources { it.replace('#spring.profiles.active#', profiles) } filter { - it.replace('#project.version#', version) + it.replace('#project.version#', version.toString()) } } } diff --git a/oauth-client-util/README.md b/oauth-client-util/README.md index 321482cc1..8cfed130b 100644 --- a/oauth-client-util/README.md +++ b/oauth-client-util/README.md @@ -10,7 +10,7 @@ Quickstart: ```groovy dependencies { - compile group: 'org.radarcns', name: 'oauth-client-util', version: '0.4.1' + compile group: 'org.radarcns', name: 'oauth-client-util', version: '0.5.0' } ``` diff --git a/package.json b/package.json index 1f9222fb4..6ebfdfe6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "management-portal", - "version": "0.4.1", + "version": "0.5.0", "description": "Description for ManagementPortal", "private": true, "cacheDirectories": [ @@ -70,7 +70,7 @@ "karma-sourcemap-loader": "0.3.7", "karma-webpack": "2.0.3", "lazypipe": "1.0.1", - "lodash": "4.17.4", + "lodash": "4.17.5", "map-stream": "0.0.7", "node-sass": "^4.5.0", "phantomjs-prebuilt": "2.1.14", @@ -116,6 +116,6 @@ "test": "yarn run lint && karma start src/test/javascript/karma.conf.js", "test:watch": "karma start --watch", "e2e": "protractor src/test/javascript/protractor.conf.js", - "postinstall": "webdriver-manager update && yarn run webpack:build" + "postinstall": "webdriver-manager update --gecko false && yarn run webpack:build" } } diff --git a/radar-auth/README.md b/radar-auth/README.md index 25f77969c..6a763a80b 100644 --- a/radar-auth/README.md +++ b/radar-auth/README.md @@ -10,7 +10,7 @@ Add the dependency to your project. Gradle: ```groovy -compile group: 'org.radarcns', name: 'radar-auth', version: '0.4.1' +compile group: 'org.radarcns', name: 'radar-auth', version: '0.5.0' ``` The library expects the identity server configuration in a file called `radar-is.yml`. Either set diff --git a/radar-auth/src/main/java/org/radarcns/auth/authentication/TokenValidator.java b/radar-auth/src/main/java/org/radarcns/auth/authentication/TokenValidator.java index 84282502b..8ef7401ee 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/authentication/TokenValidator.java +++ b/radar-auth/src/main/java/org/radarcns/auth/authentication/TokenValidator.java @@ -9,6 +9,8 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Collection; +import java.util.stream.Stream; import org.radarcns.auth.config.ServerConfig; import org.radarcns.auth.config.YamlServerConfig; import org.radarcns.auth.exception.TokenValidationException; @@ -175,18 +177,14 @@ private List loadVerifiers() throws TokenValidationException { lastFetch = Instant.now(); } - List algorithms = new LinkedList<>(); - if (config.getPublicKeyEndpoints() != null) { - algorithms.addAll(config.getPublicKeyEndpoints().stream() - .map(this::algorithmFromServerPublicKey).collect(Collectors.toList())); - } - if (config.getPublicKeys() != null) { - algorithms.addAll(config.getPublicKeys().stream() - .map(this::algorithmFromString).collect(Collectors.toList())); - } + Stream endpointKeys = streamEmptyIfNull(config.getPublicKeyEndpoints()) + .map(this::algorithmFromServerPublicKey); + + Stream stringKeys = streamEmptyIfNull(config.getPublicKeys()) + .map(this::algorithmFromString); // Create a verifier for each signature verification algorithm we created - return algorithms.stream() + return Stream.concat(endpointKeys, stringKeys) .map(alg -> JWT.require(alg) .withAudience(config.getResourceName()) .build()) @@ -206,8 +204,8 @@ private Algorithm algorithmFromServerPublicKey(URI serverUri) throws TokenValida String alg = publicKeyInfo.get("alg").asText(); String pk = publicKeyInfo.get("value").asText(); return algorithmList.stream() - .filter(algorithm -> algorithm.getJwtAlgorithm().equals(alg)) - .filter(algorithm -> pk.startsWith(algorithm.getKeyHeader())) + .filter(algorithm -> algorithm.getJwtAlgorithm().equals(alg) + && pk.startsWith(algorithm.getKeyHeader())) .findFirst() .orElseThrow(() -> new TokenValidationException("The identity server " + "reported an unsupported signing algorithm: " + alg)) @@ -226,4 +224,8 @@ private Algorithm algorithmFromString(String publicKey) { + publicKey)) .getAlgorithm(publicKey); } + + private static Stream streamEmptyIfNull(Collection collection) { + return collection != null ? collection.stream() : Stream.empty(); + } } diff --git a/radar-auth/src/main/java/org/radarcns/auth/authorization/RadarAuthorization.java b/radar-auth/src/main/java/org/radarcns/auth/authorization/RadarAuthorization.java index b1373b58c..a5ec0efed 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/authorization/RadarAuthorization.java +++ b/radar-auth/src/main/java/org/radarcns/auth/authorization/RadarAuthorization.java @@ -74,4 +74,27 @@ public static void checkPermissionOnSubject(RadarToken token, Permission permiss permission.toString(), subjectName, projectName)); } } + + /** + * Similar to {@link RadarToken#hasPermissionOnSource(Permission, String, String, String)}, but + * this method throws an exception rather than returning a boolean. Useful in combination with, + * e.g., Spring's controllers and exception translators. + * @param token The token of the logged in user + * @param permission The permission to check + * @param projectName The project for which to check the permission + * @param subjectName The name of the subject to check + * @param sourceId The source ID to check + * @throws NotAuthorizedException if the supplied token does not have the permission in the + * given project for the given subject and source. + */ + public static void checkPermissionOnSource(RadarToken token, Permission permission, + String projectName, String subjectName, String sourceId) throws NotAuthorizedException { + log.debug("Checking permission {} for user {} on source {} of subject {} in project {}", + permission.toString(), token.getSubject(), sourceId, subjectName, projectName); + if (!token.hasPermissionOnSource(permission, projectName, subjectName, sourceId)) { + throw new NotAuthorizedException(String.format("Client %s does not have " + + "permission %s on source %s of subject %s in project %s", + token.getSubject(), permission.toString(), sourceId, subjectName, projectName)); + } + } } diff --git a/radar-auth/src/main/java/org/radarcns/auth/config/Constants.java b/radar-auth/src/main/java/org/radarcns/auth/config/Constants.java index 8bd4bc18c..df4e0f135 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/config/Constants.java +++ b/radar-auth/src/main/java/org/radarcns/auth/config/Constants.java @@ -7,6 +7,7 @@ public final class Constants { //Regex for acceptable logins public static final String ENTITY_ID_REGEX = "^[_'.@A-Za-z0-9- ]*$"; + public static final String TOKEN_NAME_REGEX = "^[A-Za-z0-9]*$"; public static final String SYSTEM_ACCOUNT = "system"; public static final String ANONYMOUS_USER = "anonymoususer"; diff --git a/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java b/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java index c70c68301..0a7b9f96e 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java +++ b/radar-auth/src/main/java/org/radarcns/auth/config/YamlServerConfig.java @@ -1,11 +1,5 @@ package org.radarcns.auth.config; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import org.radarcns.auth.exception.ConfigurationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -16,6 +10,12 @@ import java.util.List; import java.util.Objects; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import org.radarcns.auth.exception.ConfigurationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Created by dverbeec on 14/06/2017. */ @@ -26,42 +26,40 @@ public class YamlServerConfig implements ServerConfig { private String resourceName; private List publicKeys = new LinkedList<>(); - private static YamlServerConfig config; - private final Logger log = LoggerFactory.getLogger(YamlServerConfig.class); - - + private static final Logger log = LoggerFactory.getLogger(YamlServerConfig.class); /** * Read the configuration from file. This method will first check if the environment variable * RADAR_IS_CONFIG_LOCATION is set. If not set, it will look for a file called * radar_is.yml on the classpath. The configuration will be kept in a static field, - * so subsequent calls to this method will return the same object. Use {@link #reloadConfig()} - * to forcibly reload the configuration from the configuration file. + * so subsequent calls to this method will return the same object. * @return The initialized configuration object based on the contents of the configuration file * @throws ConfigurationException If there is any problem loading the configuration */ public static YamlServerConfig readFromFileOrClasspath() { - if (config != null) { - return config; - } - Logger log = LoggerFactory.getLogger(YamlServerConfig.class); String customLocation = System.getenv(LOCATION_ENV); URL configFile; - try { - if (customLocation != null) { - log.info(LOCATION_ENV + " environment variable set, loading config from {}", - customLocation); + if (customLocation != null) { + log.info(LOCATION_ENV + " environment variable set, loading config from {}", + customLocation); + try { configFile = new File(customLocation).toURI().toURL(); - } else { - // if config location not defined, look for it on the classpath - log.info(LOCATION_ENV + " environment variable not set, looking for it on" - + " the classpath"); - configFile = YamlServerConfig.class.getClassLoader().getResource(CONFIG_FILE_NAME); - log.info("Config file found at {}", configFile.getPath()); + } catch (MalformedURLException ex) { + throw new ConfigurationException(ex); + } + } else { + // if config location not defined, look for it on the classpath + log.info(LOCATION_ENV + + " environment variable not set, looking for it on the classpath"); + configFile = YamlServerConfig.class.getClassLoader().getResource(CONFIG_FILE_NAME); + + if (configFile == null) { + throw new ConfigurationException("Cannot find " + CONFIG_FILE_NAME + + " file in classpath. "); } - } catch (MalformedURLException ex) { - throw new ConfigurationException(ex); } + log.info("Config file found at {}", configFile.getPath()); + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); try (InputStream stream = configFile.openStream()) { return mapper.readValue(stream, YamlServerConfig.class); @@ -70,16 +68,7 @@ public static YamlServerConfig readFromFileOrClasspath() { } } - /** - * Forcibly reload the configuration from file, and reinitialize the static field holding the - * configuration with the new object. - * @return The new configuration - * @throws ConfigurationException If there is any problem loading the configuration - */ - public static YamlServerConfig reloadConfig() { - config = null; - return readFromFileOrClasspath(); - } + public List getPublicKeyEndpoints() { return publicKeyEndpoints; diff --git a/radar-auth/src/main/java/org/radarcns/auth/token/AbstractRadarToken.java b/radar-auth/src/main/java/org/radarcns/auth/token/AbstractRadarToken.java index a07c0c88d..0a6f0a956 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/token/AbstractRadarToken.java +++ b/radar-auth/src/main/java/org/radarcns/auth/token/AbstractRadarToken.java @@ -1,12 +1,14 @@ package org.radarcns.auth.token; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.radarcns.auth.authorization.AuthoritiesConstants.PARTICIPANT; +import static org.radarcns.auth.authorization.AuthoritiesConstants.SYS_ADMIN; + import java.util.Collection; import java.util.Objects; -import org.radarcns.auth.authorization.AuthoritiesConstants; import org.radarcns.auth.authorization.Permission; -import java.util.Collections; - /** * Partial implementation of {@link RadarToken}, providing a default implementation for the three * permission checks. @@ -25,17 +27,25 @@ public boolean hasPermission(Permission permission) { @Override public boolean hasPermissionOnProject(Permission permission, String projectName) { return hasScope(permission.scopeName()) - && (isClientCredentials() || hasAuthorityForPermission(permission, projectName)); + && (isClientCredentials() || hasAuthorityForProject(permission, projectName)); } @Override public boolean hasPermissionOnSubject(Permission permission, String projectName, String subjectName) { return hasScope(permission.scopeName()) - && (isClientCredentials() || hasAuthorityForPermission(permission, projectName, + && (isClientCredentials() || hasAuthorityForSubject(permission, projectName, subjectName)); } + @Override + public boolean hasPermissionOnSource(Permission permission, String projectName, + String subjectName, String sourceId) { + return hasScope(permission.scopeName()) + && (isClientCredentials() + || hasAuthorityForSource(permission, projectName, subjectName, sourceId)); + } + protected boolean hasScope(String scope) { return getScopes().contains(scope); } @@ -51,7 +61,8 @@ protected boolean isClientCredentials() { * @return {@code true} if any authority contains the permission, {@code false} otherwise */ protected boolean hasAuthorityForPermission(Permission permission) { - return getRoles().values().stream() + return getRoles() + .values().stream() .flatMap(Collection::stream) .anyMatch(permission::isAuthorityAllowed) || hasNonProjectRelatedAuthorityForPermission(permission); @@ -64,8 +75,9 @@ protected boolean hasAuthorityForPermission(Permission permission) { * @param projectName the project name * @return {@code true} if any authority contains the permission, {@code false} otherwise */ - protected boolean hasAuthorityForPermission(Permission permission, String projectName) { - return getRoles().getOrDefault(projectName, Collections.emptyList()).stream() + protected boolean hasAuthorityForProject(Permission permission, String projectName) { + return getRoles() + .getOrDefault(projectName, emptyList()).stream() .anyMatch(permission::isAuthorityAllowed) || hasNonProjectRelatedAuthorityForPermission(permission); } @@ -78,15 +90,23 @@ protected boolean hasAuthorityForPermission(Permission permission, String projec * @param subjectName the subject name * @return {@code true} if any authority contains the permission, {@code false} otherwise */ - protected boolean hasAuthorityForPermission(Permission permission, String projectName, + protected boolean hasAuthorityForSubject(Permission permission, String projectName, String subjectName) { - // if we're only a participant, we can only do operations on our own data - return (isJustParticipant(projectName) && getSubject().equals(subjectName) - && permission.isAuthorityAllowed(AuthoritiesConstants.PARTICIPANT)) - // if we have other roles beside participant, we should check those on the - // project level - || (!isJustParticipant(projectName) && hasAuthorityForPermission(permission, - projectName)); + if (isJustParticipant(projectName)) { + // if we're only a participant, we can only do operations on our own data + return getSubject().equals(subjectName) + && permission.isAuthorityAllowed(PARTICIPANT); + } else { + // if we have other roles beside participant, we should check those on the + // project level + return hasAuthorityForProject(permission, projectName); + } + } + + protected boolean hasAuthorityForSource(Permission permission, String projectName, + String subjectName, String sourceId) { + return hasAuthorityForSubject(permission, projectName, subjectName) + && getSources().contains(sourceId); } /** @@ -97,8 +117,8 @@ protected boolean hasAuthorityForPermission(Permission permission, String projec * otherwise */ protected boolean hasNonProjectRelatedAuthorityForPermission(Permission permission) { - return getAuthorities().contains(AuthoritiesConstants.SYS_ADMIN) - && permission.isAuthorityAllowed(AuthoritiesConstants.SYS_ADMIN); + return getAuthorities().contains(SYS_ADMIN) + && permission.isAuthorityAllowed(SYS_ADMIN); } /** @@ -108,8 +128,7 @@ protected boolean hasNonProjectRelatedAuthorityForPermission(Permission permissi * {@code false} otherwise */ protected boolean isJustParticipant(String projectName) { - return Objects.equals(getRoles().get(projectName), - Collections.singletonList(AuthoritiesConstants.PARTICIPANT)); + return singletonList(PARTICIPANT).equals(getRoles().get(projectName)); } @Override diff --git a/radar-auth/src/main/java/org/radarcns/auth/token/JwtRadarToken.java b/radar-auth/src/main/java/org/radarcns/auth/token/JwtRadarToken.java index f873db946..cea49150f 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/token/JwtRadarToken.java +++ b/radar-auth/src/main/java/org/radarcns/auth/token/JwtRadarToken.java @@ -1,17 +1,21 @@ package org.radarcns.auth.token; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toList; + import com.auth0.jwt.interfaces.DecodedJWT; -import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import java.util.regex.Pattern; /** * Implementation of {@link RadarToken} based on JWT tokens. */ public class JwtRadarToken extends AbstractRadarToken { + private static final Pattern ROLE_SEPARATOR_PATTERN = Pattern.compile(":"); public static final String AUTHORITIES_CLAIM = "authorities"; public static final String ROLES_CLAIM = "roles"; @@ -118,16 +122,9 @@ private Map> parseRoles(DecodedJWT jwt) { return emptyIfNull(jwt.getClaim(ROLES_CLAIM).asList(String.class)).stream() .filter(s -> s.contains(":")) .distinct() - .map(s -> s.split(":")) - .collect(Collectors.toMap( - s -> s[0], // project - s -> Collections.singletonList(s[1]), // role - (roles1, roles2) -> { // merge - List merged = new ArrayList<>(roles1.size() + roles2.size()); - merged.addAll(roles1); - merged.addAll(roles2); - return merged; - })); + .map(ROLE_SEPARATOR_PATTERN::split) + .collect(groupingBy(s -> s[0], + mapping(s -> s[1], toList()))); } private static String emptyIfNull(String string) { diff --git a/radar-auth/src/main/java/org/radarcns/auth/token/RadarToken.java b/radar-auth/src/main/java/org/radarcns/auth/token/RadarToken.java index de37799b1..4bfb53de9 100644 --- a/radar-auth/src/main/java/org/radarcns/auth/token/RadarToken.java +++ b/radar-auth/src/main/java/org/radarcns/auth/token/RadarToken.java @@ -13,79 +13,79 @@ public interface RadarToken { /** * Get the roles defined in this token. - * @return a map describing the roles defined in this token. The keys in the map are the + * @return non-null map describing the roles defined in this token. The keys in the map are the * project names, and the values are lists of authority names associated to the project. */ Map> getRoles(); /** * Get a list of non-project related authorities. - * @return a list of authority names + * @return non-null list of authority names */ List getAuthorities(); /** * Get a list of scopes assigned to this token. - * @return a list of scope names + * @return non-null list of scope names */ List getScopes(); /** * Get a list of source names associated with this token. - * @return a list of source names + * @return non-null list of source names */ List getSources(); /** * Get this token's OAuth2 grant type. - * @return the grant type + * @return non-null grant type */ String getGrantType(); /** * Get the token subject. - * @return the subject + * @return non-null subject */ String getSubject(); /** * Get the date this token was issued. - * @return the date this token was issued + * @return date this token was issued or null */ Date getIssuedAt(); /** * Get the date this token expires. - * @return the date this token expires + * @return date this token expires or null */ Date getExpiresAt(); /** * Get the audience of the token. - * @return the list of resources that are allowed to accept the token + * @return non-null list of resources that are allowed to accept the token */ List getAudience(); /** * Get a string representation of this token. - * @return the string representation of this token + * @return non-null string representation of this token */ String getToken(); /** * Get the issuer. - * @return the issuer + * @return non-null issuer */ String getIssuer(); /** * Get the token type. - * @return the token type. + * @return non-null token type. */ String getType(); /** - * Check if this token has the given permission, not taking into account project affiliations. + * Check if this token gives the given permission, not taking into account project affiliations. * *

This token must have the permission in its set of scopes. If it's a * client credentials token, this is the only requirement, as a client credentials token is @@ -98,7 +98,7 @@ public interface RadarToken { boolean hasPermission(Permission permission); /** - * Check if this token has a permission in a specific project. + * Check if this token gives a permission in a specific project. * @param permission the permission * @param projectName the project name * @return true if this token has the permission in the project, false otherwise @@ -106,7 +106,7 @@ public interface RadarToken { boolean hasPermissionOnProject(Permission permission, String projectName); /** - * Check if this token has a permission on a subject in a given project. + * Check if this token gives a permission on a subject in a given project. * @param permission the permission * @param projectName the project name * @param subjectName the subject name @@ -114,4 +114,15 @@ public interface RadarToken { * otherwise */ boolean hasPermissionOnSubject(Permission permission, String projectName, String subjectName); + + /** + * Check if this token gives a permission on a given source. + * @param permission the permission + * @param projectName the project name + * @param subjectName the subject name + * @param sourceId the source ID + * @return true if this token gives permission for the source, false otherwise + */ + boolean hasPermissionOnSource(Permission permission, String projectName, String subjectName, + String sourceId); } diff --git a/radar-auth/src/test/java/org/radarcns/auth/authorization/RadarAuthorizationTest.java b/radar-auth/src/test/java/org/radarcns/auth/authorization/RadarAuthorizationTest.java index 3e173c9ff..50fbb7200 100644 --- a/radar-auth/src/test/java/org/radarcns/auth/authorization/RadarAuthorizationTest.java +++ b/radar-auth/src/test/java/org/radarcns/auth/authorization/RadarAuthorizationTest.java @@ -1,24 +1,23 @@ package org.radarcns.auth.authorization; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + import java.security.GeneralSecurityException; +import java.util.Arrays; import java.util.Collection; import java.util.Map.Entry; - +import java.util.Set; +import java.util.stream.Collectors; import org.junit.BeforeClass; import org.junit.Test; +import org.radarcns.auth.authorization.Permission.Entity; import org.radarcns.auth.exception.NotAuthorizedException; import org.radarcns.auth.token.JwtRadarToken; import org.radarcns.auth.token.RadarToken; import org.radarcns.auth.util.TokenTestUtils; -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; - -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - /** * Created by dverbeec on 25/09/2017. */ @@ -113,17 +112,67 @@ public void testMultipleRolesInProjectToken() throws NotAuthorizedException { } } + @Test + public void testCheckPermissionOnSource() throws NotAuthorizedException { + String project = "PROJECT1"; + // this token is participant in PROJECT2 + RadarToken token = new JwtRadarToken(TokenTestUtils.PROJECT_ADMIN_TOKEN); + String subject = "some-subject"; + String source = "source-1"; + + Permission.allPermissions() + .forEach(p -> assertNotAuthorized( + () -> RadarAuthorization.checkPermissionOnSource( + token, p, project, subject, source), + "Token should not have permission " + p + " on another subject")); + } + + @Test + public void testCheckPermissionOnOwnSource() throws NotAuthorizedException { + String project = "PROJECT2"; + // this token is participant in PROJECT2 + RadarToken token = new JwtRadarToken(TokenTestUtils.MULTIPLE_ROLES_IN_PROJECT_TOKEN); + String subject = token.getSubject(); + String source = "source-1"; // source to use + + Set permissions = Permission.allPermissions().stream() + .filter(p -> p.getEntity() == Entity.MEASUREMENT) + .collect(Collectors.toSet()); + + for (Permission p : permissions) { + RadarAuthorization.checkPermissionOnSource(token, p, project, subject, source); + } + } + + + @Test + public void testCheckPermissionOnOtherSource() { + String project = "PROJECT2"; + // this token is participant in PROJECT2 + RadarToken token = new JwtRadarToken(TokenTestUtils.MULTIPLE_ROLES_IN_PROJECT_TOKEN); + String subject = token.getSubject(); + String source = "source-2"; // source to use + + Permission.allPermissions() + .forEach(p -> assertNotAuthorized( + () -> RadarAuthorization.checkPermissionOnSource( + token, p, project, subject, source), + "Token should not have permission " + p + " on another subject")); + } + @Test public void testScopeOnlyToken() throws NotAuthorizedException { RadarToken token = new JwtRadarToken(TokenTestUtils.SCOPE_TOKEN); // test that we can do the things we have a scope for Collection scope = Arrays.asList( - Permission.SUBJECT_READ, Permission.SUBJECT_CREATE, Permission.PROJECT_READ); + Permission.SUBJECT_READ, Permission.SUBJECT_CREATE, Permission.PROJECT_READ, + Permission.MEASUREMENT_CREATE); for (Permission p : scope) { RadarAuthorization.checkPermission(token, p); RadarAuthorization.checkPermissionOnProject(token, p, ""); RadarAuthorization.checkPermissionOnSubject(token, p, "", ""); + RadarAuthorization.checkPermissionOnSource(token, p, "", "", ""); } // test we can do nothing else, for each of the checkPermission methods @@ -144,6 +193,12 @@ public void testScopeOnlyToken() throws NotAuthorizedException { .forEach(p -> assertNotAuthorized( () -> RadarAuthorization.checkPermissionOnSubject(token, p, "", ""), "Permission " + p + " is granted but not in scope.")); + + Permission.allPermissions().stream() + .filter(p -> !scope.contains(p)) + .forEach(p -> assertNotAuthorized( + () -> RadarAuthorization.checkPermissionOnSource(token, p, "", "", ""), + "Permission " + p + " is granted but not in scope.")); } private static void assertNotAuthorized(AuthorizationCheck supplier, String message) { diff --git a/radar-auth/src/test/java/org/radarcns/auth/util/TokenTestUtils.java b/radar-auth/src/test/java/org/radarcns/auth/util/TokenTestUtils.java index a7642c821..cced50384 100644 --- a/radar-auth/src/test/java/org/radarcns/auth/util/TokenTestUtils.java +++ b/radar-auth/src/test/java/org/radarcns/auth/util/TokenTestUtils.java @@ -157,7 +157,7 @@ private static void initMultipleRolesToken(Algorithm algorithm, Instant exp, Ins .withArrayClaim("authorities", new String[] {"ROLE_PROJECT_ADMIN"}) .withArrayClaim("roles", new String[] {"PROJECT2:ROLE_PROJECT_ADMIN", "PROJECT2:ROLE_PARTICIPANT"}) - .withArrayClaim("sources", new String[] {}) + .withArrayClaim("sources", new String[] {"source-1"}) .withClaim("client_id", CLIENT) .withClaim("user_name", USER) .withClaim("jti", JTI) @@ -215,7 +215,7 @@ private static void initTokenWithScopes(Algorithm algorithm, Instant exp, Instan .withAudience(CLIENT) .withSubject("i'm a trusted oauth client") .withArrayClaim("scope", new String[] {"PROJECT.READ", "SUBJECT.CREATE", - "SUBJECT.READ"}) + "SUBJECT.READ", "MEASUREMENT.CREATE"}) .withClaim("client_id", "i'm a trusted oauth client") .withClaim("jti", JTI) .withClaim("grant_type", "client_credentials") diff --git a/src/test/java/org/radarcns/management/cucumber/CucumberTest.java b/src/cucumberTest/CucumberTest.java similarity index 60% rename from src/test/java/org/radarcns/management/cucumber/CucumberTest.java rename to src/cucumberTest/CucumberTest.java index dc812e75b..80c880105 100644 --- a/src/test/java/org/radarcns/management/cucumber/CucumberTest.java +++ b/src/cucumberTest/CucumberTest.java @@ -1,4 +1,4 @@ -package org.radarcns.management.cucumber; +package cucumber; import org.junit.runner.RunWith; @@ -7,7 +7,7 @@ import cucumber.api.junit.Cucumber; @RunWith(Cucumber.class) -@CucumberOptions(plugin = "pretty", features = "src/test/features") +@CucumberOptions(plugin = "pretty", features = "src/cucumberTest/features") public class CucumberTest { } diff --git a/src/test/features/user/user.feature b/src/cucumberTest/features/user/user.feature similarity index 100% rename from src/test/features/user/user.feature rename to src/cucumberTest/features/user/user.feature diff --git a/src/test/java/org/radarcns/management/cucumber/stepdefs/StepDefs.java b/src/cucumberTest/java/cucumber/stepdefs/StepDefs.java similarity index 90% rename from src/test/java/org/radarcns/management/cucumber/stepdefs/StepDefs.java rename to src/cucumberTest/java/cucumber/stepdefs/StepDefs.java index 577f00c56..240197797 100644 --- a/src/test/java/org/radarcns/management/cucumber/stepdefs/StepDefs.java +++ b/src/cucumberTest/java/cucumber/stepdefs/StepDefs.java @@ -1,4 +1,4 @@ -package org.radarcns.management.cucumber.stepdefs; +package cucumber.stepdefs; import org.radarcns.management.ManagementPortalTestApp; diff --git a/src/test/java/org/radarcns/management/cucumber/stepdefs/UserStepDefs.java b/src/cucumberTest/java/cucumber/stepdefs/UserStepDefs.java similarity index 95% rename from src/test/java/org/radarcns/management/cucumber/stepdefs/UserStepDefs.java rename to src/cucumberTest/java/cucumber/stepdefs/UserStepDefs.java index 7d89c468b..8d6aef3a6 100644 --- a/src/test/java/org/radarcns/management/cucumber/stepdefs/UserStepDefs.java +++ b/src/cucumberTest/java/cucumber/stepdefs/UserStepDefs.java @@ -1,4 +1,4 @@ -package org.radarcns.management.cucumber.stepdefs; +package cucumber.stepdefs; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -18,7 +18,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -public class UserStepDefs extends StepDefs { +public class UserStepDefs extends cucumber.stepdefs.StepDefs { @Autowired private UserResource userResource; diff --git a/src/main/docker/etc/config/oauth_client_details.csv b/src/main/docker/etc/config/oauth_client_details.csv index 0db7fb977..0ad0b94ca 100644 --- a/src/main/docker/etc/config/oauth_client_details.csv +++ b/src/main/docker/etc/config/oauth_client_details.csv @@ -1,7 +1,7 @@ client_id;resource_ids;client_secret;scope;authorized_grant_types;redirect_uri;authorities;access_token_validity;refresh_token_validity;additional_information;autoapprove -pRMT;res_ManagementPortal,res_gateway;secret;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true}; -aRMT;res_ManagementPortal,res_gateway;secret;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true}; -THINC-IT;res_ManagementPortal,res_gateway;secret;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true}; +pRMT;res_ManagementPortal,res_gateway;;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true}; +aRMT;res_ManagementPortal,res_gateway;;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true}; +THINC-IT;res_ManagementPortal,res_gateway;;MEASUREMENT.CREATE,SUBJECT.UPDATE,SUBJECT.READ,PROJECT.READ,SOURCETYPE.READ,SOURCE.READ,SOURCETYPE.READ,SOURCEDATA.READ,USER.READ,ROLE.READ;refresh_token,authorization_code;;;43200;7948800;{"dynamic_registration": true}; radar_restapi;res_ManagementPortal;secret;SUBJECT.READ,PROJECT.READ,SOURCE.READ,SOURCETYPE.READ;client_credentials;;;43200;259200;{}; radar_redcap_integrator;res_ManagementPortal;secret;PROJECT.READ,SUBJECT.CREATE,SUBJECT.READ,SUBJECT.UPDATE;client_credentials;;;43200;259200;{}; -radar_dashboard;res_ManagementPortal,res_RestApi;secret;SUBJECT.READ,PROJECT.READ,SOURCE.READ,SOURCETYPE.READ;refresh_token,authorization_code;;;43200;259200;{}; +radar_dashboard;res_ManagementPortal,res_RestApi;;SUBJECT.READ,PROJECT.READ,SOURCE.READ,SOURCETYPE.READ;refresh_token,authorization_code;;;43200;259200;{}; diff --git a/src/main/docker/management-portal.yml b/src/main/docker/management-portal.yml index 091d5c9a7..6f5cb4895 100644 --- a/src/main/docker/management-portal.yml +++ b/src/main/docker/management-portal.yml @@ -1,7 +1,7 @@ version: '2' services: managementportal-app: - image: radarcns/management-portal:0.4.1 + image: radarcns/management-portal:0.5.0 environment: - SPRING_PROFILES_ACTIVE=prod,swagger - SPRING_DATASOURCE_URL=jdbc:postgresql://managementportal-postgresql:5432/managementportal diff --git a/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java b/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java index c3ae52543..fac8dbea9 100644 --- a/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java +++ b/src/main/java/org/radarcns/management/config/LocalKeystoreConfig.java @@ -1,15 +1,13 @@ package org.radarcns.management.config; -import org.radarcns.auth.config.ServerConfig; -import org.radarcns.management.security.jwt.RadarJwtAccessTokenConverter; -import org.radarcns.management.security.jwt.RadarKeyStoreKeyFactory; -import org.springframework.core.io.ClassPathResource; - import java.net.URI; -import java.security.KeyPair; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import org.radarcns.auth.config.ServerConfig; +import org.radarcns.management.security.jwt.JwtAlgorithm; +import org.radarcns.management.security.jwt.RadarKeyStoreKeyFactory; +import org.springframework.core.io.ClassPathResource; /** * Radar-auth server configuration for using a local keystore. This will load the MP public key @@ -30,13 +28,8 @@ public LocalKeystoreConfig(String keyStorePassword, List checkingKeyAlia // Load the key and convert to PEM format, internally spring uses the // JwtAccessTokenConverter to do that for the token_key endpoint. We can use it here as // well. - RadarJwtAccessTokenConverter converter = new RadarJwtAccessTokenConverter(); - publicKeys = checkingKeyAliases.stream() - .map(alias -> { - KeyPair keyPair = keyFactory.getKeyPair(alias); - converter.setKeyPair(keyPair); - return converter.getKey().get("value"); - }) + publicKeys = keyFactory.streamJwtAlgorithm(checkingKeyAliases) + .map(JwtAlgorithm::getEncodedString) .collect(Collectors.toList()); } diff --git a/src/main/java/org/radarcns/management/config/ManagementPortalProperties.java b/src/main/java/org/radarcns/management/config/ManagementPortalProperties.java index 2aa7f5f1b..4817e1135 100644 --- a/src/main/java/org/radarcns/management/config/ManagementPortalProperties.java +++ b/src/main/java/org/radarcns/management/config/ManagementPortalProperties.java @@ -16,6 +16,8 @@ public class ManagementPortalProperties { private final Oauth oauth = new Oauth(); + private final Common common = new Common(); + private final CatalogueServer catalogueServer = new CatalogueServer(); public ManagementPortalProperties.Frontend getFrontend() { @@ -34,19 +36,19 @@ public CatalogueServer getCatalogueServer() { return catalogueServer; } - public static class Mail { + public Common getCommon() { + return common; + } - private String from = ""; + public static class Common { private String baseUrl = ""; - public String getFrom() { - return from; - } + private String managementPortalBaseUrl = ""; - public void setFrom(String from) { - this.from = from; - } + private String privacyPolicyUrl = ""; + + private String adminPassword = ""; public String getBaseUrl() { return baseUrl; @@ -55,6 +57,44 @@ public String getBaseUrl() { public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } + + public String getPrivacyPolicyUrl() { + return privacyPolicyUrl; + } + + public void setPrivacyPolicyUrl(String privacyPolicyUrl) { + this.privacyPolicyUrl = privacyPolicyUrl; + } + + public String getAdminPassword() { + return adminPassword; + } + + public void setAdminPassword(String adminPassword) { + this.adminPassword = adminPassword; + } + + public String getManagementPortalBaseUrl() { + return managementPortalBaseUrl; + } + + public void setManagementPortalBaseUrl(String managementPortalBaseUrl) { + this.managementPortalBaseUrl = managementPortalBaseUrl; + } + } + + public static class Mail { + + private String from = ""; + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } + } public static class Frontend { @@ -120,6 +160,8 @@ public static class Oauth { private String keyStorePassword; + private String metaTokenTimeout; + public String getClientsFile() { return clientsFile; } @@ -151,6 +193,14 @@ public String getKeyStorePassword() { public void setKeyStorePassword(String keyStorePassword) { this.keyStorePassword = keyStorePassword; } + + public String getMetaTokenTimeout() { + return metaTokenTimeout; + } + + public void setMetaTokenTimeout(String metaTokenTimeout) { + this.metaTokenTimeout = metaTokenTimeout; + } } public static class CatalogueServer { diff --git a/src/main/java/org/radarcns/management/config/OAuthClientLoader.java b/src/main/java/org/radarcns/management/config/ManagementPortalSecurityConfigLoader.java similarity index 87% rename from src/main/java/org/radarcns/management/config/OAuthClientLoader.java rename to src/main/java/org/radarcns/management/config/ManagementPortalSecurityConfigLoader.java index a21fd5bf7..f57d996ee 100644 --- a/src/main/java/org/radarcns/management/config/OAuthClientLoader.java +++ b/src/main/java/org/radarcns/management/config/ManagementPortalSecurityConfigLoader.java @@ -1,12 +1,5 @@ package org.radarcns.management.config; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.MappingIterator; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.csv.CsvMapper; -import com.fasterxml.jackson.dataformat.csv.CsvSchema; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -18,7 +11,16 @@ import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.MappingIterator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.csv.CsvMapper; +import com.fasterxml.jackson.dataformat.csv.CsvSchema; import org.radarcns.auth.authorization.Permission; +import org.radarcns.management.service.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -31,10 +33,11 @@ import org.springframework.stereotype.Component; /** + * Loads security configs such as oauth-clients, and overriding admin password if specified. * Created by dverbeec on 20/11/2017. */ @Component -public class OAuthClientLoader { +public class ManagementPortalSecurityConfigLoader { @Autowired private JdbcClientDetailsService clientDetailsService; @@ -42,15 +45,37 @@ public class OAuthClientLoader { @Autowired private ManagementPortalProperties managementPortalProperties; - private static Logger logger = LoggerFactory.getLogger(OAuthClientLoader.class); + @Autowired + private UserService userService; + + private static Logger logger = + LoggerFactory.getLogger(ManagementPortalSecurityConfigLoader.class); private static final Character SEPARATOR = ';'; + /** + * Resets the admin password to the value of managementportal.common.adminPassword value if + * exists. + */ + @EventListener(ContextRefreshedEvent.class) + public void overrideAdminPassword() { + String adminPassword = managementPortalProperties.getCommon().getAdminPassword(); + + if (adminPassword != null && !adminPassword.isEmpty()) { + logger.info("Overriding admin password to configured password"); + userService.changePassword("admin", adminPassword); + } else { + logger.info("AdminPassword property is empty. Using default password..."); + } + } + + /** * Build the ClientDetails for the ManagementPortal frontend and load it to the database. */ @EventListener(ContextRefreshedEvent.class) public void loadFrontendOauthClient() { + logger.info("Loading ManagementPortal frontend client"); ManagementPortalProperties.Frontend frontend = managementPortalProperties.getFrontend(); BaseClientDetails details = new BaseClientDetails(); details.setClientId(frontend.getClientId()); diff --git a/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java b/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java index b3f4fdd35..ed44cfc53 100644 --- a/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java +++ b/src/main/java/org/radarcns/management/config/OAuth2ServerConfiguration.java @@ -1,14 +1,24 @@ package org.radarcns.management.config; +import static org.springframework.orm.jpa.vendor.Database.POSTGRESQL; + import io.github.jhipster.security.AjaxLogoutSuccessHandler; import io.github.jhipster.security.Http401UnauthorizedEntryPoint; +import java.security.KeyPair; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import javax.sql.DataSource; import org.radarcns.auth.authorization.AuthoritiesConstants; +import org.radarcns.management.config.ManagementPortalProperties.Oauth; import org.radarcns.management.security.ClaimsTokenEnhancer; import org.radarcns.management.security.PostgresApprovalStore; -import org.radarcns.management.security.jwt.EcdsaVerifier; +import org.radarcns.management.security.jwt.JwtAlgorithm; import org.radarcns.management.security.jwt.MultiVerifier; import org.radarcns.management.security.jwt.RadarJwtAccessTokenConverter; import org.radarcns.management.security.jwt.RadarKeyStoreKeyFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; @@ -26,7 +36,6 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.jwt.crypto.sign.RsaVerifier; import org.springframework.security.jwt.crypto.sign.SignatureVerifier; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; @@ -50,18 +59,9 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.filter.CorsFilter; -import javax.sql.DataSource; -import java.security.KeyPair; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPublicKey; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import static org.springframework.orm.jpa.vendor.Database.POSTGRESQL; - @Configuration public class OAuth2ServerConfiguration { + private static final Logger logger = LoggerFactory.getLogger(OAuth2ServerConfiguration.class); @Autowired private DataSource dataSource; @@ -216,33 +216,32 @@ public TokenStore tokenStore() { public JwtAccessTokenConverter accessTokenConverter() { RadarJwtAccessTokenConverter converter = new RadarJwtAccessTokenConverter(); + Oauth oauthConfig = managementPortalProperties.getOauth(); + // set the keypair for signing RadarKeyStoreKeyFactory kf = new RadarKeyStoreKeyFactory( new ClassPathResource("/config/keystore.jks"), - managementPortalProperties.getOauth().getKeyStorePassword().toCharArray()); - String signKey = managementPortalProperties.getOauth().getSigningKeyAlias(); + oauthConfig.getKeyStorePassword().toCharArray()); + String signKey = oauthConfig.getSigningKeyAlias(); + logger.debug("Using JWT signing key {}", signKey); KeyPair keyPair = kf.getKeyPair(signKey); converter.setKeyPair(keyPair); // if a list of checking keys is defined, use that for checking - if (managementPortalProperties.getOauth().getCheckingKeyAliases() != null - && !managementPortalProperties.getOauth().getCheckingKeyAliases().isEmpty()) { + List checkingAliases = oauthConfig.getCheckingKeyAliases(); + logger.debug("Using JWT verification keys {}", checkingAliases); + List verifiers = kf.streamJwtAlgorithm(checkingAliases) + .map(JwtAlgorithm::getVerifier) + .collect(Collectors.toList()); + + if (verifiers.size() > 1) { // get all public keys for verifying and set the converter's verifier // to a MultiVerifier - List verifiers = managementPortalProperties.getOauth() - .getCheckingKeyAliases().stream() - .map(alias -> kf.getKeyPair(alias).getPublic()) - .filter(publicKey -> publicKey instanceof RSAPublicKey - || publicKey instanceof ECPublicKey).map(publicKey -> { - if (publicKey instanceof RSAPublicKey) { - return new RsaVerifier((RSAPublicKey) publicKey); - } else { - return new EcdsaVerifier((ECPublicKey) publicKey); - } - }) - .collect(Collectors.toList()); converter.setVerifier(new MultiVerifier(verifiers)); - } + } else if (verifiers.size() == 1) { + // only has one verifier, use it directly + converter.setVerifier(verifiers.get(0)); + } // else, use the signing key verifier. return converter; } diff --git a/src/main/java/org/radarcns/management/config/SecurityConfiguration.java b/src/main/java/org/radarcns/management/config/SecurityConfiguration.java index 9224ba7aa..4a8848674 100644 --- a/src/main/java/org/radarcns/management/config/SecurityConfiguration.java +++ b/src/main/java/org/radarcns/management/config/SecurityConfiguration.java @@ -91,7 +91,8 @@ public void configure(WebSecurity web) throws Exception { .antMatchers("/api/account/reset_password/init") .antMatchers("/api/account/reset_password/finish") .antMatchers("/test/**") - .antMatchers("/h2-console/**"); + .antMatchers("/h2-console/**") + .antMatchers("/api/meta-token/**"); } @Override diff --git a/src/main/java/org/radarcns/management/config/SourceTypeLoader.java b/src/main/java/org/radarcns/management/config/SourceTypeLoader.java index 12b31abe7..bc7732ed1 100644 --- a/src/main/java/org/radarcns/management/config/SourceTypeLoader.java +++ b/src/main/java/org/radarcns/management/config/SourceTypeLoader.java @@ -4,7 +4,6 @@ import java.net.URL; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import org.radarcns.management.domain.SourceData; import org.radarcns.management.domain.SourceType; import org.radarcns.management.repository.SourceDataRepository; @@ -21,6 +20,7 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; /** @@ -66,61 +66,97 @@ public void run(String... args) { SourceTypeResponse.class); SourceTypeResponse catalogueDto = catalogues.getBody(); List catalogSourceTypes = new ArrayList<>(); - if (Objects.nonNull(catalogueDto.getPassiveSources())) { + if (catalogueDto.getPassiveSources() != null) { catalogSourceTypes.addAll(catalogueDto.getPassiveSources()); } - if (Objects.nonNull(catalogueDto.getActiveSources())) { + + if (catalogueDto.getActiveSources() != null) { catalogSourceTypes.addAll(catalogueDto.getActiveSources()); } - if (Objects.nonNull(catalogueDto.getMonitorSources())) { + + if (catalogueDto.getMonitorSources() != null) { catalogSourceTypes.addAll(catalogueDto.getMonitorSources()); } - for (CatalogSourceType catalogSourceType : catalogSourceTypes) { - SourceType sourceType = catalogSourceTypeMapper - .catalogSourceTypeToSourceType(catalogSourceType); - // check whether a source-type is already available with given config - if (!sourceTypeRepository - .findOneWithEagerRelationshipsByProducerAndModelAndVersion( - sourceType.getProducer(), sourceType.getModel(), - sourceType.getCatalogVersion()).isPresent()) { - // create new source-type - sourceTypeRepository.save(sourceType); - - // create source-data for the new source-type - for (CatalogSourceData catalogSourceData : catalogSourceType - .getData()) { - SourceData sourceData = catalogSourceDataMapper - .catalogSourceDataToSourceData(catalogSourceData); - // sourceDataName should be unique - // generated by combining sourceDataType and source-type configs - sourceData.sourceDataName( - sourceType.getProducer() + "_" + sourceType.getModel() + "_" - + sourceType - .getCatalogVersion() + "_" + sourceData - .getSourceDataType()); - sourceData.sourceType(sourceType); - sourceDataRepository.save(sourceData); - } - } else { - // skip for existing source-types - log.info("Source-type {} is already available ", - sourceType.getProducer() + "_" + sourceType.getModel() + "_" - + sourceType.getCatalogVersion()); - } + if (catalogueDto.getConnectorSources() != null) { + catalogSourceTypes.addAll(catalogueDto.getConnectorSources()); } - log.info("Completed source-type import from catalog-server"); + saveSourceTypesFromCatalogServer(catalogSourceTypes); + } else { log.warn("Catalog Service {} is unreachable: {}", catalogServerUrl); } } catch (MalformedURLException e) { log.warn("Invalid Url provided for Catalog server url {} : {}", catalogServerUrl, e.getMessage()); + } catch (RuntimeException exe) { + log.warn("An error has occurred during auto import of source-types. ", exe + .getMessage()); } } else { log.info("Auto source-type import is disabled"); } } + + /** + * Converts given {@link CatalogSourceType} to {@link SourceType} and saves it to the databse + * after validations. + * @param catalogSourceTypes list of source-type from catalogue-server. + */ + @Transactional + public void saveSourceTypesFromCatalogServer(List catalogSourceTypes) { + + for (CatalogSourceType catalogSourceType : catalogSourceTypes) { + + SourceType sourceType = catalogSourceTypeMapper + .catalogSourceTypeToSourceType(catalogSourceType); + + if (sourceType.getProducer() == null) { + log.warn("Catalog source-type {} does not have a vendor. " + + "Skipping importing this type", catalogSourceType.getName()); + continue; + } + + if (sourceType.getModel() == null) { + log.warn("Catalog source-type {} does not have a model. " + + "Skipping importing this type", catalogSourceType.getName()); + continue; + } + + if (sourceType.getCatalogVersion() == null) { + log.warn("Catalog source-type {} does not have a version. " + + "Skipping importing this type", catalogSourceType.getName()); + continue; + } + // check whether a source-type is already available with given config + if (!sourceTypeRepository.findOneWithEagerRelationshipsByProducerAndModelAndVersion( + sourceType.getProducer(), sourceType.getModel(), + sourceType.getCatalogVersion()).isPresent()) { + // create new source-type + sourceTypeRepository.save(sourceType); + + // create source-data for the new source-type + for (CatalogSourceData catalogSourceData : catalogSourceType.getData()) { + SourceData sourceData = catalogSourceDataMapper + .catalogSourceDataToSourceData(catalogSourceData); + // sourceDataName should be unique + // generated by combining sourceDataType and source-type configs + sourceData.sourceDataName(sourceType.getProducer() + + "_" + sourceType.getModel() + + "_" + sourceType.getCatalogVersion() + + "_" + sourceData.getSourceDataType()); + sourceData.sourceType(sourceType); + sourceDataRepository.save(sourceData); + } + } else { + // skip for existing source-types + log.info("Source-type {} is already available ", sourceType.getProducer() + + "_" + sourceType.getModel() + + "_" + sourceType.getCatalogVersion()); + } + } + log.info("Completed source-type import from catalog-server"); + } } diff --git a/src/main/java/org/radarcns/management/domain/MetaToken.java b/src/main/java/org/radarcns/management/domain/MetaToken.java new file mode 100644 index 000000000..8b0f4dc79 --- /dev/null +++ b/src/main/java/org/radarcns/management/domain/MetaToken.java @@ -0,0 +1,171 @@ +package org.radarcns.management.domain; + +import java.time.Instant; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EntityListeners; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +import org.apache.commons.lang3.RandomStringUtils; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.envers.Audited; +import org.radarcns.auth.config.Constants; +import org.radarcns.management.domain.support.AbstractEntityListener; + +@Entity +@Audited +@Table(name = "radar_meta_token") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +@EntityListeners({AbstractEntityListener.class}) +public class MetaToken extends AbstractEntity { + + + //https://math.stackexchange.com/questions/889538/ + // probability-of-collision-with-randomly-generated-id + // Current length of tokenName is 12. If we think there might be collision we can increase + // the length. + private static final int SHORT_ID_LENGTH = 12; + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") + @SequenceGenerator(name = "sequenceGenerator", initialValue = 1000) + private Long id; + + @NotNull + @Pattern(regexp = Constants.TOKEN_NAME_REGEX) + @Column(name = "token_name", nullable = false, unique = true) + private String tokenName; + + @Column(name = "token", length = 2000) + private String token; + + @Column(name = "fetched", nullable = false) + private Boolean fetched; + + @Column(name = "expiry_date") + private Instant expiryDate = null; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "subject_id") + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + private Subject subject; + + @Column(name = "client_id", nullable = false) + private String clientId; + + /** + * Meta token constructor. + * Must generate a random string as the tokenName. + */ + public MetaToken() { + this.tokenName = RandomStringUtils.randomAlphanumeric(SHORT_ID_LENGTH); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTokenName() { + return tokenName; + } + + public MetaToken tokenName(String tokenName) { + this.tokenName = tokenName; + return this; + } + + public String getToken() { + return token; + } + + public MetaToken token(String token) { + this.token = token; + return this; + } + + public boolean isFetched() { + return fetched; + } + + public MetaToken fetched(boolean fetched) { + this.fetched = fetched; + return this; + } + + public Instant getExpiryDate() { + return expiryDate; + } + + public MetaToken expiryDate(Instant expiryDate) { + this.expiryDate = expiryDate; + return this; + } + + public Subject getSubject() { + return subject; + } + + public MetaToken subject(Subject subject) { + this.subject = subject; + return this; + } + + public String getClientId() { + return clientId; + } + + public MetaToken clientId(String clientId) { + this.clientId = clientId; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MetaToken metaToken = (MetaToken) o; + return Objects.equals(id, metaToken.id) + && Objects.equals(tokenName, metaToken.tokenName) + && Objects.equals(token, metaToken.token) + && Objects.equals(fetched, metaToken.fetched) + && Objects.equals(expiryDate, metaToken.expiryDate) + && Objects.equals(clientId, metaToken.clientId) + && Objects.equals(subject, metaToken.subject); + } + + @Override + public int hashCode() { + + return Objects.hash(id, tokenName, token, fetched, expiryDate, subject, clientId); + } + + @Override + public String toString() { + return "MetaToken{" + "id=" + id + + ", tokenName='" + tokenName + + ", token='" + token + + ", fetched=" + fetched + + ", expiryDate=" + expiryDate + + ", subject=" + subject + + ", clientId=" + clientId + '}'; + } +} diff --git a/src/main/java/org/radarcns/management/domain/Subject.java b/src/main/java/org/radarcns/management/domain/Subject.java index eb3882982..f48ef2490 100644 --- a/src/main/java/org/radarcns/management/domain/Subject.java +++ b/src/main/java/org/radarcns/management/domain/Subject.java @@ -1,5 +1,7 @@ package org.radarcns.management.domain; +import static org.radarcns.auth.authorization.AuthoritiesConstants.PARTICIPANT; + import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.Cascade; @@ -28,6 +30,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; /** @@ -155,6 +158,21 @@ public void setAttributes(Map attributes) { this.attributes = attributes; } + /** + * Gets the active project of subject. + * + *

There can be only one role with PARTICIPANT authority + * and the project that is related to that role is the active role.

+ * + * @return {@link Project} currently active project of subject. + */ + public Optional getActiveProject() { + return this.getUser().getRoles().stream() + .filter(r -> r.getAuthority().getName().equals(PARTICIPANT)) + .findFirst() + .map(Role::getProject); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/src/main/java/org/radarcns/management/repository/MetaTokenRepository.java b/src/main/java/org/radarcns/management/repository/MetaTokenRepository.java new file mode 100644 index 000000000..34f5e2a3c --- /dev/null +++ b/src/main/java/org/radarcns/management/repository/MetaTokenRepository.java @@ -0,0 +1,25 @@ +package org.radarcns.management.repository; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import org.radarcns.management.domain.MetaToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.history.RevisionRepository; +import org.springframework.data.repository.query.Param; + +/** + * Spring Data JPA repository for the MetaToken entity. + */ +public interface MetaTokenRepository extends JpaRepository, + RevisionRepository { + + Optional findOneByTokenName(String tokenName); + + @Query("select metaToken from MetaToken metaToken " + + "where metaToken.fetched =:fetched or metaToken.expiryDate < :time") + List findAllByFetchedOrExpired(@Param("fetched") Boolean fetched, + @Param("time")Instant time); +} diff --git a/src/main/java/org/radarcns/management/security/jwt/AssymmetricJwtAlgorithm.java b/src/main/java/org/radarcns/management/security/jwt/AssymmetricJwtAlgorithm.java new file mode 100644 index 000000000..e4cd403c4 --- /dev/null +++ b/src/main/java/org/radarcns/management/security/jwt/AssymmetricJwtAlgorithm.java @@ -0,0 +1,39 @@ +package org.radarcns.management.security.jwt; + +import java.security.KeyPair; +import org.springframework.security.crypto.codec.Base64; +import org.springframework.security.jwt.crypto.sign.SignatureVerifier; +import org.springframework.security.jwt.crypto.sign.Signer; + +public abstract class AssymmetricJwtAlgorithm implements JwtAlgorithm { + protected final KeyPair keyPair; + private final String algorithmName; + + protected AssymmetricJwtAlgorithm(KeyPair keyPair, String algorithmName) { + this.keyPair = keyPair; + this.algorithmName = algorithmName; + } + + @Override + public Signer getSigner() { + return new AsymmetricKeySigner(keyPair.getPrivate(), algorithmName); + } + + @Override + public SignatureVerifier getVerifier() { + return new AsymmetricKeyVerifier(keyPair.getPublic(), algorithmName); + } + + /** Header used for encoding public keys. */ + protected abstract String getEncodedStringHeader(); + + /** Footer used for encoding public keys. */ + protected abstract String getEncodedStringFooter(); + + @Override + public String getEncodedString() { + return getEncodedStringHeader() + '\n' + + new String(Base64.encode(keyPair.getPublic().getEncoded())) + + '\n' + getEncodedStringFooter(); + } +} diff --git a/src/main/java/org/radarcns/management/security/jwt/EcdsaSigner.java b/src/main/java/org/radarcns/management/security/jwt/AsymmetricKeySigner.java similarity index 63% rename from src/main/java/org/radarcns/management/security/jwt/EcdsaSigner.java rename to src/main/java/org/radarcns/management/security/jwt/AsymmetricKeySigner.java index ede43ff17..ab8c87d89 100644 --- a/src/main/java/org/radarcns/management/security/jwt/EcdsaSigner.java +++ b/src/main/java/org/radarcns/management/security/jwt/AsymmetricKeySigner.java @@ -1,25 +1,18 @@ package org.radarcns.management.security.jwt; -import org.springframework.security.jwt.crypto.sign.Signer; - import java.security.GeneralSecurityException; +import java.security.PrivateKey; import java.security.Signature; -import java.security.interfaces.ECPrivateKey; +import org.springframework.security.jwt.crypto.sign.Signer; /** - * Class that creates ECDSA signatures for use in Spring Security. + * Class that creates signatures from asymmetric keys for use in Spring Security. */ -public class EcdsaSigner implements Signer { - - public static final String DEFAULT_ALGORITHM = "SHA256withECDSA"; - private final ECPrivateKey privateKey; +public class AsymmetricKeySigner implements Signer { + private final PrivateKey privateKey; private final String algorithm; - public EcdsaSigner(ECPrivateKey privateKey) { - this(privateKey, DEFAULT_ALGORITHM); - } - - public EcdsaSigner(ECPrivateKey privateKey, String signingAlgorithm) { + public AsymmetricKeySigner(PrivateKey privateKey, String signingAlgorithm) { this.privateKey = privateKey; this.algorithm = signingAlgorithm; } diff --git a/src/main/java/org/radarcns/management/security/jwt/EcdsaVerifier.java b/src/main/java/org/radarcns/management/security/jwt/AsymmetricKeyVerifier.java similarity index 68% rename from src/main/java/org/radarcns/management/security/jwt/EcdsaVerifier.java rename to src/main/java/org/radarcns/management/security/jwt/AsymmetricKeyVerifier.java index 41e562c7c..b9154e17b 100644 --- a/src/main/java/org/radarcns/management/security/jwt/EcdsaVerifier.java +++ b/src/main/java/org/radarcns/management/security/jwt/AsymmetricKeyVerifier.java @@ -1,22 +1,20 @@ package org.radarcns.management.security.jwt; -import org.springframework.security.jwt.crypto.sign.InvalidSignatureException; -import org.springframework.security.jwt.crypto.sign.SignatureVerifier; - import java.security.GeneralSecurityException; +import java.security.PublicKey; import java.security.Signature; -import java.security.interfaces.ECPublicKey; +import org.springframework.security.jwt.crypto.sign.InvalidSignatureException; +import org.springframework.security.jwt.crypto.sign.SignatureVerifier; -public class EcdsaVerifier implements SignatureVerifier { +/** + * Class that verifies signatures from asymmetric keys for use in Spring Security. + */ +public class AsymmetricKeyVerifier implements SignatureVerifier { - private final ECPublicKey publicKey; + private final PublicKey publicKey; private final String algorithm; - public EcdsaVerifier(ECPublicKey publicKey) { - this(publicKey, EcdsaSigner.DEFAULT_ALGORITHM); - } - - public EcdsaVerifier(ECPublicKey publicKey, String algorithm) { + public AsymmetricKeyVerifier(PublicKey publicKey, String algorithm) { this.publicKey = publicKey; this.algorithm = algorithm; } @@ -29,7 +27,7 @@ public void verify(byte[] content, byte[] sig) { signature.update(content); if (!signature.verify(sig)) { - throw new InvalidSignatureException("EC Signature did not match content"); + throw new InvalidSignatureException("Signature did not match content"); } } catch (GeneralSecurityException ex) { throw new SignatureException("An error occured verifying the signature", ex); diff --git a/src/main/java/org/radarcns/management/security/jwt/EcdsaJwtAlgorithm.java b/src/main/java/org/radarcns/management/security/jwt/EcdsaJwtAlgorithm.java new file mode 100644 index 000000000..320db9366 --- /dev/null +++ b/src/main/java/org/radarcns/management/security/jwt/EcdsaJwtAlgorithm.java @@ -0,0 +1,34 @@ +package org.radarcns.management.security.jwt; + +import com.auth0.jwt.algorithms.Algorithm; +import java.security.KeyPair; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; + +public class EcdsaJwtAlgorithm extends AssymmetricJwtAlgorithm { + /** ECDSA JWT algorithm. */ + public EcdsaJwtAlgorithm(KeyPair keyPair) { + super(keyPair, "SHA256withECDSA"); + if (!(keyPair.getPrivate() instanceof ECPrivateKey)) { + throw new IllegalArgumentException( + "Cannot make EcdsaJwtAlgorithm with " + keyPair.getPrivate().getClass()); + } + } + + @Override + public Algorithm getAlgorithm() { + return Algorithm.ECDSA256( + (ECPublicKey)keyPair.getPublic(), + (ECPrivateKey)keyPair.getPrivate()); + } + + @Override + public String getEncodedStringHeader() { + return "-----BEGIN EC PUBLIC KEY-----"; + } + + @Override + public String getEncodedStringFooter() { + return "-----END EC PUBLIC KEY-----"; + } +} diff --git a/src/main/java/org/radarcns/management/security/jwt/JwtAlgorithm.java b/src/main/java/org/radarcns/management/security/jwt/JwtAlgorithm.java new file mode 100644 index 000000000..2670dffa3 --- /dev/null +++ b/src/main/java/org/radarcns/management/security/jwt/JwtAlgorithm.java @@ -0,0 +1,30 @@ +package org.radarcns.management.security.jwt; + +import com.auth0.jwt.algorithms.Algorithm; +import org.springframework.security.jwt.crypto.sign.SignatureVerifier; +import org.springframework.security.jwt.crypto.sign.Signer; + +/** + * Encodes a signing and verification algorithm for JWT. + */ +public interface JwtAlgorithm { + /** + * Signer to sign JWTs with. + */ + Signer getSigner(); + + /** + * Verifier to verify JWTs with. + */ + SignatureVerifier getVerifier(); + + /** + * Auth0 Algorithm used in JWTs. + */ + Algorithm getAlgorithm(); + + /** + * Encoded public key for storage or transmission. + */ + String getEncodedString(); +} diff --git a/src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java b/src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java index bc47624b5..6c8c20a2e 100644 --- a/src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java +++ b/src/main/java/org/radarcns/management/security/jwt/RadarJwtAccessTokenConverter.java @@ -3,25 +3,21 @@ import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.algorithms.Algorithm; -import org.springframework.security.crypto.codec.Base64; -import org.springframework.security.jwt.crypto.sign.RsaSigner; -import org.springframework.security.jwt.crypto.sign.RsaVerifier; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.provider.OAuth2Authentication; -import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter; -import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; -import org.springframework.util.Assert; - import java.security.KeyPair; import java.security.PrivateKey; import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.Map; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; /** * Customized implementation of {@link JwtAccessTokenConverter} for the RADAR-base platform. @@ -30,6 +26,8 @@ * are significantly smaller than RSA signatures.

*/ public class RadarJwtAccessTokenConverter extends JwtAccessTokenConverter { + private static final Logger logger = LoggerFactory + .getLogger(RadarJwtAccessTokenConverter.class); private Algorithm algorithm; @@ -46,66 +44,70 @@ public RadarJwtAccessTokenConverter() { @Override public void setKeyPair(KeyPair keyPair) { - PrivateKey privateKey = keyPair.getPrivate(); - Assert.state(privateKey instanceof RSAPrivateKey || privateKey instanceof ECPrivateKey, - "KeyPair must be an RSA or EC keypair"); - if (privateKey instanceof ECPrivateKey) { - algorithm = Algorithm.ECDSA256((ECPublicKey) keyPair.getPublic(), - (ECPrivateKey) keyPair.getPrivate()); - setSigner(new EcdsaSigner((ECPrivateKey) privateKey)); - ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); - setVerifier(new EcdsaVerifier(publicKey)); - setVerifierKey("-----BEGIN EC PUBLIC KEY-----\n" - + new String(Base64.encode(publicKey.getEncoded())) - + "\n-----END EC PUBLIC KEY-----"); - } else { - algorithm = Algorithm.RSA256((RSAPublicKey) keyPair.getPublic(), - (RSAPrivateKey) keyPair.getPrivate()); - setSigner(new RsaSigner((RSAPrivateKey) privateKey)); - RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); - setVerifier(new RsaVerifier(publicKey)); - setVerifierKey("-----BEGIN PUBLIC KEY-----\n" - + new String(Base64.encode(publicKey.getEncoded())) - + "\n-----END PUBLIC KEY-----"); + JwtAlgorithm alg = getJwtAlgorithm(keyPair); + if (alg == null) { + throw new IllegalArgumentException("KeyPair type " + + keyPair.getPrivate().getAlgorithm() + " is unknown."); } + algorithm = alg.getAlgorithm(); + setSigner(alg.getSigner()); + setVerifier(alg.getVerifier()); + setVerifierKey(alg.getEncodedString()); } + @Override protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { // we need to override the encode method as well, Spring security does not know about // ECDSA so it can not set the 'alg' header claim of the JWT to the correct value; here // we use the auth0 JWT implementation to create a signed, encoded JWT. - Map claims = getAccessTokenConverter().convertAccessToken(accessToken, - authentication); + Map claims = getAccessTokenConverter() + .convertAccessToken(accessToken, authentication); // create a builder and add the JWT defined claims JWTCreator.Builder builder = JWT.create(); // add the string array claims - Arrays.asList("aud", "sources", "roles", "authorities", "scope").stream() + Stream.of("aud", "sources", "roles", "authorities", "scope") .filter(claims::containsKey) - .forEach(claim -> - builder.withArrayClaim(claim, - ((Collection) claims.get(claim)).toArray(new String[] {}))); + .forEach(claim -> builder.withArrayClaim(claim, + ((Collection) claims.get(claim)).toArray(new String[0]))); // add the string claims - Arrays.asList("sub", "iss", "user_name", "client_id", "grant_type", "jti", "ati").stream() + Stream.of("sub", "iss", "user_name", "client_id", "grant_type", "jti", "ati") .filter(claims::containsKey) .forEach(claim -> builder.withClaim(claim, (String) claims.get(claim))); // add the date claims, they are in seconds since epoch, we need milliseconds - Arrays.asList("exp", "iat").stream() + Stream.of("exp", "iat") .filter(claims::containsKey) .forEach(claim -> builder.withClaim(claim, new Date( ((Long) claims.get(claim)) * 1000))); - String token = builder.sign(algorithm); - return token; + return builder.sign(algorithm); + } + + /** + * Get the JWT algorithm to sign or verify JWTs with. + * @param keyPair key pair for signing/verifying. + * @return algorithm or {@code null} if the key type is unknown. + */ + public static @Nullable JwtAlgorithm getJwtAlgorithm(KeyPair keyPair) { + PrivateKey privateKey = keyPair.getPrivate(); + + if (privateKey instanceof ECPrivateKey) { + return new EcdsaJwtAlgorithm(keyPair); + } else if (privateKey instanceof RSAPrivateKey) { + return new RsaJwtAlgorithm(keyPair); + } else { + logger.warn("No JWT algorithm found for key type {}", privateKey.getClass()); + return null; + } } @Override public boolean isPublic() { + String alg = getKey().getOrDefault("alg", ""); // the signer is private in our superclass, but we can check the algorithm with getKey() - return getKey().getOrDefault("alg", "").equals("SHA256withECDSA") - || getKey().getOrDefault("alg", "").equals("SHA256withRSA"); + return alg.equals("SHA256withECDSA") || alg.equals("SHA256withRSA"); } } diff --git a/src/main/java/org/radarcns/management/security/jwt/RadarKeyStoreKeyFactory.java b/src/main/java/org/radarcns/management/security/jwt/RadarKeyStoreKeyFactory.java index 8cfc7412d..48758ea7b 100644 --- a/src/main/java/org/radarcns/management/security/jwt/RadarKeyStoreKeyFactory.java +++ b/src/main/java/org/radarcns/management/security/jwt/RadarKeyStoreKeyFactory.java @@ -1,11 +1,13 @@ package org.radarcns.management.security.jwt; -import org.springframework.core.io.Resource; - import java.security.KeyPair; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; +import java.util.Collection; +import java.util.Objects; +import java.util.stream.Stream; +import org.springframework.core.io.Resource; /** * Similar to Spring's @@ -30,6 +32,9 @@ public RadarKeyStoreKeyFactory(Resource resource, char[] password) { * Get a keypair from the store using the store password. * @param alias the keypair alias * @return the keypair + * @throws IllegalStateException if the keys cannot be loaded, e.g. the alias does not exist, + * the configured password is invalid or the store file cannot be + * loaded. */ public KeyPair getKeyPair(String alias) { return getKeyPair(alias, password); @@ -40,22 +45,40 @@ public KeyPair getKeyPair(String alias) { * @param alias the keypair alias * @param password the keypair password * @return the keypair + * @throws IllegalStateException if the keys cannot be loaded, e.g. the alias does not exist, + * the password is invalid or the store file cannot be loaded. */ public KeyPair getKeyPair(String alias, char[] password) { try { + KeyStore localStore; synchronized (lock) { if (store == null) { - synchronized (lock) { - store = KeyStore.getInstance("jks"); - store.load(resource.getInputStream(), this.password); - } + store = KeyStore.getInstance("jks"); + store.load(resource.getInputStream(), this.password); } + localStore = store; } - PrivateKey key = (PrivateKey) store.getKey(alias, password); - PublicKey publicKey = store.getCertificate(alias).getPublicKey(); + PrivateKey key = (PrivateKey) localStore.getKey(alias, password); + PublicKey publicKey = localStore.getCertificate(alias).getPublicKey(); return new KeyPair(publicKey, key); } catch (Exception e) { throw new IllegalStateException("Cannot load keys from store: " + resource, e); } } + + /** + * Stream JwtAlgorithm for given keystore key pair aliases. + * Unknown algorithms will be excluded from output. + * @param aliases key aliases, possibly null. + * @return stream of JwtAlgorithm objects matching given aliases. + */ + public Stream streamJwtAlgorithm(Collection aliases) { + if (aliases == null) { + return Stream.empty(); + } + return aliases.stream() + .map(this::getKeyPair) + .map(RadarJwtAccessTokenConverter::getJwtAlgorithm) + .filter(Objects::nonNull); + } } diff --git a/src/main/java/org/radarcns/management/security/jwt/RsaJwtAlgorithm.java b/src/main/java/org/radarcns/management/security/jwt/RsaJwtAlgorithm.java new file mode 100644 index 000000000..7e4325f6f --- /dev/null +++ b/src/main/java/org/radarcns/management/security/jwt/RsaJwtAlgorithm.java @@ -0,0 +1,34 @@ +package org.radarcns.management.security.jwt; + +import com.auth0.jwt.algorithms.Algorithm; +import java.security.KeyPair; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +public class RsaJwtAlgorithm extends AssymmetricJwtAlgorithm { + /** RSA JWT algorithm. */ + public RsaJwtAlgorithm(KeyPair keyPair) { + super(keyPair, "SHA256withRSA"); + if (!(keyPair.getPrivate() instanceof RSAPrivateKey)) { + throw new IllegalArgumentException( + "Cannot make EcdsaJwtAlgorithm with " + keyPair.getPrivate().getClass()); + } + } + + @Override + public Algorithm getAlgorithm() { + return Algorithm.RSA256( + (RSAPublicKey)keyPair.getPublic(), + (RSAPrivateKey)keyPair.getPrivate()); + } + + @Override + public String getEncodedStringHeader() { + return "-----BEGIN PUBLIC KEY-----"; + } + + @Override + public String getEncodedStringFooter() { + return "-----END PUBLIC KEY-----"; + } +} diff --git a/src/main/java/org/radarcns/management/service/MailService.java b/src/main/java/org/radarcns/management/service/MailService.java index fd5e1dadd..d79ed4d2a 100644 --- a/src/main/java/org/radarcns/management/service/MailService.java +++ b/src/main/java/org/radarcns/management/service/MailService.java @@ -2,6 +2,7 @@ import java.util.Locale; import javax.mail.internet.MimeMessage; + import org.apache.commons.lang3.CharEncoding; import org.radarcns.management.config.ManagementPortalProperties; import org.radarcns.management.domain.User; @@ -81,7 +82,8 @@ public void sendActivationEmail(User user) { Locale locale = Locale.forLanguageTag(user.getLangKey()); Context context = new Context(locale); context.setVariable(USER, user); - context.setVariable(BASE_URL, managementPortalProperties.getMail().getBaseUrl()); + context.setVariable(BASE_URL, + managementPortalProperties.getCommon().getManagementPortalBaseUrl()); String content = templateEngine.process("activationEmail", context); String subject = messageSource.getMessage("email.activation.title", null, locale); sendEmail(user.getEmail(), subject, content, false, true); @@ -97,7 +99,8 @@ public void sendCreationEmail(User user) { Locale locale = Locale.forLanguageTag(user.getLangKey()); Context context = new Context(locale); context.setVariable(USER, user); - context.setVariable(BASE_URL, managementPortalProperties.getMail().getBaseUrl()); + context.setVariable(BASE_URL, + managementPortalProperties.getCommon().getManagementPortalBaseUrl()); String content = templateEngine.process("creationEmail", context); String subject = messageSource.getMessage("email.activation.title", null, locale); sendEmail(user.getEmail(), subject, content, false, true); @@ -114,7 +117,8 @@ public void sendCreationEmailForGivenEmail(User user, String email) { Locale locale = Locale.forLanguageTag(user.getLangKey()); Context context = new Context(locale); context.setVariable(USER, user); - context.setVariable(BASE_URL, managementPortalProperties.getMail().getBaseUrl()); + context.setVariable(BASE_URL, + managementPortalProperties.getCommon().getManagementPortalBaseUrl()); String content = templateEngine.process("creationEmail", context); String subject = messageSource.getMessage("email.activation.title", null, locale); sendEmail(email, subject, content, false, true); @@ -130,7 +134,8 @@ public void sendPasswordResetMail(User user) { Locale locale = Locale.forLanguageTag(user.getLangKey()); Context context = new Context(locale); context.setVariable(USER, user); - context.setVariable(BASE_URL, managementPortalProperties.getMail().getBaseUrl()); + context.setVariable(BASE_URL, + managementPortalProperties.getCommon().getManagementPortalBaseUrl()); String content = templateEngine.process("passwordResetEmail", context); String subject = messageSource.getMessage("email.reset.title", null, locale); sendEmail(user.getEmail(), subject, content, false, true); diff --git a/src/main/java/org/radarcns/management/service/MetaTokenService.java b/src/main/java/org/radarcns/management/service/MetaTokenService.java new file mode 100644 index 000000000..ab02fd0f4 --- /dev/null +++ b/src/main/java/org/radarcns/management/service/MetaTokenService.java @@ -0,0 +1,142 @@ +package org.radarcns.management.service; + +import org.radarcns.management.config.ManagementPortalProperties; +import org.radarcns.management.domain.MetaToken; +import org.radarcns.management.domain.Subject; +import org.radarcns.management.repository.MetaTokenRepository; +import org.radarcns.management.service.dto.TokenDTO; +import org.radarcns.management.web.rest.errors.ErrorConstants; +import org.radarcns.management.web.rest.errors.NotFoundException; +import org.radarcns.management.web.rest.errors.RequestGoneException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.validation.ConstraintViolationException; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Instant; +import java.util.Collections; +import java.util.Optional; + +import static org.radarcns.management.web.rest.errors.EntityName.META_TOKEN; + +/** + * Created by nivethika. + * + *

Service to delegate MetaToken handling.

+ * + */ +@Service +@Transactional +public class MetaTokenService { + + private final Logger log = LoggerFactory.getLogger(MetaTokenService.class); + + @Autowired + private MetaTokenRepository metaTokenRepository; + + @Autowired + private ManagementPortalProperties managementPortalProperties; + + @Autowired + private SubjectService subjectService; + + /** + * Save a metaToken. + * + * @param metaToken the entity to save + * @return the persisted entity + */ + public MetaToken save(MetaToken metaToken) { + log.debug("Request to save MetaToken : {}", metaToken); + return metaTokenRepository.save(metaToken); + } + + /** + * Get one project by id. + * + * @param tokenName the id of the entity + * @return the entity + */ + public TokenDTO fetchToken(String tokenName) throws MalformedURLException { + log.debug("Request to get Token : {}", tokenName); + MetaToken fetchedToken = getToken(tokenName); + // process the response if the token is not fetched or not expired + if (!fetchedToken.isFetched() && Instant.now().isBefore(fetchedToken.getExpiryDate())) { + // create response + TokenDTO result = new TokenDTO(fetchedToken.getToken(), + new URL(managementPortalProperties.getCommon().getBaseUrl()), + subjectService.getPrivacyPolicyUrl(fetchedToken.getSubject())); + + // change fetched status to true. + fetchedToken.fetched(true); + save(fetchedToken); + return result; + } else { + throw new RequestGoneException("Token already fetched or expired. ", + META_TOKEN, "error.TokenCannotBeSent"); + } + } + + /** + * Gets a token from databased using the tokenName. + * + * @param tokenName tokenName. + * @return fetched token as {@link MetaToken}. + */ + @Transactional(readOnly = true) + public MetaToken getToken(String tokenName) { + Optional fetchedToken = metaTokenRepository.findOneByTokenName(tokenName); + + if (fetchedToken.isPresent()) { + return fetchedToken.get(); + } else { + throw new NotFoundException("Meta token not found with tokenName", META_TOKEN, + ErrorConstants.ERR_TOKEN_NOT_FOUND, + Collections.singletonMap("tokenName", tokenName)); + } + } + + /** + * Saves a unique meta-token instance, by checking for token-name collision. + * If a collision is detection, we try to save the token with a new tokenName + * @return an unique token + */ + public MetaToken saveUniqueToken(Subject subject, String clientId, String token, Boolean + fetched, Instant expiryTime ) { + MetaToken metaToken = new MetaToken() + .token(token) + .fetched(fetched) + .expiryDate(expiryTime) + .subject(subject) + .clientId(clientId); + + try { + return metaTokenRepository.save(metaToken); + } catch (ConstraintViolationException e) { + log.warn("Unique constraint violation catched... Trying to save with new tokenName"); + return saveUniqueToken(subject, clientId, token, fetched, expiryTime); + } + + } + + /** + * Expired and fetched tokens are deleted after 1 month. + *

This is scheduled to get triggered first day of the month.

+ */ + @Scheduled(cron = "0 0 0 1 * ?") + public void removeStaleTokens() { + log.info("Scheduled scan for expired and fetched meta-tokens starting now"); + + metaTokenRepository.findAllByFetchedOrExpired(true, Instant.now()) + .forEach(metaToken -> { + log.info("Deleting deleting expired or fetched token {}", + metaToken.getTokenName()); + metaTokenRepository.delete(metaToken); + }); + } +} diff --git a/src/main/java/org/radarcns/management/service/OAuthClientService.java b/src/main/java/org/radarcns/management/service/OAuthClientService.java new file mode 100644 index 000000000..2539c32ef --- /dev/null +++ b/src/main/java/org/radarcns/management/service/OAuthClientService.java @@ -0,0 +1,273 @@ +package org.radarcns.management.service; + +import static org.radarcns.management.web.rest.MetaTokenResource.DEFAULT_META_TOKEN_TIMEOUT; +import static org.radarcns.management.web.rest.errors.EntityName.OAUTH_CLIENT; +import static org.springframework.security.oauth2.common.util.OAuth2Utils.GRANT_TYPE; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.radarcns.management.config.ManagementPortalProperties; +import org.radarcns.management.domain.MetaToken; +import org.radarcns.management.domain.Subject; +import org.radarcns.management.domain.User; +import org.radarcns.management.service.dto.ClientDetailsDTO; +import org.radarcns.management.service.dto.ClientPairInfoDTO; +import org.radarcns.management.service.mapper.ClientDetailsMapper; +import org.radarcns.management.web.rest.errors.ConflictException; +import org.radarcns.management.web.rest.errors.ErrorConstants; +import org.radarcns.management.web.rest.errors.InvalidRequestException; +import org.radarcns.management.web.rest.errors.InvalidStateException; +import org.radarcns.management.web.rest.errors.NotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.NoSuchClientException; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; +import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; +import org.springframework.stereotype.Service; + +/** + * The service layer to handle OAuthClient and Token related functions. + * Created by nivethika on 03/08/2018. + */ +@Service +public class OAuthClientService { + + private final Logger log = LoggerFactory.getLogger(OAuthClientService.class); + + private static final String PROTECTED_KEY = "protected"; + + @Autowired + private JdbcClientDetailsService clientDetailsService; + + @Autowired + private ClientDetailsMapper clientDetailsMapper; + + @Autowired + private MetaTokenService metaTokenService; + + @Autowired + private ManagementPortalProperties managementPortalProperties; + + @Autowired + private AuthorizationServerEndpointsConfiguration authorizationServerEndpointsConfiguration; + + public List findAllOAuthClients() { + return clientDetailsService.listClientDetails(); + } + + /** + * Find ClientDetails by OAuth client id. + * + * @param clientId The client ID to look up + * @return a ClientDetails object with the requested client ID + * @throws NotFoundException If there is no client with the requested ID + */ + public ClientDetails findOneByClientId(String clientId) { + try { + return clientDetailsService.loadClientByClientId(clientId); + } catch (NoSuchClientException e) { + log.error("Pair client request for unknown client id: {}", clientId); + Map errorParams = new HashMap<>(); + errorParams.put("clientId", clientId); + throw new NotFoundException("Client not found for client-id", OAUTH_CLIENT, + ErrorConstants.ERR_OAUTH_CLIENT_ID_NOT_FOUND, errorParams); + } + } + + /** + * Update Oauth-client with new information. + * + * @param clientDetailsDto information to update. + * @return Updated {@link ClientDetails} instance. + */ + public ClientDetails updateOauthClient(ClientDetailsDTO clientDetailsDto) { + ClientDetails details = + clientDetailsMapper.clientDetailsDTOToClientDetails(clientDetailsDto); + // update client. + clientDetailsService.updateClientDetails(details); + + ClientDetails updated = findOneByClientId(clientDetailsDto.getClientId()); + // updateClientDetails does not update secret, so check for it separately + if (clientDetailsDto.getClientSecret() != null && !clientDetailsDto.getClientSecret() + .equals(updated.getClientSecret())) { + clientDetailsService.updateClientSecret(clientDetailsDto.getClientId(), + clientDetailsDto.getClientSecret()); + } + return findOneByClientId(clientDetailsDto.getClientId()); + } + + /** + * Checks whether a client is a protected client. + * + * @param details ClientDetails. + */ + public static void checkProtected(ClientDetails details) { + Map info = details.getAdditionalInformation(); + if (Objects.nonNull(info) && info.containsKey(PROTECTED_KEY) && info.get(PROTECTED_KEY) + .toString().equalsIgnoreCase("true")) { + throw new InvalidRequestException("Cannot modify protected client", OAUTH_CLIENT, + ErrorConstants.ERR_OAUTH_CLIENT_PROTECTED, + Collections.singletonMap("client_id", details.getClientId())); + } + } + + /** + * Deletes an oauth client. + * @param clientId of the auth-client to delete. + */ + public void deleteClientDetails(String clientId) { + clientDetailsService.removeClientDetails(clientId); + } + + /** + * Creates new oauth-client. + * + * @param clientDetailsDto data to create oauth-client. + * @return created {@link ClientDetails}. + */ + public ClientDetails createClientDetail(ClientDetailsDTO clientDetailsDto) { + // check if the client id exists + try { + ClientDetails existingClient = + clientDetailsService.loadClientByClientId(clientDetailsDto.getClientId()); + if (existingClient != null) { + throw new ConflictException("OAuth client already exists with this id", + OAUTH_CLIENT, ErrorConstants.ERR_CLIENT_ID_EXISTS, + Collections.singletonMap("client_id", clientDetailsDto.getClientId())); + } + } catch (NoSuchClientException ex) { + // Client does not exist yet, we can go ahead and create it + log.info("No client existing with client-id {}. Proceeding to create new client", + clientDetailsDto.getClientId()); + } + ClientDetails details = + clientDetailsMapper.clientDetailsDTOToClientDetails(clientDetailsDto); + // create oauth client. + clientDetailsService.addClientDetails(details); + + return findOneByClientId(clientDetailsDto.getClientId()); + + } + + /** + * Creates refresh token for oauth-subject pair. + * @param subject to create token for + * @param clientId using which client id + * @return {@link ClientPairInfoDTO} to return. + * @throws URISyntaxException when token URI cannot be formed properly. + * @throws MalformedURLException when token URL cannot be formed properly. + */ + public ClientPairInfoDTO createRefreshToken(Subject subject, String clientId) + throws URISyntaxException, MalformedURLException { + + // add the user's authorities + User user = subject.getUser(); + Set authorities = + user.getAuthorities().stream().map(a -> new SimpleGrantedAuthority(a.getName())) + .collect(Collectors.toSet()); + // lookup the OAuth client + // getOAuthClient checks if the id exists + ClientDetails details = findOneByClientId(clientId); + + OAuth2AccessToken token = + createAuthorizationCodeToken(clientId, user.getLogin(), authorities, + details.getScope(), details.getResourceIds()); + // tokenName should be generated + MetaToken metaToken = metaTokenService + .saveUniqueToken(subject, clientId, token.getRefreshToken().getValue(), false, + Instant.now().plus(getMetaTokenTimeout())); + + if (metaToken.getId() != null && metaToken.getTokenName() != null) { + // get base url from settings + String baseUrl = managementPortalProperties.getCommon().getManagementPortalBaseUrl(); + // create complete uri string + String tokenUrl = baseUrl + ResourceUriService.getUri(metaToken).getPath(); + // create response + return new ClientPairInfoDTO(metaToken.getTokenName(), new URL(tokenUrl)); + } else { + throw new InvalidStateException("Could not create a valid token", OAUTH_CLIENT, + "error.couldNotCreateToken"); + } + } + + + + + /** + * Gets the meta-token timeout from config file. If the config is not mentioned or in wrong + * format, it will return default value. + * + * @return meta-token timeout duration. + */ + private Duration getMetaTokenTimeout() { + + String timeoutConfig = managementPortalProperties.getOauth().getMetaTokenTimeout(); + + if (timeoutConfig.isEmpty()) { + return DEFAULT_META_TOKEN_TIMEOUT; + } + + try { + return Duration.parse(managementPortalProperties.getOauth().getMetaTokenTimeout()); + } catch (DateTimeParseException e) { + // if the token timeout cannot be read, log the error and use the default value. + log.warn("Cannot parse meta-token timeout config. Using default value", e); + return DEFAULT_META_TOKEN_TIMEOUT; + } + } + + /** + * Creates the actual {@link OAuth2AccessToken} token using authorization-code flow. + * + * @param clientId oauth client id. + * @param login subject-id of the token. + * @param authorities authorities to create token. + * @param scope scopes of the token. + * @param resourceIds resource-ids of the token. + * @return Created {@link OAuth2AccessToken} instance. + */ + private OAuth2AccessToken createAuthorizationCodeToken(String clientId, String login, + Set authorities, Set scope, Set resourceIds) { + + Map requestParameters = new HashMap<>(); + requestParameters.put(GRANT_TYPE , "authorization_code"); + + + Set responseTypes = Collections.singleton("code"); + + OAuth2Request oAuth2Request = + new OAuth2Request(requestParameters, clientId, authorities, true, scope, + resourceIds, null, responseTypes, Collections.emptyMap()); + + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(login, null, authorities); + OAuth2Authentication auth = new OAuth2Authentication(oAuth2Request, authenticationToken); + + AuthorizationServerTokenServices tokenServices = + authorizationServerEndpointsConfiguration.getEndpointsConfigurer() + .getTokenServices(); + + return tokenServices.createAccessToken(auth); + } +} diff --git a/src/main/java/org/radarcns/management/service/ProjectService.java b/src/main/java/org/radarcns/management/service/ProjectService.java index 2281c2e42..f9beb0a63 100644 --- a/src/main/java/org/radarcns/management/service/ProjectService.java +++ b/src/main/java/org/radarcns/management/service/ProjectService.java @@ -1,6 +1,8 @@ package org.radarcns.management.service; +import static org.radarcns.management.web.rest.errors.EntityName.PROJECT; + import org.radarcns.management.domain.Project; import org.radarcns.management.domain.SourceType; import org.radarcns.management.repository.ProjectRepository; @@ -8,7 +10,7 @@ import org.radarcns.management.service.dto.SourceTypeDTO; import org.radarcns.management.service.mapper.ProjectMapper; import org.radarcns.management.service.mapper.SourceTypeMapper; -import org.radarcns.management.web.rest.errors.CustomNotFoundException; +import org.radarcns.management.web.rest.errors.NotFoundException; import org.radarcns.management.web.rest.errors.ErrorConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -74,16 +76,15 @@ public Page findAll(Boolean fetchMinimal, Pageable pageable) { * * @param id the id of the entity * @return the entity - * @throws CustomNotFoundException if there is no project with the given id */ @Transactional(readOnly = true) - public ProjectDTO findOne(Long id) throws CustomNotFoundException { + public ProjectDTO findOne(Long id) { log.debug("Request to get Project : {}", id); return projectRepository.findOneWithEagerRelationships(id) .map(projectMapper::projectToProjectDTO) - .orElseThrow(() -> new CustomNotFoundException( + .orElseThrow(() -> new NotFoundException("Project not found with id", PROJECT, ErrorConstants.ERR_PROJECT_ID_NOT_FOUND, - Collections.singletonMap("param0", id.toString()))); + Collections.singletonMap("id", id.toString()))); } /** @@ -91,26 +92,25 @@ public ProjectDTO findOne(Long id) throws CustomNotFoundException { * * @param name the name of the entity * @return the entity - * @throws CustomNotFoundException if there is no project with the given name */ @Transactional(readOnly = true) - public ProjectDTO findOneByName(String name) throws CustomNotFoundException { + public ProjectDTO findOneByName(String name) { log.debug("Request to get Project by name: {}", name); return projectRepository.findOneWithEagerRelationshipsByName(name) .map(projectMapper::projectToProjectDTO) - .orElseThrow(() -> new CustomNotFoundException( - ErrorConstants.ERR_PROJECT_NAME_NOT_FOUND, - Collections.singletonMap("param0", name))); + .orElseThrow(() -> new NotFoundException("Project not found with projectName", + PROJECT, ErrorConstants.ERR_PROJECT_NAME_NOT_FOUND, + Collections.singletonMap("projectName", name))); } /** - * Get one project by id. + * Get source-types assigned to a project. * - * @param id the id of the entity - * @return the entity + * @param id the id of the project + * @return the list of source-types assigned. */ @Transactional(readOnly = true) - public List findSourceTypesById(Long id) { + public List findSourceTypesByProjectId(Long id) { log.debug("Request to get Project.sourceTypes of project: {}", id); List sourceTypes = projectRepository.findSourceTypesByProjectId(id); return sourceTypeMapper.sourceTypesToSourceTypeDTOs(sourceTypes); diff --git a/src/main/java/org/radarcns/management/service/ResourceUriService.java b/src/main/java/org/radarcns/management/service/ResourceUriService.java index 5e2eb5de0..954bcd840 100644 --- a/src/main/java/org/radarcns/management/service/ResourceUriService.java +++ b/src/main/java/org/radarcns/management/service/ResourceUriService.java @@ -1,5 +1,6 @@ package org.radarcns.management.service; +import org.radarcns.management.domain.MetaToken; import org.radarcns.management.domain.Source; import org.radarcns.management.domain.User; import org.radarcns.management.service.dto.ClientDetailsDTO; @@ -121,4 +122,14 @@ public static URI getUri(SourceDataDTO resource) throws URISyntaxException { public static URI getUri(ProjectDTO resource) throws URISyntaxException { return new URI(HeaderUtil.buildPath("api", "projects", resource.getProjectName())); } + + /** + * Get the API location for the given resource. + * @param resource the resource + * @return the API location + * @throws URISyntaxException See {@link URI#URI(String)} + */ + public static URI getUri(MetaToken resource) throws URISyntaxException { + return new URI(HeaderUtil.buildPath("api", "meta-token", resource.getTokenName())); + } } diff --git a/src/main/java/org/radarcns/management/service/RevisionService.java b/src/main/java/org/radarcns/management/service/RevisionService.java index 043c7f0cb..4cf6b691a 100644 --- a/src/main/java/org/radarcns/management/service/RevisionService.java +++ b/src/main/java/org/radarcns/management/service/RevisionService.java @@ -1,5 +1,8 @@ package org.radarcns.management.service; +import static org.radarcns.management.web.rest.errors.EntityName.REVISION; +import static org.radarcns.management.web.rest.errors.ErrorConstants.ERR_REVISIONS_NOT_FOUND; + import org.hibernate.envers.AuditReader; import org.hibernate.envers.AuditReaderFactory; import org.hibernate.envers.RevisionType; @@ -15,9 +18,8 @@ import org.radarcns.management.repository.CustomRevisionEntityRepository; import org.radarcns.management.service.dto.RevisionDTO; import org.radarcns.management.service.dto.RevisionInfoDTO; -import org.radarcns.management.web.rest.errors.CustomNotFoundException; -import org.radarcns.management.web.rest.errors.CustomServerException; -import org.radarcns.management.web.rest.errors.ErrorConstants; +import org.radarcns.management.web.rest.errors.InvalidStateException; +import org.radarcns.management.web.rest.errors.NotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; @@ -112,10 +114,9 @@ public EntityAuditInfo getAuditInfo(AbstractEntity entity) { first = (CustomRevisionEntity) firstRevision[1]; } catch (NonUniqueResultException ex) { // should not happen since we call 'minimize' - throw new CustomServerException(ErrorConstants.ERR_INTERNAL_SERVER_ERROR, - Collections.singletonMap("message", "Query for first revision returned a " + throw new IllegalStateException("Query for first revision returned a " + "non-unique result. Please report this to the administrator together with " - + "the request issued.")); + + "the request issued." , ex); } catch (NoResultException ex) { // we did not find any auditing info, so we just return an empty object return new EntityAuditInfo(); @@ -133,10 +134,9 @@ public EntityAuditInfo getAuditInfo(AbstractEntity entity) { last = (CustomRevisionEntity) lastRevision[1]; } catch (NonUniqueResultException ex) { // should not happen since we call 'maximize' - throw new CustomServerException(ErrorConstants.ERR_INTERNAL_SERVER_ERROR, - Collections.singletonMap("message", "Query for last revision returned a " + throw new IllegalStateException("Query for last revision returned a " + "non-unique result. Please report this to the administrator together " - + "with the request issued.")); + + "with the request issued."); } catch (NoResultException ex) { // we did not find any auditing info, so we just return an empty object return new EntityAuditInfo(); @@ -226,13 +226,14 @@ public Page getRevisionsForEntity(Pageable pageable, AbstractEntity * * @param revision the revision number * @return the revision - * @throws CustomNotFoundException if the revision number does not exist + * @throws NotFoundException if the revision number does not exist */ - public RevisionInfoDTO getRevision(Integer revision) throws CustomNotFoundException { + public RevisionInfoDTO getRevision(Integer revision) throws NotFoundException { CustomRevisionEntity revisionEntity = revisionEntityRepository.findOne(revision); if (revisionEntity == null) { - throw new CustomNotFoundException("Requested revision not found", Collections - .emptyMap()); + throw new NotFoundException("Revision not found with revision id", REVISION, + ERR_REVISIONS_NOT_FOUND, + Collections.singletonMap("revision-id", revision.toString())); } return RevisionInfoDTO.from(revisionEntity, getChangesForRevision(revision)); } @@ -256,8 +257,9 @@ public Map> getChangesForRevision(Integer revision) { revisionTypes.forEach(revisionType -> result.put(revisionType, new LinkedList<>())); CustomRevisionEntity revisionEntity = revisionEntityRepository.findOne(revision); if (revisionEntity == null) { - throw new CustomNotFoundException("The requested revision could not be found.", - Collections.emptyMap()); + throw new NotFoundException("The requested revision could not be found.", REVISION, + ERR_REVISIONS_NOT_FOUND, + Collections.singletonMap("revision-id", revision.toString())); } revisionEntity.getModifiedEntityNames().forEach(entityName -> revisionTypes.forEach(revisionType -> result.get(revisionType).addAll( @@ -339,9 +341,7 @@ private Function addMapperForClass(Class clazz) { mapper = applicationContext.getBean(Class.forName(className)); } catch (ClassNotFoundException ex) { // should not happen, we got the classname from the bean definition - throw new CustomServerException(ErrorConstants.ERR_INTERNAL_SERVER_ERROR, - Collections.singletonMap("message", "Could not get Class from the given " - + "bean classname. Please report this to the administrator.")); + throw new InvalidStateException(ex.getMessage(), REVISION, "error.classNotFound"); } // now we look for the correct method in the bean Optional method = Arrays.stream(mapper.getClass().getMethods()) @@ -376,8 +376,7 @@ private Class classForEntityName(String entityName) { } catch (ClassNotFoundException ex) { // this should not happen log.error("Unable to load class for modified entity", ex); - throw new CustomServerException(ErrorConstants.ERR_INTERNAL_SERVER_ERROR, - Collections.emptyMap()); + throw new InvalidStateException(ex.getMessage(), REVISION, "error.classNotFound"); } } } diff --git a/src/main/java/org/radarcns/management/service/SourceService.java b/src/main/java/org/radarcns/management/service/SourceService.java index 85ec76c6e..4bbe4e1fe 100644 --- a/src/main/java/org/radarcns/management/service/SourceService.java +++ b/src/main/java/org/radarcns/management/service/SourceService.java @@ -1,5 +1,9 @@ package org.radarcns.management.service; + + +import static org.radarcns.management.web.rest.errors.EntityName.SOURCE; + import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -11,12 +15,13 @@ import org.radarcns.management.service.dto.MinimalSourceDetailsDTO; import org.radarcns.management.service.dto.SourceDTO; import org.radarcns.management.service.mapper.SourceMapper; -import org.radarcns.management.web.rest.errors.CustomParameterizedException; +import org.radarcns.management.web.rest.errors.InvalidRequestException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.history.Revision; import org.springframework.data.history.Revisions; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -110,7 +115,7 @@ public Optional findOneById(Long id) { public void delete(Long id) { log.info("Request to delete Source : {}", id); Revisions sourceHistory = sourceRepository.findRevisions(id); - List sources = sourceHistory.getContent().stream().map(p -> (Source) p.getEntity()) + List sources = sourceHistory.getContent().stream().map(Revision::getEntity) .filter(Source::isAssigned).collect(Collectors.toList()); if (sources.isEmpty()) { sourceRepository.delete(id); @@ -118,7 +123,9 @@ public void delete(Long id) { Map errorParams = new HashMap<>(); errorParams.put("message", "Cannot delete source with sourceId "); errorParams.put("id", Long.toString(id)); - throw new CustomParameterizedException("error.usedSourceDeletion", errorParams); + throw new InvalidRequestException("Cannot delete a source that was once " + + "assigned.", SOURCE, "error.usedSourceDeletion", + errorParams); } } diff --git a/src/main/java/org/radarcns/management/service/SourceTypeService.java b/src/main/java/org/radarcns/management/service/SourceTypeService.java index 90bc55e04..4a202a35a 100644 --- a/src/main/java/org/radarcns/management/service/SourceTypeService.java +++ b/src/main/java/org/radarcns/management/service/SourceTypeService.java @@ -1,5 +1,14 @@ package org.radarcns.management.service; +import static org.radarcns.management.web.rest.errors.EntityName.SOURCE_TYPE; +import static org.radarcns.management.web.rest.errors.ErrorConstants.ERR_SOURCE_TYPE_NOT_FOUND; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; +import javax.validation.constraints.NotNull; + import org.radarcns.management.domain.SourceData; import org.radarcns.management.domain.SourceType; import org.radarcns.management.repository.SourceDataRepository; @@ -8,6 +17,7 @@ import org.radarcns.management.service.dto.SourceTypeDTO; import org.radarcns.management.service.mapper.ProjectMapper; import org.radarcns.management.service.mapper.SourceTypeMapper; +import org.radarcns.management.web.rest.errors.NotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -16,11 +26,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - /** * Service Implementation for managing SourceType. */ @@ -99,14 +104,17 @@ public void delete(Long id) { /** * Fetch SourceType by producer and model. */ - public SourceTypeDTO findByProducerAndModelAndVersion(String producer, String model, - String version) { + public SourceTypeDTO findByProducerAndModelAndVersion(@NotNull String producer, + @NotNull String model, @NotNull String version) { log.debug("Request to get SourceType by producer and model and version: {}, {}, {}", producer, model, version); - Optional sourceType = sourceTypeRepository - .findOneWithEagerRelationshipsByProducerAndModelAndVersion(producer, model, - version); - return sourceTypeMapper.sourceTypeToSourceTypeDTO(sourceType.orElse(null)); + return sourceTypeRepository + .findOneWithEagerRelationshipsByProducerAndModelAndVersion(producer, model, version) + .map(sourceTypeMapper::sourceTypeToSourceTypeDTO).orElseThrow( + () -> new NotFoundException( + "SourceType not found with producer, model, " + "version ", SOURCE_TYPE, + ERR_SOURCE_TYPE_NOT_FOUND, Collections.singletonMap("producer-model-version", + producer + "-" + model + "-" + version))); } /** diff --git a/src/main/java/org/radarcns/management/service/SubjectService.java b/src/main/java/org/radarcns/management/service/SubjectService.java index 2e54f1616..899aad724 100644 --- a/src/main/java/org/radarcns/management/service/SubjectService.java +++ b/src/main/java/org/radarcns/management/service/SubjectService.java @@ -1,6 +1,31 @@ package org.radarcns.management.service; +import static org.radarcns.auth.authorization.AuthoritiesConstants.INACTIVE_PARTICIPANT; +import static org.radarcns.auth.authorization.AuthoritiesConstants.PARTICIPANT; +import static org.radarcns.management.service.dto.ProjectDTO.PRIVACY_POLICY_URL; +import static org.radarcns.management.web.rest.errors.EntityName.OAUTH_CLIENT; +import static org.radarcns.management.web.rest.errors.EntityName.SUBJECT; +import static org.radarcns.management.web.rest.errors.ErrorConstants.ERR_NO_VALID_PRIVACY_POLICY_URL_CONFIGURED; +import static org.radarcns.management.web.rest.errors.ErrorConstants.ERR_SOURCE_NOT_FOUND; +import static org.radarcns.management.web.rest.errors.ErrorConstants.ERR_SUBJECT_NOT_FOUND; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + import org.hibernate.envers.query.AuditEntity; +import org.radarcns.management.config.ManagementPortalProperties; import org.radarcns.management.domain.Project; import org.radarcns.management.domain.Role; import org.radarcns.management.domain.Source; @@ -18,10 +43,11 @@ import org.radarcns.management.service.mapper.SourceMapper; import org.radarcns.management.service.mapper.SubjectMapper; import org.radarcns.management.service.util.RandomUtil; -import org.radarcns.management.web.rest.errors.CustomConflictException; -import org.radarcns.management.web.rest.errors.CustomNotFoundException; -import org.radarcns.management.web.rest.errors.CustomParameterizedException; +import org.radarcns.management.web.rest.errors.BadRequestException; +import org.radarcns.management.web.rest.errors.ConflictException; import org.radarcns.management.web.rest.errors.ErrorConstants; +import org.radarcns.management.web.rest.errors.InvalidStateException; +import org.radarcns.management.web.rest.errors.NotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -32,24 +58,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.net.URISyntaxException; -import java.time.ZonedDateTime; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import static org.radarcns.auth.authorization.AuthoritiesConstants.INACTIVE_PARTICIPANT; -import static org.radarcns.auth.authorization.AuthoritiesConstants.PARTICIPANT; - /** * Created by nivethika on 26-5-17. */ @@ -86,6 +94,9 @@ public class SubjectService { @Autowired private RevisionService revisionService; + @Autowired + private ManagementPortalProperties managementPortalProperties; + /** * Create a new subject. @@ -238,82 +249,58 @@ private void unassignAllSources(Subject subject) { */ @Transactional public MinimalSourceDetailsDTO assignOrUpdateSource(Subject subject, SourceType sourceType, - Project project, MinimalSourceDetailsDTO sourceRegistrationDto) - throws URISyntaxException { + Project project, MinimalSourceDetailsDTO sourceRegistrationDto) { Source assignedSource = null; - List sources = subjectRepository - .findSubjectSourcesBySourceType(subject.getUser().getLogin(), - sourceType.getProducer(), sourceType.getModel(), - sourceType.getCatalogVersion()); - - // update meta-data for existing sources if (sourceRegistrationDto.getSourceId() != null) { - // for manually registered devices only add meta-data - Optional sourceToUpdate = subjectRepository.findSubjectSourcesBySourceId( - subject.getUser().getLogin(), sourceRegistrationDto.getSourceId()); - if (sourceToUpdate.isPresent()) { - Source source = sourceToUpdate.get(); - if (sourceRegistrationDto.getSourceName() != null) { - source.setSourceName(sourceRegistrationDto.getSourceName()); - } - source.getAttributes().putAll(sourceRegistrationDto.getAttributes()); - source.setAssigned(true); - source.setSubject(subject); - sourceRepository.save(source); - assignedSource = source; - } else { - log.error("Cannot find a Source of sourceId already registered for subject login"); - Map errorParams = new HashMap<>(); - errorParams.put("message", - "Cannot find a Source of sourceId already registered for subject login"); - errorParams.put("sourceId", sourceRegistrationDto.getSourceId().toString()); - throw new CustomNotFoundException(ErrorConstants.ERR_SOURCE_NOT_FOUND, errorParams); - } + // update meta-data and source-name for existing sources + assignedSource = updateSourceAssignedSubject(subject, sourceRegistrationDto); + } else if (sourceType.getCanRegisterDynamically()) { + List sources = subjectRepository + .findSubjectSourcesBySourceType(subject.getUser().getLogin(), + sourceType.getProducer(), sourceType.getModel(), + sourceType.getCatalogVersion()); // create a source and register meta data // we allow only one source of a source-type per subject if (sources.isEmpty()) { - Source source1 = new Source(sourceType) + Source source = new Source(sourceType) .project(project) .assigned(true) .sourceType(sourceType) .subject(subject); - source1.getAttributes().putAll(sourceRegistrationDto.getAttributes()); + source.getAttributes().putAll(sourceRegistrationDto.getAttributes()); // if source name is provided update source name - if (Objects.nonNull(sourceRegistrationDto.getSourceName())) { + if (sourceRegistrationDto.getSourceName() != null) { // append the auto generated source-name to given source-name to avoid conflicts - source1.setSourceName( - sourceRegistrationDto.getSourceName() + "_" + source1.getSourceName()); + source.setSourceName( + sourceRegistrationDto.getSourceName() + "_" + source.getSourceName()); } - Optional sourceToUpdate = sourceRepository.findOneBySourceName( - source1.getSourceName()); - if (sourceToUpdate.isPresent()) { + // make sure there is no source available on the same name. + if (sourceRepository.findOneBySourceName(source.getSourceName()).isPresent()) { log.error("Cannot create a source with existing source-name {}", - source1.getSourceName()); - Map errorParams = new HashMap<>(); - errorParams.put("message", - "SourceName already in use. Cannot create a source with source-name "); - errorParams.put("source-name", source1.getSourceName()); - throw new CustomNotFoundException(ErrorConstants.ERR_SOURCE_NAME_EXISTS, - errorParams); + source.getSourceName()); + throw new ConflictException("SourceName already in use. Cannot create a " + + "source with existing source-name ", SUBJECT, + ErrorConstants.ERR_SOURCE_NAME_EXISTS, + Collections.singletonMap("source-name", source.getSourceName())); } - source1 = sourceRepository.save(source1); + source = sourceRepository.save(source); - assignedSource = source1; - subject.getSources().add(source1); + assignedSource = source; + subject.getSources().add(source); } else { log.error("A Source of SourceType with the specified producer, model and version " + "was already registered for subject login"); Map errorParams = new HashMap<>(); - errorParams.put("message", "A Source of SourceType with the specified producer, " - + "model and version was already registered for subject login"); errorParams.put("producer", sourceType.getProducer()); errorParams.put("model", sourceType.getModel()); errorParams.put("catalogVersion", sourceType.getCatalogVersion()); errorParams.put("subject-id", subject.getUser().getLogin()); - throw new CustomConflictException(ErrorConstants.ERR_SOURCE_TYPE_EXISTS, - errorParams, ResourceUriService.getUri(sources.get(0))); + throw new ConflictException( + "A Source of SourceType with the specified producer, model and version" + + " was already registered for subject login", + SUBJECT, ErrorConstants.ERR_SOURCE_TYPE_EXISTS, errorParams); } } @@ -324,19 +311,52 @@ public MinimalSourceDetailsDTO assignOrUpdateSource(Subject subject, SourceType log.error("Cannot find assigned source with sourceId or a source of sourceType with " + "the specified producer and model is already registered for subject login "); Map errorParams = new HashMap<>(); - errorParams.put("message", "Cannot find assigned source with sourceId or a source of " - + "sourceType with the specified producer and model is already registered " - + "for subject login "); errorParams.put("producer", sourceType.getProducer()); errorParams.put("model", sourceType.getModel()); errorParams.put("subject-id", subject.getUser().getLogin()); errorParams.put("sourceId", sourceRegistrationDto.getSourceId().toString()); - throw new CustomParameterizedException("InvalidRequest", errorParams); + throw new BadRequestException("Cannot find assigned source with sourceId or a source " + + "of sourceType with the specified producer and model is already registered " + + "for subject login ", SUBJECT, "error.InvalidDynamicSourceRegistration", + errorParams); } subjectRepository.save(subject); return sourceMapper.sourceToMinimalSourceDetailsDTO(assignedSource); } + /** + * Updates source name and attributes of the source assigned to subject. Otherwise returns + * {@link NotFoundException}. + * @param subject subject + * @param sourceRegistrationDto details of source which need to be updated. + * @return Updated {@link Source} instance. + */ + private Source updateSourceAssignedSubject(Subject subject, + MinimalSourceDetailsDTO sourceRegistrationDto) { + // for manually registered devices only add meta-data + Optional sourceToUpdate = subjectRepository.findSubjectSourcesBySourceId( + subject.getUser().getLogin(), sourceRegistrationDto.getSourceId()); + + if (sourceToUpdate.isPresent()) { + Source source = sourceToUpdate.get(); + if (sourceRegistrationDto.getSourceName() != null) { + source.setSourceName(sourceRegistrationDto.getSourceName()); + } + source.getAttributes().putAll(sourceRegistrationDto.getAttributes()); + source.setAssigned(true); + source.setSubject(subject); + + return sourceRepository.save(source); + } else { + log.error("No source with source-id to assigned to the subject with subject-login"); + Map errorParams = new HashMap<>(); + errorParams.put("sourceId", sourceRegistrationDto.getSourceId().toString()); + errorParams.put("subject-login", subject.getUser().getLogin()); + throw new NotFoundException( "No source with source-id to assigned to the subject" + + " with subject-login", SUBJECT, ERR_SOURCE_NOT_FOUND, errorParams); + } + } + /** * Gets all sources assigned to the subject identified by :login. * @@ -386,17 +406,17 @@ public List findSubjectSourcesFromRevisions(Subject sub * @param login the login of the subject * @param revision the revision number * @return the subject at the given revision - * @throws CustomNotFoundException if there was no subject with the given login at the given + * @throws NotFoundException if there was no subject with the given login at the given * revision number */ - public SubjectDTO findRevision(String login, Integer revision) throws CustomNotFoundException { + public SubjectDTO findRevision(String login, Integer revision) throws NotFoundException { // first get latest known version of the subject, if it's deleted we can't load the entity // directly by e.g. findOneByLogin SubjectDTO latest = getLatestRevision(login); Subject sub = revisionService.findRevision(revision, latest.getId(), Subject.class); if (sub == null) { - throw new CustomNotFoundException(ErrorConstants.ERR_SUBJECT_NOT_FOUND, - Collections.singletonMap("subjectLogin", login)); + throw new NotFoundException("subject not found for given login and revision.", SUBJECT, + ERR_SUBJECT_NOT_FOUND, Collections.singletonMap("subjectLogin", login)); } return subjectMapper.subjectToSubjectDTO(sub); } @@ -406,21 +426,68 @@ public SubjectDTO findRevision(String login, Integer revision) throws CustomNotF * * @param login the login of the subject * @return the latest revision for that subject - * @throws CustomNotFoundException if no subject was found with the given login + * @throws NotFoundException if no subject was found with the given login */ - public SubjectDTO getLatestRevision(String login) throws CustomNotFoundException { + public SubjectDTO getLatestRevision(String login) throws NotFoundException { UserDTO user = (UserDTO) revisionService.getLatestRevisionForEntity(User.class, Arrays.asList(AuditEntity.property("login").eq(login))) - .orElseThrow(() -> new CustomNotFoundException(ErrorConstants.ERR_SUBJECT_NOT_FOUND, + .orElseThrow(() -> new NotFoundException("Subject latest revision not found " + + "for login" , SUBJECT, ERR_SUBJECT_NOT_FOUND, Collections.singletonMap("subjectLogin", login))); return (SubjectDTO) revisionService.getLatestRevisionForEntity(Subject.class, Arrays.asList(AuditEntity.property("user").eq(user))) - .orElseThrow(() -> new CustomNotFoundException(ErrorConstants.ERR_SUBJECT_NOT_FOUND, - Collections.singletonMap("subjectLogin", login))); + .orElseThrow(() -> new NotFoundException("Subject latest revision not found " + + "for login" , SUBJECT, ERR_SUBJECT_NOT_FOUND, + Collections.singletonMap("subjectLogin", login))); } private static Predicate distinctByKey(Function keyExtractor) { final Set seen = new HashSet<>(); return t -> seen.add(keyExtractor.apply(t)); } + + + /** + * Finds {@link Subject} from databased from login provided. + * @param login of subject to look for. + * @return {@link Subject} loaded. + */ + public Subject findOneByLogin(String login) { + Optional subject = subjectRepository.findOneWithEagerBySubjectLogin(login); + return subject.orElseThrow(() -> + new NotFoundException("Subject not found with login", SUBJECT, + ERR_SUBJECT_NOT_FOUND) + ); + } + + /** + * Gets relevant privacy-policy-url for this subject. + *

+ * If the active project of the subject has a valid privacy-policy-url returns that url. + * Otherwise, it loads the default URL from ManagementPortal configurations that is + * general. + *

+ * @param subject to get relevant policy url + * @return URL of privacy policy for this token + */ + protected URL getPrivacyPolicyUrl(Subject subject) { + + // load default url from config + String policyUrl = subject.getActiveProject() + .map(p -> p.getAttributes().get(PRIVACY_POLICY_URL)) + .filter(u -> u != null && !u.isEmpty()) + .orElse(managementPortalProperties.getCommon().getPrivacyPolicyUrl()); + + try { + return new URL(policyUrl); + } catch (MalformedURLException e) { + Map params = new HashMap<>(); + params.put("url" , policyUrl); + params.put("message" , e.getMessage()); + throw new InvalidStateException("No valid privacy-policy Url configured. Please " + + "verify your project's privacy-policy url and/or general url config", + OAUTH_CLIENT, ERR_NO_VALID_PRIVACY_POLICY_URL_CONFIGURED, + params); + } + } } diff --git a/src/main/java/org/radarcns/management/service/UserService.java b/src/main/java/org/radarcns/management/service/UserService.java index 9474858fb..d210a6a37 100644 --- a/src/main/java/org/radarcns/management/service/UserService.java +++ b/src/main/java/org/radarcns/management/service/UserService.java @@ -1,5 +1,17 @@ package org.radarcns.management.service; +import static org.radarcns.management.web.rest.errors.EntityName.USER; + +import java.time.Period; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + import org.radarcns.auth.authorization.AuthoritiesConstants; import org.radarcns.auth.config.Constants; import org.radarcns.management.domain.Project; @@ -17,8 +29,8 @@ import org.radarcns.management.service.mapper.ProjectMapper; import org.radarcns.management.service.mapper.UserMapper; import org.radarcns.management.service.util.RandomUtil; -import org.radarcns.management.web.rest.errors.CustomParameterizedException; import org.radarcns.management.web.rest.errors.ErrorConstants; +import org.radarcns.management.web.rest.errors.NotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -29,15 +41,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.Period; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - /** * Service class for managing users. */ @@ -187,14 +190,17 @@ private Set getUserRoles(UserDTO userDto) { roleDto.getProjectId(), roleDto.getAuthorityName()); if (!role.isPresent() || role.get().getId() == null) { Role currentRole = new Role(); - currentRole.setAuthority(authorityRepository - .findByAuthorityName(roleDto.getAuthorityName()) - .orElseThrow(() -> new CustomParameterizedException( - ErrorConstants.ERR_INVALID_AUTHORITY, roleDto.getAuthorityName()))); + // supplied authorityname can be anything, so check if we actually have one + currentRole.setAuthority( + authorityRepository.findByAuthorityName(roleDto.getAuthorityName()) + .orElseThrow(() -> new NotFoundException("Authority not found with " + + "authorityName", USER, ErrorConstants.ERR_INVALID_AUTHORITY, + Collections.singletonMap("authorityName", + roleDto.getAuthorityName())))); if (roleDto.getProjectId() != null) { currentRole.setProject(projectRepository.getOne(roleDto.getProjectId())); } - // supplied authorityname can be anything, so check if we actually have one + if (Objects.nonNull(currentRole.getAuthority())) { roles.add(roleRepository.save(currentRole)); } @@ -261,11 +267,20 @@ public void deleteUser(String login) { } /** - * Change the current user's password. + * Change the password of the user with the given login. * @param password the new password */ public void changePassword(String password) { - userRepository.findOneByLogin(SecurityUtils.getCurrentUserLogin()).ifPresent(user -> { + changePassword(SecurityUtils.getCurrentUserLogin(), password); + } + + /** + * Change the user's password. + * @param password the new password + * @param login of the user to change password + */ + public void changePassword(String login, String password) { + userRepository.findOneByLogin(login).ifPresent(user -> { String encryptedPassword = passwordEncoder.encode(password); user.setPassword(encryptedPassword); log.debug("Changed password for User: {}", user); diff --git a/src/main/java/org/radarcns/management/service/catalog/CatalogSourceType.java b/src/main/java/org/radarcns/management/service/catalog/CatalogSourceType.java index 604920a68..83b98895a 100644 --- a/src/main/java/org/radarcns/management/service/catalog/CatalogSourceType.java +++ b/src/main/java/org/radarcns/management/service/catalog/CatalogSourceType.java @@ -78,6 +78,14 @@ public String getScope() { return scope; } + public List getData() { + return data; + } + + public String getAssessmentType() { + return assessmentType; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -86,26 +94,25 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - CatalogSourceType producer = (CatalogSourceType) o; - return Objects.equals(name, producer.name) - && Objects.equals(doc, producer.doc) - && Objects.equals(vendor, producer.vendor) - && Objects.equals(version, producer.version) - && Objects.equals(scope, producer.scope) - && Objects.equals(model, producer.model); + CatalogSourceType that = (CatalogSourceType) o; + return Objects.equals(assessmentType, that.assessmentType) + && Objects.equals(appProvider, that.appProvider) + && Objects.equals(vendor, that.vendor) + && Objects.equals(model, that.model) + && Objects.equals(version, that.version) + && Objects.equals(name, that.name) + && Objects.equals(doc, that.doc) + && Objects.equals(scope, that.scope) + && Objects.equals(properties, that.properties) + && Objects.equals(labels, that.labels) + && Objects.equals(data, that.data); } @Override public int hashCode() { - return Objects.hash(name, vendor, model, version, scope); - } - - public List getData() { - return data; - } - public String getAssessmentType() { - return assessmentType; + return Objects + .hash(assessmentType, appProvider, vendor, model, version, name, doc, scope, properties, + labels, data); } - } diff --git a/src/main/java/org/radarcns/management/service/catalog/SourceTypeResponse.java b/src/main/java/org/radarcns/management/service/catalog/SourceTypeResponse.java index c20a5c6e2..3b674f944 100644 --- a/src/main/java/org/radarcns/management/service/catalog/SourceTypeResponse.java +++ b/src/main/java/org/radarcns/management/service/catalog/SourceTypeResponse.java @@ -14,6 +14,9 @@ public class SourceTypeResponse { @JsonProperty("monitor-source-types") private List monitorSources; + @JsonProperty("connector-source-types") + private List connectorSources; + public List getPassiveSources() { return passiveSources; } @@ -25,4 +28,8 @@ public List getActiveSources() { public List getMonitorSources() { return monitorSources; } + + public List getConnectorSources() { + return connectorSources; + } } diff --git a/src/main/java/org/radarcns/management/service/dto/ClientPairInfoDTO.java b/src/main/java/org/radarcns/management/service/dto/ClientPairInfoDTO.java index b00536cc8..fdbcbe6ca 100644 --- a/src/main/java/org/radarcns/management/service/dto/ClientPairInfoDTO.java +++ b/src/main/java/org/radarcns/management/service/dto/ClientPairInfoDTO.java @@ -1,25 +1,37 @@ package org.radarcns.management.service.dto; +import java.net.URL; +import java.util.Objects; + /** * Created by dverbeec on 29/08/2017. */ public class ClientPairInfoDTO { - private final String refreshToken; + private final String tokenName; + + private final URL tokenUrl; + /** * Initialize with the given refresh token. - * @param refreshToken the refresh token + * @param tokenName the refresh token + * @param tokenUrl the refresh token */ - public ClientPairInfoDTO(String refreshToken) { - if (refreshToken == null) { - throw new IllegalArgumentException("refreshToken can not be null"); + public ClientPairInfoDTO(String tokenName, URL tokenUrl) { + if (tokenUrl == null) { + throw new IllegalArgumentException("tokenUrl can not be null"); } - this.refreshToken = refreshToken; + this.tokenName = tokenName; + this.tokenUrl = tokenUrl; } - public String getRefreshToken() { - return refreshToken; + public String getTokenName() { + return tokenName; + } + + public URL getTokenUrl() { + return tokenUrl; } @Override @@ -27,17 +39,22 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (!(o instanceof ClientPairInfoDTO)) { + if (o == null || getClass() != o.getClass()) { return false; } - ClientPairInfoDTO that = (ClientPairInfoDTO) o; - - return refreshToken.equals(that.refreshToken); + return Objects.equals(tokenName, that.tokenName) + && Objects.equals(tokenUrl, that.tokenUrl); } @Override public int hashCode() { - return refreshToken.hashCode(); + + return Objects.hash(tokenName, tokenUrl); + } + + @Override + public String toString() { + return "ClientPairInfoDTO{" + "tokenName='" + tokenName + '\'' + '}'; } } diff --git a/src/main/java/org/radarcns/management/service/dto/ProjectDTO.java b/src/main/java/org/radarcns/management/service/dto/ProjectDTO.java index 5a596505a..f7787ac8b 100644 --- a/src/main/java/org/radarcns/management/service/dto/ProjectDTO.java +++ b/src/main/java/org/radarcns/management/service/dto/ProjectDTO.java @@ -22,6 +22,7 @@ public class ProjectDTO implements Serializable { public static final String WORK_PACKAGE_KEY = "Work-package"; public static final String PHASE_KEY = "Phase"; public static final String HUMAN_READABLE_PROJECT_NAME = "Human-readable-project-name"; + public static final String PRIVACY_POLICY_URL = "Privacy-policy-url"; private Long id; diff --git a/src/main/java/org/radarcns/management/service/dto/TokenDTO.java b/src/main/java/org/radarcns/management/service/dto/TokenDTO.java new file mode 100644 index 000000000..1958fbfee --- /dev/null +++ b/src/main/java/org/radarcns/management/service/dto/TokenDTO.java @@ -0,0 +1,66 @@ +package org.radarcns.management.service.dto; + +import java.net.URL; +import java.util.Objects; + +public class TokenDTO { + private final String refreshToken; + + private final URL baseUrl; + + private final URL privacyPolicyUrl; + + /** + * Create a meta-token using refreshToken, baseUrl of platform, and privacyPolicyURL for this + * token. + * @param refreshToken refreshToken. + * @param baseUrl baseUrl of the platform + * @param privacyPolicyUrl privacyPolicyUrl for this token. + */ + public TokenDTO(String refreshToken, URL baseUrl, URL privacyPolicyUrl) { + this.refreshToken = refreshToken; + this.baseUrl = baseUrl; + this.privacyPolicyUrl = privacyPolicyUrl; + } + + public String getRefreshToken() { + return refreshToken; + } + + public URL getBaseUrl() { + return baseUrl; + } + + public URL getPrivacyPolicyUrl() { + return privacyPolicyUrl; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TokenDTO that = (TokenDTO) o; + return Objects.equals(refreshToken, that.refreshToken) + && Objects.equals(baseUrl, that.baseUrl) + && Objects.equals(privacyPolicyUrl, that.privacyPolicyUrl); + } + + @Override + public int hashCode() { + + return Objects.hash(refreshToken, baseUrl, privacyPolicyUrl); + } + + @Override + public String toString() { + return "TokenDTO{" + + "refreshToken='" + refreshToken + + ", baseUrl=" + baseUrl + + ", privacyPolicyUrl=" + privacyPolicyUrl + + '}'; + } +} diff --git a/src/main/java/org/radarcns/management/service/mapper/ProjectMapper.java b/src/main/java/org/radarcns/management/service/mapper/ProjectMapper.java index dd6dd1e57..a9054ee16 100644 --- a/src/main/java/org/radarcns/management/service/mapper/ProjectMapper.java +++ b/src/main/java/org/radarcns/management/service/mapper/ProjectMapper.java @@ -38,6 +38,7 @@ public interface ProjectMapper { @Mapping(target = "endDate", ignore = true) @Mapping(target = "projectStatus", ignore = true) @Mapping(target = "sourceTypes", ignore = true) + @Mapping(target = "attributes", ignore = true) Project descriptiveDTOToProject(MinimalProjectDetailsDTO minimalProjectDetailsDto); List descriptiveDTOsToProjects( diff --git a/src/main/java/org/radarcns/management/service/mapper/RoleMapper.java b/src/main/java/org/radarcns/management/service/mapper/RoleMapper.java index 7aa3952ef..ffc9e7905 100644 --- a/src/main/java/org/radarcns/management/service/mapper/RoleMapper.java +++ b/src/main/java/org/radarcns/management/service/mapper/RoleMapper.java @@ -2,50 +2,33 @@ import java.util.Set; + +import org.mapstruct.DecoratedWith; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.radarcns.management.domain.Authority; import org.radarcns.management.domain.Role; -import org.radarcns.management.repository.AuthorityRepository; import org.radarcns.management.service.dto.RoleDTO; -import org.radarcns.management.web.rest.errors.CustomParameterizedException; -import org.radarcns.management.web.rest.errors.ErrorConstants; -import org.springframework.beans.factory.annotation.Autowired; +import org.radarcns.management.service.mapper.decorator.RoleMapperDecorator; /** * Created by nivethika on 23-5-17. */ @Mapper(componentModel = "spring", uses = {ProjectMapper.class}) -public abstract class RoleMapper { - - @Autowired - private AuthorityRepository authorityRepository; +@DecoratedWith(RoleMapperDecorator.class) +public interface RoleMapper { @Mapping(source = "authority.name", target = "authorityName") @Mapping(source = "project.id", target = "projectId") @Mapping(source = "project.projectName", target = "projectName") - public abstract RoleDTO roleToRoleDTO(Role role); + RoleDTO roleToRoleDTO(Role role); - @Mapping(source = "authorityName", target = "authority") + @Mapping(target = "authority", ignore = true) @Mapping(source = "projectId", target = "project.id") @Mapping(target = "users", ignore = true) - public abstract Role roleDTOToRole(RoleDTO roleDtp); - - /** - * Get an {@link Authority} from its name. - * @param authorityName the authority name - * @return the {@link Authority} - * @throws CustomParameterizedException if the authority name is not in the database - */ - public Authority authorityFromAuthorityName(String authorityName) { - return authorityRepository.findByAuthorityName(authorityName) - .orElseThrow(() -> new CustomParameterizedException( - ErrorConstants.ERR_INVALID_AUTHORITY, authorityName)); - } - - public abstract Set roleDTOsToRoles(Set roleDtos); + Role roleDTOToRole(RoleDTO roleDtp); - public abstract Set rolesToRoleDTOs(Set roles); + Set roleDTOsToRoles(Set roleDtos); + Set rolesToRoleDTOs(Set roles); } diff --git a/src/main/java/org/radarcns/management/service/mapper/decorator/ProjectMapperDecorator.java b/src/main/java/org/radarcns/management/service/mapper/decorator/ProjectMapperDecorator.java index 0a5a26eec..bdb542074 100644 --- a/src/main/java/org/radarcns/management/service/mapper/decorator/ProjectMapperDecorator.java +++ b/src/main/java/org/radarcns/management/service/mapper/decorator/ProjectMapperDecorator.java @@ -6,6 +6,8 @@ import java.util.List; import org.radarcns.management.domain.Project; +import org.radarcns.management.repository.ProjectRepository; +import org.radarcns.management.service.dto.MinimalProjectDetailsDTO; import org.radarcns.management.service.dto.ProjectDTO; import org.radarcns.management.service.mapper.ProjectMapper; import org.springframework.beans.factory.annotation.Autowired; @@ -20,6 +22,9 @@ public abstract class ProjectMapperDecorator implements ProjectMapper { @Qualifier("delegate") private ProjectMapper delegate; + @Autowired + private ProjectRepository projectRepository; + @Override public ProjectDTO projectToProjectDTO(Project project) { if (project == null) { @@ -43,6 +48,11 @@ public Project projectDTOToProject(ProjectDTO projectDto) { project.getAttributes().put(HUMAN_READABLE_PROJECT_NAME, projectDto .getHumanReadableProjectName()); } + if (projectDto.getHumanReadableProjectName() != null && !projectDto + .getHumanReadableProjectName().isEmpty()) { + project.getAttributes().put(HUMAN_READABLE_PROJECT_NAME, projectDto + .getHumanReadableProjectName()); + } return project; } @@ -74,5 +84,13 @@ public List projectDTOsToProjects(List projectDtos) { return list; } + + @Override + public Project descriptiveDTOToProject(MinimalProjectDetailsDTO minimalProjectDetailsDto) { + if (minimalProjectDetailsDto == null) { + return null; + } + return projectRepository.getOne(minimalProjectDetailsDto.getId()); + } } diff --git a/src/main/java/org/radarcns/management/service/mapper/decorator/RoleMapperDecorator.java b/src/main/java/org/radarcns/management/service/mapper/decorator/RoleMapperDecorator.java new file mode 100644 index 000000000..b78a9ea53 --- /dev/null +++ b/src/main/java/org/radarcns/management/service/mapper/decorator/RoleMapperDecorator.java @@ -0,0 +1,42 @@ +package org.radarcns.management.service.mapper.decorator; + +import org.radarcns.management.domain.Role; +import org.radarcns.management.repository.AuthorityRepository; +import org.radarcns.management.service.dto.RoleDTO; +import org.radarcns.management.service.mapper.RoleMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; + +/** + * Created by nivethika on 03-8-18. + */ +public abstract class RoleMapperDecorator implements RoleMapper { + + @Autowired + @Qualifier("delegate") + private RoleMapper delegate; + + @Autowired + private AuthorityRepository authorityRepository; + + /** + * Overrides standard RoleMapperImpl and loads authority from repository if not specified. + * @param roleDto to convert to Role. + * @return converted Role instance. + */ + @Override + public Role roleDTOToRole(RoleDTO roleDto) { + + if (roleDto == null) { + return null; + } + + Role role = delegate.roleDTOToRole(roleDto); + + if (role.getAuthority() == null) { + role.setAuthority(authorityRepository.getOne(roleDto.getAuthorityName())); + } + + return role; + } +} diff --git a/src/main/java/org/radarcns/management/service/mapper/decorator/SubjectMapperDecorator.java b/src/main/java/org/radarcns/management/service/mapper/decorator/SubjectMapperDecorator.java index de7edb789..8d0a1859f 100644 --- a/src/main/java/org/radarcns/management/service/mapper/decorator/SubjectMapperDecorator.java +++ b/src/main/java/org/radarcns/management/service/mapper/decorator/SubjectMapperDecorator.java @@ -1,8 +1,9 @@ package org.radarcns.management.service.mapper.decorator; +import java.util.ArrayList; +import java.util.List; + import org.mapstruct.MappingTarget; -import org.radarcns.auth.authorization.AuthoritiesConstants; -import org.radarcns.management.domain.Role; import org.radarcns.management.domain.Subject; import org.radarcns.management.domain.audit.EntityAuditInfo; import org.radarcns.management.service.RevisionService; @@ -13,10 +14,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - /** * Created by nivethika on 30-8-17. */ @@ -39,12 +36,9 @@ public SubjectDTO subjectToSubjectDTO(Subject subject) { } SubjectDTO dto = delegate.subjectToSubjectDTO(subject); dto.setStatus(getSubjectStatus(subject)); - Optional role = subject.getUser().getRoles().stream() - .filter(r -> r.getAuthority().getName().equals(AuthoritiesConstants.PARTICIPANT)) - .findFirst(); - - role.ifPresent(role1 -> - dto.setProject(projectMapper.projectToProjectDTO(role1.getProject()))); + subject.getActiveProject() + .ifPresent( + project -> dto.setProject(projectMapper.projectToProjectDTO(project))); EntityAuditInfo auditInfo = revisionService.getAuditInfo(subject); dto.setCreatedDate(auditInfo.getCreatedAt()); diff --git a/src/main/java/org/radarcns/management/web/rest/MetaTokenResource.java b/src/main/java/org/radarcns/management/web/rest/MetaTokenResource.java new file mode 100644 index 000000000..7d71a8983 --- /dev/null +++ b/src/main/java/org/radarcns/management/web/rest/MetaTokenResource.java @@ -0,0 +1,47 @@ +package org.radarcns.management.web.rest; + + +import java.net.MalformedURLException; +import java.time.Duration; + +import com.codahale.metrics.annotation.Timed; +import org.radarcns.auth.config.Constants; +import org.radarcns.management.service.MetaTokenService; +import org.radarcns.management.service.dto.ClientPairInfoDTO; +import org.radarcns.management.service.dto.TokenDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class MetaTokenResource { + + private final Logger log = LoggerFactory.getLogger(OAuthClientsResource.class); + + public static final Duration DEFAULT_META_TOKEN_TIMEOUT = Duration.ofHours(1); + + @Autowired + private MetaTokenService metaTokenService; + + /** + * GET /api/meta-token/:tokenName. + * + *

Get refresh-token available under this tokenName.

+ * + * @param tokenName the tokenName given after pairing the subject with client + * @return the client as a {@link ClientPairInfoDTO} + */ + @GetMapping("/meta-token/{tokenName:" + Constants.TOKEN_NAME_REGEX + "}") + @Timed + public ResponseEntity getTokenByTokenName(@PathVariable("tokenName") String tokenName) + throws MalformedURLException { + log.info("Requesting token with tokenName {}", tokenName); + return ResponseEntity.ok().body(metaTokenService.fetchToken(tokenName)); + } +} diff --git a/src/main/java/org/radarcns/management/web/rest/OAuthClientsResource.java b/src/main/java/org/radarcns/management/web/rest/OAuthClientsResource.java index b1e2d50af..2b74e2f2b 100644 --- a/src/main/java/org/radarcns/management/web/rest/OAuthClientsResource.java +++ b/src/main/java/org/radarcns/management/web/rest/OAuthClientsResource.java @@ -1,22 +1,36 @@ package org.radarcns.management.web.rest; +import static org.hibernate.id.IdentifierGenerator.ENTITY_NAME; +import static org.radarcns.auth.authorization.Permission.OAUTHCLIENTS_CREATE; +import static org.radarcns.auth.authorization.Permission.OAUTHCLIENTS_DELETE; +import static org.radarcns.auth.authorization.Permission.OAUTHCLIENTS_READ; +import static org.radarcns.auth.authorization.Permission.OAUTHCLIENTS_UPDATE; +import static org.radarcns.auth.authorization.Permission.SUBJECT_UPDATE; +import static org.radarcns.auth.authorization.RadarAuthorization.checkPermission; +import static org.radarcns.auth.authorization.RadarAuthorization.checkPermissionOnSubject; +import static org.radarcns.management.security.SecurityUtils.getJWT; +import static org.radarcns.management.service.OAuthClientService.checkProtected; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; + import com.codahale.metrics.annotation.Timed; import org.radarcns.auth.config.Constants; import org.radarcns.auth.exception.NotAuthorizedException; import org.radarcns.management.domain.Subject; import org.radarcns.management.domain.User; -import org.radarcns.management.repository.SubjectRepository; +import org.radarcns.management.service.OAuthClientService; import org.radarcns.management.service.ResourceUriService; +import org.radarcns.management.service.SubjectService; import org.radarcns.management.service.UserService; import org.radarcns.management.service.dto.ClientDetailsDTO; import org.radarcns.management.service.dto.ClientPairInfoDTO; import org.radarcns.management.service.dto.SubjectDTO; import org.radarcns.management.service.mapper.ClientDetailsMapper; import org.radarcns.management.service.mapper.SubjectMapper; -import org.radarcns.management.web.rest.errors.CustomConflictException; -import org.radarcns.management.web.rest.errors.CustomNotFoundException; -import org.radarcns.management.web.rest.errors.CustomParameterizedException; -import org.radarcns.management.web.rest.errors.ErrorConstants; import org.radarcns.management.web.rest.util.HeaderUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,17 +40,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.common.OAuth2AccessToken; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration; import org.springframework.security.oauth2.provider.ClientDetails; -import org.springframework.security.oauth2.provider.NoSuchClientException; -import org.springframework.security.oauth2.provider.OAuth2Authentication; -import org.springframework.security.oauth2.provider.OAuth2Request; -import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; -import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -47,28 +51,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import javax.validation.Valid; -import java.net.URISyntaxException; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -import static org.radarcns.auth.authorization.Permission.OAUTHCLIENTS_CREATE; -import static org.radarcns.auth.authorization.Permission.OAUTHCLIENTS_DELETE; -import static org.radarcns.auth.authorization.Permission.OAUTHCLIENTS_READ; -import static org.radarcns.auth.authorization.Permission.OAUTHCLIENTS_UPDATE; -import static org.radarcns.auth.authorization.Permission.SUBJECT_UPDATE; -import static org.radarcns.auth.authorization.RadarAuthorization.checkPermission; -import static org.radarcns.auth.authorization.RadarAuthorization.checkPermissionOnSubject; -import static org.radarcns.management.security.SecurityUtils.getJWT; -import static org.springframework.security.oauth2.common.util.OAuth2Utils.GRANT_TYPE; - /** * Created by dverbeec on 5/09/2017. */ @@ -78,17 +60,15 @@ public class OAuthClientsResource { private final Logger log = LoggerFactory.getLogger(OAuthClientsResource.class); - @Autowired - private AuthorizationServerEndpointsConfiguration authorizationServerEndpointsConfiguration; @Autowired - private JdbcClientDetailsService clientDetailsService; + private OAuthClientService oAuthClientService; @Autowired private ClientDetailsMapper clientDetailsMapper; @Autowired - private SubjectRepository subjectRepository; + private SubjectService subjectService; @Autowired private SubjectMapper subjectMapper; @@ -102,9 +82,6 @@ public class OAuthClientsResource { @Autowired private AuditEventRepository eventRepository; - private static final String ENTITY_NAME = "oauthClient"; - private static final String PROTECTED_KEY = "protected"; - /** * GET /api/oauth-clients. * @@ -117,7 +94,7 @@ public class OAuthClientsResource { public ResponseEntity> getOAuthClients() throws NotAuthorizedException { checkPermission(getJWT(servletRequest), OAUTHCLIENTS_READ); return ResponseEntity.ok().body(clientDetailsMapper - .clientDetailsToClientDetailsDTO(clientDetailsService.listClientDetails())); + .clientDetailsToClientDetailsDTO(oAuthClientService.findAllOAuthClients())); } /** @@ -135,7 +112,7 @@ public ResponseEntity getOAuthClientById(@PathVariable("id") S checkPermission(getJWT(servletRequest), OAUTHCLIENTS_READ); // getOAuthClient checks if the id exists return ResponseEntity.ok().body(clientDetailsMapper - .clientDetailsToClientDetailsDTO(getOAuthClient(id))); + .clientDetailsToClientDetailsDTO(oAuthClientService.findOneByClientId(id))); } /** @@ -152,18 +129,9 @@ public ResponseEntity updateOAuthClient(@Valid @RequestBody Cl clientDetailsDto) throws NotAuthorizedException { checkPermission(getJWT(servletRequest), OAUTHCLIENTS_UPDATE); // getOAuthClient checks if the id exists - checkProtected(getOAuthClient(clientDetailsDto.getClientId())); - ClientDetails details = clientDetailsMapper - .clientDetailsDTOToClientDetails(clientDetailsDto); - clientDetailsService.updateClientDetails(details); - ClientDetails updated = getOAuthClient(clientDetailsDto.getClientId()); - // updateClientDetails does not update secret, so check for it separately - if (Objects.nonNull(clientDetailsDto.getClientSecret()) && !clientDetailsDto - .getClientSecret().equals(updated.getClientSecret())) { - clientDetailsService.updateClientSecret(clientDetailsDto.getClientId(), - clientDetailsDto.getClientSecret()); - } - updated = getOAuthClient(clientDetailsDto.getClientId()); + checkProtected(oAuthClientService.findOneByClientId(clientDetailsDto.getClientId())); + + ClientDetails updated = oAuthClientService.updateOauthClient(clientDetailsDto); return ResponseEntity.ok() .headers(HeaderUtil.createEntityUpdateAlert(ENTITY_NAME, clientDetailsDto.getClientId())) @@ -184,8 +152,8 @@ public ResponseEntity deleteOAuthClient(@PathVariable String id) throws NotAuthorizedException { checkPermission(getJWT(servletRequest), OAUTHCLIENTS_DELETE); // getOAuthClient checks if the id exists - checkProtected(getOAuthClient(id)); - clientDetailsService.removeClientDetails(id); + checkProtected(oAuthClientService.findOneByClientId(id)); + oAuthClientService.deleteClientDetails(id); return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, id)) .build(); } @@ -204,19 +172,7 @@ public ResponseEntity deleteOAuthClient(@PathVariable String id) public ResponseEntity createOAuthClient(@Valid @RequestBody ClientDetailsDTO clientDetailsDto) throws URISyntaxException, NotAuthorizedException { checkPermission(getJWT(servletRequest), OAUTHCLIENTS_CREATE); - // check if the client id exists - try { - clientDetailsService.loadClientByClientId(clientDetailsDto.getClientId()); - throw new CustomConflictException(ErrorConstants.ERR_CLIENT_ID_EXISTS, - Collections.singletonMap("client_id", clientDetailsDto.getClientId()), - ResourceUriService.getUri(clientDetailsDto)); - } catch (NoSuchClientException ex) { - // Client does not exist yet, we can go ahead and create it - } - ClientDetails details = clientDetailsMapper - .clientDetailsDTOToClientDetails(clientDetailsDto); - clientDetailsService.addClientDetails(details); - ClientDetails created = getOAuthClient(clientDetailsDto.getClientId()); + ClientDetails created = oAuthClientService.createClientDetail(clientDetailsDto); return ResponseEntity.created(ResourceUriService.getUri(clientDetailsDto)) .headers(HeaderUtil.createEntityCreationAlert(ENTITY_NAME, created.getClientId())) .body(clientDetailsMapper.clientDetailsToClientDetailsDTO(created)); @@ -236,7 +192,8 @@ public ResponseEntity createOAuthClient(@Valid @RequestBody Cl @GetMapping("/oauth-clients/pair") @Timed public ResponseEntity getRefreshToken(@RequestParam String login, - @RequestParam(value = "clientId") String clientId) throws NotAuthorizedException { + @RequestParam(value = "clientId") String clientId) + throws NotAuthorizedException, URISyntaxException, MalformedURLException { User currentUser = userService.getUserWithAuthorities(); if (currentUser == null) { // We only allow this for actual logged in users for now, not for client_credentials @@ -244,28 +201,14 @@ public ResponseEntity getRefreshToken(@RequestParam String lo } // lookup the subject - Subject subject = getSubject(login); + Subject subject = subjectService.findOneByLogin(login); SubjectDTO subjectDto = subjectMapper.subjectToSubjectDTO(subject); // Users who can update a subject can also generate a refresh token for that subject checkPermissionOnSubject(getJWT(servletRequest), SUBJECT_UPDATE, subjectDto.getProject().getProjectName(), subjectDto.getLogin()); - // lookup the OAuth client - // getOAuthClient checks if the id exists - ClientDetails details = getOAuthClient(clientId); - - // add the user's authorities - User user = subject.getUser(); - Set authorities = user.getAuthorities().stream() - .map(a -> new SimpleGrantedAuthority(a.getName())) - .collect(Collectors.toSet()); - - OAuth2AccessToken token = createToken(clientId, user.getLogin(), authorities, - details.getScope(), details.getResourceIds()); - - ClientPairInfoDTO cpi = new ClientPairInfoDTO(token.getRefreshToken().getValue()); - + ClientPairInfoDTO cpi = oAuthClientService.createRefreshToken(subject, clientId); // generate audit event eventRepository.add(new AuditEvent(currentUser.getLogin(), "PAIR_CLIENT_REQUEST", "client_id=" + clientId, "subject_login=" + login)); @@ -274,66 +217,4 @@ public ResponseEntity getRefreshToken(@RequestParam String lo return new ResponseEntity<>(cpi, HttpStatus.OK); } - private OAuth2AccessToken createToken(String clientId, String login, - Set authorities, Set scope, Set resourceIds) { - Map requestParameters = new HashMap<>(); - requestParameters.put(GRANT_TYPE , "authorization_code"); - - Set responseTypes = Collections.singleton("code"); - - OAuth2Request oAuth2Request = new OAuth2Request(requestParameters, clientId, authorities, - true, scope, resourceIds, null, responseTypes, Collections.emptyMap()); - - UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(login, null, authorities); - OAuth2Authentication auth = new OAuth2Authentication(oAuth2Request, authenticationToken); - - AuthorizationServerTokenServices tokenServices = authorizationServerEndpointsConfiguration - .getEndpointsConfigurer().getTokenServices(); - - return tokenServices.createAccessToken(auth); - } - - /** - * Find ClientDetails by OAuth client id. - * - * @param clientId The client ID to look up - * @return a ClientDetails object with the requested client ID - * @throws CustomNotFoundException If there is no client with the requested ID - */ - private ClientDetails getOAuthClient(String clientId) throws CustomNotFoundException { - try { - return clientDetailsService.loadClientByClientId(clientId); - } catch (NoSuchClientException e) { - log.error("Pair client request for unknown client id: {}", clientId); - Map errorParams = new HashMap<>(); - errorParams.put("message", "Client ID not found"); - errorParams.put("clientId", clientId); - throw new CustomNotFoundException(ErrorConstants.ERR_OAUTH_CLIENT_ID_NOT_FOUND, - errorParams); - } - } - - private Subject getSubject(String login) throws CustomNotFoundException { - Optional subject = subjectRepository.findOneWithEagerBySubjectLogin(login); - - if (!subject.isPresent()) { - log.error("Pair client request for unknown subject login: {}", login); - Map errorParams = new HashMap<>(); - errorParams.put("message", "Subject ID not found"); - errorParams.put("subjectLogin", login); - throw new CustomNotFoundException(ErrorConstants.ERR_SUBJECT_NOT_FOUND, errorParams); - } - - return subject.get(); - } - - private void checkProtected(ClientDetails details) { - Map info = details.getAdditionalInformation(); - if (Objects.nonNull(info) && info.containsKey(PROTECTED_KEY) - && info.get(PROTECTED_KEY).toString().equalsIgnoreCase("true")) { - throw new CustomParameterizedException(ErrorConstants.ERR_OAUTH_CLIENT_PROTECTED, - details.getClientId()); - } - } } diff --git a/src/main/java/org/radarcns/management/web/rest/ProjectResource.java b/src/main/java/org/radarcns/management/web/rest/ProjectResource.java index e159c6c54..644ea4364 100644 --- a/src/main/java/org/radarcns/management/web/rest/ProjectResource.java +++ b/src/main/java/org/radarcns/management/web/rest/ProjectResource.java @@ -197,7 +197,7 @@ public List getSourceTypesOfProject(@PathVariable String projectN log.debug("REST request to get Project : {}", projectName); ProjectDTO projectDto = projectService.findOneByName(projectName); checkPermissionOnProject(getJWT(servletRequest), PROJECT_READ, projectDto.getProjectName()); - return projectService.findSourceTypesById(projectDto.getId()); + return projectService.findSourceTypesByProjectId(projectDto.getId()); } diff --git a/src/main/java/org/radarcns/management/web/rest/SourceDataResource.java b/src/main/java/org/radarcns/management/web/rest/SourceDataResource.java index 585e152a5..822ed62c9 100644 --- a/src/main/java/org/radarcns/management/web/rest/SourceDataResource.java +++ b/src/main/java/org/radarcns/management/web/rest/SourceDataResource.java @@ -8,7 +8,7 @@ import org.radarcns.management.service.ResourceUriService; import org.radarcns.management.service.SourceDataService; import org.radarcns.management.service.dto.SourceDataDTO; -import org.radarcns.management.web.rest.errors.CustomConflictException; +import org.radarcns.management.web.rest.errors.ConflictException; import org.radarcns.management.web.rest.util.HeaderUtil; import org.radarcns.management.web.rest.util.PaginationUtil; import org.slf4j.Logger; @@ -41,6 +41,7 @@ import static org.radarcns.auth.authorization.Permission.SOURCEDATA_UPDATE; import static org.radarcns.auth.authorization.RadarAuthorization.checkPermission; import static org.radarcns.management.security.SecurityUtils.getJWT; +import static org.radarcns.management.web.rest.errors.EntityName.SOURCE_DATA; /** * REST controller for managing SourceData. @@ -51,8 +52,6 @@ public class SourceDataResource { private final Logger log = LoggerFactory.getLogger(SourceDataResource.class); - private static final String ENTITY_NAME = "sourceData"; - @Autowired private SourceDataService sourceDataService; @@ -74,18 +73,18 @@ public ResponseEntity createSourceData(@Valid @RequestBody Source log.debug("REST request to save SourceData : {}", sourceDataDto); checkPermission(getJWT(servletRequest), SOURCEDATA_CREATE); if (sourceDataDto.getId() != null) { - return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(ENTITY_NAME, + return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(SOURCE_DATA, "idexists", "A new sourceData cannot already have an ID")).build(); } String name = sourceDataDto.getSourceDataName(); if (sourceDataService.findOneBySourceDataName(name).isPresent()) { - throw new CustomConflictException("error.sourceDataNameAvailable", - Collections.singletonMap("sourceDataName", name), - ResourceUriService.getUri(sourceDataDto)); + throw new ConflictException("SourceData already available with source-name", + SOURCE_DATA, "error.sourceDataNameAvailable", + Collections.singletonMap("sourceDataName", name)); } SourceDataDTO result = sourceDataService.save(sourceDataDto); return ResponseEntity.created(ResourceUriService.getUri(sourceDataDto)) - .headers(HeaderUtil.createEntityCreationAlert(ENTITY_NAME, name)) + .headers(HeaderUtil.createEntityCreationAlert(SOURCE_DATA, name)) .body(result); } @@ -109,7 +108,7 @@ public ResponseEntity updateSourceData(@Valid @RequestBody Source checkPermission(getJWT(servletRequest), SOURCEDATA_UPDATE); SourceDataDTO result = sourceDataService.save(sourceDataDto); return ResponseEntity.ok().headers(HeaderUtil - .createEntityUpdateAlert(ENTITY_NAME, sourceDataDto.getSourceDataName())) + .createEntityUpdateAlert(SOURCE_DATA, sourceDataDto.getSourceDataName())) .body(result); } @@ -165,7 +164,7 @@ public ResponseEntity deleteSourceData(@PathVariable String sourceDataName } sourceDataService.delete(sourceDataDto.get().getId()); return ResponseEntity.ok().headers(HeaderUtil - .createEntityDeletionAlert(ENTITY_NAME, sourceDataName)).build(); + .createEntityDeletionAlert(SOURCE_DATA, sourceDataName)).build(); } } diff --git a/src/main/java/org/radarcns/management/web/rest/SourceTypeResource.java b/src/main/java/org/radarcns/management/web/rest/SourceTypeResource.java index df77da4f0..63c12b937 100644 --- a/src/main/java/org/radarcns/management/web/rest/SourceTypeResource.java +++ b/src/main/java/org/radarcns/management/web/rest/SourceTypeResource.java @@ -1,5 +1,24 @@ package org.radarcns.management.web.rest; +import static org.radarcns.auth.authorization.Permission.SOURCETYPE_CREATE; +import static org.radarcns.auth.authorization.Permission.SOURCETYPE_DELETE; +import static org.radarcns.auth.authorization.Permission.SOURCETYPE_READ; +import static org.radarcns.auth.authorization.Permission.SOURCETYPE_UPDATE; +import static org.radarcns.auth.authorization.RadarAuthorization.checkPermission; +import static org.radarcns.management.security.SecurityUtils.getJWT; +import static org.radarcns.management.web.rest.errors.EntityName.SOURCE_TYPE; + +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; + import com.codahale.metrics.annotation.Timed; import io.github.jhipster.web.util.ResponseUtil; import io.swagger.annotations.ApiParam; @@ -11,9 +30,9 @@ import org.radarcns.management.service.SourceTypeService; import org.radarcns.management.service.dto.ProjectDTO; import org.radarcns.management.service.dto.SourceTypeDTO; -import org.radarcns.management.web.rest.errors.CustomConflictException; -import org.radarcns.management.web.rest.errors.CustomParameterizedException; +import org.radarcns.management.web.rest.errors.ConflictException; import org.radarcns.management.web.rest.errors.ErrorConstants; +import org.radarcns.management.web.rest.errors.InvalidRequestException; import org.radarcns.management.web.rest.util.HeaderUtil; import org.radarcns.management.web.rest.util.PaginationUtil; import org.slf4j.Logger; @@ -33,22 +52,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import javax.validation.Valid; -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -import static org.radarcns.auth.authorization.Permission.SOURCETYPE_CREATE; -import static org.radarcns.auth.authorization.Permission.SOURCETYPE_DELETE; -import static org.radarcns.auth.authorization.Permission.SOURCETYPE_READ; -import static org.radarcns.auth.authorization.Permission.SOURCETYPE_UPDATE; -import static org.radarcns.auth.authorization.RadarAuthorization.checkPermission; -import static org.radarcns.management.security.SecurityUtils.getJWT; - /** * REST controller for managing SourceType. */ @@ -58,7 +61,7 @@ public class SourceTypeResource { private final Logger log = LoggerFactory.getLogger(SourceTypeResource.class); - private static final String ENTITY_NAME = "sourceType"; + @Autowired private SourceTypeService sourceTypeService; @@ -84,7 +87,7 @@ public ResponseEntity createSourceType(@Valid @RequestBody log.debug("REST request to save SourceType : {}", sourceTypeDto); checkPermission(getJWT(servletRequest), SOURCETYPE_CREATE); if (sourceTypeDto.getId() != null) { - return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(ENTITY_NAME, + return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(SOURCE_TYPE, "idexists", "A new sourceType cannot already have an ID")).build(); } Optional existing = sourceTypeRepository @@ -99,12 +102,13 @@ public ResponseEntity createSourceType(@Valid @RequestBody errorParams.put("producer", sourceTypeDto.getProducer()); errorParams.put("model", sourceTypeDto.getModel()); errorParams.put("catalogVersion", sourceTypeDto.getCatalogVersion()); - throw new CustomConflictException(ErrorConstants.ERR_SOURCE_TYPE_EXISTS, errorParams, - ResourceUriService.getUri(sourceTypeDto)); + throw new ConflictException("A SourceType with the specified producer, model and" + + "version already exists. This combination needs to be unique.", SOURCE_TYPE, + ErrorConstants.ERR_SOURCE_TYPE_EXISTS, errorParams); } SourceTypeDTO result = sourceTypeService.save(sourceTypeDto); return ResponseEntity.created(ResourceUriService.getUri(result)) - .headers(HeaderUtil.createEntityCreationAlert(ENTITY_NAME, displayName(result))) + .headers(HeaderUtil.createEntityCreationAlert(SOURCE_TYPE, displayName(result))) .body(result); } @@ -129,7 +133,7 @@ public ResponseEntity updateSourceType(@Valid @RequestBody SourceTypeDTO result = sourceTypeService.save(sourceTypeDto); return ResponseEntity.ok() .headers( - HeaderUtil.createEntityUpdateAlert(ENTITY_NAME, displayName(sourceTypeDto))) + HeaderUtil.createEntityUpdateAlert(SOURCE_TYPE, displayName(sourceTypeDto))) .body(result); } @@ -224,14 +228,17 @@ public ResponseEntity deleteSourceType(@PathVariable String producer, List projects = sourceTypeService.findProjectsBySourceType(producer, model, version); if (!projects.isEmpty()) { - throw new CustomParameterizedException(ErrorConstants.ERR_SOURCE_TYPE_IN_USE, - projects.stream() - .map(p -> p.getProjectName()) - .reduce((s1, s2) -> String.join(", ", s1, s2)) - .get()); // we know the list is not empty so calling get() is safe here + throw new InvalidRequestException( + // we know the list is not empty so calling get() is safe here + "Cannot delete a source-type that " + "is being used by project(s)", SOURCE_TYPE, + ErrorConstants.ERR_SOURCE_TYPE_IN_USE, Collections.singletonMap("project-names", + projects + .stream() + .map(ProjectDTO::getProjectName) + .collect(Collectors.joining("-")))); } sourceTypeService.delete(sourceTypeDto.getId()); - return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, + return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(SOURCE_TYPE, displayName(sourceTypeDto))).build(); } diff --git a/src/main/java/org/radarcns/management/web/rest/SubjectResource.java b/src/main/java/org/radarcns/management/web/rest/SubjectResource.java index 9ca234194..dd64e0a3f 100644 --- a/src/main/java/org/radarcns/management/web/rest/SubjectResource.java +++ b/src/main/java/org/radarcns/management/web/rest/SubjectResource.java @@ -1,5 +1,35 @@ package org.radarcns.management.web.rest; +import static org.radarcns.auth.authorization.AuthoritiesConstants.INACTIVE_PARTICIPANT; +import static org.radarcns.auth.authorization.AuthoritiesConstants.PARTICIPANT; +import static org.radarcns.auth.authorization.Permission.SOURCE_UPDATE; +import static org.radarcns.auth.authorization.Permission.SUBJECT_CREATE; +import static org.radarcns.auth.authorization.Permission.SUBJECT_DELETE; +import static org.radarcns.auth.authorization.Permission.SUBJECT_READ; +import static org.radarcns.auth.authorization.Permission.SUBJECT_UPDATE; +import static org.radarcns.auth.authorization.RadarAuthorization.checkPermission; +import static org.radarcns.auth.authorization.RadarAuthorization.checkPermissionOnProject; +import static org.radarcns.auth.authorization.RadarAuthorization.checkPermissionOnSubject; +import static org.radarcns.management.security.SecurityUtils.getJWT; +import static org.radarcns.management.web.rest.errors.EntityName.SOURCE; +import static org.radarcns.management.web.rest.errors.EntityName.SOURCE_TYPE; +import static org.radarcns.management.web.rest.errors.EntityName.SUBJECT; +import static org.radarcns.management.web.rest.errors.ErrorConstants.ERR_ACTIVE_PARTICIPANT_PROJECT_NOT_FOUND; +import static org.radarcns.management.web.rest.errors.ErrorConstants.ERR_SOURCE_TYPE_NOT_PROVIDED; +import static org.radarcns.management.web.rest.errors.ErrorConstants.ERR_VALIDATION; + +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.servlet.http.HttpServletRequest; + import com.codahale.metrics.annotation.Timed; import io.github.jhipster.web.util.ResponseUtil; import io.swagger.annotations.ApiParam; @@ -8,7 +38,7 @@ import org.radarcns.auth.config.Constants; import org.radarcns.auth.exception.NotAuthorizedException; import org.radarcns.auth.token.RadarToken; -import org.radarcns.management.domain.Role; +import org.radarcns.management.domain.Project; import org.radarcns.management.domain.Source; import org.radarcns.management.domain.SourceType; import org.radarcns.management.domain.Subject; @@ -22,12 +52,12 @@ import org.radarcns.management.service.SubjectService; import org.radarcns.management.service.dto.MinimalSourceDetailsDTO; import org.radarcns.management.service.dto.RevisionDTO; -import org.radarcns.management.service.dto.SourceTypeDTO; import org.radarcns.management.service.dto.SubjectDTO; import org.radarcns.management.service.mapper.SubjectMapper; -import org.radarcns.management.web.rest.errors.CustomNotFoundException; -import org.radarcns.management.web.rest.errors.CustomParameterizedException; +import org.radarcns.management.web.rest.errors.BadRequestException; import org.radarcns.management.web.rest.errors.ErrorConstants; +import org.radarcns.management.web.rest.errors.InvalidRequestException; +import org.radarcns.management.web.rest.errors.NotFoundException; import org.radarcns.management.web.rest.util.HeaderUtil; import org.radarcns.management.web.rest.util.PaginationUtil; import org.slf4j.Logger; @@ -50,30 +80,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import java.net.URISyntaxException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.radarcns.auth.authorization.AuthoritiesConstants.INACTIVE_PARTICIPANT; -import static org.radarcns.auth.authorization.AuthoritiesConstants.PARTICIPANT; -import static org.radarcns.auth.authorization.Permission.SOURCE_UPDATE; -import static org.radarcns.auth.authorization.Permission.SUBJECT_CREATE; -import static org.radarcns.auth.authorization.Permission.SUBJECT_DELETE; -import static org.radarcns.auth.authorization.Permission.SUBJECT_READ; -import static org.radarcns.auth.authorization.Permission.SUBJECT_UPDATE; -import static org.radarcns.auth.authorization.RadarAuthorization.checkPermission; -import static org.radarcns.auth.authorization.RadarAuthorization.checkPermissionOnProject; -import static org.radarcns.auth.authorization.RadarAuthorization.checkPermissionOnSubject; -import static org.radarcns.management.security.SecurityUtils.getJWT; - /** * REST controller for managing Subject. */ @@ -83,8 +89,6 @@ public class SubjectResource { private final Logger log = LoggerFactory.getLogger(SubjectResource.class); - private static final String ENTITY_NAME = "subject"; - @Autowired private SubjectService subjectService; @@ -127,7 +131,7 @@ public ResponseEntity createSubject(@RequestBody SubjectDTO subjectD log.debug("REST request to save Subject : {}", subjectDto); if (subjectDto.getProject() == null || subjectDto.getProject().getId() == null) { return ResponseEntity.badRequest().headers(HeaderUtil - .createFailureAlert(ENTITY_NAME, "projectrequired", + .createFailureAlert(SUBJECT, "projectrequired", "A subject should be assigned to a project")).build(); } checkPermissionOnProject(getJWT(servletRequest), SUBJECT_CREATE, @@ -135,12 +139,12 @@ public ResponseEntity createSubject(@RequestBody SubjectDTO subjectD if (subjectDto.getId() != null) { return ResponseEntity.badRequest().headers(HeaderUtil - .createFailureAlert(ENTITY_NAME, "idexists", + .createFailureAlert(SUBJECT, "idexists", "A new subject cannot already have an ID")).build(); } if (subjectDto.getLogin() == null) { return ResponseEntity.badRequest().headers(HeaderUtil - .createFailureAlert(ENTITY_NAME, "loginrequired", + .createFailureAlert(SUBJECT, "loginrequired", "A subject login is required")) .build(); } @@ -148,14 +152,14 @@ public ResponseEntity createSubject(@RequestBody SubjectDTO subjectD && subjectRepository.findOneByProjectNameAndExternalId(subjectDto.getProject() .getProjectName(), subjectDto.getExternalId()).isPresent()) { return ResponseEntity.badRequest().headers(HeaderUtil - .createFailureAlert(ENTITY_NAME, "subjectExists", + .createFailureAlert(SUBJECT, "subjectExists", "A subject with given project-id and external-id already exists")) .build(); } SubjectDTO result = subjectService.createSubject(subjectDto); return ResponseEntity.created(ResourceUriService.getUri(subjectDto)) - .headers(HeaderUtil.createEntityCreationAlert(ENTITY_NAME, result.getLogin())) + .headers(HeaderUtil.createEntityCreationAlert(SUBJECT, result.getLogin())) .body(result); } @@ -175,7 +179,7 @@ public ResponseEntity updateSubject(@RequestBody SubjectDTO subjectD log.debug("REST request to update Subject : {}", subjectDto); if (subjectDto.getProject() == null || subjectDto.getProject().getId() == null) { return ResponseEntity.badRequest().headers(HeaderUtil - .createFailureAlert(ENTITY_NAME, "projectrequired", + .createFailureAlert(SUBJECT, "projectrequired", "A subject should be assigned to a project")).body(null); } if (subjectDto.getId() == null) { @@ -185,7 +189,7 @@ public ResponseEntity updateSubject(@RequestBody SubjectDTO subjectD subjectDto.getProject().getProjectName(), subjectDto.getLogin()); SubjectDTO result = subjectService.updateSubject(subjectDto); return ResponseEntity.ok() - .headers(HeaderUtil.createEntityUpdateAlert(ENTITY_NAME, subjectDto.getLogin())) + .headers(HeaderUtil.createEntityUpdateAlert(SUBJECT, subjectDto.getLogin())) .body(result); } @@ -206,13 +210,13 @@ public ResponseEntity discontinueSubject(@RequestBody SubjectDTO sub log.debug("REST request to update Subject : {}", subjectDto); if (subjectDto.getId() == null) { return ResponseEntity.badRequest().headers(HeaderUtil - .createFailureAlert(ENTITY_NAME, "subjectNotAvailable", "No subject found")) + .createFailureAlert(SUBJECT, "subjectNotAvailable", "No subject found")) .body(null); } if (subjectDto.getProject() == null || subjectDto.getProject().getId() == null) { return ResponseEntity.badRequest().headers(HeaderUtil - .createFailureAlert(ENTITY_NAME, "projectrequired", + .createFailureAlert(SUBJECT, "projectrequired", "A subject should be assigned to a project")).body(null); } checkPermissionOnSubject(getJWT(servletRequest), SUBJECT_UPDATE, @@ -224,7 +228,7 @@ public ResponseEntity discontinueSubject(@RequestBody SubjectDTO sub "SUBJECT_DISCONTINUE", "subject_login=" + subjectDto.getLogin())); SubjectDTO result = subjectService.discontinueSubject(subjectDto); return ResponseEntity.ok() - .headers(HeaderUtil.createEntityUpdateAlert(ENTITY_NAME, subjectDto.getLogin())) + .headers(HeaderUtil.createEntityUpdateAlert(SUBJECT, subjectDto.getLogin())) .body(result); } @@ -294,11 +298,8 @@ public ResponseEntity> getAllSubjects(@ApiParam Pageable pageab public ResponseEntity getSubject(@PathVariable String login) throws NotAuthorizedException { log.debug("REST request to get Subject : {}", login); - Optional subject = subjectRepository.findOneWithEagerBySubjectLogin(login); - if (!subject.isPresent()) { - return ResponseEntity.notFound().build(); - } - SubjectDTO subjectDto = subjectMapper.subjectToSubjectDTO(subject.get()); + Subject subject = subjectService.findOneByLogin(login); + SubjectDTO subjectDto = subjectMapper.subjectToSubjectDTO(subject); checkPermissionOnSubject(getJWT(servletRequest), SUBJECT_READ, subjectDto.getProject() .getProjectName(), subjectDto.getLogin()); return ResponseUtil.wrapOrNotFound(Optional.ofNullable(subjectDto)); @@ -369,16 +370,14 @@ public ResponseEntity getSubjectRevision(@PathVariable String login, public ResponseEntity deleteSubject(@PathVariable String login) throws NotAuthorizedException { log.debug("REST request to delete Subject : {}", login); - Optional subject = subjectRepository.findOneWithEagerBySubjectLogin(login); - if (!subject.isPresent()) { - return ResponseEntity.notFound().build(); - } - SubjectDTO subjectDto = subjectMapper.subjectToSubjectDTO(subject.get()); + Subject subject = subjectService.findOneByLogin(login); + + SubjectDTO subjectDto = subjectMapper.subjectToSubjectDTO(subject); checkPermissionOnSubject(getJWT(servletRequest), SUBJECT_DELETE, subjectDto.getProject() .getProjectName(), subjectDto.getLogin()); subjectService.deleteSubject(login); return ResponseEntity.ok() - .headers(HeaderUtil.createEntityDeletionAlert(ENTITY_NAME, login)).build(); + .headers(HeaderUtil.createEntityDeletionAlert(SUBJECT, login)).build(); } /** @@ -410,65 +409,44 @@ public ResponseEntity deleteSubject(@PathVariable String login) public ResponseEntity assignSources(@PathVariable String login, @RequestBody MinimalSourceDetailsDTO sourceDto) throws URISyntaxException, NotAuthorizedException { - // check the subject id - Optional subject = subjectRepository.findOneWithEagerBySubjectLogin(login); - if (!subject.isPresent()) { - return ResponseEntity.notFound().build(); - } - Subject sub = subject.get(); - // find the PARTICIPANT role for this subject - Optional roleOptional = sub.getUser().getRoles().stream() - .filter(r -> r.getAuthority().getName().equals(PARTICIPANT)) - .findFirst(); - if (!roleOptional.isPresent()) { - // no participant role found - HashMap params = new HashMap<>(); - params.put("message", "Supplied login is not a participant in any study, a source can" - + "not be assigned."); - params.put("login", login); - throw new CustomParameterizedException("error.loginNotParticipant", params); - } - Role role = roleOptional.get(); + // find out source type id of supplied source Long sourceTypeId = sourceDto.getSourceTypeId(); if (sourceTypeId == null) { // check if combination (producer, model, version) is present - try { - String producer = Objects.requireNonNull(sourceDto.getSourceTypeProducer(), - ErrorConstants.ERR_VALIDATION); - String model = Objects.requireNonNull(sourceDto.getSourceTypeModel(), - ErrorConstants.ERR_VALIDATION); - String version = Objects.requireNonNull(sourceDto.getSourceTypeCatalogVersion(), - ErrorConstants.ERR_VALIDATION); - SourceTypeDTO sourceTypeDto = sourceTypeService - .findByProducerAndModelAndVersion(producer, model, version); - if (Objects.isNull(sourceTypeDto)) { - return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert( - ENTITY_NAME, "sourceNotFound", String.join(" ", producer, model, - version))).build(); - } - sourceTypeId = sourceTypeDto.getId(); - } catch (NullPointerException ex) { - log.error(ex.getMessage() + ", supplied sourceDto: " + sourceDto.toString()); - throw new CustomParameterizedException(ex.getMessage(), - Collections.singletonMap("message", "You must supply either the " - + "sourceTypeId, or the combination of " - + "(sourceTypeProducer, sourceTypeModel, catalogVersion) fields.")); + if (sourceDto.getSourceTypeProducer() == null + || sourceDto.getSourceTypeModel() == null + || sourceDto.getSourceTypeCatalogVersion() == null) { + throw new BadRequestException("Producer or model or version value for the " + + "source-type is null" , SOURCE_TYPE, ERR_VALIDATION); } + sourceTypeId = sourceTypeService + .findByProducerAndModelAndVersion( + sourceDto.getSourceTypeProducer(), + sourceDto.getSourceTypeModel(), + sourceDto.getSourceTypeCatalogVersion()).getId(); + } - // find whether the relevant source-type is available in the subject's project - Optional sourceType; - sourceType = projectRepository.findSourceTypeByProjectIdAndSourceTypeId( - role.getProject().getId(), sourceTypeId); - - if (!sourceType.isPresent()) { - // return bad request - return ResponseEntity.status(HttpStatus.BAD_REQUEST).headers(HeaderUtil - .createAlert("sourceTypeNotAvailable", - "The source type is not registered in the given project")).build(); - } - checkPermissionOnSubject(getJWT(servletRequest), SUBJECT_UPDATE, role.getProject() + // check the subject id + Subject sub = subjectService.findOneByLogin(login); + + // find the actively assigned project for this subject + + Project currentProject = sub.getActiveProject() + .orElseThrow(() -> + new InvalidRequestException("Requested subject does not have an active project", + SUBJECT, ERR_ACTIVE_PARTICIPANT_PROJECT_NOT_FOUND)); + + // find whether the relevant source-type is available in the subject's project + SourceType sourceType = projectRepository + .findSourceTypeByProjectIdAndSourceTypeId(currentProject.getId(), sourceTypeId) + .orElseThrow(() -> new BadRequestException("No valid source-type found for project." + + " You must provide either valid source-type id or producer, model, version of a " + + "source-type that is assigned to project", SUBJECT, ERR_SOURCE_TYPE_NOT_PROVIDED) + ); + + checkPermissionOnSubject(getJWT(servletRequest), SUBJECT_UPDATE, currentProject .getProjectName(), sub.getUser().getLogin()); // check if any of id, sourceID, sourceName were non-null @@ -478,18 +456,19 @@ public ResponseEntity assignSources(@PathVariable Strin // handle the source registration MinimalSourceDetailsDTO sourceRegistered = subjectService - .assignOrUpdateSource(sub, sourceType.get(), role.getProject(), sourceDto); + .assignOrUpdateSource(sub, sourceType, currentProject, sourceDto); // Return the correct response type, either created if a new source was created, or ok if // an existing source was provided. If an existing source was given but not found, the // assignOrUpdateSource would throw an error and we would not reach this point. if (!existing) { return ResponseEntity.created(ResourceUriService.getUri(sourceRegistered)) - .headers(HeaderUtil.createEntityUpdateAlert(ENTITY_NAME, login)) + .headers(HeaderUtil.createEntityCreationAlert(SOURCE, + sourceRegistered.getSourceName())) .body(sourceRegistered); } else { - return ResponseEntity.ok().headers(HeaderUtil.createEntityUpdateAlert(ENTITY_NAME, - login)).body(sourceRegistered); + return ResponseEntity.ok().headers(HeaderUtil.createEntityUpdateAlert(SOURCE, + sourceRegistered.getSourceName())).body(sourceRegistered); } } @@ -540,7 +519,7 @@ public ResponseEntity> getSubjectSources( * * @param attributes The {@link Map} specification * @return The {@link MinimalSourceDetailsDTO} completed with all identifying fields. - * @throws CustomNotFoundException if the subject or the source not found using given ids. + * @throws NotFoundException if the subject or the source not found using given ids. */ @PostMapping("/subjects/{login:" + Constants.ENTITY_ID_REGEX + "}/sources/{sourceName:" + Constants.ENTITY_ID_REGEX + "}") @@ -552,15 +531,13 @@ public ResponseEntity> getSubjectSources( @Timed public ResponseEntity updateSubjectSource(@PathVariable String login, @PathVariable String sourceName, @RequestBody Map attributes) - throws CustomNotFoundException, NotAuthorizedException, + throws NotFoundException, NotAuthorizedException, URISyntaxException { // check the subject id Optional subject = subjectRepository.findOneWithEagerBySubjectLogin(login); if (!subject.isPresent()) { - Map errorParams = new HashMap<>(); - errorParams.put("message", "Subject ID not found"); - errorParams.put("subjectLogin", login); - throw new CustomNotFoundException(ErrorConstants.ERR_SUBJECT_NOT_FOUND, errorParams); + throw new NotFoundException("Subject ID not found", SUBJECT, ErrorConstants + .ERR_SUBJECT_NOT_FOUND, Collections.singletonMap("subjectLogin", login)); } // check the permission to update source SubjectDTO subjectDto = subjectMapper.subjectToSubjectDTO(subject.get()); @@ -575,10 +552,10 @@ public ResponseEntity updateSubjectSource(@PathVariable // exception if source is not found under subject if (sources.isEmpty()) { Map errorParams = new HashMap<>(); - errorParams.put("message", "Source not found under assigned sources of subject"); errorParams.put("subjectLogin", login); errorParams.put("sourceName", sourceName); - throw new CustomNotFoundException(ErrorConstants.ERR_SUBJECT_NOT_FOUND, errorParams); + throw new NotFoundException("Source not found under assigned sources of " + + "subject", SUBJECT, ErrorConstants.ERR_SUBJECT_NOT_FOUND, errorParams); } // there should be only one source under a source-name. diff --git a/src/main/java/org/radarcns/management/web/rest/UserResource.java b/src/main/java/org/radarcns/management/web/rest/UserResource.java index e4e9b44b7..93d2cb4b3 100644 --- a/src/main/java/org/radarcns/management/web/rest/UserResource.java +++ b/src/main/java/org/radarcns/management/web/rest/UserResource.java @@ -15,7 +15,7 @@ import org.radarcns.management.service.UserService; import org.radarcns.management.service.dto.ProjectDTO; import org.radarcns.management.service.dto.UserDTO; -import org.radarcns.management.web.rest.errors.CustomParameterizedException; +import org.radarcns.management.web.rest.errors.InvalidRequestException; import org.radarcns.management.web.rest.util.HeaderUtil; import org.radarcns.management.web.rest.util.PaginationUtil; import org.radarcns.management.web.rest.vm.ManagedUserVM; @@ -47,6 +47,7 @@ import static org.radarcns.auth.authorization.Permission.USER_UPDATE; import static org.radarcns.auth.authorization.RadarAuthorization.checkPermission; import static org.radarcns.management.security.SecurityUtils.getJWT; +import static org.radarcns.management.web.rest.errors.EntityName.USER; /** * REST controller for managing users. @@ -79,8 +80,6 @@ public class UserResource { private final Logger log = LoggerFactory.getLogger(UserResource.class); - private static final String ENTITY_NAME = "userManagement"; - @Autowired private UserRepository userRepository; @@ -114,7 +113,7 @@ public ResponseEntity createUser(@RequestBody ManagedUserVM managedUserVm) checkPermission(getJWT(servletRequest), USER_CREATE); if (managedUserVm.getId() != null) { return ResponseEntity.badRequest() - .headers(HeaderUtil.createFailureAlert(ENTITY_NAME, "idexists", + .headers(HeaderUtil.createFailureAlert(USER, "idexists", "A new user cannot already have an ID")) .body(null); // Lowercase the user login before comparing with database @@ -122,18 +121,18 @@ public ResponseEntity createUser(@RequestBody ManagedUserVM managedUserVm) .isPresent()) { return ResponseEntity.badRequest() .headers(HeaderUtil - .createFailureAlert(ENTITY_NAME, "userexists", + .createFailureAlert(USER, "userexists", "Login already in use")) .body(null); } else if (userRepository.findOneByEmail(managedUserVm.getEmail()).isPresent()) { return ResponseEntity.badRequest() .headers(HeaderUtil - .createFailureAlert(ENTITY_NAME, "emailexists", + .createFailureAlert(USER, "emailexists", "Email already in use")) .body(null); } else if (managedUserVm.getRoles() == null || managedUserVm.getRoles().isEmpty()) { return ResponseEntity.badRequest() - .headers(HeaderUtil.createFailureAlert(ENTITY_NAME, "rolesRequired", + .headers(HeaderUtil.createFailureAlert(USER, "rolesRequired", "One or more roles are required")) .body(null); } else { @@ -163,19 +162,19 @@ public ResponseEntity updateUser(@RequestBody ManagedUserVM managedUser if (existingUser.isPresent() && (!existingUser.get().getId() .equals(managedUserVm.getId()))) { return ResponseEntity.badRequest().headers(HeaderUtil - .createFailureAlert(ENTITY_NAME, "emailexists", "Email already in use")) + .createFailureAlert(USER, "emailexists", "Email already in use")) .body(null); } existingUser = userRepository.findOneByLogin(managedUserVm.getLogin().toLowerCase()); if (existingUser.isPresent() && (!existingUser.get().getId() .equals(managedUserVm.getId()))) { return ResponseEntity.badRequest().headers(HeaderUtil - .createFailureAlert(ENTITY_NAME, "userexists", "Login already in use")) + .createFailureAlert(USER, "userexists", "Login already in use")) .body(null); } if (managedUserVm.getRoles() == null || managedUserVm.getRoles().isEmpty()) { return ResponseEntity.badRequest() - .headers(HeaderUtil.createEntityUpdateAlert(ENTITY_NAME, "rolesRequired")) + .headers(HeaderUtil.createEntityUpdateAlert(USER, "rolesRequired")) .body(null); } @@ -183,7 +182,8 @@ public ResponseEntity updateUser(@RequestBody ManagedUserVM managedUser .findOneWithEagerBySubjectLogin(managedUserVm.getLogin()); if (subject.isPresent() && managedUserVm.isActivated() && subject.get().isRemoved()) { // if the subject is also a user, check if the removed/activated states are valid - throw new CustomParameterizedException("error.invalidsubjectstate"); + throw new InvalidRequestException("Subject cannot be the user to request " + + "this changes", USER, "error.invalidsubjectstate"); } diff --git a/src/main/java/org/radarcns/management/web/rest/errors/BadRequestException.java b/src/main/java/org/radarcns/management/web/rest/errors/BadRequestException.java new file mode 100644 index 000000000..cce5cd74e --- /dev/null +++ b/src/main/java/org/radarcns/management/web/rest/errors/BadRequestException.java @@ -0,0 +1,37 @@ +package org.radarcns.management.web.rest.errors; + +import java.util.Map; +import javax.ws.rs.core.Response; + +/** + * The request could not be understood by the server due to malformed syntax. + * The client SHOULD NOT repeat the request without modifications. + * (e.g. malformed request syntax, size too large, invalid request message framing, + * or deceptive request routing). + */ +public class BadRequestException extends RadarWebApplicationException { + + /** + * Create a BadRequestException with the given message, relatedEntityName, errorCode. + * + * @param message the message. + * @param entityName relatedEntityName from {@link EntityName}. + * @param errorCode errorCode from {@link ErrorConstants} + */ + public BadRequestException(String message, String entityName, String errorCode) { + super(Response.Status.BAD_REQUEST, message, entityName, errorCode); + } + + /** + * Create a BadRequestException with the given message, relatedEntityName, errorCode. + * + * @param message the message. + * @param entityName relatedEntityName from {@link EntityName}. + * @param errorCode errorCode from {@link ErrorConstants} + * @param paramMap map of additional information. + */ + public BadRequestException(String message, String entityName, String errorCode, + Map paramMap) { + super(Response.Status.BAD_REQUEST, message, entityName, errorCode, paramMap); + } +} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/ConflictException.java b/src/main/java/org/radarcns/management/web/rest/errors/ConflictException.java new file mode 100644 index 000000000..55a5ab31d --- /dev/null +++ b/src/main/java/org/radarcns/management/web/rest/errors/ConflictException.java @@ -0,0 +1,43 @@ +package org.radarcns.management.web.rest.errors; + +import java.util.Map; +import javax.ws.rs.core.Response; + + +/** + * Created by dverbeec on 13/09/2017. + *

+ * The request could not be completed due to a conflict with the current state of the resource. + * This code is only allowed in situations where it is expected that the user might be able to + * resolve the conflict and resubmit the request. The response body SHOULD include enough + * information for the user to recognize the source of the conflict. Ideally, the response + * entity would include enough information for the user or user agent to fix the problem; + * however, that might not be possible and is not required. + *

+ */ +public class ConflictException extends RadarWebApplicationException { + + /** + * Create a {@link ConflictException} with the given message, relatedEntityName, errorCode. + * + * @param message the message. + * @param entityName relatedEntityName from {@link EntityName}. + * @param errorCode errorCode from {@link ErrorConstants} + */ + public ConflictException(String message, String entityName, String errorCode) { + super(Response.Status.CONFLICT, message, entityName, errorCode); + } + + /** + * Create a {@link ConflictException} with the given message, relatedEntityName, errorCode. + * + * @param message the message. + * @param entityName relatedEntityName from {@link EntityName}. + * @param errorCode errorCode from {@link ErrorConstants} + * @param paramMap map of additional information. + */ + public ConflictException(String message, String entityName, String errorCode, + Map paramMap) { + super(Response.Status.CONFLICT, message, entityName, errorCode, paramMap); + } +} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/CustomConflictException.java b/src/main/java/org/radarcns/management/web/rest/errors/CustomConflictException.java deleted file mode 100644 index 4853bc150..000000000 --- a/src/main/java/org/radarcns/management/web/rest/errors/CustomConflictException.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.radarcns.management.web.rest.errors; - -import java.net.URI; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -/** - * Created by dverbeec on 13/09/2017. - */ -public class CustomConflictException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - private final String message; - private final URI conflictingResource; - private final Map paramMap = new HashMap<>(); - - /** - * Create a custom conflict exception with the given message, parameter map, and url to the - * conflicting resource. - * @param message the message - * @param paramMap the parameter map - * @param conflictingResource the conflicting resource - */ - public CustomConflictException(String message, Map paramMap, - URI conflictingResource) { - super(message); - Objects.requireNonNull(message, "message can not be null"); - Objects.requireNonNull(paramMap, "paramMap can not be null"); - Objects.requireNonNull(conflictingResource, "conflictingResource can not be null"); - this.message = message; - this.conflictingResource = conflictingResource; - // add default timestamp first, so a timestamp key in the paramMap will overwrite it - this.paramMap.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") - .format(new Date())); - this.paramMap.put("conflict", conflictingResource.toString()); - this.paramMap.putAll(paramMap); - } - - public URI getConflictingResource() { - return conflictingResource; - } - - public ParameterizedErrorVM getErrorVM() { - return new ParameterizedErrorVM(message, paramMap); - } -} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/CustomNotFoundException.java b/src/main/java/org/radarcns/management/web/rest/errors/CustomNotFoundException.java deleted file mode 100644 index 7e50987f8..000000000 --- a/src/main/java/org/radarcns/management/web/rest/errors/CustomNotFoundException.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.radarcns.management.web.rest.errors; - -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -/** - * Created by dverbeec on 7/09/2017. - */ -public class CustomNotFoundException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - private final String message; - - private final Map paramMap = new HashMap<>(); - - /** - * Create a not found exception with the given message and parameter map. - * @param message the message - * @param paramMap the parameter map - */ - public CustomNotFoundException(String message, Map paramMap) { - super(message); - this.message = message; - this.paramMap.putAll(paramMap); - this.paramMap - .put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); - } - - public ParameterizedErrorVM getErrorVM() { - return new ParameterizedErrorVM(message, paramMap); - } -} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/CustomParameterizedException.java b/src/main/java/org/radarcns/management/web/rest/errors/CustomParameterizedException.java deleted file mode 100644 index 1798213e8..000000000 --- a/src/main/java/org/radarcns/management/web/rest/errors/CustomParameterizedException.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.radarcns.management.web.rest.errors; - -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -/** - * Custom, parameterized exception, which can be translated on the client side. - * - *

For example:

- * - *

{@code throw new CustomParameterizedException("error.myCustomError", "hello", "world")}

- * - *

can be translated with:

- * - *

{@code "error.myCustomError" : "The server says {{param0}} to {{param1}}"}

- */ -public class CustomParameterizedException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - private static final String PARAM = "param"; - - private final String message; - - private final Map paramMap = new HashMap<>(); - - /** - * Create an error with the given message and parameters. The parameters passed in will go - * into the parameter with as values, with keys being {@code param0, param1, ...}. - * @param message the error message - * @param params the error parameters - */ - public CustomParameterizedException(String message, String... params) { - super(message); - this.message = message; - // add default timestamp first, so a timestamp key in the paramMap will overwrite it - this.paramMap.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") - .format(new Date())); - if (params != null && params.length > 0) { - for (int i = 0; i < params.length; i++) { - paramMap.put(PARAM + i, params[i]); - } - } - } - - /** - * Create an error with the given message and parameter map. - * @param message the error message - * @param paramMap the parameter map - */ - public CustomParameterizedException(String message, Map paramMap) { - super(message); - this.message = message; - // add default timestamp first, so a timestamp key in the paramMap will overwrite it - this.paramMap.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") - .format(new Date())); - this.paramMap.putAll(paramMap); - } - - public ParameterizedErrorVM getErrorVM() { - return new ParameterizedErrorVM(message, paramMap); - } -} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/CustomServerException.java b/src/main/java/org/radarcns/management/web/rest/errors/CustomServerException.java deleted file mode 100644 index 887ec3dc5..000000000 --- a/src/main/java/org/radarcns/management/web/rest/errors/CustomServerException.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.radarcns.management.web.rest.errors; - -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -/** - * Custom exception to throw runtime errors that will be translated to an error response with - * status code 500. The parameter map can contain a key {@code message} whose value will be - * displayed in the ManagementPortalApp. - */ -public class CustomServerException extends RuntimeException { - private static final long serialVersionUID = 1L; - - private final String message; - - private final Map paramMap = new HashMap<>(); - - /** - * Create a not found exception with the given message and parameter map. - * @param message the message - * @param paramMap the parameter map - */ - public CustomServerException(String message, Map paramMap) { - super(message); - this.message = message; - this.paramMap.putAll(paramMap); - this.paramMap - .put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); - } - - public ParameterizedErrorVM getErrorVM() { - return new ParameterizedErrorVM(message, paramMap); - } -} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/EntityName.java b/src/main/java/org/radarcns/management/web/rest/errors/EntityName.java new file mode 100644 index 000000000..ed31e2d3c --- /dev/null +++ b/src/main/java/org/radarcns/management/web/rest/errors/EntityName.java @@ -0,0 +1,15 @@ +package org.radarcns.management.web.rest.errors; + +public interface EntityName { + + String OAUTH_CLIENT = "oauthClient"; + String SOURCE_DATA = "sourceData"; + String SOURCE_TYPE = "sourceType"; + String SUBJECT = "subject"; + String USER = "userManagement"; + String SOURCE = "source"; + String META_TOKEN = "meta-token"; + String PROJECT = "project"; + String REVISION = "revision"; + +} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/ErrorConstants.java b/src/main/java/org/radarcns/management/web/rest/errors/ErrorConstants.java index 7f665e6f7..4945800ef 100644 --- a/src/main/java/org/radarcns/management/web/rest/errors/ErrorConstants.java +++ b/src/main/java/org/radarcns/management/web/rest/errors/ErrorConstants.java @@ -16,12 +16,19 @@ public final class ErrorConstants { public static final String ERR_SOURCE_NAME_EXISTS = "error.sourceNameExists"; public static final String ERR_SOURCE_NOT_FOUND = "error.sourceNotFound"; public static final String ERR_SOURCE_TYPE_IN_USE = "error.sourceTypeInUse"; + public static final String ERR_SOURCE_TYPE_NOT_FOUND = "error.sourceTypeNotFound"; public static final String ERR_INVALID_AUTHORITY = "error.invalidAuthority"; public static final String ERR_PROJECT_ID_NOT_FOUND = "error.projectIdNotFound"; public static final String ERR_PROJECT_NAME_NOT_FOUND = "error.projectNameNotFound"; public static final String ERR_REVISIONS_NOT_FOUND = "error.revisionsNotFound"; - public static final String ERR_INTERNAL = "error.internalParameterized"; public static final String ERR_ENTITY_NOT_FOUND = "error.entityNotFound"; + public static final String ERR_TOKEN_NOT_FOUND = "error.tokenNotFound"; + public static final String ERR_SOURCE_TYPE_NOT_PROVIDED = "error.sourceTypeNotProvided"; + public static final String ERR_ACTIVE_PARTICIPANT_PROJECT_NOT_FOUND = "error" + + ".activeParticipantProjectNotFound"; + public static final String ERR_NO_VALID_PRIVACY_POLICY_URL_CONFIGURED = "error" + + ".noValidPrivacyPolicyUrl"; + public static final String ERR_NO_SUCH_CLIENT = "error.noSuchClient"; private ErrorConstants() { } diff --git a/src/main/java/org/radarcns/management/web/rest/errors/ErrorVM.java b/src/main/java/org/radarcns/management/web/rest/errors/ErrorVM.java index 857ad817a..2488c23db 100644 --- a/src/main/java/org/radarcns/management/web/rest/errors/ErrorVM.java +++ b/src/main/java/org/radarcns/management/web/rest/errors/ErrorVM.java @@ -25,18 +25,6 @@ public ErrorVM(String message, String description) { this.description = description; } - /** - * Initialize with the given message, description and field errors. - * @param message the message - * @param description the description - * @param fieldErrors the field errors - */ - public ErrorVM(String message, String description, List fieldErrors) { - this.message = message; - this.description = description; - this.fieldErrors = fieldErrors; - } - /** * Add a field error. * @param objectName the object name diff --git a/src/main/java/org/radarcns/management/web/rest/errors/ExceptionTranslator.java b/src/main/java/org/radarcns/management/web/rest/errors/ExceptionTranslator.java index 08ec822c6..d122b5386 100644 --- a/src/main/java/org/radarcns/management/web/rest/errors/ExceptionTranslator.java +++ b/src/main/java/org/radarcns/management/web/rest/errors/ExceptionTranslator.java @@ -1,6 +1,7 @@ package org.radarcns.management.web.rest.errors; import java.util.List; + import org.radarcns.auth.exception.NotAuthorizedException; import org.radarcns.management.web.rest.util.HeaderUtil; import org.slf4j.Logger; @@ -11,6 +12,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity.BodyBuilder; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.oauth2.provider.NoSuchClientException; import org.springframework.transaction.TransactionSystemException; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; @@ -77,47 +79,64 @@ public ErrorVM processValidationError(MethodArgumentNotValidException ex) { } @ExceptionHandler(MethodArgumentTypeMismatchException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseBody public ErrorVM processValidationError(MethodArgumentTypeMismatchException ex) { return new ErrorVM(ErrorConstants.ERR_VALIDATION, ex.getName() + ": " + ex.getMessage()); } - @ExceptionHandler(CustomParameterizedException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ResponseBody - public ParameterizedErrorVM processParameterizedValidationError( - CustomParameterizedException ex) { - return ex.getErrorVM(); + @ExceptionHandler(RadarWebApplicationException.class) + public ResponseEntity processParameterizedValidationError( + RadarWebApplicationException ex) { + return processRadarWebApplicationException(ex); } - @ExceptionHandler(CustomNotFoundException.class) - @ResponseStatus(HttpStatus.NOT_FOUND) - @ResponseBody - public ParameterizedErrorVM processParameterizedNotFound(CustomNotFoundException ex) { - return ex.getErrorVM(); + @ExceptionHandler(NotFoundException.class) + public ResponseEntity processNotFound(NotFoundException ex) { + return processRadarWebApplicationException(ex); + } + + @ExceptionHandler(InvalidStateException.class) + public ResponseEntity processNotFound( + InvalidStateException ex) { + return processRadarWebApplicationException(ex); + } + + @ExceptionHandler(RequestGoneException.class) + public ResponseEntity processNotFound(RequestGoneException ex) { + return processRadarWebApplicationException(ex); } + @ExceptionHandler(BadRequestException.class) + public ResponseEntity processNotFound(BadRequestException ex) { + return processRadarWebApplicationException(ex); + } + + @ExceptionHandler(InvalidRequestException.class) + public ResponseEntity processNotFound( + InvalidRequestException ex) { + return processRadarWebApplicationException(ex); + } + + /** - * Translate a {@link CustomConflictException}. + * Translate a {@link ConflictException}. * @param ex the exception * @return the view-model for the translated exception */ - @ExceptionHandler(CustomConflictException.class) - public ResponseEntity processParameterizedConflict( - CustomConflictException ex) { - return ResponseEntity.status(HttpStatus.CONFLICT) - .headers(HeaderUtil.createAlert(ex.getMessage(), ex.getConflictingResource() - .toString())) - .body(ex.getErrorVM()); + @ExceptionHandler(ConflictException.class) + public ResponseEntity processConflict( + ConflictException ex) { + return processRadarWebApplicationException(ex); } - @ExceptionHandler(CustomServerException.class) - @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - @ResponseBody - public ParameterizedErrorVM processParameterizedServerError(CustomServerException ex) { - return ex.getErrorVM(); + private ResponseEntity processRadarWebApplicationException( + RadarWebApplicationException exception) { + return ResponseEntity + .status(exception.getResponse().getStatus()) + .headers(HeaderUtil.createExceptionAlert(exception.getEntityName(), + exception.getErrorCode(), exception.getMessage())) + .body(exception.getExceptionVM()); } @ExceptionHandler(AccessDeniedException.class) @@ -141,6 +160,21 @@ public ErrorVM processMethodNotSupportedException(HttpRequestMethodNotSupportedE return new ErrorVM(ErrorConstants.ERR_METHOD_NOT_SUPPORTED, ex.getMessage()); } + /** + * If a client tries to initiate an OAuth flow with a non-existing client, this will + * translate the error into a bad request status. Otherwise we return an internal server + * error status, but it is not a server error. + * + * @param ex the exception + * @return the view-model for the translated exception + */ + @ExceptionHandler(NoSuchClientException.class) + @ResponseBody + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorVM processNoSuchClientException(NoSuchClientException ex) { + return new ErrorVM(ErrorConstants.ERR_NO_SUCH_CLIENT, ex.getMessage()); + } + /** * Generic exception translator. * @param ex the exception diff --git a/src/main/java/org/radarcns/management/web/rest/errors/InvalidRequestException.java b/src/main/java/org/radarcns/management/web/rest/errors/InvalidRequestException.java new file mode 100644 index 000000000..43d2dddd4 --- /dev/null +++ b/src/main/java/org/radarcns/management/web/rest/errors/InvalidRequestException.java @@ -0,0 +1,36 @@ +package org.radarcns.management.web.rest.errors; + +import java.util.Map; +import javax.ws.rs.core.Response; + +/** + * The server understood the request, but is refusing to fulfill it. + * Authorization will not help and the request SHOULD NOT be repeated. + */ +public class InvalidRequestException extends RadarWebApplicationException { + + /** + * Create a {@link InvalidRequestException} with the given message, relatedEntityName,errorCode. + * + * @param message the message. + * @param entityName relatedEntityName from {@link EntityName}. + * @param errorCode errorCode from {@link ErrorConstants} + */ + public InvalidRequestException(String message, String entityName, String errorCode) { + super(Response.Status.FORBIDDEN, message, entityName, errorCode); + } + + /** + * Create a {@link InvalidRequestException} with the given message, relatedEntityName,errorCode. + * + * @param message the message. + * @param entityName relatedEntityName from {@link EntityName}. + * @param errorCode errorCode from {@link ErrorConstants} + * @param params map of additional information. + */ + public InvalidRequestException(String message, String entityName, String errorCode, + Map params) { + super(Response.Status.FORBIDDEN, message, entityName, errorCode, params); + } + +} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/InvalidStateException.java b/src/main/java/org/radarcns/management/web/rest/errors/InvalidStateException.java new file mode 100644 index 000000000..e628abb73 --- /dev/null +++ b/src/main/java/org/radarcns/management/web/rest/errors/InvalidStateException.java @@ -0,0 +1,35 @@ +package org.radarcns.management.web.rest.errors; + + +import java.util.Map; +import javax.ws.rs.core.Response; + +/** + * The server encountered an unexpected condition which prevented it from fulfilling the request. + */ +public class InvalidStateException extends RadarWebApplicationException { + + /** + * Create a {@link InvalidStateException} with the given message, relatedEntityName, errorCode. + * + * @param message the message. + * @param entityName relatedEntityName from {@link EntityName}. + * @param errorCode errorCode from {@link ErrorConstants} + */ + public InvalidStateException(String message, String entityName, String errorCode) { + super(Response.Status.INTERNAL_SERVER_ERROR, message, entityName, errorCode); + } + + /** + * Create a {@link InvalidStateException} with the given message, relatedEntityName, errorCode. + * + * @param message the message. + * @param entityName relatedEntityName from {@link EntityName}. + * @param errorCode errorCode from {@link ErrorConstants} + * @param params map of additional information. + */ + public InvalidStateException(String message, String entityName, String errorCode, + Map params) { + super(Response.Status.INTERNAL_SERVER_ERROR, message, entityName, errorCode, params); + } +} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/NotFoundException.java b/src/main/java/org/radarcns/management/web/rest/errors/NotFoundException.java new file mode 100644 index 000000000..6d08891cd --- /dev/null +++ b/src/main/java/org/radarcns/management/web/rest/errors/NotFoundException.java @@ -0,0 +1,38 @@ +package org.radarcns.management.web.rest.errors; + +import java.util.Map; +import javax.ws.rs.core.Response; + +/** + * Created by dverbeec on 7/09/2017. + * Modified by nivethika on 2/08/2018. + *

The server has not found anything matching the Request-URI. + * No indication is given of whether the condition is temporary or permanent. + *

+ */ +public class NotFoundException extends RadarWebApplicationException { + + /** + * Create a {@link NotFoundException} with the given message, relatedEntityName, errorCode. + * + * @param message the message. + * @param entityName relatedEntityName from {@link EntityName}. + * @param errorCode errorCode from {@link ErrorConstants} + */ + public NotFoundException(String message, String entityName, String errorCode) { + super(Response.Status.NOT_FOUND, message, entityName, errorCode); + } + + /** + * Create a {@link NotFoundException} with the given message, relatedEntityName, errorCode. + * + * @param message the message. + * @param entityName relatedEntityName from {@link EntityName}. + * @param errorCode errorCode from {@link ErrorConstants} + * @param paramMap map of additional information. + */ + public NotFoundException(String message, String entityName, String errorCode, + Map paramMap) { + super(Response.Status.NOT_FOUND, message, entityName, errorCode, paramMap); + } +} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/ParameterizedErrorVM.java b/src/main/java/org/radarcns/management/web/rest/errors/ParameterizedErrorVM.java deleted file mode 100644 index 4f4307b6f..000000000 --- a/src/main/java/org/radarcns/management/web/rest/errors/ParameterizedErrorVM.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.radarcns.management.web.rest.errors; - -import java.io.Serializable; -import java.util.Map; - -/** - * View Model for sending a parameterized error message. - */ -public class ParameterizedErrorVM implements Serializable { - - private static final long serialVersionUID = 1L; - - private final String message; - private final Map paramMap; - - public ParameterizedErrorVM(String message, Map paramMap) { - this.message = message; - this.paramMap = paramMap; - } - - public String getMessage() { - return message; - } - - public Map getParams() { - return paramMap; - } - -} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/RadarWebApplicationException.java b/src/main/java/org/radarcns/management/web/rest/errors/RadarWebApplicationException.java new file mode 100644 index 000000000..252ad5317 --- /dev/null +++ b/src/main/java/org/radarcns/management/web/rest/errors/RadarWebApplicationException.java @@ -0,0 +1,84 @@ +package org.radarcns.management.web.rest.errors; + +import static java.util.Collections.emptyMap; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response.Status; + +/** + * A base parameterized exception, which can be translated on the client side. + * + *

For example:

+ * + *

{@code throw new RadarWebApplicationException("Message to client", "entity", + * "error.errorCode")}

+ * + *

can be translated with:

+ * + *

{@code "error.myCustomError" : "The server says {{param0}} to {{param1}}"}

+ */ +public abstract class RadarWebApplicationException extends WebApplicationException { + + private String message; + + private String entityName; + + private String errorCode; + + private final Map paramMap = new HashMap<>(); + + /** + * Create an exception with the given parameters. This will be used to to create response + * body of the request. + * + * @param message Error message to the client + * @param entityName Entity related to the exception + * @param errorCode error code defined in MP if relevant. + */ + public RadarWebApplicationException(Status status, String message, String entityName, + String errorCode) { + this(status, message, entityName, errorCode, emptyMap()); + } + + + /** + * A base parameterized exception, which can be translated on the client side. + * @param status {@link javax.ws.rs.core.Response.Status} code. + * @param message message to client. + * @param entityName entityRelated from {@link EntityName} + * @param errorCode errorCode from {@link ErrorConstants} + * @param params map of optional information. + */ + public RadarWebApplicationException(Status status, String message, String entityName, + String errorCode, Map params) { + super(status); + // add default timestamp first, so a timestamp key in the paramMap will overwrite it + this.paramMap.put("timestamp", + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); + this.paramMap.putAll(params); + this.message = message; + this.entityName = entityName; + this.errorCode = errorCode; + } + + @Override + public String getMessage() { + return message; + } + + public String getEntityName() { + return entityName; + } + + public String getErrorCode() { + return errorCode; + } + + protected RadarWebApplicationExceptionVM getExceptionVM() { + return new RadarWebApplicationExceptionVM(message, entityName, errorCode, paramMap); + } +} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/RadarWebApplicationExceptionVM.java b/src/main/java/org/radarcns/management/web/rest/errors/RadarWebApplicationExceptionVM.java new file mode 100644 index 000000000..9a3b17339 --- /dev/null +++ b/src/main/java/org/radarcns/management/web/rest/errors/RadarWebApplicationExceptionVM.java @@ -0,0 +1,98 @@ +package org.radarcns.management.web.rest.errors; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * View Model for sending a {@link RadarWebApplicationException}. + */ +public class RadarWebApplicationExceptionVM implements Serializable { + + private static final long serialVersionUID = 1L; + + @JsonProperty + private final String entityName; + + @JsonProperty + private final String errorCode; + + @JsonProperty + private final String message; + + @JsonProperty + private Map params; + + /** + * Creates an error view model with message, entityName and errorCode. + * + * @param message message to client. + * @param entityName entityRelated from {@link EntityName} + * @param errorCode errorCode from {@link ErrorConstants} + */ + protected RadarWebApplicationExceptionVM(String message, String entityName, String errorCode) { + this(message, entityName, errorCode, Collections.emptyMap()); + } + + /** + * Creates an error view model with message, entityName and errorCode. + * + * @param message message to client. + * @param entityName entityRelated from {@link EntityName} + * @param errorCode errorCode from {@link ErrorConstants} + * @param params map of optional information. + */ + protected RadarWebApplicationExceptionVM(String message, String entityName, String errorCode, + Map params) { + this.message = message; + this.entityName = entityName; + this.errorCode = errorCode; + this.params = params; + } + + public Map getParams() { + return params; + } + + public String getEntityName() { + return entityName; + } + + public String getErrorCode() { + return errorCode; + } + + public String getMessage() { + return message; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RadarWebApplicationExceptionVM that = (RadarWebApplicationExceptionVM) o; + return Objects.equals(entityName, that.entityName) && Objects + .equals(errorCode, that.errorCode) && Objects.equals(message, that.message) && Objects + .equals(params, that.params); + } + + @Override + public int hashCode() { + + return Objects.hash(entityName, errorCode, message, params); + } + + @Override + public String toString() { + return "RadarWebApplicationExceptionVM{" + "entityName='" + entityName + '\'' + + ", errorCode='" + errorCode + '\'' + ", message='" + message + '\'' + ", params=" + + params + '}'; + } +} diff --git a/src/main/java/org/radarcns/management/web/rest/errors/RequestGoneException.java b/src/main/java/org/radarcns/management/web/rest/errors/RequestGoneException.java new file mode 100644 index 000000000..bf45a0ccd --- /dev/null +++ b/src/main/java/org/radarcns/management/web/rest/errors/RequestGoneException.java @@ -0,0 +1,37 @@ +package org.radarcns.management.web.rest.errors; + +import java.util.Map; +import javax.ws.rs.core.Response; + +/** + * Throw when the requested resource is no longer available at the server and no forwarding + * address is known. This condition is expected to be considered permanent. Clients with + * link editing capabilities SHOULD delete references to the Request-URI after user approval. + */ +public class RequestGoneException extends RadarWebApplicationException { + + /** + * Create a {@link RequestGoneException} with the given message, relatedEntityName, errorCode. + * + * @param message the message. + * @param entityName relatedEntityName from {@link EntityName}. + * @param errorCode errorCode from {@link ErrorConstants} + */ + public RequestGoneException(String message, String entityName, String errorCode) { + super(Response.Status.GONE, message, entityName, errorCode); + } + + + /** + * Create a {@link RequestGoneException} with the given message, relatedEntityName, errorCode. + * + * @param message the message. + * @param entityName relatedEntityName from {@link EntityName}. + * @param errorCode errorCode from {@link ErrorConstants} + * @param paramMap map of additional information. + */ + public RequestGoneException(String message, String entityName, String errorCode, + Map paramMap) { + super(Response.Status.GONE, message, entityName, errorCode, paramMap); + } +} diff --git a/src/main/java/org/radarcns/management/web/rest/util/HeaderUtil.java b/src/main/java/org/radarcns/management/web/rest/util/HeaderUtil.java index 0b1953745..1bd7bdf63 100644 --- a/src/main/java/org/radarcns/management/web/rest/util/HeaderUtil.java +++ b/src/main/java/org/radarcns/management/web/rest/util/HeaderUtil.java @@ -47,7 +47,7 @@ public static HttpHeaders createEntityDeletionAlert(String entityName, String pa /** * Create headers to display a failure alert in the frontend. - * @param entityName the entity on which the failure occured + * @param entityName the entity on which the failure occurred * @param errorKey the error key in the translation dictionary * @param defaultMessage the default message * @return the {@link HttpHeaders} @@ -61,6 +61,23 @@ public static HttpHeaders createFailureAlert(String entityName, String errorKey, return headers; } + /** + * Create headers to display a failure alert in the frontend. + * @param entityName the entity on which the failure occurred + * @param errorKey the error key in the translation dictionary + * @param defaultMessage the default message + * @return the {@link HttpHeaders} + */ + public static HttpHeaders createExceptionAlert(String entityName, String errorKey, + String defaultMessage) { + //TODO: Replace createFailureAlert with error. addition + log.error("Entity creation failed, {}", defaultMessage); + HttpHeaders headers = new HttpHeaders(); + headers.add("X-managementPortalApp-error", errorKey); + headers.add("X-managementPortalApp-params", entityName); + return headers; + } + /** * URLEncode each component, prefix and join them by forward slashes. * diff --git a/src/main/resources/.h2.server.properties b/src/main/resources/.h2.server.properties index 9e987be7d..af88acc06 100644 --- a/src/main/resources/.h2.server.properties +++ b/src/main/resources/.h2.server.properties @@ -1,4 +1,5 @@ #H2 Server Properties +#Fri Jul 27 17:28:49 CEST 2018 0=JHipster H2 (Memory)|org.h2.Driver|jdbc\:h2\:mem\:managementportal|ManagementPortal webAllowOthers=true webPort=8082 diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index 6ea190134..55818d7cb 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -95,9 +95,13 @@ server: # # =================================================================== managementportal: + common: + baseUrl: http://localhost:9000 # Modify according to your server's URL + managementPortalBaseUrl: http://localhost:9000 + privacyPolicyUrl: http://info.thehyve.nl/radar-cns-privacy-policy + adminPassword: admin mail: # specific ManagementPortal mail property, for standard properties see MailProperties from: ManagementPortal@localhost - baseUrl: http://my-server-url-to-change # Modify according to your server's URL frontend: clientId: ManagementPortalapp clientSecret: my-secret-token-to-change-in-production @@ -106,6 +110,7 @@ managementportal: sessionTimeout : 86400 # session for rft cookie oauth: clientsFile: src/main/docker/etc/config/oauth_client_details.csv + metaTokenTimeout: PT1H #timeout should be specified as the ISO-8601 duration format {@code PnDTnHnMn.nS}. catalogueServer: enableAutoImport: false serverUrl: diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index 344d0287e..f37b939d4 100644 --- a/src/main/resources/config/application-prod.yml +++ b/src/main/resources/config/application-prod.yml @@ -91,9 +91,13 @@ server: # # =================================================================== managementportal: + common: + baseUrl: http://my-server-url-to-change-here # Modify according to your server's URL + managementPortalBaseUrl: http://my-server-url-to-change-here/ + privacyPolicyUrl: http://info.thehyve.nl/radar-cns-privacy-policy + adminPassword: mail: # specific JHipster mail property, for standard properties see MailProperties from: ManagementPortal@localhost - baseUrl: http://my-server-url-to-change-here # Modify according to your server's URL frontend: clientId: ManagementPortalapp clientSecret: @@ -102,6 +106,7 @@ managementportal: sessionTimeout : 86400 oauth: clientsFile: /mp-includes/config/oauth_client_details.csv + metaTokenTimeout: PT1H #timeout should be specified as the ISO-8601 duration format {@code PnDTnHnMn.nS}. catalogueServer: enableAutoImport: false serverUrl: diff --git a/src/main/resources/config/liquibase/changelog/20180724105800_add_radar-meta_token.xml b/src/main/resources/config/liquibase/changelog/20180724105800_add_radar-meta_token.xml new file mode 100644 index 000000000..7d59ebe0a --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20180724105800_add_radar-meta_token.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20180813173100_add_subject_to_meta_token.xml b/src/main/resources/config/liquibase/changelog/20180813173100_add_subject_to_meta_token.xml new file mode 100644 index 000000000..fae0742cc --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20180813173100_add_subject_to_meta_token.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20180824131400_add_client_id_to_meta_token.xml b/src/main/resources/config/liquibase/changelog/20180824131400_add_client_id_to_meta_token.xml new file mode 100644 index 000000000..198e69b3f --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20180824131400_add_client_id_to_meta_token.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 3ea25b559..93d670264 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -28,5 +28,8 @@ + + + diff --git a/src/main/webapp/app/entities/project/project-dialog.component.ts b/src/main/webapp/app/entities/project/project-dialog.component.ts index b18450931..d5f402c76 100644 --- a/src/main/webapp/app/entities/project/project-dialog.component.ts +++ b/src/main/webapp/app/entities/project/project-dialog.component.ts @@ -40,7 +40,7 @@ export class ProjectDialogComponent implements OnInit { this.authorities = ['ROLE_USER', 'ROLE_SYS_ADMIN' , 'ROLE_PROJECT_ADMIN']; this.sourceTypeService.query().subscribe( (res: Response) => { this.sourceTypes = res.json(); }, (res: Response) => this.onError(res.json())); - this.options = ['Work-package', 'Phase' , 'External-project-url' , 'External-project-id']; + this.options = ['Work-package', 'Phase' , 'External-project-url' , 'External-project-id' , 'Privacy-policy-url']; this.registerChangesInProject(); this.projectIdAsPrettyValue = true; } diff --git a/src/main/webapp/app/home/home.scss b/src/main/webapp/app/home/home.scss index 50cabcb18..baccc4d57 100644 --- a/src/main/webapp/app/home/home.scss +++ b/src/main/webapp/app/home/home.scss @@ -7,7 +7,7 @@ Main page styles display: inline-block; width: 347px; height: 497px; - background: url("../../content/images/hipsterRR.png") no-repeat center top; + background: url("../../content/images/radar-baseRR.png") no-repeat center top; background-size: contain; } @@ -20,7 +20,7 @@ only screen and ( min-device-pixel-ratio: 2), only screen and ( min-resolution: 192dpi), only screen and ( min-resolution: 2dppx) { .hipster { - background: url("../../content/images/hipsterRR.png") no-repeat center top; + background: url("../../content/images/radar-baseRR.png") no-repeat center top; background-size: contain; } } diff --git a/src/main/webapp/app/layouts/navbar/navbar.scss b/src/main/webapp/app/layouts/navbar/navbar.scss index 6fccda97f..22cb7e0b8 100644 --- a/src/main/webapp/app/layouts/navbar/navbar.scss +++ b/src/main/webapp/app/layouts/navbar/navbar.scss @@ -63,7 +63,7 @@ Logo styles width: 70px; display: inline-block; vertical-align: middle; - background: url("../../../content/images/icon.png") no-repeat center center; + background: url("../../../content/images/radar-base-icon-1.png") no-repeat center center; background-size: contain; } } diff --git a/src/main/webapp/app/shared/subject/subject-pair-dialog.component.ts b/src/main/webapp/app/shared/subject/subject-pair-dialog.component.ts index 17be002bd..61e73552a 100644 --- a/src/main/webapp/app/shared/subject/subject-pair-dialog.component.ts +++ b/src/main/webapp/app/shared/subject/subject-pair-dialog.component.ts @@ -57,7 +57,7 @@ export class SubjectPairDialogComponent implements OnInit { if (this.selectedClient != null) { this.oauthClientPairInfoService.get(this.selectedClient, this.subject).subscribe( (res) => { - this.oauthClientPairInfo = res.text(); + this.oauthClientPairInfo = res.json().tokenUrl; this.showQRCode = true; }); } @@ -65,6 +65,7 @@ export class SubjectPairDialogComponent implements OnInit { this.showQRCode = false; this.oauthClientPairInfo = ""; } + } diff --git a/src/main/webapp/content/images/hipster2x.png b/src/main/webapp/content/images/hipster2x.png deleted file mode 100644 index 4751c9168..000000000 Binary files a/src/main/webapp/content/images/hipster2x.png and /dev/null differ diff --git a/src/main/webapp/content/images/hipsterRR.png b/src/main/webapp/content/images/hipsterRR.png deleted file mode 100644 index ab55c81cf..000000000 Binary files a/src/main/webapp/content/images/hipsterRR.png and /dev/null differ diff --git a/src/main/webapp/content/images/radar-base-icon-1.png b/src/main/webapp/content/images/radar-base-icon-1.png new file mode 100644 index 000000000..9cf911dee Binary files /dev/null and b/src/main/webapp/content/images/radar-base-icon-1.png differ diff --git a/src/main/webapp/content/images/radar-baseRR.png b/src/main/webapp/content/images/radar-baseRR.png new file mode 100644 index 000000000..490d2734c Binary files /dev/null and b/src/main/webapp/content/images/radar-baseRR.png differ diff --git a/src/main/webapp/favicon.ico b/src/main/webapp/favicon.ico index 60f53f64a..25dcb2692 100644 Binary files a/src/main/webapp/favicon.ico and b/src/main/webapp/favicon.ico differ diff --git a/src/main/webapp/i18n/en/subject.json b/src/main/webapp/i18n/en/subject.json index a27eb0e61..4f2ac16bb 100644 --- a/src/main/webapp/i18n/en/subject.json +++ b/src/main/webapp/i18n/en/subject.json @@ -72,6 +72,8 @@ "loginRequired": "A subject login is required", "sourceNotFound": "A source with that sourceName is not assigned to the subject.", "sourceNameExists": "A source with that name already exists.", - "sourceTypeExists": "The subject has already been assigned a source of this type." + "sourceTypeExists": "The subject has already been assigned a source of this type.", + "activeParticipantProjectNotFound": "The subject does not have an active participant project", + "sourceTypeNotProvided": "The request does not include valid source-type properties" } } diff --git a/src/main/webapp/i18n/nl/subject.json b/src/main/webapp/i18n/nl/subject.json index 3d5dcf7ab..3f3190f66 100644 --- a/src/main/webapp/i18n/nl/subject.json +++ b/src/main/webapp/i18n/nl/subject.json @@ -72,7 +72,9 @@ "sourceNotFound": "Er is geen source met deze naam toegewezen aan de deelnemer.", "sourceNameExists": "Er bestaat reeds een source met deze naam.", "validation": "Validatie fout, controleer de ingevoerde waarden.", - "sourceTypeExists": "De deelnemer werd reeds een source van dit type toegewezen." + "sourceTypeExists": "De deelnemer werd reeds een source van dit type toegewezen.", + "activeParticipantProjectNotFound": "Het Subject heeft geen actief deelnemersproject", + "sourceTypeNotProvided" : "Het verzoek bevat geen geldige eigenschappen van het source-type" } } diff --git a/src/test/bash/run-e2e.sh b/src/test/bash/run-e2e.sh index 4fe886072..11c771fdf 100755 --- a/src/test/bash/run-e2e.sh +++ b/src/test/bash/run-e2e.sh @@ -4,6 +4,7 @@ if [ -z $TRAVIS_TAG ] then echo "Running e2e tests" + ls -al node_modules/webdriver-manager/selenium/ yarn e2e else echo "Skipping e2e tests on tag builds" diff --git a/src/test/java/org/radarcns/management/service/MetaTokenServiceTest.java b/src/test/java/org/radarcns/management/service/MetaTokenServiceTest.java new file mode 100644 index 000000000..57e6c424d --- /dev/null +++ b/src/test/java/org/radarcns/management/service/MetaTokenServiceTest.java @@ -0,0 +1,185 @@ +package org.radarcns.management.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.net.MalformedURLException; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.radarcns.management.ManagementPortalTestApp; +import org.radarcns.management.domain.MetaToken; +import org.radarcns.management.repository.MetaTokenRepository; +import org.radarcns.management.service.dto.SubjectDTO; +import org.radarcns.management.service.dto.TokenDTO; +import org.radarcns.management.service.mapper.SubjectMapper; +import org.radarcns.management.web.rest.errors.RadarWebApplicationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = ManagementPortalTestApp.class) +@Transactional +public class MetaTokenServiceTest { + + + @Autowired + private MetaTokenService metaTokenService; + + @Autowired + private MetaTokenRepository metaTokenRepository; + + @Autowired + private SubjectService subjectService; + + @Autowired + private SubjectMapper subjectMapper; + + @Autowired + private OAuthClientService oAuthClientService; + + private ClientDetails clientDetails; + + private SubjectDTO subjectDto; + + + @Before + public void setUp() { + subjectDto = SubjectServiceTest.createEntityDTO(); + subjectDto = subjectService.createSubject(subjectDto); + + clientDetails = oAuthClientService + .createClientDetail(OauthClientServiceTest.createClient()); + } + + @Test + public void testSaveThenFetchMetaToken() throws MalformedURLException { + + MetaToken metaToken = new MetaToken() + .fetched(false) + .token("{\"access_token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJhZG1pbi" + + "IsInNvdX" + + "JjZXMiOltdLCJ1c2VyX25hbWUiOiJhZG1pbiIsInJvbGVzIjpbXSwiaXNzIjoiTWFuYWdlbWVudFB" + + "vcnRhbCIsImF1dGhvcml0aWVzIjpbIlJPTEVfU1lTX0FETUlOIl0sImNsaWVudF9pZCI6Ik1hbmFn" + + "ZW1lbnRQb3J0YWxhcHAiLCJhdWQiOiJyZXNfTWFuYWdlbWVudFBvcnRhbCIsImdyYW50X3R5cGUiO" + + "iJwYXNzd29yZCIsInNjb3BlIjpbIlNPVVJDRVRZUEUuQ1JFQVRFIiwiU09VUkNFVFlQRS5SRUFEIi" + + "wiU09VUkNFVFlQRS5VUERBVEUiLCJTT1VSQ0VUWVBFLkRFTEVURSIsIlNPVVJDRURBVEEuQ1JFQVR" + + "FIiwiU09VUkNFREFUQS5SRUFEIiwiU09VUkNFREFUQS5VUERBVEUiLCJTT1VSQ0VEQVRBLkRFTEVU" + + "RSIsIlNPVVJDRS5DUkVBVEUiLCJTT1VSQ0UuUkVBRCIsIlNPVVJDRS5VUERBVEUiLCJTT1VSQ0UuR" + + "EVMRVRFIiwiU1VCSkVDVC5DUkVBVEUiLCJTVUJKRUNULlJFQUQiLCJTVUJKRUNULlVQREFURSIsIl" + + "NVQkpFQ1QuREVMRVRFIiwiVVNFUi5DUkVBVEUiLCJVU0VSLlJFQUQiLCJVU0VSLlVQREFURSIsIlV" + + "TRVIuREVMRVRFIiwiUk9MRS5DUkVBVEUiLCJST0xFLlJFQUQiLCJST0xFLlVQREFURSIsIlJPTEUu" + + "REVMRVRFIiwiUFJPSkVDVC5DUkVBVEUiLCJQUk9KRUNULlJFQUQiLCJQUk9KRUNULlVQREFURSIsI" + + "lBST0pFQ1QuREVMRVRFIiwiT0FVVEhDTElFTlRTLkNSRUFURSIsIk9BVVRIQ0xJRU5UUy5SRUFEIi" + + "wiT0FVVEhDTElFTlRTLlVQREFURSIsIk9BVVRIQ0xJRU5UUy5ERUxFVEUiLCJBVURJVC5DUkVBVEU" + + "iLCJBVURJVC5SRUFEIiwiQVVESVQuVVBEQVRFIiwiQVVESVQuREVMRVRFIiwiQVVUSE9SSVRZLkNS" + + "RUFURSIsIkFVVEhPUklUWS5SRUFEIiwiQVVUSE9SSVRZLlVQREFURSIsIkFVVEhPUklUWS5ERUxFV" + + "EUiLCJNRUFTVVJFTUVOVC5DUkVBVEUiLCJNRUFTVVJFTUVOVC5SRUFEIiwiTUVBU1VSRU1FTlQuVV" + + "BEQVRFIiwiTUVBU1VSRU1FTlQuREVMRVRFIl0sImV4cCI6MTUzMjQ1MjMyOCwiaWF0IjoxNTMyNDM" + + "3OTI4LCJqdGkiOiJkY2E3MDQ3Yi00NjdlLTQ5OTEtOWY1Zi03N2NiMTA4MTA0YzQifQ.MEUCIGHhv" + + "q-C9WRHAYecpgd3SM6ih2ejqwJ3Lp_qwsVd8o-mAiEAswpsnOoTi3qQC49y5hCFK3QODKt_9pJglF" + + "DxwPqnG0A\",\"token_type\":\"bearer\",\"expires_in\":14399,\"scope\":\"SOURCE" + + "TYPE.CREATE SOURCETYPE.READ SOURCETYPE.UPDATE SOURCETYPE.DELETE SOURCEDATA.CR" + + "EATE SOURCEDATA.READ SOURCEDATA.UPDATE SOURCEDATA.DELETE SOURCE.CREATE SOURCE" + + ".READ SOURCE.UPDATE SOURCE.DELETE SUBJECT.CREATE SUBJECT.READ SUBJECT.UPDATE " + + "SUBJECT.DELETE USER.CREATE USER.READ USER.UPDATE USER.DELETE ROLE.CREATE ROLE" + + ".READ ROLE.UPDATE ROLE.DELETE PROJECT.CREATE PROJECT.READ PROJECT.UPDATE PROJ" + + "ECT.DELETE OAUTHCLIENTS.CREATE OAUTHCLIENTS.READ OAUTHCLIENTS.UPDATE OAUTHCLI" + + "ENTS.DELETE AUDIT.CREATE AUDIT.READ AUDIT.UPDATE AUDIT.DELETE AUTHORITY.CREAT" + + "E AUTHORITY.READ AUTHORITY.UPDATE AUTHORITY.DELETE MEASUREMENT.CREATE MEASURE" + + "MENT.READ MEASUREMENT.UPDATE MEASUREMENT.DELETE\",\"sub\":\"admin\",\"sources" + + "\":[],\"grant_type\":\"password\",\"roles\":[],\"iss\":\"ManagementPortal\"," + + "\"iat\":1532437928,\"jti\":\"dca7047b-467e-4991-9f5f-77cb108104c4\"}") + .expiryDate(Instant.now().plus(Duration.ofHours(1))) + .subject(subjectMapper.subjectDTOToSubject(subjectDto)) + .clientId(clientDetails.getClientId()); + + MetaToken saved = metaTokenService.save(metaToken); + assertNotNull(saved.getId()); + assertNotNull(saved.getTokenName()); + assertFalse(saved.isFetched()); + assertTrue(saved.getExpiryDate().isAfter(Instant.now())); + + String tokenName = saved.getTokenName(); + TokenDTO fetchedToken = metaTokenService.fetchToken(tokenName); + + assertNotNull(fetchedToken); + assertNotNull(fetchedToken.getRefreshToken()); + + } + + @Test(expected = RadarWebApplicationException.class) + public void testGetAFetchedMetaToken() throws MalformedURLException { + MetaToken token = new MetaToken() + .fetched(true) + .tokenName("something") + .expiryDate(Instant.now().plus(Duration.ofHours(1))) + .subject(subjectMapper.subjectDTOToSubject(subjectDto)); + + MetaToken saved = metaTokenService.save(token); + assertNotNull(saved.getId()); + assertNotNull(saved.getTokenName()); + assertTrue(saved.isFetched()); + assertTrue(saved.getExpiryDate().isAfter(Instant.now())); + + String tokenName = saved.getTokenName(); + metaTokenService.fetchToken(tokenName); + } + + @Test(expected = RadarWebApplicationException.class) + public void testGetAnExpiredMetaToken() throws MalformedURLException { + MetaToken token = new MetaToken() + .fetched(false) + .tokenName("somethingelse") + .expiryDate(Instant.now().minus(Duration.ofHours(1))) + .subject(subjectMapper.subjectDTOToSubject(subjectDto)); + + MetaToken saved = metaTokenService.save(token); + + assertNotNull(saved.getId()); + assertNotNull(saved.getTokenName()); + assertFalse(saved.isFetched()); + assertTrue(saved.getExpiryDate().isBefore(Instant.now())); + + String tokenName = saved.getTokenName(); + + metaTokenService.fetchToken(tokenName); + } + + @Test + public void testRemovingExpiredMetaToken() { + + MetaToken tokenFetched = new MetaToken() + .fetched(true) + .tokenName("something") + .expiryDate(Instant.now().plus(Duration.ofHours(1))); + + MetaToken tokenExpired = new MetaToken() + .fetched(false) + .tokenName("somethingelse") + .expiryDate(Instant.now().minus(Duration.ofHours(1))); + + MetaToken tokenNew = new MetaToken() + .fetched(false) + .tokenName("somethingelseandelse") + .expiryDate(Instant.now().plus(Duration.ofHours(1))); + + metaTokenRepository.save(Arrays.asList(tokenFetched, tokenExpired, tokenNew)); + + metaTokenService.removeStaleTokens(); + + List availableTokens = metaTokenRepository.findAll(); + + assertEquals(availableTokens.size(), 1); + } +} diff --git a/src/test/java/org/radarcns/management/service/OauthClientServiceTest.java b/src/test/java/org/radarcns/management/service/OauthClientServiceTest.java new file mode 100644 index 000000000..1b5df079f --- /dev/null +++ b/src/test/java/org/radarcns/management/service/OauthClientServiceTest.java @@ -0,0 +1,33 @@ +package org.radarcns.management.service; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.stream.Collectors; + +import org.radarcns.management.service.dto.ClientDetailsDTO; + +public class OauthClientServiceTest { + + /** + * Create an entity for this test. + * + *

This is a static method, as tests for other entities might also need it, if they test an + * entity which requires the current entity.

+ */ + public static ClientDetailsDTO createClient() { + ClientDetailsDTO result = new ClientDetailsDTO(); + result.setClientId("TEST_CLIENT"); + result.setClientSecret("TEST_SECRET"); + result.setScope(Arrays.asList("scope-1", "scope-2").stream().collect(Collectors.toSet())); + result.setResourceIds(Arrays.asList("res-1", "res-2").stream().collect(Collectors.toSet())); + result.setAutoApproveScopes(Arrays.asList("scope-1").stream().collect(Collectors.toSet())); + result.setAuthorizedGrantTypes(Arrays.asList("password", "refresh_token", + "authorization_code").stream().collect(Collectors.toSet())); + result.setAccessTokenValiditySeconds(3600L); + result.setRefreshTokenValiditySeconds(7200L); + result.setAuthorities(Arrays.asList("AUTHORITY-1").stream().collect(Collectors.toSet())); + result.setAdditionalInformation(new HashMap<>()); + result.getAdditionalInformation().put("dynamic_registration", "true"); + return result; + } +} diff --git a/src/test/java/org/radarcns/management/service/SubjectServiceTest.java b/src/test/java/org/radarcns/management/service/SubjectServiceTest.java new file mode 100644 index 000000000..317be84e3 --- /dev/null +++ b/src/test/java/org/radarcns/management/service/SubjectServiceTest.java @@ -0,0 +1,91 @@ +package org.radarcns.management.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.radarcns.management.service.dto.ProjectDTO.PRIVACY_POLICY_URL; +import static org.radarcns.management.service.dto.SubjectDTO.SubjectStatus.ACTIVATED; + +import java.net.URL; +import java.util.Collections; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.radarcns.management.ManagementPortalTestApp; +import org.radarcns.management.domain.Subject; +import org.radarcns.management.service.dto.ProjectDTO; +import org.radarcns.management.service.dto.SubjectDTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = ManagementPortalTestApp.class) +@Transactional +public class SubjectServiceTest { + + public static final String DEFAULT_EXTERNAL_LINK = "AAAAAAAAAA"; + public static final String UPDATED_EXTERNAL_LINK = "BBBBBBBBBB"; + + public static final String DEFAULT_ENTERNAL_ID = "AAAAAAAAAA"; + public static final String UPDATED_ENTERNAL_ID = "BBBBBBBBBB"; + + public static final Boolean DEFAULT_REMOVED = false; + public static final Boolean UPDATED_REMOVED = true; + + public static final SubjectDTO.SubjectStatus DEFAULT_STATUS = ACTIVATED; + + public static final String MODEL = "App"; + public static final String PRODUCER = "THINC-IT"; + + public static final String DEFAULT_PROJECT_PRIVACY_POLICY_URL = + "http://info.thehyve.nl/radar-cns-privacy-policy"; + + + @Autowired + private SubjectService subjectService; + + @Autowired + private ProjectService projectService; + + + /** + * Create an entity for this test. + * + *

This is a static method, as tests for other entities might also need it, if they test an + * entity which requires the current entity.

+ */ + public static SubjectDTO createEntityDTO() { + SubjectDTO subject = new SubjectDTO(); + subject.setExternalLink(DEFAULT_EXTERNAL_LINK); + subject.setExternalId(DEFAULT_ENTERNAL_ID); + subject.setStatus(ACTIVATED); + ProjectDTO projectDto = new ProjectDTO(); + projectDto.setId(1L); + projectDto.setProjectName("Radar"); + projectDto.setLocation("SOMEWHERE"); + projectDto.setDescription("test"); + projectDto.setAttributes(Collections.singletonMap(PRIVACY_POLICY_URL, + DEFAULT_PROJECT_PRIVACY_POLICY_URL)); + subject.setProject(projectDto); + + return subject; + } + + @Test + @Transactional + public void testGetPrivacyPolicyUrl() { + + projectService.save(createEntityDTO().getProject()); + SubjectDTO created = subjectService.createSubject(createEntityDTO()); + assertNotNull(created.getId()); + + Subject subject = subjectService.findOneByLogin(created.getLogin()); + assertNotNull(subject); + + URL privacyPolicyUrl = subjectService.getPrivacyPolicyUrl(subject); + assertNotNull(privacyPolicyUrl); + assertEquals(privacyPolicyUrl.toExternalForm(), DEFAULT_PROJECT_PRIVACY_POLICY_URL); + + } +} diff --git a/src/test/java/org/radarcns/management/service/UserServiceIntTest.java b/src/test/java/org/radarcns/management/service/UserServiceIntTest.java index a4d708de2..b9e3d4670 100644 --- a/src/test/java/org/radarcns/management/service/UserServiceIntTest.java +++ b/src/test/java/org/radarcns/management/service/UserServiceIntTest.java @@ -14,7 +14,6 @@ import org.radarcns.management.service.dto.UserDTO; import org.radarcns.management.service.mapper.UserMapper; import org.radarcns.management.service.util.RandomUtil; -import org.radarcns.management.web.rest.UserResourceIntTest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; @@ -40,6 +39,24 @@ @Transactional public class UserServiceIntTest { + public static final String DEFAULT_LOGIN = "johndoe"; + public static final String UPDATED_LOGIN = "jhipster"; + + public static final String DEFAULT_PASSWORD = "passjohndoe"; + public static final String UPDATED_PASSWORD = "passjhipster"; + + public static final String DEFAULT_EMAIL = "johndoe@localhost"; + public static final String UPDATED_EMAIL = "jhipster@localhost"; + + public static final String DEFAULT_FIRSTNAME = "john"; + public static final String UPDATED_FIRSTNAME = "jhipsterFirstName"; + + public static final String DEFAULT_LASTNAME = "doe"; + public static final String UPDATED_LASTNAME = "jhipsterLastName"; + + public static final String DEFAULT_LANGKEY = "en"; + public static final String UPDATED_LANGKEY = "fr"; + @Autowired private UserRepository userRepository; @@ -52,14 +69,29 @@ public class UserServiceIntTest { @Autowired private CustomRevisionEntityRepository revisionEntityRepository; - //@Autowired - //private RevisionService revisionService; - private UserDTO userDto; @Before public void setUp() { - userDto = userMapper.userToUserDTO(UserResourceIntTest.createEntity()); + userDto = userMapper.userToUserDTO(createEntity()); + } + + /** + * Create a User. + * + *

This is a static method, as tests for other entities might also need it, + * if they test an entity which has a required relationship to the User entity.

+ */ + public static User createEntity() { + User user = new User(); + user.setLogin(DEFAULT_LOGIN); + user.setPassword(RandomStringUtils.random(60)); + user.setActivated(true); + user.setEmail(DEFAULT_EMAIL); + user.setFirstName(DEFAULT_FIRSTNAME); + user.setLastName(DEFAULT_LASTNAME); + user.setLangKey(DEFAULT_LANGKEY); + return user; } @Test diff --git a/src/test/java/org/radarcns/management/web/rest/OAuthClientsResourceIntTest.java b/src/test/java/org/radarcns/management/web/rest/OAuthClientsResourceIntTest.java index 210f9fb0e..b92d717f6 100644 --- a/src/test/java/org/radarcns/management/web/rest/OAuthClientsResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/OAuthClientsResourceIntTest.java @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.nullValue; +import static org.radarcns.management.service.OauthClientServiceTest.createClient; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -11,18 +12,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; import org.radarcns.management.ManagementPortalApp; -import org.radarcns.management.repository.SubjectRepository; import org.radarcns.management.security.JwtAuthenticationFilter; +import org.radarcns.management.service.OAuthClientService; +import org.radarcns.management.service.SubjectService; import org.radarcns.management.service.UserService; import org.radarcns.management.service.dto.ClientDetailsDTO; import org.radarcns.management.service.mapper.ClientDetailsMapper; @@ -35,7 +34,6 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.mock.web.MockFilterConfig; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration; import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; import org.springframework.test.context.junit4.SpringRunner; @@ -53,9 +51,6 @@ @SpringBootTest(classes = ManagementPortalApp.class) public class OAuthClientsResourceIntTest { - @Autowired - private AuthorizationServerEndpointsConfiguration authorizationServerEndpointsConfiguration; - @Autowired private JdbcClientDetailsService clientDetailsService; @@ -63,7 +58,7 @@ public class OAuthClientsResourceIntTest { private ClientDetailsMapper clientDetailsMapper; @Autowired - private SubjectRepository subjectRepository; + private SubjectService subjectService; @Autowired private SubjectMapper subjectMapper; @@ -71,6 +66,9 @@ public class OAuthClientsResourceIntTest { @Autowired private UserService userService; + @Autowired + private OAuthClientService oAuthClientService; + @Autowired private MappingJackson2HttpMessageConverter jacksonMessageConverter; @@ -95,21 +93,18 @@ public class OAuthClientsResourceIntTest { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); OAuthClientsResource oauthClientsResource = new OAuthClientsResource(); - ReflectionTestUtils.setField(oauthClientsResource, - "authorizationServerEndpointsConfiguration", - authorizationServerEndpointsConfiguration); ReflectionTestUtils.setField(oauthClientsResource, "clientDetailsMapper", clientDetailsMapper); - ReflectionTestUtils.setField(oauthClientsResource, "subjectRepository", - subjectRepository); + ReflectionTestUtils.setField(oauthClientsResource, "subjectService", + subjectService); ReflectionTestUtils.setField(oauthClientsResource, "subjectMapper", subjectMapper); ReflectionTestUtils.setField(oauthClientsResource, "userService", userService); ReflectionTestUtils.setField(oauthClientsResource, "servletRequest", servletRequest); - ReflectionTestUtils.setField(oauthClientsResource, "clientDetailsService", - clientDetailsService); + ReflectionTestUtils.setField(oauthClientsResource, "oAuthClientService", + oAuthClientService); JwtAuthenticationFilter filter = OAuthHelper.createAuthenticationFilter(); filter.init(new MockFilterConfig()); @@ -229,30 +224,15 @@ public void cannotModifyProtected() throws Exception { restProjectMockMvc.perform(delete("/api/oauth-clients/" + details.getClientId()) .contentType(TestUtil.APPLICATION_JSON_UTF8) .content(TestUtil.convertObjectToJsonBytes(details))) - .andExpect(status().isBadRequest()); + .andExpect(status().isForbidden()); // expect we can not update it now details.setRefreshTokenValiditySeconds(20L); restProjectMockMvc.perform(put("/api/oauth-clients") .contentType(TestUtil.APPLICATION_JSON_UTF8) .content(TestUtil.convertObjectToJsonBytes(details))) - .andExpect(status().isBadRequest()); + .andExpect(status().isForbidden()); } - private static ClientDetailsDTO createClient() { - ClientDetailsDTO result = new ClientDetailsDTO(); - result.setClientId("TEST_CLIENT"); - result.setClientSecret("TEST_SECRET"); - result.setScope(Arrays.asList("scope-1", "scope-2").stream().collect(Collectors.toSet())); - result.setResourceIds(Arrays.asList("res-1", "res-2").stream().collect(Collectors.toSet())); - result.setAutoApproveScopes(Arrays.asList("scope-1").stream().collect(Collectors.toSet())); - result.setAuthorizedGrantTypes(Arrays.asList("password", "refresh_token", - "authorization_code").stream().collect(Collectors.toSet())); - result.setAccessTokenValiditySeconds(3600L); - result.setRefreshTokenValiditySeconds(7200L); - result.setAuthorities(Arrays.asList("AUTHORITY-1").stream().collect(Collectors.toSet())); - result.setAdditionalInformation(new HashMap<>()); - result.getAdditionalInformation().put("dynamic_registration", "true"); - return result; - } + } diff --git a/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java b/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java index 5007d69aa..a506706a3 100644 --- a/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/SubjectResourceIntTest.java @@ -5,6 +5,16 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.radarcns.management.service.SubjectServiceTest.DEFAULT_ENTERNAL_ID; +import static org.radarcns.management.service.SubjectServiceTest.DEFAULT_EXTERNAL_LINK; +import static org.radarcns.management.service.SubjectServiceTest.DEFAULT_REMOVED; +import static org.radarcns.management.service.SubjectServiceTest.DEFAULT_STATUS; +import static org.radarcns.management.service.SubjectServiceTest.MODEL; +import static org.radarcns.management.service.SubjectServiceTest.PRODUCER; +import static org.radarcns.management.service.SubjectServiceTest.UPDATED_ENTERNAL_ID; +import static org.radarcns.management.service.SubjectServiceTest.UPDATED_EXTERNAL_LINK; +import static org.radarcns.management.service.SubjectServiceTest.UPDATED_REMOVED; +import static org.radarcns.management.service.SubjectServiceTest.createEntityDTO; import static org.radarcns.management.web.rest.TestUtil.commitTransactionAndStartNew; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -20,7 +30,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import javax.persistence.EntityManager; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import org.junit.Before; @@ -66,21 +75,6 @@ @WithMockUser public class SubjectResourceIntTest { - private static final String DEFAULT_EXTERNAL_LINK = "AAAAAAAAAA"; - private static final String UPDATED_EXTERNAL_LINK = "BBBBBBBBBB"; - - private static final String DEFAULT_ENTERNAL_ID = "AAAAAAAAAA"; - private static final String UPDATED_ENTERNAL_ID = "BBBBBBBBBB"; - - private static final Boolean DEFAULT_REMOVED = false; - private static final Boolean UPDATED_REMOVED = true; - - private static final SubjectDTO.SubjectStatus DEFAULT_STATUS = - SubjectDTO.SubjectStatus.ACTIVATED; - - private static final String MODEL = "App"; - private static final String PRODUCER = "THINC-IT"; - @Autowired private SubjectRepository subjectRepository; @@ -105,9 +99,6 @@ public class SubjectResourceIntTest { @Autowired private ExceptionTranslator exceptionTranslator; - @Autowired - private EntityManager em; - @Autowired private ProjectRepository projectRepository; @@ -140,25 +131,6 @@ public void setUp() throws ServletException { .defaultRequest(get("/").with(OAuthHelper.bearerToken())).build(); } - /** - * Create an entity for this test. - * - *

This is a static method, as tests for other entities might also need it, if they test an - * entity which requires the current entity.

- */ - public static SubjectDTO createEntityDTO(EntityManager em) { - SubjectDTO subject = new SubjectDTO(); - subject.setExternalLink(DEFAULT_EXTERNAL_LINK); - subject.setExternalId(DEFAULT_ENTERNAL_ID); - subject.setStatus(SubjectDTO.SubjectStatus.ACTIVATED); - ProjectDTO projectDto = new ProjectDTO(); - projectDto.setId(1L); - projectDto.setProjectName("Radar"); - projectDto.setLocation("SOMEWHERE"); - projectDto.setDescription("test"); - subject.setProject(projectDto); - return subject; - } @Test @Transactional @@ -166,7 +138,7 @@ public void createSubject() throws Exception { final int databaseSizeBeforeCreate = subjectRepository.findAll().size(); // Create the Subject - SubjectDTO subjectDto = createEntityDTO(em); + SubjectDTO subjectDto = createEntityDTO(); restSubjectMockMvc.perform(post("/api/subjects") .contentType(TestUtil.APPLICATION_JSON_UTF8) .content(TestUtil.convertObjectToJsonBytes(subjectDto))) @@ -186,7 +158,7 @@ public void createSubject() throws Exception { @Transactional public void createSubjectWithExistingId() throws Exception { // Create a Subject - SubjectDTO subjectDto = subjectService.createSubject(createEntityDTO(em)); + SubjectDTO subjectDto = subjectService.createSubject(createEntityDTO()); final int databaseSizeBeforeCreate = subjectRepository.findAll().size(); // An entity with an existing ID cannot be created, so this API call must fail @@ -204,7 +176,7 @@ public void createSubjectWithExistingId() throws Exception { @Transactional public void getAllSubjects() throws Exception { // Initialize the database - SubjectDTO subjectDto = subjectService.createSubject(createEntityDTO(em)); + SubjectDTO subjectDto = subjectService.createSubject(createEntityDTO()); // Get all the subjectList restSubjectMockMvc.perform(get("/api/subjects?sort=id,desc")) @@ -220,7 +192,7 @@ public void getAllSubjects() throws Exception { @Transactional public void getSubject() throws Exception { // Initialize the database - SubjectDTO subjectDto = subjectService.createSubject(createEntityDTO(em)); + SubjectDTO subjectDto = subjectService.createSubject(createEntityDTO()); // Get the subject restSubjectMockMvc.perform(get("/api/subjects/{login}", subjectDto.getLogin())) @@ -244,7 +216,7 @@ public void getNonExistingSubject() throws Exception { @Transactional public void updateSubject() throws Exception { // Initialize the database - SubjectDTO subjectDto = subjectService.createSubject(createEntityDTO(em)); + SubjectDTO subjectDto = subjectService.createSubject(createEntityDTO()); final int databaseSizeBeforeUpdate = subjectRepository.findAll().size(); // Update the subject @@ -273,7 +245,7 @@ public void updateSubject() throws Exception { @Transactional public void updateSubjectWithNewProject() throws Exception { // Initialize the database - SubjectDTO subjectDto = subjectService.createSubject(createEntityDTO(em)); + SubjectDTO subjectDto = subjectService.createSubject(createEntityDTO()); final int databaseSizeBeforeUpdate = subjectRepository.findAll().size(); // Update the subject @@ -313,7 +285,7 @@ public void updateNonExistingSubject() throws Exception { final int databaseSizeBeforeUpdate = subjectRepository.findAll().size(); // Create the Subject - SubjectDTO subjectDto = createEntityDTO(em); + SubjectDTO subjectDto = createEntityDTO(); // If the entity doesn't have an ID, it will be created instead of just being updated restSubjectMockMvc.perform(put("/api/subjects") @@ -330,7 +302,7 @@ public void updateNonExistingSubject() throws Exception { @Transactional public void deleteSubject() throws Exception { // Initialize the database - SubjectDTO subjectDto = subjectService.createSubject(createEntityDTO(em)); + SubjectDTO subjectDto = subjectService.createSubject(createEntityDTO()); final int databaseSizeBeforeDelete = subjectRepository.findAll().size(); // Get the subject @@ -356,7 +328,7 @@ public void dynamicSourceRegistrationWithId() throws Exception { final int databaseSizeBeforeCreate = subjectRepository.findAll().size(); // Create the Subject - SubjectDTO subjectDto = createEntityDTO(em); + SubjectDTO subjectDto = createEntityDTO(); restSubjectMockMvc.perform(post("/api/subjects") .contentType(TestUtil.APPLICATION_JSON_UTF8) .content(TestUtil.convertObjectToJsonBytes(subjectDto))) @@ -393,7 +365,7 @@ public void dynamicSourceRegistrationWithoutId() throws Exception { final int databaseSizeBeforeCreate = subjectRepository.findAll().size(); // Create the Subject - SubjectDTO subjectDto = createEntityDTO(em); + SubjectDTO subjectDto = createEntityDTO(); restSubjectMockMvc.perform(post("/api/subjects") .contentType(TestUtil.APPLICATION_JSON_UTF8) .content(TestUtil.convertObjectToJsonBytes(subjectDto))) @@ -436,7 +408,7 @@ public void dynamicSourceRegistrationWithoutId() throws Exception { @Transactional public void getSubjectSources() throws Exception { // Initialize the database - SubjectDTO subjectDtoToCreate = createEntityDTO(em); + SubjectDTO subjectDtoToCreate = createEntityDTO(); SourceDTO createdSource = sourceService.save(createSource()); MinimalSourceDetailsDTO sourceDto = new MinimalSourceDetailsDTO() .id(createdSource.getId()) @@ -463,7 +435,7 @@ public void getSubjectSources() throws Exception { @Transactional public void getSubjectSourcesWithQueryParam() throws Exception { // Initialize the database - SubjectDTO subjectDtoToCreate = createEntityDTO(em); + SubjectDTO subjectDtoToCreate = createEntityDTO(); SourceDTO createdSource = sourceService.save(createSource()); MinimalSourceDetailsDTO sourceDto = new MinimalSourceDetailsDTO() .id(createdSource.getId()) @@ -492,7 +464,7 @@ public void getSubjectSourcesWithQueryParam() throws Exception { @Transactional public void getInactiveSubjectSourcesWithQueryParam() throws Exception { // Initialize the database - SubjectDTO subjectDtoToCreate = createEntityDTO(em); + SubjectDTO subjectDtoToCreate = createEntityDTO(); SourceDTO createdSource = sourceService.save(createSource()); MinimalSourceDetailsDTO sourceDto = new MinimalSourceDetailsDTO() .id(createdSource.getId()) @@ -576,7 +548,7 @@ public void testDynamicRegistrationAndUpdateSourceAttributes() throws Exception final int databaseSizeBeforeCreate = subjectRepository.findAll().size(); // Create the Subject - SubjectDTO subjectDto = createEntityDTO(em); + SubjectDTO subjectDto = createEntityDTO(); restSubjectMockMvc.perform(post("/api/subjects") .contentType(TestUtil.APPLICATION_JSON_UTF8) .content(TestUtil.convertObjectToJsonBytes(subjectDto))) diff --git a/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java b/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java index 153f0fa68..d599a4b71 100644 --- a/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java +++ b/src/test/java/org/radarcns/management/web/rest/UserResourceIntTest.java @@ -37,6 +37,19 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.hasItem; +import static org.radarcns.management.service.UserServiceIntTest.DEFAULT_EMAIL; +import static org.radarcns.management.service.UserServiceIntTest.DEFAULT_FIRSTNAME; +import static org.radarcns.management.service.UserServiceIntTest.DEFAULT_LANGKEY; +import static org.radarcns.management.service.UserServiceIntTest.DEFAULT_LASTNAME; +import static org.radarcns.management.service.UserServiceIntTest.DEFAULT_LOGIN; +import static org.radarcns.management.service.UserServiceIntTest.DEFAULT_PASSWORD; +import static org.radarcns.management.service.UserServiceIntTest.UPDATED_EMAIL; +import static org.radarcns.management.service.UserServiceIntTest.UPDATED_FIRSTNAME; +import static org.radarcns.management.service.UserServiceIntTest.UPDATED_LANGKEY; +import static org.radarcns.management.service.UserServiceIntTest.UPDATED_LASTNAME; +import static org.radarcns.management.service.UserServiceIntTest.UPDATED_LOGIN; +import static org.radarcns.management.service.UserServiceIntTest.UPDATED_PASSWORD; +import static org.radarcns.management.service.UserServiceIntTest.createEntity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -55,24 +68,6 @@ @WithMockUser public class UserResourceIntTest { - private static final String DEFAULT_LOGIN = "johndoe"; - private static final String UPDATED_LOGIN = "jhipster"; - - private static final String DEFAULT_PASSWORD = "passjohndoe"; - private static final String UPDATED_PASSWORD = "passjhipster"; - - private static final String DEFAULT_EMAIL = "johndoe@localhost"; - private static final String UPDATED_EMAIL = "jhipster@localhost"; - - private static final String DEFAULT_FIRSTNAME = "john"; - private static final String UPDATED_FIRSTNAME = "jhipsterFirstName"; - - private static final String DEFAULT_LASTNAME = "doe"; - private static final String UPDATED_LASTNAME = "jhipsterLastName"; - - private static final String DEFAULT_LANGKEY = "en"; - private static final String UPDATED_LANGKEY = "fr"; - @Autowired private UserRepository userRepository; @@ -122,23 +117,7 @@ public void setUp() throws ServletException { .defaultRequest(get("/").with(OAuthHelper.bearerToken())).build(); } - /** - * Create a User. - * - *

This is a static method, as tests for other entities might also need it, - * if they test an entity which has a required relationship to the User entity.

- */ - public static User createEntity() { - User user = new User(); - user.setLogin(DEFAULT_LOGIN); - user.setPassword(RandomStringUtils.random(60)); - user.setActivated(true); - user.setEmail(DEFAULT_EMAIL); - user.setFirstName(DEFAULT_FIRSTNAME); - user.setLastName(DEFAULT_LASTNAME); - user.setLangKey(DEFAULT_LANGKEY); - return user; - } + @Before public void initTest() { diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index f3556d344..e13c7bb9c 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -111,3 +111,8 @@ managementportal: oauth: keyStorePassword: radarbase signingKeyAlias: radarbase-managementportal-ec + metaTokenTimeout: 1H + common: + baseUrl: http://localhost:8080 + managementPortalBaseUrl: http://localhost:9000 + privacyPolicyUrl: http://info.thehyve.nl/radar-cns-privacy-policy