From df7f057245206086387d761a6e82cb5e192adaae Mon Sep 17 00:00:00 2001 From: nivethika Date: Fri, 5 Jun 2020 15:42:07 +0200 Subject: [PATCH 01/80] bump dev version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 89214949..05f8add2 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' group = 'org.radarcns' -version = '1.3.1' +version = '1.4.0-SNAPSHOT' sourceCompatibility = 1.8 repositories { From 384dff564f99121b4a657df001a0f65917746d05 Mon Sep 17 00:00:00 2001 From: nivethika Date: Fri, 5 Jun 2020 16:18:39 +0200 Subject: [PATCH 02/80] reorganise project structure --- .editorconfig | 25 +++++ .../.dockerignore | 0 .../Dockerfile | 0 authorizer-app-backend/build.gradle.kts | 98 +++++++++++++++++++ .../java/org/radarbase/authorizer/Main.kt | 35 +++++++ .../RadarRestSourceAuthorizerApplication.java | 0 .../config/AuthTokenValidatorConfig.java | 0 .../authorizer/config/ConfigHelper.java | 0 .../config/ManagementPortalProperties.java | 0 .../RestSourceAuthorizerProperties.java | 0 .../config/RestSourceClientConfig.java | 0 .../authorizer/config/RestSourceClients.java | 0 .../authorizer/domain/RestSourceUser.java | 0 .../repository/RestSourceUserRepository.java | 0 .../CustomWebSecurityConfigurerAdapter.java | 0 .../security/JwtAuthenticationFilter.java | 0 .../service/RestSourceClientService.java | 0 .../service/RestSourceUserService.java | 0 .../service/dto/Oauth2AccessToken.java | 0 .../service/dto/RestSourceAccessToken.java | 0 .../dto/RestSourceClientDetailsDTO.java | 0 .../service/dto/RestSourceClients.java | 0 .../dto/RestSourceUserPropertiesDTO.java | 0 .../service/dto/RestSourceUsers.java | 0 .../authorizer/service/dto/TokenDTO.java | 0 .../service/dto/managementportal/Project.java | 0 .../service/dto/managementportal/Subject.java | 0 .../CachedManagementPortalClient.java | 0 .../ManagementPortalClient.java | 0 .../validation/ManagementPortalValidator.java | 0 .../authorizer/validation/Validator.java | 0 .../exception/ValidationFailedException.java | 0 .../webapp/exception/BadGatewayException.java | 0 .../exception/InvalidSourceTypeException.java | 0 .../webapp/exception/NotFoundException.java | 0 .../webapp/exception/TokenException.java | 0 .../webapp/resource/HealthCheckResource.java | 0 .../resource/RestSourceUserResource.java | 0 .../webapp/resource/SourceClientResource.java | 0 .../src}/main/resources/application.yml | 0 .../changes/00000000000000_initial_schema.xml | 0 .../changes/00000000000001_update_schema.xml | 0 .../changes/00000000000002_update_schema.xml | 0 .../db/changelog/db.changelog-master.yaml | 0 .../resource/RestSourceUserResourceTest.java | 0 build.gradle | 73 -------------- build.gradle.kts | 12 +++ settings.gradle | 1 + 48 files changed, 171 insertions(+), 73 deletions(-) create mode 100644 .editorconfig rename .dockerignore => authorizer-app-backend/.dockerignore (100%) rename Dockerfile => authorizer-app-backend/Dockerfile (100%) create mode 100644 authorizer-app-backend/build.gradle.kts create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/config/AuthTokenValidatorConfig.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/config/ConfigHelper.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/config/ManagementPortalProperties.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/config/RestSourceClientConfig.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/config/RestSourceClients.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/domain/RestSourceUser.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/repository/RestSourceUserRepository.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/RestSourceClientService.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/RestSourceUserService.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/dto/Oauth2AccessToken.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/dto/RestSourceAccessToken.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/dto/RestSourceClientDetailsDTO.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/dto/RestSourceClients.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/dto/RestSourceUserPropertiesDTO.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/dto/RestSourceUsers.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/dto/TokenDTO.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/dto/managementportal/Project.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/dto/managementportal/Subject.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/validation/Validator.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java (100%) rename {src => authorizer-app-backend/src}/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java (100%) rename {src => authorizer-app-backend/src}/main/resources/application.yml (100%) rename {src => authorizer-app-backend/src}/main/resources/db/changelog/changes/00000000000000_initial_schema.xml (100%) rename {src => authorizer-app-backend/src}/main/resources/db/changelog/changes/00000000000001_update_schema.xml (100%) rename {src => authorizer-app-backend/src}/main/resources/db/changelog/changes/00000000000002_update_schema.xml (100%) rename {src => authorizer-app-backend/src}/main/resources/db/changelog/db.changelog-master.yaml (100%) rename {src => authorizer-app-backend/src}/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java (100%) delete mode 100644 build.gradle create mode 100644 build.gradle.kts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..18ccaf40 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +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 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.{json,yaml,yml}] +indent_style = space +indent_size = 2 +continuation_indent_size = 4 diff --git a/.dockerignore b/authorizer-app-backend/.dockerignore similarity index 100% rename from .dockerignore rename to authorizer-app-backend/.dockerignore diff --git a/Dockerfile b/authorizer-app-backend/Dockerfile similarity index 100% rename from Dockerfile rename to authorizer-app-backend/Dockerfile diff --git a/authorizer-app-backend/build.gradle.kts b/authorizer-app-backend/build.gradle.kts new file mode 100644 index 00000000..78feb53e --- /dev/null +++ b/authorizer-app-backend/build.gradle.kts @@ -0,0 +1,98 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + java + application + kotlin("jvm") + id("org.jetbrains.kotlin.plugin.noarg") version "1.3.61" + id("org.jetbrains.kotlin.plugin.jpa") version "1.3.61" + id("org.jetbrains.kotlin.plugin.allopen") version "1.3.61" +} + + +application { + mainClassName = "org.radarbase.authorizer.MainKt" +} + +project.extra.apply { + set("okhttpVersion", "4.2.0") + set("radarJerseyVersion", "0.2.3") + set("jacksonVersion", "2.9.10") + set("slf4jVersion", "1.7.27") + set("logbackVersion", "1.2.3") + set("grizzlyVersion", "2.4.4") + set("jerseyVersion", "2.30") + set("hibernateVersion", "5.4.10.Final") + set("githubRepoName", "RADAR-base/RADAR-Rest-Source-Auth") + set("githubUrl", "https://github.com/RADAR-base/RADAR-Rest-Source-Auth.git") + set("issueUrl", "https://github.com/RADAR-base/RADAR-Rest-Source-Auth/issues") + set("website", "http://radar-base.org") + set("description", "RADAR Rest Source Authorizer handles authorization for data access from third party APIs for wearable devices or other connected sources.") +} + + +repositories { + jcenter() + mavenCentral() + maven(url = "https://dl.bintray.com/radar-base/org.radarbase") + maven(url = "https://dl.bintray.com/radar-cns/org.radarcns") + maven(url = "https://repo.thehyve.nl/content/repositories/snapshots") + maven(url = "https://oss.jfrog.org/artifactory/libs-snapshot/") +} + +dependencies { + api(kotlin("stdlib-jdk8")) + + implementation("org.radarbase:radar-jersey:${project.extra["radarJerseyVersion"]}") + + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${project.extra["jacksonVersion"]}") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${project.extra["jacksonVersion"]}") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:${project.extra["jacksonVersion"]}") + + implementation("org.slf4j:slf4j-api:${project.extra["slf4jVersion"]}") + + implementation("org.hibernate:hibernate-core:${project.extra["hibernateVersion"]}") + implementation("org.hibernate:hibernate-c3p0:${project.extra["hibernateVersion"]}") + implementation("org.liquibase:liquibase-core:3.5.3") + + implementation("com.squareup.okhttp3:okhttp:${project.extra["okhttpVersion"]}") + + runtimeOnly("com.h2database:h2:1.4.199") + runtimeOnly("org.postgresql:postgresql:42.2.5") + runtimeOnly("ch.qos.logback:logback-classic:${project.extra["logbackVersion"]}") + +// testImplementation("com.h2database:h2:1.4.199") + testImplementation("org.junit.jupiter:junit-jupiter:5.4.2") + testImplementation("org.hamcrest:hamcrest-all:1.3") + testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0") + + testImplementation("org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2:${project.extra["jerseyVersion"]}") +} + + +// config JVM target to 1.8 for kotlin compilation tasks +tasks.withType { + kotlinOptions.jvmTarget = "11" +} + +tasks.withType { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } +} + +allOpen { + annotation("javax.persistence.Entity") + annotation("javax.persistence.MappedSuperclass") + annotation("javax.persistence.Embeddable") +} + +tasks.register("downloadDependencies") { + configurations["runtimeClasspath"].files + configurations["compileClasspath"].files + + doLast { + println("Downloaded all dependencies") + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt new file mode 100644 index 00000000..f26861b3 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt @@ -0,0 +1,35 @@ +/* + * + * * Copyright 2019 The Hyve + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * * + * + */ + +package org.radarbase.authorizer + +import org.radarbase.jersey.GrizzlyServer +import org.radarbase.jersey.config.ConfigLoader +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +val logger: Logger = LoggerFactory.getLogger("org.radarbase.authorizer.Main") + +fun main(args: Array) { + val config: Config = ConfigLoader.loadConfig("authorizer.yml", args) + val resources = ConfigLoader.loadResources(config.resourceConfig, config) + + val server = GrizzlyServer(config.baseUri, resources) + server.listen() +} diff --git a/src/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java diff --git a/src/main/java/org/radarbase/authorizer/config/AuthTokenValidatorConfig.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthTokenValidatorConfig.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/config/AuthTokenValidatorConfig.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthTokenValidatorConfig.java diff --git a/src/main/java/org/radarbase/authorizer/config/ConfigHelper.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ConfigHelper.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/config/ConfigHelper.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ConfigHelper.java diff --git a/src/main/java/org/radarbase/authorizer/config/ManagementPortalProperties.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ManagementPortalProperties.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/config/ManagementPortalProperties.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ManagementPortalProperties.java diff --git a/src/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java diff --git a/src/main/java/org/radarbase/authorizer/config/RestSourceClientConfig.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClientConfig.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/config/RestSourceClientConfig.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClientConfig.java diff --git a/src/main/java/org/radarbase/authorizer/config/RestSourceClients.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClients.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/config/RestSourceClients.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClients.java diff --git a/src/main/java/org/radarbase/authorizer/domain/RestSourceUser.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/domain/RestSourceUser.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/domain/RestSourceUser.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/domain/RestSourceUser.java diff --git a/src/main/java/org/radarbase/authorizer/repository/RestSourceUserRepository.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/repository/RestSourceUserRepository.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/repository/RestSourceUserRepository.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/repository/RestSourceUserRepository.java diff --git a/src/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java diff --git a/src/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java diff --git a/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/RestSourceClientService.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.java diff --git a/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/RestSourceUserService.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.java diff --git a/src/main/java/org/radarbase/authorizer/service/dto/Oauth2AccessToken.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/Oauth2AccessToken.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/dto/Oauth2AccessToken.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/Oauth2AccessToken.java diff --git a/src/main/java/org/radarbase/authorizer/service/dto/RestSourceAccessToken.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceAccessToken.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/dto/RestSourceAccessToken.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceAccessToken.java diff --git a/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClientDetailsDTO.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClientDetailsDTO.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/dto/RestSourceClientDetailsDTO.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClientDetailsDTO.java diff --git a/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClients.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClients.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/dto/RestSourceClients.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClients.java diff --git a/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUserPropertiesDTO.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUserPropertiesDTO.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/dto/RestSourceUserPropertiesDTO.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUserPropertiesDTO.java diff --git a/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUsers.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUsers.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/dto/RestSourceUsers.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUsers.java diff --git a/src/main/java/org/radarbase/authorizer/service/dto/TokenDTO.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/TokenDTO.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/dto/TokenDTO.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/TokenDTO.java diff --git a/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Project.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Project.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/dto/managementportal/Project.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Project.java diff --git a/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Subject.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Subject.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/dto/managementportal/Subject.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Subject.java diff --git a/src/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java diff --git a/src/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java diff --git a/src/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java diff --git a/src/main/java/org/radarbase/authorizer/validation/Validator.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/Validator.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/validation/Validator.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/Validator.java diff --git a/src/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java diff --git a/src/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java diff --git a/src/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java diff --git a/src/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java diff --git a/src/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java diff --git a/src/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java diff --git a/src/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java diff --git a/src/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java similarity index 100% rename from src/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java diff --git a/src/main/resources/application.yml b/authorizer-app-backend/src/main/resources/application.yml similarity index 100% rename from src/main/resources/application.yml rename to authorizer-app-backend/src/main/resources/application.yml diff --git a/src/main/resources/db/changelog/changes/00000000000000_initial_schema.xml b/authorizer-app-backend/src/main/resources/db/changelog/changes/00000000000000_initial_schema.xml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000000_initial_schema.xml rename to authorizer-app-backend/src/main/resources/db/changelog/changes/00000000000000_initial_schema.xml diff --git a/src/main/resources/db/changelog/changes/00000000000001_update_schema.xml b/authorizer-app-backend/src/main/resources/db/changelog/changes/00000000000001_update_schema.xml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000001_update_schema.xml rename to authorizer-app-backend/src/main/resources/db/changelog/changes/00000000000001_update_schema.xml diff --git a/src/main/resources/db/changelog/changes/00000000000002_update_schema.xml b/authorizer-app-backend/src/main/resources/db/changelog/changes/00000000000002_update_schema.xml similarity index 100% rename from src/main/resources/db/changelog/changes/00000000000002_update_schema.xml rename to authorizer-app-backend/src/main/resources/db/changelog/changes/00000000000002_update_schema.xml diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/authorizer-app-backend/src/main/resources/db/changelog/db.changelog-master.yaml similarity index 100% rename from src/main/resources/db/changelog/db.changelog-master.yaml rename to authorizer-app-backend/src/main/resources/db/changelog/db.changelog-master.yaml diff --git a/src/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java b/authorizer-app-backend/src/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java similarity index 100% rename from src/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java rename to authorizer-app-backend/src/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 05f8add2..00000000 --- a/build.gradle +++ /dev/null @@ -1,73 +0,0 @@ -buildscript { - ext { - springBootVersion = '2.2.4.RELEASE' - } - repositories { - mavenCentral() - } - dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") - } -} - -apply plugin: 'java' -apply plugin: 'eclipse' -apply plugin: 'org.springframework.boot' -apply plugin: 'io.spring.dependency-management' - -group = 'org.radarcns' -version = '1.4.0-SNAPSHOT' -sourceCompatibility = 1.8 - -repositories { - jcenter() - mavenCentral() - - maven { url 'https://oss.jfrog.org/artifactory/libs-snapshot/' } - maven { url 'https://dl.bintray.com/radar-cns/org.radarcns' } -} - -ext { - githubRepoName = 'RADAR-base/RADAR-Rest-Source-Auth' - githubUrl = 'https://github.com/' + githubRepoName + '.git' - issueUrl = 'https://github.com/' + githubRepoName + '/issues' - website = 'http://radar-base.org' - description = 'RADAR Rest Source Authorizer handles authorization for data access from third party APIs for wearable devices or other connected sources.' - - okhttp3Version = '3.13.1' - managementPortalVersion = '0.5.8' -} - -dependencies { - compileOnly('org.projectlombok:lombok:1.18.10') - annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" - implementation('org.springframework.boot:spring-boot-starter') - implementation('org.springframework.boot:spring-boot-starter-data-jpa') - implementation('org.springframework.boot:spring-boot-starter-jersey') - implementation('org.springframework.boot:spring-boot-starter-validation') - implementation('org.springframework.boot:spring-boot-starter-web') - compile "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" - compile('com.fasterxml.jackson.datatype:jackson-datatype-jsr310') - compile "org.springframework.boot:spring-boot-starter-security" - compile group: 'org.radarcns', name: 'radar-auth', version: '0.5.8' - implementation('org.liquibase:liquibase-core') - implementation('org.springframework.session:spring-session-core') - implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: okhttp3Version - implementation group: 'org.radarcns', name: 'oauth-client-util', version: managementPortalVersion - runtimeOnly('org.postgresql:postgresql') - testImplementation('org.springframework.boot:spring-boot-starter-test') -} - -springBoot { - mainClassName = 'org.radarbase.authorizer.RadarRestSourceAuthorizerApplication' -} - -wrapper { - gradleVersion '6.5' -} - -task downloadDependencies { - description "Pre-downloads dependencies" - configurations.compileClasspath.files - configurations.runtimeClasspath.files -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..ed08fda7 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + kotlin("jvm") version "1.3.61" apply false +} + +subprojects { + group = "org.radarbase" + version = "1.4.0-SNAPSHOT" +} + +tasks.wrapper { + gradleVersion = "6.5" +} diff --git a/settings.gradle b/settings.gradle index 742f3366..22caeed0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ rootProject.name = 'radar-rest-sources-authorizer' +include('authorizer-app-backend') \ No newline at end of file From d0cf8113bb015bbc08d16e69a86ff39cd28ca623 Mon Sep 17 00:00:00 2001 From: nivethika Date: Thu, 27 Aug 2020 15:50:01 +0200 Subject: [PATCH 03/80] commit compilable --- .editorconfig | 4 +- authorizer-app-backend/build.gradle.kts | 1 - .../java/org/radarbase/authorizer/Config.kt | 40 ++ .../java/org/radarbase/authorizer/Main.kt | 4 +- .../RadarRestSourceAuthorizerApplication.java | 136 +++--- .../authorizer/api/ApiDeclarations.kt | 79 ++++ .../RestSourceUsers.java => api/Project.kt} | 20 +- .../authorizer/api/RestSourceClientMapper.kt | 7 + .../api/RestSourceClientMapperImpl.kt | 15 + .../authorizer/api/RestSourceUserMapper.kt | 20 + .../authorizer/config/ConfigHelper.java | 88 ++-- .../RestSourceAuthorizerProperties.java | 200 ++++---- .../authorizer/doa/AbstractJpaPersistable.kt | 45 ++ .../authorizer/doa/EntityManagerExtensions.kt | 53 +++ .../authorizer/doa/RestSourceUser.java | 277 +++++++++++ .../doa/RestSourceUserRepository.kt | 14 + .../authorizer/doa/entitiy/RestSourceUser.kt | 68 +++ .../authorizer/domain/RestSourceUser.java | 282 ----------- .../inject/ManagementPortalEnhancerFactory.kt | 63 +++ .../inject/ProjectServiceWrapper.kt | 12 + .../inject/UploadResourceEnhancer.kt | 93 ++++ .../repository/RestSourceUserRepository.java | 35 -- .../CustomWebSecurityConfigurerAdapter.java | 86 ++-- .../security/JwtAuthenticationFilter.java | 172 +++---- .../service/RestSourceClientService.java | 430 ++++++++--------- .../service/RestSourceClientService.kt | 5 + .../service/RestSourceUserService.java | 440 ++++++++--------- .../service/RestSourcesProjectService.kt | 14 + .../service/dto/Oauth2AccessToken.java | 109 ----- .../service/dto/RestSourceAccessToken.java | 37 -- .../dto/RestSourceClientDetailsDTO.java | 97 ---- .../service/dto/RestSourceClients.java | 36 -- .../dto/RestSourceUserPropertiesDTO.java | 224 --------- .../authorizer/service/dto/TokenDTO.java | 47 -- .../CachedManagementPortalClient.java | 446 +++++++++--------- .../service/managementportal/MPClient.kt | 138 ++++++ .../managementportal/MPProjectService.kt | 71 +++ .../ManagementPortalClient.java | 36 +- .../radarbase/authorizer/util/CachedSet.kt | 71 +++ .../validation/ManagementPortalValidator.java | 130 ++--- .../authorizer/validation/Validator.java | 16 +- .../exception/ValidationFailedException.java | 42 +- .../webapp/exception/BadGatewayException.java | 78 +-- .../exception/InvalidSourceTypeException.java | 70 +-- .../webapp/exception/NotFoundException.java | 64 +-- .../webapp/exception/TokenException.java | 78 +-- .../webapp/resource/HealthCheckResource.java | 50 +- .../resource/RestSourceUserResource.java | 264 +++++------ .../webapp/resource/SourceClientResource.java | 126 ++--- .../resource/RestSourceUserResourceTest.java | 4 +- build.gradle.kts | 4 +- .../etc/rest-source-authorizer/authorizer.yml | 20 + gradle/wrapper/gradle-wrapper.jar | Bin 55190 -> 58910 bytes gradlew | 53 ++- gradlew.bat | 22 +- settings.gradle | 2 +- 56 files changed, 2650 insertions(+), 2388 deletions(-) create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt rename authorizer-app-backend/src/main/java/org/radarbase/authorizer/{service/dto/RestSourceUsers.java => api/Project.kt} (59%) create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapperImpl.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUser.java create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entitiy/RestSourceUser.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/domain/RestSourceUser.java create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/UploadResourceEnhancer.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/repository/RestSourceUserRepository.java create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourcesProjectService.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/Oauth2AccessToken.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceAccessToken.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClientDetailsDTO.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClients.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUserPropertiesDTO.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/TokenDTO.java create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt create mode 100644 docker/etc/rest-source-authorizer/authorizer.yml diff --git a/.editorconfig b/.editorconfig index 18ccaf40..fbdb6f7c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,8 +7,8 @@ root = true [*] # Change these settings to your own preference indent_style = space -indent_size = 4 -continuation_indent_size = 8 +indent_size = 2 +continuation_indent_size = 4 # We recommend you to keep these unchanged end_of_line = lf diff --git a/authorizer-app-backend/build.gradle.kts b/authorizer-app-backend/build.gradle.kts index 78feb53e..8426a883 100644 --- a/authorizer-app-backend/build.gradle.kts +++ b/authorizer-app-backend/build.gradle.kts @@ -70,7 +70,6 @@ dependencies { } -// config JVM target to 1.8 for kotlin compilation tasks tasks.withType { kotlinOptions.jvmTarget = "11" } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt new file mode 100644 index 00000000..74dd1c03 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt @@ -0,0 +1,40 @@ +package org.radarbase.authorizer + +import org.radarbase.authorizer.inject.ManagementPortalEnhancerFactory +import org.radarbase.jersey.config.EnhancerFactory +import java.net.URI + +data class Config( + val service: AuthorizerServiceConfig = AuthorizerServiceConfig(), + val auth: AuthConfig = AuthConfig(), + val database: DatabaseConfig = DatabaseConfig() +) + + +data class AuthorizerServiceConfig( + var baseUri: URI = URI.create("http://0.0.0.0:8080/rest-sources/backend/"), + var advertisedBaseUri: URI? = null, + var resourceConfig: Class = ManagementPortalEnhancerFactory::class.java, + var enableCors: Boolean? = false, + var syncProjectsIntervalMin: Long = 30, + var syncParticipantsIntervalMin: Long = 30 +) + +data class AuthConfig( + var managementPortalUrl: String = "http://managementportal-app:8080/managementportal/", + var clientId: String = "radar_rest_sources_auth_backend", + var clientSecret: String? = null, + var jwtECPublicKeys: List? = null, + var jwtRSAPublicKeys: List? = null, + var jwtIssuer: String? = null, + var jwtResourceName: String = "res_restAuthorizer" +) + +data class DatabaseConfig( + var jdbcDriver: String? = "org.h2.Driver", + var jdbcUrl: String? = null, + var jdbcUser: String? = null, + var jdbcPassword: String? = null, + var hibernateDialect: String = "org.hibernate.dialect.PostgreSQLDialect", + var additionalPersistenceConfig: Map? = null +) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt index f26861b3..871ec9ce 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt @@ -28,8 +28,8 @@ val logger: Logger = LoggerFactory.getLogger("org.radarbase.authorizer.Main") fun main(args: Array) { val config: Config = ConfigLoader.loadConfig("authorizer.yml", args) - val resources = ConfigLoader.loadResources(config.resourceConfig, config) + val resources = ConfigLoader.loadResources(config.service.resourceConfig, config) - val server = GrizzlyServer(config.baseUri, resources) + val server = GrizzlyServer(config.service.baseUri, resources) server.listen() } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java index 9ac604e0..f7b021aa 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java @@ -1,68 +1,68 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer; - -import java.util.List; -import java.util.stream.Stream; - -import org.radarbase.authorizer.config.RestSourceAuthorizerProperties; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; - -@SpringBootApplication -@EnableConfigurationProperties({RestSourceAuthorizerProperties.class}) -public class RadarRestSourceAuthorizerApplication { - - @Autowired - private RestSourceAuthorizerProperties authorizerApplicationProperties; - - public static void main(String[] args) { - SpringApplication.run(RadarRestSourceAuthorizerApplication.class, args); - } - - @Bean - public WebMvcConfigurer corsConfigurer() { - return new WebMvcConfigurerAdapter() { - @Override - public void addCorsMappings(CorsRegistry registry) { - - CorsConfiguration corsConfiguration = authorizerApplicationProperties.getCors(); - Stream.of("/users/**", "/source-clients/**").forEach(p -> registry.addMapping(p) - .allowedOrigins(listToArray(corsConfiguration.getAllowedOrigins())) - .allowedMethods(listToArray(corsConfiguration.getAllowedMethods())) - .allowedHeaders(listToArray(corsConfiguration.getAllowedHeaders())) - .allowCredentials(corsConfiguration.getAllowCredentials())); - - } - }; - } - - private String[] listToArray(List list) { - return list.stream().toArray(String[]::new); - } - -} +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer; +// +//import java.util.List; +//import java.util.stream.Stream; +// +//import org.radarbase.authorizer.config.RestSourceAuthorizerProperties; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.SpringApplication; +//import org.springframework.boot.autoconfigure.SpringBootApplication; +//import org.springframework.boot.context.properties.EnableConfigurationProperties; +//import org.springframework.context.annotation.Bean; +//import org.springframework.web.cors.CorsConfiguration; +//import org.springframework.web.servlet.config.annotation.CorsRegistry; +//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +//import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +// +//@SpringBootApplication +//@EnableConfigurationProperties({RestSourceAuthorizerProperties.class}) +//public class RadarRestSourceAuthorizerApplication { +// +// @Autowired +// private RestSourceAuthorizerProperties authorizerApplicationProperties; +// +// public static void main(String[] args) { +// SpringApplication.run(RadarRestSourceAuthorizerApplication.class, args); +// } +// +// @Bean +// public WebMvcConfigurer corsConfigurer() { +// return new WebMvcConfigurerAdapter() { +// @Override +// public void addCorsMappings(CorsRegistry registry) { +// +// CorsConfiguration corsConfiguration = authorizerApplicationProperties.getCors(); +// Stream.of("/users/**", "/source-clients/**").forEach(p -> registry.addMapping(p) +// .allowedOrigins(listToArray(corsConfiguration.getAllowedOrigins())) +// .allowedMethods(listToArray(corsConfiguration.getAllowedMethods())) +// .allowedHeaders(listToArray(corsConfiguration.getAllowedHeaders())) +// .allowCredentials(corsConfiguration.getAllowCredentials())); +// +// } +// }; +// } +// +// private String[] listToArray(List list) { +// return list.stream().toArray(String[]::new); +// } +// +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt new file mode 100644 index 00000000..c07af74b --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt @@ -0,0 +1,79 @@ +package org.radarbase.authorizer.api + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import java.io.Serializable +import java.time.Instant + + +@JsonIgnoreProperties(ignoreUnknown = true) +data class Oauth2AccessToken( + @JsonProperty("access_token") var accessToken: String? = null, + @JsonProperty("refresh_token") var refreshToken: String? = null, + @JsonProperty("expires_in") var expiresIn: Int? = null, + @JsonProperty("token_type") var tokenType: String? = null, + @JsonProperty("user_id") var externalUserId: String? = null) + + +data class RestSourceClientDetailsDTO( + var sourceType: String, + var authorizationEndpoint: String, + var tokenEndpoint: String, + var grantType: String, + var clientId: String, + var scope: String? = null +) + +data class RestSourceClients( + var sourceClients: List +) + +class RestSourceUserDTO( + var id: String? = null, + var projectId: String, + var userId: String, + var sourceId: String? = null, + var externalUserId: String? = null, + var startDate: Instant, + var endDate: Instant? = null, + var sourceType: String? = null, + var isAuthorized: Boolean = false, + var version: String? = null, + var timesReset: Long = 0) : Serializable { + companion object { + private const val serialVersionUID = 1L + } +} + +data class RestSourceUsers( + var users: List +) + +class TokenDTO( + var accessToken: String? = null, + var expiresAt: Instant? = null +) + +data class Page( + val pageNumber: Int = 1, + val pageSize: Int? = null, + val totalElements: Long? = null) { + val offset: Int + get() = (this.pageNumber - 1) * this.pageSize!! + + fun createValid(maximum: Int? = null): Page { + val imposedNumber = pageNumber.coerceAtLeast(1) + + val imposedSize = if (maximum != null) { + require(maximum >= 1) { "Maximum page size should be at least 1" } + pageSize?.coerceAtLeast(1)?.coerceAtMost(maximum) ?: maximum + } else { + pageSize?.coerceAtLeast(1) + } + return if (imposedNumber == pageNumber && imposedSize == pageSize) { + this + } else { + copy(pageNumber = imposedNumber, pageSize = imposedSize) + } + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUsers.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt similarity index 59% rename from authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUsers.java rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt index 91fa1f64..47b50cf8 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUsers.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt @@ -1,6 +1,6 @@ /* * - * * Copyright 2018 The Hyve + * * Copyright 2019 The Hyve * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. @@ -17,20 +17,12 @@ * */ -package org.radarbase.authorizer.service.dto; +package org.radarbase.authorizer.api -import java.util.List; +data class ProjectList(val projects: List) -public class RestSourceUsers { +data class Project(val id: String, val name: String? = null, val location: String? = null, val organization: String? = null, val description: String? = null) - List users; +data class UserList(val users: List) - public List getUsers() { - return users; - } - - public RestSourceUsers users(List users) { - this.users = users; - return this; - } -} +data class User(val id: String, val projectId: String, val externalId: String? = null, val status: String) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt new file mode 100644 index 00000000..af68c056 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt @@ -0,0 +1,7 @@ +package org.radarbase.authorizer.api + +import org.radarbase.authorizer.config.RestSourceClientConfig + +interface RestSourceClientMapper { + fun fromRestSourceClientConfig(config: RestSourceClientConfig): RestSourceClientDetailsDTO +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapperImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapperImpl.kt new file mode 100644 index 00000000..1588b1ca --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapperImpl.kt @@ -0,0 +1,15 @@ +package org.radarbase.authorizer.api + +import org.radarbase.authorizer.config.RestSourceClientConfig + +class RestSourceClientMapperImpl : RestSourceClientMapper { + + override fun fromRestSourceClientConfig(config: RestSourceClientConfig) = RestSourceClientDetailsDTO ( + authorizationEndpoint = config.authorizationEndpoint, + sourceType = config.sourceType, + grantType = config.grantType, + clientId = config.clientId, + scope = config.scope, + tokenEndpoint = config.tokenEndpoint + ) +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt new file mode 100644 index 00000000..067e2c03 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt @@ -0,0 +1,20 @@ +package org.radarbase.authorizer.api + +import org.radarbase.authorizer.doa.entitiy.RestSourceUser + + +interface RestSourceUserMapper { + fun fromRestSourceUser(user: RestSourceUser) = RestSourceUserDTO( + id = user.id.toString(), + projectId = user.projectId, + userId = user.userId, + sourceId = user.sourceId, + isAuthorized = user.authorized, + sourceType = user.sourceType, + endDate = user.endDate, + startDate = user.startDate, + externalUserId = user.externalUserId, + version = user.version, + timesReset = user.timesReset + ) +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ConfigHelper.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ConfigHelper.java index 7d87ad75..427b5325 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ConfigHelper.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ConfigHelper.java @@ -1,44 +1,44 @@ -package org.radarbase.authorizer.config; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.fasterxml.jackson.dataformat.yaml.YAMLParser; -import java.io.File; -import java.io.IOException; -import javax.naming.ConfigurationException; -import javax.validation.constraints.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ConfigHelper { - - private static final Logger LOGGER = LoggerFactory.getLogger(ConfigHelper.class); - - public static T loadPropertiesFromFile(@NotNull String path, - @NotNull TypeReference typeReference) - throws ConfigurationException { - LOGGER.info("Loading config from {}", path); - YAMLFactory yamlFactory = new YAMLFactory(); - try { - YAMLParser yamlParser = yamlFactory.createParser(new File(path)); - T properties = new ObjectMapper(yamlFactory) - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) - .readValue(yamlParser, typeReference); - - if (properties == null) { - LOGGER.error("No valid configurations available on configured path. Please " - + "check the syntax and file name"); - throw new ConfigurationException( - "No valid configs are provided" + "."); - } - return properties; - } catch (IOException e) { - LOGGER.error("Could not successfully read config file at {}", path); - throw new ConfigurationException("Could not successfully read config file at " + path); - } - } -} +//package org.radarbase.authorizer.config; +// +//import com.fasterxml.jackson.core.type.TypeReference; +//import com.fasterxml.jackson.databind.DeserializationFeature; +//import com.fasterxml.jackson.databind.ObjectMapper; +//import com.fasterxml.jackson.databind.PropertyNamingStrategy; +//import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +//import com.fasterxml.jackson.dataformat.yaml.YAMLParser; +//import java.io.File; +//import java.io.IOException; +//import javax.naming.ConfigurationException; +//import javax.validation.constraints.NotNull; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +// +//public class ConfigHelper { +// +// private static final Logger LOGGER = LoggerFactory.getLogger(ConfigHelper.class); +// +// public static T loadPropertiesFromFile(@NotNull String path, +// @NotNull TypeReference typeReference) +// throws ConfigurationException { +// LOGGER.info("Loading config from {}", path); +// YAMLFactory yamlFactory = new YAMLFactory(); +// try { +// YAMLParser yamlParser = yamlFactory.createParser(new File(path)); +// T properties = new ObjectMapper(yamlFactory) +// .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) +// .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) +// .readValue(yamlParser, typeReference); +// +// if (properties == null) { +// LOGGER.error("No valid configurations available on configured path. Please " +// + "check the syntax and file name"); +// throw new ConfigurationException( +// "No valid configs are provided" + "."); +// } +// return properties; +// } catch (IOException e) { +// LOGGER.error("Could not successfully read config file at {}", path); +// throw new ConfigurationException("Could not successfully read config file at " + path); +// } +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java index cf39027d..424c15a5 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java @@ -1,100 +1,100 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.config; - -import java.util.Objects; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.web.cors.CorsConfiguration; - -@ConfigurationProperties(prefix = "rest-source-authorizer", ignoreUnknownFields = false) -public class RestSourceAuthorizerProperties { - - private CorsConfiguration cors; - - private AuthTokenValidatorConfig auth; - - private String sourceClientsFilePath; - - private String validator; - - private ManagementPortalProperties managementPortal; - - public CorsConfiguration getCors() { - return cors; - } - - public void setCors(CorsConfiguration cors) { - this.cors = cors; - } - - public String getSourceClientsFilePath() { - return sourceClientsFilePath; - } - - public void setSourceClientsFilePath(String sourceClientsFilePath) { - this.sourceClientsFilePath = sourceClientsFilePath; - } - - public String getValidator() { - return validator; - } - - public void setValidator(String validator) { - this.validator = validator; - } - - public ManagementPortalProperties getManagementPortal() { - return managementPortal; - } - - public void setManagementPortal( - ManagementPortalProperties managementPortal) { - this.managementPortal = managementPortal; - } - - public AuthTokenValidatorConfig getAuth() { - return auth; - } - - public void setAuth(AuthTokenValidatorConfig auth) { - this.auth = auth; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RestSourceAuthorizerProperties that = (RestSourceAuthorizerProperties) o; - return Objects.equals(cors, that.cors) - && Objects.equals(sourceClientsFilePath, that.sourceClientsFilePath) - && Objects.equals(auth, that.auth); - } - - @Override - public int hashCode() { - - return Objects.hash(cors, sourceClientsFilePath); - } -} +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer.config; +// +//import java.util.Objects; +// +//import org.springframework.boot.context.properties.ConfigurationProperties; +//import org.springframework.web.cors.CorsConfiguration; +// +//@ConfigurationProperties(prefix = "rest-source-authorizer", ignoreUnknownFields = false) +//public class RestSourceAuthorizerProperties { +// +// private CorsConfiguration cors; +// +// private AuthTokenValidatorConfig auth; +// +// private String sourceClientsFilePath; +// +// private String validator; +// +// private ManagementPortalProperties managementPortal; +// +// public CorsConfiguration getCors() { +// return cors; +// } +// +// public void setCors(CorsConfiguration cors) { +// this.cors = cors; +// } +// +// public String getSourceClientsFilePath() { +// return sourceClientsFilePath; +// } +// +// public void setSourceClientsFilePath(String sourceClientsFilePath) { +// this.sourceClientsFilePath = sourceClientsFilePath; +// } +// +// public String getValidator() { +// return validator; +// } +// +// public void setValidator(String validator) { +// this.validator = validator; +// } +// +// public ManagementPortalProperties getManagementPortal() { +// return managementPortal; +// } +// +// public void setManagementPortal( +// ManagementPortalProperties managementPortal) { +// this.managementPortal = managementPortal; +// } +// +// public AuthTokenValidatorConfig getAuth() { +// return auth; +// } +// +// public void setAuth(AuthTokenValidatorConfig auth) { +// this.auth = auth; +// } +// +// @Override +// public boolean equals(Object o) { +// if (this == o) { +// return true; +// } +// if (o == null || getClass() != o.getClass()) { +// return false; +// } +// RestSourceAuthorizerProperties that = (RestSourceAuthorizerProperties) o; +// return Objects.equals(cors, that.cors) +// && Objects.equals(sourceClientsFilePath, that.sourceClientsFilePath) +// && Objects.equals(auth, that.auth); +// } +// +// @Override +// public int hashCode() { +// +// return Objects.hash(cors, sourceClientsFilePath); +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt new file mode 100644 index 00000000..daf5c7a0 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt @@ -0,0 +1,45 @@ +/* + * + * * Copyright 2019 The Hyve + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * * + * + */ + +package org.radarbase.upload.doa + +import javax.persistence.GeneratedValue +import javax.persistence.Id +import javax.persistence.MappedSuperclass + +@MappedSuperclass +abstract class AbstractJpaPersistable { + @Id + @GeneratedValue + var id: T? = null + + override fun equals(other: Any?): Boolean { + if (this === other) return true + + if (other == null || javaClass != other.javaClass) return false + + other as AbstractJpaPersistable<*> + + return id != null && id == other.id + } + + override fun hashCode(): Int = id.hashCode() + + override fun toString() = "Entity of type ${this.javaClass.name} with id: $id" +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt new file mode 100644 index 00000000..fedb46f1 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt @@ -0,0 +1,53 @@ +package org.radarbase.authorizer.doa + +import org.radarbase.jersey.exception.HttpInternalServerException +import java.io.Closeable +import javax.persistence.EntityManager +import javax.persistence.EntityTransaction + +/** + * Run a transaction and commit it. If an exception occurs, the transaction is rolled back. + */ +fun EntityManager.transact(transactionOperation: EntityManager.() -> T) = createTransaction { + it.use { transactionOperation() } +} + +/** + * Start a transaction without committing it. If an exception occurs, the transaction is rolled back. + */ +fun EntityManager.createTransaction(transactionOperation: EntityManager.(CloseableTransaction) -> T): T { + val currentTransaction = transaction + ?: throw HttpInternalServerException("transaction_not_found", "Cannot find a transaction from EntityManager") + + currentTransaction.begin() + try { + return transactionOperation(object : CloseableTransaction { + override val transaction: EntityTransaction = currentTransaction + + override fun close() { + try { + transaction.commit() + } catch (ex: Exception) { +// logger.error("Rolling back operation", ex) + if (currentTransaction.isActive) { + currentTransaction.rollback() + } + throw ex + } + } + }) + } catch (ex: Exception) { +// logger.error("Rolling back operation", ex) + if (currentTransaction.isActive) { + currentTransaction.rollback() + } + throw ex + } +} + + + +interface CloseableTransaction: Closeable { + val transaction: EntityTransaction + override fun close() +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUser.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUser.java new file mode 100644 index 00000000..4b745dac --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUser.java @@ -0,0 +1,277 @@ +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer.doa; +// +//import java.time.Duration; +//import java.time.Instant; +//import java.util.UUID; +//import javax.persistence.Entity; +//import javax.persistence.GeneratedValue; +//import javax.persistence.GenerationType; +//import javax.persistence.Id; +//import javax.persistence.Table; +//import javax.persistence.Transient; +// +//@Entity +//@Table(name = "rest_source_user") +//public class RestSourceUser { +// +// @Transient +// private static final Duration EXPIRY_TIME_MARGIN = Duration.ofMinutes(5); +// +// @Id +// @GeneratedValue(strategy = GenerationType.IDENTITY) +// private long id; +// +// // The version to be appended to ID for RESET of a user +// // This should be updated whenever the user is RESET. +// // By default this is null for backwards compatibility +// private String version = null; +// +// // The number of times a user has been reset +// private long timesReset = 0; +// +// // Project ID to be used in org.radarcns.kafka.ObservationKey record keys +// private String projectId; +// +// // User ID to be used in org.radarcns.kafka.ObservationKey record keys +// private String userId; +// +// // Source ID to be used in org.radarcns.kafka.ObservationKey record keys +// private String sourceId; +// +// // Date from when to collect data. +// private Instant startDate; +// +// // Date until when to collect data. +// private Instant endDate; +// +// +// private String sourceType; +// +// // is authorized by user +// private Boolean authorized = false; +// +// private String externalUserId; +// +// private String accessToken; +// +// private String refreshToken; +// +// private Integer expiresIn; +// +// private Instant expiresAt; +// +// private String tokenType; +// +// public RestSourceUser() { +// if (this.sourceId == null) { +// this.sourceId = UUID.randomUUID().toString(); +// } +// } +// +// public RestSourceUser(RestSourceUserPropertiesDTO restSourceUserPropertiesDTO) { +// if (restSourceUserPropertiesDTO.getId() != null) { +// this.id = Long.valueOf(restSourceUserPropertiesDTO.getId()); +// } +// this.projectId = restSourceUserPropertiesDTO.getProjectId(); +// this.userId = restSourceUserPropertiesDTO.getUserId(); +// this.sourceId = restSourceUserPropertiesDTO.getSourceId(); +// this.sourceType = restSourceUserPropertiesDTO.getSourceType(); +// this.startDate = restSourceUserPropertiesDTO.getStartDate(); +// this.endDate = restSourceUserPropertiesDTO.getEndDate(); +// this.externalUserId = restSourceUserPropertiesDTO.getExternalUserId(); +// this.authorized = restSourceUserPropertiesDTO.isAuthorized(); +// } +// +// public long getId() { +// return id; +// } +// +// public RestSourceUser id(Long id) { +// this.id = id; +// return this; +// } +// +// public String getVersion() { +// return version; +// } +// +// public RestSourceUser version(String version) { +// this.version = version; +// return this; +// } +// +// public long getTimesReset() { +// return timesReset; +// } +// +// public RestSourceUser setTimesReset(long timesReset) { +// this.timesReset = timesReset; +// return this; +// } +// +// public String getProjectId() { +// return projectId; +// } +// +// public RestSourceUser projectId(String projectId) { +// this.projectId = projectId; +// return this; +// } +// +// public String getUserId() { +// return userId; +// } +// +// public RestSourceUser userId(String userId) { +// this.userId = userId; +// return this; +// } +// +// public String getSourceId() { +// return sourceId; +// } +// +// public RestSourceUser sourceId(String sourceId) { +// this.sourceId = sourceId; +// return this; +// } +// +// public Instant getStartDate() { +// return startDate; +// } +// +// public RestSourceUser startDate(Instant startDate) { +// this.startDate = startDate; +// return this; +// } +// +// public Instant getEndDate() { +// return endDate; +// } +// +// public RestSourceUser endDate(Instant endDate) { +// this.endDate = endDate; +// return this; +// } +// +// public String getSourceType() { +// return sourceType; +// } +// +// public RestSourceUser sourceType(String sourceType) { +// this.sourceType = sourceType; +// return this; +// } +// +// public Boolean getAuthorized() { +// return authorized; +// } +// +// public RestSourceUser authorized(Boolean authorized) { +// this.authorized = authorized; +// return this; +// } +// +// public String getExternalUserId() { +// return externalUserId; +// } +// +// public RestSourceUser externalUserId(String externalUserId) { +// this.externalUserId = externalUserId; +// return this; +// } +// +// public String getAccessToken() { +// return accessToken; +// } +// +// public RestSourceUser accessToken(String accessToken) { +// this.accessToken = accessToken; +// return this; +// } +// +// public String getRefreshToken() { +// return refreshToken; +// } +// +// public RestSourceUser refreshToken(String refreshToken) { +// this.refreshToken = refreshToken; +// return this; +// } +// +// public Integer getExpiresIn() { +// return expiresIn; +// } +// +// public RestSourceUser expiresIn(Integer expiresIn) { +// this.expiresIn = expiresIn; +// return this; +// } +// +// public Instant getExpiresAt() { +// return expiresAt; +// } +// +// public RestSourceUser expiresIn(Instant expiresAt) { +// this.expiresAt = expiresAt; +// return this; +// } +// +// public String getTokenType() { +// return tokenType; +// } +// +// public RestSourceUser tokenType(String tokenType) { +// this.tokenType = tokenType; +// return this; +// } +// +// /** +// * Updates only a subset of user properties during update. +// * Does not update properties such as id and token data. +// * +// * @param sourceUserDto user details to update. +// */ +// public void safeUpdateProperties(RestSourceUserPropertiesDTO sourceUserDto) { +// this.projectId = sourceUserDto.getProjectId(); +// this.userId = sourceUserDto.getUserId(); +// this.sourceId = sourceUserDto.getSourceId(); +// this.startDate = sourceUserDto.getStartDate(); +// this.endDate = sourceUserDto.getEndDate(); +// } +// +// +// /** +// * Updates only the token related properties during update. +// * Does not update properties such as id and study information. +// * +// * @param restSourceAccessToken token details to update. +// */ +// public void safeUpdateTokenDetails(RestSourceAccessToken restSourceAccessToken) { +// this.accessToken = restSourceAccessToken.getAccessToken(); +// this.refreshToken = restSourceAccessToken.getRefreshToken(); +// this.expiresIn = restSourceAccessToken.getExpiresIn(); +// this.expiresAt = Instant.now() +// .plusSeconds(restSourceAccessToken.getExpiresIn()) +// .minus(EXPIRY_TIME_MARGIN); +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt new file mode 100644 index 00000000..02232645 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -0,0 +1,14 @@ +package org.radarbase.authorizer.doa + +import org.radarbase.authorizer.api.Page +import org.radarbase.authorizer.doa.entitiy.RestSourceUser + +interface RestSourceUserRepository { + + fun create(user: RestSourceUser): RestSourceUser + fun read(id: Long): RestSourceUser? + fun update(user: RestSourceUser): RestSourceUser + fun query(page: Page, sourceType: String?, externalUserId: String?): Pair, Page> + fun findAllBySourceType(sourceType: String?): List + fun delete(user: RestSourceUser) +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entitiy/RestSourceUser.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entitiy/RestSourceUser.kt new file mode 100644 index 00000000..31e380a3 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entitiy/RestSourceUser.kt @@ -0,0 +1,68 @@ +package org.radarbase.authorizer.doa.entitiy + +import org.radarbase.upload.doa.AbstractJpaPersistable +import java.time.Duration +import java.time.Instant +import java.util.* +import javax.persistence.* + +@Entity +@Table(name = "rest_source_user") +class RestSourceUser : AbstractJpaPersistable() { + @Transient + private val EXPIRY_TIME_MARGIN = Duration.ofMinutes(5) + + + // The version to be appended to ID for RESET of a user + // This should be updated whenever the user is RESET. + // By default this is null for backwards compatibility + @Column(name = "version") + var version: String? = null + + // The number of times a user has been reset + @Column(name = "time_reset") + var timesReset: Long = 0 + + // Project ID to be used in org.radarcns.kafka.ObservationKey record keys + @Column(name = "project_id") + lateinit var projectId: String + + // User ID to be used in org.radarcns.kafka.ObservationKey record keys + @Column(name = "user_id") + lateinit var userId: String + + // Source ID to be used in org.radarcns.kafka.ObservationKey record keys + @Column(name = "source_id") + var sourceId: String = UUID.randomUUID().toString() + + // Date from when to collect data. + @Column(name = "start_date") + lateinit var startDate: Instant + + @Column(name = "end_date") + var endDate: Instant? = null + + @Column(name = "source_type") + var sourceType: String? = null + + // is authorized by user + var authorized = false + + @Column(name = "external_user_id") + lateinit var externalUserId: String + + @Column(name = "access_token") + var accessToken: String? = null + + @Column(name = "refresh_token") + var refreshToken: String? = null + + @Column(name = "expires_in") + var expiresIn: Int? = null + + @Column(name = "expires_at") + var expiresAt: Instant? = null + + @Column(name = "token_type") + var tokenType: String? = null +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/domain/RestSourceUser.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/domain/RestSourceUser.java deleted file mode 100644 index 9b12b4ac..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/domain/RestSourceUser.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.domain; - -import java.time.Duration; -import java.time.Instant; -import java.util.UUID; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Table; -import javax.persistence.Transient; - -import lombok.Data; -import org.radarbase.authorizer.service.dto.RestSourceAccessToken; -import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; - -@Data -@Entity -@Table(name = "rest_source_user") -public class RestSourceUser { - - @Transient - private static final Duration EXPIRY_TIME_MARGIN = Duration.ofMinutes(5); - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; - - // The version to be appended to ID for RESET of a user - // This should be updated whenever the user is RESET. - // By default this is null for backwards compatibility - private String version = null; - - // The number of times a user has been reset - private long timesReset = 0; - - // Project ID to be used in org.radarcns.kafka.ObservationKey record keys - private String projectId; - - // User ID to be used in org.radarcns.kafka.ObservationKey record keys - private String userId; - - // Source ID to be used in org.radarcns.kafka.ObservationKey record keys - private String sourceId; - - // Date from when to collect data. - private Instant startDate; - - // Date until when to collect data. - private Instant endDate; - - - private String sourceType; - - // is authorized by user - private Boolean authorized = false; - - private String externalUserId; - - private String accessToken; - - private String refreshToken; - - private Integer expiresIn; - - private Instant expiresAt; - - private String tokenType; - - public RestSourceUser() { - if (this.sourceId == null) { - this.sourceId = UUID.randomUUID().toString(); - } - } - - public RestSourceUser(RestSourceUserPropertiesDTO restSourceUserPropertiesDTO) { - if (restSourceUserPropertiesDTO.getId() != null) { - this.id = Long.valueOf(restSourceUserPropertiesDTO.getId()); - } - this.projectId = restSourceUserPropertiesDTO.getProjectId(); - this.userId = restSourceUserPropertiesDTO.getUserId(); - this.sourceId = restSourceUserPropertiesDTO.getSourceId(); - this.sourceType = restSourceUserPropertiesDTO.getSourceType(); - this.startDate = restSourceUserPropertiesDTO.getStartDate(); - this.endDate = restSourceUserPropertiesDTO.getEndDate(); - this.externalUserId = restSourceUserPropertiesDTO.getExternalUserId(); - this.authorized = restSourceUserPropertiesDTO.isAuthorized(); - } - - public long getId() { - return id; - } - - public RestSourceUser id(Long id) { - this.id = id; - return this; - } - - public String getVersion() { - return version; - } - - public RestSourceUser version(String version) { - this.version = version; - return this; - } - - public long getTimesReset() { - return timesReset; - } - - public RestSourceUser setTimesReset(long timesReset) { - this.timesReset = timesReset; - return this; - } - - public String getProjectId() { - return projectId; - } - - public RestSourceUser projectId(String projectId) { - this.projectId = projectId; - return this; - } - - public String getUserId() { - return userId; - } - - public RestSourceUser userId(String userId) { - this.userId = userId; - return this; - } - - public String getSourceId() { - return sourceId; - } - - public RestSourceUser sourceId(String sourceId) { - this.sourceId = sourceId; - return this; - } - - public Instant getStartDate() { - return startDate; - } - - public RestSourceUser startDate(Instant startDate) { - this.startDate = startDate; - return this; - } - - public Instant getEndDate() { - return endDate; - } - - public RestSourceUser endDate(Instant endDate) { - this.endDate = endDate; - return this; - } - - public String getSourceType() { - return sourceType; - } - - public RestSourceUser sourceType(String sourceType) { - this.sourceType = sourceType; - return this; - } - - public Boolean getAuthorized() { - return authorized; - } - - public RestSourceUser authorized(Boolean authorized) { - this.authorized = authorized; - return this; - } - - public String getExternalUserId() { - return externalUserId; - } - - public RestSourceUser externalUserId(String externalUserId) { - this.externalUserId = externalUserId; - return this; - } - - public String getAccessToken() { - return accessToken; - } - - public RestSourceUser accessToken(String accessToken) { - this.accessToken = accessToken; - return this; - } - - public String getRefreshToken() { - return refreshToken; - } - - public RestSourceUser refreshToken(String refreshToken) { - this.refreshToken = refreshToken; - return this; - } - - public Integer getExpiresIn() { - return expiresIn; - } - - public RestSourceUser expiresIn(Integer expiresIn) { - this.expiresIn = expiresIn; - return this; - } - - public Instant getExpiresAt() { - return expiresAt; - } - - public RestSourceUser expiresIn(Instant expiresAt) { - this.expiresAt = expiresAt; - return this; - } - - public String getTokenType() { - return tokenType; - } - - public RestSourceUser tokenType(String tokenType) { - this.tokenType = tokenType; - return this; - } - - /** - * Updates only a subset of user properties during update. - * Does not update properties such as id and token data. - * - * @param sourceUserDto user details to update. - */ - public void safeUpdateProperties(RestSourceUserPropertiesDTO sourceUserDto) { - this.projectId = sourceUserDto.getProjectId(); - this.userId = sourceUserDto.getUserId(); - this.sourceId = sourceUserDto.getSourceId(); - this.startDate = sourceUserDto.getStartDate(); - this.endDate = sourceUserDto.getEndDate(); - } - - - /** - * Updates only the token related properties during update. - * Does not update properties such as id and study information. - * - * @param restSourceAccessToken token details to update. - */ - public void safeUpdateTokenDetails(RestSourceAccessToken restSourceAccessToken) { - this.accessToken = restSourceAccessToken.getAccessToken(); - this.refreshToken = restSourceAccessToken.getRefreshToken(); - this.expiresIn = restSourceAccessToken.getExpiresIn(); - this.expiresAt = Instant.now() - .plusSeconds(restSourceAccessToken.getExpiresIn()) - .minus(EXPIRY_TIME_MARGIN); - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt new file mode 100644 index 00000000..58dd2f22 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt @@ -0,0 +1,63 @@ +/* + * + * * Copyright 2019 The Hyve + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * * + * + */ + +package org.radarbase.authorizer.inject + +import org.glassfish.jersey.internal.inject.AbstractBinder +import org.radarbase.authorizer.Config +import org.radarbase.authorizer.service.RadarProjectService +import org.radarbase.authorizer.service.managementportal.MPClient +import org.radarbase.authorizer.service.managementportal.MPProjectService +import org.radarbase.jersey.auth.AuthConfig +import org.radarbase.jersey.auth.ProjectService +import org.radarbase.jersey.config.ConfigLoader +import org.radarbase.jersey.config.EnhancerFactory +import org.radarbase.jersey.config.JerseyResourceEnhancer +import javax.inject.Singleton + +/** This binder needs to register all non-Jersey classes, otherwise initialization fails. */ +class ManagementPortalEnhancerFactory(private val config: Config) : EnhancerFactory { + override fun createEnhancers(): List = listOf( + UploadResourceEnhancer(config), + MPClientResourceEnhancer(), + ConfigLoader.Enhancers.radar(AuthConfig( + managementPortalUrl = config.auth.managementPortalUrl, + jwtResourceName = config.auth.jwtResourceName)), + ConfigLoader.Enhancers.managementPortal, + ConfigLoader.Enhancers.generalException, + ConfigLoader.Enhancers.httpException) + + class MPClientResourceEnhancer: JerseyResourceEnhancer { + override fun enhanceBinder(binder: AbstractBinder) { + binder.apply { + bind(MPClient::class.java) + .to(MPClient::class.java) + .`in`(Singleton::class.java) + + bind(ProjectServiceWrapper::class.java) + .to(ProjectService::class.java) + .`in`(Singleton::class.java) + + bind(MPProjectService::class.java) + .to(RadarProjectService::class.java) + .`in`(Singleton::class.java) + } + } + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt new file mode 100644 index 00000000..5d9c01f8 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt @@ -0,0 +1,12 @@ +package org.radarbase.authorizer.inject + +import org.radarbase.authorizer.service.RadarProjectService +import org.radarbase.jersey.auth.ProjectService +import javax.inject.Provider +import javax.ws.rs.core.Context + +class ProjectServiceWrapper( + @Context private val mpProjectService: Provider +): ProjectService { + override fun ensureProject(projectId: String) = mpProjectService.get().ensureProject(projectId) +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/UploadResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/UploadResourceEnhancer.kt new file mode 100644 index 00000000..0af38ec4 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/UploadResourceEnhancer.kt @@ -0,0 +1,93 @@ +package org.radarbase.authorizer.inject + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.KotlinModule +import okhttp3.OkHttpClient +import org.glassfish.jersey.internal.inject.AbstractBinder +import org.glassfish.jersey.server.ResourceConfig +import org.radarbase.authorizer.Config +import org.radarbase.jersey.config.ConfigLoader +import org.radarbase.jersey.config.JerseyResourceEnhancer +import java.util.concurrent.TimeUnit +import javax.ws.rs.ext.ContextResolver + +class UploadResourceEnhancer(private val config: Config): JerseyResourceEnhancer { + private val client = OkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + override val classes: Array> get() { + return if (config.service.enableCors == true) { + arrayOf( + ConfigLoader.Filters.logResponse, + ConfigLoader.Filters.cors) + } else { + arrayOf( + ConfigLoader.Filters.logResponse) + } + } + + override val packages: Array = arrayOf( + "org.radarbase.authorizer.exception", + "org.radarbase.authorizer.filter", + "org.radarbase.authorizer.lifecycle", + "org.radarbase.authorizer.resource") + + override fun ResourceConfig.enhance() { + register(ContextResolver { OBJECT_MAPPER }) + } + + override fun AbstractBinder.enhance() { + // Bind instances. These cannot use any injects themselves + bind(config) + .to(Config::class.java) + + bind(client) + .to(OkHttpClient::class.java) + + bind(OBJECT_MAPPER) + .to(ObjectMapper::class.java) + +// bind(QueuedCallbackManager::class.java) +// .to(CallbackManager::class.java) +// .`in`(Singleton::class.java) +// +// // Bind factories. +// bindFactory(DoaEntityManagerFactoryFactory::class.java) +// .to(EntityManagerFactory::class.java) +// .`in`(Singleton::class.java) +// +// bindFactory(DoaEntityManagerFactory::class.java) +// .to(EntityManager::class.java) +// .`in`(PerLookup::class.java) + +// bind(RecordMapperImpl::class.java) +// .to(RecordMapper::class.java) +// .`in`(Singleton::class.java) +// +// bind(SourceTypeMapperImpl::class.java) +// .to(SourceTypeMapper::class.java) +// .`in`(Singleton::class.java) +// +// bind(RecordRepositoryImpl::class.java) +// .to(RecordRepository::class.java) +// .`in`(Singleton::class.java) +// +// bind(SourceTypeRepositoryImpl::class.java) +// .to(SourceTypeRepository::class.java) +// .`in`(Singleton::class.java) + } + + companion object { + private val OBJECT_MAPPER: ObjectMapper = ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(JavaTimeModule()) + .registerModule(KotlinModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/repository/RestSourceUserRepository.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/repository/RestSourceUserRepository.java deleted file mode 100644 index 13f8cbcb..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/repository/RestSourceUserRepository.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.repository; - -import java.util.List; -import java.util.Optional; - -import org.radarbase.authorizer.domain.RestSourceUser; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface RestSourceUserRepository extends JpaRepository { - Optional findBySourceTypeAndExternalUserId(String sourceType, - String externalUserId); - - List findAllBySourceType(String sourceType); -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java index 0fa7f9d5..866080aa 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java @@ -1,43 +1,43 @@ -package org.radarbase.authorizer.security; - -import org.radarbase.authorizer.config.RestSourceAuthorizerProperties; -import org.radarcns.auth.authentication.TokenValidator; -import org.radarcns.auth.config.TokenVerifierPublicKeyConfig; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; - -@Configuration -public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { - - @Autowired - private RestSourceAuthorizerProperties config; - - @Override - public void configure(WebSecurity web) { - // Overridden to exclude some url's - web.ignoring().antMatchers("/health"); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - - http.csrf().disable() - .addFilterAfter(new JwtAuthenticationFilter(getTokenValidator()), - BasicAuthenticationFilter.class); - } - - @Bean - public TokenValidator getTokenValidator() { - TokenVerifierPublicKeyConfig authVerifierConfig = new TokenVerifierPublicKeyConfig(); - authVerifierConfig.setPublicKeys(config.getAuth().getPublicKeys()); - authVerifierConfig.setPublicKeyEndpoints(config.getAuth().getPublicKeyEndpoints()); - authVerifierConfig.setResourceName(config.getAuth().getResourceName()); - return new TokenValidator(authVerifierConfig); - } - -} \ No newline at end of file +//package org.radarbase.authorizer.security; +// +//import org.radarbase.authorizer.config.RestSourceAuthorizerProperties; +//import org.radarcns.auth.authentication.TokenValidator; +//import org.radarcns.auth.config.TokenVerifierPublicKeyConfig; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.security.config.annotation.web.builders.HttpSecurity; +//import org.springframework.security.config.annotation.web.builders.WebSecurity; +//import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +//import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +// +//@Configuration +//public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { +// +// @Autowired +// private RestSourceAuthorizerProperties config; +// +// @Override +// public void configure(WebSecurity web) { +// // Overridden to exclude some url's +// web.ignoring().antMatchers("/health"); +// } +// +// @Override +// protected void configure(HttpSecurity http) throws Exception { +// +// http.csrf().disable() +// .addFilterAfter(new JwtAuthenticationFilter(getTokenValidator()), +// BasicAuthenticationFilter.class); +// } +// +// @Bean +// public TokenValidator getTokenValidator() { +// TokenVerifierPublicKeyConfig authVerifierConfig = new TokenVerifierPublicKeyConfig(); +// authVerifierConfig.setPublicKeys(config.getAuth().getPublicKeys()); +// authVerifierConfig.setPublicKeyEndpoints(config.getAuth().getPublicKeyEndpoints()); +// authVerifierConfig.setResourceName(config.getAuth().getResourceName()); +// return new TokenValidator(authVerifierConfig); +// } +// +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java index c2adae03..2d1fab48 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java @@ -1,86 +1,86 @@ -package org.radarbase.authorizer.security; - -import java.io.IOException; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.radarcns.auth.authentication.TokenValidator; -import org.radarcns.auth.exception.TokenValidationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.web.cors.CorsUtils; -import org.springframework.web.filter.GenericFilterBean; - -/** - * Created by dverbeec on 29/09/2017. - */ -public class JwtAuthenticationFilter extends GenericFilterBean { - - private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class); - private final TokenValidator validator; - public static final String TOKEN_ATTRIBUTE = "jwt"; - public static String BEARER_TYPE = "Bearer"; - - public JwtAuthenticationFilter(TokenValidator validator) { - this.validator = validator; - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws - IOException, ServletException { - if (CorsUtils.isPreFlightRequest((HttpServletRequest) request)) { - log.debug("Skipping JWT check for preflight request"); - chain.doFilter(request, response); - return; - } - - if (((HttpServletRequest) request).getRequestURI().contains("management/health")) { - log.debug("Skipping JWT check for Health check request"); - chain.doFilter(request, response); - return; - } - - try { - request.setAttribute(TOKEN_ATTRIBUTE, - validator.validateAccessToken(getToken(request, response))); - log.debug("Request authenticated successfully"); - chain.doFilter(request, response); - } catch (TokenValidationException ex) { - log.error(ex.getMessage()); - HttpServletResponse res = (HttpServletResponse) response; - res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - res.setHeader(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE); - res.getOutputStream().println( - "{\"error\": \"" + "Unauthorized" + ",\n" - + "\"status\": \"" + HttpServletResponse.SC_UNAUTHORIZED + ",\n" - + "\"message\": \"" + ex.getMessage() + ",\n" - + "\"path\": \"" + ((HttpServletRequest) request).getRequestURI() + "\n" - + "\"}"); - } - } - - private String getToken(ServletRequest request, ServletResponse response) { - HttpServletRequest req = (HttpServletRequest) request; - HttpServletResponse res = (HttpServletResponse) response; - String authorizationHeader = req.getHeader(HttpHeaders.AUTHORIZATION); - - // Check if the HTTP Authorization header is present and formatted correctly - if (authorizationHeader == null || !authorizationHeader - .startsWith(BEARER_TYPE)) { - log.error("No authorization header provided in the request"); - res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - res.setHeader(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE); - throw new TokenValidationException("No " + BEARER_TYPE + " token " - + "present in the request."); - } - - // Extract the token from the HTTP Authorization header - return authorizationHeader.substring( - BEARER_TYPE.length()).trim(); - } -} +//package org.radarbase.authorizer.security; +// +//import java.io.IOException; +//import javax.servlet.FilterChain; +//import javax.servlet.ServletException; +//import javax.servlet.ServletRequest; +//import javax.servlet.ServletResponse; +//import javax.servlet.http.HttpServletRequest; +//import javax.servlet.http.HttpServletResponse; +// +//import org.radarcns.auth.authentication.TokenValidator; +//import org.radarcns.auth.exception.TokenValidationException; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.http.HttpHeaders; +//import org.springframework.web.cors.CorsUtils; +//import org.springframework.web.filter.GenericFilterBean; +// +///** +// * Created by dverbeec on 29/09/2017. +// */ +//public class JwtAuthenticationFilter extends GenericFilterBean { +// +// private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class); +// private final TokenValidator validator; +// public static final String TOKEN_ATTRIBUTE = "jwt"; +// public static String BEARER_TYPE = "Bearer"; +// +// public JwtAuthenticationFilter(TokenValidator validator) { +// this.validator = validator; +// } +// +// @Override +// public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws +// IOException, ServletException { +// if (CorsUtils.isPreFlightRequest((HttpServletRequest) request)) { +// log.debug("Skipping JWT check for preflight request"); +// chain.doFilter(request, response); +// return; +// } +// +// if (((HttpServletRequest) request).getRequestURI().contains("management/health")) { +// log.debug("Skipping JWT check for Health check request"); +// chain.doFilter(request, response); +// return; +// } +// +// try { +// request.setAttribute(TOKEN_ATTRIBUTE, +// validator.validateAccessToken(getToken(request, response))); +// log.debug("Request authenticated successfully"); +// chain.doFilter(request, response); +// } catch (TokenValidationException ex) { +// log.error(ex.getMessage()); +// HttpServletResponse res = (HttpServletResponse) response; +// res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); +// res.setHeader(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE); +// res.getOutputStream().println( +// "{\"error\": \"" + "Unauthorized" + ",\n" +// + "\"status\": \"" + HttpServletResponse.SC_UNAUTHORIZED + ",\n" +// + "\"message\": \"" + ex.getMessage() + ",\n" +// + "\"path\": \"" + ((HttpServletRequest) request).getRequestURI() + "\n" +// + "\"}"); +// } +// } +// +// private String getToken(ServletRequest request, ServletResponse response) { +// HttpServletRequest req = (HttpServletRequest) request; +// HttpServletResponse res = (HttpServletResponse) response; +// String authorizationHeader = req.getHeader(HttpHeaders.AUTHORIZATION); +// +// // Check if the HTTP Authorization header is present and formatted correctly +// if (authorizationHeader == null || !authorizationHeader +// .startsWith(BEARER_TYPE)) { +// log.error("No authorization header provided in the request"); +// res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); +// res.setHeader(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE); +// throw new TokenValidationException("No " + BEARER_TYPE + " token " +// + "present in the request."); +// } +// +// // Extract the token from the HTTP Authorization header +// return authorizationHeader.substring( +// BEARER_TYPE.length()).trim(); +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.java index 10702441..b169ae2c 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.java @@ -1,215 +1,215 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.service; - -import com.fasterxml.jackson.core.type.TypeReference; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import javax.annotation.PostConstruct; -import javax.naming.ConfigurationException; -import javax.validation.constraints.NotNull; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import okhttp3.Credentials; -import okhttp3.FormBody; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.radarbase.authorizer.config.ConfigHelper; -import org.radarbase.authorizer.config.RestSourceClientConfig; -import org.radarbase.authorizer.config.RestSourceAuthorizerProperties; -import org.radarbase.authorizer.service.dto.RestSourceAccessToken; -import org.radarbase.authorizer.service.dto.RestSourceClientDetailsDTO; -import org.radarbase.authorizer.service.dto.RestSourceClients; -import org.radarbase.authorizer.webapp.exception.BadGatewayException; -import org.radarbase.authorizer.webapp.exception.InvalidSourceTypeException; -import org.radarbase.authorizer.webapp.exception.TokenException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -public class RestSourceClientService { - - private static final Logger LOGGER = LoggerFactory.getLogger(RestSourceClientService.class); - - @Autowired - private RestSourceAuthorizerProperties restSourceAuthorizerProperties; - - private OkHttpClient client; - - private ObjectMapper mapper; - - // contains private data - private Map configMap; - - // contains sharable public data - private Map clientDetailsDTOMap; - - @PostConstruct - public void init() throws ConfigurationException { - String path = restSourceAuthorizerProperties.getSourceClientsFilePath(); - if (Objects.isNull(path) || path.equals("")) { - LOGGER.info("No source clients file specified, not loading source clients"); - return; - } - List restSourceClientConfigs = loadDeviceClientConfigs(path); - - this.configMap = restSourceClientConfigs.stream() - .collect(Collectors.toMap(RestSourceClientConfig::getSourceType, p -> p)); - this.clientDetailsDTOMap = restSourceClientConfigs.stream() - .collect(Collectors.toMap(RestSourceClientConfig::getSourceType, - RestSourceClientDetailsDTO::new)); - - LOGGER.info("Source client configs loaded..."); - this.client = new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .build(); - this.mapper = new ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - - } - - private List loadDeviceClientConfigs(@NotNull String path) throws - ConfigurationException { - org.radarbase.authorizer.config.RestSourceClients restSourceClients = - ConfigHelper.loadPropertiesFromFile(path, - new TypeReference() {}); - - if (restSourceClients.getRestSourceClients() == null) { - LOGGER.error("No valid configurations available on configured path. Please " - + "check the syntax and file name"); - throw new ConfigurationException( - "No valid source-client configs are provided" + "."); - } - return restSourceClients.getRestSourceClients(); - } - - - public RestSourceClients getAllRestSourceClientDetails() { - return new RestSourceClients() - .sourceClients(new ArrayList<>(this.clientDetailsDTOMap.values())); - } - - public List getAvailableDeviceTypes() { - return new ArrayList<>(this.clientDetailsDTOMap.keySet()); - } - - private RestSourceClientConfig getClientAuthorizationConfig(String sourceType) { - if (this.configMap.containsKey(sourceType)) { - return this.configMap.get(sourceType); - } else { - throw new InvalidSourceTypeException(sourceType); - } - } - - public RestSourceClientDetailsDTO getAllRestSourceClientDetails(String sourceType) { - return this.clientDetailsDTOMap.get(sourceType); - } - - - RestSourceAccessToken getAccessTokenWithAuthorizeCode(String code, String sourceType) { - - RestSourceClientConfig authorizationConfig = getClientAuthorizationConfig(sourceType); - FormBody form = new FormBody.Builder() - .add("code", code) - .add("grant_type", "authorization_code") - .add("client_id", authorizationConfig.getClientId()) - .build(); - LOGGER.info("Requesting access token with authorization code"); - return processTokenRequest(form, authorizationConfig); - - } - - private RestSourceAccessToken processTokenRequest(FormBody form, - RestSourceClientConfig authorizationConfig) { - String credentials = Credentials - .basic(authorizationConfig.getClientId(), authorizationConfig.getClientSecret()); - - Request request = new Request.Builder().addHeader("Accept", "application/json") - .addHeader("Authorization", credentials) - .addHeader("Content-Type", "application/x-www-form-urlencoded") - .url(authorizationConfig.getTokenEndpoint()) - .post(form) - .build(); - - try (Response response = client.newCall(request).execute()) { - if (response.isSuccessful()) { - ResponseBody responseBody = response.body(); - if (responseBody == null) { - throw new BadGatewayException("No response from server"); - } - - try { - return mapper.readValue(responseBody.string(), RestSourceAccessToken.class); - } catch (IOException e) { - throw new TokenException("Cannot read token response"); - } - } else { - throw new BadGatewayException( - "Failed to execute the request : Response-code :" + response.code() - + " received when requesting token from server with " + "message " - + response.message()); - } - } catch (IOException e) { - throw new BadGatewayException(e); - } - } - - RestSourceAccessToken refreshToken(String refreshToken, String sourceType) { - FormBody form = new FormBody.Builder() - .add("grant_type", "refresh_token") - .add("refresh_token", refreshToken) - .build(); - LOGGER.info("Requesting to refreshToken"); - return processTokenRequest(form, getClientAuthorizationConfig(sourceType)); - } - - boolean revokeToken(String accessToken, String sourceType) { - - RestSourceClientConfig authorizationConfig = getClientAuthorizationConfig(sourceType); - FormBody form = new FormBody.Builder().add("token", accessToken).build(); - LOGGER.info("Requesting to revoke access token"); - String credentials = Credentials - .basic(authorizationConfig.getClientId(), authorizationConfig.getClientSecret()); - - Request request = new Request.Builder().addHeader("Accept", "application/json") - .addHeader("Authorization", credentials) - .addHeader("Content-Type", "application/x-www-form-urlencoded") - .url(authorizationConfig.getTokenEndpoint()).post(form).build(); - - try (Response response = client.newCall(request).execute()) { - return response.isSuccessful(); - } catch (IOException e) { - throw new BadGatewayException(e); - } - - } -} +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer.service; +// +//import com.fasterxml.jackson.core.type.TypeReference; +//import java.io.IOException; +//import java.util.ArrayList; +//import java.util.List; +//import java.util.Map; +//import java.util.Objects; +//import java.util.concurrent.TimeUnit; +//import java.util.stream.Collectors; +//import javax.annotation.PostConstruct; +//import javax.naming.ConfigurationException; +//import javax.validation.constraints.NotNull; +// +//import com.fasterxml.jackson.databind.DeserializationFeature; +//import com.fasterxml.jackson.databind.ObjectMapper; +//import okhttp3.Credentials; +//import okhttp3.FormBody; +//import okhttp3.OkHttpClient; +//import okhttp3.Request; +//import okhttp3.Response; +//import okhttp3.ResponseBody; +//import org.radarbase.authorizer.config.ConfigHelper; +//import org.radarbase.authorizer.config.RestSourceClientConfig; +//import org.radarbase.authorizer.config.RestSourceAuthorizerProperties; +//import org.radarbase.authorizer.service.dto.RestSourceAccessToken; +//import org.radarbase.authorizer.service.dto.RestSourceClientDetailsDTO; +//import org.radarbase.authorizer.service.dto.RestSourceClients; +//import org.radarbase.authorizer.webapp.exception.BadGatewayException; +//import org.radarbase.authorizer.webapp.exception.InvalidSourceTypeException; +//import org.radarbase.authorizer.webapp.exception.TokenException; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.stereotype.Service; +// +//@Service +//public class RestSourceClientService { +// +// private static final Logger LOGGER = LoggerFactory.getLogger(RestSourceClientService.class); +// +// @Autowired +// private RestSourceAuthorizerProperties restSourceAuthorizerProperties; +// +// private OkHttpClient client; +// +// private ObjectMapper mapper; +// +// // contains private data +// private Map configMap; +// +// // contains sharable public data +// private Map clientDetailsDTOMap; +// +// @PostConstruct +// public void init() throws ConfigurationException { +// String path = restSourceAuthorizerProperties.getSourceClientsFilePath(); +// if (Objects.isNull(path) || path.equals("")) { +// LOGGER.info("No source clients file specified, not loading source clients"); +// return; +// } +// List restSourceClientConfigs = loadDeviceClientConfigs(path); +// +// this.configMap = restSourceClientConfigs.stream() +// .collect(Collectors.toMap(RestSourceClientConfig::getSourceType, p -> p)); +// this.clientDetailsDTOMap = restSourceClientConfigs.stream() +// .collect(Collectors.toMap(RestSourceClientConfig::getSourceType, +// RestSourceClientDetailsDTO::new)); +// +// LOGGER.info("Source client configs loaded..."); +// this.client = new OkHttpClient.Builder() +// .connectTimeout(10, TimeUnit.SECONDS) +// .writeTimeout(10, TimeUnit.SECONDS) +// .readTimeout(30, TimeUnit.SECONDS) +// .build(); +// this.mapper = new ObjectMapper() +// .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); +// +// } +// +// private List loadDeviceClientConfigs(@NotNull String path) throws +// ConfigurationException { +// org.radarbase.authorizer.config.RestSourceClients restSourceClients = +// ConfigHelper.loadPropertiesFromFile(path, +// new TypeReference() {}); +// +// if (restSourceClients.getRestSourceClients() == null) { +// LOGGER.error("No valid configurations available on configured path. Please " +// + "check the syntax and file name"); +// throw new ConfigurationException( +// "No valid source-client configs are provided" + "."); +// } +// return restSourceClients.getRestSourceClients(); +// } +// +// +// public RestSourceClients getAllRestSourceClientDetails() { +// return new RestSourceClients() +// .sourceClients(new ArrayList<>(this.clientDetailsDTOMap.values())); +// } +// +// public List getAvailableDeviceTypes() { +// return new ArrayList<>(this.clientDetailsDTOMap.keySet()); +// } +// +// private RestSourceClientConfig getClientAuthorizationConfig(String sourceType) { +// if (this.configMap.containsKey(sourceType)) { +// return this.configMap.get(sourceType); +// } else { +// throw new InvalidSourceTypeException(sourceType); +// } +// } +// +// public RestSourceClientDetailsDTO getAllRestSourceClientDetails(String sourceType) { +// return this.clientDetailsDTOMap.get(sourceType); +// } +// +// +// RestSourceAccessToken getAccessTokenWithAuthorizeCode(String code, String sourceType) { +// +// RestSourceClientConfig authorizationConfig = getClientAuthorizationConfig(sourceType); +// FormBody form = new FormBody.Builder() +// .add("code", code) +// .add("grant_type", "authorization_code") +// .add("client_id", authorizationConfig.getClientId()) +// .build(); +// LOGGER.info("Requesting access token with authorization code"); +// return processTokenRequest(form, authorizationConfig); +// +// } +// +// private RestSourceAccessToken processTokenRequest(FormBody form, +// RestSourceClientConfig authorizationConfig) { +// String credentials = Credentials +// .basic(authorizationConfig.getClientId(), authorizationConfig.getClientSecret()); +// +// Request request = new Request.Builder().addHeader("Accept", "application/json") +// .addHeader("Authorization", credentials) +// .addHeader("Content-Type", "application/x-www-form-urlencoded") +// .url(authorizationConfig.getTokenEndpoint()) +// .post(form) +// .build(); +// +// try (Response response = client.newCall(request).execute()) { +// if (response.isSuccessful()) { +// ResponseBody responseBody = response.body(); +// if (responseBody == null) { +// throw new BadGatewayException("No response from server"); +// } +// +// try { +// return mapper.readValue(responseBody.string(), RestSourceAccessToken.class); +// } catch (IOException e) { +// throw new TokenException("Cannot read token response"); +// } +// } else { +// throw new BadGatewayException( +// "Failed to execute the request : Response-code :" + response.code() +// + " received when requesting token from server with " + "message " +// + response.message()); +// } +// } catch (IOException e) { +// throw new BadGatewayException(e); +// } +// } +// +// RestSourceAccessToken refreshToken(String refreshToken, String sourceType) { +// FormBody form = new FormBody.Builder() +// .add("grant_type", "refresh_token") +// .add("refresh_token", refreshToken) +// .build(); +// LOGGER.info("Requesting to refreshToken"); +// return processTokenRequest(form, getClientAuthorizationConfig(sourceType)); +// } +// +// boolean revokeToken(String accessToken, String sourceType) { +// +// RestSourceClientConfig authorizationConfig = getClientAuthorizationConfig(sourceType); +// FormBody form = new FormBody.Builder().add("token", accessToken).build(); +// LOGGER.info("Requesting to revoke access token"); +// String credentials = Credentials +// .basic(authorizationConfig.getClientId(), authorizationConfig.getClientSecret()); +// +// Request request = new Request.Builder().addHeader("Accept", "application/json") +// .addHeader("Authorization", credentials) +// .addHeader("Content-Type", "application/x-www-form-urlencoded") +// .url(authorizationConfig.getTokenEndpoint()).post(form).build(); +// +// try (Response response = client.newCall(request).execute()) { +// return response.isSuccessful(); +// } catch (IOException e) { +// throw new BadGatewayException(e); +// } +// +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.kt new file mode 100644 index 00000000..8448f999 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.kt @@ -0,0 +1,5 @@ +package org.radarbase.authorizer.service + +class RestSourceClientService { + +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.java index 70cfd98a..e3fe758a 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.java @@ -1,220 +1,220 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.service; - -import java.time.Instant; -import java.util.Optional; -import java.util.stream.Collectors; -import javax.validation.constraints.NotNull; -import org.radarbase.authorizer.domain.RestSourceUser; -import org.radarbase.authorizer.repository.RestSourceUserRepository; -import org.radarbase.authorizer.service.dto.RestSourceAccessToken; -import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; -import org.radarbase.authorizer.service.dto.RestSourceUsers; -import org.radarbase.authorizer.service.dto.TokenDTO; -import org.radarbase.authorizer.webapp.exception.NotFoundException; -import org.radarbase.authorizer.webapp.exception.TokenException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional -public class RestSourceUserService { - - private final Logger log = LoggerFactory.getLogger(RestSourceUserService.class); - - @Autowired - private RestSourceUserRepository restSourceUserRepository; - - @Autowired - private RestSourceClientService authorizationService; - - @Transactional(readOnly = true) - public RestSourceUsers getAllRestSourceUsers() { - log.debug("Querying all saved source users"); - return new RestSourceUsers() - .users(this.restSourceUserRepository.findAll() - .stream() - .map(RestSourceUserPropertiesDTO::new) - .collect(Collectors.toList())); - } - - public RestSourceUserPropertiesDTO save(RestSourceUserPropertiesDTO restSourceUserPropertiesDTO) { - RestSourceUser restSourceUser = - this.restSourceUserRepository.save(new RestSourceUser(restSourceUserPropertiesDTO)); - return new RestSourceUserPropertiesDTO(restSourceUser); - } - - @Transactional - public RestSourceUserPropertiesDTO authorizeAndStoreDevice(@NotNull String code, - @NotNull String sourceType) { - RestSourceAccessToken accessToken = - authorizationService.getAccessTokenWithAuthorizeCode(code, sourceType); - - if (accessToken != null) { - - Optional existingUser = restSourceUserRepository - .findBySourceTypeAndExternalUserId(sourceType, accessToken.getExternalUserId()); - - RestSourceUser resultUser; - if (existingUser.isPresent()) { - resultUser = existingUser.get(); - resultUser.safeUpdateTokenDetails(accessToken); - } else { - resultUser = new RestSourceUser() - .authorized(true) - .externalUserId(accessToken.getExternalUserId()) - .sourceType(sourceType) - .startDate(Instant.now()); - resultUser.safeUpdateTokenDetails(accessToken); - - resultUser = this.restSourceUserRepository.save(resultUser); - } - return new RestSourceUserPropertiesDTO(resultUser); - } else { - log.error("Cannot get token a using authorization_code"); - throw new TokenException(); - } - } - - @Transactional(readOnly = true) - public RestSourceUserPropertiesDTO getRestSourceUserById(Long id) { - Optional user = restSourceUserRepository.findById(id); - - if (user.isPresent()) { - return new RestSourceUserPropertiesDTO(user.get()); - } else { - throw new NotFoundException("RestSourceUser not found with id " + id); - } - } - - @Transactional - public RestSourceUserPropertiesDTO updateRestSourceUser(Long id, - RestSourceUserPropertiesDTO sourceUserPropertiesDTO) { - - Optional sourceUser = restSourceUserRepository.findById(id); - - if (sourceUser.isPresent()) { - RestSourceUser restSourceUserToSave = sourceUser.get(); - restSourceUserToSave.safeUpdateProperties(sourceUserPropertiesDTO); - return new RestSourceUserPropertiesDTO(restSourceUserRepository.save(restSourceUserToSave)); - } else { - throw new NotFoundException( - "Unable to update rest source user. RestSourceUser not found with " + "id " - + sourceUserPropertiesDTO.getId()); - } - - } - - /** - * Removes user from database and revokes access token and refresh token - * - * @param id userID - */ - @Transactional - public void revokeTokenAndDeleteUser(Long id) { - Optional user = restSourceUserRepository.findById(id); - - if (user.isPresent()) { - RestSourceUser restSourceUser = user.get(); - authorizationService - .revokeToken(restSourceUser.getAccessToken(), restSourceUser.getSourceType()); - restSourceUserRepository.deleteById(id); - - } else { - throw new NotFoundException("RestSourceUser not found with id " + id); - } - } - - @Transactional(readOnly = true) - public TokenDTO getDeviceTokenByUserId(Long id) { - Optional user = restSourceUserRepository.findById(id); - - if (user.isPresent()) { - RestSourceUser restSourceUser = user.get(); - return new TokenDTO() - .accessToken(restSourceUser.getAccessToken()) - .expiresAt(restSourceUser.getExpiresAt()); - } else { - throw new NotFoundException("RestSourceUser not found with id " + id); - } - } - - @Transactional - public TokenDTO refreshTokenForUser(Long id) { - Optional user = restSourceUserRepository.findById(id); - if (user.isPresent()) { - RestSourceUser restSourceUser = user.get(); - // refresh token by user id and source-type - RestSourceAccessToken accessToken = authorizationService - .refreshToken(restSourceUser.getRefreshToken(), restSourceUser.getSourceType()); - // update token - if (accessToken != null) { - restSourceUser.safeUpdateTokenDetails(accessToken); - restSourceUser = this.restSourceUserRepository.save(restSourceUser); - return new TokenDTO() - .accessToken(restSourceUser.getAccessToken()) - .expiresAt(restSourceUser.getExpiresAt()); - } else { - throw new TokenException("Could not refresh token successfully"); - } - - } else { - throw new NotFoundException("RestSourceUser not found with id " + id); - } - } - - @Transactional(readOnly = true) - public RestSourceUsers getAllUsersBySourceType(String sourceType) { - log.debug("Querying all saved users by source-type {}", sourceType); - return new RestSourceUsers() - .users(this.restSourceUserRepository.findAllBySourceType(sourceType) - .stream() - .map(RestSourceUserPropertiesDTO::new) - .collect(Collectors.toList())); - } - - /** - * This resets the user by updating the current version. Currently, the version is calculated as a - * String based on the current time instant. This may change in the future. - * - * @param id the database ID of the User - * @return The updated User details - */ - public RestSourceUserPropertiesDTO resetUser(Long id) { - Optional sourceUser = restSourceUserRepository.findById(id); - - if (sourceUser.isPresent()) { - RestSourceUser restSourceUserToSave = sourceUser.get(); - return new RestSourceUserPropertiesDTO( - restSourceUserRepository.save( - restSourceUserToSave - .version(Instant.now().toString()) - .setTimesReset(restSourceUserToSave.getTimesReset() + 1))); - } else { - throw new NotFoundException( - "Unable to reset rest source user. RestSourceUser not found with " + "id " - + id); - } - } -} +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer.service; +// +//import java.time.Instant; +//import java.util.Optional; +//import java.util.stream.Collectors; +//import javax.validation.constraints.NotNull; +//import org.radarbase.authorizer.doa.RestSourceUser; +//import org.radarbase.authorizer.doa.RestSourceUserRepository; +//import org.radarbase.authorizer.service.dto.RestSourceAccessToken; +//import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; +//import org.radarbase.authorizer.service.dto.RestSourceUsers; +//import org.radarbase.authorizer.service.dto.TokenDTO; +//import org.radarbase.authorizer.webapp.exception.NotFoundException; +//import org.radarbase.authorizer.webapp.exception.TokenException; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.stereotype.Service; +//import org.springframework.transaction.annotation.Transactional; +// +//@Service +//@Transactional +//public class RestSourceUserService { +// +// private final Logger log = LoggerFactory.getLogger(RestSourceUserService.class); +// +// @Autowired +// private RestSourceUserRepository restSourceUserRepository; +// +// @Autowired +// private RestSourceClientService authorizationService; +// +// @Transactional(readOnly = true) +// public RestSourceUsers getAllRestSourceUsers() { +// log.debug("Querying all saved source users"); +// return new RestSourceUsers() +// .users(this.restSourceUserRepository.findAll() +// .stream() +// .map(RestSourceUserPropertiesDTO::new) +// .collect(Collectors.toList())); +// } +// +// public RestSourceUserPropertiesDTO save(RestSourceUserPropertiesDTO restSourceUserPropertiesDTO) { +// RestSourceUser restSourceUser = +// this.restSourceUserRepository.save(new RestSourceUser(restSourceUserPropertiesDTO)); +// return new RestSourceUserPropertiesDTO(restSourceUser); +// } +// +// @Transactional +// public RestSourceUserPropertiesDTO authorizeAndStoreDevice(@NotNull String code, +// @NotNull String sourceType) { +// RestSourceAccessToken accessToken = +// authorizationService.getAccessTokenWithAuthorizeCode(code, sourceType); +// +// if (accessToken != null) { +// +// Optional existingUser = restSourceUserRepository +// .findBySourceTypeAndExternalUserId(sourceType, accessToken.getExternalUserId()); +// +// RestSourceUser resultUser; +// if (existingUser.isPresent()) { +// resultUser = existingUser.get(); +// resultUser.safeUpdateTokenDetails(accessToken); +// } else { +// resultUser = new RestSourceUser() +// .authorized(true) +// .externalUserId(accessToken.getExternalUserId()) +// .sourceType(sourceType) +// .startDate(Instant.now()); +// resultUser.safeUpdateTokenDetails(accessToken); +// +// resultUser = this.restSourceUserRepository.save(resultUser); +// } +// return new RestSourceUserPropertiesDTO(resultUser); +// } else { +// log.error("Cannot get token a using authorization_code"); +// throw new TokenException(); +// } +// } +// +// @Transactional(readOnly = true) +// public RestSourceUserPropertiesDTO getRestSourceUserById(Long id) { +// Optional user = restSourceUserRepository.findById(id); +// +// if (user.isPresent()) { +// return new RestSourceUserPropertiesDTO(user.get()); +// } else { +// throw new NotFoundException("RestSourceUser not found with id " + id); +// } +// } +// +// @Transactional +// public RestSourceUserPropertiesDTO updateRestSourceUser(Long id, +// RestSourceUserPropertiesDTO sourceUserPropertiesDTO) { +// +// Optional sourceUser = restSourceUserRepository.findById(id); +// +// if (sourceUser.isPresent()) { +// RestSourceUser restSourceUserToSave = sourceUser.get(); +// restSourceUserToSave.safeUpdateProperties(sourceUserPropertiesDTO); +// return new RestSourceUserPropertiesDTO(restSourceUserRepository.save(restSourceUserToSave)); +// } else { +// throw new NotFoundException( +// "Unable to update rest source user. RestSourceUser not found with " + "id " +// + sourceUserPropertiesDTO.getId()); +// } +// +// } +// +// /** +// * Removes user from database and revokes access token and refresh token +// * +// * @param id userID +// */ +// @Transactional +// public void revokeTokenAndDeleteUser(Long id) { +// Optional user = restSourceUserRepository.findById(id); +// +// if (user.isPresent()) { +// RestSourceUser restSourceUser = user.get(); +// authorizationService +// .revokeToken(restSourceUser.getAccessToken(), restSourceUser.getSourceType()); +// restSourceUserRepository.deleteById(id); +// +// } else { +// throw new NotFoundException("RestSourceUser not found with id " + id); +// } +// } +// +// @Transactional(readOnly = true) +// public TokenDTO getDeviceTokenByUserId(Long id) { +// Optional user = restSourceUserRepository.findById(id); +// +// if (user.isPresent()) { +// RestSourceUser restSourceUser = user.get(); +// return new TokenDTO() +// .accessToken(restSourceUser.getAccessToken()) +// .expiresAt(restSourceUser.getExpiresAt()); +// } else { +// throw new NotFoundException("RestSourceUser not found with id " + id); +// } +// } +// +// @Transactional +// public TokenDTO refreshTokenForUser(Long id) { +// Optional user = restSourceUserRepository.findById(id); +// if (user.isPresent()) { +// RestSourceUser restSourceUser = user.get(); +// // refresh token by user id and source-type +// RestSourceAccessToken accessToken = authorizationService +// .refreshToken(restSourceUser.getRefreshToken(), restSourceUser.getSourceType()); +// // update token +// if (accessToken != null) { +// restSourceUser.safeUpdateTokenDetails(accessToken); +// restSourceUser = this.restSourceUserRepository.save(restSourceUser); +// return new TokenDTO() +// .accessToken(restSourceUser.getAccessToken()) +// .expiresAt(restSourceUser.getExpiresAt()); +// } else { +// throw new TokenException("Could not refresh token successfully"); +// } +// +// } else { +// throw new NotFoundException("RestSourceUser not found with id " + id); +// } +// } +// +// @Transactional(readOnly = true) +// public RestSourceUsers getAllUsersBySourceType(String sourceType) { +// log.debug("Querying all saved users by source-type {}", sourceType); +// return new RestSourceUsers() +// .users(this.restSourceUserRepository.findAllBySourceType(sourceType) +// .stream() +// .map(RestSourceUserPropertiesDTO::new) +// .collect(Collectors.toList())); +// } +// +// /** +// * This resets the user by updating the current version. Currently, the version is calculated as a +// * String based on the current time instant. This may change in the future. +// * +// * @param id the database ID of the User +// * @return The updated User details +// */ +// public RestSourceUserPropertiesDTO resetUser(Long id) { +// Optional sourceUser = restSourceUserRepository.findById(id); +// +// if (sourceUser.isPresent()) { +// RestSourceUser restSourceUserToSave = sourceUser.get(); +// return new RestSourceUserPropertiesDTO( +// restSourceUserRepository.save( +// restSourceUserToSave +// .version(Instant.now().toString()) +// .setTimesReset(restSourceUserToSave.getTimesReset() + 1))); +// } else { +// throw new NotFoundException( +// "Unable to reset rest source user. RestSourceUser not found with " + "id " +// + id); +// } +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourcesProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourcesProjectService.kt new file mode 100644 index 00000000..35393231 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourcesProjectService.kt @@ -0,0 +1,14 @@ +package org.radarbase.authorizer.service + +import org.radarbase.authorizer.api.Project +import org.radarbase.authorizer.api.User +import org.radarbase.jersey.auth.Auth +import org.radarbase.jersey.auth.ProjectService + + +interface RadarProjectService : ProjectService { + fun project(projectId: String): Project + fun userProjects(auth: Auth): List + fun projectUsers(projectId: String): List + fun userByExternalId(projectId: String, externalUserId: String): User? +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/Oauth2AccessToken.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/Oauth2AccessToken.java deleted file mode 100644 index c7792f17..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/Oauth2AccessToken.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.service.dto; - - -import java.util.Objects; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class Oauth2AccessToken { - - @JsonProperty("access_token") - private String accessToken; - - @JsonProperty("refresh_token") - private String refreshToken; - - @JsonProperty("expires_in") - private Integer expiresIn; - - @JsonProperty("token_type") - private String tokenType; - - public String getAccessToken() { - return accessToken; - } - - public Oauth2AccessToken accessToken(String accessToken) { - this.accessToken = accessToken; - return this; - } - - public String getRefreshToken() { - return refreshToken; - } - - public Oauth2AccessToken refreshToken(String refreshToken) { - this.refreshToken = refreshToken; - return this; - } - - public Integer getExpiresIn() { - return expiresIn; - } - - public Oauth2AccessToken expiresIn(Integer expiresIn) { - this.expiresIn = expiresIn; - return this; - } - - public String getTokenType() { - return tokenType; - } - - public Oauth2AccessToken tokenType(String tokenType) { - this.tokenType = tokenType; - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Oauth2AccessToken that = (Oauth2AccessToken) o; - return Objects.equals(accessToken, that.accessToken) - && Objects.equals(refreshToken, that.refreshToken) - && Objects.equals(expiresIn, that.expiresIn) - && Objects.equals(tokenType, that.tokenType); - } - - @Override - public int hashCode() { - - return Objects.hash(accessToken, refreshToken, expiresIn, tokenType); - } - - @Override - public String toString() { - return "Oauth2AccessToken{" - + "accessToken='" + accessToken + '\'' - + ", refreshToken='" + refreshToken + '\'' - + ", expiresIn='" + expiresIn + '\'' - + ", tokenType='" + tokenType + '\'' - + '}'; - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceAccessToken.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceAccessToken.java deleted file mode 100644 index a9c9e5a5..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceAccessToken.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.service.dto; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class RestSourceAccessToken extends Oauth2AccessToken { - - @JsonProperty("user_id") - private String externalUserId; - - public String getExternalUserId() { - return externalUserId; - } - - public RestSourceAccessToken externalUserId(String externalUserId) { - this.externalUserId = externalUserId; - return this; - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClientDetailsDTO.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClientDetailsDTO.java deleted file mode 100644 index 5b083125..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClientDetailsDTO.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.service.dto; - -import org.radarbase.authorizer.config.RestSourceClientConfig; - -public class RestSourceClientDetailsDTO { - - private String sourceType; - - private String authorizationEndpoint; - - private String tokenEndpoint; - - private String grantType; - - private String scope; - - private String clientId; - - public RestSourceClientDetailsDTO() { - } - - public RestSourceClientDetailsDTO(RestSourceClientConfig restSourceClientConfig) { - this.authorizationEndpoint = restSourceClientConfig.getAuthorizationEndpoint(); - this.sourceType = restSourceClientConfig.getSourceType(); - this.grantType = restSourceClientConfig.getGrantType(); - this.clientId = restSourceClientConfig.getClientId(); - this.scope = restSourceClientConfig.getScope(); - this.tokenEndpoint = restSourceClientConfig.getTokenEndpoint(); - } - - public String getSourceType() { - return sourceType; - } - - public void setSourceType(String sourceType) { - this.sourceType = sourceType; - } - - public String getAuthorizationEndpoint() { - return authorizationEndpoint; - } - - public void setAuthorizationEndpoint(String authorizationEndpoint) { - this.authorizationEndpoint = authorizationEndpoint; - } - - public String getTokenEndpoint() { - return tokenEndpoint; - } - - public void setTokenEndpoint(String tokenEndpoint) { - this.tokenEndpoint = tokenEndpoint; - } - - public String getGrantType() { - return grantType; - } - - public void setGrantType(String grantType) { - this.grantType = grantType; - } - - public String getScope() { - return scope; - } - - public void setScope(String scope) { - this.scope = scope; - } - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClients.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClients.java deleted file mode 100644 index 024e7873..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceClients.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.service.dto; - -import java.util.List; - -public class RestSourceClients { - - private List sourceClients; - - public List getSourceClients() { - return sourceClients; - } - - public RestSourceClients sourceClients(List sourceClients) { - this.sourceClients = sourceClients; - return this; - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUserPropertiesDTO.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUserPropertiesDTO.java deleted file mode 100644 index 6d88fd1b..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/RestSourceUserPropertiesDTO.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.service.dto; - - -import java.io.Serializable; -import java.time.Instant; -import java.util.Objects; - -import org.radarbase.authorizer.domain.RestSourceUser; - - -public class RestSourceUserPropertiesDTO implements Serializable { - - private static final long serialVersionUID = 1L; - // Unique user key - private String id; - - // Version to reset the user - private String version = null; - - // The number of times a user has been reset - private long timesReset = 0; - - // Project ID to be used in org.radarcns.kafka.ObservationKey record keys - private String projectId; - - // User ID to be used in org.radarcns.kafka.ObservationKey record keys - private String userId; - - // Source ID to be used in org.radarcns.kafka.ObservationKey record keys - private String sourceId; - - // Date from when to collect data. - private Instant startDate; - - // Date until when to collect data. - private Instant endDate; - - private String sourceType; - - // is authorized by user - private Boolean authorized = false; - - private String externalUserId; - - public RestSourceUserPropertiesDTO() { - } - - public RestSourceUserPropertiesDTO(RestSourceUser restSourceUser) { - this.id = String.valueOf(restSourceUser.getId()); - this.projectId = restSourceUser.getProjectId(); - this.userId = restSourceUser.getUserId(); - this.sourceId = restSourceUser.getSourceId(); - this.authorized = restSourceUser.getAuthorized(); - this.sourceType = restSourceUser.getSourceType(); - this.endDate = restSourceUser.getEndDate(); - this.startDate = restSourceUser.getStartDate(); - this.externalUserId = restSourceUser.getExternalUserId(); - this.version = restSourceUser.getVersion(); - this.timesReset = restSourceUser.getTimesReset(); - } - - public String getId() { - return id; - } - - public RestSourceUserPropertiesDTO id(String id) { - this.id = id; - return this; - } - - public String getVersion() { - return version; - } - - public RestSourceUserPropertiesDTO version(String version) { - this.version = version; - return this; - } - - public long getTimesReset() { - return timesReset; - } - - public RestSourceUserPropertiesDTO setTimesReset(long timesReset) { - this.timesReset = timesReset; - return this; - } - - public String getProjectId() { - return projectId; - } - - public RestSourceUserPropertiesDTO projectId(String projectId) { - this.projectId = projectId; - return this; - } - - public String getUserId() { - return userId; - } - - public RestSourceUserPropertiesDTO userId(String userId) { - this.userId = userId; - return this; - } - - public String getSourceId() { - return sourceId; - } - - public RestSourceUserPropertiesDTO sourceId(String sourceId) { - this.sourceId = sourceId; - return this; - } - - public Instant getStartDate() { - return startDate; - } - - public RestSourceUserPropertiesDTO startDate(Instant stateDate) { - this.startDate = stateDate; - return this; - } - - public Instant getEndDate() { - return endDate; - } - - public RestSourceUserPropertiesDTO endDate(Instant endDate) { - this.endDate = endDate; - return this; - } - - - public String getSourceType() { - return sourceType; - } - - public RestSourceUserPropertiesDTO sourceType(String sourceType) { - this.sourceType = sourceType; - return this; - } - - public Boolean isAuthorized() { - return authorized; - } - - public RestSourceUserPropertiesDTO authorized(Boolean isAuthorized) { - this.authorized = isAuthorized; - return this; - } - - public String getExternalUserId() { - return externalUserId; - } - - public RestSourceUserPropertiesDTO externalDeviceId(String externalDeviceId) { - this.externalUserId = externalDeviceId; - return this; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RestSourceUserPropertiesDTO that = (RestSourceUserPropertiesDTO) o; - return Objects.equals(id, that.id) && Objects.equals(projectId, that.projectId) - && Objects.equals(userId, that.userId) - && Objects.equals(sourceId, that.sourceId) - && Objects.equals(startDate, that.startDate) - && Objects.equals(endDate, that.endDate) - && Objects.equals(sourceType, that.sourceType) - && Objects.equals(authorized, that.authorized) - && Objects.equals(externalUserId, that.externalUserId); - } - - @Override - public int hashCode() { - - return Objects - .hash(id, projectId, userId, sourceId, startDate, endDate, sourceType, authorized, - externalUserId); - } - - @Override - public String toString() { - return "RestSourceUserPropertiesDTO{" - + "id='" + id + '\'' - + ", projectId='" + projectId + '\'' - + ", userId='" + userId + '\'' - + ", sourceId='" + sourceId + '\'' - + ", startDate=" + startDate + '\'' - + ", endDate=" + endDate + '\'' - + ", sourceType=" + sourceType + '\'' - + ", authorized=" + authorized + '\'' - + ", externalUserId='" + externalUserId + '\'' - + '}'; - } - - -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/TokenDTO.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/TokenDTO.java deleted file mode 100644 index 1a907a59..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/TokenDTO.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.service.dto; - -import java.time.Instant; - -public class TokenDTO { - - private String accessToken; - - private Instant expiresAt; - - public String getAccessToken() { - return accessToken; - } - - public TokenDTO accessToken(String accessToken) { - this.accessToken = accessToken; - return this; - } - - public Instant getExpiresAt() { - return expiresAt; - } - - public TokenDTO expiresAt(Instant expiresAt) { - this.expiresAt = expiresAt; - return this; - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java index a25ba426..6bfff214 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java @@ -1,223 +1,223 @@ -package org.radarbase.authorizer.service.managementportal; - -import static org.radarbase.authorizer.validation.ManagementPortalValidator.MP_VALIDATOR_PROPERTY_VALUE; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.time.Duration; -import java.time.Instant; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import org.radarbase.authorizer.config.ManagementPortalProperties; -import org.radarbase.authorizer.config.RestSourceAuthorizerProperties; -import org.radarbase.authorizer.service.dto.managementportal.Project; -import org.radarbase.authorizer.service.dto.managementportal.Subject; -import org.radarcns.exception.TokenException; -import org.radarcns.oauth.OAuth2AccessTokenDetails; -import org.radarcns.oauth.OAuth2Client; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.http.HttpHeaders; -import org.springframework.stereotype.Service; - -@Service -@ConditionalOnProperty(value = "rest-source-authorizer.validator", havingValue = MP_VALIDATOR_PROPERTY_VALUE) -public class CachedManagementPortalClient implements ManagementPortalClient { - - private static final Logger LOGGER = LoggerFactory.getLogger(CachedManagementPortalClient.class); - private Set subjects; - private Set projects; - - private Duration expiry = Duration.ofHours(1); - private Instant lastFetch; - - private OkHttpClient httpClient; - - private OAuth2Client oAuth2Client; - - private ManagementPortalProperties properties; - - private ObjectMapper mapper = new ObjectMapper(); - - @Autowired - public CachedManagementPortalClient(RestSourceAuthorizerProperties restSourceAuthorizerProperties) - throws MalformedURLException { - subjects = new HashSet<>(); - projects = new HashSet<>(); - lastFetch = Instant.MIN; - this.properties = restSourceAuthorizerProperties.getManagementPortal(); - init(); - } - - public CachedManagementPortalClient(ManagementPortalProperties managementPortalProperties, - Duration expiry) throws MalformedURLException { - this.expiry = expiry; - subjects = new HashSet<>(); - projects = new HashSet<>(); - lastFetch = Instant.MIN; - this.properties = managementPortalProperties; - init(); - } - - private void init() throws MalformedURLException { - - this.httpClient = new OkHttpClient.Builder() - .connectTimeout(20, TimeUnit.SECONDS) - .writeTimeout(20, TimeUnit.SECONDS) - .readTimeout(50, TimeUnit.SECONDS) - .build(); - - this.mapper = new ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - - this.oAuth2Client = new OAuth2Client.Builder() - .credentials(properties.getOauthClientId(), properties.getOauthClientSecret()) - .endpoint(new URL(properties.getBaseUrl()), this.properties.getTokenPath()) - .httpClient(httpClient) - .build(); - - LOGGER.info(this.properties.toString()); - LOGGER - .info("Trying to get a Token and check if it has required permissions at the endpoint: {}", - this.oAuth2Client.getTokenEndpoint()); - try { - OAuth2AccessTokenDetails accessToken = this.oAuth2Client.getValidToken(); - if (accessToken.getScope().contains("PROJECT.READ") && accessToken.getScope() - .contains("SUBJECT.READ")) { - LOGGER.info("The client has sufficient privileges. Proceeding normally..."); - } else { - throw new IllegalStateException( - "The configured oAuth client [" + this.properties.getOauthClientId() + ", " - + this.properties.getOauthClientSecret() - + "] does not have sufficient privileges on Management portal." - + " Please update it on Management portal or use a different client."); - } - } catch (TokenException exc) { - throw new IllegalStateException( - "There was a problem getting the oAuth token from the server: " + exc); - } - } - - @Override - public Subject getSubject(String subjectId) throws IOException, TokenException { - // First check if need to refresh subjects cache - if (isUpdateRequired()) { - update(); - return this.subjects.stream() - .filter(subject1 -> subject1.getSubjectId().equals(subjectId)) - .findFirst() - .orElse(null); - } else { - // Try to find the subject in cache if not updated - return this.subjects.stream() - .filter(subject1 -> subject1.getSubjectId().equals(subjectId)) - .findFirst() - .orElse(querySubject(subjectId)); - } - } - - @Override - public Project getProject(String projectId) throws IOException, TokenException { - if (isUpdateRequired()) { - update(); - return this.projects.stream() - .filter(project1 -> project1.getProjectId().equals(projectId)) - .findFirst() - .orElse(null); - } else { - return this.projects.stream() - .filter(project1 -> project1.getProjectId().equals(projectId)) - .findFirst() - .orElse(queryProject(projectId)); - } - } - - @Override - public Set getAllSubjects() throws IOException, TokenException { - if (isUpdateRequired()) { - update(); - } - return this.subjects; - } - - @Override - public Set getAllProjects() throws IOException, TokenException { - if (isUpdateRequired()) { - update(); - } - return this.projects; - } - - private Subject querySubject(String subjectId) throws IOException, TokenException { - Subject subject = queryEntity( - properties.getBaseUrl() + properties.getSubjectsPath() + "/" + subjectId, - new TypeReference() { - }); - this.subjects.add(subject); - return subject; - } - - private Project queryProject(String projectId) throws IOException, TokenException { - Project project = queryEntity( - properties.getBaseUrl() + properties.getProjectsPath() + "/" + projectId, - new TypeReference() { - }); - - this.projects.add(project); - return project; - } - - private T queryEntity(String url, TypeReference t) - throws TokenException, IOException { - Request request = new Request.Builder() - .addHeader(HttpHeaders.AUTHORIZATION, - "Bearer " + oAuth2Client.getValidToken().getAccessToken()) - .url(new URL(url)) - .get() - .build(); - Response response = httpClient.newCall(request).execute(); - if (response.isSuccessful() && response.body() != null) { - return mapper.readValue(response.body().string(), t); - } else { - throw new IOException( - "The Request was not successful: Status-" + response.code() + ", Body-" + - (response.body() != null ? response.body().string() : "")); - } - } - - private Set queryAllSubjects() throws IOException, TokenException { - // get subjects from MP - return queryEntity(properties.getBaseUrl() + properties.getSubjectsPath(), - new TypeReference>() { - }); - } - - private Set queryAllProjects() throws IOException, TokenException { - // get projects from MP - return queryEntity(properties.getBaseUrl() + properties.getProjectsPath(), - new TypeReference>() { - }); - } - - synchronized private void update() throws IOException, TokenException { - Set subjects1 = queryAllSubjects(); - Set projects1 = queryAllProjects(); - this.subjects = subjects1 == null ? new HashSet<>() : subjects1; - this.projects = projects1 == null ? new HashSet<>() : projects1; - lastFetch = Instant.now(); - } - - synchronized private boolean isUpdateRequired() { - return this.lastFetch.plus(this.expiry).isBefore(Instant.now()); - } -} +//package org.radarbase.authorizer.service.managementportal; +// +//import static org.radarbase.authorizer.validation.ManagementPortalValidator.MP_VALIDATOR_PROPERTY_VALUE; +// +//import com.fasterxml.jackson.core.type.TypeReference; +//import com.fasterxml.jackson.databind.DeserializationFeature; +//import com.fasterxml.jackson.databind.ObjectMapper; +//import java.io.IOException; +//import java.net.MalformedURLException; +//import java.net.URL; +//import java.time.Duration; +//import java.time.Instant; +//import java.util.HashSet; +//import java.util.Set; +//import java.util.concurrent.TimeUnit; +//import okhttp3.OkHttpClient; +//import okhttp3.Request; +//import okhttp3.Response; +//import org.radarbase.authorizer.config.ManagementPortalProperties; +//import org.radarbase.authorizer.config.RestSourceAuthorizerProperties; +//import org.radarbase.authorizer.service.dto.managementportal.Project; +//import org.radarbase.authorizer.service.dto.managementportal.Subject; +//import org.radarcns.exception.TokenException; +//import org.radarcns.oauth.OAuth2AccessTokenDetails; +//import org.radarcns.oauth.OAuth2Client; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +//import org.springframework.http.HttpHeaders; +//import org.springframework.stereotype.Service; +// +//@Service +//@ConditionalOnProperty(value = "rest-source-authorizer.validator", havingValue = MP_VALIDATOR_PROPERTY_VALUE) +//public class CachedManagementPortalClient implements ManagementPortalClient { +// +// private static final Logger LOGGER = LoggerFactory.getLogger(CachedManagementPortalClient.class); +// private Set subjects; +// private Set projects; +// +// private Duration expiry = Duration.ofHours(1); +// private Instant lastFetch; +// +// private OkHttpClient httpClient; +// +// private OAuth2Client oAuth2Client; +// +// private ManagementPortalProperties properties; +// +// private ObjectMapper mapper = new ObjectMapper(); +// +// @Autowired +// public CachedManagementPortalClient(RestSourceAuthorizerProperties restSourceAuthorizerProperties) +// throws MalformedURLException { +// subjects = new HashSet<>(); +// projects = new HashSet<>(); +// lastFetch = Instant.MIN; +// this.properties = restSourceAuthorizerProperties.getManagementPortal(); +// init(); +// } +// +// public CachedManagementPortalClient(ManagementPortalProperties managementPortalProperties, +// Duration expiry) throws MalformedURLException { +// this.expiry = expiry; +// subjects = new HashSet<>(); +// projects = new HashSet<>(); +// lastFetch = Instant.MIN; +// this.properties = managementPortalProperties; +// init(); +// } +// +// private void init() throws MalformedURLException { +// +// this.httpClient = new OkHttpClient.Builder() +// .connectTimeout(20, TimeUnit.SECONDS) +// .writeTimeout(20, TimeUnit.SECONDS) +// .readTimeout(50, TimeUnit.SECONDS) +// .build(); +// +// this.mapper = new ObjectMapper() +// .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); +// +// this.oAuth2Client = new OAuth2Client.Builder() +// .credentials(properties.getOauthClientId(), properties.getOauthClientSecret()) +// .endpoint(new URL(properties.getBaseUrl()), this.properties.getTokenPath()) +// .httpClient(httpClient) +// .build(); +// +// LOGGER.info(this.properties.toString()); +// LOGGER +// .info("Trying to get a Token and check if it has required permissions at the endpoint: {}", +// this.oAuth2Client.getTokenEndpoint()); +// try { +// OAuth2AccessTokenDetails accessToken = this.oAuth2Client.getValidToken(); +// if (accessToken.getScope().contains("PROJECT.READ") && accessToken.getScope() +// .contains("SUBJECT.READ")) { +// LOGGER.info("The client has sufficient privileges. Proceeding normally..."); +// } else { +// throw new IllegalStateException( +// "The configured oAuth client [" + this.properties.getOauthClientId() + ", " +// + this.properties.getOauthClientSecret() +// + "] does not have sufficient privileges on Management portal." +// + " Please update it on Management portal or use a different client."); +// } +// } catch (TokenException exc) { +// throw new IllegalStateException( +// "There was a problem getting the oAuth token from the server: " + exc); +// } +// } +// +// @Override +// public Subject getSubject(String subjectId) throws IOException, TokenException { +// // First check if need to refresh subjects cache +// if (isUpdateRequired()) { +// update(); +// return this.subjects.stream() +// .filter(subject1 -> subject1.getSubjectId().equals(subjectId)) +// .findFirst() +// .orElse(null); +// } else { +// // Try to find the subject in cache if not updated +// return this.subjects.stream() +// .filter(subject1 -> subject1.getSubjectId().equals(subjectId)) +// .findFirst() +// .orElse(querySubject(subjectId)); +// } +// } +// +// @Override +// public Project getProject(String projectId) throws IOException, TokenException { +// if (isUpdateRequired()) { +// update(); +// return this.projects.stream() +// .filter(project1 -> project1.getProjectId().equals(projectId)) +// .findFirst() +// .orElse(null); +// } else { +// return this.projects.stream() +// .filter(project1 -> project1.getProjectId().equals(projectId)) +// .findFirst() +// .orElse(queryProject(projectId)); +// } +// } +// +// @Override +// public Set getAllSubjects() throws IOException, TokenException { +// if (isUpdateRequired()) { +// update(); +// } +// return this.subjects; +// } +// +// @Override +// public Set getAllProjects() throws IOException, TokenException { +// if (isUpdateRequired()) { +// update(); +// } +// return this.projects; +// } +// +// private Subject querySubject(String subjectId) throws IOException, TokenException { +// Subject subject = queryEntity( +// properties.getBaseUrl() + properties.getSubjectsPath() + "/" + subjectId, +// new TypeReference() { +// }); +// this.subjects.add(subject); +// return subject; +// } +// +// private Project queryProject(String projectId) throws IOException, TokenException { +// Project project = queryEntity( +// properties.getBaseUrl() + properties.getProjectsPath() + "/" + projectId, +// new TypeReference() { +// }); +// +// this.projects.add(project); +// return project; +// } +// +// private T queryEntity(String url, TypeReference t) +// throws TokenException, IOException { +// Request request = new Request.Builder() +// .addHeader(HttpHeaders.AUTHORIZATION, +// "Bearer " + oAuth2Client.getValidToken().getAccessToken()) +// .url(new URL(url)) +// .get() +// .build(); +// Response response = httpClient.newCall(request).execute(); +// if (response.isSuccessful() && response.body() != null) { +// return mapper.readValue(response.body().string(), t); +// } else { +// throw new IOException( +// "The Request was not successful: Status-" + response.code() + ", Body-" + +// (response.body() != null ? response.body().string() : "")); +// } +// } +// +// private Set queryAllSubjects() throws IOException, TokenException { +// // get subjects from MP +// return queryEntity(properties.getBaseUrl() + properties.getSubjectsPath(), +// new TypeReference>() { +// }); +// } +// +// private Set queryAllProjects() throws IOException, TokenException { +// // get projects from MP +// return queryEntity(properties.getBaseUrl() + properties.getProjectsPath(), +// new TypeReference>() { +// }); +// } +// +// synchronized private void update() throws IOException, TokenException { +// Set subjects1 = queryAllSubjects(); +// Set projects1 = queryAllProjects(); +// this.subjects = subjects1 == null ? new HashSet<>() : subjects1; +// this.projects = projects1 == null ? new HashSet<>() : projects1; +// lastFetch = Instant.now(); +// } +// +// synchronized private boolean isUpdateRequired() { +// return this.lastFetch.plus(this.expiry).isBefore(Instant.now()); +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt new file mode 100644 index 00000000..2d114753 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt @@ -0,0 +1,138 @@ +/* + * + * * Copyright 2019 The Hyve + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * * + * + */ + +package org.radarbase.authorizer.service.managementportal + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import okhttp3.* +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.radarbase.authorizer.Config +import org.radarbase.authorizer.api.Project +import org.radarbase.authorizer.api.User +import org.radarbase.jersey.auth.Auth +import org.radarbase.jersey.exception.HttpBadGatewayException +import org.slf4j.LoggerFactory +import java.net.MalformedURLException +import java.time.Duration +import java.time.Instant +import javax.ws.rs.core.Context + +class MPClient(@Context config: Config, @Context private val auth: Auth) { + private val clientId: String = config.auth.clientId + private val clientSecret: String = config.auth.clientSecret ?: throw IllegalArgumentException("Cannot configure managementportal client without client secret") + private val httpClient = OkHttpClient() + private val baseUrl: HttpUrl = config.auth.managementPortalUrl.toHttpUrlOrNull() + ?: throw MalformedURLException("Cannot parse base URL ${config.auth.managementPortalUrl} as an URL") + private val mapper = jacksonObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + private val projectListReader = mapper.readerFor(object : TypeReference>(){}) + private val userListReader = mapper.readerFor(object : TypeReference>(){}) + + private var token: String? = null + private var expiration: Instant? = null + + private val validToken: String? + get() { + val localToken = token ?: return null + expiration?.takeIf { it > Instant.now() } ?: return null + return localToken + } + + private fun ensureToken(): String { + var localToken = validToken + + return if (localToken != null) { + localToken + } else { + val request = Request.Builder().apply { + url(baseUrl.resolve("oauth/token")!!) + post(FormBody.Builder().apply { + add("grant_type", "client_credentials") + add("client_id", clientId) + add("client_secret", clientSecret) + }.build()) + header("Authorization", Credentials.basic(clientId, clientSecret)) + }.build() + + val result = mapper.readTree(execute(request)) + localToken = result["access_token"].asText() + ?: throw HttpBadGatewayException("ManagementPortal did not provide an access token") + expiration = Instant.now() + Duration.ofSeconds(result["expires_in"].asLong()) - Duration.ofMinutes(5) + token = localToken + localToken + } + } + + fun readProjects(): List { + logger.debug("Requesting for projects") + val request = Request.Builder().apply { + url(baseUrl.resolve("api/projects")!!) + header("Authorization", "Bearer ${ensureToken()}") + }.build() + + return projectListReader.readValue>(execute(request)) + .map { Project( + id = it.id, + name = it.name, + location = it.location, + organization = it.organization, + description = it.description) } + } + + private fun execute(request: Request): String { + return httpClient.newCall(request).execute().use { response -> + if (response.isSuccessful) { + response.body?.string() + ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") + } else { + logger.error("Cannot connect to managementportal ", response.code) + throw HttpBadGatewayException("Cannot connect to managementportal : Response-code ${response.code}") + } + } + } + + fun readParticipants(projectId: String): List { + val request = Request.Builder().apply { + url(baseUrl.newBuilder() + .addPathSegments("api/projects/$projectId/subjects") + .addQueryParameter("page", "0") + .addQueryParameter("size", Int.MAX_VALUE.toString()) + .build()) + header("Authorization", "Bearer ${ensureToken()}") + }.build() + + return userListReader.readValue>(execute(request)) + .map { User( + id = it.login, + projectId = projectId, + externalId = it.externalId, + status = it.status) } + } + + data class SubjectDto(val login: String, val externalId: String? = null, val status: String = "DEACTIVATED", val attributes: Map = mapOf()) + + data class ProjectDto(@JsonProperty("projectName") val id: String, @JsonProperty("humanReadableProjectName") val name: String? = null, val location: String? = null, val organization: String? = null, val description: String? = null) + + companion object { + private val logger = LoggerFactory.getLogger(MPClient::class.java) + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt new file mode 100644 index 00000000..82025140 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt @@ -0,0 +1,71 @@ +/* + * + * * Copyright 2019 The Hyve + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * * + * + */ + +package org.radarbase.authorizer.service.managementportal + +import org.radarbase.authorizer.Config +import org.radarbase.authorizer.api.Project +import org.radarbase.authorizer.api.User +import org.radarbase.authorizer.service.RadarProjectService +import org.radarbase.jersey.auth.Auth +import org.radarbase.jersey.exception.HttpNotFoundException +import org.radarbase.upload.util.CachedSet +import org.radarcns.auth.authorization.Permission +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import javax.ws.rs.core.Context + +class MPProjectService(@Context private val config: Config, @Context private val mpClient: MPClient): RadarProjectService { + private val projects = CachedSet( + Duration.ofMinutes(config.service.syncProjectsIntervalMin), + Duration.ofMinutes(1)) { + mpClient.readProjects() + } + + private val participants: ConcurrentMap> = ConcurrentHashMap() + + override fun ensureProject(projectId: String) { + if (projects.find { it.id == projectId } == null) { + throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") + } + } + + override fun userProjects(auth: Auth): List { + return projects.get() + .filter { auth.token.hasPermissionOnProject(Permission.PROJECT_READ, it.id) } + } + + override fun project(projectId: String) : Project = projects.find { it.id == projectId } ?: + throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") + + override fun projectUsers(projectId: String): List { + val projectParticipants = participants.computeIfAbsent(projectId) { + CachedSet(Duration.ofMinutes(config.service.syncParticipantsIntervalMin), Duration.ofMinutes(1)) { + mpClient.readParticipants(projectId) + } + } + + return projectParticipants.get().toList() + } + + override fun userByExternalId(projectId: String, externalUserId: String): User? = + projectUsers(projectId).find { it.externalId == externalUserId } + +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java index ed049e34..331d4af0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java @@ -1,18 +1,18 @@ -package org.radarbase.authorizer.service.managementportal; - -import java.io.IOException; -import java.util.Collection; -import org.radarbase.authorizer.service.dto.managementportal.Project; -import org.radarbase.authorizer.service.dto.managementportal.Subject; -import org.radarcns.exception.TokenException; - -public interface ManagementPortalClient { - - S getSubject(String subjectId) throws IOException, TokenException; - - P getProject(String projectId) throws IOException, TokenException; - - Collection getAllSubjects() throws IOException, TokenException; - - Collection

getAllProjects() throws IOException, TokenException; -} +//package org.radarbase.authorizer.service.managementportal; +// +//import java.io.IOException; +//import java.util.Collection; +//import org.radarbase.authorizer.service.dto.managementportal.Project; +//import org.radarbase.authorizer.service.dto.managementportal.Subject; +//import org.radarcns.exception.TokenException; +// +//public interface ManagementPortalClient { +// +// S getSubject(String subjectId) throws IOException, TokenException; +// +// P getProject(String projectId) throws IOException, TokenException; +// +// Collection getAllSubjects() throws IOException, TokenException; +// +// Collection

getAllProjects() throws IOException, TokenException; +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt new file mode 100644 index 00000000..682335ad --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt @@ -0,0 +1,71 @@ +/* + * + * * Copyright 2019 The Hyve + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * * + * + */ + +package org.radarbase.upload.util + +import java.time.Duration +import java.time.Instant + +class CachedSet( + private val refreshDuration: Duration, + private val retryDuration: Duration, + private val supplier: () -> Iterable) { + @set:Synchronized + private var cached: Set = emptySet() + set(value) { + val now = Instant.now() + field = value + nextRefresh = now.plus(refreshDuration) + nextRetry = now.plus(retryDuration) + } + + private var nextRefresh: Instant = Instant.MIN + private var nextRetry: Instant = Instant.MIN + + @get:Synchronized + private val state: State + get () { + val now = Instant.now() + return State(cached, + now.isAfter(nextRefresh), + now.isAfter(nextRetry)) + } + + private fun refresh() = supplier.invoke().toSet() + .also { cached = it } + + fun contains(value: T) = state.query({ it.contains(value) }, { it }) + fun find(predicate: (T) -> Boolean): T? = state.query({ it.find(predicate) }, { it != null }) + fun get(): Set = state.query({ it }, { it.isNotEmpty() }) + + private inner class State(val cache: Set, val mustRefresh: Boolean, val mayRetry: Boolean) { + fun query(method: (Set) -> S, validityPredicate: (S) -> Boolean): S { + var result: S + if (mustRefresh) { + result = method(refresh()) + } else { + result = method(cache) + if (!validityPredicate(result) && mayRetry) { + result = method(refresh()) + } + } + return result + } + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java index bcf67309..f8ff264c 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java @@ -1,65 +1,65 @@ -package org.radarbase.authorizer.validation; - -import static org.radarbase.authorizer.validation.ManagementPortalValidator.MP_VALIDATOR_PROPERTY_VALUE; - -import java.io.IOException; -import java.net.MalformedURLException; -import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; -import org.radarbase.authorizer.service.dto.managementportal.Subject; -import org.radarbase.authorizer.service.managementportal.ManagementPortalClient; -import org.radarbase.authorizer.validation.exception.ValidationFailedException; -import org.radarcns.exception.TokenException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; - -@Component -@ConditionalOnProperty(value = "rest-source-authorizer.validator", havingValue = MP_VALIDATOR_PROPERTY_VALUE) -public class ManagementPortalValidator implements Validator { - - public static final String MP_VALIDATOR_PROPERTY_VALUE = "managementportal"; - - private static final Logger logger = LoggerFactory.getLogger(ManagementPortalValidator.class); - private final ManagementPortalClient mpClient; - - @Autowired - public ManagementPortalValidator(ManagementPortalClient mpClient) { - this.mpClient = mpClient; - } - - @Override - public boolean validate(RestSourceUserPropertiesDTO restSourceUser) { - if (restSourceUser == null) { - return false; - } - - Subject subject; - try { - subject = mpClient.getSubject(restSourceUser.getUserId()); - } catch (TokenException exc) { - logger.warn("Cannot get a valid token from Management Portal.", exc); - throw new ValidationFailedException(restSourceUser, this, - "Cannot get a valid token from Management Portal. " + exc.getMessage()); - } catch (MalformedURLException exc) { - logger.warn("URL is mis-configured.", exc); - throw new ValidationFailedException(restSourceUser, this, - "URL is mis-configured. " + exc.getMessage()); - } catch (IOException exc) { - logger.warn("An error occurred while making Validating request.", exc); - throw new ValidationFailedException(restSourceUser, this, - "An error occurred while making Validating request. " + exc.getMessage()); - } - if (subject != null) { - return subject.getProject().getProjectId().equals(restSourceUser.getProjectId()); - } else { - logger.warn("The subject with id {} was not found in the Management portal.", - restSourceUser.getUserId()); - throw new ValidationFailedException(restSourceUser, this, - "The subject with id " + restSourceUser.getUserId() - + " was not found in the Management portal." - ); - } - } -} +//package org.radarbase.authorizer.validation; +// +//import static org.radarbase.authorizer.validation.ManagementPortalValidator.MP_VALIDATOR_PROPERTY_VALUE; +// +//import java.io.IOException; +//import java.net.MalformedURLException; +//import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; +//import org.radarbase.authorizer.service.dto.managementportal.Subject; +//import org.radarbase.authorizer.service.managementportal.ManagementPortalClient; +//import org.radarbase.authorizer.validation.exception.ValidationFailedException; +//import org.radarcns.exception.TokenException; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +//import org.springframework.stereotype.Component; +// +//@Component +//@ConditionalOnProperty(value = "rest-source-authorizer.validator", havingValue = MP_VALIDATOR_PROPERTY_VALUE) +//public class ManagementPortalValidator implements Validator { +// +// public static final String MP_VALIDATOR_PROPERTY_VALUE = "managementportal"; +// +// private static final Logger logger = LoggerFactory.getLogger(ManagementPortalValidator.class); +// private final ManagementPortalClient mpClient; +// +// @Autowired +// public ManagementPortalValidator(ManagementPortalClient mpClient) { +// this.mpClient = mpClient; +// } +// +// @Override +// public boolean validate(RestSourceUserPropertiesDTO restSourceUser) { +// if (restSourceUser == null) { +// return false; +// } +// +// Subject subject; +// try { +// subject = mpClient.getSubject(restSourceUser.getUserId()); +// } catch (TokenException exc) { +// logger.warn("Cannot get a valid token from Management Portal.", exc); +// throw new ValidationFailedException(restSourceUser, this, +// "Cannot get a valid token from Management Portal. " + exc.getMessage()); +// } catch (MalformedURLException exc) { +// logger.warn("URL is mis-configured.", exc); +// throw new ValidationFailedException(restSourceUser, this, +// "URL is mis-configured. " + exc.getMessage()); +// } catch (IOException exc) { +// logger.warn("An error occurred while making Validating request.", exc); +// throw new ValidationFailedException(restSourceUser, this, +// "An error occurred while making Validating request. " + exc.getMessage()); +// } +// if (subject != null) { +// return subject.getProject().getProjectId().equals(restSourceUser.getProjectId()); +// } else { +// logger.warn("The subject with id {} was not found in the Management portal.", +// restSourceUser.getUserId()); +// throw new ValidationFailedException(restSourceUser, this, +// "The subject with id " + restSourceUser.getUserId() +// + " was not found in the Management portal." +// ); +// } +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/Validator.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/Validator.java index 32bd9e76..85cd6c8b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/Validator.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/Validator.java @@ -1,8 +1,8 @@ -package org.radarbase.authorizer.validation; - -import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; - -public interface Validator { - - boolean validate(RestSourceUserPropertiesDTO projectId); -} +//package org.radarbase.authorizer.validation; +// +//import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; +// +//public interface Validator { +// +// boolean validate(RestSourceUserPropertiesDTO projectId); +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java index c5ca14eb..70f9a418 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java @@ -1,21 +1,21 @@ -package org.radarbase.authorizer.validation.exception; - -import org.radarbase.authorizer.validation.Validator; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.EXPECTATION_FAILED, reason = "Validation Failed for the Request.") -public class ValidationFailedException extends RuntimeException { - - public ValidationFailedException(Object entity, Validator validator) { - super( - "Validation Failed for [" + entity + "] using Validator [" + validator.getClass().getName() - + "]."); - } - - public ValidationFailedException(Object entity, Validator validator, String reason) { - super( - "Validation Failed for [" + entity + "] using Validator [" + validator.getClass().getName() - + "] due to [" + reason + "]."); - } -} +//package org.radarbase.authorizer.validation.exception; +// +//import org.radarbase.authorizer.validation.Validator; +//import org.springframework.http.HttpStatus; +//import org.springframework.web.bind.annotation.ResponseStatus; +// +//@ResponseStatus(value = HttpStatus.EXPECTATION_FAILED, reason = "Validation Failed for the Request.") +//public class ValidationFailedException extends RuntimeException { +// +// public ValidationFailedException(Object entity, Validator validator) { +// super( +// "Validation Failed for [" + entity + "] using Validator [" + validator.getClass().getName() +// + "]."); +// } +// +// public ValidationFailedException(Object entity, Validator validator, String reason) { +// super( +// "Validation Failed for [" + entity + "] using Validator [" + validator.getClass().getName() +// + "] due to [" + reason + "]."); +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java index 65d4fd26..451ed19a 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java @@ -1,39 +1,39 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.webapp.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.BAD_GATEWAY) -public class BadGatewayException extends RuntimeException { - - public BadGatewayException() { - super("Something went wrong in communication with other services"); - } - - public BadGatewayException(String message) { - super(message); - } - - public BadGatewayException(Throwable cause) { - super("Something went wrong in communication with other services", cause); - } -} +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer.webapp.exception; +// +//import org.springframework.http.HttpStatus; +//import org.springframework.web.bind.annotation.ResponseStatus; +// +//@ResponseStatus(value = HttpStatus.BAD_GATEWAY) +//public class BadGatewayException extends RuntimeException { +// +// public BadGatewayException() { +// super("Something went wrong in communication with other services"); +// } +// +// public BadGatewayException(String message) { +// super(message); +// } +// +// public BadGatewayException(Throwable cause) { +// super("Something went wrong in communication with other services", cause); +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java index 4e7318db..2caf2d9a 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java @@ -1,35 +1,35 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.webapp.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.EXPECTATION_FAILED) -public class InvalidSourceTypeException extends RuntimeException { - - public InvalidSourceTypeException(String sourceType) { - super("Unsupported source type [ " + sourceType + " ] found "); - } - - public InvalidSourceTypeException(String sourceType, Throwable cause) { - super("Cannot find configurations for type " + sourceType, cause); - } -} +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer.webapp.exception; +// +//import org.springframework.http.HttpStatus; +//import org.springframework.web.bind.annotation.ResponseStatus; +// +//@ResponseStatus(HttpStatus.EXPECTATION_FAILED) +//public class InvalidSourceTypeException extends RuntimeException { +// +// public InvalidSourceTypeException(String sourceType) { +// super("Unsupported source type [ " + sourceType + " ] found "); +// } +// +// public InvalidSourceTypeException(String sourceType, Throwable cause) { +// super("Cannot find configurations for type " + sourceType, cause); +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java index bca63a02..8f92ec1e 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java @@ -1,32 +1,32 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.webapp.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) -public class NotFoundException extends RuntimeException { - - public NotFoundException(String message) { - super(message); - } - -} +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer.webapp.exception; +// +//import org.springframework.http.HttpStatus; +//import org.springframework.web.bind.annotation.ResponseStatus; +// +//@ResponseStatus(HttpStatus.NOT_FOUND) +//public class NotFoundException extends RuntimeException { +// +// public NotFoundException(String message) { +// super(message); +// } +// +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java index f93516ab..9c72ce59 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java @@ -1,39 +1,39 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.webapp.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.UNAUTHORIZED) -public class TokenException extends RuntimeException { - - public TokenException() { - super("Unable to get a valid access token"); - } - - public TokenException(String message) { - super(message); - } - - public TokenException(Throwable cause) { - super("Unable to get a valid access token", cause); - } -} +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer.webapp.exception; +// +//import org.springframework.http.HttpStatus; +//import org.springframework.web.bind.annotation.ResponseStatus; +// +//@ResponseStatus(value = HttpStatus.UNAUTHORIZED) +//public class TokenException extends RuntimeException { +// +// public TokenException() { +// super("Unable to get a valid access token"); +// } +// +// public TokenException(String message) { +// super(message); +// } +// +// public TokenException(Throwable cause) { +// super("Unable to get a valid access token", cause); +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java index 7ae02fb8..927a634e 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java @@ -1,25 +1,25 @@ -package org.radarbase.authorizer.webapp.resource; - -import org.radarbase.authorizer.service.RestSourceClientService; -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.RestController; - -@RestController -public class HealthCheckResource { - private Logger logger = LoggerFactory.getLogger(HealthCheckResource.class); - - @Autowired - private RestSourceClientService restSourceClientService; - - @GetMapping("/health") - public ResponseEntity getAllDeviceProperties() { - logger.debug("Health check"); - this.restSourceClientService.getAllRestSourceClientDetails(); - return ResponseEntity.ok("Alive"); - } - -} +//package org.radarbase.authorizer.webapp.resource; +// +//import org.radarbase.authorizer.service.RestSourceClientService; +//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.RestController; +// +//@RestController +//public class HealthCheckResource { +// private Logger logger = LoggerFactory.getLogger(HealthCheckResource.class); +// +// @Autowired +// private RestSourceClientService restSourceClientService; +// +// @GetMapping("/health") +// public ResponseEntity getAllDeviceProperties() { +// logger.debug("Health check"); +// this.restSourceClientService.getAllRestSourceClientDetails(); +// return ResponseEntity.ok("Alive"); +// } +// +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java index 00d63b42..7ea45f10 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java @@ -1,132 +1,132 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.webapp.resource; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Optional; -import javax.validation.Valid; -import org.radarbase.authorizer.service.RestSourceUserService; -import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; -import org.radarbase.authorizer.service.dto.RestSourceUsers; -import org.radarbase.authorizer.service.dto.TokenDTO; -import org.radarbase.authorizer.validation.Validator; -import org.radarbase.authorizer.validation.exception.ValidationFailedException; -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.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class RestSourceUserResource { - - private final RestSourceUserService restSourceUserService; - private Logger logger = LoggerFactory.getLogger(RestSourceUserResource.class); - private Validator validator; - - @Autowired - public RestSourceUserResource( - RestSourceUserService restSourceUserService, - Optional validator) { - this.restSourceUserService = restSourceUserService; - this.validator = validator.orElse(null); - } - - @PostMapping("/users") - public ResponseEntity addAuthorizedRestSourceUser(@RequestParam(value = "code") String code, - @RequestParam(value = "state") String state) throws URISyntaxException { - logger.debug("Add a rest-source user with code {} and state {}", code, state); - RestSourceUserPropertiesDTO - user = this.restSourceUserService.authorizeAndStoreDevice(code, state); - return ResponseEntity - .created(new URI("/user/" + user.getId())).body(user); - } - - @GetMapping("/users") - public ResponseEntity getAllRestSources( - @RequestParam(value = "source-type", required = false) String sourceType) { - if (sourceType != null && !sourceType.isEmpty()) { - logger.debug("Get all rest source users by type {}", sourceType); - return ResponseEntity - .ok(this.restSourceUserService.getAllUsersBySourceType(sourceType)); - } - - logger.debug("Get all rest source users"); - return ResponseEntity - .ok(this.restSourceUserService.getAllRestSourceUsers()); - } - - @GetMapping("/users/{id}") - public ResponseEntity getRestSourceUserById( - @PathVariable String id) { - logger.debug("Get rest source user with id {}", id); - return ResponseEntity - .ok(this.restSourceUserService.getRestSourceUserById(Long.valueOf(id))); - } - - @PostMapping("/users/{id}") - public ResponseEntity updateDeviceUser(@Valid @PathVariable String id, - @RequestBody RestSourceUserPropertiesDTO restSourceUser, - @RequestParam(value = "validate", defaultValue = "false") Boolean isValidate) { - logger.debug("Requesting to update rest source user"); - if (isValidate && validator != null && !validator.validate(restSourceUser)) { - logger.warn("Validation Failed for: {}", restSourceUser); - throw new ValidationFailedException(restSourceUser, validator); - } - return ResponseEntity - .ok(this.restSourceUserService.updateRestSourceUser(Long.valueOf(id), restSourceUser)); - } - - @PostMapping("/users/{id}/reset") - public ResponseEntity resetDeviceUser(@Valid @PathVariable String id) { - logger.debug("Requesting to reset rest source user"); - return ResponseEntity.ok(this.restSourceUserService.resetUser(Long.valueOf(id))); - } - - @DeleteMapping("/users/{id}") - public ResponseEntity deleteDeviceUser(@Valid @PathVariable String id) { - logger.debug("Requesting to delete rest source user"); - this.restSourceUserService.revokeTokenAndDeleteUser(Long.valueOf(id)); - return ResponseEntity - .ok().header("user-removed", id).build(); - } - - - @GetMapping("/users/{id}/token") - public ResponseEntity getUserToken(@PathVariable String id) { - logger.debug("Get user token for rest source user id {}", id); - return ResponseEntity - .ok(this.restSourceUserService.getDeviceTokenByUserId(Long.valueOf(id))); - } - - @PostMapping("/users/{id}/token") - public ResponseEntity requestRefreshTokenForUser(@PathVariable String id) { - logger.debug("Refreshing user token for rest source user id {}", id); - return ResponseEntity - .ok(this.restSourceUserService.refreshTokenForUser(Long.valueOf(id))); - } -} \ No newline at end of file +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer.webapp.resource; +// +//import java.net.URI; +//import java.net.URISyntaxException; +//import java.util.Optional; +//import javax.validation.Valid; +//import org.radarbase.authorizer.service.RestSourceUserService; +//import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; +//import org.radarbase.authorizer.service.dto.RestSourceUsers; +//import org.radarbase.authorizer.service.dto.TokenDTO; +//import org.radarbase.authorizer.validation.Validator; +//import org.radarbase.authorizer.validation.exception.ValidationFailedException; +//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.DeleteMapping; +//import org.springframework.web.bind.annotation.GetMapping; +//import org.springframework.web.bind.annotation.PathVariable; +//import org.springframework.web.bind.annotation.PostMapping; +//import org.springframework.web.bind.annotation.RequestBody; +//import org.springframework.web.bind.annotation.RequestParam; +//import org.springframework.web.bind.annotation.RestController; +// +//@RestController +//public class RestSourceUserResource { +// +// private final RestSourceUserService restSourceUserService; +// private Logger logger = LoggerFactory.getLogger(RestSourceUserResource.class); +// private Validator validator; +// +// @Autowired +// public RestSourceUserResource( +// RestSourceUserService restSourceUserService, +// Optional validator) { +// this.restSourceUserService = restSourceUserService; +// this.validator = validator.orElse(null); +// } +// +// @PostMapping("/users") +// public ResponseEntity addAuthorizedRestSourceUser(@RequestParam(value = "code") String code, +// @RequestParam(value = "state") String state) throws URISyntaxException { +// logger.debug("Add a rest-source user with code {} and state {}", code, state); +// RestSourceUserPropertiesDTO +// user = this.restSourceUserService.authorizeAndStoreDevice(code, state); +// return ResponseEntity +// .created(new URI("/user/" + user.getId())).body(user); +// } +// +// @GetMapping("/users") +// public ResponseEntity getAllRestSources( +// @RequestParam(value = "source-type", required = false) String sourceType) { +// if (sourceType != null && !sourceType.isEmpty()) { +// logger.debug("Get all rest source users by type {}", sourceType); +// return ResponseEntity +// .ok(this.restSourceUserService.getAllUsersBySourceType(sourceType)); +// } +// +// logger.debug("Get all rest source users"); +// return ResponseEntity +// .ok(this.restSourceUserService.getAllRestSourceUsers()); +// } +// +// @GetMapping("/users/{id}") +// public ResponseEntity getRestSourceUserById( +// @PathVariable String id) { +// logger.debug("Get rest source user with id {}", id); +// return ResponseEntity +// .ok(this.restSourceUserService.getRestSourceUserById(Long.valueOf(id))); +// } +// +// @PostMapping("/users/{id}") +// public ResponseEntity updateDeviceUser(@Valid @PathVariable String id, +// @RequestBody RestSourceUserPropertiesDTO restSourceUser, +// @RequestParam(value = "validate", defaultValue = "false") Boolean isValidate) { +// logger.debug("Requesting to update rest source user"); +// if (isValidate && validator != null && !validator.validate(restSourceUser)) { +// logger.warn("Validation Failed for: {}", restSourceUser); +// throw new ValidationFailedException(restSourceUser, validator); +// } +// return ResponseEntity +// .ok(this.restSourceUserService.updateRestSourceUser(Long.valueOf(id), restSourceUser)); +// } +// +// @PostMapping("/users/{id}/reset") +// public ResponseEntity resetDeviceUser(@Valid @PathVariable String id) { +// logger.debug("Requesting to reset rest source user"); +// return ResponseEntity.ok(this.restSourceUserService.resetUser(Long.valueOf(id))); +// } +// +// @DeleteMapping("/users/{id}") +// public ResponseEntity deleteDeviceUser(@Valid @PathVariable String id) { +// logger.debug("Requesting to delete rest source user"); +// this.restSourceUserService.revokeTokenAndDeleteUser(Long.valueOf(id)); +// return ResponseEntity +// .ok().header("user-removed", id).build(); +// } +// +// +// @GetMapping("/users/{id}/token") +// public ResponseEntity getUserToken(@PathVariable String id) { +// logger.debug("Get user token for rest source user id {}", id); +// return ResponseEntity +// .ok(this.restSourceUserService.getDeviceTokenByUserId(Long.valueOf(id))); +// } +// +// @PostMapping("/users/{id}/token") +// public ResponseEntity requestRefreshTokenForUser(@PathVariable String id) { +// logger.debug("Refreshing user token for rest source user id {}", id); +// return ResponseEntity +// .ok(this.restSourceUserService.refreshTokenForUser(Long.valueOf(id))); +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java index 6376481f..5eaad5f6 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java @@ -1,63 +1,63 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.webapp.resource; - -import java.util.List; - -import org.radarbase.authorizer.service.RestSourceClientService; -import org.radarbase.authorizer.service.dto.RestSourceClientDetailsDTO; -import org.radarbase.authorizer.service.dto.RestSourceClients; -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.RestController; - -@RestController -public class SourceClientResource { - - private Logger logger = LoggerFactory.getLogger(SourceClientResource.class); - - @Autowired - private RestSourceClientService restSourceClientService; - - @GetMapping("/source-clients") - public ResponseEntity getAllDeviceProperties() { - logger.debug("Get all source clients details"); - return ResponseEntity.ok(this.restSourceClientService.getAllRestSourceClientDetails()); - } - - - @GetMapping("/source-clients/type") - public ResponseEntity> getAllAvailableDeviceTypes() { - logger.debug("Get all source-types"); - return ResponseEntity.ok(this.restSourceClientService.getAvailableDeviceTypes()); - } - - @GetMapping("/source-clients/{sourceType}") - public ResponseEntity getDeviceAuthDetailsByDeviceType( - @PathVariable String sourceType) { - logger.info("Get source clients detail by type {}", sourceType); - return ResponseEntity.ok(this.restSourceClientService.getAllRestSourceClientDetails(sourceType)); - } - -} +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer.webapp.resource; +// +//import java.util.List; +// +//import org.radarbase.authorizer.service.RestSourceClientService; +//import org.radarbase.authorizer.service.dto.RestSourceClientDetailsDTO; +//import org.radarbase.authorizer.service.dto.RestSourceClients; +//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.RestController; +// +//@RestController +//public class SourceClientResource { +// +// private Logger logger = LoggerFactory.getLogger(SourceClientResource.class); +// +// @Autowired +// private RestSourceClientService restSourceClientService; +// +// @GetMapping("/source-clients") +// public ResponseEntity getAllDeviceProperties() { +// logger.debug("Get all source clients details"); +// return ResponseEntity.ok(this.restSourceClientService.getAllRestSourceClientDetails()); +// } +// +// +// @GetMapping("/source-clients/type") +// public ResponseEntity> getAllAvailableDeviceTypes() { +// logger.debug("Get all source-types"); +// return ResponseEntity.ok(this.restSourceClientService.getAvailableDeviceTypes()); +// } +// +// @GetMapping("/source-clients/{sourceType}") +// public ResponseEntity getDeviceAuthDetailsByDeviceType( +// @PathVariable String sourceType) { +// logger.info("Get source clients detail by type {}", sourceType); +// return ResponseEntity.ok(this.restSourceClientService.getAllRestSourceClientDetails(sourceType)); +// } +// +//} diff --git a/authorizer-app-backend/src/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java b/authorizer-app-backend/src/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java index 911c64c9..5e500b9b 100644 --- a/authorizer-app-backend/src/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java +++ b/authorizer-app-backend/src/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java @@ -33,8 +33,8 @@ import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; import org.radarbase.authorizer.RadarRestSourceAuthorizerApplication; -import org.radarbase.authorizer.domain.RestSourceUser; -import org.radarbase.authorizer.repository.RestSourceUserRepository; +import org.radarbase.authorizer.doa.RestSourceUser; +import org.radarbase.authorizer.doa.RestSourceUserRepository; import org.radarbase.authorizer.service.RestSourceUserService; import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; import org.springframework.beans.factory.annotation.Autowired; diff --git a/build.gradle.kts b/build.gradle.kts index ed08fda7..dbe1c6fc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,10 @@ plugins { - kotlin("jvm") version "1.3.61" apply false + kotlin("jvm") version "1.4.0" apply false } subprojects { group = "org.radarbase" - version = "1.4.0-SNAPSHOT" + version = "2.0.0-SNAPSHOT" } tasks.wrapper { diff --git a/docker/etc/rest-source-authorizer/authorizer.yml b/docker/etc/rest-source-authorizer/authorizer.yml new file mode 100644 index 00000000..9a5560b5 --- /dev/null +++ b/docker/etc/rest-source-authorizer/authorizer.yml @@ -0,0 +1,20 @@ +service: + # Interval time in minutes for syncing projects and subjects. + baseUri: http://0.0.0.0:8080/rest-sources/backend/ + advertisedBaseUri: http://0.0.0.0:8080/rest-sources/backend/ + enableCors: true + +auth: + # Management Portal URL + managementPortalUrl: http://managementportal-app:8080/managementportal/ + # OAuth2 Client id of more-promasys-sync application + clientId: radar_rest_sources_auth_backend + # OAuth2 Client Secret of more_promasys_sync_client client + clientSecret: secret + +database: + jdbcDriver: org.postgresql.Driver + jdbcUrl: jdbc:postgresql://radarbase-postgresql:5432/uploadconnector + jdbcUser: radarcns + jdbcPassword: radarcns + hibernateDialect: org.hibernate.dialect.PostgreSQLDialect diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 87b738cbd051603d91cc39de6cb000dd98fe6b02..62d4c053550b91381bbd28b1afc82d634bf73a8a 100644 GIT binary patch delta 25879 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 Nq$R_^uaqX; diff --git a/gradlew b/gradlew index af6708ff..fbd7c515 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -66,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -109,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -138,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 6d57edc7..5093609d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,8 +29,11 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -65,6 +84,7 @@ set CMD_LINE_ARGS=%* set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% diff --git a/settings.gradle b/settings.gradle index 22caeed0..c000da9f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ rootProject.name = 'radar-rest-sources-authorizer' -include('authorizer-app-backend') \ No newline at end of file +include(':authorizer-app-backend') \ No newline at end of file From ebde02865ed7402f1752d814d513ac2f90a25b26 Mon Sep 17 00:00:00 2001 From: nivethika Date: Fri, 28 Aug 2020 10:37:59 +0200 Subject: [PATCH 04/80] fix entity mapping and liquibase loading --- authorizer-app-backend/build.gradle.kts | 2 +- .../java/org/radarbase/authorizer/Main.kt | 4 + .../authorizer/api/RestSourceUserMapper.kt | 9 ++- .../authorizer/doa/AbstractJpaPersistable.kt | 2 +- .../doa/RestSourceUserRepository.kt | 14 ++-- .../doa/RestSourceUserRepositoryImpl.kt | 50 +++++++++++++ .../authorizer/doa/entitiy/RestSourceUser.kt | 68 ----------------- .../authorizer/doa/entity/RestSourceUser.kt | 71 ++++++++++++++++++ ...ancer.kt => AuthorizerResourceEnhancer.kt} | 45 +++++++----- .../inject/DoaEntityManagerFactory.kt | 47 ++++++++++++ .../inject/DoaEntityManagerFactoryFactory.kt | 73 +++++++++++++++++++ .../inject/ManagementPortalEnhancerFactory.kt | 2 +- .../authorizer/resources/ProjectResource.kt | 45 ++++++++++++ .../resources/RestSourceUserResource.kt | 51 +++++++++++++ .../main/resources/META-INF/persistence.xml | 45 ++++++++++++ .../changelog/changes/db.changelog-master.xml | 10 +++ .../db/changelog/db.changelog-master.yaml | 7 -- 17 files changed, 441 insertions(+), 104 deletions(-) create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entitiy/RestSourceUser.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt rename authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/{UploadResourceEnhancer.kt => AuthorizerResourceEnhancer.kt} (68%) create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt create mode 100644 authorizer-app-backend/src/main/resources/META-INF/persistence.xml create mode 100644 authorizer-app-backend/src/main/resources/db/changelog/changes/db.changelog-master.xml delete mode 100644 authorizer-app-backend/src/main/resources/db/changelog/db.changelog-master.yaml diff --git a/authorizer-app-backend/build.gradle.kts b/authorizer-app-backend/build.gradle.kts index 8426a883..172d83ef 100644 --- a/authorizer-app-backend/build.gradle.kts +++ b/authorizer-app-backend/build.gradle.kts @@ -17,7 +17,7 @@ application { project.extra.apply { set("okhttpVersion", "4.2.0") set("radarJerseyVersion", "0.2.3") - set("jacksonVersion", "2.9.10") + set("jacksonVersion", "2.10.2") set("slf4jVersion", "1.7.27") set("logbackVersion", "1.2.3") set("grizzlyVersion", "2.4.4") diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt index 871ec9ce..bb4bdc11 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt @@ -19,6 +19,8 @@ package org.radarbase.authorizer +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule import org.radarbase.jersey.GrizzlyServer import org.radarbase.jersey.config.ConfigLoader import org.slf4j.Logger @@ -27,6 +29,8 @@ import org.slf4j.LoggerFactory val logger: Logger = LoggerFactory.getLogger("org.radarbase.authorizer.Main") fun main(args: Array) { +// val mapper = ObjectMapper(YAMLFactory()) +// .registerModule(KotlinModule()) val config: Config = ConfigLoader.loadConfig("authorizer.yml", args) val resources = ConfigLoader.loadResources(config.service.resourceConfig, config) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt index 067e2c03..290db051 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt @@ -1,9 +1,9 @@ package org.radarbase.authorizer.api -import org.radarbase.authorizer.doa.entitiy.RestSourceUser +import org.radarbase.authorizer.doa.entity.RestSourceUser -interface RestSourceUserMapper { +class RestSourceUserMapper { fun fromRestSourceUser(user: RestSourceUser) = RestSourceUserDTO( id = user.id.toString(), projectId = user.projectId, @@ -17,4 +17,9 @@ interface RestSourceUserMapper { version = user.version, timesReset = user.timesReset ) + + fun fromRestSourceUsers(records: List, page: Page?) = RestSourceUsers( + users = records.map(::fromRestSourceUser) + ) + } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt index daf5c7a0..cd8020c4 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt @@ -17,7 +17,7 @@ * */ -package org.radarbase.upload.doa +package org.radarbase.authorizer.doa import javax.persistence.GeneratedValue import javax.persistence.Id diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index 02232645..f6e4559f 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -1,14 +1,14 @@ package org.radarbase.authorizer.doa import org.radarbase.authorizer.api.Page -import org.radarbase.authorizer.doa.entitiy.RestSourceUser +import org.radarbase.authorizer.doa.entity.RestSourceUser interface RestSourceUserRepository { - fun create(user: RestSourceUser): RestSourceUser - fun read(id: Long): RestSourceUser? - fun update(user: RestSourceUser): RestSourceUser - fun query(page: Page, sourceType: String?, externalUserId: String?): Pair, Page> - fun findAllBySourceType(sourceType: String?): List - fun delete(user: RestSourceUser) +// fun create(user: RestSourceUser): RestSourceUser +// fun read(id: Long): RestSourceUser? +// fun update(user: RestSourceUser): RestSourceUser + fun query(page: Page, sourceType: String? = null, externalUserId: String? = null): Pair, Page> +// fun findAllBySourceType(sourceType: String?): List +// fun delete(user: RestSourceUser) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt new file mode 100644 index 00000000..e0517de9 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -0,0 +1,50 @@ +package org.radarbase.authorizer.doa + +import org.radarbase.authorizer.api.Page +import org.radarbase.authorizer.doa.entity.RestSourceUser +import javax.inject.Provider +import javax.persistence.EntityManager +import javax.ws.rs.core.Context + +class RestSourceUserRepositoryImpl( + @Context private var em: Provider +) : RestSourceUserRepository { +// override fun create(user: RestSourceUser): RestSourceUser { +// TODO("Not yet implemented") +// } + + override fun query(page: Page, sourceType: String?, externalUserId: String?): Pair, Page> { + var queryString = "SELECT u FROM RestSourceUser u" + var countQueryString = "SELECT count(u) FROM RestSourceUser u" + + + val actualPage = page.createValid(maximum = 100) + return em.get().transact { + val query = createQuery(queryString, RestSourceUser::class.java) +// .setParameter("projectId", projectId) + .setFirstResult(actualPage.offset) + .setMaxResults(actualPage.pageSize!!) + + val countQuery = createQuery(countQueryString) +// .setParameter("projectId", projectId) + +// userId?.let { +// query.setParameter("userId", it) +// countQuery.setParameter("userId", it) +// } +// status?.let { +// query.setParameter("status", RecordStatus.valueOf(it)) +// countQuery.setParameter("status", RecordStatus.valueOf(it)) +// } +// sourceType?.let { +// query.setParameter("sourceType", it) +// countQuery.setParameter("sourceType", it) +// } + val records = query.resultList + val count = countQuery.singleResult as Long + + Pair(records, actualPage.copy(totalElements = count)) + } + } + +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entitiy/RestSourceUser.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entitiy/RestSourceUser.kt deleted file mode 100644 index 31e380a3..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entitiy/RestSourceUser.kt +++ /dev/null @@ -1,68 +0,0 @@ -package org.radarbase.authorizer.doa.entitiy - -import org.radarbase.upload.doa.AbstractJpaPersistable -import java.time.Duration -import java.time.Instant -import java.util.* -import javax.persistence.* - -@Entity -@Table(name = "rest_source_user") -class RestSourceUser : AbstractJpaPersistable() { - @Transient - private val EXPIRY_TIME_MARGIN = Duration.ofMinutes(5) - - - // The version to be appended to ID for RESET of a user - // This should be updated whenever the user is RESET. - // By default this is null for backwards compatibility - @Column(name = "version") - var version: String? = null - - // The number of times a user has been reset - @Column(name = "time_reset") - var timesReset: Long = 0 - - // Project ID to be used in org.radarcns.kafka.ObservationKey record keys - @Column(name = "project_id") - lateinit var projectId: String - - // User ID to be used in org.radarcns.kafka.ObservationKey record keys - @Column(name = "user_id") - lateinit var userId: String - - // Source ID to be used in org.radarcns.kafka.ObservationKey record keys - @Column(name = "source_id") - var sourceId: String = UUID.randomUUID().toString() - - // Date from when to collect data. - @Column(name = "start_date") - lateinit var startDate: Instant - - @Column(name = "end_date") - var endDate: Instant? = null - - @Column(name = "source_type") - var sourceType: String? = null - - // is authorized by user - var authorized = false - - @Column(name = "external_user_id") - lateinit var externalUserId: String - - @Column(name = "access_token") - var accessToken: String? = null - - @Column(name = "refresh_token") - var refreshToken: String? = null - - @Column(name = "expires_in") - var expiresIn: Int? = null - - @Column(name = "expires_at") - var expiresAt: Instant? = null - - @Column(name = "token_type") - var tokenType: String? = null -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt new file mode 100644 index 00000000..afd59ec8 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt @@ -0,0 +1,71 @@ +package org.radarbase.authorizer.doa.entity + +import org.radarbase.authorizer.doa.AbstractJpaPersistable +import java.time.Duration +import java.time.Instant +import java.util.* +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table +import javax.persistence.Transient + +@Entity +@Table(name = "rest_source_user") +class RestSourceUser : AbstractJpaPersistable() { + @Transient + private val EXPIRY_TIME_MARGIN = Duration.ofMinutes(5) + + // Project ID to be used in org.radarcns.kafka.ObservationKey record keys + @Column(name = "project_id") + lateinit var projectId: String + + // User ID to be used in org.radarcns.kafka.ObservationKey record keys + @Column(name = "user_id") + lateinit var userId: String + + // Source ID to be used in org.radarcns.kafka.ObservationKey record keys + @Column(name = "source_id") + var sourceId: String = UUID.randomUUID().toString() + + @Column(name = "source_type") + var sourceType: String? = null + + // Date from when to collect data. + @Column(name = "start_date") + lateinit var startDate: Instant + + @Column(name = "end_date") + var endDate: Instant? = null + + @Column(name = "external_user_id") + lateinit var externalUserId: String + + // is authorized by user + @Column(name = "authorized") + var authorized = false + + @Column(name = "access_token") + var accessToken: String? = null + + @Column(name = "refresh_token") + var refreshToken: String? = null + + @Column(name = "expires_in") + var expiresIn: Int? = null + + @Column(name = "expires_at") + var expiresAt: Instant? = null + + @Column(name = "token_type") + var tokenType: String? = null + + // The version to be appended to ID for RESET of a user + // This should be updated whenever the user is RESET. + // By default this is null for backwards compatibility + @Column(name = "version") + var version: String? = null + + // The number of times a user has been reset + @Column(name = "times_reset") + var timesReset: Long = 0 +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/UploadResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt similarity index 68% rename from authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/UploadResourceEnhancer.kt rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt index 0af38ec4..c79dd98d 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/UploadResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt @@ -7,14 +7,22 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.KotlinModule import okhttp3.OkHttpClient import org.glassfish.jersey.internal.inject.AbstractBinder +import org.glassfish.jersey.internal.inject.PerLookup import org.glassfish.jersey.server.ResourceConfig import org.radarbase.authorizer.Config +import org.radarbase.authorizer.DatabaseConfig +import org.radarbase.authorizer.api.RestSourceUserMapper +import org.radarbase.authorizer.doa.RestSourceUserRepository +import org.radarbase.authorizer.doa.RestSourceUserRepositoryImpl import org.radarbase.jersey.config.ConfigLoader import org.radarbase.jersey.config.JerseyResourceEnhancer import java.util.concurrent.TimeUnit +import javax.inject.Singleton +import javax.persistence.EntityManager +import javax.persistence.EntityManagerFactory import javax.ws.rs.ext.ContextResolver -class UploadResourceEnhancer(private val config: Config): JerseyResourceEnhancer { +class AuthorizerResourceEnhancer(private val config: Config): JerseyResourceEnhancer { private val client = OkHttpClient().newBuilder() .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) @@ -36,7 +44,7 @@ class UploadResourceEnhancer(private val config: Config): JerseyResourceEnhancer "org.radarbase.authorizer.exception", "org.radarbase.authorizer.filter", "org.radarbase.authorizer.lifecycle", - "org.radarbase.authorizer.resource") + "org.radarbase.authorizer.resources") override fun ResourceConfig.enhance() { register(ContextResolver { OBJECT_MAPPER }) @@ -47,6 +55,9 @@ class UploadResourceEnhancer(private val config: Config): JerseyResourceEnhancer bind(config) .to(Config::class.java) + bind(config.database) + .to(DatabaseConfig::class.java) + bind(client) .to(OkHttpClient::class.java) @@ -57,22 +68,22 @@ class UploadResourceEnhancer(private val config: Config): JerseyResourceEnhancer // .to(CallbackManager::class.java) // .`in`(Singleton::class.java) // -// // Bind factories. -// bindFactory(DoaEntityManagerFactoryFactory::class.java) -// .to(EntityManagerFactory::class.java) -// .`in`(Singleton::class.java) -// -// bindFactory(DoaEntityManagerFactory::class.java) -// .to(EntityManager::class.java) -// .`in`(PerLookup::class.java) + // Bind factories. + bindFactory(DoaEntityManagerFactoryFactory::class.java) + .to(EntityManagerFactory::class.java) + .`in`(Singleton::class.java) -// bind(RecordMapperImpl::class.java) -// .to(RecordMapper::class.java) -// .`in`(Singleton::class.java) -// -// bind(SourceTypeMapperImpl::class.java) -// .to(SourceTypeMapper::class.java) -// .`in`(Singleton::class.java) + bindFactory(DoaEntityManagerFactory::class.java) + .to(EntityManager::class.java) + .`in`(PerLookup::class.java) + + bind(RestSourceUserMapper::class.java) + .to(RestSourceUserMapper::class.java) + .`in`(Singleton::class.java) + + bind(RestSourceUserRepositoryImpl::class.java) + .to(RestSourceUserRepository::class.java) + .`in`(Singleton::class.java) // // bind(RecordRepositoryImpl::class.java) // .to(RecordRepository::class.java) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt new file mode 100644 index 00000000..7d651dee --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt @@ -0,0 +1,47 @@ +/* + * + * * Copyright 2019 The Hyve + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * * + * + */ + +package org.radarbase.authorizer.inject + +import org.glassfish.jersey.internal.inject.DisposableSupplier +import org.slf4j.LoggerFactory +import javax.persistence.EntityManager +import javax.persistence.EntityManagerFactory +import javax.ws.rs.core.Context + +class DoaEntityManagerFactory( + @Context private val emf: EntityManagerFactory +) : DisposableSupplier { + + override fun get(): EntityManager { + logger.debug("Creating EntityManager...") + return emf.createEntityManager() + } + + override fun dispose(instance: EntityManager?) { + instance?.let { + logger.debug("Disposing EntityManager") + it.close() + } + } + + companion object { + private val logger = LoggerFactory.getLogger(DoaEntityManagerFactory::class.java) + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt new file mode 100644 index 00000000..20d09cdb --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt @@ -0,0 +1,73 @@ +/* + * + * * Copyright 2019 The Hyve + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * * + * + */ + +package org.radarbase.authorizer.inject + +import liquibase.Liquibase +import liquibase.database.DatabaseFactory +import liquibase.database.jvm.JdbcConnection +import liquibase.exception.LiquibaseException +import liquibase.resource.ClassLoaderResourceAccessor +import org.glassfish.jersey.internal.inject.DisposableSupplier +import org.hibernate.internal.SessionImpl +import org.radarbase.authorizer.DatabaseConfig +import org.slf4j.LoggerFactory +import javax.persistence.EntityManagerFactory +import javax.persistence.Persistence +import javax.ws.rs.core.Context + +class DoaEntityManagerFactoryFactory(@Context config: DatabaseConfig) : DisposableSupplier { + @Suppress("UNCHECKED_CAST") + private val configMap = ( + mapOf( + "javax.persistence.jdbc.driver" to config.jdbcDriver, + "javax.persistence.jdbc.url" to config.jdbcUrl, + "javax.persistence.jdbc.user" to config.jdbcUser, + "javax.persistence.jdbc.password" to config.jdbcPassword, + "hibernate.dialect" to config.hibernateDialect) + + (config.additionalPersistenceConfig ?: emptyMap())) + .filterValues { it != null } as Map + + override fun get(): EntityManagerFactory { + logger.info("Initializing EntityManagerFactory with config: $configMap") + return Persistence.createEntityManagerFactory("org.radarbase.authorizer.doa", configMap) + .also { initializeDatabase(it) } + } + + private fun initializeDatabase(emf: EntityManagerFactory) { + logger.info("Initializing Liquibase") + val connection = emf.createEntityManager().unwrap(SessionImpl::class.java).connection() + try { + val database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(JdbcConnection(connection)) + val liquibase = Liquibase("db/changelog/changes/db.changelog-master.xml", ClassLoaderResourceAccessor(), database) + liquibase.update("test") + } catch (e: LiquibaseException) { + logger.error("Failed to initialize database", e) + } + } + + override fun dispose(instance: EntityManagerFactory?) { + logger.info("Disposing EntityManagerFactory") + instance?.close() + } + + companion object { + private val logger = LoggerFactory.getLogger(DoaEntityManagerFactoryFactory::class.java) + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt index 58dd2f22..334287fe 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt @@ -34,7 +34,7 @@ import javax.inject.Singleton /** This binder needs to register all non-Jersey classes, otherwise initialization fails. */ class ManagementPortalEnhancerFactory(private val config: Config) : EnhancerFactory { override fun createEnhancers(): List = listOf( - UploadResourceEnhancer(config), + AuthorizerResourceEnhancer(config), MPClientResourceEnhancer(), ConfigLoader.Enhancers.radar(AuthConfig( managementPortalUrl = config.auth.managementPortalUrl, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt new file mode 100644 index 00000000..8c923ed5 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt @@ -0,0 +1,45 @@ +package org.radarbase.authorizer.resources + +import org.radarbase.authorizer.api.Project +import org.radarbase.authorizer.api.ProjectList +import org.radarbase.authorizer.api.UserList +import org.radarbase.authorizer.service.RadarProjectService +import org.radarbase.jersey.auth.Auth +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission +import org.radarcns.auth.authorization.Permission +import javax.annotation.Resource +import javax.inject.Singleton +import javax.ws.rs.* +import javax.ws.rs.core.Context +import javax.ws.rs.core.MediaType + +@Path("projects") +@Authenticated +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Resource +@Singleton +class ProjectResource( + @Context private val projectService: RadarProjectService, + @Context private val auth: Auth +) { + + @GET + @NeedsPermission(Permission.Entity.PROJECT, Permission.Operation.READ) + fun projects() = ProjectList(projectService.userProjects(auth)) + + @GET + @Path("{projectId}/users") + @NeedsPermission(Permission.Entity.PROJECT, Permission.Operation.READ, "projectId") + fun users(@PathParam("projectId") projectId: String): UserList { + return UserList(projectService.projectUsers(projectId)) + } + + @GET + @Path("{projectId}") + @NeedsPermission(Permission.Entity.PROJECT, Permission.Operation.READ, "projectId") + fun project(@PathParam("projectId") projectId: String): Project { + return projectService.project(projectId) + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt new file mode 100644 index 00000000..ae9bea65 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt @@ -0,0 +1,51 @@ +package org.radarbase.authorizer.resources + +import org.radarbase.authorizer.api.Page +import org.radarbase.authorizer.api.RestSourceUserMapper +import org.radarbase.authorizer.api.RestSourceUsers +import org.radarbase.authorizer.doa.RestSourceUserRepository +import org.radarbase.jersey.auth.Auth +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.exception.HttpBadRequestException +import org.radarcns.auth.authorization.Permission +import javax.annotation.Resource +import javax.inject.Singleton +import javax.ws.rs.* +import javax.ws.rs.core.Context +import javax.ws.rs.core.MediaType + +@Path("users") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Resource +@Authenticated +@Singleton +class RestSourceUserResource( + @Context private val userRepository: RestSourceUserRepository, + @Context private val userMapper: RestSourceUserMapper, + @Context private val auth: Auth +) { + + @GET + fun query( + @QueryParam("projectId") projectId: String?, + @QueryParam("userId") userId: String?, + @QueryParam("size") pageSize: Int?, + @DefaultValue("1") @QueryParam("page") pageNumber: Int, + @QueryParam("sourceType") sourceType: String?, + @QueryParam("status") status: String?): RestSourceUsers { +// projectId +// ?: throw HttpBadRequestException("missing_project", "Required project ID not provided.") + +// if (userId != null) { +// auth.checkPermissionOnSubject(Permission.SUBJECT_READ, projectId, userId) +// } else { +// auth.checkPermissionOnProject(Permission.PROJECT_READ, projectId) +// } + + val queryPage = Page(pageNumber = pageNumber, pageSize = pageSize) + val (records, page) = userRepository.query(queryPage) + + return userMapper.fromRestSourceUsers(records, page) + } +} diff --git a/authorizer-app-backend/src/main/resources/META-INF/persistence.xml b/authorizer-app-backend/src/main/resources/META-INF/persistence.xml new file mode 100644 index 00000000..e9733401 --- /dev/null +++ b/authorizer-app-backend/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,45 @@ + + + + + org.radarbase.authorizer.doa.entity.RestSourceUser + + + + + + + + + + + + + + + + + diff --git a/authorizer-app-backend/src/main/resources/db/changelog/changes/db.changelog-master.xml b/authorizer-app-backend/src/main/resources/db/changelog/changes/db.changelog-master.xml new file mode 100644 index 00000000..4b039aba --- /dev/null +++ b/authorizer-app-backend/src/main/resources/db/changelog/changes/db.changelog-master.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/authorizer-app-backend/src/main/resources/db/changelog/db.changelog-master.yaml b/authorizer-app-backend/src/main/resources/db/changelog/db.changelog-master.yaml deleted file mode 100644 index 99f8606c..00000000 --- a/authorizer-app-backend/src/main/resources/db/changelog/db.changelog-master.yaml +++ /dev/null @@ -1,7 +0,0 @@ -databaseChangeLog: - - include: - file: db/changelog/changes/00000000000000_initial_schema.xml - - include: - file: db/changelog/changes/00000000000001_update_schema.xml - - include: - file: db/changelog/changes/00000000000002_update_schema.xml \ No newline at end of file From 3fdd5b644776f96d6760b76206516e47c5389ccd Mon Sep 17 00:00:00 2001 From: nivethika Date: Fri, 28 Aug 2020 11:09:09 +0200 Subject: [PATCH 05/80] fix sequence generator init --- .../org/radarbase/authorizer/doa/AbstractJpaPersistable.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt index cd8020c4..de78cb1d 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt @@ -19,14 +19,13 @@ package org.radarbase.authorizer.doa -import javax.persistence.GeneratedValue -import javax.persistence.Id -import javax.persistence.MappedSuperclass +import javax.persistence.* @MappedSuperclass abstract class AbstractJpaPersistable { @Id - @GeneratedValue + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") + @SequenceGenerator(name = "sequenceGenerator", initialValue = 1000) var id: T? = null override fun equals(other: Any?): Boolean { From fe01ef2c250ec8b9a7c4be1c48dcedc2095ca684 Mon Sep 17 00:00:00 2001 From: nivethika Date: Fri, 28 Aug 2020 18:03:12 +0200 Subject: [PATCH 06/80] add source-client resource and RestSourceUser resource with 3 APIs migrated --- .../java/org/radarbase/authorizer/Config.kt | 30 +++- .../authorizer/api/ApiDeclarations.kt | 100 ++++++------ .../authorizer/api/RestSourceClientMapper.kt | 17 +- .../api/RestSourceClientMapperImpl.kt | 15 -- .../authorizer/api/RestSourceUserMapper.kt | 4 +- .../authorizer/config/RestSourceClients.java | 74 ++++----- .../doa/RestSourceUserRepository.kt | 9 +- .../doa/RestSourceUserRepositoryImpl.kt | 55 ++++++- .../authorizer/doa/entity/RestSourceUser.kt | 8 +- .../inject/AuthorizerResourceEnhancer.kt | 153 +++++++++--------- .../inject/DoaEntityManagerFactory.kt | 2 +- .../resources/RestSourceUserResource.kt | 60 ++++++- .../resources/SourceClientResource.kt | 41 +++++ .../service/RestSourceAuthorizationService.kt | 87 ++++++++++ .../service/RestSourceClientService.kt | 5 - .../managementportal/oauth_client_details.csv | 2 +- 16 files changed, 454 insertions(+), 208 deletions(-) delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapperImpl.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.kt diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt index 74dd1c03..f03456e8 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt @@ -7,10 +7,10 @@ import java.net.URI data class Config( val service: AuthorizerServiceConfig = AuthorizerServiceConfig(), val auth: AuthConfig = AuthConfig(), - val database: DatabaseConfig = DatabaseConfig() + val database: DatabaseConfig = DatabaseConfig(), + val restSourceClients: List = emptyList() ) - data class AuthorizerServiceConfig( var baseUri: URI = URI.create("http://0.0.0.0:8080/rest-sources/backend/"), var advertisedBaseUri: URI? = null, @@ -31,10 +31,24 @@ data class AuthConfig( ) data class DatabaseConfig( - var jdbcDriver: String? = "org.h2.Driver", - var jdbcUrl: String? = null, - var jdbcUser: String? = null, - var jdbcPassword: String? = null, - var hibernateDialect: String = "org.hibernate.dialect.PostgreSQLDialect", - var additionalPersistenceConfig: Map? = null + val jdbcDriver: String? = "org.h2.Driver", + val jdbcUrl: String? = null, + val jdbcUser: String? = null, + val jdbcPassword: String? = null, + val hibernateDialect: String = "org.hibernate.dialect.PostgreSQLDialect", + val additionalPersistenceConfig: Map? = null +) + +data class RestSourceClient( + val sourceType: String, + val authorizationEndpoint: String, + val tokenEndpoint: String, + val clientId: String, + val clientSecret: String, + val grantType: String? = null, + val scope: String? = null +) + +data class RestSourceClients( + val clients: List ) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt index c07af74b..6b5a13e0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt @@ -7,73 +7,73 @@ import java.time.Instant @JsonIgnoreProperties(ignoreUnknown = true) -data class Oauth2AccessToken( - @JsonProperty("access_token") var accessToken: String? = null, - @JsonProperty("refresh_token") var refreshToken: String? = null, - @JsonProperty("expires_in") var expiresIn: Int? = null, - @JsonProperty("token_type") var tokenType: String? = null, - @JsonProperty("user_id") var externalUserId: String? = null) +data class RestOauth2AccessToken( + @JsonProperty("access_token") var accessToken: String, + @JsonProperty("refresh_token") var refreshToken: String? = null, + @JsonProperty("expires_in") var expiresIn: Int = 0, + @JsonProperty("token_type") var tokenType: String? = null, + @JsonProperty("user_id") var externalUserId: String? = null) -data class RestSourceClientDetailsDTO( - var sourceType: String, - var authorizationEndpoint: String, - var tokenEndpoint: String, - var grantType: String, - var clientId: String, - var scope: String? = null +data class ShareableClientDetail( + val sourceType: String, + val authorizationEndpoint: String, + val tokenEndpoint: String, + val grantType: String?, + val clientId: String, + val scope: String? ) -data class RestSourceClients( - var sourceClients: List +data class ShareableClientDetails( + val sourceClients: List ) class RestSourceUserDTO( - var id: String? = null, - var projectId: String, - var userId: String, - var sourceId: String? = null, - var externalUserId: String? = null, - var startDate: Instant, - var endDate: Instant? = null, - var sourceType: String? = null, - var isAuthorized: Boolean = false, - var version: String? = null, - var timesReset: Long = 0) : Serializable { - companion object { - private const val serialVersionUID = 1L - } + val id: String?, + val projectId: String?, + val userId: String?, + val sourceId: String, + val externalUserId: String, + val startDate: Instant, + val endDate: Instant? = null, + val sourceType: String, + val isAuthorized: Boolean = false, + val version: String? = null, + val timesReset: Long = 0) : Serializable { + companion object { + private const val serialVersionUID = 1L + } } data class RestSourceUsers( - var users: List + val users: List ) class TokenDTO( - var accessToken: String? = null, - var expiresAt: Instant? = null + val accessToken: String? = null, + val expiresAt: Instant? = null ) data class Page( - val pageNumber: Int = 1, - val pageSize: Int? = null, - val totalElements: Long? = null) { - val offset: Int - get() = (this.pageNumber - 1) * this.pageSize!! + val pageNumber: Int = 1, + val pageSize: Int? = null, + val totalElements: Long? = null) { + val offset: Int + get() = (this.pageNumber - 1) * this.pageSize!! - fun createValid(maximum: Int? = null): Page { - val imposedNumber = pageNumber.coerceAtLeast(1) + fun createValid(maximum: Int? = null): Page { + val imposedNumber = pageNumber.coerceAtLeast(1) - val imposedSize = if (maximum != null) { - require(maximum >= 1) { "Maximum page size should be at least 1" } - pageSize?.coerceAtLeast(1)?.coerceAtMost(maximum) ?: maximum - } else { - pageSize?.coerceAtLeast(1) - } - return if (imposedNumber == pageNumber && imposedSize == pageSize) { - this - } else { - copy(pageNumber = imposedNumber, pageSize = imposedSize) - } + val imposedSize = if (maximum != null) { + require(maximum >= 1) { "Maximum page size should be at least 1" } + pageSize?.coerceAtLeast(1)?.coerceAtMost(maximum) ?: maximum + } else { + pageSize?.coerceAtLeast(1) + } + return if (imposedNumber == pageNumber && imposedSize == pageSize) { + this + } else { + copy(pageNumber = imposedNumber, pageSize = imposedSize) } + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt index af68c056..0c563484 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt @@ -1,7 +1,18 @@ package org.radarbase.authorizer.api -import org.radarbase.authorizer.config.RestSourceClientConfig +import org.radarbase.authorizer.RestSourceClient -interface RestSourceClientMapper { - fun fromRestSourceClientConfig(config: RestSourceClientConfig): RestSourceClientDetailsDTO +class RestSourceClientMapper { + fun fromSourceClientConfig(client: RestSourceClient)= ShareableClientDetail( + clientId = client.clientId, + sourceType = client.sourceType, + scope = client.scope, + authorizationEndpoint = client.authorizationEndpoint, + tokenEndpoint = client.tokenEndpoint, + grantType = client.grantType, + ) + + fun fromSourceClientConfigs(clientConfigs: List) = ShareableClientDetails( + sourceClients = clientConfigs.map(::fromSourceClientConfig) + ) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapperImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapperImpl.kt deleted file mode 100644 index 1588b1ca..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapperImpl.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.radarbase.authorizer.api - -import org.radarbase.authorizer.config.RestSourceClientConfig - -class RestSourceClientMapperImpl : RestSourceClientMapper { - - override fun fromRestSourceClientConfig(config: RestSourceClientConfig) = RestSourceClientDetailsDTO ( - authorizationEndpoint = config.authorizationEndpoint, - sourceType = config.sourceType, - grantType = config.grantType, - clientId = config.clientId, - scope = config.scope, - tokenEndpoint = config.tokenEndpoint - ) -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt index 290db051..cc58ff5e 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt @@ -4,7 +4,7 @@ import org.radarbase.authorizer.doa.entity.RestSourceUser class RestSourceUserMapper { - fun fromRestSourceUser(user: RestSourceUser) = RestSourceUserDTO( + fun fromEntity(user: RestSourceUser) = RestSourceUserDTO( id = user.id.toString(), projectId = user.projectId, userId = user.userId, @@ -19,7 +19,7 @@ class RestSourceUserMapper { ) fun fromRestSourceUsers(records: List, page: Page?) = RestSourceUsers( - users = records.map(::fromRestSourceUser) + users = records.map(::fromEntity) ) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClients.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClients.java index 4e620680..d66152a9 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClients.java +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClients.java @@ -1,37 +1,37 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.config; - -import java.util.List; - - -public class RestSourceClients { - - private List restSourceClients; - - public List getRestSourceClients() { - return restSourceClients; - } - - public RestSourceClients restSourceClients(List restSourceClients) { - this.restSourceClients = restSourceClients; - return this; - } -} +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer.config; +// +//import java.util.List; +// +// +//public class RestSourceClients { +// +// private List restSourceClients; +// +// public List getRestSourceClients() { +// return restSourceClients; +// } +// +// public RestSourceClients restSourceClients(List restSourceClients) { +// this.restSourceClients = restSourceClients; +// return this; +// } +//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index f6e4559f..ed37e3c3 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -1,14 +1,17 @@ package org.radarbase.authorizer.doa import org.radarbase.authorizer.api.Page +import org.radarbase.authorizer.api.RestOauth2AccessToken +import org.radarbase.authorizer.api.RestSourceUserDTO import org.radarbase.authorizer.doa.entity.RestSourceUser interface RestSourceUserRepository { -// fun create(user: RestSourceUser): RestSourceUser -// fun read(id: Long): RestSourceUser? -// fun update(user: RestSourceUser): RestSourceUser + fun create(user: RestOauth2AccessToken, sourceType: String): RestSourceUser + fun read(id: Long): RestSourceUser? + fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser fun query(page: Page, sourceType: String? = null, externalUserId: String? = null): Pair, Page> +// fun findByExtenalId(sourceType: String, externalUserId: String) : RestSourceUser? // fun findAllBySourceType(sourceType: String?): List // fun delete(user: RestSourceUser) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index e0517de9..32282f47 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -1,17 +1,63 @@ package org.radarbase.authorizer.doa import org.radarbase.authorizer.api.Page +import org.radarbase.authorizer.api.RestOauth2AccessToken +import org.radarbase.authorizer.api.RestSourceUserDTO import org.radarbase.authorizer.doa.entity.RestSourceUser +import org.radarbase.jersey.exception.HttpBadGatewayException +import java.time.Duration +import java.time.Instant import javax.inject.Provider import javax.persistence.EntityManager +import javax.persistence.Transient import javax.ws.rs.core.Context class RestSourceUserRepositoryImpl( @Context private var em: Provider ) : RestSourceUserRepository { -// override fun create(user: RestSourceUser): RestSourceUser { -// TODO("Not yet implemented") -// } + + override fun create(token: RestOauth2AccessToken, sourceType: String): RestSourceUser = em.get().createTransaction { + val externalUserId = token.externalUserId ?: throw HttpBadGatewayException("Could not get externalId from token") + + val queryString = "SELECT u FROM RestSourceUser u where u.sourceType = :sourceType AND u.externalUserId = :externalUserId" + val existingUser = createQuery(queryString, RestSourceUser::class.java) + .setParameter("sourceType", sourceType) + .setParameter("externalUserId", externalUserId) + .resultList.firstOrNull() + + if(existingUser == null) { + RestSourceUser().apply { + this.authorized = true + this.externalUserId = externalUserId + this.sourceType = sourceType + this.startDate = Instant.now() + this.accessToken = token.accessToken + this.refreshToken = token.refreshToken + this.expiresIn = token.expiresIn + this.expiresAt = Instant.now().plusSeconds(token.expiresIn.toLong()).minus(expiryTimeMargin) + }.also { persist(it) } + } else { + existingUser.apply { + this.accessToken = token.accessToken + this.refreshToken = token.refreshToken + this.expiresIn = token.expiresIn + this.expiresAt = Instant.now().plusSeconds(token.expiresIn.toLong()).minus(expiryTimeMargin) + }.also { merge(it) } + } + } + + override fun read(id: Long): RestSourceUser? = em.get().transact { find(RestSourceUser::class.java, id) } + + override fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser = em.get().createTransaction { + existingUser.apply { + this.projectId = user.projectId + this.userId = user.userId + this.sourceId = user.sourceId + this.startDate = user.startDate + this.endDate = user.endDate + }.also { merge(it) } + } + override fun query(page: Page, sourceType: String?, externalUserId: String?): Pair, Page> { var queryString = "SELECT u FROM RestSourceUser u" @@ -47,4 +93,7 @@ class RestSourceUserRepositoryImpl( } } + companion object { + private val expiryTimeMargin = Duration.ofMinutes(5) + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt index afd59ec8..3a0b33d0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt @@ -12,23 +12,21 @@ import javax.persistence.Transient @Entity @Table(name = "rest_source_user") class RestSourceUser : AbstractJpaPersistable() { - @Transient - private val EXPIRY_TIME_MARGIN = Duration.ofMinutes(5) // Project ID to be used in org.radarcns.kafka.ObservationKey record keys @Column(name = "project_id") - lateinit var projectId: String + var projectId: String? = null // User ID to be used in org.radarcns.kafka.ObservationKey record keys @Column(name = "user_id") - lateinit var userId: String + var userId: String? = null // Source ID to be used in org.radarcns.kafka.ObservationKey record keys @Column(name = "source_id") var sourceId: String = UUID.randomUUID().toString() @Column(name = "source_type") - var sourceType: String? = null + lateinit var sourceType: String // Date from when to collect data. @Column(name = "start_date") diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt index c79dd98d..ab1ac239 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt @@ -11,9 +11,12 @@ import org.glassfish.jersey.internal.inject.PerLookup import org.glassfish.jersey.server.ResourceConfig import org.radarbase.authorizer.Config import org.radarbase.authorizer.DatabaseConfig +import org.radarbase.authorizer.RestSourceClients +import org.radarbase.authorizer.api.RestSourceClientMapper import org.radarbase.authorizer.api.RestSourceUserMapper import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.doa.RestSourceUserRepositoryImpl +import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.jersey.config.ConfigLoader import org.radarbase.jersey.config.JerseyResourceEnhancer import java.util.concurrent.TimeUnit @@ -22,83 +25,85 @@ import javax.persistence.EntityManager import javax.persistence.EntityManagerFactory import javax.ws.rs.ext.ContextResolver -class AuthorizerResourceEnhancer(private val config: Config): JerseyResourceEnhancer { - private val client = OkHttpClient().newBuilder() - .connectTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .build() - - override val classes: Array> get() { - return if (config.service.enableCors == true) { - arrayOf( - ConfigLoader.Filters.logResponse, - ConfigLoader.Filters.cors) - } else { - arrayOf( - ConfigLoader.Filters.logResponse) - } - } +class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnhancer { + private val client = OkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() - override val packages: Array = arrayOf( - "org.radarbase.authorizer.exception", - "org.radarbase.authorizer.filter", - "org.radarbase.authorizer.lifecycle", - "org.radarbase.authorizer.resources") + private val restSourceClients = RestSourceClients(config.restSourceClients) - override fun ResourceConfig.enhance() { - register(ContextResolver { OBJECT_MAPPER }) + override val classes: Array> + get() { + return if (config.service.enableCors == true) { + arrayOf( + ConfigLoader.Filters.logResponse, + ConfigLoader.Filters.cors) + } else { + arrayOf( + ConfigLoader.Filters.logResponse) + } } - override fun AbstractBinder.enhance() { - // Bind instances. These cannot use any injects themselves - bind(config) - .to(Config::class.java) - - bind(config.database) - .to(DatabaseConfig::class.java) - - bind(client) - .to(OkHttpClient::class.java) - - bind(OBJECT_MAPPER) - .to(ObjectMapper::class.java) - -// bind(QueuedCallbackManager::class.java) -// .to(CallbackManager::class.java) -// .`in`(Singleton::class.java) -// - // Bind factories. - bindFactory(DoaEntityManagerFactoryFactory::class.java) - .to(EntityManagerFactory::class.java) - .`in`(Singleton::class.java) - - bindFactory(DoaEntityManagerFactory::class.java) - .to(EntityManager::class.java) - .`in`(PerLookup::class.java) - - bind(RestSourceUserMapper::class.java) - .to(RestSourceUserMapper::class.java) - .`in`(Singleton::class.java) - - bind(RestSourceUserRepositoryImpl::class.java) - .to(RestSourceUserRepository::class.java) - .`in`(Singleton::class.java) -// -// bind(RecordRepositoryImpl::class.java) -// .to(RecordRepository::class.java) -// .`in`(Singleton::class.java) -// -// bind(SourceTypeRepositoryImpl::class.java) -// .to(SourceTypeRepository::class.java) -// .`in`(Singleton::class.java) - } + override val packages: Array = arrayOf( + "org.radarbase.authorizer.exception", + "org.radarbase.authorizer.resources") - companion object { - private val OBJECT_MAPPER: ObjectMapper = ObjectMapper() - .setSerializationInclusion(JsonInclude.Include.NON_NULL) - .registerModule(JavaTimeModule()) - .registerModule(KotlinModule()) - .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) - } + override fun ResourceConfig.enhance() { + register(ContextResolver { OBJECT_MAPPER }) + } + + override fun AbstractBinder.enhance() { + // Bind instances. These cannot use any injects themselves + bind(config) + .to(Config::class.java) + + bind(config.database) + .to(DatabaseConfig::class.java) + + bind(restSourceClients) + .to(RestSourceClients::class.java) + + bind(client) + .to(OkHttpClient::class.java) + + bind(OBJECT_MAPPER) + .to(ObjectMapper::class.java) + + // Bind factories. + bindFactory(DoaEntityManagerFactoryFactory::class.java) + .to(EntityManagerFactory::class.java) + .`in`(Singleton::class.java) + + bindFactory(DoaEntityManagerFactory::class.java) + .to(EntityManager::class.java) + .`in`(PerLookup::class.java) + + bind(RestSourceUserMapper::class.java) + .to(RestSourceUserMapper::class.java) + .`in`(Singleton::class.java) + + bind(RestSourceClientMapper::class.java) + .to(RestSourceClientMapper::class.java) + .`in`(Singleton::class.java) + + bind(RestSourceUserRepositoryImpl::class.java) + .to(RestSourceUserRepository::class.java) + .`in`(Singleton::class.java) + + bind(RestSourceAuthorizationService::class.java) + .to(RestSourceAuthorizationService::class.java) + .`in`(Singleton::class.java) + + + } + + companion object { + private val OBJECT_MAPPER: ObjectMapper = ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(JavaTimeModule()) + .registerModule(KotlinModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt index 7d651dee..99525564 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt @@ -1,6 +1,6 @@ /* * - * * Copyright 2019 The Hyve + * * Copyright 2020 The Hyve * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt index ae9bea65..c4b0e6c8 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt @@ -1,18 +1,29 @@ package org.radarbase.authorizer.resources import org.radarbase.authorizer.api.Page +import org.radarbase.authorizer.api.RestSourceUserDTO import org.radarbase.authorizer.api.RestSourceUserMapper import org.radarbase.authorizer.api.RestSourceUsers import org.radarbase.authorizer.doa.RestSourceUserRepository +import org.radarbase.authorizer.doa.entity.RestSourceUser +import org.radarbase.authorizer.logger +import org.radarbase.authorizer.service.RadarProjectService +import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.jersey.auth.Auth import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission import org.radarbase.jersey.exception.HttpBadRequestException +import org.radarbase.jersey.exception.HttpNotFoundException import org.radarcns.auth.authorization.Permission +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.net.URI import javax.annotation.Resource import javax.inject.Singleton import javax.ws.rs.* import javax.ws.rs.core.Context import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response @Path("users") @Produces(MediaType.APPLICATION_JSON) @@ -23,6 +34,8 @@ import javax.ws.rs.core.MediaType class RestSourceUserResource( @Context private val userRepository: RestSourceUserRepository, @Context private val userMapper: RestSourceUserMapper, + @Context private val authorizationService: RestSourceAuthorizationService, + @Context private val projectService: RadarProjectService, @Context private val auth: Auth ) { @@ -33,7 +46,7 @@ class RestSourceUserResource( @QueryParam("size") pageSize: Int?, @DefaultValue("1") @QueryParam("page") pageNumber: Int, @QueryParam("sourceType") sourceType: String?, - @QueryParam("status") status: String?): RestSourceUsers { + @QueryParam("externalId") externalId: String?): RestSourceUsers { // projectId // ?: throw HttpBadRequestException("missing_project", "Required project ID not provided.") @@ -48,4 +61,49 @@ class RestSourceUserResource( return userMapper.fromRestSourceUsers(records, page) } + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + fun create( + @FormParam("code") code: String, + @FormParam("state") state: String): Response { + logger.info("code $code state $state") + val accessToken = authorizationService.requestAccessToken(code, sourceType= state) + val user = userRepository.create(accessToken, state) + + return Response.created(URI("users/${user.id}")) + .entity(userMapper.fromEntity(user)) + .build() + } + + @POST + @Path("{id}") + fun update( + @PathParam("id") userId: Long, + user: RestSourceUserDTO, + @QueryParam("validate") validate: Boolean): RestSourceUserDTO { + val existingUser = validate(userId, user) + + val updatedUser = userRepository.update(existingUser, user) + return userMapper.fromEntity(updatedUser) + } + + fun validate(id: Long, user: RestSourceUserDTO) : RestSourceUser { + val existingUser = ensureUser(id) + val projectId = user.projectId ?: throw HttpBadRequestException("missing_project_id", "project cannot be empty") + val userId = user.userId ?: throw HttpBadRequestException("missing_user_id", "subject-id/user-id cannot be empty") + auth.checkPermissionOnSubject(Permission.SUBJECT_UPDATE, projectId, userId) + + projectService.projectUsers(projectId).find { it.id == userId } ?: throw HttpBadRequestException("user_not_found", "user $userId not found in project $projectId") + return existingUser + } + + fun ensureUser(userId: Long): RestSourceUser { + return userRepository.read(userId) + ?: throw HttpNotFoundException("user_not_found", "Rest-Source-User with ID $userId does not exist") + } + + companion object { + val logger: Logger = LoggerFactory.getLogger(RestSourceUserResource::class.java) + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt new file mode 100644 index 00000000..886ae782 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt @@ -0,0 +1,41 @@ +package org.radarbase.authorizer.resources + +import org.radarbase.authorizer.RestSourceClients +import org.radarbase.authorizer.api.* +import org.radarbase.jersey.auth.Auth +import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.exception.HttpNotFoundException +import javax.annotation.Resource +import javax.inject.Singleton +import javax.ws.rs.* +import javax.ws.rs.core.Context +import javax.ws.rs.core.MediaType + +@Path("source-clients") +@Produces(MediaType.APPLICATION_JSON) +@Resource +@Authenticated +@Singleton +class SourceClientResource( + @Context private val restSourceClients: RestSourceClients, + @Context private val clientMapper : RestSourceClientMapper, + @Context private val auth: Auth +) { + + private val sourceTypes = restSourceClients.clients.map { it.sourceType } + + private val sharableClientDetails = clientMapper.fromSourceClientConfigs(restSourceClients.clients) + + @GET + fun clients(): ShareableClientDetails = sharableClientDetails + + @GET + @Path("type") + fun types(): List = sourceTypes + + @GET + @Path("{type}") + fun client(@PathParam("type") type: String): ShareableClientDetail { + return sharableClientDetails.sourceClients.find { it.sourceType == type } ?: throw HttpNotFoundException("source-type-not-found", "Client with source-type $type is not configured") + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt new file mode 100644 index 00000000..1526ee1e --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt @@ -0,0 +1,87 @@ +package org.radarbase.authorizer.service + +import com.fasterxml.jackson.databind.ObjectMapper +import okhttp3.Credentials +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import org.radarbase.authorizer.RestSourceClients +import org.radarbase.authorizer.api.RestOauth2AccessToken +import org.radarbase.authorizer.doa.RestSourceUserRepository +import org.radarbase.jersey.exception.HttpBadGatewayException +import org.radarbase.jersey.exception.HttpBadRequestException +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import javax.ws.rs.core.Context + +class RestSourceAuthorizationService( + @Context private val restSourceClients: RestSourceClients, + @Context private val httpClient: OkHttpClient, + @Context private val objectMapper: ObjectMapper +) { + + private val configMap = restSourceClients.clients.map { it.sourceType to it }.toMap() + + fun requestAccessToken(code: String, sourceType: String): RestOauth2AccessToken { + val authorizationConfig = configMap[sourceType] + ?: throw HttpBadRequestException("client-config-not-found", "Cannot find client configurations for source-type $sourceType") + + val form = FormBody.Builder() + .add("code", code) + .add("grant_type", "authorization_code") + .add("client_id", authorizationConfig.clientId) + .build(); + logger.info("Requesting access token with authorization code") + return objectMapper.readValue(execute(post(form, sourceType)), RestOauth2AccessToken::class.java) + } + + + fun refreshToken(refreshToken: String, sourceType: String): RestOauth2AccessToken { + val form = FormBody.Builder() + .add("grant_type", "refresh_token") + .add("refresh_token", refreshToken) + .build(); + logger.info("Requesting to refreshToken") + return objectMapper.readValue(execute(post(form, sourceType)), RestOauth2AccessToken::class.java) + } + + + fun revokeToken(accessToken: String, sourceType: String): Boolean { + val form = FormBody.Builder().add("token", accessToken).build(); + logger.info("Requesting to revoke access token"); + + httpClient.newCall(post(form, sourceType)).execute().use { response -> + return response.isSuccessful + } + + } + + private fun post(form: FormBody, sourceType: String): Request { + val authorizationConfig = configMap[sourceType] + ?: throw HttpBadRequestException("client-config-not-found", "Cannot find client configurations for source-type $sourceType") + + return Request.Builder().apply { + url(authorizationConfig.tokenEndpoint) + post(form) + header("Authorization", Credentials.basic(authorizationConfig.clientId, authorizationConfig.clientSecret)) + header("Content-Type", "application/x-www-form-urlencoded") + header("Accept", "application/json") + }.build() + } + + private fun execute(request: Request): String { + return httpClient.newCall(request).execute().use { response -> + if (response.isSuccessful) { + response.body?.string() + ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") + } else { + logger.error("Cannot connect to managementportal ", response.code) + throw HttpBadGatewayException("Cannot connect to managementportal : Response-code ${response.code}") + } + } + } + + companion object { + val logger: Logger = LoggerFactory.getLogger(RestSourceAuthorizationService::class.java) + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.kt deleted file mode 100644 index 8448f999..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.radarbase.authorizer.service - -class RestSourceClientService { - -} diff --git a/docker/etc/managementportal/oauth_client_details.csv b/docker/etc/managementportal/oauth_client_details.csv index 66d709c0..4aa2254f 100644 --- a/docker/etc/managementportal/oauth_client_details.csv +++ b/docker/etc/managementportal/oauth_client_details.csv @@ -6,4 +6,4 @@ radar_restapi;res_ManagementPortal;secret;SUBJECT.READ,PROJECT.READ,SOURCE.READ, 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,MEASUREMENT.READ;client_credentials;;;43200;259200;{}; radar_rest_sources_auth_backend;res_ManagementPortal;secret;SUBJECT.READ,PROJECT.READ;client_credentials;;;43200;259200;{}; -radar_rest_sources_authorizer;res_restAuthorizer;;SOURCETYPE.READ,PROJECT.READ,SUBJECT.READ;authorization_code;http://localhost:8080/rest-sources/authorizer/login;3600;78000;; +radar_rest_sources_authorizer;res_restAuthorizer;;SOURCETYPE.READ,PROJECT.READ,SUBJECT.READ,SUBJECT.UPDATE;authorization_code;http://localhost:8080/login;3600;78000;; From 213a27f2c17dddc6c9a412559073752a24ef4ab4 Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 31 Aug 2020 11:23:57 +0200 Subject: [PATCH 07/80] add apis to read and delete user by id --- .../doa/RestSourceUserRepository.kt | 2 +- .../doa/RestSourceUserRepositoryImpl.kt | 5 ++++ .../resources/RestSourceUserResource.kt | 26 ++++++++++++++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index ed37e3c3..b625baab 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -13,5 +13,5 @@ interface RestSourceUserRepository { fun query(page: Page, sourceType: String? = null, externalUserId: String? = null): Pair, Page> // fun findByExtenalId(sourceType: String, externalUserId: String) : RestSourceUser? // fun findAllBySourceType(sourceType: String?): List -// fun delete(user: RestSourceUser) + fun delete(user: RestSourceUser) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index 32282f47..3519add1 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -93,6 +93,11 @@ class RestSourceUserRepositoryImpl( } } + override fun delete(user: RestSourceUser) = em.get().transact { + remove(merge(user)) + } + + companion object { private val expiryTimeMargin = Duration.ofMinutes(5) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt index c4b0e6c8..a604dd54 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt @@ -82,17 +82,37 @@ class RestSourceUserResource( @PathParam("id") userId: Long, user: RestSourceUserDTO, @QueryParam("validate") validate: Boolean): RestSourceUserDTO { - val existingUser = validate(userId, user) + val existingUser = validate(userId, user, Permission.SUBJECT_UPDATE) val updatedUser = userRepository.update(existingUser, user) return userMapper.fromEntity(updatedUser) } - fun validate(id: Long, user: RestSourceUserDTO) : RestSourceUser { + @GET + @Path("{id}") + fun readUser(@PathParam("id") userId: Long) : RestSourceUserDTO { + val user = ensureUser(userId) + auth.checkPermissionOnSubject(Permission.SUBJECT_READ, user.projectId, user.userId) + return userMapper.fromEntity(user) + } + + @DELETE + @Path("{id}") + fun deleteUser(@PathParam("id") userId: Long) : Response { + val user = ensureUser(userId) + auth.checkPermissionOnSubject(Permission.SUBJECT_UPDATE, user.projectId, user.userId) + if (user.accessToken != null) { + authorizationService.revokeToken(user.accessToken!!, user.sourceType) + } + userRepository.delete(user) + return Response.noContent().header("user-removed", userId).build() + } + + private fun validate(id: Long, user: RestSourceUserDTO, permission: Permission) : RestSourceUser { val existingUser = ensureUser(id) val projectId = user.projectId ?: throw HttpBadRequestException("missing_project_id", "project cannot be empty") val userId = user.userId ?: throw HttpBadRequestException("missing_user_id", "subject-id/user-id cannot be empty") - auth.checkPermissionOnSubject(Permission.SUBJECT_UPDATE, projectId, userId) + auth.checkPermissionOnSubject(permission, projectId, userId) projectService.projectUsers(projectId).find { it.id == userId } ?: throw HttpBadRequestException("user_not_found", "user $userId not found in project $projectId") return existingUser From 9b43550b2a540df03f57d39292f8850ca943c8a2 Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 31 Aug 2020 13:22:06 +0200 Subject: [PATCH 08/80] add temp volume to postgres --- docker-compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 24f89154..b3d43d00 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,7 +30,9 @@ services: radarbase-postgresql: image: radarbase/radarbase-postgres:latest # ports: -# - "5432:5432" +# - "5434:5432" +# volumes: +# - "./data/:/var/lib/postgresql/data/" environment: - POSTGRES_USER=radarcns - POSTGRES_PASSWORD=radarcns From 2231638e9c5d2048df56436004339da436fd3b7d Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 31 Aug 2020 14:08:26 +0200 Subject: [PATCH 09/80] use correct seq name and remove abstract interface --- .../authorizer/doa/AbstractJpaPersistable.kt | 44 ------------------- .../authorizer/doa/entity/RestSourceUser.kt | 27 +++++++++--- 2 files changed, 21 insertions(+), 50 deletions(-) delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt deleted file mode 100644 index de78cb1d..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/AbstractJpaPersistable.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * - * * Copyright 2019 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.doa - -import javax.persistence.* - -@MappedSuperclass -abstract class AbstractJpaPersistable { - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") - @SequenceGenerator(name = "sequenceGenerator", initialValue = 1000) - var id: T? = null - - override fun equals(other: Any?): Boolean { - if (this === other) return true - - if (other == null || javaClass != other.javaClass) return false - - other as AbstractJpaPersistable<*> - - return id != null && id == other.id - } - - override fun hashCode(): Int = id.hashCode() - - override fun toString() = "Entity of type ${this.javaClass.name} with id: $id" -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt index 3a0b33d0..025cbec2 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt @@ -1,18 +1,18 @@ package org.radarbase.authorizer.doa.entity -import org.radarbase.authorizer.doa.AbstractJpaPersistable import java.time.Duration import java.time.Instant import java.util.* -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.Table -import javax.persistence.Transient +import javax.persistence.* @Entity @Table(name = "rest_source_user") -class RestSourceUser : AbstractJpaPersistable() { +class RestSourceUser { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") + @SequenceGenerator(name = "sequenceGenerator", sequenceName = "rest_source_user_id_seq", initialValue = 1, allocationSize = 1) + var id: Long? = null // Project ID to be used in org.radarcns.kafka.ObservationKey record keys @Column(name = "project_id") var projectId: String? = null @@ -66,4 +66,19 @@ class RestSourceUser : AbstractJpaPersistable() { // The number of times a user has been reset @Column(name = "times_reset") var timesReset: Long = 0 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + + if (other == null || javaClass != other.javaClass) return false + + other as RestSourceUser + + return id != null && id == other.id + } + + override fun hashCode(): Int = id.hashCode() + + override fun toString() = "Entity of type ${this.javaClass.name} with id: $id" + } From 4885ae0c4e2d11250ed57c7c53068d77e7cd7c8b Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 31 Aug 2020 14:09:22 +0200 Subject: [PATCH 10/80] remove migrated code --- .../config/AuthTokenValidatorConfig.java | 61 ----- .../authorizer/config/ConfigHelper.java | 44 ---- .../config/ManagementPortalProperties.java | 106 --------- .../RestSourceAuthorizerProperties.java | 100 -------- .../config/RestSourceClientConfig.java | 119 ---------- .../authorizer/config/RestSourceClients.java | 37 --- .../service/dto/managementportal/Project.java | 33 --- .../service/dto/managementportal/Subject.java | 40 ---- .../CachedManagementPortalClient.java | 223 ------------------ .../ManagementPortalClient.java | 18 -- .../validation/ManagementPortalValidator.java | 65 ----- .../authorizer/validation/Validator.java | 8 - .../exception/ValidationFailedException.java | 21 -- .../webapp/exception/BadGatewayException.java | 39 --- .../exception/InvalidSourceTypeException.java | 35 --- .../webapp/exception/NotFoundException.java | 32 --- .../webapp/exception/TokenException.java | 39 --- .../webapp/resource/SourceClientResource.java | 63 ----- 18 files changed, 1083 deletions(-) delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthTokenValidatorConfig.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ConfigHelper.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ManagementPortalProperties.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClientConfig.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClients.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Project.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Subject.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/Validator.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthTokenValidatorConfig.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthTokenValidatorConfig.java deleted file mode 100644 index 9c261f07..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthTokenValidatorConfig.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.radarbase.authorizer.config; - -import java.net.URI; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; - -public class AuthTokenValidatorConfig implements org.radarcns.auth.config.TokenValidatorConfig { - - private List publicKeyEndpoints = new LinkedList<>(); - - private String resourceName; - - private List publicKeys = new LinkedList<>(); - - @Override - public List getPublicKeyEndpoints() { - return publicKeyEndpoints; - } - - @Override - public String getResourceName() { - return resourceName; - } - - @Override - public List getPublicKeys() { - return publicKeys; - } - - public void setPublicKeyEndpoints(List publicKeyEndpoints) { - this.publicKeyEndpoints = publicKeyEndpoints; - } - - public void setResourceName(String resourceName) { - this.resourceName = resourceName; - } - - public void setPublicKeys(List publicKeys) { - this.publicKeys = publicKeys; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AuthTokenValidatorConfig that = (AuthTokenValidatorConfig) o; - return Objects.equals(publicKeyEndpoints, that.publicKeyEndpoints) && Objects - .equals(resourceName, that.resourceName) && Objects - .equals(publicKeys, that.publicKeys); - } - - @Override - public int hashCode() { - return Objects.hash(publicKeyEndpoints, resourceName, publicKeys); - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ConfigHelper.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ConfigHelper.java deleted file mode 100644 index 427b5325..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ConfigHelper.java +++ /dev/null @@ -1,44 +0,0 @@ -//package org.radarbase.authorizer.config; -// -//import com.fasterxml.jackson.core.type.TypeReference; -//import com.fasterxml.jackson.databind.DeserializationFeature; -//import com.fasterxml.jackson.databind.ObjectMapper; -//import com.fasterxml.jackson.databind.PropertyNamingStrategy; -//import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -//import com.fasterxml.jackson.dataformat.yaml.YAMLParser; -//import java.io.File; -//import java.io.IOException; -//import javax.naming.ConfigurationException; -//import javax.validation.constraints.NotNull; -//import org.slf4j.Logger; -//import org.slf4j.LoggerFactory; -// -//public class ConfigHelper { -// -// private static final Logger LOGGER = LoggerFactory.getLogger(ConfigHelper.class); -// -// public static T loadPropertiesFromFile(@NotNull String path, -// @NotNull TypeReference typeReference) -// throws ConfigurationException { -// LOGGER.info("Loading config from {}", path); -// YAMLFactory yamlFactory = new YAMLFactory(); -// try { -// YAMLParser yamlParser = yamlFactory.createParser(new File(path)); -// T properties = new ObjectMapper(yamlFactory) -// .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) -// .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) -// .readValue(yamlParser, typeReference); -// -// if (properties == null) { -// LOGGER.error("No valid configurations available on configured path. Please " -// + "check the syntax and file name"); -// throw new ConfigurationException( -// "No valid configs are provided" + "."); -// } -// return properties; -// } catch (IOException e) { -// LOGGER.error("Could not successfully read config file at {}", path); -// throw new ConfigurationException("Could not successfully read config file at " + path); -// } -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ManagementPortalProperties.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ManagementPortalProperties.java deleted file mode 100644 index e69bc8ba..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/ManagementPortalProperties.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.radarbase.authorizer.config; - -import java.util.Objects; -import javax.validation.constraints.NotNull; - -public class ManagementPortalProperties { - - @NotNull - private String baseUrl; - - @NotNull - private String projectsPath; - - @NotNull - private String subjectsPath; - - @NotNull - private String oauthClientId; - - @NotNull - private String oauthClientSecret; - - @NotNull - private String tokenPath; - - public String getTokenPath() { - return tokenPath; - } - - public void setTokenPath(String tokenPath) { - this.tokenPath = tokenPath; - } - - public String getBaseUrl() { - return baseUrl; - } - - public void setBaseUrl(String baseUrl) { - this.baseUrl = baseUrl; - } - - public String getProjectsPath() { - return projectsPath; - } - - public void setProjectsPath(String projectsPath) { - this.projectsPath = projectsPath; - } - - public String getSubjectsPath() { - return subjectsPath; - } - - public void setSubjectsPath(String subjectsPath) { - this.subjectsPath = subjectsPath; - } - - public String getOauthClientId() { - return oauthClientId; - } - - public void setOauthClientId(String oauthClientId) { - this.oauthClientId = oauthClientId; - } - - public String getOauthClientSecret() { - return oauthClientSecret; - } - - public void setOauthClientSecret(String oauthClientSecret) { - this.oauthClientSecret = oauthClientSecret; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ManagementPortalProperties that = (ManagementPortalProperties) o; - return baseUrl.equals(that.baseUrl) && - projectsPath.equals(that.projectsPath) && - subjectsPath.equals(that.subjectsPath) && - oauthClientId.equals(that.oauthClientId) && - oauthClientSecret.equals(that.oauthClientSecret); - } - - @Override - public int hashCode() { - return Objects.hash(baseUrl, projectsPath, subjectsPath, oauthClientId, oauthClientSecret); - } - - @Override - public String toString() { - return "ManagementPortalProperties{" + - "baseUrl='" + baseUrl + '\'' + - ", projectsPath='" + projectsPath + '\'' + - ", subjectsPath='" + subjectsPath + '\'' + - ", oauthClientId='" + oauthClientId + '\'' + - ", oauthClientSecret='" + oauthClientSecret + '\'' + - ", tokenPath='" + tokenPath + '\'' + - '}'; - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java deleted file mode 100644 index 424c15a5..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceAuthorizerProperties.java +++ /dev/null @@ -1,100 +0,0 @@ -///* -// * -// * * Copyright 2018 The Hyve -// * * -// * * Licensed under the Apache License, Version 2.0 (the "License"); -// * * you may not use this file except in compliance with the License. -// * * You may obtain a copy of the License at -// * * -// * * http://www.apache.org/licenses/LICENSE-2.0 -// * * -// * * Unless required by applicable law or agreed to in writing, software -// * * distributed under the License is distributed on an "AS IS" BASIS, -// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * * See the License for the specific language governing permissions and -// * * limitations under the License. -// * * -// * -// */ -// -//package org.radarbase.authorizer.config; -// -//import java.util.Objects; -// -//import org.springframework.boot.context.properties.ConfigurationProperties; -//import org.springframework.web.cors.CorsConfiguration; -// -//@ConfigurationProperties(prefix = "rest-source-authorizer", ignoreUnknownFields = false) -//public class RestSourceAuthorizerProperties { -// -// private CorsConfiguration cors; -// -// private AuthTokenValidatorConfig auth; -// -// private String sourceClientsFilePath; -// -// private String validator; -// -// private ManagementPortalProperties managementPortal; -// -// public CorsConfiguration getCors() { -// return cors; -// } -// -// public void setCors(CorsConfiguration cors) { -// this.cors = cors; -// } -// -// public String getSourceClientsFilePath() { -// return sourceClientsFilePath; -// } -// -// public void setSourceClientsFilePath(String sourceClientsFilePath) { -// this.sourceClientsFilePath = sourceClientsFilePath; -// } -// -// public String getValidator() { -// return validator; -// } -// -// public void setValidator(String validator) { -// this.validator = validator; -// } -// -// public ManagementPortalProperties getManagementPortal() { -// return managementPortal; -// } -// -// public void setManagementPortal( -// ManagementPortalProperties managementPortal) { -// this.managementPortal = managementPortal; -// } -// -// public AuthTokenValidatorConfig getAuth() { -// return auth; -// } -// -// public void setAuth(AuthTokenValidatorConfig auth) { -// this.auth = auth; -// } -// -// @Override -// public boolean equals(Object o) { -// if (this == o) { -// return true; -// } -// if (o == null || getClass() != o.getClass()) { -// return false; -// } -// RestSourceAuthorizerProperties that = (RestSourceAuthorizerProperties) o; -// return Objects.equals(cors, that.cors) -// && Objects.equals(sourceClientsFilePath, that.sourceClientsFilePath) -// && Objects.equals(auth, that.auth); -// } -// -// @Override -// public int hashCode() { -// -// return Objects.hash(cors, sourceClientsFilePath); -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClientConfig.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClientConfig.java deleted file mode 100644 index 8ce61283..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClientConfig.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.config; - -import java.util.Objects; - -public class RestSourceClientConfig { - - private String sourceType; - - private String authorizationEndpoint; - - private String tokenEndpoint; - - private String grantType; - - private String scope; - - private String clientId; - - private String clientSecret; - - public String getSourceType() { - return sourceType; - } - - public void setSourceType(String sourceType) { - this.sourceType = sourceType; - } - - public String getAuthorizationEndpoint() { - return authorizationEndpoint; - } - - public void setAuthorizationEndpoint(String authorizationEndpoint) { - this.authorizationEndpoint = authorizationEndpoint; - } - - public String getTokenEndpoint() { - return tokenEndpoint; - } - - public void setTokenEndpoint(String tokenEndpoint) { - this.tokenEndpoint = tokenEndpoint; - } - - public String getGrantType() { - return grantType; - } - - public void setGrantType(String grantType) { - this.grantType = grantType; - } - - public String getScope() { - return scope; - } - - public void setScope(String scope) { - this.scope = scope; - } - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public String getClientSecret() { - return clientSecret; - } - - public void setClientSecret(String clientSecret) { - this.clientSecret = clientSecret; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RestSourceClientConfig that = (RestSourceClientConfig) o; - return Objects.equals(sourceType, that.sourceType) && Objects - .equals(authorizationEndpoint, that.authorizationEndpoint) && Objects - .equals(tokenEndpoint, that.tokenEndpoint) && Objects - .equals(grantType, that.grantType) && Objects.equals(scope, that.scope) && Objects - .equals(clientId, that.clientId) && Objects.equals(clientSecret, that.clientSecret); - } - - @Override - public int hashCode() { - - return Objects - .hash(sourceType, authorizationEndpoint, tokenEndpoint, grantType, scope, clientId, - clientSecret); - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClients.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClients.java deleted file mode 100644 index d66152a9..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClients.java +++ /dev/null @@ -1,37 +0,0 @@ -///* -// * -// * * Copyright 2018 The Hyve -// * * -// * * Licensed under the Apache License, Version 2.0 (the "License"); -// * * you may not use this file except in compliance with the License. -// * * You may obtain a copy of the License at -// * * -// * * http://www.apache.org/licenses/LICENSE-2.0 -// * * -// * * Unless required by applicable law or agreed to in writing, software -// * * distributed under the License is distributed on an "AS IS" BASIS, -// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * * See the License for the specific language governing permissions and -// * * limitations under the License. -// * * -// * -// */ -// -//package org.radarbase.authorizer.config; -// -//import java.util.List; -// -// -//public class RestSourceClients { -// -// private List restSourceClients; -// -// public List getRestSourceClients() { -// return restSourceClients; -// } -// -// public RestSourceClients restSourceClients(List restSourceClients) { -// this.restSourceClients = restSourceClients; -// return this; -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Project.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Project.java deleted file mode 100644 index 3c0ecf23..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Project.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.radarbase.authorizer.service.dto.managementportal; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Objects; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class Project { - - @JsonProperty("projectName") - private String projectId; - - public String getProjectId() { - return projectId; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Project project = (Project) o; - return Objects.equals(projectId, project.projectId); - } - - @Override - public int hashCode() { - return Objects.hash(projectId); - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Subject.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Subject.java deleted file mode 100644 index 7d01c46d..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/dto/managementportal/Subject.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.radarbase.authorizer.service.dto.managementportal; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Objects; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class Subject { - - private Project project; - - @JsonProperty("login") - private String subjectId; - - public Project getProject() { - return project; - } - - public String getSubjectId() { - return subjectId; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Subject subject = (Subject) o; - return project.equals(subject.project) && - subjectId.equals(subject.subjectId); - } - - @Override - public int hashCode() { - return Objects.hash(project, subjectId); - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java deleted file mode 100644 index 6bfff214..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/CachedManagementPortalClient.java +++ /dev/null @@ -1,223 +0,0 @@ -//package org.radarbase.authorizer.service.managementportal; -// -//import static org.radarbase.authorizer.validation.ManagementPortalValidator.MP_VALIDATOR_PROPERTY_VALUE; -// -//import com.fasterxml.jackson.core.type.TypeReference; -//import com.fasterxml.jackson.databind.DeserializationFeature; -//import com.fasterxml.jackson.databind.ObjectMapper; -//import java.io.IOException; -//import java.net.MalformedURLException; -//import java.net.URL; -//import java.time.Duration; -//import java.time.Instant; -//import java.util.HashSet; -//import java.util.Set; -//import java.util.concurrent.TimeUnit; -//import okhttp3.OkHttpClient; -//import okhttp3.Request; -//import okhttp3.Response; -//import org.radarbase.authorizer.config.ManagementPortalProperties; -//import org.radarbase.authorizer.config.RestSourceAuthorizerProperties; -//import org.radarbase.authorizer.service.dto.managementportal.Project; -//import org.radarbase.authorizer.service.dto.managementportal.Subject; -//import org.radarcns.exception.TokenException; -//import org.radarcns.oauth.OAuth2AccessTokenDetails; -//import org.radarcns.oauth.OAuth2Client; -//import org.slf4j.Logger; -//import org.slf4j.LoggerFactory; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -//import org.springframework.http.HttpHeaders; -//import org.springframework.stereotype.Service; -// -//@Service -//@ConditionalOnProperty(value = "rest-source-authorizer.validator", havingValue = MP_VALIDATOR_PROPERTY_VALUE) -//public class CachedManagementPortalClient implements ManagementPortalClient { -// -// private static final Logger LOGGER = LoggerFactory.getLogger(CachedManagementPortalClient.class); -// private Set subjects; -// private Set projects; -// -// private Duration expiry = Duration.ofHours(1); -// private Instant lastFetch; -// -// private OkHttpClient httpClient; -// -// private OAuth2Client oAuth2Client; -// -// private ManagementPortalProperties properties; -// -// private ObjectMapper mapper = new ObjectMapper(); -// -// @Autowired -// public CachedManagementPortalClient(RestSourceAuthorizerProperties restSourceAuthorizerProperties) -// throws MalformedURLException { -// subjects = new HashSet<>(); -// projects = new HashSet<>(); -// lastFetch = Instant.MIN; -// this.properties = restSourceAuthorizerProperties.getManagementPortal(); -// init(); -// } -// -// public CachedManagementPortalClient(ManagementPortalProperties managementPortalProperties, -// Duration expiry) throws MalformedURLException { -// this.expiry = expiry; -// subjects = new HashSet<>(); -// projects = new HashSet<>(); -// lastFetch = Instant.MIN; -// this.properties = managementPortalProperties; -// init(); -// } -// -// private void init() throws MalformedURLException { -// -// this.httpClient = new OkHttpClient.Builder() -// .connectTimeout(20, TimeUnit.SECONDS) -// .writeTimeout(20, TimeUnit.SECONDS) -// .readTimeout(50, TimeUnit.SECONDS) -// .build(); -// -// this.mapper = new ObjectMapper() -// .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); -// -// this.oAuth2Client = new OAuth2Client.Builder() -// .credentials(properties.getOauthClientId(), properties.getOauthClientSecret()) -// .endpoint(new URL(properties.getBaseUrl()), this.properties.getTokenPath()) -// .httpClient(httpClient) -// .build(); -// -// LOGGER.info(this.properties.toString()); -// LOGGER -// .info("Trying to get a Token and check if it has required permissions at the endpoint: {}", -// this.oAuth2Client.getTokenEndpoint()); -// try { -// OAuth2AccessTokenDetails accessToken = this.oAuth2Client.getValidToken(); -// if (accessToken.getScope().contains("PROJECT.READ") && accessToken.getScope() -// .contains("SUBJECT.READ")) { -// LOGGER.info("The client has sufficient privileges. Proceeding normally..."); -// } else { -// throw new IllegalStateException( -// "The configured oAuth client [" + this.properties.getOauthClientId() + ", " -// + this.properties.getOauthClientSecret() -// + "] does not have sufficient privileges on Management portal." -// + " Please update it on Management portal or use a different client."); -// } -// } catch (TokenException exc) { -// throw new IllegalStateException( -// "There was a problem getting the oAuth token from the server: " + exc); -// } -// } -// -// @Override -// public Subject getSubject(String subjectId) throws IOException, TokenException { -// // First check if need to refresh subjects cache -// if (isUpdateRequired()) { -// update(); -// return this.subjects.stream() -// .filter(subject1 -> subject1.getSubjectId().equals(subjectId)) -// .findFirst() -// .orElse(null); -// } else { -// // Try to find the subject in cache if not updated -// return this.subjects.stream() -// .filter(subject1 -> subject1.getSubjectId().equals(subjectId)) -// .findFirst() -// .orElse(querySubject(subjectId)); -// } -// } -// -// @Override -// public Project getProject(String projectId) throws IOException, TokenException { -// if (isUpdateRequired()) { -// update(); -// return this.projects.stream() -// .filter(project1 -> project1.getProjectId().equals(projectId)) -// .findFirst() -// .orElse(null); -// } else { -// return this.projects.stream() -// .filter(project1 -> project1.getProjectId().equals(projectId)) -// .findFirst() -// .orElse(queryProject(projectId)); -// } -// } -// -// @Override -// public Set getAllSubjects() throws IOException, TokenException { -// if (isUpdateRequired()) { -// update(); -// } -// return this.subjects; -// } -// -// @Override -// public Set getAllProjects() throws IOException, TokenException { -// if (isUpdateRequired()) { -// update(); -// } -// return this.projects; -// } -// -// private Subject querySubject(String subjectId) throws IOException, TokenException { -// Subject subject = queryEntity( -// properties.getBaseUrl() + properties.getSubjectsPath() + "/" + subjectId, -// new TypeReference() { -// }); -// this.subjects.add(subject); -// return subject; -// } -// -// private Project queryProject(String projectId) throws IOException, TokenException { -// Project project = queryEntity( -// properties.getBaseUrl() + properties.getProjectsPath() + "/" + projectId, -// new TypeReference() { -// }); -// -// this.projects.add(project); -// return project; -// } -// -// private T queryEntity(String url, TypeReference t) -// throws TokenException, IOException { -// Request request = new Request.Builder() -// .addHeader(HttpHeaders.AUTHORIZATION, -// "Bearer " + oAuth2Client.getValidToken().getAccessToken()) -// .url(new URL(url)) -// .get() -// .build(); -// Response response = httpClient.newCall(request).execute(); -// if (response.isSuccessful() && response.body() != null) { -// return mapper.readValue(response.body().string(), t); -// } else { -// throw new IOException( -// "The Request was not successful: Status-" + response.code() + ", Body-" + -// (response.body() != null ? response.body().string() : "")); -// } -// } -// -// private Set queryAllSubjects() throws IOException, TokenException { -// // get subjects from MP -// return queryEntity(properties.getBaseUrl() + properties.getSubjectsPath(), -// new TypeReference>() { -// }); -// } -// -// private Set queryAllProjects() throws IOException, TokenException { -// // get projects from MP -// return queryEntity(properties.getBaseUrl() + properties.getProjectsPath(), -// new TypeReference>() { -// }); -// } -// -// synchronized private void update() throws IOException, TokenException { -// Set subjects1 = queryAllSubjects(); -// Set projects1 = queryAllProjects(); -// this.subjects = subjects1 == null ? new HashSet<>() : subjects1; -// this.projects = projects1 == null ? new HashSet<>() : projects1; -// lastFetch = Instant.now(); -// } -// -// synchronized private boolean isUpdateRequired() { -// return this.lastFetch.plus(this.expiry).isBefore(Instant.now()); -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java deleted file mode 100644 index 331d4af0..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/ManagementPortalClient.java +++ /dev/null @@ -1,18 +0,0 @@ -//package org.radarbase.authorizer.service.managementportal; -// -//import java.io.IOException; -//import java.util.Collection; -//import org.radarbase.authorizer.service.dto.managementportal.Project; -//import org.radarbase.authorizer.service.dto.managementportal.Subject; -//import org.radarcns.exception.TokenException; -// -//public interface ManagementPortalClient { -// -// S getSubject(String subjectId) throws IOException, TokenException; -// -// P getProject(String projectId) throws IOException, TokenException; -// -// Collection getAllSubjects() throws IOException, TokenException; -// -// Collection

getAllProjects() throws IOException, TokenException; -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java deleted file mode 100644 index f8ff264c..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/ManagementPortalValidator.java +++ /dev/null @@ -1,65 +0,0 @@ -//package org.radarbase.authorizer.validation; -// -//import static org.radarbase.authorizer.validation.ManagementPortalValidator.MP_VALIDATOR_PROPERTY_VALUE; -// -//import java.io.IOException; -//import java.net.MalformedURLException; -//import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; -//import org.radarbase.authorizer.service.dto.managementportal.Subject; -//import org.radarbase.authorizer.service.managementportal.ManagementPortalClient; -//import org.radarbase.authorizer.validation.exception.ValidationFailedException; -//import org.radarcns.exception.TokenException; -//import org.slf4j.Logger; -//import org.slf4j.LoggerFactory; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -//import org.springframework.stereotype.Component; -// -//@Component -//@ConditionalOnProperty(value = "rest-source-authorizer.validator", havingValue = MP_VALIDATOR_PROPERTY_VALUE) -//public class ManagementPortalValidator implements Validator { -// -// public static final String MP_VALIDATOR_PROPERTY_VALUE = "managementportal"; -// -// private static final Logger logger = LoggerFactory.getLogger(ManagementPortalValidator.class); -// private final ManagementPortalClient mpClient; -// -// @Autowired -// public ManagementPortalValidator(ManagementPortalClient mpClient) { -// this.mpClient = mpClient; -// } -// -// @Override -// public boolean validate(RestSourceUserPropertiesDTO restSourceUser) { -// if (restSourceUser == null) { -// return false; -// } -// -// Subject subject; -// try { -// subject = mpClient.getSubject(restSourceUser.getUserId()); -// } catch (TokenException exc) { -// logger.warn("Cannot get a valid token from Management Portal.", exc); -// throw new ValidationFailedException(restSourceUser, this, -// "Cannot get a valid token from Management Portal. " + exc.getMessage()); -// } catch (MalformedURLException exc) { -// logger.warn("URL is mis-configured.", exc); -// throw new ValidationFailedException(restSourceUser, this, -// "URL is mis-configured. " + exc.getMessage()); -// } catch (IOException exc) { -// logger.warn("An error occurred while making Validating request.", exc); -// throw new ValidationFailedException(restSourceUser, this, -// "An error occurred while making Validating request. " + exc.getMessage()); -// } -// if (subject != null) { -// return subject.getProject().getProjectId().equals(restSourceUser.getProjectId()); -// } else { -// logger.warn("The subject with id {} was not found in the Management portal.", -// restSourceUser.getUserId()); -// throw new ValidationFailedException(restSourceUser, this, -// "The subject with id " + restSourceUser.getUserId() -// + " was not found in the Management portal." -// ); -// } -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/Validator.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/Validator.java deleted file mode 100644 index 85cd6c8b..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/Validator.java +++ /dev/null @@ -1,8 +0,0 @@ -//package org.radarbase.authorizer.validation; -// -//import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; -// -//public interface Validator { -// -// boolean validate(RestSourceUserPropertiesDTO projectId); -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java deleted file mode 100644 index 70f9a418..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/validation/exception/ValidationFailedException.java +++ /dev/null @@ -1,21 +0,0 @@ -//package org.radarbase.authorizer.validation.exception; -// -//import org.radarbase.authorizer.validation.Validator; -//import org.springframework.http.HttpStatus; -//import org.springframework.web.bind.annotation.ResponseStatus; -// -//@ResponseStatus(value = HttpStatus.EXPECTATION_FAILED, reason = "Validation Failed for the Request.") -//public class ValidationFailedException extends RuntimeException { -// -// public ValidationFailedException(Object entity, Validator validator) { -// super( -// "Validation Failed for [" + entity + "] using Validator [" + validator.getClass().getName() -// + "]."); -// } -// -// public ValidationFailedException(Object entity, Validator validator, String reason) { -// super( -// "Validation Failed for [" + entity + "] using Validator [" + validator.getClass().getName() -// + "] due to [" + reason + "]."); -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java deleted file mode 100644 index 451ed19a..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/BadGatewayException.java +++ /dev/null @@ -1,39 +0,0 @@ -///* -// * -// * * Copyright 2018 The Hyve -// * * -// * * Licensed under the Apache License, Version 2.0 (the "License"); -// * * you may not use this file except in compliance with the License. -// * * You may obtain a copy of the License at -// * * -// * * http://www.apache.org/licenses/LICENSE-2.0 -// * * -// * * Unless required by applicable law or agreed to in writing, software -// * * distributed under the License is distributed on an "AS IS" BASIS, -// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * * See the License for the specific language governing permissions and -// * * limitations under the License. -// * * -// * -// */ -// -//package org.radarbase.authorizer.webapp.exception; -// -//import org.springframework.http.HttpStatus; -//import org.springframework.web.bind.annotation.ResponseStatus; -// -//@ResponseStatus(value = HttpStatus.BAD_GATEWAY) -//public class BadGatewayException extends RuntimeException { -// -// public BadGatewayException() { -// super("Something went wrong in communication with other services"); -// } -// -// public BadGatewayException(String message) { -// super(message); -// } -// -// public BadGatewayException(Throwable cause) { -// super("Something went wrong in communication with other services", cause); -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java deleted file mode 100644 index 2caf2d9a..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/InvalidSourceTypeException.java +++ /dev/null @@ -1,35 +0,0 @@ -///* -// * -// * * Copyright 2018 The Hyve -// * * -// * * Licensed under the Apache License, Version 2.0 (the "License"); -// * * you may not use this file except in compliance with the License. -// * * You may obtain a copy of the License at -// * * -// * * http://www.apache.org/licenses/LICENSE-2.0 -// * * -// * * Unless required by applicable law or agreed to in writing, software -// * * distributed under the License is distributed on an "AS IS" BASIS, -// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * * See the License for the specific language governing permissions and -// * * limitations under the License. -// * * -// * -// */ -// -//package org.radarbase.authorizer.webapp.exception; -// -//import org.springframework.http.HttpStatus; -//import org.springframework.web.bind.annotation.ResponseStatus; -// -//@ResponseStatus(HttpStatus.EXPECTATION_FAILED) -//public class InvalidSourceTypeException extends RuntimeException { -// -// public InvalidSourceTypeException(String sourceType) { -// super("Unsupported source type [ " + sourceType + " ] found "); -// } -// -// public InvalidSourceTypeException(String sourceType, Throwable cause) { -// super("Cannot find configurations for type " + sourceType, cause); -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java deleted file mode 100644 index 8f92ec1e..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/NotFoundException.java +++ /dev/null @@ -1,32 +0,0 @@ -///* -// * -// * * Copyright 2018 The Hyve -// * * -// * * Licensed under the Apache License, Version 2.0 (the "License"); -// * * you may not use this file except in compliance with the License. -// * * You may obtain a copy of the License at -// * * -// * * http://www.apache.org/licenses/LICENSE-2.0 -// * * -// * * Unless required by applicable law or agreed to in writing, software -// * * distributed under the License is distributed on an "AS IS" BASIS, -// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * * See the License for the specific language governing permissions and -// * * limitations under the License. -// * * -// * -// */ -// -//package org.radarbase.authorizer.webapp.exception; -// -//import org.springframework.http.HttpStatus; -//import org.springframework.web.bind.annotation.ResponseStatus; -// -//@ResponseStatus(HttpStatus.NOT_FOUND) -//public class NotFoundException extends RuntimeException { -// -// public NotFoundException(String message) { -// super(message); -// } -// -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java deleted file mode 100644 index 9c72ce59..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/exception/TokenException.java +++ /dev/null @@ -1,39 +0,0 @@ -///* -// * -// * * Copyright 2018 The Hyve -// * * -// * * Licensed under the Apache License, Version 2.0 (the "License"); -// * * you may not use this file except in compliance with the License. -// * * You may obtain a copy of the License at -// * * -// * * http://www.apache.org/licenses/LICENSE-2.0 -// * * -// * * Unless required by applicable law or agreed to in writing, software -// * * distributed under the License is distributed on an "AS IS" BASIS, -// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * * See the License for the specific language governing permissions and -// * * limitations under the License. -// * * -// * -// */ -// -//package org.radarbase.authorizer.webapp.exception; -// -//import org.springframework.http.HttpStatus; -//import org.springframework.web.bind.annotation.ResponseStatus; -// -//@ResponseStatus(value = HttpStatus.UNAUTHORIZED) -//public class TokenException extends RuntimeException { -// -// public TokenException() { -// super("Unable to get a valid access token"); -// } -// -// public TokenException(String message) { -// super(message); -// } -// -// public TokenException(Throwable cause) { -// super("Unable to get a valid access token", cause); -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java deleted file mode 100644 index 5eaad5f6..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/SourceClientResource.java +++ /dev/null @@ -1,63 +0,0 @@ -///* -// * -// * * Copyright 2018 The Hyve -// * * -// * * Licensed under the Apache License, Version 2.0 (the "License"); -// * * you may not use this file except in compliance with the License. -// * * You may obtain a copy of the License at -// * * -// * * http://www.apache.org/licenses/LICENSE-2.0 -// * * -// * * Unless required by applicable law or agreed to in writing, software -// * * distributed under the License is distributed on an "AS IS" BASIS, -// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * * See the License for the specific language governing permissions and -// * * limitations under the License. -// * * -// * -// */ -// -//package org.radarbase.authorizer.webapp.resource; -// -//import java.util.List; -// -//import org.radarbase.authorizer.service.RestSourceClientService; -//import org.radarbase.authorizer.service.dto.RestSourceClientDetailsDTO; -//import org.radarbase.authorizer.service.dto.RestSourceClients; -//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.RestController; -// -//@RestController -//public class SourceClientResource { -// -// private Logger logger = LoggerFactory.getLogger(SourceClientResource.class); -// -// @Autowired -// private RestSourceClientService restSourceClientService; -// -// @GetMapping("/source-clients") -// public ResponseEntity getAllDeviceProperties() { -// logger.debug("Get all source clients details"); -// return ResponseEntity.ok(this.restSourceClientService.getAllRestSourceClientDetails()); -// } -// -// -// @GetMapping("/source-clients/type") -// public ResponseEntity> getAllAvailableDeviceTypes() { -// logger.debug("Get all source-types"); -// return ResponseEntity.ok(this.restSourceClientService.getAvailableDeviceTypes()); -// } -// -// @GetMapping("/source-clients/{sourceType}") -// public ResponseEntity getDeviceAuthDetailsByDeviceType( -// @PathVariable String sourceType) { -// logger.info("Get source clients detail by type {}", sourceType); -// return ResponseEntity.ok(this.restSourceClientService.getAllRestSourceClientDetails(sourceType)); -// } -// -//} From e03cb1cb5cf8814fc9208d61cbde09f421fcd9c3 Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 31 Aug 2020 17:16:18 +0200 Subject: [PATCH 11/80] fix bugs with transaction commit and liquibase changelog include --- .../radarbase/authorizer/doa/EntityManagerExtensions.kt | 7 ++++--- .../authorizer/doa/RestSourceUserRepositoryImpl.kt | 4 ++-- .../authorizer/inject/AuthorizerResourceEnhancer.kt | 4 ++-- .../resources/db/changelog/changes/db.changelog-master.xml | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt index fedb46f1..047fe6da 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt @@ -1,5 +1,6 @@ package org.radarbase.authorizer.doa +import org.radarbase.authorizer.logger import org.radarbase.jersey.exception.HttpInternalServerException import java.io.Closeable import javax.persistence.EntityManager @@ -15,7 +16,7 @@ fun EntityManager.transact(transactionOperation: EntityManager.() -> T) = cr /** * Start a transaction without committing it. If an exception occurs, the transaction is rolled back. */ -fun EntityManager.createTransaction(transactionOperation: EntityManager.(CloseableTransaction) -> T): T { +private fun EntityManager.createTransaction(transactionOperation: EntityManager.(CloseableTransaction) -> T): T { val currentTransaction = transaction ?: throw HttpInternalServerException("transaction_not_found", "Cannot find a transaction from EntityManager") @@ -28,7 +29,7 @@ fun EntityManager.createTransaction(transactionOperation: EntityManager.(Clo try { transaction.commit() } catch (ex: Exception) { -// logger.error("Rolling back operation", ex) + logger.error("Rolling back operation", ex) if (currentTransaction.isActive) { currentTransaction.rollback() } @@ -37,7 +38,7 @@ fun EntityManager.createTransaction(transactionOperation: EntityManager.(Clo } }) } catch (ex: Exception) { -// logger.error("Rolling back operation", ex) + logger.error("Rolling back operation", ex) if (currentTransaction.isActive) { currentTransaction.rollback() } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index 3519add1..322a48f7 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -16,7 +16,7 @@ class RestSourceUserRepositoryImpl( @Context private var em: Provider ) : RestSourceUserRepository { - override fun create(token: RestOauth2AccessToken, sourceType: String): RestSourceUser = em.get().createTransaction { + override fun create(token: RestOauth2AccessToken, sourceType: String): RestSourceUser = em.get().transact { val externalUserId = token.externalUserId ?: throw HttpBadGatewayException("Could not get externalId from token") val queryString = "SELECT u FROM RestSourceUser u where u.sourceType = :sourceType AND u.externalUserId = :externalUserId" @@ -48,7 +48,7 @@ class RestSourceUserRepositoryImpl( override fun read(id: Long): RestSourceUser? = em.get().transact { find(RestSourceUser::class.java, id) } - override fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser = em.get().createTransaction { + override fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser = em.get().transact { existingUser.apply { this.projectId = user.projectId this.userId = user.userId diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt index ab1ac239..d11e35f0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt @@ -7,7 +7,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.KotlinModule import okhttp3.OkHttpClient import org.glassfish.jersey.internal.inject.AbstractBinder -import org.glassfish.jersey.internal.inject.PerLookup +import org.glassfish.jersey.process.internal.RequestScoped import org.glassfish.jersey.server.ResourceConfig import org.radarbase.authorizer.Config import org.radarbase.authorizer.DatabaseConfig @@ -78,7 +78,7 @@ class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnh bindFactory(DoaEntityManagerFactory::class.java) .to(EntityManager::class.java) - .`in`(PerLookup::class.java) + .`in`(RequestScoped::class.java) bind(RestSourceUserMapper::class.java) .to(RestSourceUserMapper::class.java) diff --git a/authorizer-app-backend/src/main/resources/db/changelog/changes/db.changelog-master.xml b/authorizer-app-backend/src/main/resources/db/changelog/changes/db.changelog-master.xml index 4b039aba..6d97c064 100644 --- a/authorizer-app-backend/src/main/resources/db/changelog/changes/db.changelog-master.xml +++ b/authorizer-app-backend/src/main/resources/db/changelog/changes/db.changelog-master.xml @@ -5,6 +5,6 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd"> - + From 4ace7f408da191dd3037ff9713f0b477777dc5e0 Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 31 Aug 2020 21:00:34 +0200 Subject: [PATCH 12/80] add reset endpoint --- .../doa/RestSourceUserRepository.kt | 2 ++ .../doa/RestSourceUserRepositoryImpl.kt | 10 ++++++- .../resources/RestSourceUserResource.kt | 14 +++++++-- .../rest-source-user-list.component.ts | 29 ++----------------- .../app/services/rest-source-user.service.ts | 4 +-- 5 files changed, 27 insertions(+), 32 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index b625baab..3f9e3a4a 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -4,6 +4,7 @@ import org.radarbase.authorizer.api.Page import org.radarbase.authorizer.api.RestOauth2AccessToken import org.radarbase.authorizer.api.RestSourceUserDTO import org.radarbase.authorizer.doa.entity.RestSourceUser +import java.time.Instant interface RestSourceUserRepository { @@ -14,4 +15,5 @@ interface RestSourceUserRepository { // fun findByExtenalId(sourceType: String, externalUserId: String) : RestSourceUser? // fun findAllBySourceType(sourceType: String?): List fun delete(user: RestSourceUser) + fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?): RestSourceUser } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index 322a48f7..b76635a8 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -58,7 +58,6 @@ class RestSourceUserRepositoryImpl( }.also { merge(it) } } - override fun query(page: Page, sourceType: String?, externalUserId: String?): Pair, Page> { var queryString = "SELECT u FROM RestSourceUser u" var countQueryString = "SELECT count(u) FROM RestSourceUser u" @@ -97,6 +96,15 @@ class RestSourceUserRepositoryImpl( remove(merge(user)) } + override fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?) = em.get().transact { + user.apply { + this.version = Instant.now().toString() + this.timesReset += 1 + this.startDate = startDate + this.endDate = endDate + }.also { merge(it) } + } + companion object { private val expiryTimeMargin = Duration.ofMinutes(5) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt index a604dd54..cb1626f2 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt @@ -80,8 +80,7 @@ class RestSourceUserResource( @Path("{id}") fun update( @PathParam("id") userId: Long, - user: RestSourceUserDTO, - @QueryParam("validate") validate: Boolean): RestSourceUserDTO { + user: RestSourceUserDTO): RestSourceUserDTO { val existingUser = validate(userId, user, Permission.SUBJECT_UPDATE) val updatedUser = userRepository.update(existingUser, user) @@ -108,6 +107,17 @@ class RestSourceUserResource( return Response.noContent().header("user-removed", userId).build() } + @POST + @Path("{id}/reset") + fun reset( + @PathParam("id") userId: Long, + user: RestSourceUserDTO): RestSourceUserDTO { + val existingUser = validate(userId, user, Permission.SUBJECT_UPDATE) + + val updatedUser = userRepository.reset(existingUser, user.startDate, user.endDate ?: existingUser.endDate) + return userMapper.fromEntity(updatedUser) + } + private fun validate(id: Long, user: RestSourceUserDTO, permission: Permission) : RestSourceUser { val existingUser = ensureUser(id) val projectId = user.projectId ?: throw HttpBadRequestException("missing_project_id", "project cannot be empty") diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts index 7b2ce964..f237db2b 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts @@ -95,7 +95,7 @@ export class RestSourceUserListComponent implements OnInit, AfterViewInit { } resetUser(restSourceUser: RestSourceUser) { - this.restSourceUserService.resetUser(restSourceUser.id).subscribe(() => { + this.restSourceUserService.resetUser(restSourceUser).subscribe(() => { this.loadAllRestSourceUsers(); }); } @@ -117,33 +117,8 @@ export class RestSourceUserListComponent implements OnInit, AfterViewInit { }); dialogRef.afterClosed().subscribe((user: RestSourceUser) => { - if ( - user.startDate != restSourceUser.startDate || - user.endDate != restSourceUser.endDate - ) { - console.log('Updating user details...'); - this.restSourceUserService.updateUser(user).subscribe( - () => { - console.log('Resetting user...'); - this.resetUser(user); - }, - (err: HttpErrorResponse) => { - if (err.error instanceof ErrorEvent) { - // A client-side or network error occurred. Handle it accordingly. - this.errorMessage = - 'Something went wrong. Please check your connection.'; - } else { - // The backend returned an unsuccessful response code. - // The response body may contain clues as to what went wrong, - this.errorMessage = `Backend Error: Status=${err.status}, - Body: ${err.error.error}, ${err.error.message}`; - } - } - ); - } else { - console.log('Resetting user...'); + console.log('Resetting user...', user); this.resetUser(user); - } }); } } diff --git a/authorizer-app/src/app/services/rest-source-user.service.ts b/authorizer-app/src/app/services/rest-source-user.service.ts index a7a5a4f5..4a775e08 100644 --- a/authorizer-app/src/app/services/rest-source-user.service.ts +++ b/authorizer-app/src/app/services/rest-source-user.service.ts @@ -39,7 +39,7 @@ export class RestSourceUserService { return this.http.delete(this.serviceUrl + '/' + userId); } - resetUser(userId: string): Observable { - return this.http.post(this.serviceUrl + '/' + userId + '/reset', new HttpParams()); + resetUser(user: RestSourceUser): Observable { + return this.http.post(this.serviceUrl + '/' + user.id + '/reset', user); } } From f3693b1230e0099941e398986fd9ba3a67266ec8 Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 31 Aug 2020 21:18:12 +0200 Subject: [PATCH 13/80] add token endpoints --- .../authorizer/api/ApiDeclarations.kt | 4 +- .../doa/RestSourceUserRepository.kt | 2 +- .../doa/RestSourceUserRepositoryImpl.kt | 2 +- .../resources/RestSourceUserResource.kt | 51 +++++++++++++------ 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt index 6b5a13e0..81b24e76 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt @@ -50,8 +50,8 @@ data class RestSourceUsers( ) class TokenDTO( - val accessToken: String? = null, - val expiresAt: Instant? = null + val accessToken: String?, + val expiresAt: Instant? ) data class Page( diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index 3f9e3a4a..46e5fcb7 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -8,7 +8,7 @@ import java.time.Instant interface RestSourceUserRepository { - fun create(user: RestOauth2AccessToken, sourceType: String): RestSourceUser + fun createOrUpdate(user: RestOauth2AccessToken, sourceType: String): RestSourceUser fun read(id: Long): RestSourceUser? fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser fun query(page: Page, sourceType: String? = null, externalUserId: String? = null): Pair, Page> diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index b76635a8..f510d30b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -16,7 +16,7 @@ class RestSourceUserRepositoryImpl( @Context private var em: Provider ) : RestSourceUserRepository { - override fun create(token: RestOauth2AccessToken, sourceType: String): RestSourceUser = em.get().transact { + override fun createOrUpdate(token: RestOauth2AccessToken, sourceType: String): RestSourceUser = em.get().transact { val externalUserId = token.externalUserId ?: throw HttpBadGatewayException("Could not get externalId from token") val queryString = "SELECT u FROM RestSourceUser u where u.sourceType = :sourceType AND u.externalUserId = :externalUserId" diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt index cb1626f2..f0d63765 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt @@ -1,18 +1,14 @@ package org.radarbase.authorizer.resources -import org.radarbase.authorizer.api.Page -import org.radarbase.authorizer.api.RestSourceUserDTO -import org.radarbase.authorizer.api.RestSourceUserMapper -import org.radarbase.authorizer.api.RestSourceUsers +import org.radarbase.authorizer.api.* import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.doa.entity.RestSourceUser -import org.radarbase.authorizer.logger import org.radarbase.authorizer.service.RadarProjectService import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.jersey.auth.Auth import org.radarbase.jersey.auth.Authenticated -import org.radarbase.jersey.auth.NeedsPermission import org.radarbase.jersey.exception.HttpBadRequestException +import org.radarbase.jersey.exception.HttpConflictException import org.radarbase.jersey.exception.HttpNotFoundException import org.radarcns.auth.authorization.Permission import org.slf4j.Logger @@ -68,8 +64,8 @@ class RestSourceUserResource( @FormParam("code") code: String, @FormParam("state") state: String): Response { logger.info("code $code state $state") - val accessToken = authorizationService.requestAccessToken(code, sourceType= state) - val user = userRepository.create(accessToken, state) + val accessToken = authorizationService.requestAccessToken(code, sourceType = state) + val user = userRepository.createOrUpdate(accessToken, state) return Response.created(URI("users/${user.id}")) .entity(userMapper.fromEntity(user)) @@ -89,7 +85,7 @@ class RestSourceUserResource( @GET @Path("{id}") - fun readUser(@PathParam("id") userId: Long) : RestSourceUserDTO { + fun readUser(@PathParam("id") userId: Long): RestSourceUserDTO { val user = ensureUser(userId) auth.checkPermissionOnSubject(Permission.SUBJECT_READ, user.projectId, user.userId) return userMapper.fromEntity(user) @@ -97,7 +93,7 @@ class RestSourceUserResource( @DELETE @Path("{id}") - fun deleteUser(@PathParam("id") userId: Long) : Response { + fun deleteUser(@PathParam("id") userId: Long): Response { val user = ensureUser(userId) auth.checkPermissionOnSubject(Permission.SUBJECT_UPDATE, user.projectId, user.userId) if (user.accessToken != null) { @@ -114,17 +110,42 @@ class RestSourceUserResource( user: RestSourceUserDTO): RestSourceUserDTO { val existingUser = validate(userId, user, Permission.SUBJECT_UPDATE) - val updatedUser = userRepository.reset(existingUser, user.startDate, user.endDate ?: existingUser.endDate) + val updatedUser = userRepository.reset(existingUser, user.startDate, user.endDate + ?: existingUser.endDate) return userMapper.fromEntity(updatedUser) } - private fun validate(id: Long, user: RestSourceUserDTO, permission: Permission) : RestSourceUser { + @GET + @Path("{id}/token") + fun requestToken(@PathParam("id") userId: Long): TokenDTO { + val user = ensureUser(userId) + auth.checkPermissionOnSubject(Permission.MEASUREMENT_CREATE, user.projectId, user.userId) + return TokenDTO(user.accessToken, user.expiresAt) + } + + @POST + @Path("{id}/token") + fun refreshToken(@PathParam("id") userId: Long): TokenDTO { + val user = ensureUser(userId) + auth.checkPermissionOnSubject(Permission.MEASUREMENT_CREATE, user.projectId, user.userId) + val rft = user.refreshToken + ?: throw HttpConflictException("refresh_token_not_found", "No refresh-token found for user ${user.externalUserId} with source-type ${user.sourceType}") + + val updatedUser = userRepository.createOrUpdate(authorizationService.refreshToken(rft, user.sourceType), user.sourceType) + + return TokenDTO(updatedUser.accessToken, updatedUser.expiresAt) + } + + private fun validate(id: Long, user: RestSourceUserDTO, permission: Permission): RestSourceUser { val existingUser = ensureUser(id) - val projectId = user.projectId ?: throw HttpBadRequestException("missing_project_id", "project cannot be empty") - val userId = user.userId ?: throw HttpBadRequestException("missing_user_id", "subject-id/user-id cannot be empty") + val projectId = user.projectId + ?: throw HttpBadRequestException("missing_project_id", "project cannot be empty") + val userId = user.userId + ?: throw HttpBadRequestException("missing_user_id", "subject-id/user-id cannot be empty") auth.checkPermissionOnSubject(permission, projectId, userId) - projectService.projectUsers(projectId).find { it.id == userId } ?: throw HttpBadRequestException("user_not_found", "user $userId not found in project $projectId") + projectService.projectUsers(projectId).find { it.id == userId } + ?: throw HttpBadRequestException("user_not_found", "user $userId not found in project $projectId") return existingUser } From a42966a910f67d6a4a2163ed36050278edf43be1 Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 31 Aug 2020 21:59:00 +0200 Subject: [PATCH 14/80] add query params --- .../doa/RestSourceUserRepository.kt | 2 +- .../doa/RestSourceUserRepositoryImpl.kt | 54 ++++++++++++------- .../resources/RestSourceUserResource.kt | 18 +++---- 3 files changed, 42 insertions(+), 32 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index 46e5fcb7..aafa9c8d 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -11,7 +11,7 @@ interface RestSourceUserRepository { fun createOrUpdate(user: RestOauth2AccessToken, sourceType: String): RestSourceUser fun read(id: Long): RestSourceUser? fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser - fun query(page: Page, sourceType: String? = null, externalUserId: String? = null): Pair, Page> + fun query(page: Page, projectId: String?, sourceType: String?): Pair, Page> // fun findByExtenalId(sourceType: String, externalUserId: String) : RestSourceUser? // fun findAllBySourceType(sourceType: String?): List fun delete(user: RestSourceUser) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index f510d30b..b630ea0b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -9,7 +9,6 @@ import java.time.Duration import java.time.Instant import javax.inject.Provider import javax.persistence.EntityManager -import javax.persistence.Transient import javax.ws.rs.core.Context class RestSourceUserRepositoryImpl( @@ -58,37 +57,54 @@ class RestSourceUserRepositoryImpl( }.also { merge(it) } } - override fun query(page: Page, sourceType: String?, externalUserId: String?): Pair, Page> { + override fun query(page: Page, projectId: String?, sourceType: String?): Pair, Page> { var queryString = "SELECT u FROM RestSourceUser u" var countQueryString = "SELECT count(u) FROM RestSourceUser u" + when { + projectId != null && sourceType != null -> { + queryString += " WHERE u.projectId = :projectId AND u.sourceType = :sourceType" + countQueryString += " WHERE u.projectId = :projectId AND u.sourceType = :sourceType" + } + projectId != null && sourceType == null -> { + queryString += " WHERE u.projectId = :projectId" + countQueryString += " WHERE u.projectId = :projectId" + } + projectId == null && sourceType != null -> { + queryString += " WHERE u.sourceType = :sourceType" + countQueryString += " WHERE u.sourceType = :sourceType" + } + } val actualPage = page.createValid(maximum = 100) return em.get().transact { val query = createQuery(queryString, RestSourceUser::class.java) -// .setParameter("projectId", projectId) .setFirstResult(actualPage.offset) .setMaxResults(actualPage.pageSize!!) val countQuery = createQuery(countQueryString) -// .setParameter("projectId", projectId) - -// userId?.let { -// query.setParameter("userId", it) -// countQuery.setParameter("userId", it) -// } -// status?.let { -// query.setParameter("status", RecordStatus.valueOf(it)) -// countQuery.setParameter("status", RecordStatus.valueOf(it)) -// } -// sourceType?.let { -// query.setParameter("sourceType", it) -// countQuery.setParameter("sourceType", it) -// } - val records = query.resultList + + when { + projectId != null && sourceType != null -> { + query.setParameter("projectId", projectId) + countQuery.setParameter("projectId", projectId) + query.setParameter("sourceType", sourceType) + countQuery.setParameter("sourceType", sourceType) + } + projectId != null && sourceType == null -> { + query.setParameter("projectId", projectId) + countQuery.setParameter("projectId", projectId) + } + projectId == null && sourceType != null -> { + query.setParameter("sourceType", sourceType) + countQuery.setParameter("sourceType", sourceType) + } + } + + val users = query.resultList val count = countQuery.singleResult as Long - Pair(records, actualPage.copy(totalElements = count)) + Pair(users, actualPage.copy(totalElements = count)) } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt index f0d63765..77a949e1 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt @@ -38,22 +38,16 @@ class RestSourceUserResource( @GET fun query( @QueryParam("projectId") projectId: String?, - @QueryParam("userId") userId: String?, - @QueryParam("size") pageSize: Int?, - @DefaultValue("1") @QueryParam("page") pageNumber: Int, @QueryParam("sourceType") sourceType: String?, - @QueryParam("externalId") externalId: String?): RestSourceUsers { -// projectId -// ?: throw HttpBadRequestException("missing_project", "Required project ID not provided.") + @QueryParam("size") pageSize: Int?, + @DefaultValue("1") @QueryParam("page") pageNumber: Int): RestSourceUsers { -// if (userId != null) { -// auth.checkPermissionOnSubject(Permission.SUBJECT_READ, projectId, userId) -// } else { -// auth.checkPermissionOnProject(Permission.PROJECT_READ, projectId) -// } + if (projectId != null) { + auth.checkPermissionOnProject(Permission.PROJECT_READ, projectId) + } val queryPage = Page(pageNumber = pageNumber, pageSize = pageSize) - val (records, page) = userRepository.query(queryPage) + val (records, page) = userRepository.query(queryPage, projectId, sourceType) return userMapper.fromRestSourceUsers(records, page) } From ab194e56d5baae7fb492bce02a8321e55d601322 Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 31 Aug 2020 22:09:07 +0200 Subject: [PATCH 15/80] add healthcheck and remove old code --- .../java/org/radarbase/authorizer/Main.kt | 14 +- .../RadarRestSourceAuthorizerApplication.java | 68 ------ .../doa/RestSourceUserRepository.kt | 15 +- .../resources/HeathCheckResource.kt | 23 ++ ...ojectService.kt => RadarProjectService.kt} | 0 .../service/RestSourceClientService.java | 215 ----------------- .../service/RestSourceUserService.java | 220 ------------------ .../webapp/resource/HealthCheckResource.java | 25 -- .../resource/RestSourceUserResource.java | 132 ----------- 9 files changed, 36 insertions(+), 676 deletions(-) delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt rename authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/{RestSourcesProjectService.kt => RadarProjectService.kt} (100%) delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt index bb4bdc11..1a0aea13 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt @@ -1,6 +1,6 @@ /* * - * * Copyright 2019 The Hyve + * * Copyright 2020 The Hyve * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. @@ -19,8 +19,6 @@ package org.radarbase.authorizer -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.KotlinModule import org.radarbase.jersey.GrizzlyServer import org.radarbase.jersey.config.ConfigLoader import org.slf4j.Logger @@ -29,11 +27,9 @@ import org.slf4j.LoggerFactory val logger: Logger = LoggerFactory.getLogger("org.radarbase.authorizer.Main") fun main(args: Array) { -// val mapper = ObjectMapper(YAMLFactory()) -// .registerModule(KotlinModule()) - val config: Config = ConfigLoader.loadConfig("authorizer.yml", args) - val resources = ConfigLoader.loadResources(config.service.resourceConfig, config) + val config: Config = ConfigLoader.loadConfig("authorizer.yml", args) + val resources = ConfigLoader.loadResources(config.service.resourceConfig, config) - val server = GrizzlyServer(config.service.baseUri, resources) - server.listen() + val server = GrizzlyServer(config.service.baseUri, resources) + server.listen() } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java deleted file mode 100644 index f7b021aa..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/RadarRestSourceAuthorizerApplication.java +++ /dev/null @@ -1,68 +0,0 @@ -///* -// * -// * * Copyright 2018 The Hyve -// * * -// * * Licensed under the Apache License, Version 2.0 (the "License"); -// * * you may not use this file except in compliance with the License. -// * * You may obtain a copy of the License at -// * * -// * * http://www.apache.org/licenses/LICENSE-2.0 -// * * -// * * Unless required by applicable law or agreed to in writing, software -// * * distributed under the License is distributed on an "AS IS" BASIS, -// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * * See the License for the specific language governing permissions and -// * * limitations under the License. -// * * -// * -// */ -// -//package org.radarbase.authorizer; -// -//import java.util.List; -//import java.util.stream.Stream; -// -//import org.radarbase.authorizer.config.RestSourceAuthorizerProperties; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.SpringApplication; -//import org.springframework.boot.autoconfigure.SpringBootApplication; -//import org.springframework.boot.context.properties.EnableConfigurationProperties; -//import org.springframework.context.annotation.Bean; -//import org.springframework.web.cors.CorsConfiguration; -//import org.springframework.web.servlet.config.annotation.CorsRegistry; -//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -//import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; -// -//@SpringBootApplication -//@EnableConfigurationProperties({RestSourceAuthorizerProperties.class}) -//public class RadarRestSourceAuthorizerApplication { -// -// @Autowired -// private RestSourceAuthorizerProperties authorizerApplicationProperties; -// -// public static void main(String[] args) { -// SpringApplication.run(RadarRestSourceAuthorizerApplication.class, args); -// } -// -// @Bean -// public WebMvcConfigurer corsConfigurer() { -// return new WebMvcConfigurerAdapter() { -// @Override -// public void addCorsMappings(CorsRegistry registry) { -// -// CorsConfiguration corsConfiguration = authorizerApplicationProperties.getCors(); -// Stream.of("/users/**", "/source-clients/**").forEach(p -> registry.addMapping(p) -// .allowedOrigins(listToArray(corsConfiguration.getAllowedOrigins())) -// .allowedMethods(listToArray(corsConfiguration.getAllowedMethods())) -// .allowedHeaders(listToArray(corsConfiguration.getAllowedHeaders())) -// .allowCredentials(corsConfiguration.getAllowCredentials())); -// -// } -// }; -// } -// -// private String[] listToArray(List list) { -// return list.stream().toArray(String[]::new); -// } -// -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index aafa9c8d..d45da616 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -8,12 +8,13 @@ import java.time.Instant interface RestSourceUserRepository { - fun createOrUpdate(user: RestOauth2AccessToken, sourceType: String): RestSourceUser - fun read(id: Long): RestSourceUser? - fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser - fun query(page: Page, projectId: String?, sourceType: String?): Pair, Page> -// fun findByExtenalId(sourceType: String, externalUserId: String) : RestSourceUser? + fun createOrUpdate(user: RestOauth2AccessToken, sourceType: String): RestSourceUser + fun read(id: Long): RestSourceUser? + fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser + fun query(page: Page, projectId: String? = null, sourceType: String? = null): Pair, Page> + + // fun findByExtenalId(sourceType: String, externalUserId: String) : RestSourceUser? // fun findAllBySourceType(sourceType: String?): List - fun delete(user: RestSourceUser) - fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?): RestSourceUser + fun delete(user: RestSourceUser) + fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?): RestSourceUser } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt new file mode 100644 index 00000000..38f0b126 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt @@ -0,0 +1,23 @@ +package org.radarbase.authorizer.resources + +import org.radarbase.authorizer.api.Page +import org.radarbase.authorizer.doa.RestSourceUserRepository +import javax.annotation.Resource +import javax.inject.Singleton +import javax.ws.rs.GET +import javax.ws.rs.Path +import javax.ws.rs.core.Context +import javax.ws.rs.core.Response + +@Path("health") +@Resource +@Singleton +class HealthCheckResource( + @Context private var userRepository: RestSourceUserRepository +) { + @GET + fun check(): Response { + userRepository.query(Page(0, 1)) + return Response.ok("Initialized dao and made a sample query...").build() + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourcesProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt similarity index 100% rename from authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourcesProjectService.kt rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.java deleted file mode 100644 index b169ae2c..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceClientService.java +++ /dev/null @@ -1,215 +0,0 @@ -///* -// * -// * * Copyright 2018 The Hyve -// * * -// * * Licensed under the Apache License, Version 2.0 (the "License"); -// * * you may not use this file except in compliance with the License. -// * * You may obtain a copy of the License at -// * * -// * * http://www.apache.org/licenses/LICENSE-2.0 -// * * -// * * Unless required by applicable law or agreed to in writing, software -// * * distributed under the License is distributed on an "AS IS" BASIS, -// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * * See the License for the specific language governing permissions and -// * * limitations under the License. -// * * -// * -// */ -// -//package org.radarbase.authorizer.service; -// -//import com.fasterxml.jackson.core.type.TypeReference; -//import java.io.IOException; -//import java.util.ArrayList; -//import java.util.List; -//import java.util.Map; -//import java.util.Objects; -//import java.util.concurrent.TimeUnit; -//import java.util.stream.Collectors; -//import javax.annotation.PostConstruct; -//import javax.naming.ConfigurationException; -//import javax.validation.constraints.NotNull; -// -//import com.fasterxml.jackson.databind.DeserializationFeature; -//import com.fasterxml.jackson.databind.ObjectMapper; -//import okhttp3.Credentials; -//import okhttp3.FormBody; -//import okhttp3.OkHttpClient; -//import okhttp3.Request; -//import okhttp3.Response; -//import okhttp3.ResponseBody; -//import org.radarbase.authorizer.config.ConfigHelper; -//import org.radarbase.authorizer.config.RestSourceClientConfig; -//import org.radarbase.authorizer.config.RestSourceAuthorizerProperties; -//import org.radarbase.authorizer.service.dto.RestSourceAccessToken; -//import org.radarbase.authorizer.service.dto.RestSourceClientDetailsDTO; -//import org.radarbase.authorizer.service.dto.RestSourceClients; -//import org.radarbase.authorizer.webapp.exception.BadGatewayException; -//import org.radarbase.authorizer.webapp.exception.InvalidSourceTypeException; -//import org.radarbase.authorizer.webapp.exception.TokenException; -//import org.slf4j.Logger; -//import org.slf4j.LoggerFactory; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.stereotype.Service; -// -//@Service -//public class RestSourceClientService { -// -// private static final Logger LOGGER = LoggerFactory.getLogger(RestSourceClientService.class); -// -// @Autowired -// private RestSourceAuthorizerProperties restSourceAuthorizerProperties; -// -// private OkHttpClient client; -// -// private ObjectMapper mapper; -// -// // contains private data -// private Map configMap; -// -// // contains sharable public data -// private Map clientDetailsDTOMap; -// -// @PostConstruct -// public void init() throws ConfigurationException { -// String path = restSourceAuthorizerProperties.getSourceClientsFilePath(); -// if (Objects.isNull(path) || path.equals("")) { -// LOGGER.info("No source clients file specified, not loading source clients"); -// return; -// } -// List restSourceClientConfigs = loadDeviceClientConfigs(path); -// -// this.configMap = restSourceClientConfigs.stream() -// .collect(Collectors.toMap(RestSourceClientConfig::getSourceType, p -> p)); -// this.clientDetailsDTOMap = restSourceClientConfigs.stream() -// .collect(Collectors.toMap(RestSourceClientConfig::getSourceType, -// RestSourceClientDetailsDTO::new)); -// -// LOGGER.info("Source client configs loaded..."); -// this.client = new OkHttpClient.Builder() -// .connectTimeout(10, TimeUnit.SECONDS) -// .writeTimeout(10, TimeUnit.SECONDS) -// .readTimeout(30, TimeUnit.SECONDS) -// .build(); -// this.mapper = new ObjectMapper() -// .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); -// -// } -// -// private List loadDeviceClientConfigs(@NotNull String path) throws -// ConfigurationException { -// org.radarbase.authorizer.config.RestSourceClients restSourceClients = -// ConfigHelper.loadPropertiesFromFile(path, -// new TypeReference() {}); -// -// if (restSourceClients.getRestSourceClients() == null) { -// LOGGER.error("No valid configurations available on configured path. Please " -// + "check the syntax and file name"); -// throw new ConfigurationException( -// "No valid source-client configs are provided" + "."); -// } -// return restSourceClients.getRestSourceClients(); -// } -// -// -// public RestSourceClients getAllRestSourceClientDetails() { -// return new RestSourceClients() -// .sourceClients(new ArrayList<>(this.clientDetailsDTOMap.values())); -// } -// -// public List getAvailableDeviceTypes() { -// return new ArrayList<>(this.clientDetailsDTOMap.keySet()); -// } -// -// private RestSourceClientConfig getClientAuthorizationConfig(String sourceType) { -// if (this.configMap.containsKey(sourceType)) { -// return this.configMap.get(sourceType); -// } else { -// throw new InvalidSourceTypeException(sourceType); -// } -// } -// -// public RestSourceClientDetailsDTO getAllRestSourceClientDetails(String sourceType) { -// return this.clientDetailsDTOMap.get(sourceType); -// } -// -// -// RestSourceAccessToken getAccessTokenWithAuthorizeCode(String code, String sourceType) { -// -// RestSourceClientConfig authorizationConfig = getClientAuthorizationConfig(sourceType); -// FormBody form = new FormBody.Builder() -// .add("code", code) -// .add("grant_type", "authorization_code") -// .add("client_id", authorizationConfig.getClientId()) -// .build(); -// LOGGER.info("Requesting access token with authorization code"); -// return processTokenRequest(form, authorizationConfig); -// -// } -// -// private RestSourceAccessToken processTokenRequest(FormBody form, -// RestSourceClientConfig authorizationConfig) { -// String credentials = Credentials -// .basic(authorizationConfig.getClientId(), authorizationConfig.getClientSecret()); -// -// Request request = new Request.Builder().addHeader("Accept", "application/json") -// .addHeader("Authorization", credentials) -// .addHeader("Content-Type", "application/x-www-form-urlencoded") -// .url(authorizationConfig.getTokenEndpoint()) -// .post(form) -// .build(); -// -// try (Response response = client.newCall(request).execute()) { -// if (response.isSuccessful()) { -// ResponseBody responseBody = response.body(); -// if (responseBody == null) { -// throw new BadGatewayException("No response from server"); -// } -// -// try { -// return mapper.readValue(responseBody.string(), RestSourceAccessToken.class); -// } catch (IOException e) { -// throw new TokenException("Cannot read token response"); -// } -// } else { -// throw new BadGatewayException( -// "Failed to execute the request : Response-code :" + response.code() -// + " received when requesting token from server with " + "message " -// + response.message()); -// } -// } catch (IOException e) { -// throw new BadGatewayException(e); -// } -// } -// -// RestSourceAccessToken refreshToken(String refreshToken, String sourceType) { -// FormBody form = new FormBody.Builder() -// .add("grant_type", "refresh_token") -// .add("refresh_token", refreshToken) -// .build(); -// LOGGER.info("Requesting to refreshToken"); -// return processTokenRequest(form, getClientAuthorizationConfig(sourceType)); -// } -// -// boolean revokeToken(String accessToken, String sourceType) { -// -// RestSourceClientConfig authorizationConfig = getClientAuthorizationConfig(sourceType); -// FormBody form = new FormBody.Builder().add("token", accessToken).build(); -// LOGGER.info("Requesting to revoke access token"); -// String credentials = Credentials -// .basic(authorizationConfig.getClientId(), authorizationConfig.getClientSecret()); -// -// Request request = new Request.Builder().addHeader("Accept", "application/json") -// .addHeader("Authorization", credentials) -// .addHeader("Content-Type", "application/x-www-form-urlencoded") -// .url(authorizationConfig.getTokenEndpoint()).post(form).build(); -// -// try (Response response = client.newCall(request).execute()) { -// return response.isSuccessful(); -// } catch (IOException e) { -// throw new BadGatewayException(e); -// } -// -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.java deleted file mode 100644 index e3fe758a..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.java +++ /dev/null @@ -1,220 +0,0 @@ -///* -// * -// * * Copyright 2018 The Hyve -// * * -// * * Licensed under the Apache License, Version 2.0 (the "License"); -// * * you may not use this file except in compliance with the License. -// * * You may obtain a copy of the License at -// * * -// * * http://www.apache.org/licenses/LICENSE-2.0 -// * * -// * * Unless required by applicable law or agreed to in writing, software -// * * distributed under the License is distributed on an "AS IS" BASIS, -// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * * See the License for the specific language governing permissions and -// * * limitations under the License. -// * * -// * -// */ -// -//package org.radarbase.authorizer.service; -// -//import java.time.Instant; -//import java.util.Optional; -//import java.util.stream.Collectors; -//import javax.validation.constraints.NotNull; -//import org.radarbase.authorizer.doa.RestSourceUser; -//import org.radarbase.authorizer.doa.RestSourceUserRepository; -//import org.radarbase.authorizer.service.dto.RestSourceAccessToken; -//import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; -//import org.radarbase.authorizer.service.dto.RestSourceUsers; -//import org.radarbase.authorizer.service.dto.TokenDTO; -//import org.radarbase.authorizer.webapp.exception.NotFoundException; -//import org.radarbase.authorizer.webapp.exception.TokenException; -//import org.slf4j.Logger; -//import org.slf4j.LoggerFactory; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.stereotype.Service; -//import org.springframework.transaction.annotation.Transactional; -// -//@Service -//@Transactional -//public class RestSourceUserService { -// -// private final Logger log = LoggerFactory.getLogger(RestSourceUserService.class); -// -// @Autowired -// private RestSourceUserRepository restSourceUserRepository; -// -// @Autowired -// private RestSourceClientService authorizationService; -// -// @Transactional(readOnly = true) -// public RestSourceUsers getAllRestSourceUsers() { -// log.debug("Querying all saved source users"); -// return new RestSourceUsers() -// .users(this.restSourceUserRepository.findAll() -// .stream() -// .map(RestSourceUserPropertiesDTO::new) -// .collect(Collectors.toList())); -// } -// -// public RestSourceUserPropertiesDTO save(RestSourceUserPropertiesDTO restSourceUserPropertiesDTO) { -// RestSourceUser restSourceUser = -// this.restSourceUserRepository.save(new RestSourceUser(restSourceUserPropertiesDTO)); -// return new RestSourceUserPropertiesDTO(restSourceUser); -// } -// -// @Transactional -// public RestSourceUserPropertiesDTO authorizeAndStoreDevice(@NotNull String code, -// @NotNull String sourceType) { -// RestSourceAccessToken accessToken = -// authorizationService.getAccessTokenWithAuthorizeCode(code, sourceType); -// -// if (accessToken != null) { -// -// Optional existingUser = restSourceUserRepository -// .findBySourceTypeAndExternalUserId(sourceType, accessToken.getExternalUserId()); -// -// RestSourceUser resultUser; -// if (existingUser.isPresent()) { -// resultUser = existingUser.get(); -// resultUser.safeUpdateTokenDetails(accessToken); -// } else { -// resultUser = new RestSourceUser() -// .authorized(true) -// .externalUserId(accessToken.getExternalUserId()) -// .sourceType(sourceType) -// .startDate(Instant.now()); -// resultUser.safeUpdateTokenDetails(accessToken); -// -// resultUser = this.restSourceUserRepository.save(resultUser); -// } -// return new RestSourceUserPropertiesDTO(resultUser); -// } else { -// log.error("Cannot get token a using authorization_code"); -// throw new TokenException(); -// } -// } -// -// @Transactional(readOnly = true) -// public RestSourceUserPropertiesDTO getRestSourceUserById(Long id) { -// Optional user = restSourceUserRepository.findById(id); -// -// if (user.isPresent()) { -// return new RestSourceUserPropertiesDTO(user.get()); -// } else { -// throw new NotFoundException("RestSourceUser not found with id " + id); -// } -// } -// -// @Transactional -// public RestSourceUserPropertiesDTO updateRestSourceUser(Long id, -// RestSourceUserPropertiesDTO sourceUserPropertiesDTO) { -// -// Optional sourceUser = restSourceUserRepository.findById(id); -// -// if (sourceUser.isPresent()) { -// RestSourceUser restSourceUserToSave = sourceUser.get(); -// restSourceUserToSave.safeUpdateProperties(sourceUserPropertiesDTO); -// return new RestSourceUserPropertiesDTO(restSourceUserRepository.save(restSourceUserToSave)); -// } else { -// throw new NotFoundException( -// "Unable to update rest source user. RestSourceUser not found with " + "id " -// + sourceUserPropertiesDTO.getId()); -// } -// -// } -// -// /** -// * Removes user from database and revokes access token and refresh token -// * -// * @param id userID -// */ -// @Transactional -// public void revokeTokenAndDeleteUser(Long id) { -// Optional user = restSourceUserRepository.findById(id); -// -// if (user.isPresent()) { -// RestSourceUser restSourceUser = user.get(); -// authorizationService -// .revokeToken(restSourceUser.getAccessToken(), restSourceUser.getSourceType()); -// restSourceUserRepository.deleteById(id); -// -// } else { -// throw new NotFoundException("RestSourceUser not found with id " + id); -// } -// } -// -// @Transactional(readOnly = true) -// public TokenDTO getDeviceTokenByUserId(Long id) { -// Optional user = restSourceUserRepository.findById(id); -// -// if (user.isPresent()) { -// RestSourceUser restSourceUser = user.get(); -// return new TokenDTO() -// .accessToken(restSourceUser.getAccessToken()) -// .expiresAt(restSourceUser.getExpiresAt()); -// } else { -// throw new NotFoundException("RestSourceUser not found with id " + id); -// } -// } -// -// @Transactional -// public TokenDTO refreshTokenForUser(Long id) { -// Optional user = restSourceUserRepository.findById(id); -// if (user.isPresent()) { -// RestSourceUser restSourceUser = user.get(); -// // refresh token by user id and source-type -// RestSourceAccessToken accessToken = authorizationService -// .refreshToken(restSourceUser.getRefreshToken(), restSourceUser.getSourceType()); -// // update token -// if (accessToken != null) { -// restSourceUser.safeUpdateTokenDetails(accessToken); -// restSourceUser = this.restSourceUserRepository.save(restSourceUser); -// return new TokenDTO() -// .accessToken(restSourceUser.getAccessToken()) -// .expiresAt(restSourceUser.getExpiresAt()); -// } else { -// throw new TokenException("Could not refresh token successfully"); -// } -// -// } else { -// throw new NotFoundException("RestSourceUser not found with id " + id); -// } -// } -// -// @Transactional(readOnly = true) -// public RestSourceUsers getAllUsersBySourceType(String sourceType) { -// log.debug("Querying all saved users by source-type {}", sourceType); -// return new RestSourceUsers() -// .users(this.restSourceUserRepository.findAllBySourceType(sourceType) -// .stream() -// .map(RestSourceUserPropertiesDTO::new) -// .collect(Collectors.toList())); -// } -// -// /** -// * This resets the user by updating the current version. Currently, the version is calculated as a -// * String based on the current time instant. This may change in the future. -// * -// * @param id the database ID of the User -// * @return The updated User details -// */ -// public RestSourceUserPropertiesDTO resetUser(Long id) { -// Optional sourceUser = restSourceUserRepository.findById(id); -// -// if (sourceUser.isPresent()) { -// RestSourceUser restSourceUserToSave = sourceUser.get(); -// return new RestSourceUserPropertiesDTO( -// restSourceUserRepository.save( -// restSourceUserToSave -// .version(Instant.now().toString()) -// .setTimesReset(restSourceUserToSave.getTimesReset() + 1))); -// } else { -// throw new NotFoundException( -// "Unable to reset rest source user. RestSourceUser not found with " + "id " -// + id); -// } -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java deleted file mode 100644 index 927a634e..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/HealthCheckResource.java +++ /dev/null @@ -1,25 +0,0 @@ -//package org.radarbase.authorizer.webapp.resource; -// -//import org.radarbase.authorizer.service.RestSourceClientService; -//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.RestController; -// -//@RestController -//public class HealthCheckResource { -// private Logger logger = LoggerFactory.getLogger(HealthCheckResource.class); -// -// @Autowired -// private RestSourceClientService restSourceClientService; -// -// @GetMapping("/health") -// public ResponseEntity getAllDeviceProperties() { -// logger.debug("Health check"); -// this.restSourceClientService.getAllRestSourceClientDetails(); -// return ResponseEntity.ok("Alive"); -// } -// -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java deleted file mode 100644 index 7ea45f10..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResource.java +++ /dev/null @@ -1,132 +0,0 @@ -///* -// * -// * * Copyright 2018 The Hyve -// * * -// * * Licensed under the Apache License, Version 2.0 (the "License"); -// * * you may not use this file except in compliance with the License. -// * * You may obtain a copy of the License at -// * * -// * * http://www.apache.org/licenses/LICENSE-2.0 -// * * -// * * Unless required by applicable law or agreed to in writing, software -// * * distributed under the License is distributed on an "AS IS" BASIS, -// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * * See the License for the specific language governing permissions and -// * * limitations under the License. -// * * -// * -// */ -// -//package org.radarbase.authorizer.webapp.resource; -// -//import java.net.URI; -//import java.net.URISyntaxException; -//import java.util.Optional; -//import javax.validation.Valid; -//import org.radarbase.authorizer.service.RestSourceUserService; -//import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; -//import org.radarbase.authorizer.service.dto.RestSourceUsers; -//import org.radarbase.authorizer.service.dto.TokenDTO; -//import org.radarbase.authorizer.validation.Validator; -//import org.radarbase.authorizer.validation.exception.ValidationFailedException; -//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.DeleteMapping; -//import org.springframework.web.bind.annotation.GetMapping; -//import org.springframework.web.bind.annotation.PathVariable; -//import org.springframework.web.bind.annotation.PostMapping; -//import org.springframework.web.bind.annotation.RequestBody; -//import org.springframework.web.bind.annotation.RequestParam; -//import org.springframework.web.bind.annotation.RestController; -// -//@RestController -//public class RestSourceUserResource { -// -// private final RestSourceUserService restSourceUserService; -// private Logger logger = LoggerFactory.getLogger(RestSourceUserResource.class); -// private Validator validator; -// -// @Autowired -// public RestSourceUserResource( -// RestSourceUserService restSourceUserService, -// Optional validator) { -// this.restSourceUserService = restSourceUserService; -// this.validator = validator.orElse(null); -// } -// -// @PostMapping("/users") -// public ResponseEntity addAuthorizedRestSourceUser(@RequestParam(value = "code") String code, -// @RequestParam(value = "state") String state) throws URISyntaxException { -// logger.debug("Add a rest-source user with code {} and state {}", code, state); -// RestSourceUserPropertiesDTO -// user = this.restSourceUserService.authorizeAndStoreDevice(code, state); -// return ResponseEntity -// .created(new URI("/user/" + user.getId())).body(user); -// } -// -// @GetMapping("/users") -// public ResponseEntity getAllRestSources( -// @RequestParam(value = "source-type", required = false) String sourceType) { -// if (sourceType != null && !sourceType.isEmpty()) { -// logger.debug("Get all rest source users by type {}", sourceType); -// return ResponseEntity -// .ok(this.restSourceUserService.getAllUsersBySourceType(sourceType)); -// } -// -// logger.debug("Get all rest source users"); -// return ResponseEntity -// .ok(this.restSourceUserService.getAllRestSourceUsers()); -// } -// -// @GetMapping("/users/{id}") -// public ResponseEntity getRestSourceUserById( -// @PathVariable String id) { -// logger.debug("Get rest source user with id {}", id); -// return ResponseEntity -// .ok(this.restSourceUserService.getRestSourceUserById(Long.valueOf(id))); -// } -// -// @PostMapping("/users/{id}") -// public ResponseEntity updateDeviceUser(@Valid @PathVariable String id, -// @RequestBody RestSourceUserPropertiesDTO restSourceUser, -// @RequestParam(value = "validate", defaultValue = "false") Boolean isValidate) { -// logger.debug("Requesting to update rest source user"); -// if (isValidate && validator != null && !validator.validate(restSourceUser)) { -// logger.warn("Validation Failed for: {}", restSourceUser); -// throw new ValidationFailedException(restSourceUser, validator); -// } -// return ResponseEntity -// .ok(this.restSourceUserService.updateRestSourceUser(Long.valueOf(id), restSourceUser)); -// } -// -// @PostMapping("/users/{id}/reset") -// public ResponseEntity resetDeviceUser(@Valid @PathVariable String id) { -// logger.debug("Requesting to reset rest source user"); -// return ResponseEntity.ok(this.restSourceUserService.resetUser(Long.valueOf(id))); -// } -// -// @DeleteMapping("/users/{id}") -// public ResponseEntity deleteDeviceUser(@Valid @PathVariable String id) { -// logger.debug("Requesting to delete rest source user"); -// this.restSourceUserService.revokeTokenAndDeleteUser(Long.valueOf(id)); -// return ResponseEntity -// .ok().header("user-removed", id).build(); -// } -// -// -// @GetMapping("/users/{id}/token") -// public ResponseEntity getUserToken(@PathVariable String id) { -// logger.debug("Get user token for rest source user id {}", id); -// return ResponseEntity -// .ok(this.restSourceUserService.getDeviceTokenByUserId(Long.valueOf(id))); -// } -// -// @PostMapping("/users/{id}/token") -// public ResponseEntity requestRefreshTokenForUser(@PathVariable String id) { -// logger.debug("Refreshing user token for rest source user id {}", id); -// return ResponseEntity -// .ok(this.restSourceUserService.refreshTokenForUser(Long.valueOf(id))); -// } -//} From 13b8a7e7d9cdeb0164cd3685b85a78d7a8dabcf9 Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 31 Aug 2020 22:14:58 +0200 Subject: [PATCH 16/80] remove old code --- .../authorizer/api/RestSourceClientMapper.kt | 2 +- .../authorizer/doa/RestSourceUser.java | 277 ------------------ .../doa/RestSourceUserRepository.kt | 3 - .../doa/RestSourceUserRepositoryImpl.kt | 1 - .../inject/AuthorizerResourceEnhancer.kt | 1 - .../CustomWebSecurityConfigurerAdapter.java | 43 --- .../security/JwtAuthenticationFilter.java | 86 ------ .../managementportal/MPProjectService.kt | 2 +- .../radarbase/authorizer/util/CachedSet.kt | 2 +- 9 files changed, 3 insertions(+), 414 deletions(-) delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUser.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt index 0c563484..d1294e08 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt @@ -3,7 +3,7 @@ package org.radarbase.authorizer.api import org.radarbase.authorizer.RestSourceClient class RestSourceClientMapper { - fun fromSourceClientConfig(client: RestSourceClient)= ShareableClientDetail( + private fun fromSourceClientConfig(client: RestSourceClient)= ShareableClientDetail( clientId = client.clientId, sourceType = client.sourceType, scope = client.scope, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUser.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUser.java deleted file mode 100644 index 4b745dac..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUser.java +++ /dev/null @@ -1,277 +0,0 @@ -///* -// * -// * * Copyright 2018 The Hyve -// * * -// * * Licensed under the Apache License, Version 2.0 (the "License"); -// * * you may not use this file except in compliance with the License. -// * * You may obtain a copy of the License at -// * * -// * * http://www.apache.org/licenses/LICENSE-2.0 -// * * -// * * Unless required by applicable law or agreed to in writing, software -// * * distributed under the License is distributed on an "AS IS" BASIS, -// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * * See the License for the specific language governing permissions and -// * * limitations under the License. -// * * -// * -// */ -// -//package org.radarbase.authorizer.doa; -// -//import java.time.Duration; -//import java.time.Instant; -//import java.util.UUID; -//import javax.persistence.Entity; -//import javax.persistence.GeneratedValue; -//import javax.persistence.GenerationType; -//import javax.persistence.Id; -//import javax.persistence.Table; -//import javax.persistence.Transient; -// -//@Entity -//@Table(name = "rest_source_user") -//public class RestSourceUser { -// -// @Transient -// private static final Duration EXPIRY_TIME_MARGIN = Duration.ofMinutes(5); -// -// @Id -// @GeneratedValue(strategy = GenerationType.IDENTITY) -// private long id; -// -// // The version to be appended to ID for RESET of a user -// // This should be updated whenever the user is RESET. -// // By default this is null for backwards compatibility -// private String version = null; -// -// // The number of times a user has been reset -// private long timesReset = 0; -// -// // Project ID to be used in org.radarcns.kafka.ObservationKey record keys -// private String projectId; -// -// // User ID to be used in org.radarcns.kafka.ObservationKey record keys -// private String userId; -// -// // Source ID to be used in org.radarcns.kafka.ObservationKey record keys -// private String sourceId; -// -// // Date from when to collect data. -// private Instant startDate; -// -// // Date until when to collect data. -// private Instant endDate; -// -// -// private String sourceType; -// -// // is authorized by user -// private Boolean authorized = false; -// -// private String externalUserId; -// -// private String accessToken; -// -// private String refreshToken; -// -// private Integer expiresIn; -// -// private Instant expiresAt; -// -// private String tokenType; -// -// public RestSourceUser() { -// if (this.sourceId == null) { -// this.sourceId = UUID.randomUUID().toString(); -// } -// } -// -// public RestSourceUser(RestSourceUserPropertiesDTO restSourceUserPropertiesDTO) { -// if (restSourceUserPropertiesDTO.getId() != null) { -// this.id = Long.valueOf(restSourceUserPropertiesDTO.getId()); -// } -// this.projectId = restSourceUserPropertiesDTO.getProjectId(); -// this.userId = restSourceUserPropertiesDTO.getUserId(); -// this.sourceId = restSourceUserPropertiesDTO.getSourceId(); -// this.sourceType = restSourceUserPropertiesDTO.getSourceType(); -// this.startDate = restSourceUserPropertiesDTO.getStartDate(); -// this.endDate = restSourceUserPropertiesDTO.getEndDate(); -// this.externalUserId = restSourceUserPropertiesDTO.getExternalUserId(); -// this.authorized = restSourceUserPropertiesDTO.isAuthorized(); -// } -// -// public long getId() { -// return id; -// } -// -// public RestSourceUser id(Long id) { -// this.id = id; -// return this; -// } -// -// public String getVersion() { -// return version; -// } -// -// public RestSourceUser version(String version) { -// this.version = version; -// return this; -// } -// -// public long getTimesReset() { -// return timesReset; -// } -// -// public RestSourceUser setTimesReset(long timesReset) { -// this.timesReset = timesReset; -// return this; -// } -// -// public String getProjectId() { -// return projectId; -// } -// -// public RestSourceUser projectId(String projectId) { -// this.projectId = projectId; -// return this; -// } -// -// public String getUserId() { -// return userId; -// } -// -// public RestSourceUser userId(String userId) { -// this.userId = userId; -// return this; -// } -// -// public String getSourceId() { -// return sourceId; -// } -// -// public RestSourceUser sourceId(String sourceId) { -// this.sourceId = sourceId; -// return this; -// } -// -// public Instant getStartDate() { -// return startDate; -// } -// -// public RestSourceUser startDate(Instant startDate) { -// this.startDate = startDate; -// return this; -// } -// -// public Instant getEndDate() { -// return endDate; -// } -// -// public RestSourceUser endDate(Instant endDate) { -// this.endDate = endDate; -// return this; -// } -// -// public String getSourceType() { -// return sourceType; -// } -// -// public RestSourceUser sourceType(String sourceType) { -// this.sourceType = sourceType; -// return this; -// } -// -// public Boolean getAuthorized() { -// return authorized; -// } -// -// public RestSourceUser authorized(Boolean authorized) { -// this.authorized = authorized; -// return this; -// } -// -// public String getExternalUserId() { -// return externalUserId; -// } -// -// public RestSourceUser externalUserId(String externalUserId) { -// this.externalUserId = externalUserId; -// return this; -// } -// -// public String getAccessToken() { -// return accessToken; -// } -// -// public RestSourceUser accessToken(String accessToken) { -// this.accessToken = accessToken; -// return this; -// } -// -// public String getRefreshToken() { -// return refreshToken; -// } -// -// public RestSourceUser refreshToken(String refreshToken) { -// this.refreshToken = refreshToken; -// return this; -// } -// -// public Integer getExpiresIn() { -// return expiresIn; -// } -// -// public RestSourceUser expiresIn(Integer expiresIn) { -// this.expiresIn = expiresIn; -// return this; -// } -// -// public Instant getExpiresAt() { -// return expiresAt; -// } -// -// public RestSourceUser expiresIn(Instant expiresAt) { -// this.expiresAt = expiresAt; -// return this; -// } -// -// public String getTokenType() { -// return tokenType; -// } -// -// public RestSourceUser tokenType(String tokenType) { -// this.tokenType = tokenType; -// return this; -// } -// -// /** -// * Updates only a subset of user properties during update. -// * Does not update properties such as id and token data. -// * -// * @param sourceUserDto user details to update. -// */ -// public void safeUpdateProperties(RestSourceUserPropertiesDTO sourceUserDto) { -// this.projectId = sourceUserDto.getProjectId(); -// this.userId = sourceUserDto.getUserId(); -// this.sourceId = sourceUserDto.getSourceId(); -// this.startDate = sourceUserDto.getStartDate(); -// this.endDate = sourceUserDto.getEndDate(); -// } -// -// -// /** -// * Updates only the token related properties during update. -// * Does not update properties such as id and study information. -// * -// * @param restSourceAccessToken token details to update. -// */ -// public void safeUpdateTokenDetails(RestSourceAccessToken restSourceAccessToken) { -// this.accessToken = restSourceAccessToken.getAccessToken(); -// this.refreshToken = restSourceAccessToken.getRefreshToken(); -// this.expiresIn = restSourceAccessToken.getExpiresIn(); -// this.expiresAt = Instant.now() -// .plusSeconds(restSourceAccessToken.getExpiresIn()) -// .minus(EXPIRY_TIME_MARGIN); -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index d45da616..f41c16de 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -12,9 +12,6 @@ interface RestSourceUserRepository { fun read(id: Long): RestSourceUser? fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser fun query(page: Page, projectId: String? = null, sourceType: String? = null): Pair, Page> - - // fun findByExtenalId(sourceType: String, externalUserId: String) : RestSourceUser? -// fun findAllBySourceType(sourceType: String?): List fun delete(user: RestSourceUser) fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?): RestSourceUser } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index b630ea0b..94b24655 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -121,7 +121,6 @@ class RestSourceUserRepositoryImpl( }.also { merge(it) } } - companion object { private val expiryTimeMargin = Duration.ofMinutes(5) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt index d11e35f0..011ac740 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt @@ -96,7 +96,6 @@ class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnh .to(RestSourceAuthorizationService::class.java) .`in`(Singleton::class.java) - } companion object { diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java deleted file mode 100644 index 866080aa..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/CustomWebSecurityConfigurerAdapter.java +++ /dev/null @@ -1,43 +0,0 @@ -//package org.radarbase.authorizer.security; -// -//import org.radarbase.authorizer.config.RestSourceAuthorizerProperties; -//import org.radarcns.auth.authentication.TokenValidator; -//import org.radarcns.auth.config.TokenVerifierPublicKeyConfig; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.context.annotation.Bean; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.security.config.annotation.web.builders.HttpSecurity; -//import org.springframework.security.config.annotation.web.builders.WebSecurity; -//import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -//import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -// -//@Configuration -//public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { -// -// @Autowired -// private RestSourceAuthorizerProperties config; -// -// @Override -// public void configure(WebSecurity web) { -// // Overridden to exclude some url's -// web.ignoring().antMatchers("/health"); -// } -// -// @Override -// protected void configure(HttpSecurity http) throws Exception { -// -// http.csrf().disable() -// .addFilterAfter(new JwtAuthenticationFilter(getTokenValidator()), -// BasicAuthenticationFilter.class); -// } -// -// @Bean -// public TokenValidator getTokenValidator() { -// TokenVerifierPublicKeyConfig authVerifierConfig = new TokenVerifierPublicKeyConfig(); -// authVerifierConfig.setPublicKeys(config.getAuth().getPublicKeys()); -// authVerifierConfig.setPublicKeyEndpoints(config.getAuth().getPublicKeyEndpoints()); -// authVerifierConfig.setResourceName(config.getAuth().getResourceName()); -// return new TokenValidator(authVerifierConfig); -// } -// -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java deleted file mode 100644 index 2d1fab48..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/security/JwtAuthenticationFilter.java +++ /dev/null @@ -1,86 +0,0 @@ -//package org.radarbase.authorizer.security; -// -//import java.io.IOException; -//import javax.servlet.FilterChain; -//import javax.servlet.ServletException; -//import javax.servlet.ServletRequest; -//import javax.servlet.ServletResponse; -//import javax.servlet.http.HttpServletRequest; -//import javax.servlet.http.HttpServletResponse; -// -//import org.radarcns.auth.authentication.TokenValidator; -//import org.radarcns.auth.exception.TokenValidationException; -//import org.slf4j.Logger; -//import org.slf4j.LoggerFactory; -//import org.springframework.http.HttpHeaders; -//import org.springframework.web.cors.CorsUtils; -//import org.springframework.web.filter.GenericFilterBean; -// -///** -// * Created by dverbeec on 29/09/2017. -// */ -//public class JwtAuthenticationFilter extends GenericFilterBean { -// -// private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class); -// private final TokenValidator validator; -// public static final String TOKEN_ATTRIBUTE = "jwt"; -// public static String BEARER_TYPE = "Bearer"; -// -// public JwtAuthenticationFilter(TokenValidator validator) { -// this.validator = validator; -// } -// -// @Override -// public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws -// IOException, ServletException { -// if (CorsUtils.isPreFlightRequest((HttpServletRequest) request)) { -// log.debug("Skipping JWT check for preflight request"); -// chain.doFilter(request, response); -// return; -// } -// -// if (((HttpServletRequest) request).getRequestURI().contains("management/health")) { -// log.debug("Skipping JWT check for Health check request"); -// chain.doFilter(request, response); -// return; -// } -// -// try { -// request.setAttribute(TOKEN_ATTRIBUTE, -// validator.validateAccessToken(getToken(request, response))); -// log.debug("Request authenticated successfully"); -// chain.doFilter(request, response); -// } catch (TokenValidationException ex) { -// log.error(ex.getMessage()); -// HttpServletResponse res = (HttpServletResponse) response; -// res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); -// res.setHeader(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE); -// res.getOutputStream().println( -// "{\"error\": \"" + "Unauthorized" + ",\n" -// + "\"status\": \"" + HttpServletResponse.SC_UNAUTHORIZED + ",\n" -// + "\"message\": \"" + ex.getMessage() + ",\n" -// + "\"path\": \"" + ((HttpServletRequest) request).getRequestURI() + "\n" -// + "\"}"); -// } -// } -// -// private String getToken(ServletRequest request, ServletResponse response) { -// HttpServletRequest req = (HttpServletRequest) request; -// HttpServletResponse res = (HttpServletResponse) response; -// String authorizationHeader = req.getHeader(HttpHeaders.AUTHORIZATION); -// -// // Check if the HTTP Authorization header is present and formatted correctly -// if (authorizationHeader == null || !authorizationHeader -// .startsWith(BEARER_TYPE)) { -// log.error("No authorization header provided in the request"); -// res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); -// res.setHeader(HttpHeaders.WWW_AUTHENTICATE, BEARER_TYPE); -// throw new TokenValidationException("No " + BEARER_TYPE + " token " -// + "present in the request."); -// } -// -// // Extract the token from the HTTP Authorization header -// return authorizationHeader.substring( -// BEARER_TYPE.length()).trim(); -// } -//} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt index 82025140..c7eb8403 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt @@ -23,9 +23,9 @@ import org.radarbase.authorizer.Config import org.radarbase.authorizer.api.Project import org.radarbase.authorizer.api.User import org.radarbase.authorizer.service.RadarProjectService +import org.radarbase.authorizer.util.CachedSet import org.radarbase.jersey.auth.Auth import org.radarbase.jersey.exception.HttpNotFoundException -import org.radarbase.upload.util.CachedSet import org.radarcns.auth.authorization.Permission import java.time.Duration import java.util.concurrent.ConcurrentHashMap diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt index 682335ad..29435a2e 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt @@ -17,7 +17,7 @@ * */ -package org.radarbase.upload.util +package org.radarbase.authorizer.util import java.time.Duration import java.time.Instant From fb539202d400df473f5b9fb0a8e10c57a05ef6fe Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 31 Aug 2020 22:15:53 +0200 Subject: [PATCH 17/80] code reformat --- .../org/radarbase/authorizer/api/Project.kt | 2 +- .../authorizer/api/RestSourceClientMapper.kt | 2 +- .../authorizer/api/RestSourceUserMapper.kt | 30 +-- .../authorizer/doa/EntityManagerExtensions.kt | 57 +++--- .../doa/RestSourceUserRepositoryImpl.kt | 13 +- .../authorizer/doa/entity/RestSourceUser.kt | 2 +- .../inject/DoaEntityManagerFactory.kt | 26 +-- .../inject/DoaEntityManagerFactoryFactory.kt | 64 +++--- .../inject/ManagementPortalEnhancerFactory.kt | 46 ++--- .../inject/ProjectServiceWrapper.kt | 6 +- .../resources/SourceClientResource.kt | 14 +- .../authorizer/service/RadarProjectService.kt | 8 +- .../service/RestSourceAuthorizationService.kt | 1 - .../service/managementportal/MPClient.kt | 185 +++++++++--------- .../managementportal/MPProjectService.kt | 54 ++--- .../radarbase/authorizer/util/CachedSet.kt | 76 +++---- 16 files changed, 298 insertions(+), 288 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt index 47b50cf8..2c711556 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt @@ -21,7 +21,7 @@ package org.radarbase.authorizer.api data class ProjectList(val projects: List) -data class Project(val id: String, val name: String? = null, val location: String? = null, val organization: String? = null, val description: String? = null) +data class Project(val id: String, val name: String? = null, val location: String? = null, val organization: String? = null, val description: String? = null) data class UserList(val users: List) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt index d1294e08..49d955ea 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt @@ -3,7 +3,7 @@ package org.radarbase.authorizer.api import org.radarbase.authorizer.RestSourceClient class RestSourceClientMapper { - private fun fromSourceClientConfig(client: RestSourceClient)= ShareableClientDetail( + private fun fromSourceClientConfig(client: RestSourceClient) = ShareableClientDetail( clientId = client.clientId, sourceType = client.sourceType, scope = client.scope, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt index cc58ff5e..9a823f34 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt @@ -4,22 +4,22 @@ import org.radarbase.authorizer.doa.entity.RestSourceUser class RestSourceUserMapper { - fun fromEntity(user: RestSourceUser) = RestSourceUserDTO( - id = user.id.toString(), - projectId = user.projectId, - userId = user.userId, - sourceId = user.sourceId, - isAuthorized = user.authorized, - sourceType = user.sourceType, - endDate = user.endDate, - startDate = user.startDate, - externalUserId = user.externalUserId, - version = user.version, - timesReset = user.timesReset - ) + fun fromEntity(user: RestSourceUser) = RestSourceUserDTO( + id = user.id.toString(), + projectId = user.projectId, + userId = user.userId, + sourceId = user.sourceId, + isAuthorized = user.authorized, + sourceType = user.sourceType, + endDate = user.endDate, + startDate = user.startDate, + externalUserId = user.externalUserId, + version = user.version, + timesReset = user.timesReset + ) - fun fromRestSourceUsers(records: List, page: Page?) = RestSourceUsers( + fun fromRestSourceUsers(records: List, page: Page?) = RestSourceUsers( users = records.map(::fromEntity) - ) + ) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt index 047fe6da..81ab9cdb 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt @@ -10,45 +10,44 @@ import javax.persistence.EntityTransaction * Run a transaction and commit it. If an exception occurs, the transaction is rolled back. */ fun EntityManager.transact(transactionOperation: EntityManager.() -> T) = createTransaction { - it.use { transactionOperation() } + it.use { transactionOperation() } } /** * Start a transaction without committing it. If an exception occurs, the transaction is rolled back. */ private fun EntityManager.createTransaction(transactionOperation: EntityManager.(CloseableTransaction) -> T): T { - val currentTransaction = transaction - ?: throw HttpInternalServerException("transaction_not_found", "Cannot find a transaction from EntityManager") - - currentTransaction.begin() - try { - return transactionOperation(object : CloseableTransaction { - override val transaction: EntityTransaction = currentTransaction - - override fun close() { - try { - transaction.commit() - } catch (ex: Exception) { - logger.error("Rolling back operation", ex) - if (currentTransaction.isActive) { - currentTransaction.rollback() - } - throw ex - } - } - }) - } catch (ex: Exception) { - logger.error("Rolling back operation", ex) - if (currentTransaction.isActive) { + val currentTransaction = transaction + ?: throw HttpInternalServerException("transaction_not_found", "Cannot find a transaction from EntityManager") + + currentTransaction.begin() + try { + return transactionOperation(object : CloseableTransaction { + override val transaction: EntityTransaction = currentTransaction + + override fun close() { + try { + transaction.commit() + } catch (ex: Exception) { + logger.error("Rolling back operation", ex) + if (currentTransaction.isActive) { currentTransaction.rollback() + } + throw ex } - throw ex + } + }) + } catch (ex: Exception) { + logger.error("Rolling back operation", ex) + if (currentTransaction.isActive) { + currentTransaction.rollback() } + throw ex + } } - -interface CloseableTransaction: Closeable { - val transaction: EntityTransaction - override fun close() +interface CloseableTransaction : Closeable { + val transaction: EntityTransaction + override fun close() } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index 94b24655..b3864ce2 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -16,7 +16,8 @@ class RestSourceUserRepositoryImpl( ) : RestSourceUserRepository { override fun createOrUpdate(token: RestOauth2AccessToken, sourceType: String): RestSourceUser = em.get().transact { - val externalUserId = token.externalUserId ?: throw HttpBadGatewayException("Could not get externalId from token") + val externalUserId = token.externalUserId + ?: throw HttpBadGatewayException("Could not get externalId from token") val queryString = "SELECT u FROM RestSourceUser u where u.sourceType = :sourceType AND u.externalUserId = :externalUserId" val existingUser = createQuery(queryString, RestSourceUser::class.java) @@ -24,7 +25,7 @@ class RestSourceUserRepositoryImpl( .setParameter("externalUserId", externalUserId) .resultList.firstOrNull() - if(existingUser == null) { + if (existingUser == null) { RestSourceUser().apply { this.authorized = true this.externalUserId = externalUserId @@ -50,10 +51,10 @@ class RestSourceUserRepositoryImpl( override fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser = em.get().transact { existingUser.apply { this.projectId = user.projectId - this.userId = user.userId - this.sourceId = user.sourceId - this.startDate = user.startDate - this.endDate = user.endDate + this.userId = user.userId + this.sourceId = user.sourceId + this.startDate = user.startDate + this.endDate = user.endDate }.also { merge(it) } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt index 025cbec2..f7072051 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt @@ -1,6 +1,5 @@ package org.radarbase.authorizer.doa.entity -import java.time.Duration import java.time.Instant import java.util.* import javax.persistence.* @@ -13,6 +12,7 @@ class RestSourceUser { @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") @SequenceGenerator(name = "sequenceGenerator", sequenceName = "rest_source_user_id_seq", initialValue = 1, allocationSize = 1) var id: Long? = null + // Project ID to be used in org.radarcns.kafka.ObservationKey record keys @Column(name = "project_id") var projectId: String? = null diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt index 99525564..b6bf668b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt @@ -26,22 +26,22 @@ import javax.persistence.EntityManagerFactory import javax.ws.rs.core.Context class DoaEntityManagerFactory( - @Context private val emf: EntityManagerFactory + @Context private val emf: EntityManagerFactory ) : DisposableSupplier { - override fun get(): EntityManager { - logger.debug("Creating EntityManager...") - return emf.createEntityManager() - } + override fun get(): EntityManager { + logger.debug("Creating EntityManager...") + return emf.createEntityManager() + } - override fun dispose(instance: EntityManager?) { - instance?.let { - logger.debug("Disposing EntityManager") - it.close() - } + override fun dispose(instance: EntityManager?) { + instance?.let { + logger.debug("Disposing EntityManager") + it.close() } + } - companion object { - private val logger = LoggerFactory.getLogger(DoaEntityManagerFactory::class.java) - } + companion object { + private val logger = LoggerFactory.getLogger(DoaEntityManagerFactory::class.java) + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt index 20d09cdb..caea0d9c 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt @@ -33,41 +33,41 @@ import javax.persistence.Persistence import javax.ws.rs.core.Context class DoaEntityManagerFactoryFactory(@Context config: DatabaseConfig) : DisposableSupplier { - @Suppress("UNCHECKED_CAST") - private val configMap = ( - mapOf( - "javax.persistence.jdbc.driver" to config.jdbcDriver, - "javax.persistence.jdbc.url" to config.jdbcUrl, - "javax.persistence.jdbc.user" to config.jdbcUser, - "javax.persistence.jdbc.password" to config.jdbcPassword, - "hibernate.dialect" to config.hibernateDialect) - + (config.additionalPersistenceConfig ?: emptyMap())) - .filterValues { it != null } as Map + @Suppress("UNCHECKED_CAST") + private val configMap = ( + mapOf( + "javax.persistence.jdbc.driver" to config.jdbcDriver, + "javax.persistence.jdbc.url" to config.jdbcUrl, + "javax.persistence.jdbc.user" to config.jdbcUser, + "javax.persistence.jdbc.password" to config.jdbcPassword, + "hibernate.dialect" to config.hibernateDialect) + + (config.additionalPersistenceConfig ?: emptyMap())) + .filterValues { it != null } as Map - override fun get(): EntityManagerFactory { - logger.info("Initializing EntityManagerFactory with config: $configMap") - return Persistence.createEntityManagerFactory("org.radarbase.authorizer.doa", configMap) - .also { initializeDatabase(it) } - } + override fun get(): EntityManagerFactory { + logger.info("Initializing EntityManagerFactory with config: $configMap") + return Persistence.createEntityManagerFactory("org.radarbase.authorizer.doa", configMap) + .also { initializeDatabase(it) } + } - private fun initializeDatabase(emf: EntityManagerFactory) { - logger.info("Initializing Liquibase") - val connection = emf.createEntityManager().unwrap(SessionImpl::class.java).connection() - try { - val database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(JdbcConnection(connection)) - val liquibase = Liquibase("db/changelog/changes/db.changelog-master.xml", ClassLoaderResourceAccessor(), database) - liquibase.update("test") - } catch (e: LiquibaseException) { - logger.error("Failed to initialize database", e) - } + private fun initializeDatabase(emf: EntityManagerFactory) { + logger.info("Initializing Liquibase") + val connection = emf.createEntityManager().unwrap(SessionImpl::class.java).connection() + try { + val database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(JdbcConnection(connection)) + val liquibase = Liquibase("db/changelog/changes/db.changelog-master.xml", ClassLoaderResourceAccessor(), database) + liquibase.update("test") + } catch (e: LiquibaseException) { + logger.error("Failed to initialize database", e) } + } - override fun dispose(instance: EntityManagerFactory?) { - logger.info("Disposing EntityManagerFactory") - instance?.close() - } + override fun dispose(instance: EntityManagerFactory?) { + logger.info("Disposing EntityManagerFactory") + instance?.close() + } - companion object { - private val logger = LoggerFactory.getLogger(DoaEntityManagerFactoryFactory::class.java) - } + companion object { + private val logger = LoggerFactory.getLogger(DoaEntityManagerFactoryFactory::class.java) + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt index 334287fe..95854ae1 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt @@ -33,31 +33,31 @@ import javax.inject.Singleton /** This binder needs to register all non-Jersey classes, otherwise initialization fails. */ class ManagementPortalEnhancerFactory(private val config: Config) : EnhancerFactory { - override fun createEnhancers(): List = listOf( - AuthorizerResourceEnhancer(config), - MPClientResourceEnhancer(), - ConfigLoader.Enhancers.radar(AuthConfig( - managementPortalUrl = config.auth.managementPortalUrl, - jwtResourceName = config.auth.jwtResourceName)), - ConfigLoader.Enhancers.managementPortal, - ConfigLoader.Enhancers.generalException, - ConfigLoader.Enhancers.httpException) + override fun createEnhancers(): List = listOf( + AuthorizerResourceEnhancer(config), + MPClientResourceEnhancer(), + ConfigLoader.Enhancers.radar(AuthConfig( + managementPortalUrl = config.auth.managementPortalUrl, + jwtResourceName = config.auth.jwtResourceName)), + ConfigLoader.Enhancers.managementPortal, + ConfigLoader.Enhancers.generalException, + ConfigLoader.Enhancers.httpException) - class MPClientResourceEnhancer: JerseyResourceEnhancer { - override fun enhanceBinder(binder: AbstractBinder) { - binder.apply { - bind(MPClient::class.java) - .to(MPClient::class.java) - .`in`(Singleton::class.java) + class MPClientResourceEnhancer : JerseyResourceEnhancer { + override fun enhanceBinder(binder: AbstractBinder) { + binder.apply { + bind(MPClient::class.java) + .to(MPClient::class.java) + .`in`(Singleton::class.java) - bind(ProjectServiceWrapper::class.java) - .to(ProjectService::class.java) - .`in`(Singleton::class.java) + bind(ProjectServiceWrapper::class.java) + .to(ProjectService::class.java) + .`in`(Singleton::class.java) - bind(MPProjectService::class.java) - .to(RadarProjectService::class.java) - .`in`(Singleton::class.java) - } - } + bind(MPProjectService::class.java) + .to(RadarProjectService::class.java) + .`in`(Singleton::class.java) + } } + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt index 5d9c01f8..c19bbfa1 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt @@ -6,7 +6,7 @@ import javax.inject.Provider import javax.ws.rs.core.Context class ProjectServiceWrapper( - @Context private val mpProjectService: Provider -): ProjectService { - override fun ensureProject(projectId: String) = mpProjectService.get().ensureProject(projectId) + @Context private val mpProjectService: Provider +) : ProjectService { + override fun ensureProject(projectId: String) = mpProjectService.get().ensureProject(projectId) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt index 886ae782..fd51f9a1 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt @@ -1,13 +1,18 @@ package org.radarbase.authorizer.resources import org.radarbase.authorizer.RestSourceClients -import org.radarbase.authorizer.api.* +import org.radarbase.authorizer.api.RestSourceClientMapper +import org.radarbase.authorizer.api.ShareableClientDetail +import org.radarbase.authorizer.api.ShareableClientDetails import org.radarbase.jersey.auth.Auth import org.radarbase.jersey.auth.Authenticated import org.radarbase.jersey.exception.HttpNotFoundException import javax.annotation.Resource import javax.inject.Singleton -import javax.ws.rs.* +import javax.ws.rs.GET +import javax.ws.rs.Path +import javax.ws.rs.PathParam +import javax.ws.rs.Produces import javax.ws.rs.core.Context import javax.ws.rs.core.MediaType @@ -18,7 +23,7 @@ import javax.ws.rs.core.MediaType @Singleton class SourceClientResource( @Context private val restSourceClients: RestSourceClients, - @Context private val clientMapper : RestSourceClientMapper, + @Context private val clientMapper: RestSourceClientMapper, @Context private val auth: Auth ) { @@ -36,6 +41,7 @@ class SourceClientResource( @GET @Path("{type}") fun client(@PathParam("type") type: String): ShareableClientDetail { - return sharableClientDetails.sourceClients.find { it.sourceType == type } ?: throw HttpNotFoundException("source-type-not-found", "Client with source-type $type is not configured") + return sharableClientDetails.sourceClients.find { it.sourceType == type } + ?: throw HttpNotFoundException("source-type-not-found", "Client with source-type $type is not configured") } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt index 35393231..df05685c 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt @@ -7,8 +7,8 @@ import org.radarbase.jersey.auth.ProjectService interface RadarProjectService : ProjectService { - fun project(projectId: String): Project - fun userProjects(auth: Auth): List - fun projectUsers(projectId: String): List - fun userByExternalId(projectId: String, externalUserId: String): User? + fun project(projectId: String): Project + fun userProjects(auth: Auth): List + fun projectUsers(projectId: String): List + fun userByExternalId(projectId: String, externalUserId: String): User? } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt index 1526ee1e..071780d2 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt @@ -7,7 +7,6 @@ import okhttp3.OkHttpClient import okhttp3.Request import org.radarbase.authorizer.RestSourceClients import org.radarbase.authorizer.api.RestOauth2AccessToken -import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.jersey.exception.HttpBadGatewayException import org.radarbase.jersey.exception.HttpBadRequestException import org.slf4j.Logger diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt index 2d114753..b523715c 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt @@ -37,102 +37,107 @@ import java.time.Instant import javax.ws.rs.core.Context class MPClient(@Context config: Config, @Context private val auth: Auth) { - private val clientId: String = config.auth.clientId - private val clientSecret: String = config.auth.clientSecret ?: throw IllegalArgumentException("Cannot configure managementportal client without client secret") - private val httpClient = OkHttpClient() - private val baseUrl: HttpUrl = config.auth.managementPortalUrl.toHttpUrlOrNull() - ?: throw MalformedURLException("Cannot parse base URL ${config.auth.managementPortalUrl} as an URL") - private val mapper = jacksonObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - private val projectListReader = mapper.readerFor(object : TypeReference>(){}) - private val userListReader = mapper.readerFor(object : TypeReference>(){}) - - private var token: String? = null - private var expiration: Instant? = null - - private val validToken: String? - get() { - val localToken = token ?: return null - expiration?.takeIf { it > Instant.now() } ?: return null - return localToken - } - - private fun ensureToken(): String { - var localToken = validToken - - return if (localToken != null) { - localToken - } else { - val request = Request.Builder().apply { - url(baseUrl.resolve("oauth/token")!!) - post(FormBody.Builder().apply { - add("grant_type", "client_credentials") - add("client_id", clientId) - add("client_secret", clientSecret) - }.build()) - header("Authorization", Credentials.basic(clientId, clientSecret)) - }.build() - - val result = mapper.readTree(execute(request)) - localToken = result["access_token"].asText() - ?: throw HttpBadGatewayException("ManagementPortal did not provide an access token") - expiration = Instant.now() + Duration.ofSeconds(result["expires_in"].asLong()) - Duration.ofMinutes(5) - token = localToken - localToken - } + private val clientId: String = config.auth.clientId + private val clientSecret: String = config.auth.clientSecret + ?: throw IllegalArgumentException("Cannot configure managementportal client without client secret") + private val httpClient = OkHttpClient() + private val baseUrl: HttpUrl = config.auth.managementPortalUrl.toHttpUrlOrNull() + ?: throw MalformedURLException("Cannot parse base URL ${config.auth.managementPortalUrl} as an URL") + private val mapper = jacksonObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + private val projectListReader = mapper.readerFor(object : TypeReference>() {}) + private val userListReader = mapper.readerFor(object : TypeReference>() {}) + + private var token: String? = null + private var expiration: Instant? = null + + private val validToken: String? + get() { + val localToken = token ?: return null + expiration?.takeIf { it > Instant.now() } ?: return null + return localToken } - fun readProjects(): List { - logger.debug("Requesting for projects") - val request = Request.Builder().apply { - url(baseUrl.resolve("api/projects")!!) - header("Authorization", "Bearer ${ensureToken()}") - }.build() - - return projectListReader.readValue>(execute(request)) - .map { Project( - id = it.id, - name = it.name, - location = it.location, - organization = it.organization, - description = it.description) } + private fun ensureToken(): String { + var localToken = validToken + + return if (localToken != null) { + localToken + } else { + val request = Request.Builder().apply { + url(baseUrl.resolve("oauth/token")!!) + post(FormBody.Builder().apply { + add("grant_type", "client_credentials") + add("client_id", clientId) + add("client_secret", clientSecret) + }.build()) + header("Authorization", Credentials.basic(clientId, clientSecret)) + }.build() + + val result = mapper.readTree(execute(request)) + localToken = result["access_token"].asText() + ?: throw HttpBadGatewayException("ManagementPortal did not provide an access token") + expiration = Instant.now() + Duration.ofSeconds(result["expires_in"].asLong()) - Duration.ofMinutes(5) + token = localToken + localToken } - - private fun execute(request: Request): String { - return httpClient.newCall(request).execute().use { response -> - if (response.isSuccessful) { - response.body?.string() - ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") - } else { - logger.error("Cannot connect to managementportal ", response.code) - throw HttpBadGatewayException("Cannot connect to managementportal : Response-code ${response.code}") - } + } + + fun readProjects(): List { + logger.debug("Requesting for projects") + val request = Request.Builder().apply { + url(baseUrl.resolve("api/projects")!!) + header("Authorization", "Bearer ${ensureToken()}") + }.build() + + return projectListReader.readValue>(execute(request)) + .map { + Project( + id = it.id, + name = it.name, + location = it.location, + organization = it.organization, + description = it.description) } + } + + private fun execute(request: Request): String { + return httpClient.newCall(request).execute().use { response -> + if (response.isSuccessful) { + response.body?.string() + ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") + } else { + logger.error("Cannot connect to managementportal ", response.code) + throw HttpBadGatewayException("Cannot connect to managementportal : Response-code ${response.code}") + } } + } + + fun readParticipants(projectId: String): List { + val request = Request.Builder().apply { + url(baseUrl.newBuilder() + .addPathSegments("api/projects/$projectId/subjects") + .addQueryParameter("page", "0") + .addQueryParameter("size", Int.MAX_VALUE.toString()) + .build()) + header("Authorization", "Bearer ${ensureToken()}") + }.build() + + return userListReader.readValue>(execute(request)) + .map { + User( + id = it.login, + projectId = projectId, + externalId = it.externalId, + status = it.status) + } + } - fun readParticipants(projectId: String): List { - val request = Request.Builder().apply { - url(baseUrl.newBuilder() - .addPathSegments("api/projects/$projectId/subjects") - .addQueryParameter("page", "0") - .addQueryParameter("size", Int.MAX_VALUE.toString()) - .build()) - header("Authorization", "Bearer ${ensureToken()}") - }.build() - - return userListReader.readValue>(execute(request)) - .map { User( - id = it.login, - projectId = projectId, - externalId = it.externalId, - status = it.status) } - } - - data class SubjectDto(val login: String, val externalId: String? = null, val status: String = "DEACTIVATED", val attributes: Map = mapOf()) + data class SubjectDto(val login: String, val externalId: String? = null, val status: String = "DEACTIVATED", val attributes: Map = mapOf()) - data class ProjectDto(@JsonProperty("projectName") val id: String, @JsonProperty("humanReadableProjectName") val name: String? = null, val location: String? = null, val organization: String? = null, val description: String? = null) + data class ProjectDto(@JsonProperty("projectName") val id: String, @JsonProperty("humanReadableProjectName") val name: String? = null, val location: String? = null, val organization: String? = null, val description: String? = null) - companion object { - private val logger = LoggerFactory.getLogger(MPClient::class.java) - } + companion object { + private val logger = LoggerFactory.getLogger(MPClient::class.java) + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt index c7eb8403..a661b40c 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt @@ -32,40 +32,40 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap import javax.ws.rs.core.Context -class MPProjectService(@Context private val config: Config, @Context private val mpClient: MPClient): RadarProjectService { - private val projects = CachedSet( - Duration.ofMinutes(config.service.syncProjectsIntervalMin), - Duration.ofMinutes(1)) { - mpClient.readProjects() - } - - private val participants: ConcurrentMap> = ConcurrentHashMap() +class MPProjectService(@Context private val config: Config, @Context private val mpClient: MPClient) : RadarProjectService { + private val projects = CachedSet( + Duration.ofMinutes(config.service.syncProjectsIntervalMin), + Duration.ofMinutes(1)) { + mpClient.readProjects() + } - override fun ensureProject(projectId: String) { - if (projects.find { it.id == projectId } == null) { - throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") - } - } + private val participants: ConcurrentMap> = ConcurrentHashMap() - override fun userProjects(auth: Auth): List { - return projects.get() - .filter { auth.token.hasPermissionOnProject(Permission.PROJECT_READ, it.id) } + override fun ensureProject(projectId: String) { + if (projects.find { it.id == projectId } == null) { + throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") } + } - override fun project(projectId: String) : Project = projects.find { it.id == projectId } ?: - throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") + override fun userProjects(auth: Auth): List { + return projects.get() + .filter { auth.token.hasPermissionOnProject(Permission.PROJECT_READ, it.id) } + } - override fun projectUsers(projectId: String): List { - val projectParticipants = participants.computeIfAbsent(projectId) { - CachedSet(Duration.ofMinutes(config.service.syncParticipantsIntervalMin), Duration.ofMinutes(1)) { - mpClient.readParticipants(projectId) - } - } + override fun project(projectId: String): Project = projects.find { it.id == projectId } + ?: throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") - return projectParticipants.get().toList() + override fun projectUsers(projectId: String): List { + val projectParticipants = participants.computeIfAbsent(projectId) { + CachedSet(Duration.ofMinutes(config.service.syncParticipantsIntervalMin), Duration.ofMinutes(1)) { + mpClient.readParticipants(projectId) + } } - override fun userByExternalId(projectId: String, externalUserId: String): User? = - projectUsers(projectId).find { it.externalId == externalUserId } + return projectParticipants.get().toList() + } + + override fun userByExternalId(projectId: String, externalUserId: String): User? = + projectUsers(projectId).find { it.externalId == externalUserId } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt index 29435a2e..25479b72 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt @@ -23,49 +23,49 @@ import java.time.Duration import java.time.Instant class CachedSet( - private val refreshDuration: Duration, - private val retryDuration: Duration, - private val supplier: () -> Iterable) { - @set:Synchronized - private var cached: Set = emptySet() - set(value) { - val now = Instant.now() - field = value - nextRefresh = now.plus(refreshDuration) - nextRetry = now.plus(retryDuration) - } + private val refreshDuration: Duration, + private val retryDuration: Duration, + private val supplier: () -> Iterable) { + @set:Synchronized + private var cached: Set = emptySet() + set(value) { + val now = Instant.now() + field = value + nextRefresh = now.plus(refreshDuration) + nextRetry = now.plus(retryDuration) + } - private var nextRefresh: Instant = Instant.MIN - private var nextRetry: Instant = Instant.MIN + private var nextRefresh: Instant = Instant.MIN + private var nextRetry: Instant = Instant.MIN - @get:Synchronized - private val state: State - get () { - val now = Instant.now() - return State(cached, - now.isAfter(nextRefresh), - now.isAfter(nextRetry)) - } + @get:Synchronized + private val state: State + get() { + val now = Instant.now() + return State(cached, + now.isAfter(nextRefresh), + now.isAfter(nextRetry)) + } - private fun refresh() = supplier.invoke().toSet() - .also { cached = it } + private fun refresh() = supplier.invoke().toSet() + .also { cached = it } - fun contains(value: T) = state.query({ it.contains(value) }, { it }) - fun find(predicate: (T) -> Boolean): T? = state.query({ it.find(predicate) }, { it != null }) - fun get(): Set = state.query({ it }, { it.isNotEmpty() }) + fun contains(value: T) = state.query({ it.contains(value) }, { it }) + fun find(predicate: (T) -> Boolean): T? = state.query({ it.find(predicate) }, { it != null }) + fun get(): Set = state.query({ it }, { it.isNotEmpty() }) - private inner class State(val cache: Set, val mustRefresh: Boolean, val mayRetry: Boolean) { - fun query(method: (Set) -> S, validityPredicate: (S) -> Boolean): S { - var result: S - if (mustRefresh) { - result = method(refresh()) - } else { - result = method(cache) - if (!validityPredicate(result) && mayRetry) { - result = method(refresh()) - } - } - return result + private inner class State(val cache: Set, val mustRefresh: Boolean, val mayRetry: Boolean) { + fun query(method: (Set) -> S, validityPredicate: (S) -> Boolean): S { + var result: S + if (mustRefresh) { + result = method(refresh()) + } else { + result = method(cache) + if (!validityPredicate(result) && mayRetry) { + result = method(refresh()) } + } + return result } + } } From 533721bece9804037319b9254597b446548226ef Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 1 Sep 2020 09:25:22 +0200 Subject: [PATCH 18/80] correct code format --- .../java/org/radarbase/authorizer/Main.kt | 8 +- .../authorizer/api/ApiDeclarations.kt | 36 +-- .../authorizer/api/RestSourceClientMapper.kt | 22 +- .../authorizer/api/RestSourceUserMapper.kt | 32 +-- .../authorizer/doa/EntityManagerExtensions.kt | 50 ++-- .../doa/RestSourceUserRepository.kt | 12 +- .../doa/RestSourceUserRepositoryImpl.kt | 202 ++++++++-------- .../authorizer/doa/entity/RestSourceUser.kt | 102 ++++---- .../inject/AuthorizerResourceEnhancer.kt | 130 +++++----- .../inject/DoaEntityManagerFactory.kt | 24 +- .../inject/DoaEntityManagerFactoryFactory.kt | 64 ++--- .../inject/ManagementPortalEnhancerFactory.kt | 46 ++-- .../inject/ProjectServiceWrapper.kt | 2 +- .../resources/HeathCheckResource.kt | 10 +- .../authorizer/resources/ProjectResource.kt | 30 +-- .../resources/RestSourceUserResource.kt | 226 +++++++++--------- .../resources/SourceClientResource.kt | 26 +- .../authorizer/service/RadarProjectService.kt | 8 +- .../service/RestSourceAuthorizationService.kt | 102 ++++---- .../service/managementportal/MPClient.kt | 190 +++++++-------- .../managementportal/MPProjectService.kt | 52 ++-- .../radarbase/authorizer/util/CachedSet.kt | 70 +++--- 22 files changed, 722 insertions(+), 722 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt index 1a0aea13..cad03ed8 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt @@ -27,9 +27,9 @@ import org.slf4j.LoggerFactory val logger: Logger = LoggerFactory.getLogger("org.radarbase.authorizer.Main") fun main(args: Array) { - val config: Config = ConfigLoader.loadConfig("authorizer.yml", args) - val resources = ConfigLoader.loadResources(config.service.resourceConfig, config) + val config: Config = ConfigLoader.loadConfig("authorizer.yml", args) + val resources = ConfigLoader.loadResources(config.service.resourceConfig, config) - val server = GrizzlyServer(config.service.baseUri, resources) - server.listen() + val server = GrizzlyServer(config.service.baseUri, resources) + server.listen() } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt index 81b24e76..5fb6c52e 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt @@ -40,9 +40,9 @@ class RestSourceUserDTO( val isAuthorized: Boolean = false, val version: String? = null, val timesReset: Long = 0) : Serializable { - companion object { - private const val serialVersionUID = 1L - } + companion object { + private const val serialVersionUID = 1L + } } data class RestSourceUsers( @@ -58,22 +58,22 @@ data class Page( val pageNumber: Int = 1, val pageSize: Int? = null, val totalElements: Long? = null) { - val offset: Int - get() = (this.pageNumber - 1) * this.pageSize!! + val offset: Int + get() = (this.pageNumber - 1) * this.pageSize!! - fun createValid(maximum: Int? = null): Page { - val imposedNumber = pageNumber.coerceAtLeast(1) + fun createValid(maximum: Int? = null): Page { + val imposedNumber = pageNumber.coerceAtLeast(1) - val imposedSize = if (maximum != null) { - require(maximum >= 1) { "Maximum page size should be at least 1" } - pageSize?.coerceAtLeast(1)?.coerceAtMost(maximum) ?: maximum - } else { - pageSize?.coerceAtLeast(1) - } - return if (imposedNumber == pageNumber && imposedSize == pageSize) { - this - } else { - copy(pageNumber = imposedNumber, pageSize = imposedSize) + val imposedSize = if (maximum != null) { + require(maximum >= 1) { "Maximum page size should be at least 1" } + pageSize?.coerceAtLeast(1)?.coerceAtMost(maximum) ?: maximum + } else { + pageSize?.coerceAtLeast(1) + } + return if (imposedNumber == pageNumber && imposedSize == pageSize) { + this + } else { + copy(pageNumber = imposedNumber, pageSize = imposedSize) + } } - } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt index 49d955ea..d822e291 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt @@ -3,16 +3,16 @@ package org.radarbase.authorizer.api import org.radarbase.authorizer.RestSourceClient class RestSourceClientMapper { - private fun fromSourceClientConfig(client: RestSourceClient) = ShareableClientDetail( - clientId = client.clientId, - sourceType = client.sourceType, - scope = client.scope, - authorizationEndpoint = client.authorizationEndpoint, - tokenEndpoint = client.tokenEndpoint, - grantType = client.grantType, - ) + private fun fromSourceClientConfig(client: RestSourceClient) = ShareableClientDetail( + clientId = client.clientId, + sourceType = client.sourceType, + scope = client.scope, + authorizationEndpoint = client.authorizationEndpoint, + tokenEndpoint = client.tokenEndpoint, + grantType = client.grantType, + ) - fun fromSourceClientConfigs(clientConfigs: List) = ShareableClientDetails( - sourceClients = clientConfigs.map(::fromSourceClientConfig) - ) + fun fromSourceClientConfigs(clientConfigs: List) = ShareableClientDetails( + sourceClients = clientConfigs.map(::fromSourceClientConfig) + ) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt index 9a823f34..d6abd7a9 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt @@ -4,22 +4,22 @@ import org.radarbase.authorizer.doa.entity.RestSourceUser class RestSourceUserMapper { - fun fromEntity(user: RestSourceUser) = RestSourceUserDTO( - id = user.id.toString(), - projectId = user.projectId, - userId = user.userId, - sourceId = user.sourceId, - isAuthorized = user.authorized, - sourceType = user.sourceType, - endDate = user.endDate, - startDate = user.startDate, - externalUserId = user.externalUserId, - version = user.version, - timesReset = user.timesReset - ) + fun fromEntity(user: RestSourceUser) = RestSourceUserDTO( + id = user.id.toString(), + projectId = user.projectId, + userId = user.userId, + sourceId = user.sourceId, + isAuthorized = user.authorized, + sourceType = user.sourceType, + endDate = user.endDate, + startDate = user.startDate, + externalUserId = user.externalUserId, + version = user.version, + timesReset = user.timesReset + ) - fun fromRestSourceUsers(records: List, page: Page?) = RestSourceUsers( - users = records.map(::fromEntity) - ) + fun fromRestSourceUsers(records: List, page: Page?) = RestSourceUsers( + users = records.map(::fromEntity) + ) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt index 81ab9cdb..0d74f3a7 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt @@ -10,44 +10,44 @@ import javax.persistence.EntityTransaction * Run a transaction and commit it. If an exception occurs, the transaction is rolled back. */ fun EntityManager.transact(transactionOperation: EntityManager.() -> T) = createTransaction { - it.use { transactionOperation() } + it.use { transactionOperation() } } /** * Start a transaction without committing it. If an exception occurs, the transaction is rolled back. */ private fun EntityManager.createTransaction(transactionOperation: EntityManager.(CloseableTransaction) -> T): T { - val currentTransaction = transaction - ?: throw HttpInternalServerException("transaction_not_found", "Cannot find a transaction from EntityManager") + val currentTransaction = transaction + ?: throw HttpInternalServerException("transaction_not_found", "Cannot find a transaction from EntityManager") - currentTransaction.begin() - try { - return transactionOperation(object : CloseableTransaction { - override val transaction: EntityTransaction = currentTransaction + currentTransaction.begin() + try { + return transactionOperation(object : CloseableTransaction { + override val transaction: EntityTransaction = currentTransaction - override fun close() { - try { - transaction.commit() - } catch (ex: Exception) { - logger.error("Rolling back operation", ex) - if (currentTransaction.isActive) { + override fun close() { + try { + transaction.commit() + } catch (ex: Exception) { + logger.error("Rolling back operation", ex) + if (currentTransaction.isActive) { + currentTransaction.rollback() + } + throw ex + } + } + }) + } catch (ex: Exception) { + logger.error("Rolling back operation", ex) + if (currentTransaction.isActive) { currentTransaction.rollback() - } - throw ex } - } - }) - } catch (ex: Exception) { - logger.error("Rolling back operation", ex) - if (currentTransaction.isActive) { - currentTransaction.rollback() + throw ex } - throw ex - } } interface CloseableTransaction : Closeable { - val transaction: EntityTransaction - override fun close() + val transaction: EntityTransaction + override fun close() } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index f41c16de..2b1002db 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -8,10 +8,10 @@ import java.time.Instant interface RestSourceUserRepository { - fun createOrUpdate(user: RestOauth2AccessToken, sourceType: String): RestSourceUser - fun read(id: Long): RestSourceUser? - fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser - fun query(page: Page, projectId: String? = null, sourceType: String? = null): Pair, Page> - fun delete(user: RestSourceUser) - fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?): RestSourceUser + fun createOrUpdate(user: RestOauth2AccessToken, sourceType: String): RestSourceUser + fun read(id: Long): RestSourceUser? + fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser + fun query(page: Page, projectId: String? = null, sourceType: String? = null): Pair, Page> + fun delete(user: RestSourceUser) + fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?): RestSourceUser } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index b3864ce2..dd87f75c 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -15,114 +15,114 @@ class RestSourceUserRepositoryImpl( @Context private var em: Provider ) : RestSourceUserRepository { - override fun createOrUpdate(token: RestOauth2AccessToken, sourceType: String): RestSourceUser = em.get().transact { - val externalUserId = token.externalUserId - ?: throw HttpBadGatewayException("Could not get externalId from token") - - val queryString = "SELECT u FROM RestSourceUser u where u.sourceType = :sourceType AND u.externalUserId = :externalUserId" - val existingUser = createQuery(queryString, RestSourceUser::class.java) - .setParameter("sourceType", sourceType) - .setParameter("externalUserId", externalUserId) - .resultList.firstOrNull() - - if (existingUser == null) { - RestSourceUser().apply { - this.authorized = true - this.externalUserId = externalUserId - this.sourceType = sourceType - this.startDate = Instant.now() - this.accessToken = token.accessToken - this.refreshToken = token.refreshToken - this.expiresIn = token.expiresIn - this.expiresAt = Instant.now().plusSeconds(token.expiresIn.toLong()).minus(expiryTimeMargin) - }.also { persist(it) } - } else { - existingUser.apply { - this.accessToken = token.accessToken - this.refreshToken = token.refreshToken - this.expiresIn = token.expiresIn - this.expiresAt = Instant.now().plusSeconds(token.expiresIn.toLong()).minus(expiryTimeMargin) - }.also { merge(it) } - } - } - - override fun read(id: Long): RestSourceUser? = em.get().transact { find(RestSourceUser::class.java, id) } - - override fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser = em.get().transact { - existingUser.apply { - this.projectId = user.projectId - this.userId = user.userId - this.sourceId = user.sourceId - this.startDate = user.startDate - this.endDate = user.endDate - }.also { merge(it) } - } - - override fun query(page: Page, projectId: String?, sourceType: String?): Pair, Page> { - var queryString = "SELECT u FROM RestSourceUser u" - var countQueryString = "SELECT count(u) FROM RestSourceUser u" - - when { - projectId != null && sourceType != null -> { - queryString += " WHERE u.projectId = :projectId AND u.sourceType = :sourceType" - countQueryString += " WHERE u.projectId = :projectId AND u.sourceType = :sourceType" - } - projectId != null && sourceType == null -> { - queryString += " WHERE u.projectId = :projectId" - countQueryString += " WHERE u.projectId = :projectId" - } - projectId == null && sourceType != null -> { - queryString += " WHERE u.sourceType = :sourceType" - countQueryString += " WHERE u.sourceType = :sourceType" - } + override fun createOrUpdate(token: RestOauth2AccessToken, sourceType: String): RestSourceUser = em.get().transact { + val externalUserId = token.externalUserId + ?: throw HttpBadGatewayException("Could not get externalId from token") + + val queryString = "SELECT u FROM RestSourceUser u where u.sourceType = :sourceType AND u.externalUserId = :externalUserId" + val existingUser = createQuery(queryString, RestSourceUser::class.java) + .setParameter("sourceType", sourceType) + .setParameter("externalUserId", externalUserId) + .resultList.firstOrNull() + + if (existingUser == null) { + RestSourceUser().apply { + this.authorized = true + this.externalUserId = externalUserId + this.sourceType = sourceType + this.startDate = Instant.now() + this.accessToken = token.accessToken + this.refreshToken = token.refreshToken + this.expiresIn = token.expiresIn + this.expiresAt = Instant.now().plusSeconds(token.expiresIn.toLong()).minus(expiryTimeMargin) + }.also { persist(it) } + } else { + existingUser.apply { + this.accessToken = token.accessToken + this.refreshToken = token.refreshToken + this.expiresIn = token.expiresIn + this.expiresAt = Instant.now().plusSeconds(token.expiresIn.toLong()).minus(expiryTimeMargin) + }.also { merge(it) } + } } - val actualPage = page.createValid(maximum = 100) - return em.get().transact { - val query = createQuery(queryString, RestSourceUser::class.java) - .setFirstResult(actualPage.offset) - .setMaxResults(actualPage.pageSize!!) + override fun read(id: Long): RestSourceUser? = em.get().transact { find(RestSourceUser::class.java, id) } - val countQuery = createQuery(countQueryString) + override fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser = em.get().transact { + existingUser.apply { + this.projectId = user.projectId + this.userId = user.userId + this.sourceId = user.sourceId + this.startDate = user.startDate + this.endDate = user.endDate + }.also { merge(it) } + } - when { - projectId != null && sourceType != null -> { - query.setParameter("projectId", projectId) - countQuery.setParameter("projectId", projectId) - query.setParameter("sourceType", sourceType) - countQuery.setParameter("sourceType", sourceType) + override fun query(page: Page, projectId: String?, sourceType: String?): Pair, Page> { + var queryString = "SELECT u FROM RestSourceUser u" + var countQueryString = "SELECT count(u) FROM RestSourceUser u" + + when { + projectId != null && sourceType != null -> { + queryString += " WHERE u.projectId = :projectId AND u.sourceType = :sourceType" + countQueryString += " WHERE u.projectId = :projectId AND u.sourceType = :sourceType" + } + projectId != null && sourceType == null -> { + queryString += " WHERE u.projectId = :projectId" + countQueryString += " WHERE u.projectId = :projectId" + } + projectId == null && sourceType != null -> { + queryString += " WHERE u.sourceType = :sourceType" + countQueryString += " WHERE u.sourceType = :sourceType" + } } - projectId != null && sourceType == null -> { - query.setParameter("projectId", projectId) - countQuery.setParameter("projectId", projectId) - } - projectId == null && sourceType != null -> { - query.setParameter("sourceType", sourceType) - countQuery.setParameter("sourceType", sourceType) + + val actualPage = page.createValid(maximum = 100) + return em.get().transact { + val query = createQuery(queryString, RestSourceUser::class.java) + .setFirstResult(actualPage.offset) + .setMaxResults(actualPage.pageSize!!) + + val countQuery = createQuery(countQueryString) + + when { + projectId != null && sourceType != null -> { + query.setParameter("projectId", projectId) + countQuery.setParameter("projectId", projectId) + query.setParameter("sourceType", sourceType) + countQuery.setParameter("sourceType", sourceType) + } + projectId != null && sourceType == null -> { + query.setParameter("projectId", projectId) + countQuery.setParameter("projectId", projectId) + } + projectId == null && sourceType != null -> { + query.setParameter("sourceType", sourceType) + countQuery.setParameter("sourceType", sourceType) + } + } + + val users = query.resultList + val count = countQuery.singleResult as Long + + Pair(users, actualPage.copy(totalElements = count)) } - } + } + + override fun delete(user: RestSourceUser) = em.get().transact { + remove(merge(user)) + } - val users = query.resultList - val count = countQuery.singleResult as Long + override fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?) = em.get().transact { + user.apply { + this.version = Instant.now().toString() + this.timesReset += 1 + this.startDate = startDate + this.endDate = endDate + }.also { merge(it) } + } - Pair(users, actualPage.copy(totalElements = count)) + companion object { + private val expiryTimeMargin = Duration.ofMinutes(5) } - } - - override fun delete(user: RestSourceUser) = em.get().transact { - remove(merge(user)) - } - - override fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?) = em.get().transact { - user.apply { - this.version = Instant.now().toString() - this.timesReset += 1 - this.startDate = startDate - this.endDate = endDate - }.also { merge(it) } - } - - companion object { - private val expiryTimeMargin = Duration.ofMinutes(5) - } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt index f7072051..8d4b6848 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt @@ -8,77 +8,77 @@ import javax.persistence.* @Table(name = "rest_source_user") class RestSourceUser { - @Id - @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") - @SequenceGenerator(name = "sequenceGenerator", sequenceName = "rest_source_user_id_seq", initialValue = 1, allocationSize = 1) - var id: Long? = null + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") + @SequenceGenerator(name = "sequenceGenerator", sequenceName = "rest_source_user_id_seq", initialValue = 1, allocationSize = 1) + var id: Long? = null - // Project ID to be used in org.radarcns.kafka.ObservationKey record keys - @Column(name = "project_id") - var projectId: String? = null + // Project ID to be used in org.radarcns.kafka.ObservationKey record keys + @Column(name = "project_id") + var projectId: String? = null - // User ID to be used in org.radarcns.kafka.ObservationKey record keys - @Column(name = "user_id") - var userId: String? = null + // User ID to be used in org.radarcns.kafka.ObservationKey record keys + @Column(name = "user_id") + var userId: String? = null - // Source ID to be used in org.radarcns.kafka.ObservationKey record keys - @Column(name = "source_id") - var sourceId: String = UUID.randomUUID().toString() + // Source ID to be used in org.radarcns.kafka.ObservationKey record keys + @Column(name = "source_id") + var sourceId: String = UUID.randomUUID().toString() - @Column(name = "source_type") - lateinit var sourceType: String + @Column(name = "source_type") + lateinit var sourceType: String - // Date from when to collect data. - @Column(name = "start_date") - lateinit var startDate: Instant + // Date from when to collect data. + @Column(name = "start_date") + lateinit var startDate: Instant - @Column(name = "end_date") - var endDate: Instant? = null + @Column(name = "end_date") + var endDate: Instant? = null - @Column(name = "external_user_id") - lateinit var externalUserId: String + @Column(name = "external_user_id") + lateinit var externalUserId: String - // is authorized by user - @Column(name = "authorized") - var authorized = false + // is authorized by user + @Column(name = "authorized") + var authorized = false - @Column(name = "access_token") - var accessToken: String? = null + @Column(name = "access_token") + var accessToken: String? = null - @Column(name = "refresh_token") - var refreshToken: String? = null + @Column(name = "refresh_token") + var refreshToken: String? = null - @Column(name = "expires_in") - var expiresIn: Int? = null + @Column(name = "expires_in") + var expiresIn: Int? = null - @Column(name = "expires_at") - var expiresAt: Instant? = null + @Column(name = "expires_at") + var expiresAt: Instant? = null - @Column(name = "token_type") - var tokenType: String? = null + @Column(name = "token_type") + var tokenType: String? = null - // The version to be appended to ID for RESET of a user - // This should be updated whenever the user is RESET. - // By default this is null for backwards compatibility - @Column(name = "version") - var version: String? = null + // The version to be appended to ID for RESET of a user + // This should be updated whenever the user is RESET. + // By default this is null for backwards compatibility + @Column(name = "version") + var version: String? = null - // The number of times a user has been reset - @Column(name = "times_reset") - var timesReset: Long = 0 + // The number of times a user has been reset + @Column(name = "times_reset") + var timesReset: Long = 0 - override fun equals(other: Any?): Boolean { - if (this === other) return true + override fun equals(other: Any?): Boolean { + if (this === other) return true - if (other == null || javaClass != other.javaClass) return false + if (other == null || javaClass != other.javaClass) return false - other as RestSourceUser + other as RestSourceUser - return id != null && id == other.id - } + return id != null && id == other.id + } - override fun hashCode(): Int = id.hashCode() + override fun hashCode(): Int = id.hashCode() - override fun toString() = "Entity of type ${this.javaClass.name} with id: $id" + override fun toString() = "Entity of type ${this.javaClass.name} with id: $id" } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt index 011ac740..db383e3f 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt @@ -26,83 +26,83 @@ import javax.persistence.EntityManagerFactory import javax.ws.rs.ext.ContextResolver class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnhancer { - private val client = OkHttpClient().newBuilder() - .connectTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .build() - - private val restSourceClients = RestSourceClients(config.restSourceClients) - - override val classes: Array> - get() { - return if (config.service.enableCors == true) { - arrayOf( - ConfigLoader.Filters.logResponse, - ConfigLoader.Filters.cors) - } else { - arrayOf( - ConfigLoader.Filters.logResponse) - } + private val client = OkHttpClient().newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + private val restSourceClients = RestSourceClients(config.restSourceClients) + + override val classes: Array> + get() { + return if (config.service.enableCors == true) { + arrayOf( + ConfigLoader.Filters.logResponse, + ConfigLoader.Filters.cors) + } else { + arrayOf( + ConfigLoader.Filters.logResponse) + } + } + + override val packages: Array = arrayOf( + "org.radarbase.authorizer.exception", + "org.radarbase.authorizer.resources") + + override fun ResourceConfig.enhance() { + register(ContextResolver { OBJECT_MAPPER }) } - override val packages: Array = arrayOf( - "org.radarbase.authorizer.exception", - "org.radarbase.authorizer.resources") + override fun AbstractBinder.enhance() { + // Bind instances. These cannot use any injects themselves + bind(config) + .to(Config::class.java) - override fun ResourceConfig.enhance() { - register(ContextResolver { OBJECT_MAPPER }) - } + bind(config.database) + .to(DatabaseConfig::class.java) - override fun AbstractBinder.enhance() { - // Bind instances. These cannot use any injects themselves - bind(config) - .to(Config::class.java) + bind(restSourceClients) + .to(RestSourceClients::class.java) - bind(config.database) - .to(DatabaseConfig::class.java) + bind(client) + .to(OkHttpClient::class.java) - bind(restSourceClients) - .to(RestSourceClients::class.java) + bind(OBJECT_MAPPER) + .to(ObjectMapper::class.java) - bind(client) - .to(OkHttpClient::class.java) + // Bind factories. + bindFactory(DoaEntityManagerFactoryFactory::class.java) + .to(EntityManagerFactory::class.java) + .`in`(Singleton::class.java) - bind(OBJECT_MAPPER) - .to(ObjectMapper::class.java) + bindFactory(DoaEntityManagerFactory::class.java) + .to(EntityManager::class.java) + .`in`(RequestScoped::class.java) - // Bind factories. - bindFactory(DoaEntityManagerFactoryFactory::class.java) - .to(EntityManagerFactory::class.java) - .`in`(Singleton::class.java) + bind(RestSourceUserMapper::class.java) + .to(RestSourceUserMapper::class.java) + .`in`(Singleton::class.java) - bindFactory(DoaEntityManagerFactory::class.java) - .to(EntityManager::class.java) - .`in`(RequestScoped::class.java) + bind(RestSourceClientMapper::class.java) + .to(RestSourceClientMapper::class.java) + .`in`(Singleton::class.java) - bind(RestSourceUserMapper::class.java) - .to(RestSourceUserMapper::class.java) - .`in`(Singleton::class.java) + bind(RestSourceUserRepositoryImpl::class.java) + .to(RestSourceUserRepository::class.java) + .`in`(Singleton::class.java) - bind(RestSourceClientMapper::class.java) - .to(RestSourceClientMapper::class.java) - .`in`(Singleton::class.java) + bind(RestSourceAuthorizationService::class.java) + .to(RestSourceAuthorizationService::class.java) + .`in`(Singleton::class.java) - bind(RestSourceUserRepositoryImpl::class.java) - .to(RestSourceUserRepository::class.java) - .`in`(Singleton::class.java) - - bind(RestSourceAuthorizationService::class.java) - .to(RestSourceAuthorizationService::class.java) - .`in`(Singleton::class.java) - - } + } - companion object { - private val OBJECT_MAPPER: ObjectMapper = ObjectMapper() - .setSerializationInclusion(JsonInclude.Include.NON_NULL) - .registerModule(JavaTimeModule()) - .registerModule(KotlinModule()) - .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) - } + companion object { + private val OBJECT_MAPPER: ObjectMapper = ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(JavaTimeModule()) + .registerModule(KotlinModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt index b6bf668b..d2188851 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt @@ -29,19 +29,19 @@ class DoaEntityManagerFactory( @Context private val emf: EntityManagerFactory ) : DisposableSupplier { - override fun get(): EntityManager { - logger.debug("Creating EntityManager...") - return emf.createEntityManager() - } + override fun get(): EntityManager { + logger.debug("Creating EntityManager...") + return emf.createEntityManager() + } - override fun dispose(instance: EntityManager?) { - instance?.let { - logger.debug("Disposing EntityManager") - it.close() + override fun dispose(instance: EntityManager?) { + instance?.let { + logger.debug("Disposing EntityManager") + it.close() + } } - } - companion object { - private val logger = LoggerFactory.getLogger(DoaEntityManagerFactory::class.java) - } + companion object { + private val logger = LoggerFactory.getLogger(DoaEntityManagerFactory::class.java) + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt index caea0d9c..512efe2e 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt @@ -33,41 +33,41 @@ import javax.persistence.Persistence import javax.ws.rs.core.Context class DoaEntityManagerFactoryFactory(@Context config: DatabaseConfig) : DisposableSupplier { - @Suppress("UNCHECKED_CAST") - private val configMap = ( - mapOf( - "javax.persistence.jdbc.driver" to config.jdbcDriver, - "javax.persistence.jdbc.url" to config.jdbcUrl, - "javax.persistence.jdbc.user" to config.jdbcUser, - "javax.persistence.jdbc.password" to config.jdbcPassword, - "hibernate.dialect" to config.hibernateDialect) - + (config.additionalPersistenceConfig ?: emptyMap())) - .filterValues { it != null } as Map + @Suppress("UNCHECKED_CAST") + private val configMap = ( + mapOf( + "javax.persistence.jdbc.driver" to config.jdbcDriver, + "javax.persistence.jdbc.url" to config.jdbcUrl, + "javax.persistence.jdbc.user" to config.jdbcUser, + "javax.persistence.jdbc.password" to config.jdbcPassword, + "hibernate.dialect" to config.hibernateDialect) + + (config.additionalPersistenceConfig ?: emptyMap())) + .filterValues { it != null } as Map - override fun get(): EntityManagerFactory { - logger.info("Initializing EntityManagerFactory with config: $configMap") - return Persistence.createEntityManagerFactory("org.radarbase.authorizer.doa", configMap) - .also { initializeDatabase(it) } - } + override fun get(): EntityManagerFactory { + logger.info("Initializing EntityManagerFactory with config: $configMap") + return Persistence.createEntityManagerFactory("org.radarbase.authorizer.doa", configMap) + .also { initializeDatabase(it) } + } - private fun initializeDatabase(emf: EntityManagerFactory) { - logger.info("Initializing Liquibase") - val connection = emf.createEntityManager().unwrap(SessionImpl::class.java).connection() - try { - val database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(JdbcConnection(connection)) - val liquibase = Liquibase("db/changelog/changes/db.changelog-master.xml", ClassLoaderResourceAccessor(), database) - liquibase.update("test") - } catch (e: LiquibaseException) { - logger.error("Failed to initialize database", e) + private fun initializeDatabase(emf: EntityManagerFactory) { + logger.info("Initializing Liquibase") + val connection = emf.createEntityManager().unwrap(SessionImpl::class.java).connection() + try { + val database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(JdbcConnection(connection)) + val liquibase = Liquibase("db/changelog/changes/db.changelog-master.xml", ClassLoaderResourceAccessor(), database) + liquibase.update("test") + } catch (e: LiquibaseException) { + logger.error("Failed to initialize database", e) + } } - } - override fun dispose(instance: EntityManagerFactory?) { - logger.info("Disposing EntityManagerFactory") - instance?.close() - } + override fun dispose(instance: EntityManagerFactory?) { + logger.info("Disposing EntityManagerFactory") + instance?.close() + } - companion object { - private val logger = LoggerFactory.getLogger(DoaEntityManagerFactoryFactory::class.java) - } + companion object { + private val logger = LoggerFactory.getLogger(DoaEntityManagerFactoryFactory::class.java) + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt index 95854ae1..c6266bb8 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt @@ -33,31 +33,31 @@ import javax.inject.Singleton /** This binder needs to register all non-Jersey classes, otherwise initialization fails. */ class ManagementPortalEnhancerFactory(private val config: Config) : EnhancerFactory { - override fun createEnhancers(): List = listOf( - AuthorizerResourceEnhancer(config), - MPClientResourceEnhancer(), - ConfigLoader.Enhancers.radar(AuthConfig( - managementPortalUrl = config.auth.managementPortalUrl, - jwtResourceName = config.auth.jwtResourceName)), - ConfigLoader.Enhancers.managementPortal, - ConfigLoader.Enhancers.generalException, - ConfigLoader.Enhancers.httpException) + override fun createEnhancers(): List = listOf( + AuthorizerResourceEnhancer(config), + MPClientResourceEnhancer(), + ConfigLoader.Enhancers.radar(AuthConfig( + managementPortalUrl = config.auth.managementPortalUrl, + jwtResourceName = config.auth.jwtResourceName)), + ConfigLoader.Enhancers.managementPortal, + ConfigLoader.Enhancers.generalException, + ConfigLoader.Enhancers.httpException) - class MPClientResourceEnhancer : JerseyResourceEnhancer { - override fun enhanceBinder(binder: AbstractBinder) { - binder.apply { - bind(MPClient::class.java) - .to(MPClient::class.java) - .`in`(Singleton::class.java) + class MPClientResourceEnhancer : JerseyResourceEnhancer { + override fun enhanceBinder(binder: AbstractBinder) { + binder.apply { + bind(MPClient::class.java) + .to(MPClient::class.java) + .`in`(Singleton::class.java) - bind(ProjectServiceWrapper::class.java) - .to(ProjectService::class.java) - .`in`(Singleton::class.java) + bind(ProjectServiceWrapper::class.java) + .to(ProjectService::class.java) + .`in`(Singleton::class.java) - bind(MPProjectService::class.java) - .to(RadarProjectService::class.java) - .`in`(Singleton::class.java) - } + bind(MPProjectService::class.java) + .to(RadarProjectService::class.java) + .`in`(Singleton::class.java) + } + } } - } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt index c19bbfa1..d319e7ce 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt @@ -8,5 +8,5 @@ import javax.ws.rs.core.Context class ProjectServiceWrapper( @Context private val mpProjectService: Provider ) : ProjectService { - override fun ensureProject(projectId: String) = mpProjectService.get().ensureProject(projectId) + override fun ensureProject(projectId: String) = mpProjectService.get().ensureProject(projectId) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt index 38f0b126..7e9667d7 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt @@ -15,9 +15,9 @@ import javax.ws.rs.core.Response class HealthCheckResource( @Context private var userRepository: RestSourceUserRepository ) { - @GET - fun check(): Response { - userRepository.query(Page(0, 1)) - return Response.ok("Initialized dao and made a sample query...").build() - } + @GET + fun check(): Response { + userRepository.query(Page(0, 1)) + return Response.ok("Initialized dao and made a sample query...").build() + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt index 8c923ed5..8f024c81 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt @@ -25,21 +25,21 @@ class ProjectResource( @Context private val auth: Auth ) { - @GET - @NeedsPermission(Permission.Entity.PROJECT, Permission.Operation.READ) - fun projects() = ProjectList(projectService.userProjects(auth)) + @GET + @NeedsPermission(Permission.Entity.PROJECT, Permission.Operation.READ) + fun projects() = ProjectList(projectService.userProjects(auth)) - @GET - @Path("{projectId}/users") - @NeedsPermission(Permission.Entity.PROJECT, Permission.Operation.READ, "projectId") - fun users(@PathParam("projectId") projectId: String): UserList { - return UserList(projectService.projectUsers(projectId)) - } + @GET + @Path("{projectId}/users") + @NeedsPermission(Permission.Entity.PROJECT, Permission.Operation.READ, "projectId") + fun users(@PathParam("projectId") projectId: String): UserList { + return UserList(projectService.projectUsers(projectId)) + } - @GET - @Path("{projectId}") - @NeedsPermission(Permission.Entity.PROJECT, Permission.Operation.READ, "projectId") - fun project(@PathParam("projectId") projectId: String): Project { - return projectService.project(projectId) - } + @GET + @Path("{projectId}") + @NeedsPermission(Permission.Entity.PROJECT, Permission.Operation.READ, "projectId") + fun project(@PathParam("projectId") projectId: String): Project { + return projectService.project(projectId) + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt index 77a949e1..8d37d025 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt @@ -35,120 +35,120 @@ class RestSourceUserResource( @Context private val auth: Auth ) { - @GET - fun query( - @QueryParam("projectId") projectId: String?, - @QueryParam("sourceType") sourceType: String?, - @QueryParam("size") pageSize: Int?, - @DefaultValue("1") @QueryParam("page") pageNumber: Int): RestSourceUsers { - - if (projectId != null) { - auth.checkPermissionOnProject(Permission.PROJECT_READ, projectId) + @GET + fun query( + @QueryParam("projectId") projectId: String?, + @QueryParam("sourceType") sourceType: String?, + @QueryParam("size") pageSize: Int?, + @DefaultValue("1") @QueryParam("page") pageNumber: Int): RestSourceUsers { + + if (projectId != null) { + auth.checkPermissionOnProject(Permission.PROJECT_READ, projectId) + } + + val queryPage = Page(pageNumber = pageNumber, pageSize = pageSize) + val (records, page) = userRepository.query(queryPage, projectId, sourceType) + + return userMapper.fromRestSourceUsers(records, page) + } + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + fun create( + @FormParam("code") code: String, + @FormParam("state") state: String): Response { + logger.info("code $code state $state") + val accessToken = authorizationService.requestAccessToken(code, sourceType = state) + val user = userRepository.createOrUpdate(accessToken, state) + + return Response.created(URI("users/${user.id}")) + .entity(userMapper.fromEntity(user)) + .build() + } + + @POST + @Path("{id}") + fun update( + @PathParam("id") userId: Long, + user: RestSourceUserDTO): RestSourceUserDTO { + val existingUser = validate(userId, user, Permission.SUBJECT_UPDATE) + + val updatedUser = userRepository.update(existingUser, user) + return userMapper.fromEntity(updatedUser) + } + + @GET + @Path("{id}") + fun readUser(@PathParam("id") userId: Long): RestSourceUserDTO { + val user = ensureUser(userId) + auth.checkPermissionOnSubject(Permission.SUBJECT_READ, user.projectId, user.userId) + return userMapper.fromEntity(user) + } + + @DELETE + @Path("{id}") + fun deleteUser(@PathParam("id") userId: Long): Response { + val user = ensureUser(userId) + auth.checkPermissionOnSubject(Permission.SUBJECT_UPDATE, user.projectId, user.userId) + if (user.accessToken != null) { + authorizationService.revokeToken(user.accessToken!!, user.sourceType) + } + userRepository.delete(user) + return Response.noContent().header("user-removed", userId).build() + } + + @POST + @Path("{id}/reset") + fun reset( + @PathParam("id") userId: Long, + user: RestSourceUserDTO): RestSourceUserDTO { + val existingUser = validate(userId, user, Permission.SUBJECT_UPDATE) + + val updatedUser = userRepository.reset(existingUser, user.startDate, user.endDate + ?: existingUser.endDate) + return userMapper.fromEntity(updatedUser) + } + + @GET + @Path("{id}/token") + fun requestToken(@PathParam("id") userId: Long): TokenDTO { + val user = ensureUser(userId) + auth.checkPermissionOnSubject(Permission.MEASUREMENT_CREATE, user.projectId, user.userId) + return TokenDTO(user.accessToken, user.expiresAt) + } + + @POST + @Path("{id}/token") + fun refreshToken(@PathParam("id") userId: Long): TokenDTO { + val user = ensureUser(userId) + auth.checkPermissionOnSubject(Permission.MEASUREMENT_CREATE, user.projectId, user.userId) + val rft = user.refreshToken + ?: throw HttpConflictException("refresh_token_not_found", "No refresh-token found for user ${user.externalUserId} with source-type ${user.sourceType}") + + val updatedUser = userRepository.createOrUpdate(authorizationService.refreshToken(rft, user.sourceType), user.sourceType) + + return TokenDTO(updatedUser.accessToken, updatedUser.expiresAt) + } + + private fun validate(id: Long, user: RestSourceUserDTO, permission: Permission): RestSourceUser { + val existingUser = ensureUser(id) + val projectId = user.projectId + ?: throw HttpBadRequestException("missing_project_id", "project cannot be empty") + val userId = user.userId + ?: throw HttpBadRequestException("missing_user_id", "subject-id/user-id cannot be empty") + auth.checkPermissionOnSubject(permission, projectId, userId) + + projectService.projectUsers(projectId).find { it.id == userId } + ?: throw HttpBadRequestException("user_not_found", "user $userId not found in project $projectId") + return existingUser + } + + fun ensureUser(userId: Long): RestSourceUser { + return userRepository.read(userId) + ?: throw HttpNotFoundException("user_not_found", "Rest-Source-User with ID $userId does not exist") } - val queryPage = Page(pageNumber = pageNumber, pageSize = pageSize) - val (records, page) = userRepository.query(queryPage, projectId, sourceType) - - return userMapper.fromRestSourceUsers(records, page) - } - - @POST - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - fun create( - @FormParam("code") code: String, - @FormParam("state") state: String): Response { - logger.info("code $code state $state") - val accessToken = authorizationService.requestAccessToken(code, sourceType = state) - val user = userRepository.createOrUpdate(accessToken, state) - - return Response.created(URI("users/${user.id}")) - .entity(userMapper.fromEntity(user)) - .build() - } - - @POST - @Path("{id}") - fun update( - @PathParam("id") userId: Long, - user: RestSourceUserDTO): RestSourceUserDTO { - val existingUser = validate(userId, user, Permission.SUBJECT_UPDATE) - - val updatedUser = userRepository.update(existingUser, user) - return userMapper.fromEntity(updatedUser) - } - - @GET - @Path("{id}") - fun readUser(@PathParam("id") userId: Long): RestSourceUserDTO { - val user = ensureUser(userId) - auth.checkPermissionOnSubject(Permission.SUBJECT_READ, user.projectId, user.userId) - return userMapper.fromEntity(user) - } - - @DELETE - @Path("{id}") - fun deleteUser(@PathParam("id") userId: Long): Response { - val user = ensureUser(userId) - auth.checkPermissionOnSubject(Permission.SUBJECT_UPDATE, user.projectId, user.userId) - if (user.accessToken != null) { - authorizationService.revokeToken(user.accessToken!!, user.sourceType) + companion object { + val logger: Logger = LoggerFactory.getLogger(RestSourceUserResource::class.java) } - userRepository.delete(user) - return Response.noContent().header("user-removed", userId).build() - } - - @POST - @Path("{id}/reset") - fun reset( - @PathParam("id") userId: Long, - user: RestSourceUserDTO): RestSourceUserDTO { - val existingUser = validate(userId, user, Permission.SUBJECT_UPDATE) - - val updatedUser = userRepository.reset(existingUser, user.startDate, user.endDate - ?: existingUser.endDate) - return userMapper.fromEntity(updatedUser) - } - - @GET - @Path("{id}/token") - fun requestToken(@PathParam("id") userId: Long): TokenDTO { - val user = ensureUser(userId) - auth.checkPermissionOnSubject(Permission.MEASUREMENT_CREATE, user.projectId, user.userId) - return TokenDTO(user.accessToken, user.expiresAt) - } - - @POST - @Path("{id}/token") - fun refreshToken(@PathParam("id") userId: Long): TokenDTO { - val user = ensureUser(userId) - auth.checkPermissionOnSubject(Permission.MEASUREMENT_CREATE, user.projectId, user.userId) - val rft = user.refreshToken - ?: throw HttpConflictException("refresh_token_not_found", "No refresh-token found for user ${user.externalUserId} with source-type ${user.sourceType}") - - val updatedUser = userRepository.createOrUpdate(authorizationService.refreshToken(rft, user.sourceType), user.sourceType) - - return TokenDTO(updatedUser.accessToken, updatedUser.expiresAt) - } - - private fun validate(id: Long, user: RestSourceUserDTO, permission: Permission): RestSourceUser { - val existingUser = ensureUser(id) - val projectId = user.projectId - ?: throw HttpBadRequestException("missing_project_id", "project cannot be empty") - val userId = user.userId - ?: throw HttpBadRequestException("missing_user_id", "subject-id/user-id cannot be empty") - auth.checkPermissionOnSubject(permission, projectId, userId) - - projectService.projectUsers(projectId).find { it.id == userId } - ?: throw HttpBadRequestException("user_not_found", "user $userId not found in project $projectId") - return existingUser - } - - fun ensureUser(userId: Long): RestSourceUser { - return userRepository.read(userId) - ?: throw HttpNotFoundException("user_not_found", "Rest-Source-User with ID $userId does not exist") - } - - companion object { - val logger: Logger = LoggerFactory.getLogger(RestSourceUserResource::class.java) - } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt index fd51f9a1..2f7d2dc0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt @@ -27,21 +27,21 @@ class SourceClientResource( @Context private val auth: Auth ) { - private val sourceTypes = restSourceClients.clients.map { it.sourceType } + private val sourceTypes = restSourceClients.clients.map { it.sourceType } - private val sharableClientDetails = clientMapper.fromSourceClientConfigs(restSourceClients.clients) + private val sharableClientDetails = clientMapper.fromSourceClientConfigs(restSourceClients.clients) - @GET - fun clients(): ShareableClientDetails = sharableClientDetails + @GET + fun clients(): ShareableClientDetails = sharableClientDetails - @GET - @Path("type") - fun types(): List = sourceTypes + @GET + @Path("type") + fun types(): List = sourceTypes - @GET - @Path("{type}") - fun client(@PathParam("type") type: String): ShareableClientDetail { - return sharableClientDetails.sourceClients.find { it.sourceType == type } - ?: throw HttpNotFoundException("source-type-not-found", "Client with source-type $type is not configured") - } + @GET + @Path("{type}") + fun client(@PathParam("type") type: String): ShareableClientDetail { + return sharableClientDetails.sourceClients.find { it.sourceType == type } + ?: throw HttpNotFoundException("source-type-not-found", "Client with source-type $type is not configured") + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt index df05685c..35393231 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt @@ -7,8 +7,8 @@ import org.radarbase.jersey.auth.ProjectService interface RadarProjectService : ProjectService { - fun project(projectId: String): Project - fun userProjects(auth: Auth): List - fun projectUsers(projectId: String): List - fun userByExternalId(projectId: String, externalUserId: String): User? + fun project(projectId: String): Project + fun userProjects(auth: Auth): List + fun projectUsers(projectId: String): List + fun userByExternalId(projectId: String, externalUserId: String): User? } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt index 071780d2..a90a600b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt @@ -19,68 +19,68 @@ class RestSourceAuthorizationService( @Context private val objectMapper: ObjectMapper ) { - private val configMap = restSourceClients.clients.map { it.sourceType to it }.toMap() + private val configMap = restSourceClients.clients.map { it.sourceType to it }.toMap() - fun requestAccessToken(code: String, sourceType: String): RestOauth2AccessToken { - val authorizationConfig = configMap[sourceType] - ?: throw HttpBadRequestException("client-config-not-found", "Cannot find client configurations for source-type $sourceType") + fun requestAccessToken(code: String, sourceType: String): RestOauth2AccessToken { + val authorizationConfig = configMap[sourceType] + ?: throw HttpBadRequestException("client-config-not-found", "Cannot find client configurations for source-type $sourceType") - val form = FormBody.Builder() - .add("code", code) - .add("grant_type", "authorization_code") - .add("client_id", authorizationConfig.clientId) - .build(); - logger.info("Requesting access token with authorization code") - return objectMapper.readValue(execute(post(form, sourceType)), RestOauth2AccessToken::class.java) - } + val form = FormBody.Builder() + .add("code", code) + .add("grant_type", "authorization_code") + .add("client_id", authorizationConfig.clientId) + .build(); + logger.info("Requesting access token with authorization code") + return objectMapper.readValue(execute(post(form, sourceType)), RestOauth2AccessToken::class.java) + } - fun refreshToken(refreshToken: String, sourceType: String): RestOauth2AccessToken { - val form = FormBody.Builder() - .add("grant_type", "refresh_token") - .add("refresh_token", refreshToken) - .build(); - logger.info("Requesting to refreshToken") - return objectMapper.readValue(execute(post(form, sourceType)), RestOauth2AccessToken::class.java) - } + fun refreshToken(refreshToken: String, sourceType: String): RestOauth2AccessToken { + val form = FormBody.Builder() + .add("grant_type", "refresh_token") + .add("refresh_token", refreshToken) + .build(); + logger.info("Requesting to refreshToken") + return objectMapper.readValue(execute(post(form, sourceType)), RestOauth2AccessToken::class.java) + } - fun revokeToken(accessToken: String, sourceType: String): Boolean { - val form = FormBody.Builder().add("token", accessToken).build(); - logger.info("Requesting to revoke access token"); + fun revokeToken(accessToken: String, sourceType: String): Boolean { + val form = FormBody.Builder().add("token", accessToken).build(); + logger.info("Requesting to revoke access token"); - httpClient.newCall(post(form, sourceType)).execute().use { response -> - return response.isSuccessful - } + httpClient.newCall(post(form, sourceType)).execute().use { response -> + return response.isSuccessful + } - } + } - private fun post(form: FormBody, sourceType: String): Request { - val authorizationConfig = configMap[sourceType] - ?: throw HttpBadRequestException("client-config-not-found", "Cannot find client configurations for source-type $sourceType") + private fun post(form: FormBody, sourceType: String): Request { + val authorizationConfig = configMap[sourceType] + ?: throw HttpBadRequestException("client-config-not-found", "Cannot find client configurations for source-type $sourceType") - return Request.Builder().apply { - url(authorizationConfig.tokenEndpoint) - post(form) - header("Authorization", Credentials.basic(authorizationConfig.clientId, authorizationConfig.clientSecret)) - header("Content-Type", "application/x-www-form-urlencoded") - header("Accept", "application/json") - }.build() - } + return Request.Builder().apply { + url(authorizationConfig.tokenEndpoint) + post(form) + header("Authorization", Credentials.basic(authorizationConfig.clientId, authorizationConfig.clientSecret)) + header("Content-Type", "application/x-www-form-urlencoded") + header("Accept", "application/json") + }.build() + } - private fun execute(request: Request): String { - return httpClient.newCall(request).execute().use { response -> - if (response.isSuccessful) { - response.body?.string() - ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") - } else { - logger.error("Cannot connect to managementportal ", response.code) - throw HttpBadGatewayException("Cannot connect to managementportal : Response-code ${response.code}") - } + private fun execute(request: Request): String { + return httpClient.newCall(request).execute().use { response -> + if (response.isSuccessful) { + response.body?.string() + ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") + } else { + logger.error("Cannot connect to managementportal ", response.code) + throw HttpBadGatewayException("Cannot connect to managementportal : Response-code ${response.code}") + } + } } - } - companion object { - val logger: Logger = LoggerFactory.getLogger(RestSourceAuthorizationService::class.java) - } + companion object { + val logger: Logger = LoggerFactory.getLogger(RestSourceAuthorizationService::class.java) + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt index b523715c..dd5c7ed8 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt @@ -37,107 +37,107 @@ import java.time.Instant import javax.ws.rs.core.Context class MPClient(@Context config: Config, @Context private val auth: Auth) { - private val clientId: String = config.auth.clientId - private val clientSecret: String = config.auth.clientSecret - ?: throw IllegalArgumentException("Cannot configure managementportal client without client secret") - private val httpClient = OkHttpClient() - private val baseUrl: HttpUrl = config.auth.managementPortalUrl.toHttpUrlOrNull() - ?: throw MalformedURLException("Cannot parse base URL ${config.auth.managementPortalUrl} as an URL") - private val mapper = jacksonObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - private val projectListReader = mapper.readerFor(object : TypeReference>() {}) - private val userListReader = mapper.readerFor(object : TypeReference>() {}) - - private var token: String? = null - private var expiration: Instant? = null - - private val validToken: String? - get() { - val localToken = token ?: return null - expiration?.takeIf { it > Instant.now() } ?: return null - return localToken + private val clientId: String = config.auth.clientId + private val clientSecret: String = config.auth.clientSecret + ?: throw IllegalArgumentException("Cannot configure managementportal client without client secret") + private val httpClient = OkHttpClient() + private val baseUrl: HttpUrl = config.auth.managementPortalUrl.toHttpUrlOrNull() + ?: throw MalformedURLException("Cannot parse base URL ${config.auth.managementPortalUrl} as an URL") + private val mapper = jacksonObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + private val projectListReader = mapper.readerFor(object : TypeReference>() {}) + private val userListReader = mapper.readerFor(object : TypeReference>() {}) + + private var token: String? = null + private var expiration: Instant? = null + + private val validToken: String? + get() { + val localToken = token ?: return null + expiration?.takeIf { it > Instant.now() } ?: return null + return localToken + } + + private fun ensureToken(): String { + var localToken = validToken + + return if (localToken != null) { + localToken + } else { + val request = Request.Builder().apply { + url(baseUrl.resolve("oauth/token")!!) + post(FormBody.Builder().apply { + add("grant_type", "client_credentials") + add("client_id", clientId) + add("client_secret", clientSecret) + }.build()) + header("Authorization", Credentials.basic(clientId, clientSecret)) + }.build() + + val result = mapper.readTree(execute(request)) + localToken = result["access_token"].asText() + ?: throw HttpBadGatewayException("ManagementPortal did not provide an access token") + expiration = Instant.now() + Duration.ofSeconds(result["expires_in"].asLong()) - Duration.ofMinutes(5) + token = localToken + localToken + } } - private fun ensureToken(): String { - var localToken = validToken - - return if (localToken != null) { - localToken - } else { - val request = Request.Builder().apply { - url(baseUrl.resolve("oauth/token")!!) - post(FormBody.Builder().apply { - add("grant_type", "client_credentials") - add("client_id", clientId) - add("client_secret", clientSecret) - }.build()) - header("Authorization", Credentials.basic(clientId, clientSecret)) - }.build() - - val result = mapper.readTree(execute(request)) - localToken = result["access_token"].asText() - ?: throw HttpBadGatewayException("ManagementPortal did not provide an access token") - expiration = Instant.now() + Duration.ofSeconds(result["expires_in"].asLong()) - Duration.ofMinutes(5) - token = localToken - localToken + fun readProjects(): List { + logger.debug("Requesting for projects") + val request = Request.Builder().apply { + url(baseUrl.resolve("api/projects")!!) + header("Authorization", "Bearer ${ensureToken()}") + }.build() + + return projectListReader.readValue>(execute(request)) + .map { + Project( + id = it.id, + name = it.name, + location = it.location, + organization = it.organization, + description = it.description) + } } - } - - fun readProjects(): List { - logger.debug("Requesting for projects") - val request = Request.Builder().apply { - url(baseUrl.resolve("api/projects")!!) - header("Authorization", "Bearer ${ensureToken()}") - }.build() - - return projectListReader.readValue>(execute(request)) - .map { - Project( - id = it.id, - name = it.name, - location = it.location, - organization = it.organization, - description = it.description) + + private fun execute(request: Request): String { + return httpClient.newCall(request).execute().use { response -> + if (response.isSuccessful) { + response.body?.string() + ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") + } else { + logger.error("Cannot connect to managementportal ", response.code) + throw HttpBadGatewayException("Cannot connect to managementportal : Response-code ${response.code}") + } } - } - - private fun execute(request: Request): String { - return httpClient.newCall(request).execute().use { response -> - if (response.isSuccessful) { - response.body?.string() - ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") - } else { - logger.error("Cannot connect to managementportal ", response.code) - throw HttpBadGatewayException("Cannot connect to managementportal : Response-code ${response.code}") - } } - } - - fun readParticipants(projectId: String): List { - val request = Request.Builder().apply { - url(baseUrl.newBuilder() - .addPathSegments("api/projects/$projectId/subjects") - .addQueryParameter("page", "0") - .addQueryParameter("size", Int.MAX_VALUE.toString()) - .build()) - header("Authorization", "Bearer ${ensureToken()}") - }.build() - - return userListReader.readValue>(execute(request)) - .map { - User( - id = it.login, - projectId = projectId, - externalId = it.externalId, - status = it.status) - } - } - data class SubjectDto(val login: String, val externalId: String? = null, val status: String = "DEACTIVATED", val attributes: Map = mapOf()) + fun readParticipants(projectId: String): List { + val request = Request.Builder().apply { + url(baseUrl.newBuilder() + .addPathSegments("api/projects/$projectId/subjects") + .addQueryParameter("page", "0") + .addQueryParameter("size", Int.MAX_VALUE.toString()) + .build()) + header("Authorization", "Bearer ${ensureToken()}") + }.build() + + return userListReader.readValue>(execute(request)) + .map { + User( + id = it.login, + projectId = projectId, + externalId = it.externalId, + status = it.status) + } + } + + data class SubjectDto(val login: String, val externalId: String? = null, val status: String = "DEACTIVATED", val attributes: Map = mapOf()) - data class ProjectDto(@JsonProperty("projectName") val id: String, @JsonProperty("humanReadableProjectName") val name: String? = null, val location: String? = null, val organization: String? = null, val description: String? = null) + data class ProjectDto(@JsonProperty("projectName") val id: String, @JsonProperty("humanReadableProjectName") val name: String? = null, val location: String? = null, val organization: String? = null, val description: String? = null) - companion object { - private val logger = LoggerFactory.getLogger(MPClient::class.java) - } + companion object { + private val logger = LoggerFactory.getLogger(MPClient::class.java) + } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt index a661b40c..4964c970 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt @@ -33,39 +33,39 @@ import java.util.concurrent.ConcurrentMap import javax.ws.rs.core.Context class MPProjectService(@Context private val config: Config, @Context private val mpClient: MPClient) : RadarProjectService { - private val projects = CachedSet( - Duration.ofMinutes(config.service.syncProjectsIntervalMin), - Duration.ofMinutes(1)) { - mpClient.readProjects() - } + private val projects = CachedSet( + Duration.ofMinutes(config.service.syncProjectsIntervalMin), + Duration.ofMinutes(1)) { + mpClient.readProjects() + } + + private val participants: ConcurrentMap> = ConcurrentHashMap() - private val participants: ConcurrentMap> = ConcurrentHashMap() + override fun ensureProject(projectId: String) { + if (projects.find { it.id == projectId } == null) { + throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") + } + } - override fun ensureProject(projectId: String) { - if (projects.find { it.id == projectId } == null) { - throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") + override fun userProjects(auth: Auth): List { + return projects.get() + .filter { auth.token.hasPermissionOnProject(Permission.PROJECT_READ, it.id) } } - } - override fun userProjects(auth: Auth): List { - return projects.get() - .filter { auth.token.hasPermissionOnProject(Permission.PROJECT_READ, it.id) } - } + override fun project(projectId: String): Project = projects.find { it.id == projectId } + ?: throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") - override fun project(projectId: String): Project = projects.find { it.id == projectId } - ?: throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") + override fun projectUsers(projectId: String): List { + val projectParticipants = participants.computeIfAbsent(projectId) { + CachedSet(Duration.ofMinutes(config.service.syncParticipantsIntervalMin), Duration.ofMinutes(1)) { + mpClient.readParticipants(projectId) + } + } - override fun projectUsers(projectId: String): List { - val projectParticipants = participants.computeIfAbsent(projectId) { - CachedSet(Duration.ofMinutes(config.service.syncParticipantsIntervalMin), Duration.ofMinutes(1)) { - mpClient.readParticipants(projectId) - } + return projectParticipants.get().toList() } - return projectParticipants.get().toList() - } - - override fun userByExternalId(projectId: String, externalUserId: String): User? = - projectUsers(projectId).find { it.externalId == externalUserId } + override fun userByExternalId(projectId: String, externalUserId: String): User? = + projectUsers(projectId).find { it.externalId == externalUserId } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt index 25479b72..6d4dd0c3 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt @@ -26,46 +26,46 @@ class CachedSet( private val refreshDuration: Duration, private val retryDuration: Duration, private val supplier: () -> Iterable) { - @set:Synchronized - private var cached: Set = emptySet() - set(value) { - val now = Instant.now() - field = value - nextRefresh = now.plus(refreshDuration) - nextRetry = now.plus(retryDuration) - } + @set:Synchronized + private var cached: Set = emptySet() + set(value) { + val now = Instant.now() + field = value + nextRefresh = now.plus(refreshDuration) + nextRetry = now.plus(retryDuration) + } - private var nextRefresh: Instant = Instant.MIN - private var nextRetry: Instant = Instant.MIN + private var nextRefresh: Instant = Instant.MIN + private var nextRetry: Instant = Instant.MIN - @get:Synchronized - private val state: State - get() { - val now = Instant.now() - return State(cached, - now.isAfter(nextRefresh), - now.isAfter(nextRetry)) - } + @get:Synchronized + private val state: State + get() { + val now = Instant.now() + return State(cached, + now.isAfter(nextRefresh), + now.isAfter(nextRetry)) + } - private fun refresh() = supplier.invoke().toSet() - .also { cached = it } + private fun refresh() = supplier.invoke().toSet() + .also { cached = it } - fun contains(value: T) = state.query({ it.contains(value) }, { it }) - fun find(predicate: (T) -> Boolean): T? = state.query({ it.find(predicate) }, { it != null }) - fun get(): Set = state.query({ it }, { it.isNotEmpty() }) + fun contains(value: T) = state.query({ it.contains(value) }, { it }) + fun find(predicate: (T) -> Boolean): T? = state.query({ it.find(predicate) }, { it != null }) + fun get(): Set = state.query({ it }, { it.isNotEmpty() }) - private inner class State(val cache: Set, val mustRefresh: Boolean, val mayRetry: Boolean) { - fun query(method: (Set) -> S, validityPredicate: (S) -> Boolean): S { - var result: S - if (mustRefresh) { - result = method(refresh()) - } else { - result = method(cache) - if (!validityPredicate(result) && mayRetry) { - result = method(refresh()) + private inner class State(val cache: Set, val mustRefresh: Boolean, val mayRetry: Boolean) { + fun query(method: (Set) -> S, validityPredicate: (S) -> Boolean): S { + var result: S + if (mustRefresh) { + result = method(refresh()) + } else { + result = method(cache) + if (!validityPredicate(result) && mayRetry) { + result = method(refresh()) + } + } + return result } - } - return result } - } } From 40f0bcec00b7aa1737b5c91f9b8f701acd8693a7 Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 1 Sep 2020 09:30:52 +0200 Subject: [PATCH 19/80] modify health check --- .../authorizer/resources/HeathCheckResource.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt index 7e9667d7..e10f9d5a 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt @@ -17,7 +17,13 @@ class HealthCheckResource( ) { @GET fun check(): Response { - userRepository.query(Page(0, 1)) - return Response.ok("Initialized dao and made a sample query...").build() + val status = try { + userRepository.query(Page(0, 1)) + HealthStatus("UP") + } catch (ex: Throwable) { + HealthStatus("DOWN") + } + return Response.ok(status).build() } + data class HealthStatus(val status: String) } From 251b7ea68c61e4cbc80fc2ad07c4d2c94e586714 Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 1 Sep 2020 14:02:03 +0200 Subject: [PATCH 20/80] small tweaks and copyright update --- .editorconfig | 2 +- authorizer-app-backend/build.gradle.kts | 1 - .../java/org/radarbase/authorizer/Config.kt | 16 ++++++++++++ .../java/org/radarbase/authorizer/Main.kt | 25 ++++++++---------- .../authorizer/api/ApiDeclarations.kt | 16 ++++++++++++ .../org/radarbase/authorizer/api/Project.kt | 25 ++++++++---------- .../authorizer/api/RestSourceClientMapper.kt | 16 ++++++++++++ .../authorizer/api/RestSourceUserMapper.kt | 16 ++++++++++++ .../authorizer/doa/EntityManagerExtensions.kt | 16 ++++++++++++ .../doa/RestSourceUserRepository.kt | 1 - .../doa/RestSourceUserRepositoryImpl.kt | 16 ++++++++++++ .../authorizer/doa/entity/RestSourceUser.kt | 16 ++++++++++++ .../inject/AuthorizerResourceEnhancer.kt | 16 ++++++++++++ .../inject/DoaEntityManagerFactory.kt | 25 ++++++++---------- .../inject/DoaEntityManagerFactoryFactory.kt | 25 ++++++++---------- .../inject/ManagementPortalEnhancerFactory.kt | 25 ++++++++---------- .../inject/ProjectServiceWrapper.kt | 16 ++++++++++++ .../resources/HeathCheckResource.kt | 16 ++++++++++++ .../authorizer/resources/ProjectResource.kt | 18 ++++++++++++- .../resources/RestSourceUserResource.kt | 26 ++++++++++++++++++- .../resources/SourceClientResource.kt | 21 +++++++++++++++ .../authorizer/service/RadarProjectService.kt | 16 ++++++++++++ .../service/RestSourceAuthorizationService.kt | 16 ++++++++++++ .../service/managementportal/MPClient.kt | 25 ++++++++---------- .../managementportal/MPProjectService.kt | 25 ++++++++---------- .../radarbase/authorizer/util/CachedSet.kt | 25 ++++++++---------- 26 files changed, 344 insertions(+), 117 deletions(-) diff --git a/.editorconfig b/.editorconfig index fbdb6f7c..2664f2e3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,7 +7,7 @@ root = true [*] # Change these settings to your own preference indent_style = space -indent_size = 2 +indent_size = 4 continuation_indent_size = 4 # We recommend you to keep these unchanged diff --git a/authorizer-app-backend/build.gradle.kts b/authorizer-app-backend/build.gradle.kts index 172d83ef..de46ca5c 100644 --- a/authorizer-app-backend/build.gradle.kts +++ b/authorizer-app-backend/build.gradle.kts @@ -61,7 +61,6 @@ dependencies { runtimeOnly("org.postgresql:postgresql:42.2.5") runtimeOnly("ch.qos.logback:logback-classic:${project.extra["logbackVersion"]}") -// testImplementation("com.h2database:h2:1.4.199") testImplementation("org.junit.jupiter:junit-jupiter:5.4.2") testImplementation("org.hamcrest:hamcrest-all:1.3") testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0") diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt index f03456e8..63152acc 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer import org.radarbase.authorizer.inject.ManagementPortalEnhancerFactory diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt index cad03ed8..c0eb6248 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Main.kt @@ -1,20 +1,17 @@ /* + * Copyright 2020 The Hyve * - * * Copyright 2020 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.radarbase.authorizer diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt index 5fb6c52e..c3cc4ae6 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.api import com.fasterxml.jackson.annotation.JsonIgnoreProperties diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt index 2c711556..83c97ca1 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt @@ -1,20 +1,17 @@ /* + * Copyright 2020 The Hyve * - * * Copyright 2019 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.radarbase.authorizer.api diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt index d822e291..89a5909f 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceClientMapper.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.api import org.radarbase.authorizer.RestSourceClient diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt index d6abd7a9..214ac6b0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.api import org.radarbase.authorizer.doa.entity.RestSourceUser diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt index 0d74f3a7..24dee5c0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.doa import org.radarbase.authorizer.logger diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index 2b1002db..511258b5 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -7,7 +7,6 @@ import org.radarbase.authorizer.doa.entity.RestSourceUser import java.time.Instant interface RestSourceUserRepository { - fun createOrUpdate(user: RestOauth2AccessToken, sourceType: String): RestSourceUser fun read(id: Long): RestSourceUser? fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index dd87f75c..87afde9b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.doa import org.radarbase.authorizer.api.Page diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt index 8d4b6848..fbe106b2 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RestSourceUser.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.doa.entity import java.time.Instant diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt index db383e3f..59e5d00e 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.inject import com.fasterxml.jackson.annotation.JsonInclude diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt index d2188851..fb3ec58a 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt @@ -1,20 +1,17 @@ /* + * Copyright 2020 The Hyve * - * * Copyright 2020 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.radarbase.authorizer.inject diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt index 512efe2e..044612e6 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt @@ -1,20 +1,17 @@ /* + * Copyright 2020 The Hyve * - * * Copyright 2019 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.radarbase.authorizer.inject diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt index c6266bb8..0f007958 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt @@ -1,20 +1,17 @@ /* + * Copyright 2020 The Hyve * - * * Copyright 2019 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.radarbase.authorizer.inject diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt index d319e7ce..4795111f 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.inject import org.radarbase.authorizer.service.RadarProjectService diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt index e10f9d5a..f81fd707 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.resources import org.radarbase.authorizer.api.Page diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt index 8f024c81..79f3db6b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.resources import org.radarbase.authorizer.api.Project @@ -31,7 +47,7 @@ class ProjectResource( @GET @Path("{projectId}/users") - @NeedsPermission(Permission.Entity.PROJECT, Permission.Operation.READ, "projectId") + @NeedsPermission(Permission.Entity.SUBJECT, Permission.Operation.READ, "projectId") fun users(@PathParam("projectId") projectId: String): UserList { return UserList(projectService.projectUsers(projectId)) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt index 8d37d025..5d1b2e21 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.resources import org.radarbase.authorizer.api.* @@ -7,6 +23,7 @@ import org.radarbase.authorizer.service.RadarProjectService import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.jersey.auth.Auth import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission import org.radarbase.jersey.exception.HttpBadRequestException import org.radarbase.jersey.exception.HttpConflictException import org.radarbase.jersey.exception.HttpNotFoundException @@ -36,6 +53,7 @@ class RestSourceUserResource( ) { @GET + @NeedsPermission(Permission.Entity.SUBJECT, Permission.Operation.READ) fun query( @QueryParam("projectId") projectId: String?, @QueryParam("sourceType") sourceType: String?, @@ -43,7 +61,7 @@ class RestSourceUserResource( @DefaultValue("1") @QueryParam("page") pageNumber: Int): RestSourceUsers { if (projectId != null) { - auth.checkPermissionOnProject(Permission.PROJECT_READ, projectId) + auth.checkPermissionOnProject(Permission.SUBJECT_READ, projectId) } val queryPage = Page(pageNumber = pageNumber, pageSize = pageSize) @@ -68,6 +86,7 @@ class RestSourceUserResource( @POST @Path("{id}") + @NeedsPermission(Permission.Entity.SUBJECT, Permission.Operation.UPDATE) fun update( @PathParam("id") userId: Long, user: RestSourceUserDTO): RestSourceUserDTO { @@ -79,6 +98,7 @@ class RestSourceUserResource( @GET @Path("{id}") + @NeedsPermission(Permission.Entity.SUBJECT, Permission.Operation.READ) fun readUser(@PathParam("id") userId: Long): RestSourceUserDTO { val user = ensureUser(userId) auth.checkPermissionOnSubject(Permission.SUBJECT_READ, user.projectId, user.userId) @@ -87,6 +107,7 @@ class RestSourceUserResource( @DELETE @Path("{id}") + @NeedsPermission(Permission.Entity.SUBJECT, Permission.Operation.UPDATE) fun deleteUser(@PathParam("id") userId: Long): Response { val user = ensureUser(userId) auth.checkPermissionOnSubject(Permission.SUBJECT_UPDATE, user.projectId, user.userId) @@ -99,6 +120,7 @@ class RestSourceUserResource( @POST @Path("{id}/reset") + @NeedsPermission(Permission.Entity.SUBJECT, Permission.Operation.UPDATE) fun reset( @PathParam("id") userId: Long, user: RestSourceUserDTO): RestSourceUserDTO { @@ -111,6 +133,7 @@ class RestSourceUserResource( @GET @Path("{id}/token") + @NeedsPermission(Permission.Entity.MEASUREMENT, Permission.Operation.CREATE) fun requestToken(@PathParam("id") userId: Long): TokenDTO { val user = ensureUser(userId) auth.checkPermissionOnSubject(Permission.MEASUREMENT_CREATE, user.projectId, user.userId) @@ -119,6 +142,7 @@ class RestSourceUserResource( @POST @Path("{id}/token") + @NeedsPermission(Permission.Entity.MEASUREMENT, Permission.Operation.CREATE) fun refreshToken(@PathParam("id") userId: Long): TokenDTO { val user = ensureUser(userId) auth.checkPermissionOnSubject(Permission.MEASUREMENT_CREATE, user.projectId, user.userId) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt index 2f7d2dc0..dea175ba 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.resources import org.radarbase.authorizer.RestSourceClients @@ -6,7 +22,9 @@ import org.radarbase.authorizer.api.ShareableClientDetail import org.radarbase.authorizer.api.ShareableClientDetails import org.radarbase.jersey.auth.Auth import org.radarbase.jersey.auth.Authenticated +import org.radarbase.jersey.auth.NeedsPermission import org.radarbase.jersey.exception.HttpNotFoundException +import org.radarcns.auth.authorization.Permission import javax.annotation.Resource import javax.inject.Singleton import javax.ws.rs.GET @@ -32,14 +50,17 @@ class SourceClientResource( private val sharableClientDetails = clientMapper.fromSourceClientConfigs(restSourceClients.clients) @GET + @NeedsPermission(Permission.Entity.SOURCETYPE, Permission.Operation.READ) fun clients(): ShareableClientDetails = sharableClientDetails @GET @Path("type") + @NeedsPermission(Permission.Entity.SOURCETYPE, Permission.Operation.READ) fun types(): List = sourceTypes @GET @Path("{type}") + @NeedsPermission(Permission.Entity.SOURCETYPE, Permission.Operation.READ) fun client(@PathParam("type") type: String): ShareableClientDetail { return sharableClientDetails.sourceClients.find { it.sourceType == type } ?: throw HttpNotFoundException("source-type-not-found", "Client with source-type $type is not configured") diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt index 35393231..0c97ae52 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.service import org.radarbase.authorizer.api.Project diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt index a90a600b..6e8e1bfe 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.service import com.fasterxml.jackson.databind.ObjectMapper diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt index dd5c7ed8..bf5fe1f5 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt @@ -1,20 +1,17 @@ /* + * Copyright 2020 The Hyve * - * * Copyright 2019 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.radarbase.authorizer.service.managementportal diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt index 4964c970..1f6b4db8 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt @@ -1,20 +1,17 @@ /* + * Copyright 2020 The Hyve * - * * Copyright 2019 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.radarbase.authorizer.service.managementportal diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt index 6d4dd0c3..8d65b10d 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt @@ -1,20 +1,17 @@ /* + * Copyright 2020 The Hyve * - * * Copyright 2019 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.radarbase.authorizer.util From 8b2a2f1557c34e75b9c94d81a594ae8c45ae64e6 Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 1 Sep 2020 14:03:28 +0200 Subject: [PATCH 21/80] upgrade gradle --- .../doa/RestSourceUserRepository.kt | 16 +++++++++++++ build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 58910 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- gradlew.bat | 21 +++--------------- 6 files changed, 22 insertions(+), 21 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index 511258b5..78c7d2d8 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.radarbase.authorizer.doa import org.radarbase.authorizer.api.Page diff --git a/build.gradle.kts b/build.gradle.kts index dbe1c6fc..bf8cbb73 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,5 +8,5 @@ subprojects { } tasks.wrapper { - gradleVersion = "6.5" + gradleVersion = "6.6.1" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c053550b91381bbd28b1afc82d634bf73a8a..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 6656 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 Yte!%o?r4GTrVtxD61Ym!|5fq-1K$0e!T1w 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 622ab64a..12d38de6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index fbd7c515..4f906e0c 100755 --- a/gradlew +++ b/gradlew @@ -130,7 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath diff --git a/gradlew.bat b/gradlew.bat index 5093609d..107acd32 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +64,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell From d3371acbc1c2008f5ad03d24e764a47bd06d72bd Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 1 Sep 2020 14:04:04 +0200 Subject: [PATCH 22/80] migrate Dockerfile --- authorizer-app-backend/Dockerfile | 52 ++++++++++++++++++------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/authorizer-app-backend/Dockerfile b/authorizer-app-backend/Dockerfile index adf91fe5..8463b767 100644 --- a/authorizer-app-backend/Dockerfile +++ b/authorizer-app-backend/Dockerfile @@ -1,22 +1,32 @@ # Build stage -FROM openjdk:8-jdk-alpine as builder - -WORKDIR /app -COPY gradlew /app/ -COPY gradle/wrapper gradle/wrapper -COPY gradle gradle -COPY build.gradle settings.gradle /app/ -RUN ./gradlew --version downloadDependencies - -COPY src src -RUN ./gradlew assemble -# Run stage -FROM openjdk:8-jre-alpine - -# Add the war and changelogs files from build stage -COPY --from=builder app/build/libs/*.jar /app.jar - -EXPOSE 8080 -CMD echo "The application will start in ${APP_SLEEP}s..." && \ - sleep ${APP_SLEEP} && \ - java -Djava.security.egd=file:/dev/./urandom -jar /app.jar +FROM gradle:6.6.1-jdk11 as builder + + +RUN mkdir /code +WORKDIR /code + +COPY ./build.gradle.kts ./settings.gradle.kts /code/ +COPY authorizer-app-backend/build.gradle.kts /code/authorizer-app-backend/ +RUN gradle :authorizer-app-backend:downloadDependencies + +COPY authorizer-app-backend/src /code/authorizer-app-backend/src + +RUN gradle -Dkotlin.compiler.execution.strategy="in-process" -Dorg.gradle.parallel=false -Pkotlin.incremental=false :authorizer-app-backend:distTar \ + && cd authorizer-app-backend/build/distributions \ + && tar xf *.tar \ + && rm *.tar authorizer-app-backend-*/lib/authorizer-app-backend-*.jar + +FROM openjdk:11-jre-slim + +MAINTAINER @nivemaham @blootsvoets + +LABEL description="RADAR-base rest sources authorizer backend application" + +COPY --from=builder /code/authorizer-app-backend/build/distributions/authorizer-app-backend-*/bin/* /usr/bin/ +COPY --from=builder /code/authorizer-app-backend/build/distributions/authorizer-app-backend-*/lib/* /usr/lib/ +COPY --from=builder /code/authorizer-app-backend/build/libs/authorizer-app-backend-*.jar /usr/lib/ + +EXPOSE 8090 + +CMD ["authorizer-app-backend"] + From 524fec9ace4be1c375192b62ad7b841795166989 Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 1 Sep 2020 14:19:45 +0200 Subject: [PATCH 23/80] update docker-compose --- README.md | 4 +- authorizer-app-backend/Dockerfile | 2 +- .../resource/RestSourceUserResourceTest.java | 260 +++++++++--------- docker-compose.yml | 21 +- .../etc/rest-source-authorizer/authorizer.yml | 10 +- docker/etc/webserver/nginx-proxy.conf | 2 +- 6 files changed, 150 insertions(+), 149 deletions(-) diff --git a/README.md b/README.md index 9d8e3e79..5af3a274 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ An application to get authorization from users to access their data through 3rd 1. It has one active entity where we store user properties. 2. Has liquibase support to enable seamless database schema migration. 3. Has a simple web-service with REST Endpoints to share configured source-type client details and authorized users. -4. Currently various source-types can be configured using a YAML file and these entries are stored in memory. +4. Currently various source-types can be configured using the configuration file and these entries are stored in memory. ## APIs to be used by REST Source-Connectors `RADAR REST Source-Connectors` can use the APIs as follows. - 1. To get all configured users for a particular source-type use `GET */users/{source-type}` . + 1. To get all configured users for a particular source-type use `GET */users?sourceType={source-type}` . 2. To get details of a particular user use `GET */users/{id}`. 3. To get the token details of a particular user use `GET */users/{id}/token`. 4. To refresh the token of a particular user use `POST /users/{id}/token`. diff --git a/authorizer-app-backend/Dockerfile b/authorizer-app-backend/Dockerfile index 8463b767..ccf488fb 100644 --- a/authorizer-app-backend/Dockerfile +++ b/authorizer-app-backend/Dockerfile @@ -26,7 +26,7 @@ COPY --from=builder /code/authorizer-app-backend/build/distributions/authorizer- COPY --from=builder /code/authorizer-app-backend/build/distributions/authorizer-app-backend-*/lib/* /usr/lib/ COPY --from=builder /code/authorizer-app-backend/build/libs/authorizer-app-backend-*.jar /usr/lib/ -EXPOSE 8090 +EXPOSE 8085 CMD ["authorizer-app-backend"] diff --git a/authorizer-app-backend/src/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java b/authorizer-app-backend/src/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java index 5e500b9b..1d2b5e53 100644 --- a/authorizer-app-backend/src/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java +++ b/authorizer-app-backend/src/test/java/org/radarbase/authorizer/webapp/resource/RestSourceUserResourceTest.java @@ -1,130 +1,130 @@ -/* - * - * * Copyright 2018 The Hyve - * * - * * Licensed under the Apache License, Version 2.0 (the "License"); - * * you may not use this file except in compliance with the License. - * * You may obtain a copy of the License at - * * - * * http://www.apache.org/licenses/LICENSE-2.0 - * * - * * Unless required by applicable law or agreed to in writing, software - * * distributed under the License is distributed on an "AS IS" BASIS, - * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * * See the License for the specific language governing permissions and - * * limitations under the License. - * * - * - */ - -package org.radarbase.authorizer.webapp.resource; - -import static org.hamcrest.Matchers.hasItem; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.time.Duration; -import java.time.Instant; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.MockitoAnnotations; -import org.radarbase.authorizer.RadarRestSourceAuthorizerApplication; -import org.radarbase.authorizer.doa.RestSourceUser; -import org.radarbase.authorizer.doa.RestSourceUserRepository; -import org.radarbase.authorizer.service.RestSourceUserService; -import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.transaction.annotation.Transactional; - -@RunWith(SpringRunner.class) -@SpringBootTest(classes = RadarRestSourceAuthorizerApplication.class) -public class RestSourceUserResourceTest { - - @Autowired - private RestSourceUserService restSourceUserService; - - @Autowired - private RestSourceUserRepository restSourceUserRepository; - - public static final String DEFAULT_PROJ_NAME = "Test-proj"; - public static final String DEFAULT_USER_ID = "Test-sub"; - public static final String DEFAULT_SOURCE_ID = "Test-source"; - public static final String DEFAULT_DEVICE_TYPE = "Fitbit"; - public static final Instant DEFAULT_START_TIME = Instant.now().minus(Duration.ofHours(1)); - public static final Instant DEFAULT_END_TIME = Instant.now().plus(Duration.ofHours(1)); - public static final Boolean DEFAULT_AUTHORIZED = false; - public static final String DEFAULT_EXTERNAL_USER_ID = "86420984"; - - private RestSourceUser sampleRestSourceUser; - - private MockMvc restUserMockMvc; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - RestSourceUserResource restSourceUserResource = - new RestSourceUserResource(restSourceUserService, null); - ReflectionTestUtils.setField(restSourceUserResource, "restSourceUserService", restSourceUserService); - - - - this.restUserMockMvc = MockMvcBuilders.standaloneSetup(restSourceUserResource).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 RestSourceUser createEntity() { - return new RestSourceUser() - .projectId(DEFAULT_PROJ_NAME) - .userId(DEFAULT_USER_ID) - .sourceId(DEFAULT_SOURCE_ID) - .startDate(DEFAULT_START_TIME) - .endDate(DEFAULT_END_TIME) - .externalUserId(DEFAULT_EXTERNAL_USER_ID) - .authorized(DEFAULT_AUTHORIZED); - } - - public static RestSourceUserPropertiesDTO createDefaultDeviceDto() { - return new RestSourceUserPropertiesDTO(createEntity()); - } - - @Before - public void initTest() { - sampleRestSourceUser = createEntity(); - } - - @Test - @Transactional - public void getAllUsers() throws Exception { - // Initialize the database - restSourceUserRepository.saveAndFlush(sampleRestSourceUser); - - // Get all the users - restUserMockMvc.perform(get("/users")) - .andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) - .andExpect(jsonPath("$.users.[*].projectId").value(hasItem(DEFAULT_PROJ_NAME))) - .andExpect(jsonPath("$.users.[*].userId").value(hasItem(DEFAULT_USER_ID))) - .andExpect(jsonPath("$.users.[*].sourceId").value(hasItem(DEFAULT_SOURCE_ID))) - .andExpect(jsonPath("$.users.[*].externalUserId").value( - hasItem(DEFAULT_EXTERNAL_USER_ID.toString()))); - } - - -} +///* +// * +// * * Copyright 2018 The Hyve +// * * +// * * Licensed under the Apache License, Version 2.0 (the "License"); +// * * you may not use this file except in compliance with the License. +// * * You may obtain a copy of the License at +// * * +// * * http://www.apache.org/licenses/LICENSE-2.0 +// * * +// * * Unless required by applicable law or agreed to in writing, software +// * * distributed under the License is distributed on an "AS IS" BASIS, +// * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * * See the License for the specific language governing permissions and +// * * limitations under the License. +// * * +// * +// */ +// +//package org.radarbase.authorizer.webapp.resource; +// +//import static org.hamcrest.Matchers.hasItem; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +// +//import java.time.Duration; +//import java.time.Instant; +// +//import org.junit.Before; +//import org.junit.Test; +//import org.junit.runner.RunWith; +//import org.mockito.MockitoAnnotations; +//import org.radarbase.authorizer.RadarRestSourceAuthorizerApplication; +//import org.radarbase.authorizer.doa.RestSourceUser; +//import org.radarbase.authorizer.doa.RestSourceUserRepository; +//import org.radarbase.authorizer.service.RestSourceUserService; +//import org.radarbase.authorizer.service.dto.RestSourceUserPropertiesDTO; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.http.MediaType; +//import org.springframework.test.context.junit4.SpringRunner; +//import org.springframework.test.util.ReflectionTestUtils; +//import org.springframework.test.web.servlet.MockMvc; +//import org.springframework.test.web.servlet.setup.MockMvcBuilders; +//import org.springframework.transaction.annotation.Transactional; +// +//@RunWith(SpringRunner.class) +//@SpringBootTest(classes = RadarRestSourceAuthorizerApplication.class) +//public class RestSourceUserResourceTest { +// +// @Autowired +// private RestSourceUserService restSourceUserService; +// +// @Autowired +// private RestSourceUserRepository restSourceUserRepository; +// +// public static final String DEFAULT_PROJ_NAME = "Test-proj"; +// public static final String DEFAULT_USER_ID = "Test-sub"; +// public static final String DEFAULT_SOURCE_ID = "Test-source"; +// public static final String DEFAULT_DEVICE_TYPE = "Fitbit"; +// public static final Instant DEFAULT_START_TIME = Instant.now().minus(Duration.ofHours(1)); +// public static final Instant DEFAULT_END_TIME = Instant.now().plus(Duration.ofHours(1)); +// public static final Boolean DEFAULT_AUTHORIZED = false; +// public static final String DEFAULT_EXTERNAL_USER_ID = "86420984"; +// +// private RestSourceUser sampleRestSourceUser; +// +// private MockMvc restUserMockMvc; +// +// @Before +// public void setUp() { +// MockitoAnnotations.initMocks(this); +// +// RestSourceUserResource restSourceUserResource = +// new RestSourceUserResource(restSourceUserService, null); +// ReflectionTestUtils.setField(restSourceUserResource, "restSourceUserService", restSourceUserService); +// +// +// +// this.restUserMockMvc = MockMvcBuilders.standaloneSetup(restSourceUserResource).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 RestSourceUser createEntity() { +// return new RestSourceUser() +// .projectId(DEFAULT_PROJ_NAME) +// .userId(DEFAULT_USER_ID) +// .sourceId(DEFAULT_SOURCE_ID) +// .startDate(DEFAULT_START_TIME) +// .endDate(DEFAULT_END_TIME) +// .externalUserId(DEFAULT_EXTERNAL_USER_ID) +// .authorized(DEFAULT_AUTHORIZED); +// } +// +// public static RestSourceUserPropertiesDTO createDefaultDeviceDto() { +// return new RestSourceUserPropertiesDTO(createEntity()); +// } +// +// @Before +// public void initTest() { +// sampleRestSourceUser = createEntity(); +// } +// +// @Test +// @Transactional +// public void getAllUsers() throws Exception { +// // Initialize the database +// restSourceUserRepository.saveAndFlush(sampleRestSourceUser); +// +// // Get all the users +// restUserMockMvc.perform(get("/users")) +// .andExpect(status().isOk()) +// .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) +// .andExpect(jsonPath("$.users.[*].projectId").value(hasItem(DEFAULT_PROJ_NAME))) +// .andExpect(jsonPath("$.users.[*].userId").value(hasItem(DEFAULT_USER_ID))) +// .andExpect(jsonPath("$.users.[*].sourceId").value(hasItem(DEFAULT_SOURCE_ID))) +// .andExpect(jsonPath("$.users.[*].externalUserId").value( +// hasItem(DEFAULT_EXTERNAL_USER_ID.toString()))); +// } +// +// +//} diff --git a/docker-compose.yml b/docker-compose.yml index 24f89154..df1df56d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,8 +29,10 @@ services: radarbase-postgresql: image: radarbase/radarbase-postgres:latest +# volumes: +# - "./data/:/var/lib/postgresql/data/" # ports: -# - "5432:5432" +# - "5434:5432" environment: - POSTGRES_USER=radarcns - POSTGRES_PASSWORD=radarcns @@ -41,24 +43,15 @@ services: image: radarbase/radar-rest-source-auth-backend:dev build: context: . + dockerfile: authorizer-app-backend/Dockerfile depends_on: - radarbase-postgresql - managementportal-app # ports: -# - "8085:8080" - environment: - - SPRING_DATASOURCE_URL=jdbc:postgresql://radarbase-postgresql:5432/restsourceauthorizer - - SPRING_DATASOURCE_USERNAME=radarcns - - SPRING_DATASOURCE_PASSWORD=radarcns - - REST_SOURCE_AUTHORIZER_SOURCE_CLIENTS_FILE_PATH=app-includes/rest_source_clients_configs.yml - - REST_SOURCE_AUTHORIZER_VALIDATOR=managementportal - - REST_SOURCE_AUTHORIZER_MANAGEMENT_PORTAL_BASE_URL=http://managementportal-app:8080/managementportal/ - - REST_SOURCE_AUTHORIZER_MANAGEMENT_PORTAL_OAUTH_CLIENT_ID=radar_rest_sources_auth_backend - - REST_SOURCE_AUTHORIZER_MANAGEMENT_PORTAL_OAUTH_CLIENT_SECRET=secret - - LOGGING_LEVEL_ORG_RADARBASE_AUTHORIZER=DEBUG - - APP_SLEEP=10 # gives time for the database to boot before the application +# - "8085:8085" volumes: - - ./docker/etc/rest-source-authorizer/:/app-includes/ + - ./docker/etc/rest-source-authorizer/authorizer.yml:/etc/authorizer-app-backend/authorizer.yml + command: ["authorizer-app-backend", "/etc/authorizer-app-backend/authorizer.yml"] healthcheck: test: ["CMD", "wget", "--spider", "http://localhost:8080/health"] interval: 1m30s diff --git a/docker/etc/rest-source-authorizer/authorizer.yml b/docker/etc/rest-source-authorizer/authorizer.yml index 9a5560b5..791c48d3 100644 --- a/docker/etc/rest-source-authorizer/authorizer.yml +++ b/docker/etc/rest-source-authorizer/authorizer.yml @@ -1,6 +1,6 @@ service: # Interval time in minutes for syncing projects and subjects. - baseUri: http://0.0.0.0:8080/rest-sources/backend/ + baseUri: http://0.0.0.0:8085/rest-sources/backend/ advertisedBaseUri: http://0.0.0.0:8080/rest-sources/backend/ enableCors: true @@ -18,3 +18,11 @@ database: jdbcUser: radarcns jdbcPassword: radarcns hibernateDialect: org.hibernate.dialect.PostgreSQLDialect + +restSourceClients: + - sourceType: FitBit + authorizationEndpoint: https://www.fitbit.com/oauth2/authorize + tokenEndpoint: https://api.fitbit.com/oauth2/token + clientId: Fitbit-clientid + clientSecret: Fitbit-clientsecret + scope: activity heartrate sleep profile diff --git a/docker/etc/webserver/nginx-proxy.conf b/docker/etc/webserver/nginx-proxy.conf index 73fe4f46..9c459dc9 100644 --- a/docker/etc/webserver/nginx-proxy.conf +++ b/docker/etc/webserver/nginx-proxy.conf @@ -22,7 +22,7 @@ http { } location /rest-sources/backend/ { - proxy_pass http://radar-rest-sources-backend:8080/; + proxy_pass http://radar-rest-sources-backend:8085/rest-sources/backend/; proxy_set_header Host $host; } From 51904dab1e2690155b7ce5897870cbc9b99f84f2 Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 1 Sep 2020 14:43:41 +0200 Subject: [PATCH 24/80] update README --- README.md | 72 +++++++------------ build.gradle.kts | 2 +- .../managementportal/oauth_client_details.csv | 2 +- .../authorizer.yml.template | 35 +++++++++ 4 files changed, 61 insertions(+), 50 deletions(-) create mode 100644 docker/etc/rest-source-authorizer/authorizer.yml.template diff --git a/README.md b/README.md index 5af3a274..d8b2eeb6 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,9 @@ An application to get authorization from users to access their data through 3rd 3. To get the token details of a particular user use `GET */users/{id}/token`. 4. To refresh the token of a particular user use `POST /users/{id}/token`. -## Usage -To run this application from source: - -```$cmd -./gradlew build assemble -java -jar radar-rest-sources-authorizer*.jar -``` ## Installation To install functional RADAR-base Rest-Sources Authorizer application with minimal dependencies from source, please use the `docker-compose.yml` under the root directory -1. Copy the `docker/etc/rest-sources-authorizer/rest_source_clients_configs.yml.template` into `docker/etc/rest-sources-authorizer/rest_source_clients_configs.yml` and modify the `client_id` and `client_secret` with your Fitbit client application credentials. +1. Copy the `docker/etc/rest-source-authorizer/authorizer.yml.template` into `docker/etc/rest-source-authorizer/authorizer.yml` and modify the `restSourceClients.FitBit.clientId` and `restSourceClients.FitBit.clientSecret` with your Fitbit client application credentials. ```bash docker-compose up -d --build ``` @@ -32,48 +25,31 @@ You can find the Authorizer app running on `http://localhost:8080/rest-sources/a You can find the Management Portal app running on `http://localhost:8080/managementportal/` ## Validation -There is validation available for the properties of the subject entered by the user. These are currenlty validated using the details from the Management portal. You can configure this according to your requirements as follows - - -### If don't need validation -Add the `REST_SOURCE_AUTHORIZER_VALIDATOR` env var to your docker-compose service to disable validation- -```yaml - radar-rest-sources-backend: - image: radarbase/radar-rest-source-auth-backend:1.2.1 -... - environment: -... - - REST_SOURCE_AUTHORIZER_VALIDATOR="" - volumes: - - ./etc/rest-source-authorizer/:/app-includes/ -... - -``` -**Note: This will only disable backend validation. The frontend validation(based on Regex) will still exist.** - -### Enable validation using Management Portal +All users registered with the application will be validated against ManagementPortal for integrity and security. +Front-end application will perform additional validation based on regex to improve user experience. -#### First Create a new oAuth client in Management Portal +### Registering OAuth Clients with ManagementPortal To add new OAuth clients, you can add at runtime through the UI on Management Portal, or you can add them to the OAuth clients file referenced by the MANAGEMENTPORTAL_OAUTH_CLIENTS_FILE configuration option. For more info, see [officail docs](https://github.com/RADAR-base/ManagementPortal#oauth-clients) -The OAuth client should have the following properties- - -1. scope - `PROJECT.READ, SUBJECT.READ` -2. grant_type - `client_credentials` +The OAuth client for authorizer-app-backend should have the following properties. +```properties +client-id: radar_rest_sources_auth_backend +client-secret: Confidential +grant-type: client_credentials +resources: res_ManagementPortal +scope: PROJECT.READ,SUBJECT.READ +``` -#### Then add the following to your rest authoriser service -Add the following env vars to your docker-compose service- -```yaml - radar-rest-sources-backend: - image: radarbase/radar-rest-source-auth-backend:1.2.1 -... - environment: -... - - REST_SOURCE_AUTHORIZER_VALIDATOR=managementportal - - REST_SOURCE_AUTHORIZER_MANAGEMENT_PORTAL_BASE_URL=http://managementportal-app:8080/managementportal/ - - REST_SOURCE_AUTHORIZER_MANAGEMENT_PORTAL_OAUTH_CLIENT_ID=radar_rest_sources_auth - - REST_SOURCE_AUTHORIZER_MANAGEMENT_PORTAL_OAUTH_CLIENT_SECRET=secret - volumes: - - ./etc/rest-source-authorizer/:/app-includes/ -... +The OAuth client for authorizer-app should have the following properties. +```properties +client-id: radar_rest_sources_authorizer +client-secret: Empty +grant-type: authorization_code +resources: res_restAuthorizer +scope: SOURCETYPE.READ,PROJECT.READ,SUBJECT.READ,SUBJECT.UPDATE +callback-url: /login +# the callback-url should be resolvable and match with the environment variable of radar-rest-sources-authorizer -> AUTH_CALLBACK_URL in the docker-compose.yml file. ``` +## Migrating from 1.*.* version to 2.* -**Note**: Make sure to configure the client id and client secret as created in the Management portal +1. Move configurations from application.yml and environment variables to `authorizer.yml` following the description in `authorizer.yml.template`. +2. Move configurations from rest_source_clients_configs.yml to `restSourceClients` in corresponding YAML format in `authorizer.yml`. diff --git a/build.gradle.kts b/build.gradle.kts index bf8cbb73..49bdd6c2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { subprojects { group = "org.radarbase" - version = "2.0.0-SNAPSHOT" + version = "2.0-SNAPSHOT" } tasks.wrapper { diff --git a/docker/etc/managementportal/oauth_client_details.csv b/docker/etc/managementportal/oauth_client_details.csv index 4aa2254f..a7095d45 100644 --- a/docker/etc/managementportal/oauth_client_details.csv +++ b/docker/etc/managementportal/oauth_client_details.csv @@ -6,4 +6,4 @@ radar_restapi;res_ManagementPortal;secret;SUBJECT.READ,PROJECT.READ,SOURCE.READ, 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,MEASUREMENT.READ;client_credentials;;;43200;259200;{}; radar_rest_sources_auth_backend;res_ManagementPortal;secret;SUBJECT.READ,PROJECT.READ;client_credentials;;;43200;259200;{}; -radar_rest_sources_authorizer;res_restAuthorizer;;SOURCETYPE.READ,PROJECT.READ,SUBJECT.READ,SUBJECT.UPDATE;authorization_code;http://localhost:8080/login;3600;78000;; +radar_rest_sources_authorizer;res_restAuthorizer;;SOURCETYPE.READ,PROJECT.READ,SUBJECT.READ,SUBJECT.UPDATE;authorization_code;http://localhost:8080/rest-sources/authorizer/login;3600;78000;; diff --git a/docker/etc/rest-source-authorizer/authorizer.yml.template b/docker/etc/rest-source-authorizer/authorizer.yml.template new file mode 100644 index 00000000..5b10918a --- /dev/null +++ b/docker/etc/rest-source-authorizer/authorizer.yml.template @@ -0,0 +1,35 @@ +service: + # base url for authorizer-app-backend service + baseUri: http://0.0.0.0:8085/rest-sources/backend/ + # advertised url of the service + advertisedBaseUri: http://0.0.0.0:8080/rest-sources/backend/ + # if cors filter should be enabled + enableCors: true + +auth: + # Management Portal URL + managementPortalUrl: http://managementportal-app:8080/managementportal/ + # OAuth2 Client id of authorizer-app-backend application + clientId: radar_rest_sources_auth_backend + # OAuth2 Client Secret of authorizer-app-backend client + clientSecret: secret + +database: + # JDBC Driver of the database used for persistence + jdbcDriver: org.postgresql.Driver + # JBDC database connection url + jdbcUrl: jdbc:postgresql://radarbase-postgresql:5432/restsourceauthorizer + # Database username + jdbcUser: radarcns + # Database password + jdbcPassword: radarcns + hibernateDialect: org.hibernate.dialect.PostgreSQLDialect + +restSourceClients: + # List of rest-source types and properties e.g FitBit, Garmin + - sourceType: FitBit + authorizationEndpoint: https://www.fitbit.com/oauth2/authorize + tokenEndpoint: https://api.fitbit.com/oauth2/token + clientId: Fitbit-clientid + clientSecret: Fitbit-clientsecret + scope: activity heartrate sleep profile From 5be383649a5b339b4b9c4d0c7dd7854d65148c6c Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 1 Sep 2020 15:00:05 +0200 Subject: [PATCH 25/80] core query param name to match with old endpoint --- .../radarbase/authorizer/resources/RestSourceUserResource.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt index 5d1b2e21..738086e0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt @@ -55,8 +55,8 @@ class RestSourceUserResource( @GET @NeedsPermission(Permission.Entity.SUBJECT, Permission.Operation.READ) fun query( - @QueryParam("projectId") projectId: String?, - @QueryParam("sourceType") sourceType: String?, + @QueryParam("project-id") projectId: String?, + @QueryParam("source-type") sourceType: String?, @QueryParam("size") pageSize: Int?, @DefaultValue("1") @QueryParam("page") pageNumber: Int): RestSourceUsers { From ad9a6e3ddaae851a87689011129e6936c983f85c Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 1 Sep 2020 15:00:44 +0200 Subject: [PATCH 26/80] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8b2eeb6..aa741cd7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ An application to get authorization from users to access their data through 3rd ## APIs to be used by REST Source-Connectors `RADAR REST Source-Connectors` can use the APIs as follows. - 1. To get all configured users for a particular source-type use `GET */users?sourceType={source-type}` . + 1. To get all configured users for a particular source-type use `GET */users?source-type={source-type}` . 2. To get details of a particular user use `GET */users/{id}`. 3. To get the token details of a particular user use `GET */users/{id}/token`. 4. To refresh the token of a particular user use `POST /users/{id}/token`. From fe48613358a6013efdd686add5627f16de445771 Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 1 Sep 2020 15:26:33 +0200 Subject: [PATCH 27/80] remove old template --- .../rest_source_clients_configs.yml.template | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 docker/etc/rest-source-authorizer/rest_source_clients_configs.yml.template diff --git a/docker/etc/rest-source-authorizer/rest_source_clients_configs.yml.template b/docker/etc/rest-source-authorizer/rest_source_clients_configs.yml.template deleted file mode 100644 index 0d482abf..00000000 --- a/docker/etc/rest-source-authorizer/rest_source_clients_configs.yml.template +++ /dev/null @@ -1,7 +0,0 @@ -rest_source_clients: - - source_type: FitBit - authorization_endpoint: https://www.fitbit.com/oauth2/authorize - token_endpoint: https://api.fitbit.com/oauth2/token - client_id: FITBITIT - client_secret: FITBITSECRET - scope: activity heartrate sleep profile \ No newline at end of file From b5d4a90b04f27fc3d98bc54356d162cd6ce081f6 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Tue, 1 Sep 2020 15:49:20 +0200 Subject: [PATCH 28/80] Update .editorconfig --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 2664f2e3..18ccaf40 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ root = true # Change these settings to your own preference indent_style = space indent_size = 4 -continuation_indent_size = 4 +continuation_indent_size = 8 # We recommend you to keep these unchanged end_of_line = lf From c6c6c41cead180dc910e573a33751fdac2079595 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Tue, 1 Sep 2020 16:44:29 +0200 Subject: [PATCH 29/80] Update dependencies --- .gitignore | 5 ++-- authorizer-app-backend/build.gradle.kts | 35 ++++++++++++++----------- build.gradle.kts | 4 --- settings.gradle | 2 -- settings.gradle.kts | 12 +++++++++ 5 files changed, 35 insertions(+), 23 deletions(-) delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore index bdbff1e6..5fb2a277 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .gradle -/build/ +build/ +out/ !gradle/wrapper/gradle-wrapper.jar ### STS ### @@ -26,4 +27,4 @@ local.properties /nbdist/ /.nb-gradle/ -/src/main/docker/etc/rest_source_clients_configs.yml \ No newline at end of file +/src/main/docker/etc/rest_source_clients_configs.yml diff --git a/authorizer-app-backend/build.gradle.kts b/authorizer-app-backend/build.gradle.kts index de46ca5c..ac948331 100644 --- a/authorizer-app-backend/build.gradle.kts +++ b/authorizer-app-backend/build.gradle.kts @@ -4,9 +4,9 @@ plugins { java application kotlin("jvm") - id("org.jetbrains.kotlin.plugin.noarg") version "1.3.61" - id("org.jetbrains.kotlin.plugin.jpa") version "1.3.61" - id("org.jetbrains.kotlin.plugin.allopen") version "1.3.61" + id("org.jetbrains.kotlin.plugin.noarg") + id("org.jetbrains.kotlin.plugin.jpa") + id("org.jetbrains.kotlin.plugin.allopen") } @@ -15,14 +15,19 @@ application { } project.extra.apply { - set("okhttpVersion", "4.2.0") - set("radarJerseyVersion", "0.2.3") - set("jacksonVersion", "2.10.2") - set("slf4jVersion", "1.7.27") + set("okhttpVersion", "4.8.1") + set("radarJerseyVersion", "0.2.4") + set("jacksonVersion", "2.11.2") + set("slf4jVersion", "1.7.30") set("logbackVersion", "1.2.3") set("grizzlyVersion", "2.4.4") - set("jerseyVersion", "2.30") - set("hibernateVersion", "5.4.10.Final") + set("jerseyVersion", "2.31") + set("hibernateVersion", "5.4.20.Final") + set("postgresVersion", "42.2.16") + set("liquibaseVersion", "3.10.2") + set("h2Version", "1.4.200") + set("junitVersion", "5.6.2") + set("mockitoKotlinVersion", "2.2.0") set("githubRepoName", "RADAR-base/RADAR-Rest-Source-Auth") set("githubUrl", "https://github.com/RADAR-base/RADAR-Rest-Source-Auth.git") set("issueUrl", "https://github.com/RADAR-base/RADAR-Rest-Source-Auth/issues") @@ -42,6 +47,7 @@ repositories { dependencies { api(kotlin("stdlib-jdk8")) + implementation(kotlin("reflect")) implementation("org.radarbase:radar-jersey:${project.extra["radarJerseyVersion"]}") @@ -53,22 +59,21 @@ dependencies { implementation("org.hibernate:hibernate-core:${project.extra["hibernateVersion"]}") implementation("org.hibernate:hibernate-c3p0:${project.extra["hibernateVersion"]}") - implementation("org.liquibase:liquibase-core:3.5.3") + implementation("org.liquibase:liquibase-core:${project.extra["liquibaseVersion"]}") implementation("com.squareup.okhttp3:okhttp:${project.extra["okhttpVersion"]}") - runtimeOnly("com.h2database:h2:1.4.199") - runtimeOnly("org.postgresql:postgresql:42.2.5") + runtimeOnly("com.h2database:h2:${project.extra["h2Version"]}") + runtimeOnly("org.postgresql:postgresql:${project.extra["postgresVersion"]}") runtimeOnly("ch.qos.logback:logback-classic:${project.extra["logbackVersion"]}") - testImplementation("org.junit.jupiter:junit-jupiter:5.4.2") + testImplementation("org.junit.jupiter:junit-jupiter:${project.extra["junitVersion"]}") testImplementation("org.hamcrest:hamcrest-all:1.3") - testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0") + testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:${project.extra["mockitoKotlinVersion"]}") testImplementation("org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2:${project.extra["jerseyVersion"]}") } - tasks.withType { kotlinOptions.jvmTarget = "11" } diff --git a/build.gradle.kts b/build.gradle.kts index 49bdd6c2..3f6e0f0b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,3 @@ -plugins { - kotlin("jvm") version "1.4.0" apply false -} - subprojects { group = "org.radarbase" version = "2.0-SNAPSHOT" diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index c000da9f..00000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = 'radar-rest-sources-authorizer' -include(':authorizer-app-backend') \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..6540b1ce --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,12 @@ +rootProject.name = "radar-rest-sources-authorizer" +include(":authorizer-app-backend") + +pluginManagement { + val kotlinVersion: String by settings + plugins { + kotlin("jvm") version kotlinVersion + id("org.jetbrains.kotlin.plugin.noarg") version kotlinVersion + id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion + id("org.jetbrains.kotlin.plugin.allopen") version kotlinVersion + } +} From ffa590afdca3f51200a943e2ce65254d443ea2c8 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Tue, 1 Sep 2020 16:45:36 +0200 Subject: [PATCH 30/80] Ensure only allowed projects are read from DB --- .../doa/RestSourceUserRepository.kt | 4 +- .../doa/RestSourceUserRepositoryImpl.kt | 52 +++++++------------ .../resources/HeathCheckResource.kt | 2 +- .../resources/RestSourceUserResource.kt | 14 +++-- .../authorizer/service/RadarProjectService.kt | 4 +- .../managementportal/MPProjectService.kt | 4 +- 6 files changed, 38 insertions(+), 42 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index 78c7d2d8..41e0b194 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt @@ -23,10 +23,10 @@ import org.radarbase.authorizer.doa.entity.RestSourceUser import java.time.Instant interface RestSourceUserRepository { - fun createOrUpdate(user: RestOauth2AccessToken, sourceType: String): RestSourceUser + fun createOrUpdate(token: RestOauth2AccessToken, sourceType: String): RestSourceUser fun read(id: Long): RestSourceUser? fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser - fun query(page: Page, projectId: String? = null, sourceType: String? = null): Pair, Page> + fun query(page: Page, projects: List, sourceType: String? = null): Pair, Page> fun delete(user: RestSourceUser) fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?): RestSourceUser } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index 87afde9b..22e49395 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -71,26 +71,21 @@ class RestSourceUserRepositoryImpl( this.sourceId = user.sourceId this.startDate = user.startDate this.endDate = user.endDate - }.also { merge(it) } + merge(this) + } } - override fun query(page: Page, projectId: String?, sourceType: String?): Pair, Page> { - var queryString = "SELECT u FROM RestSourceUser u" - var countQueryString = "SELECT count(u) FROM RestSourceUser u" - - when { - projectId != null && sourceType != null -> { - queryString += " WHERE u.projectId = :projectId AND u.sourceType = :sourceType" - countQueryString += " WHERE u.projectId = :projectId AND u.sourceType = :sourceType" - } - projectId != null && sourceType == null -> { - queryString += " WHERE u.projectId = :projectId" - countQueryString += " WHERE u.projectId = :projectId" - } - projectId == null && sourceType != null -> { - queryString += " WHERE u.sourceType = :sourceType" - countQueryString += " WHERE u.sourceType = :sourceType" - } + override fun query( + page: Page, + projects: List, + sourceType: String? + ): Pair, Page> { + var queryString = "SELECT u FROM RestSourceUser u WHERE u.projectId IN (:projects)" + var countQueryString = "SELECT count(u) FROM RestSourceUser u WHERE u.projectId IN (:projects)" + + if (sourceType != null) { + queryString += " AND u.sourceType = :sourceType" + countQueryString += " AND u.sourceType = :sourceType" } val actualPage = page.createValid(maximum = 100) @@ -101,21 +96,12 @@ class RestSourceUserRepositoryImpl( val countQuery = createQuery(countQueryString) - when { - projectId != null && sourceType != null -> { - query.setParameter("projectId", projectId) - countQuery.setParameter("projectId", projectId) - query.setParameter("sourceType", sourceType) - countQuery.setParameter("sourceType", sourceType) - } - projectId != null && sourceType == null -> { - query.setParameter("projectId", projectId) - countQuery.setParameter("projectId", projectId) - } - projectId == null && sourceType != null -> { - query.setParameter("sourceType", sourceType) - countQuery.setParameter("sourceType", sourceType) - } + query.setParameter("projects", projects) + countQuery.setParameter("projects", projects) + + if (sourceType != null) { + query.setParameter("sourceType", sourceType) + countQuery.setParameter("sourceType", sourceType) } val users = query.resultList diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt index f81fd707..e3596c80 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt @@ -34,7 +34,7 @@ class HealthCheckResource( @GET fun check(): Response { val status = try { - userRepository.query(Page(0, 1)) + userRepository.query(Page(0, 1), emptyList()) HealthStatus("UP") } catch (ex: Throwable) { HealthStatus("DOWN") diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt index 738086e0..220f2478 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt @@ -60,14 +60,22 @@ class RestSourceUserResource( @QueryParam("size") pageSize: Int?, @DefaultValue("1") @QueryParam("page") pageNumber: Int): RestSourceUsers { - if (projectId != null) { + val projects = if (projectId != null) { auth.checkPermissionOnProject(Permission.SUBJECT_READ, projectId) + listOf(projectId) + } else { + projectService.userProjects(auth, Permission.SUBJECT_READ) + .map { it.id } } + if (projects.isEmpty()) return RestSourceUsers(emptyList()) + val queryPage = Page(pageNumber = pageNumber, pageSize = pageSize) - val (records, page) = userRepository.query(queryPage, projectId, sourceType) + val (records, page) = userRepository.query(queryPage, projects, sourceType) - return userMapper.fromRestSourceUsers(records, page) + return userMapper.fromRestSourceUsers(records.filter { + auth.token.hasPermissionOnSubject(Permission.SUBJECT_READ, it.projectId, it.userId) + }, page) } @POST diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt index 0c97ae52..92f6dc88 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt @@ -20,11 +20,13 @@ import org.radarbase.authorizer.api.Project import org.radarbase.authorizer.api.User import org.radarbase.jersey.auth.Auth import org.radarbase.jersey.auth.ProjectService +import org.radarcns.auth.authorization.Permission +import org.radarcns.auth.authorization.Permission.PROJECT_READ interface RadarProjectService : ProjectService { fun project(projectId: String): Project - fun userProjects(auth: Auth): List + fun userProjects(auth: Auth, permission: Permission = PROJECT_READ): List fun projectUsers(projectId: String): List fun userByExternalId(projectId: String, externalUserId: String): User? } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt index 1f6b4db8..eca312a6 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt @@ -44,9 +44,9 @@ class MPProjectService(@Context private val config: Config, @Context private val } } - override fun userProjects(auth: Auth): List { + override fun userProjects(auth: Auth, permission: Permission): List { return projects.get() - .filter { auth.token.hasPermissionOnProject(Permission.PROJECT_READ, it.id) } + .filter { auth.token.hasPermissionOnProject(permission, it.id) } } override fun project(projectId: String): Project = projects.find { it.id == projectId } From 2410740fe989c397de0c5207de0d6989dc2d6010 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Tue, 1 Sep 2020 16:45:58 +0200 Subject: [PATCH 31/80] Small readability change --- .../authorizer/doa/RestSourceUserRepositoryImpl.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index 22e49395..b9dd3918 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -50,15 +50,17 @@ class RestSourceUserRepositoryImpl( this.accessToken = token.accessToken this.refreshToken = token.refreshToken this.expiresIn = token.expiresIn - this.expiresAt = Instant.now().plusSeconds(token.expiresIn.toLong()).minus(expiryTimeMargin) - }.also { persist(it) } + this.expiresAt = startDate.plusSeconds(token.expiresIn.toLong()).minus(expiryTimeMargin) + persist(this) + } } else { existingUser.apply { this.accessToken = token.accessToken this.refreshToken = token.refreshToken this.expiresIn = token.expiresIn this.expiresAt = Instant.now().plusSeconds(token.expiresIn.toLong()).minus(expiryTimeMargin) - }.also { merge(it) } + merge(this) + } } } From 33ed3fa32c09d420723e3230cf7f77f8a3884713 Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 1 Sep 2020 17:35:34 +0200 Subject: [PATCH 32/80] commit gradle.properties file --- gradle.properties | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 gradle.properties diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..64d48770 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +# +# Copyright 2020 The Hyve +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +kotlinVersion=1.4.0 + From 3014ebd98de7604728e021d829674467fe10881b Mon Sep 17 00:00:00 2001 From: nivethika Date: Tue, 1 Sep 2020 19:31:26 +0200 Subject: [PATCH 33/80] fix dockerfile --- authorizer-app-backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizer-app-backend/Dockerfile b/authorizer-app-backend/Dockerfile index ccf488fb..cef83f25 100644 --- a/authorizer-app-backend/Dockerfile +++ b/authorizer-app-backend/Dockerfile @@ -5,7 +5,7 @@ FROM gradle:6.6.1-jdk11 as builder RUN mkdir /code WORKDIR /code -COPY ./build.gradle.kts ./settings.gradle.kts /code/ +COPY ./build.gradle.kts ./settings.gradle.kts ./gradle.properties /code/ COPY authorizer-app-backend/build.gradle.kts /code/authorizer-app-backend/ RUN gradle :authorizer-app-backend:downloadDependencies From 6473c46291d8a0c54187e71d00c385c050db7372 Mon Sep 17 00:00:00 2001 From: nivethika Date: Wed, 2 Sep 2020 11:31:41 +0200 Subject: [PATCH 34/80] correct database name and add documentation for newly added api --- authorizer-app-backend/API-Documentation.md | 49 +++++++++++++++++++ .../etc/rest-source-authorizer/authorizer.yml | 2 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 authorizer-app-backend/API-Documentation.md diff --git a/authorizer-app-backend/API-Documentation.md b/authorizer-app-backend/API-Documentation.md new file mode 100644 index 00000000..39b5beee --- /dev/null +++ b/authorizer-app-backend/API-Documentation.md @@ -0,0 +1,49 @@ +#API Documentation + +## API documentation + +1. Request for authorized projects + +```bash +GET /projects +``` +Response format +```json +{ + "projects": [ + { + "id": "test", + "location": "test", + "description": "test" + } + ] +} +``` +2. Request for project details by id +```bash +GET /projects/test +``` +Response format +```json +{ + "id": "test", + "location": "test", + "description": "test" +} +``` +3. Requests for subjects/participants of a project +```bash +GET /projects/test/users +``` +Response format +```json +{ + "users": [ + { + "id": "628277bb-239e-4137-9d15-9d8f6bb05618", + "projectId": "test", + "status": "ACTIVATED" + } + ] +} +``` diff --git a/docker/etc/rest-source-authorizer/authorizer.yml b/docker/etc/rest-source-authorizer/authorizer.yml index 791c48d3..d66669d0 100644 --- a/docker/etc/rest-source-authorizer/authorizer.yml +++ b/docker/etc/rest-source-authorizer/authorizer.yml @@ -14,7 +14,7 @@ auth: database: jdbcDriver: org.postgresql.Driver - jdbcUrl: jdbc:postgresql://radarbase-postgresql:5432/uploadconnector + jdbcUrl: jdbc:postgresql://radarbase-postgresql:5432/restsourceauthorizer jdbcUser: radarcns jdbcPassword: radarcns hibernateDialect: org.hibernate.dialect.PostgreSQLDialect From 775489deae2a1e4b7ed721959da67678ea954240 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Wed, 2 Sep 2020 16:08:32 +0200 Subject: [PATCH 35/80] Applied radar-jersey-hibernate --- authorizer-app-backend/build.gradle.kts | 14 +--- .../java/org/radarbase/authorizer/Config.kt | 10 +-- .../authorizer/doa/EntityManagerExtensions.kt | 69 ------------------ .../doa/RestSourceUserRepositoryImpl.kt | 17 ++--- .../inject/AuthorizerResourceEnhancer.kt | 16 ----- .../inject/DoaEntityManagerFactory.kt | 44 ------------ .../inject/DoaEntityManagerFactoryFactory.kt | 70 ------------------- .../inject/ManagementPortalEnhancerFactory.kt | 34 +++++---- .../main/resources/META-INF/persistence.xml | 45 ------------ .../src/main/resources/application.yml | 37 ---------- 10 files changed, 32 insertions(+), 324 deletions(-) delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt delete mode 100644 authorizer-app-backend/src/main/resources/META-INF/persistence.xml delete mode 100644 authorizer-app-backend/src/main/resources/application.yml diff --git a/authorizer-app-backend/build.gradle.kts b/authorizer-app-backend/build.gradle.kts index ac948331..83780c77 100644 --- a/authorizer-app-backend/build.gradle.kts +++ b/authorizer-app-backend/build.gradle.kts @@ -1,7 +1,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - java application kotlin("jvm") id("org.jetbrains.kotlin.plugin.noarg") @@ -16,16 +15,12 @@ application { project.extra.apply { set("okhttpVersion", "4.8.1") - set("radarJerseyVersion", "0.2.4") + set("radarJerseyVersion", "0.3.0-SNAPSHOT") set("jacksonVersion", "2.11.2") set("slf4jVersion", "1.7.30") set("logbackVersion", "1.2.3") - set("grizzlyVersion", "2.4.4") set("jerseyVersion", "2.31") - set("hibernateVersion", "5.4.20.Final") - set("postgresVersion", "42.2.16") set("liquibaseVersion", "3.10.2") - set("h2Version", "1.4.200") set("junitVersion", "5.6.2") set("mockitoKotlinVersion", "2.2.0") set("githubRepoName", "RADAR-base/RADAR-Rest-Source-Auth") @@ -50,6 +45,7 @@ dependencies { implementation(kotlin("reflect")) implementation("org.radarbase:radar-jersey:${project.extra["radarJerseyVersion"]}") + implementation("org.radarbase:radar-jersey-hibernate:${project.extra["radarJerseyVersion"]}") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${project.extra["jacksonVersion"]}") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${project.extra["jacksonVersion"]}") @@ -57,14 +53,8 @@ dependencies { implementation("org.slf4j:slf4j-api:${project.extra["slf4jVersion"]}") - implementation("org.hibernate:hibernate-core:${project.extra["hibernateVersion"]}") - implementation("org.hibernate:hibernate-c3p0:${project.extra["hibernateVersion"]}") - implementation("org.liquibase:liquibase-core:${project.extra["liquibaseVersion"]}") - implementation("com.squareup.okhttp3:okhttp:${project.extra["okhttpVersion"]}") - runtimeOnly("com.h2database:h2:${project.extra["h2Version"]}") - runtimeOnly("org.postgresql:postgresql:${project.extra["postgresVersion"]}") runtimeOnly("ch.qos.logback:logback-classic:${project.extra["logbackVersion"]}") testImplementation("org.junit.jupiter:junit-jupiter:${project.extra["junitVersion"]}") diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt index 63152acc..c2364c72 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt @@ -18,6 +18,7 @@ package org.radarbase.authorizer import org.radarbase.authorizer.inject.ManagementPortalEnhancerFactory import org.radarbase.jersey.config.EnhancerFactory +import org.radarbase.jersey.hibernate.config.DatabaseConfig import java.net.URI data class Config( @@ -46,15 +47,6 @@ data class AuthConfig( var jwtResourceName: String = "res_restAuthorizer" ) -data class DatabaseConfig( - val jdbcDriver: String? = "org.h2.Driver", - val jdbcUrl: String? = null, - val jdbcUser: String? = null, - val jdbcPassword: String? = null, - val hibernateDialect: String = "org.hibernate.dialect.PostgreSQLDialect", - val additionalPersistenceConfig: Map? = null -) - data class RestSourceClient( val sourceType: String, val authorizationEndpoint: String, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt deleted file mode 100644 index 24dee5c0..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/EntityManagerExtensions.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2020 The Hyve - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.radarbase.authorizer.doa - -import org.radarbase.authorizer.logger -import org.radarbase.jersey.exception.HttpInternalServerException -import java.io.Closeable -import javax.persistence.EntityManager -import javax.persistence.EntityTransaction - -/** - * Run a transaction and commit it. If an exception occurs, the transaction is rolled back. - */ -fun EntityManager.transact(transactionOperation: EntityManager.() -> T) = createTransaction { - it.use { transactionOperation() } -} - -/** - * Start a transaction without committing it. If an exception occurs, the transaction is rolled back. - */ -private fun EntityManager.createTransaction(transactionOperation: EntityManager.(CloseableTransaction) -> T): T { - val currentTransaction = transaction - ?: throw HttpInternalServerException("transaction_not_found", "Cannot find a transaction from EntityManager") - - currentTransaction.begin() - try { - return transactionOperation(object : CloseableTransaction { - override val transaction: EntityTransaction = currentTransaction - - override fun close() { - try { - transaction.commit() - } catch (ex: Exception) { - logger.error("Rolling back operation", ex) - if (currentTransaction.isActive) { - currentTransaction.rollback() - } - throw ex - } - } - }) - } catch (ex: Exception) { - logger.error("Rolling back operation", ex) - if (currentTransaction.isActive) { - currentTransaction.rollback() - } - throw ex - } -} - - -interface CloseableTransaction : Closeable { - val transaction: EntityTransaction - override fun close() -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt index b9dd3918..5a76549f 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt @@ -21,6 +21,7 @@ import org.radarbase.authorizer.api.RestOauth2AccessToken import org.radarbase.authorizer.api.RestSourceUserDTO import org.radarbase.authorizer.doa.entity.RestSourceUser import org.radarbase.jersey.exception.HttpBadGatewayException +import org.radarbase.jersey.hibernate.HibernateRepository import java.time.Duration import java.time.Instant import javax.inject.Provider @@ -28,10 +29,10 @@ import javax.persistence.EntityManager import javax.ws.rs.core.Context class RestSourceUserRepositoryImpl( - @Context private var em: Provider -) : RestSourceUserRepository { + @Context em: Provider +) : RestSourceUserRepository, HibernateRepository(em) { - override fun createOrUpdate(token: RestOauth2AccessToken, sourceType: String): RestSourceUser = em.get().transact { + override fun createOrUpdate(token: RestOauth2AccessToken, sourceType: String): RestSourceUser = transact { val externalUserId = token.externalUserId ?: throw HttpBadGatewayException("Could not get externalId from token") @@ -64,9 +65,9 @@ class RestSourceUserRepositoryImpl( } } - override fun read(id: Long): RestSourceUser? = em.get().transact { find(RestSourceUser::class.java, id) } + override fun read(id: Long): RestSourceUser? = transact { find(RestSourceUser::class.java, id) } - override fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser = em.get().transact { + override fun update(existingUser: RestSourceUser, user: RestSourceUserDTO): RestSourceUser = transact { existingUser.apply { this.projectId = user.projectId this.userId = user.userId @@ -91,7 +92,7 @@ class RestSourceUserRepositoryImpl( } val actualPage = page.createValid(maximum = 100) - return em.get().transact { + return transact { val query = createQuery(queryString, RestSourceUser::class.java) .setFirstResult(actualPage.offset) .setMaxResults(actualPage.pageSize!!) @@ -113,11 +114,11 @@ class RestSourceUserRepositoryImpl( } } - override fun delete(user: RestSourceUser) = em.get().transact { + override fun delete(user: RestSourceUser) = transact { remove(merge(user)) } - override fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?) = em.get().transact { + override fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?) = transact { user.apply { this.version = Instant.now().toString() this.timesReset += 1 diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt index 59e5d00e..c464ded7 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt @@ -23,10 +23,8 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.KotlinModule import okhttp3.OkHttpClient import org.glassfish.jersey.internal.inject.AbstractBinder -import org.glassfish.jersey.process.internal.RequestScoped import org.glassfish.jersey.server.ResourceConfig import org.radarbase.authorizer.Config -import org.radarbase.authorizer.DatabaseConfig import org.radarbase.authorizer.RestSourceClients import org.radarbase.authorizer.api.RestSourceClientMapper import org.radarbase.authorizer.api.RestSourceUserMapper @@ -37,8 +35,6 @@ import org.radarbase.jersey.config.ConfigLoader import org.radarbase.jersey.config.JerseyResourceEnhancer import java.util.concurrent.TimeUnit import javax.inject.Singleton -import javax.persistence.EntityManager -import javax.persistence.EntityManagerFactory import javax.ws.rs.ext.ContextResolver class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnhancer { @@ -75,9 +71,6 @@ class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnh bind(config) .to(Config::class.java) - bind(config.database) - .to(DatabaseConfig::class.java) - bind(restSourceClients) .to(RestSourceClients::class.java) @@ -87,15 +80,6 @@ class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnh bind(OBJECT_MAPPER) .to(ObjectMapper::class.java) - // Bind factories. - bindFactory(DoaEntityManagerFactoryFactory::class.java) - .to(EntityManagerFactory::class.java) - .`in`(Singleton::class.java) - - bindFactory(DoaEntityManagerFactory::class.java) - .to(EntityManager::class.java) - .`in`(RequestScoped::class.java) - bind(RestSourceUserMapper::class.java) .to(RestSourceUserMapper::class.java) .`in`(Singleton::class.java) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt deleted file mode 100644 index fb3ec58a..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactory.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 The Hyve - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.radarbase.authorizer.inject - -import org.glassfish.jersey.internal.inject.DisposableSupplier -import org.slf4j.LoggerFactory -import javax.persistence.EntityManager -import javax.persistence.EntityManagerFactory -import javax.ws.rs.core.Context - -class DoaEntityManagerFactory( - @Context private val emf: EntityManagerFactory -) : DisposableSupplier { - - override fun get(): EntityManager { - logger.debug("Creating EntityManager...") - return emf.createEntityManager() - } - - override fun dispose(instance: EntityManager?) { - instance?.let { - logger.debug("Disposing EntityManager") - it.close() - } - } - - companion object { - private val logger = LoggerFactory.getLogger(DoaEntityManagerFactory::class.java) - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt deleted file mode 100644 index 044612e6..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/DoaEntityManagerFactoryFactory.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2020 The Hyve - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.radarbase.authorizer.inject - -import liquibase.Liquibase -import liquibase.database.DatabaseFactory -import liquibase.database.jvm.JdbcConnection -import liquibase.exception.LiquibaseException -import liquibase.resource.ClassLoaderResourceAccessor -import org.glassfish.jersey.internal.inject.DisposableSupplier -import org.hibernate.internal.SessionImpl -import org.radarbase.authorizer.DatabaseConfig -import org.slf4j.LoggerFactory -import javax.persistence.EntityManagerFactory -import javax.persistence.Persistence -import javax.ws.rs.core.Context - -class DoaEntityManagerFactoryFactory(@Context config: DatabaseConfig) : DisposableSupplier { - @Suppress("UNCHECKED_CAST") - private val configMap = ( - mapOf( - "javax.persistence.jdbc.driver" to config.jdbcDriver, - "javax.persistence.jdbc.url" to config.jdbcUrl, - "javax.persistence.jdbc.user" to config.jdbcUser, - "javax.persistence.jdbc.password" to config.jdbcPassword, - "hibernate.dialect" to config.hibernateDialect) - + (config.additionalPersistenceConfig ?: emptyMap())) - .filterValues { it != null } as Map - - override fun get(): EntityManagerFactory { - logger.info("Initializing EntityManagerFactory with config: $configMap") - return Persistence.createEntityManagerFactory("org.radarbase.authorizer.doa", configMap) - .also { initializeDatabase(it) } - } - - private fun initializeDatabase(emf: EntityManagerFactory) { - logger.info("Initializing Liquibase") - val connection = emf.createEntityManager().unwrap(SessionImpl::class.java).connection() - try { - val database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(JdbcConnection(connection)) - val liquibase = Liquibase("db/changelog/changes/db.changelog-master.xml", ClassLoaderResourceAccessor(), database) - liquibase.update("test") - } catch (e: LiquibaseException) { - logger.error("Failed to initialize database", e) - } - } - - override fun dispose(instance: EntityManagerFactory?) { - logger.info("Disposing EntityManagerFactory") - instance?.close() - } - - companion object { - private val logger = LoggerFactory.getLogger(DoaEntityManagerFactoryFactory::class.java) - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt index 0f007958..e20bff85 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt @@ -18,6 +18,7 @@ package org.radarbase.authorizer.inject import org.glassfish.jersey.internal.inject.AbstractBinder import org.radarbase.authorizer.Config +import org.radarbase.authorizer.doa.entity.RestSourceUser import org.radarbase.authorizer.service.RadarProjectService import org.radarbase.authorizer.service.managementportal.MPClient import org.radarbase.authorizer.service.managementportal.MPProjectService @@ -26,6 +27,7 @@ import org.radarbase.jersey.auth.ProjectService import org.radarbase.jersey.config.ConfigLoader import org.radarbase.jersey.config.EnhancerFactory import org.radarbase.jersey.config.JerseyResourceEnhancer +import org.radarbase.jersey.hibernate.config.HibernateResourceEnhancer import javax.inject.Singleton /** This binder needs to register all non-Jersey classes, otherwise initialization fails. */ @@ -34,27 +36,31 @@ class ManagementPortalEnhancerFactory(private val config: Config) : EnhancerFact AuthorizerResourceEnhancer(config), MPClientResourceEnhancer(), ConfigLoader.Enhancers.radar(AuthConfig( - managementPortalUrl = config.auth.managementPortalUrl, - jwtResourceName = config.auth.jwtResourceName)), + managementPortalUrl = config.auth.managementPortalUrl, + jwtResourceName = config.auth.jwtResourceName, + )), + HibernateResourceEnhancer(config.database.copy( + managedClasses = listOf( + RestSourceUser::class.qualifiedName!!, + ) + )), ConfigLoader.Enhancers.managementPortal, ConfigLoader.Enhancers.generalException, ConfigLoader.Enhancers.httpException) class MPClientResourceEnhancer : JerseyResourceEnhancer { - override fun enhanceBinder(binder: AbstractBinder) { - binder.apply { - bind(MPClient::class.java) - .to(MPClient::class.java) - .`in`(Singleton::class.java) + override fun AbstractBinder.enhance() { + bind(MPClient::class.java) + .to(MPClient::class.java) + .`in`(Singleton::class.java) - bind(ProjectServiceWrapper::class.java) - .to(ProjectService::class.java) - .`in`(Singleton::class.java) + bind(ProjectServiceWrapper::class.java) + .to(ProjectService::class.java) + .`in`(Singleton::class.java) - bind(MPProjectService::class.java) - .to(RadarProjectService::class.java) - .`in`(Singleton::class.java) - } + bind(MPProjectService::class.java) + .to(RadarProjectService::class.java) + .`in`(Singleton::class.java) } } } diff --git a/authorizer-app-backend/src/main/resources/META-INF/persistence.xml b/authorizer-app-backend/src/main/resources/META-INF/persistence.xml deleted file mode 100644 index e9733401..00000000 --- a/authorizer-app-backend/src/main/resources/META-INF/persistence.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - org.radarbase.authorizer.doa.entity.RestSourceUser - - - - - - - - - - - - - - - - - diff --git a/authorizer-app-backend/src/main/resources/application.yml b/authorizer-app-backend/src/main/resources/application.yml deleted file mode 100644 index a0d4cbef..00000000 --- a/authorizer-app-backend/src/main/resources/application.yml +++ /dev/null @@ -1,37 +0,0 @@ -spring: - datasource: - url: jdbc:postgresql://localhost:5432/restsourceauthorizer - username: radarcns - password: radarcns - driver-class-name: org.postgresql.Driver - - jpa: - database-platform: org.hibernate.dialect.PostgreSQL94Dialect - properties.hibernate.temp.use_jdbc_metadata_defaults: false - - jackson: - serialization: - write_dates_as_timestamps: false - -rest-source-authorizer: - auth: - resource-name: res_restAuthorizer - public-key-endpoints: - - http://managementportal-app:8080/managementportal/oauth/token_key - cors: - allowed-origins: '*' - allowed-methods: GET, PUT, POST, DELETE, OPTIONS - allowed-headers: Authorization, Content-Type - allow-credentials: true - max-age: 1800 - - source-clients-file-path: docker/etc/rest-source-authorizer/rest_source_clients_configs.yml - - validator: managementportal - management-portal: - base-url: "http://localhost:8090/managementportal/" - projects-path: "api/projects" - subjects-path: "api/subjects" - token_path: "oauth/token" - oauth-client-id: "radar_rest_sources_auth_backend" - oauth-client-secret: "secret" From 30bad78884d8ca51213e2a000fd25c5fe929b2ae Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Wed, 2 Sep 2020 16:37:05 +0200 Subject: [PATCH 36/80] Use OkHttpExtensions --- .../inject/AuthorizerResourceEnhancer.kt | 10 +++-- .../service/RestSourceAuthorizationService.kt | 28 +++---------- .../service/managementportal/MPClient.kt | 42 +++++++------------ .../authorizer/util/OkHttpExtensions.kt | 28 +++++++++++++ 4 files changed, 56 insertions(+), 52 deletions(-) create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OkHttpExtensions.kt diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt index 59e5d00e..db050ac6 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt @@ -17,6 +17,7 @@ package org.radarbase.authorizer.inject import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule @@ -116,9 +117,10 @@ class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnh companion object { private val OBJECT_MAPPER: ObjectMapper = ObjectMapper() - .setSerializationInclusion(JsonInclude.Include.NON_NULL) - .registerModule(JavaTimeModule()) - .registerModule(KotlinModule()) - .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(JavaTimeModule()) + .registerModule(KotlinModule()) + .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt index 6e8e1bfe..35082284 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt @@ -23,7 +23,8 @@ import okhttp3.OkHttpClient import okhttp3.Request import org.radarbase.authorizer.RestSourceClients import org.radarbase.authorizer.api.RestOauth2AccessToken -import org.radarbase.jersey.exception.HttpBadGatewayException +import org.radarbase.authorizer.util.request +import org.radarbase.authorizer.util.requestValue import org.radarbase.jersey.exception.HttpBadRequestException import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -34,7 +35,7 @@ class RestSourceAuthorizationService( @Context private val httpClient: OkHttpClient, @Context private val objectMapper: ObjectMapper ) { - + private val tokenReader = objectMapper.readerFor(RestOauth2AccessToken::class.java) private val configMap = restSourceClients.clients.map { it.sourceType to it }.toMap() fun requestAccessToken(code: String, sourceType: String): RestOauth2AccessToken { @@ -47,28 +48,23 @@ class RestSourceAuthorizationService( .add("client_id", authorizationConfig.clientId) .build(); logger.info("Requesting access token with authorization code") - return objectMapper.readValue(execute(post(form, sourceType)), RestOauth2AccessToken::class.java) + return httpClient.requestValue(post(form, sourceType), tokenReader) } - fun refreshToken(refreshToken: String, sourceType: String): RestOauth2AccessToken { val form = FormBody.Builder() .add("grant_type", "refresh_token") .add("refresh_token", refreshToken) .build(); logger.info("Requesting to refreshToken") - return objectMapper.readValue(execute(post(form, sourceType)), RestOauth2AccessToken::class.java) + return httpClient.requestValue(post(form, sourceType), tokenReader) } - fun revokeToken(accessToken: String, sourceType: String): Boolean { val form = FormBody.Builder().add("token", accessToken).build(); logger.info("Requesting to revoke access token"); - httpClient.newCall(post(form, sourceType)).execute().use { response -> - return response.isSuccessful - } - + return httpClient.request(post(form, sourceType)) } private fun post(form: FormBody, sourceType: String): Request { @@ -84,18 +80,6 @@ class RestSourceAuthorizationService( }.build() } - private fun execute(request: Request): String { - return httpClient.newCall(request).execute().use { response -> - if (response.isSuccessful) { - response.body?.string() - ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") - } else { - logger.error("Cannot connect to managementportal ", response.code) - throw HttpBadGatewayException("Cannot connect to managementportal : Response-code ${response.code}") - } - } - } - companion object { val logger: Logger = LoggerFactory.getLogger(RestSourceAuthorizationService::class.java) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt index bf5fe1f5..f50365ab 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt @@ -18,32 +18,35 @@ package org.radarbase.authorizer.service.managementportal import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.ObjectMapper import okhttp3.* import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.radarbase.authorizer.Config import org.radarbase.authorizer.api.Project +import org.radarbase.authorizer.api.RestOauth2AccessToken import org.radarbase.authorizer.api.User +import org.radarbase.authorizer.util.requestValue import org.radarbase.jersey.auth.Auth -import org.radarbase.jersey.exception.HttpBadGatewayException import org.slf4j.LoggerFactory import java.net.MalformedURLException import java.time.Duration import java.time.Instant import javax.ws.rs.core.Context -class MPClient(@Context config: Config, @Context private val auth: Auth) { +class MPClient( + @Context config: Config, + @Context private val auth: Auth, + @Context private val objectMapper: ObjectMapper, +) { private val clientId: String = config.auth.clientId private val clientSecret: String = config.auth.clientSecret ?: throw IllegalArgumentException("Cannot configure managementportal client without client secret") private val httpClient = OkHttpClient() private val baseUrl: HttpUrl = config.auth.managementPortalUrl.toHttpUrlOrNull() ?: throw MalformedURLException("Cannot parse base URL ${config.auth.managementPortalUrl} as an URL") - private val mapper = jacksonObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - private val projectListReader = mapper.readerFor(object : TypeReference>() {}) - private val userListReader = mapper.readerFor(object : TypeReference>() {}) + private val projectListReader = objectMapper.readerFor(object : TypeReference>() {}) + private val userListReader = objectMapper.readerFor(object : TypeReference>() {}) + private val tokenReader = objectMapper.readerFor(RestOauth2AccessToken::class.java) private var token: String? = null private var expiration: Instant? = null @@ -71,10 +74,9 @@ class MPClient(@Context config: Config, @Context private val auth: Auth) { header("Authorization", Credentials.basic(clientId, clientSecret)) }.build() - val result = mapper.readTree(execute(request)) - localToken = result["access_token"].asText() - ?: throw HttpBadGatewayException("ManagementPortal did not provide an access token") - expiration = Instant.now() + Duration.ofSeconds(result["expires_in"].asLong()) - Duration.ofMinutes(5) + val result = httpClient.requestValue(request, tokenReader) + localToken = result.accessToken + expiration = Instant.now() + Duration.ofSeconds(result.expiresIn.toLong()) - Duration.ofMinutes(5) token = localToken localToken } @@ -87,7 +89,7 @@ class MPClient(@Context config: Config, @Context private val auth: Auth) { header("Authorization", "Bearer ${ensureToken()}") }.build() - return projectListReader.readValue>(execute(request)) + return httpClient.requestValue>(request, projectListReader) .map { Project( id = it.id, @@ -98,18 +100,6 @@ class MPClient(@Context config: Config, @Context private val auth: Auth) { } } - private fun execute(request: Request): String { - return httpClient.newCall(request).execute().use { response -> - if (response.isSuccessful) { - response.body?.string() - ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") - } else { - logger.error("Cannot connect to managementportal ", response.code) - throw HttpBadGatewayException("Cannot connect to managementportal : Response-code ${response.code}") - } - } - } - fun readParticipants(projectId: String): List { val request = Request.Builder().apply { url(baseUrl.newBuilder() @@ -120,7 +110,7 @@ class MPClient(@Context config: Config, @Context private val auth: Auth) { header("Authorization", "Bearer ${ensureToken()}") }.build() - return userListReader.readValue>(execute(request)) + return httpClient.requestValue>(request, userListReader) .map { User( id = it.login, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OkHttpExtensions.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OkHttpExtensions.kt new file mode 100644 index 00000000..6293c1ff --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OkHttpExtensions.kt @@ -0,0 +1,28 @@ +package org.radarbase.authorizer.util + +import com.fasterxml.jackson.databind.ObjectReader +import okhttp3.OkHttpClient +import okhttp3.Request +import org.radarbase.jersey.exception.HttpBadGatewayException +import org.slf4j.LoggerFactory + +fun OkHttpClient.requestValue(request: Request, reader: ObjectReader): T { + return newCall(request).execute().use { response -> + if (response.isSuccessful) { + response.body?.byteStream() + ?.let { reader.readValue(it) } + ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") + } else { + logger.error("Cannot connect to {}: HTTP status {} - {}", request.url, response.code, response.body?.string()) + throw HttpBadGatewayException("Cannot connect to ${request.url}: HTTP status ${response.code}") + } + } +} + +fun OkHttpClient.request(request: Request): Boolean { + return newCall(request).execute().use { response -> + response.isSuccessful + } +} + +private val logger = LoggerFactory.getLogger(OkHttpClient::class.java) From e31eba13967c016c7f321f1dc29da5a8d3b51df8 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Wed, 2 Sep 2020 16:42:28 +0200 Subject: [PATCH 37/80] Small readability/reusability updates --- .../service/RestSourceAuthorizationService.kt | 1 - .../service/managementportal/MPClient.kt | 27 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt index 35082284..688b06c5 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt @@ -63,7 +63,6 @@ class RestSourceAuthorizationService( fun revokeToken(accessToken: String, sourceType: String): Boolean { val form = FormBody.Builder().add("token", accessToken).build(); logger.info("Requesting to revoke access token"); - return httpClient.request(post(form, sourceType)) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt index f50365ab..58e9eeba 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt @@ -37,11 +37,11 @@ class MPClient( @Context config: Config, @Context private val auth: Auth, @Context private val objectMapper: ObjectMapper, + @Context private val httpClient: OkHttpClient ) { private val clientId: String = config.auth.clientId private val clientSecret: String = config.auth.clientSecret ?: throw IllegalArgumentException("Cannot configure managementportal client without client secret") - private val httpClient = OkHttpClient() private val baseUrl: HttpUrl = config.auth.managementPortalUrl.toHttpUrlOrNull() ?: throw MalformedURLException("Cannot parse base URL ${config.auth.managementPortalUrl} as an URL") private val projectListReader = objectMapper.readerFor(object : TypeReference>() {}) @@ -59,7 +59,7 @@ class MPClient( } private fun ensureToken(): String { - var localToken = validToken + val localToken = validToken return if (localToken != null) { localToken @@ -74,11 +74,11 @@ class MPClient( header("Authorization", Credentials.basic(clientId, clientSecret)) }.build() - val result = httpClient.requestValue(request, tokenReader) - localToken = result.accessToken - expiration = Instant.now() + Duration.ofSeconds(result.expiresIn.toLong()) - Duration.ofMinutes(5) - token = localToken - localToken + httpClient.requestValue(request, tokenReader).let { + expiration = Instant.now() + Duration.ofSeconds(it.expiresIn.toLong()) - Duration.ofMinutes(5) + token = it.accessToken + it.accessToken + } } } @@ -120,9 +120,18 @@ class MPClient( } } - data class SubjectDto(val login: String, val externalId: String? = null, val status: String = "DEACTIVATED", val attributes: Map = mapOf()) + data class SubjectDto( + val login: String, + val externalId: String? = null, + val status: String = "DEACTIVATED", + val attributes: Map = mapOf()) - data class ProjectDto(@JsonProperty("projectName") val id: String, @JsonProperty("humanReadableProjectName") val name: String? = null, val location: String? = null, val organization: String? = null, val description: String? = null) + data class ProjectDto( + @JsonProperty("projectName") val id: String, + @JsonProperty("humanReadableProjectName") val name: String? = null, + val location: String? = null, + val organization: String? = null, + val description: String? = null) companion object { private val logger = LoggerFactory.getLogger(MPClient::class.java) From af79255c0279742d01f478fb0e881c14f68d8dbc Mon Sep 17 00:00:00 2001 From: nivethika Date: Thu, 3 Sep 2020 13:38:13 +0200 Subject: [PATCH 38/80] add state-store check to validate against spoofing --- .../java/org/radarbase/authorizer/Config.kt | 3 +- .../authorizer/api/ApiDeclarations.kt | 3 +- .../inject/AuthorizerResourceEnhancer.kt | 7 +++ .../resources/RestSourceUserResource.kt | 11 +++- .../resources/SourceClientResource.kt | 3 + .../radarbase/authorizer/util/StateStore.kt | 56 +++++++++++++++++++ ...urce-user-registration-form.component.html | 2 +- .../app/models/source-client-details.model.ts | 1 + 8 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/StateStore.kt diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt index 63152acc..7dc09613 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/Config.kt @@ -33,7 +33,8 @@ data class AuthorizerServiceConfig( var resourceConfig: Class = ManagementPortalEnhancerFactory::class.java, var enableCors: Boolean? = false, var syncProjectsIntervalMin: Long = 30, - var syncParticipantsIntervalMin: Long = 30 + var syncParticipantsIntervalMin: Long = 30, + val stateStoreExpiryInMin: Long = 5 ) data class AuthConfig( diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt index c3cc4ae6..ec3404d4 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt @@ -37,7 +37,8 @@ data class ShareableClientDetail( val tokenEndpoint: String, val grantType: String?, val clientId: String, - val scope: String? + val scope: String?, + val state: String? = null, ) data class ShareableClientDetails( diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt index 59e5d00e..6717de55 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt @@ -33,8 +33,10 @@ import org.radarbase.authorizer.api.RestSourceUserMapper import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.doa.RestSourceUserRepositoryImpl import org.radarbase.authorizer.service.RestSourceAuthorizationService +import org.radarbase.authorizer.util.StateStore import org.radarbase.jersey.config.ConfigLoader import org.radarbase.jersey.config.JerseyResourceEnhancer +import java.time.Duration import java.util.concurrent.TimeUnit import javax.inject.Singleton import javax.persistence.EntityManager @@ -50,6 +52,8 @@ class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnh private val restSourceClients = RestSourceClients(config.restSourceClients) + private val stateStore = StateStore(Duration.ofMinutes(config.service.stateStoreExpiryInMin)) + override val classes: Array> get() { return if (config.service.enableCors == true) { @@ -87,6 +91,9 @@ class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnh bind(OBJECT_MAPPER) .to(ObjectMapper::class.java) + bind(stateStore) + .to(StateStore::class.java) + // Bind factories. bindFactory(DoaEntityManagerFactoryFactory::class.java) .to(EntityManagerFactory::class.java) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt index 220f2478..ce89e661 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt @@ -21,6 +21,8 @@ import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.doa.entity.RestSourceUser import org.radarbase.authorizer.service.RadarProjectService import org.radarbase.authorizer.service.RestSourceAuthorizationService +import org.radarbase.authorizer.util.StateStore +import org.radarbase.authorizer.util.StateStore.State.Companion.toState import org.radarbase.jersey.auth.Auth import org.radarbase.jersey.auth.Authenticated import org.radarbase.jersey.auth.NeedsPermission @@ -49,6 +51,7 @@ class RestSourceUserResource( @Context private val userMapper: RestSourceUserMapper, @Context private val authorizationService: RestSourceAuthorizationService, @Context private val projectService: RadarProjectService, + @Context private val stateStore: StateStore, @Context private val auth: Auth ) { @@ -83,9 +86,11 @@ class RestSourceUserResource( fun create( @FormParam("code") code: String, @FormParam("state") state: String): Response { - logger.info("code $code state $state") - val accessToken = authorizationService.requestAccessToken(code, sourceType = state) - val user = userRepository.createOrUpdate(accessToken, state) + logger.info("Authorizing with code $code state $state") + val state = toState(string = state) + if (!stateStore.isValid(state)) throw HttpBadRequestException("state_not_found", "State has expired or not found") + val accessToken = authorizationService.requestAccessToken(code, sourceType = state.sourceType) + val user = userRepository.createOrUpdate(accessToken, state.sourceType) return Response.created(URI("users/${user.id}")) .entity(userMapper.fromEntity(user)) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt index dea175ba..5655ba8e 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/SourceClientResource.kt @@ -20,6 +20,7 @@ import org.radarbase.authorizer.RestSourceClients import org.radarbase.authorizer.api.RestSourceClientMapper import org.radarbase.authorizer.api.ShareableClientDetail import org.radarbase.authorizer.api.ShareableClientDetails +import org.radarbase.authorizer.util.StateStore import org.radarbase.jersey.auth.Auth import org.radarbase.jersey.auth.Authenticated import org.radarbase.jersey.auth.NeedsPermission @@ -42,6 +43,7 @@ import javax.ws.rs.core.MediaType class SourceClientResource( @Context private val restSourceClients: RestSourceClients, @Context private val clientMapper: RestSourceClientMapper, + @Context private val stateStore: StateStore, @Context private val auth: Auth ) { @@ -63,6 +65,7 @@ class SourceClientResource( @NeedsPermission(Permission.Entity.SOURCETYPE, Permission.Operation.READ) fun client(@PathParam("type") type: String): ShareableClientDetail { return sharableClientDetails.sourceClients.find { it.sourceType == type } + ?.copy( state = stateStore.generateState(type).toString()) ?: throw HttpNotFoundException("source-type-not-found", "Client with source-type $type is not configured") } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/StateStore.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/StateStore.kt new file mode 100644 index 00000000..16fbc97d --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/StateStore.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2020 The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.radarbase.authorizer.util + +import java.time.Duration +import java.time.Instant +import java.util.* +import java.util.concurrent.ThreadLocalRandom + +class StateStore(expiryTime: Duration = Duration.ofMinutes(5)) { + + private val expiryTime = expiryTime + private val stateExpiry = mutableMapOf() + + fun generateState(sourceType: String): State { + val randomBytes = ByteArray(6) + ThreadLocalRandom.current().nextBytes(randomBytes) + val uniqueId: String = STATE_ENCODER.encodeToString(randomBytes) + val state = State(uniqueId, sourceType) + stateExpiry[state] = Instant.now().plus(expiryTime) + return state + } + + fun isValid(state: State): Boolean { + val expired: Instant? = stateExpiry.remove(state) + return expired != null && expired.isAfter(Instant.now()) + } + + companion object { + private val STATE_ENCODER: Base64.Encoder = Base64.getUrlEncoder().withoutPadding() + } + + data class State(val uuid: String, val sourceType: String) { + override fun toString() = "state=$uuid&sourceType=$sourceType" + companion object { + fun toState(string: String) : State { + val map = string.split("&") + return State(map[0].split("=")[1], map[1].split("=")[1]) + } + } + } +} diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-registration-form.component.html b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-registration-form.component.html index aed39d47..8760b730 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-registration-form.component.html +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-registration-form.component.html @@ -12,7 +12,7 @@ [action]="sourceClientDetail.authorizationEndpoint"> - + diff --git a/authorizer-app/src/app/models/source-client-details.model.ts b/authorizer-app/src/app/models/source-client-details.model.ts index 604ffcab..0c0c0eab 100644 --- a/authorizer-app/src/app/models/source-client-details.model.ts +++ b/authorizer-app/src/app/models/source-client-details.model.ts @@ -5,4 +5,5 @@ export class RestSourceClientDetails { scope?: string; redirectUrl?: string; grantType?: string; + state: string } From 2d41caace87d7f3f03a3e267dff77fdb9d370770 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Mon, 7 Sep 2020 16:25:02 +0200 Subject: [PATCH 39/80] Use radar-jersey mpclient --- authorizer-app-backend/build.gradle.kts | 6 +- .../org/radarbase/authorizer/api/Project.kt | 18 +++ .../inject/AuthorizerResourceEnhancer.kt | 24 --- .../inject/ManagementPortalEnhancerFactory.kt | 54 +++---- .../inject/ProjectServiceWrapper.kt | 28 ---- .../resources/HeathCheckResource.kt | 45 ------ .../authorizer/resources/ProjectResource.kt | 14 +- .../resources/RestSourceUserResource.kt | 2 +- .../authorizer/service/RadarProjectService.kt | 32 ---- .../service/RestSourceAuthorizationService.kt | 26 +--- .../service/managementportal/MPClient.kt | 140 ------------------ .../managementportal/MPProjectService.kt | 68 --------- .../radarbase/authorizer/util/CachedSet.kt | 68 --------- 13 files changed, 59 insertions(+), 466 deletions(-) delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt diff --git a/authorizer-app-backend/build.gradle.kts b/authorizer-app-backend/build.gradle.kts index 83780c77..091ead0a 100644 --- a/authorizer-app-backend/build.gradle.kts +++ b/authorizer-app-backend/build.gradle.kts @@ -65,7 +65,11 @@ dependencies { } tasks.withType { - kotlinOptions.jvmTarget = "11" + kotlinOptions { + jvmTarget = "11" + apiVersion = "1.4" + languageVersion = "1.4" + } } tasks.withType { diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt index 83c97ca1..d27f7bc1 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt @@ -16,10 +16,28 @@ package org.radarbase.authorizer.api +import org.radarbase.jersey.service.managementportal.MPProject +import org.radarbase.jersey.service.managementportal.MPUser + data class ProjectList(val projects: List) data class Project(val id: String, val name: String? = null, val location: String? = null, val organization: String? = null, val description: String? = null) +fun MPProject.toProject() = Project( + id = id, + name = name, + location = location, + organization = organization, + description = description, +) + data class UserList(val users: List) data class User(val id: String, val projectId: String, val externalId: String? = null, val status: String) + +fun MPUser.toUser() = User( + id = id, + projectId = checkNotNull(projectId) { "User must have a project ID" }, + externalId = externalId, + status = status, +) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt index c464ded7..33b872c0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/AuthorizerResourceEnhancer.kt @@ -38,12 +38,6 @@ import javax.inject.Singleton import javax.ws.rs.ext.ContextResolver class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnhancer { - private val client = OkHttpClient().newBuilder() - .connectTimeout(10, TimeUnit.SECONDS) - .writeTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .build() - private val restSourceClients = RestSourceClients(config.restSourceClients) override val classes: Array> @@ -62,10 +56,6 @@ class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnh "org.radarbase.authorizer.exception", "org.radarbase.authorizer.resources") - override fun ResourceConfig.enhance() { - register(ContextResolver { OBJECT_MAPPER }) - } - override fun AbstractBinder.enhance() { // Bind instances. These cannot use any injects themselves bind(config) @@ -74,12 +64,6 @@ class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnh bind(restSourceClients) .to(RestSourceClients::class.java) - bind(client) - .to(OkHttpClient::class.java) - - bind(OBJECT_MAPPER) - .to(ObjectMapper::class.java) - bind(RestSourceUserMapper::class.java) .to(RestSourceUserMapper::class.java) .`in`(Singleton::class.java) @@ -97,12 +81,4 @@ class AuthorizerResourceEnhancer(private val config: Config) : JerseyResourceEnh .`in`(Singleton::class.java) } - - companion object { - private val OBJECT_MAPPER: ObjectMapper = ObjectMapper() - .setSerializationInclusion(JsonInclude.Include.NON_NULL) - .registerModule(JavaTimeModule()) - .registerModule(KotlinModule()) - .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) - } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt index e20bff85..382eff51 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ManagementPortalEnhancerFactory.kt @@ -16,51 +16,41 @@ package org.radarbase.authorizer.inject -import org.glassfish.jersey.internal.inject.AbstractBinder import org.radarbase.authorizer.Config import org.radarbase.authorizer.doa.entity.RestSourceUser -import org.radarbase.authorizer.service.RadarProjectService -import org.radarbase.authorizer.service.managementportal.MPClient -import org.radarbase.authorizer.service.managementportal.MPProjectService import org.radarbase.jersey.auth.AuthConfig -import org.radarbase.jersey.auth.ProjectService +import org.radarbase.jersey.auth.MPConfig import org.radarbase.jersey.config.ConfigLoader import org.radarbase.jersey.config.EnhancerFactory import org.radarbase.jersey.config.JerseyResourceEnhancer import org.radarbase.jersey.hibernate.config.HibernateResourceEnhancer -import javax.inject.Singleton /** This binder needs to register all non-Jersey classes, otherwise initialization fails. */ class ManagementPortalEnhancerFactory(private val config: Config) : EnhancerFactory { - override fun createEnhancers(): List = listOf( - AuthorizerResourceEnhancer(config), - MPClientResourceEnhancer(), - ConfigLoader.Enhancers.radar(AuthConfig( - managementPortalUrl = config.auth.managementPortalUrl, + override fun createEnhancers(): List { + val authConfig = AuthConfig( + managementPortal = MPConfig( + url = config.auth.managementPortalUrl, + clientId = config.auth.clientId, + clientSecret = config.auth.clientSecret, + syncProjectsIntervalMin = config.service.syncProjectsIntervalMin, + syncParticipantsIntervalMin = config.service.syncParticipantsIntervalMin, + ), jwtResourceName = config.auth.jwtResourceName, - )), - HibernateResourceEnhancer(config.database.copy( + ) + val dbConfig = config.database.copy( managedClasses = listOf( RestSourceUser::class.qualifiedName!!, ) - )), - ConfigLoader.Enhancers.managementPortal, - ConfigLoader.Enhancers.generalException, - ConfigLoader.Enhancers.httpException) - - class MPClientResourceEnhancer : JerseyResourceEnhancer { - override fun AbstractBinder.enhance() { - bind(MPClient::class.java) - .to(MPClient::class.java) - .`in`(Singleton::class.java) - - bind(ProjectServiceWrapper::class.java) - .to(ProjectService::class.java) - .`in`(Singleton::class.java) - - bind(MPProjectService::class.java) - .to(RadarProjectService::class.java) - .`in`(Singleton::class.java) - } + ) + return listOf( + AuthorizerResourceEnhancer(config), + ConfigLoader.Enhancers.radar(authConfig), + ConfigLoader.Enhancers.health, + HibernateResourceEnhancer(dbConfig), + ConfigLoader.Enhancers.managementPortal(authConfig), + ConfigLoader.Enhancers.generalException, + ConfigLoader.Enhancers.httpException, + ) } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt deleted file mode 100644 index 4795111f..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/inject/ProjectServiceWrapper.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2020 The Hyve - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.radarbase.authorizer.inject - -import org.radarbase.authorizer.service.RadarProjectService -import org.radarbase.jersey.auth.ProjectService -import javax.inject.Provider -import javax.ws.rs.core.Context - -class ProjectServiceWrapper( - @Context private val mpProjectService: Provider -) : ProjectService { - override fun ensureProject(projectId: String) = mpProjectService.get().ensureProject(projectId) -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt deleted file mode 100644 index e3596c80..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/HeathCheckResource.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 The Hyve - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.radarbase.authorizer.resources - -import org.radarbase.authorizer.api.Page -import org.radarbase.authorizer.doa.RestSourceUserRepository -import javax.annotation.Resource -import javax.inject.Singleton -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.core.Context -import javax.ws.rs.core.Response - -@Path("health") -@Resource -@Singleton -class HealthCheckResource( - @Context private var userRepository: RestSourceUserRepository -) { - @GET - fun check(): Response { - val status = try { - userRepository.query(Page(0, 1), emptyList()) - HealthStatus("UP") - } catch (ex: Throwable) { - HealthStatus("DOWN") - } - return Response.ok(status).build() - } - data class HealthStatus(val status: String) -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt index 79f3db6b..c085246a 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt @@ -16,13 +16,11 @@ package org.radarbase.authorizer.resources -import org.radarbase.authorizer.api.Project -import org.radarbase.authorizer.api.ProjectList -import org.radarbase.authorizer.api.UserList -import org.radarbase.authorizer.service.RadarProjectService +import org.radarbase.authorizer.api.* import org.radarbase.jersey.auth.Auth import org.radarbase.jersey.auth.Authenticated import org.radarbase.jersey.auth.NeedsPermission +import org.radarbase.jersey.service.managementportal.RadarProjectService import org.radarcns.auth.authorization.Permission import javax.annotation.Resource import javax.inject.Singleton @@ -43,19 +41,21 @@ class ProjectResource( @GET @NeedsPermission(Permission.Entity.PROJECT, Permission.Operation.READ) - fun projects() = ProjectList(projectService.userProjects(auth)) + fun projects() = ProjectList(projectService.userProjects(auth) + .map { it.toProject() }) @GET @Path("{projectId}/users") @NeedsPermission(Permission.Entity.SUBJECT, Permission.Operation.READ, "projectId") fun users(@PathParam("projectId") projectId: String): UserList { - return UserList(projectService.projectUsers(projectId)) + return UserList(projectService.projectUsers(projectId) + .map { it.toUser() }) } @GET @Path("{projectId}") @NeedsPermission(Permission.Entity.PROJECT, Permission.Operation.READ, "projectId") fun project(@PathParam("projectId") projectId: String): Project { - return projectService.project(projectId) + return projectService.project(projectId).toProject() } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt index 220f2478..6bba7aac 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RestSourceUserResource.kt @@ -19,7 +19,6 @@ package org.radarbase.authorizer.resources import org.radarbase.authorizer.api.* import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.doa.entity.RestSourceUser -import org.radarbase.authorizer.service.RadarProjectService import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.jersey.auth.Auth import org.radarbase.jersey.auth.Authenticated @@ -27,6 +26,7 @@ import org.radarbase.jersey.auth.NeedsPermission import org.radarbase.jersey.exception.HttpBadRequestException import org.radarbase.jersey.exception.HttpConflictException import org.radarbase.jersey.exception.HttpNotFoundException +import org.radarbase.jersey.service.managementportal.RadarProjectService import org.radarcns.auth.authorization.Permission import org.slf4j.Logger import org.slf4j.LoggerFactory diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt deleted file mode 100644 index 92f6dc88..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020 The Hyve - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.radarbase.authorizer.service - -import org.radarbase.authorizer.api.Project -import org.radarbase.authorizer.api.User -import org.radarbase.jersey.auth.Auth -import org.radarbase.jersey.auth.ProjectService -import org.radarcns.auth.authorization.Permission -import org.radarcns.auth.authorization.Permission.PROJECT_READ - - -interface RadarProjectService : ProjectService { - fun project(projectId: String): Project - fun userProjects(auth: Auth, permission: Permission = PROJECT_READ): List - fun projectUsers(projectId: String): List - fun userByExternalId(projectId: String, externalUserId: String): User? -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt index 6e8e1bfe..8c6137d0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceAuthorizationService.kt @@ -23,8 +23,9 @@ import okhttp3.OkHttpClient import okhttp3.Request import org.radarbase.authorizer.RestSourceClients import org.radarbase.authorizer.api.RestOauth2AccessToken -import org.radarbase.jersey.exception.HttpBadGatewayException import org.radarbase.jersey.exception.HttpBadRequestException +import org.radarbase.jersey.util.request +import org.radarbase.jersey.util.requestJson import org.slf4j.Logger import org.slf4j.LoggerFactory import javax.ws.rs.core.Context @@ -34,8 +35,8 @@ class RestSourceAuthorizationService( @Context private val httpClient: OkHttpClient, @Context private val objectMapper: ObjectMapper ) { - private val configMap = restSourceClients.clients.map { it.sourceType to it }.toMap() + private val tokenReader = objectMapper.readerFor(RestOauth2AccessToken::class.java) fun requestAccessToken(code: String, sourceType: String): RestOauth2AccessToken { val authorizationConfig = configMap[sourceType] @@ -47,7 +48,7 @@ class RestSourceAuthorizationService( .add("client_id", authorizationConfig.clientId) .build(); logger.info("Requesting access token with authorization code") - return objectMapper.readValue(execute(post(form, sourceType)), RestOauth2AccessToken::class.java) + return httpClient.requestJson(post(form, sourceType), tokenReader) } @@ -57,7 +58,7 @@ class RestSourceAuthorizationService( .add("refresh_token", refreshToken) .build(); logger.info("Requesting to refreshToken") - return objectMapper.readValue(execute(post(form, sourceType)), RestOauth2AccessToken::class.java) + return httpClient.requestJson(post(form, sourceType), tokenReader) } @@ -65,10 +66,7 @@ class RestSourceAuthorizationService( val form = FormBody.Builder().add("token", accessToken).build(); logger.info("Requesting to revoke access token"); - httpClient.newCall(post(form, sourceType)).execute().use { response -> - return response.isSuccessful - } - + return httpClient.request(post(form, sourceType)) } private fun post(form: FormBody, sourceType: String): Request { @@ -84,18 +82,6 @@ class RestSourceAuthorizationService( }.build() } - private fun execute(request: Request): String { - return httpClient.newCall(request).execute().use { response -> - if (response.isSuccessful) { - response.body?.string() - ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") - } else { - logger.error("Cannot connect to managementportal ", response.code) - throw HttpBadGatewayException("Cannot connect to managementportal : Response-code ${response.code}") - } - } - } - companion object { val logger: Logger = LoggerFactory.getLogger(RestSourceAuthorizationService::class.java) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt deleted file mode 100644 index bf5fe1f5..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPClient.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2020 The Hyve - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.radarbase.authorizer.service.managementportal - -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import okhttp3.* -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import org.radarbase.authorizer.Config -import org.radarbase.authorizer.api.Project -import org.radarbase.authorizer.api.User -import org.radarbase.jersey.auth.Auth -import org.radarbase.jersey.exception.HttpBadGatewayException -import org.slf4j.LoggerFactory -import java.net.MalformedURLException -import java.time.Duration -import java.time.Instant -import javax.ws.rs.core.Context - -class MPClient(@Context config: Config, @Context private val auth: Auth) { - private val clientId: String = config.auth.clientId - private val clientSecret: String = config.auth.clientSecret - ?: throw IllegalArgumentException("Cannot configure managementportal client without client secret") - private val httpClient = OkHttpClient() - private val baseUrl: HttpUrl = config.auth.managementPortalUrl.toHttpUrlOrNull() - ?: throw MalformedURLException("Cannot parse base URL ${config.auth.managementPortalUrl} as an URL") - private val mapper = jacksonObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - private val projectListReader = mapper.readerFor(object : TypeReference>() {}) - private val userListReader = mapper.readerFor(object : TypeReference>() {}) - - private var token: String? = null - private var expiration: Instant? = null - - private val validToken: String? - get() { - val localToken = token ?: return null - expiration?.takeIf { it > Instant.now() } ?: return null - return localToken - } - - private fun ensureToken(): String { - var localToken = validToken - - return if (localToken != null) { - localToken - } else { - val request = Request.Builder().apply { - url(baseUrl.resolve("oauth/token")!!) - post(FormBody.Builder().apply { - add("grant_type", "client_credentials") - add("client_id", clientId) - add("client_secret", clientSecret) - }.build()) - header("Authorization", Credentials.basic(clientId, clientSecret)) - }.build() - - val result = mapper.readTree(execute(request)) - localToken = result["access_token"].asText() - ?: throw HttpBadGatewayException("ManagementPortal did not provide an access token") - expiration = Instant.now() + Duration.ofSeconds(result["expires_in"].asLong()) - Duration.ofMinutes(5) - token = localToken - localToken - } - } - - fun readProjects(): List { - logger.debug("Requesting for projects") - val request = Request.Builder().apply { - url(baseUrl.resolve("api/projects")!!) - header("Authorization", "Bearer ${ensureToken()}") - }.build() - - return projectListReader.readValue>(execute(request)) - .map { - Project( - id = it.id, - name = it.name, - location = it.location, - organization = it.organization, - description = it.description) - } - } - - private fun execute(request: Request): String { - return httpClient.newCall(request).execute().use { response -> - if (response.isSuccessful) { - response.body?.string() - ?: throw HttpBadGatewayException("ManagementPortal did not provide a result") - } else { - logger.error("Cannot connect to managementportal ", response.code) - throw HttpBadGatewayException("Cannot connect to managementportal : Response-code ${response.code}") - } - } - } - - fun readParticipants(projectId: String): List { - val request = Request.Builder().apply { - url(baseUrl.newBuilder() - .addPathSegments("api/projects/$projectId/subjects") - .addQueryParameter("page", "0") - .addQueryParameter("size", Int.MAX_VALUE.toString()) - .build()) - header("Authorization", "Bearer ${ensureToken()}") - }.build() - - return userListReader.readValue>(execute(request)) - .map { - User( - id = it.login, - projectId = projectId, - externalId = it.externalId, - status = it.status) - } - } - - data class SubjectDto(val login: String, val externalId: String? = null, val status: String = "DEACTIVATED", val attributes: Map = mapOf()) - - data class ProjectDto(@JsonProperty("projectName") val id: String, @JsonProperty("humanReadableProjectName") val name: String? = null, val location: String? = null, val organization: String? = null, val description: String? = null) - - companion object { - private val logger = LoggerFactory.getLogger(MPClient::class.java) - } -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt deleted file mode 100644 index eca312a6..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/managementportal/MPProjectService.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2020 The Hyve - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.radarbase.authorizer.service.managementportal - -import org.radarbase.authorizer.Config -import org.radarbase.authorizer.api.Project -import org.radarbase.authorizer.api.User -import org.radarbase.authorizer.service.RadarProjectService -import org.radarbase.authorizer.util.CachedSet -import org.radarbase.jersey.auth.Auth -import org.radarbase.jersey.exception.HttpNotFoundException -import org.radarcns.auth.authorization.Permission -import java.time.Duration -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentMap -import javax.ws.rs.core.Context - -class MPProjectService(@Context private val config: Config, @Context private val mpClient: MPClient) : RadarProjectService { - private val projects = CachedSet( - Duration.ofMinutes(config.service.syncProjectsIntervalMin), - Duration.ofMinutes(1)) { - mpClient.readProjects() - } - - private val participants: ConcurrentMap> = ConcurrentHashMap() - - override fun ensureProject(projectId: String) { - if (projects.find { it.id == projectId } == null) { - throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") - } - } - - override fun userProjects(auth: Auth, permission: Permission): List { - return projects.get() - .filter { auth.token.hasPermissionOnProject(permission, it.id) } - } - - override fun project(projectId: String): Project = projects.find { it.id == projectId } - ?: throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") - - override fun projectUsers(projectId: String): List { - val projectParticipants = participants.computeIfAbsent(projectId) { - CachedSet(Duration.ofMinutes(config.service.syncParticipantsIntervalMin), Duration.ofMinutes(1)) { - mpClient.readParticipants(projectId) - } - } - - return projectParticipants.get().toList() - } - - override fun userByExternalId(projectId: String, externalUserId: String): User? = - projectUsers(projectId).find { it.externalId == externalUserId } - -} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt deleted file mode 100644 index 8d65b10d..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/CachedSet.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2020 The Hyve - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.radarbase.authorizer.util - -import java.time.Duration -import java.time.Instant - -class CachedSet( - private val refreshDuration: Duration, - private val retryDuration: Duration, - private val supplier: () -> Iterable) { - @set:Synchronized - private var cached: Set = emptySet() - set(value) { - val now = Instant.now() - field = value - nextRefresh = now.plus(refreshDuration) - nextRetry = now.plus(retryDuration) - } - - private var nextRefresh: Instant = Instant.MIN - private var nextRetry: Instant = Instant.MIN - - @get:Synchronized - private val state: State - get() { - val now = Instant.now() - return State(cached, - now.isAfter(nextRefresh), - now.isAfter(nextRetry)) - } - - private fun refresh() = supplier.invoke().toSet() - .also { cached = it } - - fun contains(value: T) = state.query({ it.contains(value) }, { it }) - fun find(predicate: (T) -> Boolean): T? = state.query({ it.find(predicate) }, { it != null }) - fun get(): Set = state.query({ it }, { it.isNotEmpty() }) - - private inner class State(val cache: Set, val mustRefresh: Boolean, val mayRetry: Boolean) { - fun query(method: (Set) -> S, validityPredicate: (S) -> Boolean): S { - var result: S - if (mustRefresh) { - result = method(refresh()) - } else { - result = method(cache) - if (!validityPredicate(result) && mayRetry) { - result = method(refresh()) - } - } - return result - } - } -} From 270b88eb38ce5908a287a291ed48ccce0906e32c Mon Sep 17 00:00:00 2001 From: peyman-mohtashami Date: Thu, 10 Sep 2020 11:16:47 +0200 Subject: [PATCH 40/80] Modify toolbar, change title and logo --- ...ource-user-list-delete-dialog.component.ts | 249 ++++++++++++++++++ ...source-user-list-reset-dialog.component.ts | 249 ++++++++++++++++++ .../shared/toolbar/toolbar.component.html | 4 +- .../app/models/rest-source-project.model.ts | 12 + authorizer-app/src/assets/chdr_logo.png | Bin 0 -> 368161 bytes authorizer-app/src/proxy.conf.json | 13 + 6 files changed, 525 insertions(+), 2 deletions(-) create mode 100644 authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-delete-dialog.component.ts create mode 100644 authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.component.ts create mode 100644 authorizer-app/src/app/models/rest-source-project.model.ts create mode 100644 authorizer-app/src/assets/chdr_logo.png create mode 100644 authorizer-app/src/proxy.conf.json diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-delete-dialog.component.ts b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-delete-dialog.component.ts new file mode 100644 index 00000000..8b4860c9 --- /dev/null +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-delete-dialog.component.ts @@ -0,0 +1,249 @@ +import { + AfterViewInit, + Component, + Inject, + OnInit, + ViewChild +} from '@angular/core'; +import { + MAT_DIALOG_DATA, + MatDatepickerInputEvent, + MatDialog, + MatDialogRef, + MatPaginator, + MatSort, + MatTableDataSource +} from '@angular/material'; +import { RestSourceUser } from '../../models/rest-source-user.model'; +import { RestSourceUserService } from '../../services/rest-source-user.service'; +import {FormControl, FormGroup} from '@angular/forms'; +import { HttpErrorResponse } from '@angular/common/http'; +import * as moment from 'moment'; +import deepcopy from 'ts-deepcopy'; +import {RestSourceProject} from "../../models/rest-source-project.model"; +import {ActivatedRoute, Router} from "@angular/router"; + +@Component({ + selector: 'rest-source-list', + templateUrl: './rest-source-user-list.component.html', + styleUrls: ['./rest-source-user-list.component.css'] +}) +export class RestSourceUserListComponent implements OnInit, AfterViewInit { + displayedColumns = [ + 'id', + // 'projectId', + 'userId', + // 'sourceId', + 'startDate', + 'endDate', + 'externalUserId', + 'authorized', + // 'version', + 'edit', + 'reset', + 'delete' + ]; + columnsToDisplay = [ + 'id', + 'userId', + 'externalUserId', + 'startDate', + 'endDate', + 'authorized', + 'actions' + ]; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + errorMessage: string; + restSourceUsers: RestSourceUser[]; + restSourceProjects: RestSourceProject[]; + selectedProject: string = '' + //public isCollapsed = true; + + dataSource: MatTableDataSource; + + constructor( + private restSourceUserService: RestSourceUserService, + public dialog: MatDialog, + private router: Router, + private activatedRoute: ActivatedRoute, + + ) {} + + ngOnInit() { + console.log(this.activatedRoute.snapshot.queryParams.project) + this.selectedProject = this.activatedRoute.snapshot.queryParams.project; + + this.loadAllRestSourceProjects() + //this.loadAllRestSourceUsers(); + this.dataSource = new MatTableDataSource(this.restSourceUsers); + this.dataSource.filterPredicate = function(data, filter: string): boolean { + return data.id.toLowerCase().includes(filter) || data.userId.toLowerCase().includes(filter) || data.externalUserId.toString().includes(filter); + }; + this.onChangeProject(this.selectedProject) + } + + /** + * Set the paginator and sort after the view init since this component will + * be able to query its view for the initialized paginator and sort. + */ + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + applyFilter(filterValue: string) { + filterValue = filterValue.trim(); // Remove whitespace + filterValue = filterValue.toLowerCase(); // Datasource defaults to lowercase matches + this.dataSource.filter = filterValue; + + + } + + private loadAllRestSourceUsers() { + this.restSourceUserService.getAllUsers().subscribe( + (data: any) => { + this.restSourceUsers = data.users; + this.dataSource.data = this.restSourceUsers; + console.log(this.restSourceUsers) + }, + () => { + this.errorMessage = 'Cannot load registered users!'; + } + ); + } + + private loadAllRestSourceUsersOfProject(projectId: string) { + this.restSourceUserService.getAllUsersOfProject(projectId).subscribe( + (data: any) => { + this.restSourceUsers = data.users; + this.dataSource.data = this.restSourceUsers; + console.log("loadAllRestSourceUsersOfProject", this.restSourceUsers) + }, + () => { + this.errorMessage = 'Cannot load registered users!'; + } + ); + } + + private loadAllRestSourceProjects() { + this.restSourceUserService.getAllProjects().subscribe( + (data: any) => { + console.log(data) + this.restSourceProjects = data.projects; + // this.dataSource.data = this.restSourceUsers; + + + }, + () => { + this.errorMessage = 'Cannot load projects!'; + } + ); + } + + removeDevice(restSourceUser: RestSourceUser) { + this.restSourceUserService.deleteUser(restSourceUser.id).subscribe(() => { + this.loadAllRestSourceUsers(); + }); + } + + resetUser(restSourceUser: RestSourceUser) { + this.restSourceUserService.resetUser(restSourceUser).subscribe(() => { + this.loadAllRestSourceUsers(); + }); + } + + openDeleteDialog(restSourceUser: RestSourceUser) { + const dialogRef = this.dialog.open(RestSourceUserListDeleteDialog, { + data: restSourceUser + }); + + dialogRef.afterClosed().subscribe(user => { + console.log('Deleting user...'); + this.removeDevice(user); + }); + } + + openResetDialog(restSourceUser: RestSourceUser) { + const dialogRef = this.dialog.open(RestSourceUserListResetDialog, { + data: restSourceUser + }); + + dialogRef.afterClosed().subscribe((user: RestSourceUser) => { + console.log('Resetting user...', user); + this.resetUser(user); + }); + } + + onChangeProject(projectId: string) { + this.selectedProject = projectId + console.log("change project", projectId) + if(projectId === ''){ + //this.loadAllRestSourceUsers() + }else{ + console.log('get users') + this.loadAllRestSourceUsersOfProject(projectId) + this.applyFilter("") + } + this.router.navigate(['/users'], {queryParams: {project: this.selectedProject}}); + } + + // onChangeUser(userId: string) { + // console.log("change user", userId) + // if(userId!=='all') { + // this.applyFilter(userId) + // }else{ + // this.applyFilter("") + // } + // } +} + +@Component({ + selector: 'rest-source-user-list-delete-dialog', + templateUrl: 'rest-source-user-list-delete-dialog.html' +}) +export class RestSourceUserListDeleteDialog { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: RestSourceUser + ) {} + + closeDeleteDialog(): void { + this.dialogRef.close(); + } +} + +@Component({ + selector: 'rest-source-user-list-reset-dialog', + templateUrl: 'rest-source-user-list-reset-dialog.html' +}) +export class RestSourceUserListResetDialog { + startDateFormControl: FormControl; + endDateFormControl: FormControl; + + // Stores a copy of the data so as to not modify the original content + dataCopy: RestSourceUser; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: RestSourceUser + ) { + this.startDateFormControl = new FormControl(moment(this.data.startDate)); + this.endDateFormControl = new FormControl(moment(this.data.endDate)); + this.dataCopy = deepcopy(this.data); + } + + closeResetDialog(): void { + this.dialogRef.close(); + } + + updateStartDateValue(event: MatDatepickerInputEvent) { + this.dataCopy.startDate = event.value.toISOString(); + } + + updateEndDateValue(event: MatDatepickerInputEvent) { + this.dataCopy.endDate = event.value.toISOString(); + } +} diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.component.ts b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.component.ts new file mode 100644 index 00000000..8b4860c9 --- /dev/null +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.component.ts @@ -0,0 +1,249 @@ +import { + AfterViewInit, + Component, + Inject, + OnInit, + ViewChild +} from '@angular/core'; +import { + MAT_DIALOG_DATA, + MatDatepickerInputEvent, + MatDialog, + MatDialogRef, + MatPaginator, + MatSort, + MatTableDataSource +} from '@angular/material'; +import { RestSourceUser } from '../../models/rest-source-user.model'; +import { RestSourceUserService } from '../../services/rest-source-user.service'; +import {FormControl, FormGroup} from '@angular/forms'; +import { HttpErrorResponse } from '@angular/common/http'; +import * as moment from 'moment'; +import deepcopy from 'ts-deepcopy'; +import {RestSourceProject} from "../../models/rest-source-project.model"; +import {ActivatedRoute, Router} from "@angular/router"; + +@Component({ + selector: 'rest-source-list', + templateUrl: './rest-source-user-list.component.html', + styleUrls: ['./rest-source-user-list.component.css'] +}) +export class RestSourceUserListComponent implements OnInit, AfterViewInit { + displayedColumns = [ + 'id', + // 'projectId', + 'userId', + // 'sourceId', + 'startDate', + 'endDate', + 'externalUserId', + 'authorized', + // 'version', + 'edit', + 'reset', + 'delete' + ]; + columnsToDisplay = [ + 'id', + 'userId', + 'externalUserId', + 'startDate', + 'endDate', + 'authorized', + 'actions' + ]; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + errorMessage: string; + restSourceUsers: RestSourceUser[]; + restSourceProjects: RestSourceProject[]; + selectedProject: string = '' + //public isCollapsed = true; + + dataSource: MatTableDataSource; + + constructor( + private restSourceUserService: RestSourceUserService, + public dialog: MatDialog, + private router: Router, + private activatedRoute: ActivatedRoute, + + ) {} + + ngOnInit() { + console.log(this.activatedRoute.snapshot.queryParams.project) + this.selectedProject = this.activatedRoute.snapshot.queryParams.project; + + this.loadAllRestSourceProjects() + //this.loadAllRestSourceUsers(); + this.dataSource = new MatTableDataSource(this.restSourceUsers); + this.dataSource.filterPredicate = function(data, filter: string): boolean { + return data.id.toLowerCase().includes(filter) || data.userId.toLowerCase().includes(filter) || data.externalUserId.toString().includes(filter); + }; + this.onChangeProject(this.selectedProject) + } + + /** + * Set the paginator and sort after the view init since this component will + * be able to query its view for the initialized paginator and sort. + */ + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + applyFilter(filterValue: string) { + filterValue = filterValue.trim(); // Remove whitespace + filterValue = filterValue.toLowerCase(); // Datasource defaults to lowercase matches + this.dataSource.filter = filterValue; + + + } + + private loadAllRestSourceUsers() { + this.restSourceUserService.getAllUsers().subscribe( + (data: any) => { + this.restSourceUsers = data.users; + this.dataSource.data = this.restSourceUsers; + console.log(this.restSourceUsers) + }, + () => { + this.errorMessage = 'Cannot load registered users!'; + } + ); + } + + private loadAllRestSourceUsersOfProject(projectId: string) { + this.restSourceUserService.getAllUsersOfProject(projectId).subscribe( + (data: any) => { + this.restSourceUsers = data.users; + this.dataSource.data = this.restSourceUsers; + console.log("loadAllRestSourceUsersOfProject", this.restSourceUsers) + }, + () => { + this.errorMessage = 'Cannot load registered users!'; + } + ); + } + + private loadAllRestSourceProjects() { + this.restSourceUserService.getAllProjects().subscribe( + (data: any) => { + console.log(data) + this.restSourceProjects = data.projects; + // this.dataSource.data = this.restSourceUsers; + + + }, + () => { + this.errorMessage = 'Cannot load projects!'; + } + ); + } + + removeDevice(restSourceUser: RestSourceUser) { + this.restSourceUserService.deleteUser(restSourceUser.id).subscribe(() => { + this.loadAllRestSourceUsers(); + }); + } + + resetUser(restSourceUser: RestSourceUser) { + this.restSourceUserService.resetUser(restSourceUser).subscribe(() => { + this.loadAllRestSourceUsers(); + }); + } + + openDeleteDialog(restSourceUser: RestSourceUser) { + const dialogRef = this.dialog.open(RestSourceUserListDeleteDialog, { + data: restSourceUser + }); + + dialogRef.afterClosed().subscribe(user => { + console.log('Deleting user...'); + this.removeDevice(user); + }); + } + + openResetDialog(restSourceUser: RestSourceUser) { + const dialogRef = this.dialog.open(RestSourceUserListResetDialog, { + data: restSourceUser + }); + + dialogRef.afterClosed().subscribe((user: RestSourceUser) => { + console.log('Resetting user...', user); + this.resetUser(user); + }); + } + + onChangeProject(projectId: string) { + this.selectedProject = projectId + console.log("change project", projectId) + if(projectId === ''){ + //this.loadAllRestSourceUsers() + }else{ + console.log('get users') + this.loadAllRestSourceUsersOfProject(projectId) + this.applyFilter("") + } + this.router.navigate(['/users'], {queryParams: {project: this.selectedProject}}); + } + + // onChangeUser(userId: string) { + // console.log("change user", userId) + // if(userId!=='all') { + // this.applyFilter(userId) + // }else{ + // this.applyFilter("") + // } + // } +} + +@Component({ + selector: 'rest-source-user-list-delete-dialog', + templateUrl: 'rest-source-user-list-delete-dialog.html' +}) +export class RestSourceUserListDeleteDialog { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: RestSourceUser + ) {} + + closeDeleteDialog(): void { + this.dialogRef.close(); + } +} + +@Component({ + selector: 'rest-source-user-list-reset-dialog', + templateUrl: 'rest-source-user-list-reset-dialog.html' +}) +export class RestSourceUserListResetDialog { + startDateFormControl: FormControl; + endDateFormControl: FormControl; + + // Stores a copy of the data so as to not modify the original content + dataCopy: RestSourceUser; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: RestSourceUser + ) { + this.startDateFormControl = new FormControl(moment(this.data.startDate)); + this.endDateFormControl = new FormControl(moment(this.data.endDate)); + this.dataCopy = deepcopy(this.data); + } + + closeResetDialog(): void { + this.dialogRef.close(); + } + + updateStartDateValue(event: MatDatepickerInputEvent) { + this.dataCopy.startDate = event.value.toISOString(); + } + + updateEndDateValue(event: MatDatepickerInputEvent) { + this.dataCopy.endDate = event.value.toISOString(); + } +} diff --git a/authorizer-app/src/app/components/shared/toolbar/toolbar.component.html b/authorizer-app/src/app/components/shared/toolbar/toolbar.component.html index 076a67fb..978a0815 100644 --- a/authorizer-app/src/app/components/shared/toolbar/toolbar.component.html +++ b/authorizer-app/src/app/components/shared/toolbar/toolbar.component.html @@ -1,9 +1,9 @@
- +
- Welcome to RADAR-Base REST Source Authorizer + CHDR MORE® REST Source Authorizer - + + +
+ + + + + + + +
+ + diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-registration-form.component.ts b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-registration-form.component.ts index 0e86c710..967528e7 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-registration-form.component.ts +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-registration-form.component.ts @@ -16,14 +16,14 @@ export class RestSourceUserRegistrationFormComponent implements OnInit { sourceTypes: string[]; sourceClientDetail: RestSourceClientDetails; - + showForm: boolean = false constructor(private restSourceUserService: RestSourceUserService, private sourceClientAuthorizationService: SourceClientAuthorizationService, private fb: FormBuilder, private platformLocation: PlatformLocation ) { - this.createForm(); + } @@ -31,14 +31,17 @@ export class RestSourceUserRegistrationFormComponent implements OnInit { this.sourceClientAuthorizationService.getDeviceTypes().subscribe( data => { this.sourceTypes = data; + this.createForm(); + this.intiSourceClientDetails() } ); } createForm() { this.sourceTypeForm = this.fb.group({ - selectedDeviceType: '', + selectedDeviceType: this.sourceTypes[0], }); + this.showForm = true } onChange(sourceType: any) { @@ -52,4 +55,15 @@ export class RestSourceUserRegistrationFormComponent implements OnInit { ); } + intiSourceClientDetails() { + this.sourceClientAuthorizationService.getSourceClientAuthDetails(this.sourceTypes[0]).subscribe( + data => { + this.sourceClientDetail = data; + this.callbackUrl = window.location.origin + + this.platformLocation.getBaseHrefFromDOM() + + 'users:new'; + } + ); + } + } diff --git a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html index 0f26fe75..f7eeb07c 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html +++ b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html @@ -1,78 +1,99 @@ -{{ errorMessage}} -
-

Enter details for User with External User Id: {{restSourceUser.externalUserId}}

-
-
- - -
+ -
- - -
+
+ + {{ errorMessage}} + +
-
+
+

Enter details for User

+

Edit details for User

+
+ + +
-
- Project and User Id is required. -
-
- Project and User Id must be at least 4 characters long. -
-
- The pattern of the entered User Id is not correct. -
-
+
+ + +
-
- - -
+
+ + +
+ +
+ + +
-
- -
- -
- -
+
+ + +
+ +
+ +
+ +
+
+
-
- -
- -
- -
+
+ +
+ +
+
+
+ +
+ + +
+ +
+ + +
- + + - - + +
diff --git a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts index 192f078f..70849ee8 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts +++ b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts @@ -9,27 +9,37 @@ import { HttpErrorResponse } from '@angular/common/http'; import { RestSourceUser } from '../../models/rest-source-user.model'; import { RestSourceUserService } from '../../services/rest-source-user.service'; import { SourceClientAuthorizationService } from '../../services/source-client-authorization.service'; +import {RestSourceProject} from "../../models/rest-source-project.model"; +import {Location} from '@angular/common'; @Component({ selector: 'update-rest-source-user', templateUrl: './update-rest-source-user.component.html', providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] }) + export class UpdateRestSourceUserComponent implements OnInit { + isEditing = false; errorMessage?: string; - restSourceUser: RestSourceUser; startDate: Date; endDate: Date; - isEditing = false; + restSourceUser: RestSourceUser; + restSourceUsers: any[] = []; + restSourceProjects: RestSourceProject[] = []; constructor( private restSourceUserService: RestSourceUserService, private sourceClientAuthorizationService: SourceClientAuthorizationService, private router: Router, - private activatedRoute: ActivatedRoute + private activatedRoute: ActivatedRoute, + private _location: Location ) {} ngOnInit() { + this.loadAllRestSourceProjects() + } + + initialize(){ this.activatedRoute.params.subscribe(params => { if (params.hasOwnProperty('id')) { this.restSourceUserService.getUserById(params['id']).subscribe( @@ -38,8 +48,10 @@ export class UpdateRestSourceUserComponent implements OnInit { this.isEditing = true; this.startDate = new Date(this.restSourceUser.startDate); this.endDate = new Date(this.restSourceUser.endDate); + this.onChangeProject(this.restSourceUser.projectId) }, (err: Response) => { + console.log('Cannot retrieve current user details', err) this.errorMessage = 'Cannot retrieve current user details'; window.setTimeout(() => this.router.navigate(['']), 5000); } @@ -57,9 +69,20 @@ export class UpdateRestSourceUserComponent implements OnInit { }); } - private updateRestSourceUser() { - this.restSourceUser.startDate = this.startDate.toISOString(); - this.restSourceUser.endDate = this.endDate.toISOString(); + updateRestSourceUser() { + if(!this.endDate || !this.startDate){ + this.errorMessage = + 'Please select Start Date and End Date'; + return; + } + try{ + this.restSourceUser.startDate = this.startDate.toISOString(); + this.restSourceUser.endDate = this.endDate.toISOString(); + }catch(err){ + this.errorMessage = + 'Please enter valid Start Date and End Date'; + return; + } this.restSourceUserService.updateUser(this.restSourceUser).subscribe( () => { return this.router.navigate(['/users']); @@ -73,7 +96,7 @@ export class UpdateRestSourceUserComponent implements OnInit { // The backend returned an unsuccessful response code. // The response body may contain clues as to what went wrong, this.errorMessage = `Backend Error: Status=${err.status}, - Body: ${err.error.error}, ${err.error.message}`; + Body: ${err.error.error}, ${err.error.message}`; if (err.status == 417) { this.errorMessage += ' Please check the details are correct and try again.'; @@ -83,19 +106,55 @@ export class UpdateRestSourceUserComponent implements OnInit { ); } - private cancelUpdateUser() { - return this.router.navigate(['/users']); - } - private addRestSourceUser(code: string, state: string) { this.restSourceUserService.addAuthorizedUser(code, state).subscribe( data => { + this.onChangeProject(data.projectId) this.restSourceUser = data; }, (err: Response) => { + console.log('Cannot retrieve current user details', err) this.errorMessage = 'Cannot retrieve current user details'; window.setTimeout(() => this.router.navigate(['']), 5000); } ); } + + private loadAllRestSourceProjects() { + this.restSourceUserService.getAllProjects().subscribe( + (data: any) => { + this.restSourceProjects = data.projects; + this.initialize() + }, + () => { + this.errorMessage = 'Cannot load projects!'; + } + ); + } + + onChangeProject(projectId: string) { + if(projectId === ''){ + }else{ + this.loadAllRestSourceUsersOfProject(projectId) + } + } + + private loadAllRestSourceUsersOfProject(projectId: string) { + this.restSourceUserService.getAllUsersOfProject(projectId).subscribe( + (data: any) => { + this.restSourceUsers = data.users; + }, + () => { + this.errorMessage = 'Cannot load registered users!'; + } + ); + } + + cancelUpdateUser() { + return this.router.navigate(['/users']); + } + + backClicked() { + this._location.back(); + } } From 4e6cc6b8a0222c31c1a1a6e3ead0c0eb2d15ad11 Mon Sep 17 00:00:00 2001 From: peyman-mohtashami Date: Thu, 10 Sep 2020 11:20:21 +0200 Subject: [PATCH 42/80] Modify app.module and models and styles --- authorizer-app/src/app/app.module.ts | 8 ++++---- .../src/app/models/rest-source-project.model.ts | 12 +++--------- authorizer-app/src/styles.css | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/authorizer-app/src/app/app.module.ts b/authorizer-app/src/app/app.module.ts index 2a03acc4..815b74e9 100644 --- a/authorizer-app/src/app/app.module.ts +++ b/authorizer-app/src/app/app.module.ts @@ -20,8 +20,6 @@ import { } from '@angular/material'; import { RestSourceUserListComponent, - RestSourceUserListDeleteDialog, - RestSourceUserListResetDialog } from './components/rest-source-authorization/rest-source-user-list.component'; import { RouterModule, Routes } from '@angular/router'; @@ -44,6 +42,8 @@ import { ToolbarComponent } from './components/shared/toolbar/toolbar.component' import { UpdateRestSourceUserComponent } from './components/rest-source-authorization/update-rest-source-user.component'; import { AuthInterceptor } from './auth.interceptor'; import { ErrorInterceptor } from './error.interceptor'; +import {RestSourceUserListDeleteDialog} from "./components/rest-source-authorization/rest-source-user-list-delete-dialog.component"; +import {RestSourceUserListResetDialog} from "./components/rest-source-authorization/rest-source-user-list-reset-dialog.component"; const appRoutes: Routes = [ { @@ -96,8 +96,8 @@ const appRoutes: Routes = [ RestSourceUserListComponent, LoginPageComponent, ToolbarComponent, - RestSourceUserListDeleteDialog, - RestSourceUserListResetDialog + RestSourceUserListResetDialog, + RestSourceUserListDeleteDialog ], entryComponents: [ RestSourceUserListDeleteDialog, diff --git a/authorizer-app/src/app/models/rest-source-project.model.ts b/authorizer-app/src/app/models/rest-source-project.model.ts index 032d1daa..3200595f 100644 --- a/authorizer-app/src/app/models/rest-source-project.model.ts +++ b/authorizer-app/src/app/models/rest-source-project.model.ts @@ -1,12 +1,6 @@ -export class RestSourceUser { +export class RestSourceProject { id?: string; version?: string; - projectId?: string; - userId?: string; - sourceId?: string; - startDate?: string; - endDate?: string; - externalUserId?: string; - authorized?: boolean; - timesReset?: number; + description ?: string; + location ?: string; } diff --git a/authorizer-app/src/styles.css b/authorizer-app/src/styles.css index c4398e42..66864c41 100644 --- a/authorizer-app/src/styles.css +++ b/authorizer-app/src/styles.css @@ -2,3 +2,20 @@ @import "~bootstrap/dist/css/bootstrap.css"; @import "~font-awesome/css/font-awesome.css"; @import "@angular/material/prebuilt-themes/indigo-pink.css"; + +.mat-cell { + align-content: start !important; + text-align: start !important; +} +.mat-header-cell { + align-content: start !important; + text-align: start !important; +} + +.page-container{ + padding: 1em; +} + +.users-container{ + margin-top:3em; +} From 24cfd03f448e24c5063404e650619b0a0c62084d Mon Sep 17 00:00:00 2001 From: peyman-mohtashami Date: Thu, 10 Sep 2020 11:22:47 +0200 Subject: [PATCH 43/80] Add getAllUsersOfProject and project model --- .../src/app/services/rest-source-user.service.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/authorizer-app/src/app/services/rest-source-user.service.ts b/authorizer-app/src/app/services/rest-source-user.service.ts index 4a775e08..5f9b699d 100644 --- a/authorizer-app/src/app/services/rest-source-user.service.ts +++ b/authorizer-app/src/app/services/rest-source-user.service.ts @@ -3,6 +3,7 @@ import {HttpClient, HttpParams} from '@angular/common/http'; import {Observable} from 'rxjs/internal/Observable'; import {RestSourceUser} from '../models/rest-source-user.model'; import {environment} from '../../environments/environment'; +import {RestSourceProject} from "../models/rest-source-project.model"; @Injectable({ providedIn: 'root' @@ -18,6 +19,14 @@ export class RestSourceUserService { return this.http.get(this.serviceUrl); } + getAllUsersOfProject(projectId: string): Observable { + return this.http.get(environment.backendBaseUrl + '/users?project-id='+projectId); + } + + getAllProjects(): Observable { + return this.http.get(environment.backendBaseUrl + '/projects'); + } + updateUser(sourceUser: RestSourceUser): Observable { const params = new HttpParams().set('validate', String(environment.doValidate)); return this.http.post(this.serviceUrl + '/' + sourceUser.id, sourceUser, {params}); From e745882c09cde95af38240d314ea45b33ba10c05 Mon Sep 17 00:00:00 2001 From: peyman-mohtashami Date: Wed, 16 Sep 2020 11:14:27 +0200 Subject: [PATCH 44/80] Revert back radar-base toolbar --- .../shared/toolbar/toolbar.component.html | 2 +- authorizer-app/src/assets/chdr_logo.png | Bin 368161 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 authorizer-app/src/assets/chdr_logo.png diff --git a/authorizer-app/src/app/components/shared/toolbar/toolbar.component.html b/authorizer-app/src/app/components/shared/toolbar/toolbar.component.html index 978a0815..999a6b94 100644 --- a/authorizer-app/src/app/components/shared/toolbar/toolbar.component.html +++ b/authorizer-app/src/app/components/shared/toolbar/toolbar.component.html @@ -1,7 +1,7 @@
- +
CHDR MORE® REST Source Authorizer diff --git a/authorizer-app/src/assets/chdr_logo.png b/authorizer-app/src/assets/chdr_logo.png deleted file mode 100644 index c7c147204ce822351ba77bd9bdde2810b5291c93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 368161 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�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 z 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%0AkM3jgj2OSC&WW>3#x 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 From b9a0fefe58f2b538ed5fde721b78c1f8e3706127 Mon Sep 17 00:00:00 2001 From: nivethika Date: Wed, 16 Sep 2020 15:27:51 +0200 Subject: [PATCH 45/80] misc fixes --- .../rest-source-user-list-reset-dialog.html | 7 +++--- .../rest-source-user-list.component.ts | 4 +-- .../update-rest-source-user.component.html | 4 +-- .../update-rest-source-user.component.ts | 25 ++++++++++--------- .../app/models/rest-source-project.model.ts | 9 ++++++- .../src/app/models/rest-source-user.model.ts | 3 ++- .../app/services/rest-source-user.service.ts | 13 ++++++---- 7 files changed, 39 insertions(+), 26 deletions(-) diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.html b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.html index 66a25152..015a422b 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.html +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.html @@ -1,9 +1,10 @@

Reset User in project {{data.projectId}}?

-

This will pull all the data for the user (id: {{data.id}}, user-id: {{data.userId}}) again in the specified date range. -

+

This will pull all the data for the user (project: {{data.projectId}}, user-id: {{data.userId}} with external-user-id: {{data.externalUserId}}) again in the specified date range.

+

Are you sure you want to continue resetting? +

If yes, please proceed by pressing the Ok button.


-

You can now change the date range if required:

+

Optionally, you can change the date range if required:

diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts index 0aa7fa3c..cbeec985 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts @@ -12,7 +12,7 @@ import { } from '@angular/material'; import { RestSourceUser } from '../../models/rest-source-user.model'; import { RestSourceUserService } from '../../services/rest-source-user.service'; -import {RestSourceProject} from "../../models/rest-source-project.model"; +import {RadarProject} from "../../models/rest-source-project.model"; import {ActivatedRoute, Router} from "@angular/router"; import {RestSourceUserListDeleteDialog} from "./rest-source-user-list-delete-dialog.component"; import {RestSourceUserListResetDialog} from "./rest-source-user-list-reset-dialog.component"; @@ -38,7 +38,7 @@ export class RestSourceUserListComponent implements OnInit, AfterViewInit { errorMessage: string; restSourceUsers: RestSourceUser[]; - restSourceProjects: RestSourceProject[]; + restSourceProjects: RadarProject[]; selectedProject: string = '' dataSource: MatTableDataSource; diff --git a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html index f7eeb07c..d2a42b3c 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html +++ b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html @@ -35,7 +35,7 @@

Edit details for User

@@ -74,7 +74,7 @@

Edit details for User

+ id="isAuthorized" [value]= "restSourceUser.isAuthorized? 'Yes':'No'">
diff --git a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts index 70849ee8..974857a0 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts +++ b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts @@ -9,8 +9,8 @@ import { HttpErrorResponse } from '@angular/common/http'; import { RestSourceUser } from '../../models/rest-source-user.model'; import { RestSourceUserService } from '../../services/rest-source-user.service'; import { SourceClientAuthorizationService } from '../../services/source-client-authorization.service'; -import {RestSourceProject} from "../../models/rest-source-project.model"; -import {Location} from '@angular/common'; +import { RadarProject, RadarSubject } from '../../models/rest-source-project.model'; +import { Location } from '@angular/common'; @Component({ selector: 'update-rest-source-user', @@ -24,8 +24,8 @@ export class UpdateRestSourceUserComponent implements OnInit { startDate: Date; endDate: Date; restSourceUser: RestSourceUser; - restSourceUsers: any[] = []; - restSourceProjects: RestSourceProject[] = []; + subjects: RadarSubject[] = []; + restSourceProjects: RadarProject[] = []; constructor( private restSourceUserService: RestSourceUserService, @@ -44,11 +44,13 @@ export class UpdateRestSourceUserComponent implements OnInit { if (params.hasOwnProperty('id')) { this.restSourceUserService.getUserById(params['id']).subscribe( user => { + console.log("user ", user) this.restSourceUser = user; + console.log("rest user ", this.restSourceUser) this.isEditing = true; this.startDate = new Date(this.restSourceUser.startDate); this.endDate = new Date(this.restSourceUser.endDate); - this.onChangeProject(this.restSourceUser.projectId) + // this.onChangeProject(this.restSourceUser.projectId) }, (err: Response) => { console.log('Cannot retrieve current user details', err) @@ -85,7 +87,7 @@ export class UpdateRestSourceUserComponent implements OnInit { } this.restSourceUserService.updateUser(this.restSourceUser).subscribe( () => { - return this.router.navigate(['/users']); + return this.router.navigate(['/users'], {queryParams: {project: this.restSourceUser.projectId}}); }, (err: HttpErrorResponse) => { if (err.error instanceof ErrorEvent) { @@ -133,16 +135,15 @@ export class UpdateRestSourceUserComponent implements OnInit { } onChangeProject(projectId: string) { - if(projectId === ''){ - }else{ - this.loadAllRestSourceUsersOfProject(projectId) + if(projectId){ + this.loadAllSubjectsOfProject(projectId) } } - private loadAllRestSourceUsersOfProject(projectId: string) { - this.restSourceUserService.getAllUsersOfProject(projectId).subscribe( + private loadAllSubjectsOfProject(projectId: string) { + this.restSourceUserService.getAllSubjectsOfProjects(projectId).subscribe( (data: any) => { - this.restSourceUsers = data.users; + this.subjects = data.users; }, () => { this.errorMessage = 'Cannot load registered users!'; diff --git a/authorizer-app/src/app/models/rest-source-project.model.ts b/authorizer-app/src/app/models/rest-source-project.model.ts index 3200595f..7b8ca37a 100644 --- a/authorizer-app/src/app/models/rest-source-project.model.ts +++ b/authorizer-app/src/app/models/rest-source-project.model.ts @@ -1,6 +1,13 @@ -export class RestSourceProject { +export class RadarProject { id?: string; version?: string; description ?: string; location ?: string; } + +export class RadarSubject { + id?: string; + projectId?: string; + externalId?: string; + status?: string +} diff --git a/authorizer-app/src/app/models/rest-source-user.model.ts b/authorizer-app/src/app/models/rest-source-user.model.ts index 032d1daa..4ece6d72 100644 --- a/authorizer-app/src/app/models/rest-source-user.model.ts +++ b/authorizer-app/src/app/models/rest-source-user.model.ts @@ -7,6 +7,7 @@ export class RestSourceUser { startDate?: string; endDate?: string; externalUserId?: string; - authorized?: boolean; + sourceType?: string; + isAuthorized?: boolean; timesReset?: number; } diff --git a/authorizer-app/src/app/services/rest-source-user.service.ts b/authorizer-app/src/app/services/rest-source-user.service.ts index 5f9b699d..03adb604 100644 --- a/authorizer-app/src/app/services/rest-source-user.service.ts +++ b/authorizer-app/src/app/services/rest-source-user.service.ts @@ -3,7 +3,7 @@ import {HttpClient, HttpParams} from '@angular/common/http'; import {Observable} from 'rxjs/internal/Observable'; import {RestSourceUser} from '../models/rest-source-user.model'; import {environment} from '../../environments/environment'; -import {RestSourceProject} from "../models/rest-source-project.model"; +import {RadarProject} from "../models/rest-source-project.model"; @Injectable({ providedIn: 'root' @@ -23,13 +23,16 @@ export class RestSourceUserService { return this.http.get(environment.backendBaseUrl + '/users?project-id='+projectId); } - getAllProjects(): Observable { - return this.http.get(environment.backendBaseUrl + '/projects'); + getAllProjects(): Observable { + return this.http.get(environment.backendBaseUrl + '/projects'); + } + + getAllSubjectsOfProjects(projectId: string): Observable { + return this.http.get(environment.backendBaseUrl + '/projects/' + projectId +'/users'); } updateUser(sourceUser: RestSourceUser): Observable { - const params = new HttpParams().set('validate', String(environment.doValidate)); - return this.http.post(this.serviceUrl + '/' + sourceUser.id, sourceUser, {params}); + return this.http.post(this.serviceUrl + '/' + sourceUser.id, sourceUser); } addAuthorizedUser(code: string, state: string): Observable { From e39b4f8340d862a0a5c42a081f7f72cbe3b214dc Mon Sep 17 00:00:00 2001 From: nivethika Date: Wed, 16 Sep 2020 16:18:11 +0200 Subject: [PATCH 46/80] correct toolbar header --- .../src/app/components/shared/toolbar/toolbar.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizer-app/src/app/components/shared/toolbar/toolbar.component.html b/authorizer-app/src/app/components/shared/toolbar/toolbar.component.html index 999a6b94..9bd4c10e 100644 --- a/authorizer-app/src/app/components/shared/toolbar/toolbar.component.html +++ b/authorizer-app/src/app/components/shared/toolbar/toolbar.component.html @@ -3,7 +3,7 @@
- CHDR MORE® REST Source Authorizer + RADAR-Base REST Source Authorizer -

+ + required name="startDate" [(ngModel)]="startDate" ngbDatepicker #e="ngbDatepicker" [disabled]="isEditing">
@@ -62,8 +65,10 @@

Edit details for User

+ + required name="endDate" [(ngModel)]="endDate" ngbDatepicker #d="ngbDatepicker" [disabled]="isEditing">
@@ -86,14 +91,17 @@

Edit details for User

- --> + - + -->
From f063363d2414006e33c797d28cceab801cabf8b7 Mon Sep 17 00:00:00 2001 From: Peyman Mohtashami Date: Wed, 18 Nov 2020 16:44:07 +0100 Subject: [PATCH 73/80] Modify userId value to be set by externalId or userId --- .../rest-source-user-list-delete-dialog.html | 3 ++- .../rest-source-user-list-reset-dialog.html | 2 +- .../rest-source-user-list.component.html | 8 ++++---- .../update-rest-source-user.component.html | 17 +++-------------- .../update-rest-source-user.component.ts | 1 + .../src/app/models/rest-source-project.model.ts | 3 ++- .../src/app/models/rest-source-user.model.ts | 3 +++ .../app/services/rest-source-user.service.ts | 4 ++-- 8 files changed, 18 insertions(+), 23 deletions(-) diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-delete-dialog.html b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-delete-dialog.html index a05b558f..cd6939ef 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-delete-dialog.html +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-delete-dialog.html @@ -1,7 +1,8 @@

WARNING!

Are you sure you want to delete the user with- id: {{data.id}}, - user-id: {{data.userId}} and project-id: {{data.projectId}}? + user-id: {{data.externalId? data.externalId : data.userId}} + and project-id: {{data.projectId}}?

diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.html b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.html index 015a422b..d66588bb 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.html +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.html @@ -1,6 +1,6 @@

Reset User in project {{data.projectId}}?

-

This will pull all the data for the user (project: {{data.projectId}}, user-id: {{data.userId}} with external-user-id: {{data.externalUserId}}) again in the specified date range.

+

This will pull all the data for the user (project: {{data.projectId}}, user-id: {{data.externalId? data.externalId : data.userId}} with external-user-id: {{data.serviceUserId}}) again in the specified date range.

Are you sure you want to continue resetting?

If yes, please proceed by pressing the Ok button.


diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.html b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.html index d21fb62c..d526f11b 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.html +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.html @@ -40,11 +40,11 @@ User ID - {{user.userId | slice:0:8 }}... + {{user.externalId? user.externalId : ((user.userId | slice:0:8) + '...')}} External User ID - {{user.externalUserId}} + {{user.serviceUserId}} Start Date @@ -64,9 +64,9 @@ - - @@ -101,7 +93,4 @@

Details for User

-
diff --git a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts index e90d9c3f..c0c8cc16 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts +++ b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts @@ -51,6 +51,7 @@ export class UpdateRestSourceUserComponent implements OnInit { this.startDate = new Date(this.restSourceUser.startDate); this.endDate = new Date(this.restSourceUser.endDate); // this.onChangeProject(this.restSourceUser.projectId) + this.loadAllSubjectsOfProject(user.projectId); }, (err: Response) => { console.log('Cannot retrieve current user details', err) diff --git a/authorizer-app/src/app/models/rest-source-project.model.ts b/authorizer-app/src/app/models/rest-source-project.model.ts index 7b8ca37a..7b957805 100644 --- a/authorizer-app/src/app/models/rest-source-project.model.ts +++ b/authorizer-app/src/app/models/rest-source-project.model.ts @@ -9,5 +9,6 @@ export class RadarSubject { id?: string; projectId?: string; externalId?: string; - status?: string + status?: string; + humanReadableUserId?: string; } diff --git a/authorizer-app/src/app/models/rest-source-user.model.ts b/authorizer-app/src/app/models/rest-source-user.model.ts index 4ece6d72..9ce9c456 100644 --- a/authorizer-app/src/app/models/rest-source-user.model.ts +++ b/authorizer-app/src/app/models/rest-source-user.model.ts @@ -3,6 +3,9 @@ export class RestSourceUser { version?: string; projectId?: string; userId?: string; + humanReadableUserId?: string; + serviceUserId?: string; + externalId?: string; sourceId?: string; startDate?: string; endDate?: string; diff --git a/authorizer-app/src/app/services/rest-source-user.service.ts b/authorizer-app/src/app/services/rest-source-user.service.ts index 364d9cc2..86142c25 100644 --- a/authorizer-app/src/app/services/rest-source-user.service.ts +++ b/authorizer-app/src/app/services/rest-source-user.service.ts @@ -27,8 +27,8 @@ export class RestSourceUserService { return this.http.get(environment.backendBaseUrl + '/projects'); } - getAllSubjectsOfProjects(projectId: string): Observable { - return this.http.get(environment.backendBaseUrl + '/projects/' + projectId +'/users'); + getAllSubjectsOfProjects(projectId: string): Observable { + return this.http.get(environment.backendBaseUrl + '/projects/' + projectId + '/users'); } updateUser(sourceUser: RestSourceUser): Observable { From 9ef31e5390c3febe18950935df4b80d1fec1c214 Mon Sep 17 00:00:00 2001 From: nivethika Date: Wed, 18 Nov 2020 17:47:25 +0100 Subject: [PATCH 74/80] fix issues with logging in as PROJECT-ADMIN. Do not query when projectId is undefined --- .../rest-source-user-list.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts index 41a6e374..ecceb030 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts @@ -140,11 +140,13 @@ export class RestSourceUserListComponent implements OnInit, AfterViewInit { onChangeProject(projectId: string) { this.selectedProject = projectId - if(projectId !== ''){ + if (projectId) { this.loadAllRestSourceUsersOfProject(projectId) this.applyFilter("") + this.router.navigate(['/users'], {queryParams: {project: this.selectedProject}}); + } else { + this.router.navigate(['/users']); } - this.router.navigate(['/users'], {queryParams: {project: this.selectedProject}}); } } From 9d4db66f668c5f4f1f030d8b37e55c0b0051f685 Mon Sep 17 00:00:00 2001 From: Peyman Mohtashami Date: Thu, 19 Nov 2020 13:30:12 +0100 Subject: [PATCH 75/80] Modify serviceUserId, externalId and userId in views --- .../rest-source-user-list-delete-dialog.html | 6 +++--- .../rest-source-user-list-reset-dialog.html | 4 ++-- .../rest-source-user-list.component.html | 4 ++-- .../rest-source-user-list.component.ts | 10 ++++++---- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-delete-dialog.html b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-delete-dialog.html index cd6939ef..0be94c0d 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-delete-dialog.html +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-delete-dialog.html @@ -1,8 +1,8 @@

WARNING!

-

Are you sure you want to delete the user with- id: {{data.id}}, - user-id: {{data.externalId? data.externalId : data.userId}} - and project-id: {{data.projectId}}? +

Are you sure you want to delete the user with ID: {{data.id}}, + User ID: {{data.externalId}} ({{data.userId}}) + and Project ID: {{data.projectId}}?

diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.html b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.html index d66588bb..60801181 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.html +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list-reset-dialog.html @@ -1,8 +1,8 @@

Reset User in project {{data.projectId}}?

-

This will pull all the data for the user (project: {{data.projectId}}, user-id: {{data.externalId? data.externalId : data.userId}} with external-user-id: {{data.serviceUserId}}) again in the specified date range.

+

This will pull all the data for the user (Project: {{data.projectId}}, User ID: {{ data.externalId }} ({{data.userId}}) with Service User ID: {{data.serviceUserId}}) again in the specified date range.

Are you sure you want to continue resetting? -

If yes, please proceed by pressing the Ok button.

+

If yes, please proceed by pressing the Ok button.


Optionally, you can change the date range if required:

diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.html b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.html index dd0b168e..c122332b 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.html +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.html @@ -40,11 +40,11 @@ User ID - {{user.externalId? user.externalId : ((user.userId | slice:0:8) + '...')}} + {{(user.userId | slice:0:8) + '...'}} External User ID - {{user.serviceUserId}} + {{user.externalId}} Start Date diff --git a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts index 41a6e374..c9d7570a 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts +++ b/authorizer-app/src/app/components/rest-source-authorization/rest-source-user-list.component.ts @@ -39,7 +39,7 @@ export class RestSourceUserListComponent implements OnInit, AfterViewInit { errorMessage: string; restSourceUsers: RestSourceUser[]; restSourceProjects: RadarProject[]; - selectedProject: string = '' + selectedProject = ''; dataSource: MatTableDataSource; @@ -54,12 +54,14 @@ export class RestSourceUserListComponent implements OnInit, AfterViewInit { ngOnInit() { this.selectedProject = this.activatedRoute.snapshot.queryParams.project; - this.loadAllRestSourceProjects() + this.loadAllRestSourceProjects(); this.dataSource = new MatTableDataSource(this.restSourceUsers); this.dataSource.filterPredicate = function(data, filter: string): boolean { - return data.id.toLowerCase().includes(filter) || data.userId.toLowerCase().includes(filter) || data.externalUserId.toString().includes(filter); + return data.id.toLowerCase().includes(filter) || + data.userId.toLowerCase().includes(filter) || + data.externalId.toString().includes(filter); }; - this.onChangeProject(this.selectedProject) + this.onChangeProject(this.selectedProject); } /** From 5d5af3ecf5983b900fef6602c400254c3826c2d5 Mon Sep 17 00:00:00 2001 From: Peyman Mohtashami Date: Thu, 19 Nov 2020 13:31:03 +0100 Subject: [PATCH 76/80] Add sort userId and externalId in registration user dropdown menu --- .../update-rest-source-user.component.html | 6 +-- .../update-rest-source-user.component.ts | 38 +++++++++++-------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html index 56f17af9..59d50746 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html +++ b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html @@ -11,7 +11,7 @@

Enter details for User

Details for User

- +
@@ -33,9 +33,9 @@

Details for User

- - +
diff --git a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts index ce496c25..c6a70a65 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts +++ b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts @@ -36,26 +36,23 @@ export class UpdateRestSourceUserComponent implements OnInit { ) {} ngOnInit() { - this.loadAllRestSourceProjects() + this.loadAllRestSourceProjects(); } - initialize(){ + initialize() { this.activatedRoute.params.subscribe(params => { if (params.hasOwnProperty('id')) { this.restSourceUserService.getUserById(params['id']).subscribe( user => { - console.log("user ", user) this.restSourceUser = user; - console.log("rest user ", this.restSourceUser) - this.subjects = [{id: this.restSourceUser.userId}] + this.subjects = [{id: this.restSourceUser.userId}]; this.isEditing = true; this.startDate = new Date(this.restSourceUser.startDate); this.endDate = new Date(this.restSourceUser.endDate); - // this.onChangeProject(this.restSourceUser.projectId) this.loadAllSubjectsOfProject(user.projectId); }, (err: Response) => { - console.log('Cannot retrieve current user details', err) + console.log('Cannot retrieve current user details', err); this.errorMessage = 'Cannot retrieve current user details'; window.setTimeout(() => this.router.navigate(['']), 5000); } @@ -74,19 +71,19 @@ export class UpdateRestSourceUserComponent implements OnInit { } updateRestSourceUser() { - if(!this.endDate || !this.startDate){ + if (!this.endDate || !this.startDate) { this.errorMessage = 'Please select Start Date and End Date'; return; } if (this.endDate <= this.startDate) { - this.errorMessage = 'Please set the end date later than the start date.' + this.errorMessage = 'Please set the end date later than the start date.'; return; } - try{ + try { this.restSourceUser.startDate = this.startDate.toISOString(); this.restSourceUser.endDate = this.endDate.toISOString(); - }catch(err){ + } catch (err) { this.errorMessage = 'Please enter valid Start Date and End Date'; return; @@ -117,11 +114,11 @@ export class UpdateRestSourceUserComponent implements OnInit { private addRestSourceUser(code: string, state: string) { this.restSourceUserService.addAuthorizedUser(code, state).subscribe( data => { - this.onChangeProject(data.projectId) + this.onChangeProject(data.projectId); this.restSourceUser = data; }, (err: HttpErrorResponse) => { - this.errorMessage = err.statusText + " : " + err.error.error_description + this.errorMessage = err.statusText + ' : ' + err.error.error_description; window.setTimeout(() => this.router.navigate(['']), 10000); } ); @@ -131,7 +128,7 @@ export class UpdateRestSourceUserComponent implements OnInit { this.restSourceUserService.getAllProjects().subscribe( (data: any) => { this.restSourceProjects = data.projects; - this.initialize() + this.initialize(); }, () => { this.errorMessage = 'Cannot load projects!'; @@ -140,8 +137,8 @@ export class UpdateRestSourceUserComponent implements OnInit { } onChangeProject(projectId: string) { - if(projectId){ - this.loadAllSubjectsOfProject(projectId) + if (projectId) { + this.loadAllSubjectsOfProject(projectId); } } @@ -149,6 +146,15 @@ export class UpdateRestSourceUserComponent implements OnInit { this.restSourceUserService.getAllSubjectsOfProjects(projectId).subscribe( (data: any) => { this.subjects = data.users; + let withExternalId = this.subjects.filter(s => s.externalId); + let withoutExternalId = this.subjects.filter(s => !s.externalId); + withExternalId = withExternalId.sort((a, b) => { + return (a.externalId < b.externalId) ? -1 : 1; + }); + withoutExternalId = withoutExternalId.sort((a, b) => { + return (a.id < b.id) ? -1 : 1; + }); + this.subjects = [...withExternalId, ...withoutExternalId]; }, () => { this.errorMessage = 'Cannot load registered users!'; From ee74682289af9c37a004bc81fb380ee0d8c9527f Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Mon, 23 Nov 2020 15:34:47 +0100 Subject: [PATCH 77/80] Fix backend DTO --- .../authorizer/api/ApiDeclarations.kt | 3 +- .../authorizer/api/RestSourceUserMapper.kt | 42 ++++++++++--------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt index e29f50b3..357b4ba2 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/ApiDeclarations.kt @@ -50,8 +50,9 @@ class RestSourceUserDTO( val projectId: String?, val userId: String?, val humanReadableUserId: String?, + val externalId: String?, val sourceId: String, - val externalUserId: String, + val serviceUserId: String, val startDate: Instant, val endDate: Instant? = null, val sourceType: String, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt index 81e7a580..47c0967e 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt @@ -24,26 +24,28 @@ import javax.ws.rs.core.Context class RestSourceUserMapper( @Context private val projectService: RadarProjectService, ) { - fun fromEntity(user: RestSourceUser) = RestSourceUserDTO( - id = user.id.toString(), - projectId = user.projectId, - userId = user.userId, - humanReadableUserId = projectService.getUser(user.projectId!!, user.userId!!) - ?.let { mpUser -> - mpUser.attributes["Human-readable-identifier"] - ?.takeIf { it.isNotBlank() && it != "null" } - ?: mpUser.externalId - }, - sourceId = user.sourceId, - isAuthorized = user.authorized, - hasValidToken = user.hasValidToken(), - sourceType = user.sourceType, - endDate = user.endDate, - startDate = user.startDate, - externalUserId = user.externalUserId, - version = user.version, - timesReset = user.timesReset - ) + fun fromEntity(user: RestSourceUser): RestSourceUserDTO { + val mpUser = user.projectId?.let { p -> + user.userId?.let { u -> projectService.getUser(p, u) } + } + return RestSourceUserDTO( + id = user.id.toString(), + projectId = user.projectId, + userId = user.userId, + humanReadableUserId = mpUser?.attributes?.get("Human-readable-identifier") + ?.takeIf { it.isNotBlank() && it != "null" }, + externalId = mpUser?.externalId, + sourceId = user.sourceId, + isAuthorized = user.authorized, + hasValidToken = user.hasValidToken(), + sourceType = user.sourceType, + endDate = user.endDate, + startDate = user.startDate, + serviceUserId = user.externalUserId, + version = user.version, + timesReset = user.timesReset + ) + } fun fromRestSourceUsers(records: List, page: Page?) = RestSourceUsers( users = records.map(::fromEntity) From 99644b7779924d147c82700897d2620524f94d96 Mon Sep 17 00:00:00 2001 From: Peyman Mohtashami Date: Tue, 24 Nov 2020 11:17:24 +0100 Subject: [PATCH 78/80] Modify access denied redirection --- .../update-rest-source-user.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts index c6a70a65..1418aa77 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts +++ b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.ts @@ -60,7 +60,8 @@ export class UpdateRestSourceUserComponent implements OnInit { } else { this.activatedRoute.queryParams.subscribe((params: Params) => { if (params.hasOwnProperty('error')) { - this.errorMessage = params['error_description']; + this.errorMessage = 'Access Denied'; + window.setTimeout(() => this.router.navigate(['']), 5000); } else { this.errorMessage = null; this.addRestSourceUser(params['code'], params['state']); From bad0f10248dfca0a67eec300723fb21345a72835 Mon Sep 17 00:00:00 2001 From: Peyman Mohtashami Date: Tue, 24 Nov 2020 11:18:31 +0100 Subject: [PATCH 79/80] Fix project and user select null value --- .../update-rest-source-user.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html index 59d50746..c3d90e24 100644 --- a/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html +++ b/authorizer-app/src/app/components/rest-source-authorization/update-rest-source-user.component.html @@ -26,7 +26,7 @@

Details for User

@@ -34,7 +34,7 @@

Details for User

From d6faef58659a80924b8eafbc5167291a96d276c6 Mon Sep 17 00:00:00 2001 From: nivethika Date: Mon, 30 Nov 2020 17:19:07 +0100 Subject: [PATCH 80/80] prepare release --- authorizer-app/package.json | 2 +- build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authorizer-app/package.json b/authorizer-app/package.json index 9dc95b9d..b9682fde 100644 --- a/authorizer-app/package.json +++ b/authorizer-app/package.json @@ -1,6 +1,6 @@ { "name": "authorizer-app", - "version": "1.2.1", + "version": "2.0", "description": "Simple app to authorize to collect data from third party services ", "repository": { "type": "git", diff --git a/build.gradle.kts b/build.gradle.kts index 3f6e0f0b..4596d1a6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ subprojects { group = "org.radarbase" - version = "2.0-SNAPSHOT" + version = "2.0" } tasks.wrapper {

hF!uhK-5KVaNDjj??W{K0W_@L%{niTHW^>f62d zS6>C><6GhXuE+aWIJ8Y>!H~Ssk3_m>bAw=d4OCZG>Mp#eTwF!zVQkDq-S`*$2g*grMD)IzvBD019b52g@Q-nDw*4G@mkW?v zzD=9qaM}mW0KbDY8WHo6VCP~Ra^FklI%e;={fCiiOU$}#q zIA!(qNeS0)76dvKA60#M*Rr+5%DTgiaR$Hl=0cuK2**?&KL&HDwUM?d^%nFuOpZP~5x!#P1)&9Qt*bNqQWrR&S&w@|-@|(B^Zf+1Qh&%` zT>~xg7X0O6oqPrF_b!OoH`}ab9ZE!7gVGbnvkuE^w;rSFTmdp;4Jk=Ld4}$+63RwTgG^D7-tnfNSTqD7-=-} zb7ycasH{j~q|_p^CqqFyj%baL;>BD#&k={9xEF%rNFv%gVqAsH^b+$=1AZMFb`xhe zdvlH5ahZdLQ2)|s!vMQd!Cny!SsT5z!>;_I;wQ2^MsC|mQr&ob8AQk<#-P`Lt$e)X z2Ndzb-uB?hZO*}`2&<5Jj+CCOA?0NROwbpN{!89)ev!3c7T&@lbR!mAw}&0T1pY++ zxSn&|GGG@_w$May+3JNyU@$Yy%6bHUmAa?Ed@V;mJm+Ya&tG^vi>g_JGv;6Wgt?TV zsRxe5Mtu@P*mIan4Vnr9_;9_NQ3o2=S-)w2`ruHUq0mZCDB$O(O`S6&FS9)+!l#}1 zao3rFu(^Y0XI!;lSa!ZMJ;DM;gV0tDNkx`A*m&S-UpX|!MB+b<*jJYwqyyzSDq_rj z4)HOV#_s~dt)hS@2tfop7F`k+_FOw#d=+?pagbi6T>PX@&;K% z>w7q-{Q%l-=d18-nM4{h4l+$h6as2GG3XOpN~_G9F=!eqSI$1%boZu!ZJPU$ydr3{ zoPrKC9#xdE*#1pRwy#|Z0T`JOK-v<2mNDQpKZ_}{^2Y} zX}uN+*Y|bCD{0_V5nHQPlL_y(&JVwmHaDWk=9hHiyVE^^F1@-{Rq+2mY$54)MpVOs zNX-1yY0u)jlP|East}CA*Zp%-gL*bCh-3p5ZKnpG$f9r`qW5O}GC(-128TNg7RFY; zkn|>$h`LQoIZn)zK{GSO9lfXA=~NrX{G>~;1NVl4WR#P?FPy%#yt`&hW-k0=({4F6t3O@GQ2nvQgZVviee?ha8-n z-luAeVH?^uKHNeP#*`KBY$X%L=b@qLy^|GHo0mE*89RTEa>W$Cb|(eNrM>v%VIAUmF8g|$IX3h4VE{tl?YQ=u>r>M`F85)8tMl)Nz;xc5_1f#(#f zlPey=SW=vOp^!U4#Ivu2qTg!NFb~FfmRgSN-}^wXO}^^d`#{fSuvF^~)9+yzk1qbw zR{=fgl+@&wa<~%Xhvt)(HPC}XZJBaMa_j~>Jnq?^PtUJYTQ*p5-N@`i!1a|B9~4k` z|3K2?q-o&{2VFp<0`Yo)-q|R|EVg3XN6vU)0j|V$+U>%tsqO_iFfu|=8h6G)don(r z#O#4t=>1?6P$3PCkN zEA9NQP?(gyO^IPX7`CeZ%A169q)xm~KF(ipB0@WTD@o_)<~sWY_6C6UoQP#FxMx)t z8omJrypMajrZ^*yxeK0}$_E*kA60Lb z%^Nzc&%CrRUW8z_itqOMEEvX&AJXzpf}Z&h81KVhV=lagvHF0j!gtn%dp}KSo4E_# zPw%{G(oInTA=FrS3mMiJci%nlw-_V;?MN2tHhEXXZ;)FLU}sg%GkPz~_{K#3@guGH z6>8Xb)2&SNj{ZU_r8fxFybXflC`GscGFy;XoK-Eqk`EhA--lYs;)_&l_{d})ui}_i#favQSVsSj_CQ_zrY`{s%P2EZM35=~{y8DL(Pe8<6d0a0YXp1I= zWzsu8)o)SDyaI|7Fk9-LdR5m#AhxOlc7wuNvgEO}qD`RlRktkFQH+;QI+EG@r3JQ< zQE8ZSACjhN4&&a{jwI)O&E-N=V94yuuI7Y0@tLi^y|oWs14bLdQ;%{%@{ zCwu?CF%<$>-jcKkb=2e59Zb}r9C5=D?kL`CBK|&(tygCsJfA+woh^oe22^R9iXo-A zON9AwE3QM32@k=dji2?yCc<@6d^YAfe<)4&8qP|yKu~g)L^y&S6fIOB82r&YcSivaW#H-Sz{c|U0|T( zdwdWrW%wB>f`;bF{NJ`+$+!1qd43{$9_+Oqiw)ntwm(?isu^pV|Ko^P)?ZO%ssu#d zJ|iUqD{DI=lJ2a2R}KD7+iW#xPv7C&H8Gr^>Y}=YknNVP$NLGE~@dDRvE?5Dat;#8 zE`#^-Ejl*k#5s1^wnKMhohnZ12qnepJrDKSsS9TX6@n$%HNL7w8+TzooQrlA`+}*+ zbiC)wv7`QGf)KZVNZy!dugguX7Aksh%z~de>#K+#9~utDjm32cy3ds=;TNLGZ4kbU zP8i0H?tP%g&(65Yz>%@noyZ@TJRak<9Z3F0Wid@)n5%|hxA*rJ6W0vVR8Rq!Tl2tD zq#Pf{WMN5o?mzTKQWn7+z=&LV0I5J$zf5V{&16J{?8paGK!jDdFxp$AooS36nZGl8 zg)<(600yH(Q_Z|{EJ3Izlql?;>ZYvzuh|&21CzzA?sZ|n%(q&~TJ0X2{5tvgZawEQ zfI_Yjf)JYSUQZ>8Q_cVmd**yT7Mk;F=!T~Il7OhDqcp%^P<^feTn2iiZVmEVQhjF{ z#{HEzp#94hGP0{P@gfN<9QMUtE3^HFaX0dk2b#gBQ|6AoUy@XzLHPJ;DPt7;P-7M2 zv6>lAdIOt!d%LcGG|;qxR*nXm^dw?R#`mh5AqEp+iXl`M3ZMIS7^vTxLbfYmMeWv z{TNx4#D8xc%LtuYcgz8_8=7 zZ>*G9KL>9mW zTN&Bg(XHcnrrQIEF3aj>Y_9${T<}vI_9o;6%wX`#jtWFPgh6@uJUMr$Ct2h?)9Qxe zQq-*n_(G60Wz9;A-QI3ls-oZbX-C@UA92EBvAi4(g;s208Dlz=>^aViiy*KtEqndY zJUBuihA%PxoXjT9{7u3R#h;uhtmtNngtnC*boNh?jhVIx)OxKuq$%!L{Id@aCL_N3 zh1|UCH}|Q{9b6t43^!nv*csT#RGJK=%jJl*$G|Wg^FO=z#9J5Zo#1QD%DHI2R^`y(WN8?19Q5VmAo#+j&p~69lKJR zrZtRBv@bf5>GQOf3pRYpoQ&(JV3>*iQ7yDf!P$J5t}2NihH_&dumnZ9Bd;}p z8185Vd-80IIcgl{>bEUtPrkS4j==bYjij>)!e=(dm-0~`em^BEc{~8>6^k~Xw3c&E zvg0>69uSoAwwv>=SxeJ5f)Y_<0lPxhG+e5AjkS+5`5wD4uz_;DG_9Uh*|3?H<>A zZPJvrM=%*UwrxzXDi{Go6E!0XumB(E1`^Z7Ah52`Q+33|k%;y}K&EB~YR4-@nj^_8 zliBQ7bpj&lpnE#ej@`a@DaIg}!ravG<|P(&y2I>MIA+Mo;}Za2Hm1i#!fN!IhvP1p19b znvJS6U$V>QC=Ts+OU!u-Fvs9m+Y@K{lhlmep{sNwT&eBd_)# zdCuMAtz_gV%%yi~Ln4QU(7C^eogF7n4SS5jtneAUyBD0_Xb?$&P{?;}zrh;w2E32@ z{N9ri>J5gleHnl!AAUGZ<%bhQLIXe*BPdwwFm~|Ev}xPi(O)=Q$^r?W2r>$--@^CO#a~e<2p;J~@ zhmd-^j0TvO7M%ELr7SIw2+k+&A>_?Q@2;F4Wi-5v^?i6C=se0&dIi4IWjLDkQvS*# zFO$49ZDzQ+shHe_a;rV;+KEMSFQOjxg((3C06xTR7ft~Y>zT>KqBBta8(|{BUQIcZ z5@?Okl2S%fmC#_K+cj^f=S8|oWz^&WuS^&;lBhar%2gOlYOaBMC#B}-o!*2)v}Z#Z zE9(q%(&q}s2S+%(&{m)(xzcuHks2+Hha$|+6TxgS8IB8{*h146Y@r#4%|gtd27!Nw z^uJgD2I2$frfp8)PzxPRAt0)Kc6ayBa^js-11)VakqrX$qukRbv|r1Of^c*z%{aT2 zrmx!~q<@U|jOBLWt4MIoPSmy~Qs8!KlY^5THF8rqoICu`5!dyLtwWKC z85L>4!LOfz7t60PExinFPUvmy&I`F(nB zq z%+vAyN=5z`GeD&Z$EQR2;U@dKByB`epy(eg8OiXiZDAJfiuhf@g3|9CUz)N zaNR1+O6pyIS+loSG}MASg~jy4w{GV#aHY}Gg=40j)>1xHu${<^rQp{ON%3ROmkaaK z%n*fxA|MWJGv+FEvd2Q`M)N|qcu=Au5o7`)kTwbm;t(KdHgQ$w7JVB0a=|;P`*!F5)#IGAO=h--kE&zS$t%2b=sF4DrW0In+9Hg49s=RABpP!D8u-;X zpE?2c-HJi(*um6UgS=R+BZO2vu1-ii*b0Zh+C1Xe&UoYi1r)$LskcOsI)7$3R;O0O|c?K0l}<*3x@bU7Km1B zN9-7;KuP|aKm7JJnUcOx!K@7xFW`>`z4`nq5GbCnRVbgQkSEVd%d^c-`{El2i$jaJ zu*m03C@17V6&J%;X^6kiQiQnQ;q|oK$%!9wHjSk*zG zrKS{#jfpd*@nDAw#wRbfoUpXTdwd1og(aByv264*Mj3zQcgU*63*J;JAkR}@a@i4@O^pnQX!U)6zsAp3s%5m-SUFOp4wy z7=&L?iwCod733#YW^0FwN8i!BMXrHtcr)eo@0S)aGGnGRt|z>SjA{<8*nQX?{L)C< zoZx~JG@x1dVEbYMiq?ZuOPQv>sS^~mOycXDI!6=_ng0e4nc+w<2-QBo zYxVsBI{+1aIZms%4S0JVl9N+pniAQDOnDO|J~t`WeC4mUTq_c)L4Ga ziH(K*_=j2=eZiTl*Srb+7~?e`&Uof-pSIF#o}a|;((;b+ITO2zO)+oY3IXFsyYTI& zTsX4vO~V3;EQFA_3cfh5_EqN=a%*)175M$8vA$p&>0B6gzdpv17qnl2-Y&xhhcbH~ z=(S0f7J>-Bw|n=v`OUtQ3kx zkt84z+(qdjtm95}`8!i;U=Z?zAm9KBsw^Nfa(wRgsq2DMmmJGkV*NvwA_zg9PRokfOlHaoQ%5J0KWdm;X>Nc{`+lK@4Ni&5@zw-c0ZM$Rtbi z6Di*%9C__lA$=lDF83M)VGmUP@1u>kV4~Emnf6ny&+MjKeP(%N?CJ)VEz}RB8uUX; z)HDX0C7+oDVHbqwf3VBA5N@p>v}+MVO+y&*?|;1CUl_~S718&6k1H>%usQbma3zBv zd=7#813vbA!d0>iFpf`v(RoisHl_8=f^iA=peRcZaU@Qti#xzDYPCHMQ=yICiWOBO zt#4eh&GPcQ^GV%*T$Vd~8`Xiy=}_`>ho;_&m8UTQ7Z68>VWq(QZn;Jx)>fDlHGo^^G{!Fn~pXKR0O=Jbgy*h%t| zT=@k>;ArhwTqQtP8N8Neb|IT|DVSWCbcJY2?&$x5>6(WQj@O03TOr1_F&n!XJBIz{ z51Y2Z8;Gy8#&0aE3uXEU2tSAH=KHAQ;v{P|Uk^_UL9(622ujMZsubAy|Xv(sXZOd=qP+gqh+k zFIZ{Z6cc9n*GutXKJXAor1`-P^Q&86cG`#Yz<7tw^UOi1hmw2ZO!e0%zPqB8FMM@r z96v_n;8(xEarP$J{V0@3y!$~Hy!Ny$j zV8W`M)V{T{fDd?ntpv`R3;~lq(?HuC<2z+wTgKzvd$di0J}ki|zhi2H%1zC)uRS59 zih3nAShT}{v8#=~9)w-Sca4JKHwe~$@xI(l((iytZX@j&Bddf6m-l)pI$bS}tm>NR zw>T#AdkwVp@{MHF<$i%H2>=+KhhU*8V+PR9rY2L&lACfe_>Z5xXv9zq()nFReK>+bTz!hk%Q)MGeOZD@?r;1yU5^F7I4SeVR9~`36@<0jn zV+S&0F)zcFTsTK58@F0;Ibo^z3TK3z@h}DYPR)#lW2iY04qw3ftW)fk6N@$d1pe;D zE?I|pw@v$2o~tev6>Gj9W5o;yEmH{0_tlk(3@Cnv6Lb&oMU9n-vwY?0NnbE5J0Iq! z51uWy*2nUH5BlCU!RSF4tTNb@G8*WPl)N$T@T)PnXZ?D#)(ouwef(Cz*eP5-Ga~DP z62>pQP5{AV9F0*g=e$9rP0R>0-cAIbU4}tx6hID#1s-z&#{-`hb+-Z3A~y5feJ%5rFD2;;ymi7Fv;DI z9mWDUzxfC|jH#8;F-yVNDLs0^*wHzvryMU+#^U=yQr0GkxmCu! zmez{h=9a2HY7@=muX9~J3LMa4W(w&LhuH1z_Lq^=czlC-=-+@;ZNRwIChmOIgRl@M zaK%flc)ApFVQNsSg3Cd!U{*i9)aE)+ir3TGKgRZ|Faeuiw+6?Iqimw>7Hs(Fo^RdSgeZ+iV`sORvSy;x zQf_c|`en$wfx~0%J$F<*|CMLiicew0HWU2;5?v1j+mWtVXhQ)Dh&Z5*1yK1Z6RpdX zAp*;V0J_hGfi-r8!yCHdwN*9XZa0Tt;)#jU%Pt37R#wW|?-`#wExeS;YxA&ehUrto z$*Y~r*hZ4x9m2{K>=gcqi3HAt=xu0EoE%GtW8CC`vEBIl1%J4lg5}{;&cW};_Q^8X`kVDTJ6Kk@8N$3#k*8^Gj+g*>lcejGib8!!VY6D&Y-{?HRG49B>maVWYm@6 zq9M?Fu(-Xc7srvhHOTt*P1F^qq^PBlxkE5BRsB#hy(5`v zH|DP)M1KN;G>;c)U0``C#I*F5+)*uJg?PTB>+`Ibluo~4Mt;WbLnp4}`DWzIQ-0wU z0AkGz(y<2A!aa1^2cPN{5H5vkLYKwpUA~d?wK~L477?*?yAY<{tRcf`g z0naxzaBNZxbbO0;ZC5m(w5$P76=DT`=cdsP0q?>;r{zWDRWhB(%mXk5UMp=0;{AUi z841#~P=(7oC?M?00wM<9iA8DCcL!93-@*)xO8E*3t!CAYY5jkp@1nVjpUQrz*kll7k9N7KVl=&prGl!5x6ucE?}2 zIaB~li37LduicF=`xOHIHkgS1fj&BDCE~#o#CWJb$tjYR_0wxZdSzY(Z3lL-{6D+9 z(bDr(*t;?3V8+I1W4w~aXlg^eyHK~-KXDuabbyd<`0a+f>0a8x(huT1*4Ls)zWSS- zST=xsSE>$Cc%kx7EMC+i;re{gousGc1vhjiSN*zARZ(=139j`gW5>1YY4OnA4zG%; z3P&aogOGSn%G@!0CUY@b7lcIYdR)sLHH3_GjRGPV{9;%@{LSg%j%wiUy#Vuw$9q3n zk9r7s4gNk&UVUpm`68tU;X8=@;UJ+GFEIWq}g8cLX?Peikzt)Ja~3S67E!dgff9a3KoB@4@vkb?vyEvbF^;|=^nM4FVxIYP|itp&9Xn(_6WDD-h)AC^P$f!=Q z_<0K%5t4DMg_a<|+J!`nW3EVYJrEMPxoK$6Bzg*WE&{xSDA|ux*L=pPeRs(nQWRM8 z;yAX?J>6`zkvr$CBIBnmAyK7e&g|t^wv_}( z6d{4#5A^z86wlzgqu}j6;~T3TWj?VOd~O$eGa3790V!G^@<+ntN;VgDR}qaPrY|Bn!EgD0mOdOMvWKwFn^$%e3Gw|-6P{6*^D9Qyk>PdJwmp<^#nz+Qc=x(@ zx)${Bq?tVQM+b`g5@C)zTyx>bD*z#J+Jo?M{hEw42#GL_TYAgP;J;_P=R~6QT(Ml} z1DEVoK*U^33p$blSz9ELCjf8k*lf8CyHly( zrcHC-1JZ-hSq9_$DSL$3bSv1L$if`>Pi{tiIHwuvt6Hl_D!fe#GIIXmUZYFPN*jYO znSl99`$y@kfg01fAB1(c2;_r_0@-{GEd{H1OUSRZaL{OsxeIf$?4!q+p6bYswBLg9 z1J1(J>D^G%Ux#lPn+$zW*)*O;@T zyyp5zqyp>a2ZT_O{OS)T(0m)Q{4T>GgjhhtwGi-(^cpqLW#E6Vkk!p2xXcyq!24ie zXGMx9GVX;G=6H9)d*&viAZ!})vNC_2Dh!r9JO-LtTEcQSse&` z{2HSTEqrA8P8Q_?qCjk31w@Wu=T(BD*1kTjz*LkSsT0s& z03V2v2z(n&_k(p#_YNYj)TBW`1mg<*ifuxR%Vrls7}LGmRcftD{weIB*k*n&?BP1f zQ=rJmD_ogZU1<0g=xMKrP(>yh_i|@4%Z=GlQBWsHN^a%@m?WoRot81hn$cm`u!t45 z##zH$&1L-*NSd^|7y6c)kZsBz-)o5<@QBNfroyA>U$ZyDur_}8W!f|-(IeyeYQ(JL zP{2J^C>BCcB)F`dovkR!imFrc?L9)&OLNT=Q|4xL3^^TH%Z#vKzEfHc`ru4)+0g~k zv`7r+KhWbRn|P)@_^q5k=;PM~)rcu@q9`~q=;mhcENkveh|mC~;+Im=xZ+eG#zE4w zP=o^#5OG+tvtOdDmXI$7T}a*;?CLldae=V_Bpu}z;llftE!wqP2hT`PVp8|$KTL@8 z;eo8f?s2`>C}Rpw?q)Kq9Gf4b`KBM7H=!Q}Lp4P=ydp+|SEp(zLxwpXCteOQVe?MpJ6 z$SOG72->115JQr*4mDv$h5b^v5)TchEB9b4TGbWv9^z*X@141v+&f+F<#cI}R^-`7 z29v*P`90vf4#hJq_taAKw1fK2L@XiZEo55lK#xeSJ}Ib5kLON-EmeK&Dzat7X28vah|A+uWa zK4K8cS;WH9%ZbHSv$@f1Uf+E3+Wva*g{zDx(naodK}bw-PYZV3L%`8e0S0zkMR?eI z&l@>&pCU|gMlpX-cpt(jezOL(F?MO3qlrFQjhPZbs#xmcQzoUNg7^s z$D(tX?3YW^l)}&~k5J0jlGm9`hb7(gPdkd)={Q%}&?=TT=Vqy3PjkX8Fmmk@7J_SF zhWa7S<#RLC#aqe9Q9qFMI4y0`3u`}Q9>5)Gd>3(ZYtM~&we(=Dw>MF`5e_v*sx1?# zaM-bU7YdmF!x(a!!QgvtWTwVdDRV~$XPy(ZdoYFYuwU9|U#Dds0gb1mX^wDl719dx zhAIZEFXp6I-d;fJ9CLvms9($O<#b(#rVf;veqk-pM^gbeV9rcpUQsFKxfMoKorx)1 zYqgYhY*+uIfAIaj;)z9l%BkW+IHyr}T;Q;RfCvX(k6uB{m*IQ*Tz&1{A@TmhXfzNxVMrA4^t3^xcl?zh8n4z+SX0YZ{R`N@iZUn=-XZMN4^E4x<-~xsi<0I(j z7;j{NCt9;0EoX(dyhusoo@XH_K7X1OHo`alVj$mnKy4~tv&U7Z@w?%uY*awFd=$nw z8T0@}h8(0NCq!nMd;ISdG z33KQXX?^f&b?rtHI)wtSOSgc?FGYo*R8LB^U-+yMWq*6l>kPbmb{LvI7R6(&(LqZS1hJ~(T*I0fwlx{Bs zLXm_}MLO-8KZM(z@}}(Uo7d(&Y?eOA z5CiVp!TM#&5^1j`_cdlJ~#(Bz&^D$wvINL8E zWLoV}T>Z zyZGY5Ju^Ek8a(f)%Id0$3(W8muK-6t?-DrqQdRjH7rr+$6*$P3>2P)nf|=rTmCl4x z{JSPO>mp|qEtbLtU~I8>x#8My_L~wOA2|bScr6dfqF2-Pr)+8hI^Yg>c%Vc6JTSWb zPwr`D<(@m$!4a3?u3FfmKqm>4xWh@3mMLj2 z?J9h2ZeXxUmU%P{?T>*(-sO>|^Ml%**42DD8WUK0&DcR#g%ICRQFPZJf{Tl|MUN&M zPTkTjDpuHq4X2M3JZyYl@Nv=w~{*T8y#0D}>^iGfhMoCJ2T#z3I@SyM&={ zIXn}Fpsf=lyX69*eW(>Q*BWhU8N3xGK7xm$Y1CF6=$2QFYwcth>%qirDVnDR=}VJ|n$ObVJ|F&YJ*ZvuFK zW;n+bg0>i9icjNF0)Vr227(_Kz1LObLO~-4pruUk=>zd`wbooeX;sidF;`=H-G~xC zHUxH`Uf`wRXl4-YgazElK)`SfVpuMG%vUvt#VrOWSx-w7aCrQAqrlb!iP!NN1hgX} z69;2+f9Padk9?o}FeQ{Qh8sE2kA8Mg+iZtzIX+b;KGOXgFoj*YrmzDEF8Hqx@DL{y zo4P3vtpY?mG{jx<0ng9Owjdc`+0(G&54;FBoIk-*wP^cLTr)E_ej&2wZo{ zm5jKr2Fh0U%^cp}m8Rwfcn<5Jo|-Bsn}1+*$A5h5&^^9s#VLE*s(+6lU`XQX9pu@M zSCE)1g)Y$h>t9DEOz20{cwL4=mLM4&df3$gl{Y3s9%_glwZQW3wp)u@?H`rkFe~hv znVO1qn&MGN2S7vH67@<5M)ei6z zcj5lLR9;z0TQp?uaX7Pi2;n>agW;^W7X8M;h=3K$t2YR-yNY9YYU@SyiK}B^0g>un z8}GL z&{H0Gf--s!@3L!PI^THh49cMkRA?BeG}KVX1C`M${}eg@2R>wr3!Wfo_H@x`+J>s} zad)o5$N68Z7GE&~&I{N?)^`7>_VZlrh54q$lypN0W7uA6^EVtg^-pYGfrkxLk=b;2 zqiJiwK$Oi3jy)DB?l~nl_0AmDN^yDp3$hX~sjox3*n*#3gm&>65LVmtZ%P^>5MpaM zx0xD&uxZdL&WBd<4wMWU=^P&0?KJGuDtNhE6#4Jt_mRbSwoqh>3<9a6tFp?Q zs%dLu&*t6IRb@3-ad-~&IERRLm!5Aq-uB@5E(draewGx@R6cN3&~n4b>DoIkK6Tw0 zb)O0x_>fbu8gX8Dx0hhHH4zq9bAT5N&UCH6&_KQya?{c@B7iyk5(4Q5HT9FPOU7P+ z0Onqe?^Wr8--Ur^0q#r@FW-Lig51+6zgtLJ& z;Crwx?#FxTf_>OaJ+;*X4yDz0kwKby9S>L%50W3aL^_Y2Z-`&RgtU*y_SDLYiF(fi4wzy=FhAyzR)!vI%&we4#JCJp2ugCV=hX1Ci_k8< zi!dR#iK49@9Nj#LHTNSnC@k*~UzEoQqXVE-{2n1Q_9HCHb3cj)R~3Kmm6SDMxv#t!9+g|BI? zuBaZE!+uSN&&2vqRaD-)5F9H(+jSq9fH#p9dK_EQzLA(-#XE=SfK5D-*5#^t+`E6J z@EO{yB%m8f`Uo?4(PAx}Pzt*pRF|+m)@3KnyshDO@s$eykYY|FD2VDRJMcTSU!#0d*EZPB`R`xFLS@X)Nzq{ePo$uaz z5E9Qq(wyGvlU648bh&#<4E5#meI5HA5s3SFnP@d;u5Lz$l=mU5umPVXcG?~14c|t7Sg}GTNirjz z-7|=AngisJDreT=cbts;iMe?spB2_Z7>1|Ykc`9bZ>8n0MY-lA>jYdKPvguYkGY6th}cc+P^kNYb%3QudxKeA z(VM0mffv+Qcx+8I?dH>hcY&-W3_QQ4IeGRKux!Fo3F}1)r)pJ!;cmw#745FhtBUe2ExWU6W zMt@77x)6@N*X$zCe)0{8&cAxxjx#wgjPFY}w#KI+a~TBEStiRTxzY4ZJq?s>M7JGl z-C{5AQ$|ff)!1K`~Qs+2!@Wh|^9zQB7iRLfQWnYm(2GByL_ z<$GYz7dFW`5I}wxG$20!Fn=&}q$B(kkvF!W?UDhrz!Zn~Lfp~Ud5iG9^tMxz z9Nr#iZ!OYneh+K5x)cQ9=AGFj?<1+avYfV-bT;B>!ZaLF_#EraY_t(_`*KE}7$uTh zSBgZig_R^Mo=Qpw#L(EieX>#q_?ufWH{&GozbtEKUM0rhKPjz?4fOi+(0g>R8oS%I zQJgQh0rk|eJrJ1Tv>AOJVeUR9Ntr>L-TUvj|6E4?_z861757qbZ`vkQ$7a;clv}UB zwbpQZl)TM1UAZ8c5u!ve1s)6{-sG);nrbk0edEf}0z}dzNvbT6-p}?TTg#j=?R+?Q zsq%WJ;Vf8>#x!yd;@Q0o#1yL*Hu}E7E|MMyVt*6N7C-YZ%@sfQhL1z^qrB6~$}?PI zEKt=~rt$oWsz&rF%wsMhe8wx#0yku$y*<|bCqNqK^?~jO3MI8+#+XK_=_s_>nUJV< z@>ANC#XXur$nvf$LFEky->Q}3%ulr@%k!7A+5JFTO;QO$wm2B1yPzfgZ3h|BQ?^j) z&R~m@(qG`Q5<#YbbSb_tXI&9fYfJC>sr8&`(!aF6bh>;hT=L5@h}cXe8AJ|kcSmB8 zhpH~8`5(u~o9{0owf?@k?5$=anUT_!eA!PX6b2(*l(fR*VZlkuV(bYp|17>lW+FCw zr1MerFPtPabgz`SFr@|T3qNpAQ+tnZis6_*>gdst4`CfQ=k=@RURDc+aORrt6@bwf zf5qxV)ll3j;0zEG3&y*p=ziNbEA=%99aOi$=M0S5^Dr4q{x&7UgV55wlM(>=5f=3O zUUcK6^_;~+n=DbhR3-Jx8rBtf{9t*_xD_|8R(t*A_`JNN#py}!B2 zC&y#VC(OJa!$Hn-`J6%2c46&59N)3l!g^_IwQmI4ns(LV7Jqoy=t{s1aJI=v%C~n< z)9YRngV^(np5@T1s6d@KbN*0R8AMJv;{(Wb_6zIbcIUm+F9?BqjDqoJOt7R;GPd72 zBD>I$tE{E!hboP`$6%)v4u+S>4t3a!ZOuTeiB_R874_iNPi zl<>oONuz0-+Nx^5loZY$l>R3M?K+bhs9Qj!-FjrE)Cp9swB@%Tb2qmXXFqz9orO14 zmWY?X1n<$q)0N(R#+o!@zLFqSTL%r??_jPTTT7Ds3NS7e=5fu$-&*^E_MWQu?%M0p zz#TRh=RCiXY^&GJh?e=>>0AIzqz-tB**@{?+ zZDiq4R!=rhCIe0=Wbq;1$saP+$f<_35_W0bi1Hf$TqJKyT0~mxIqK2m>WHdiv&RJRTPI1fRXJCj8wba286<`-+=$(AJRpI~74tJ^ng*aU%U@UY5Q0b+n8 zElQgCmOuER%rh|RXd7pV-YiRJ4;^u@37sW3H&7V-H@jcPFLL>1F`avehxrT-an{ja z|Kng(pFB|{L(=TzxCA9Wim(XfwQY*cK{}89P9pEa=-e>xGqN|(*Ko7 zg26Bjv;*XpgcR7~{(ij@MD!&@lYKElE61}ds|{?Bdz+rvcD;P&jgji0gUIjEYy#tX0LN-ViS-??P= z1)Mh=it~mp^|W_6y`On7XJ}?78K`MD)^vAO@F5ujahL^jvld_sdo?HX+~*-MD7e{1 zeV+{grb|AOv%tVgNePjCrMM>`2u)NNuDEB%TFHb@SCXD~5u&1zHDG-h2fal{t!YqK<^;dmnrNnVh{P-3JdxHah z{@6S1-}=nYXi`FkI>#4bIQ=lSvp&F%#Rn3d87hA<*ByZe;tItjLq;GC%g&b)|Wv>d_zL) zi8-U&dz|ZWhY^Za(sieuMKAu(DA7 zZC@$KKj8t#{h^S^Y7kNECNMPW1Xu2`W$pzm@WIQrk|#30Cq}E?Rq8^87>6ctb^E5S zG$OFhS09S+>Q7lyy9YhGGR!EoJM$yw*y&j8i8B>Ftn*l6MeIWj{sdFEkIzb&~CR6U6;1bW!|n! zqt^%sH!1B!hpq-`mA?WPH{U?%f5Qc>KVIlPlcQ{>}H%B>FMnM~CnXW8cZ+o9-Kvu+(O^-pU1R!HgY@ z=Pi*Y;}(8IEf_dj>Ru#&?Zn#h#vd)Yf=LTJe|1!S08T*6?yGfk=3V`zp(RMKHso<9 z60+7kff8dU=QC$?%EiUOewX4LlkbH^b4;|c_BkUvEcVR}P09|ERF%h`<0ft(9BQg* z1smTtRz_Qa5lCuD=E_R$mBKtUvO^AFVtza>6EQ~0O4U6|ma>QC$r*=%7`7_y7LaT0 zz9jlSrRB>&IR;JgS~w;e1poTAc*qP;Cx3>z-&n_8a5#QRZ#opAGKP$Cnf2k|IL-lh zGAU|00}YU=K|_n%002M$Nkl@WIn;^%%rD184D8GL=P z@WNex$?)_G+2WP^IDz798*3Yulk}~`tl{%+&rFKq>Gj*xx*qA078cHW-oGGtE^=+K zce?X=aZ1J+w0qw9?D0$Kb*Jn2WgtC@t9|IWZ9jQs`chKiQogII-C!VJCU+no4(?1W zMhunFF0M@6G@iWvMzb**XV7VD_jIZIB3nSi8L4Vl(+t8mFyte8`S^~RaL-N2OH z)N~Fs6a2=p$G{xEjKwU~Q0< zd{u%Lmd;$8Mmo<-jl|%eaJ5d!ROkJnF`f2bEXd=QHd)RzjlcCw2GfQ{lQuQgSYK1M zE@WLH@Q8;Z2x}HT*dkoKjwZkol{7Jkg1}-VV0k1l%EBr+x!U-it=JS?1Pq8h$D}o! zy0H~CMgEKpM$&u?6Ta1wZ@F#%xP)C-VTJY%2x%~c`#`RAfSQ(okbdOONnlQR->qc! zPBnD{m4%7wCq88;8Nq`^`=pnF1Tg)|%}g;K5Ydm9fnllcVjD6$mxs)N!=f9pn{~vz z%mblmRSif@1xg#h{nwNjTH*LkA!W)oGN=JOi{XrOtbKp=# zKK*7yUVB(l7);PUpL!(;YX=!zxLK}DFzN9}&X70X%O-V?pY;Ho$s^vroV3}HZ3hDRjDds zMykC|2gx9!Uwdt2?>lPSRZ_lpW@>W?P?uop%u33;$}ln;rjI?AJ-AH$gt&kNf5V+x zfMzPz9y0b)Ss~JU%o|t*QG(&DN!s58_K*ym=k%o)f}gUmLg4XHakS@C>{Z;v9Aj2~ zVGqrR&?ylf#y^&-1?Ktpg-J6zBSzXl;NgI{QwoOGv`W!Aq;ZvnB-VTayb#XoTMIwe z3ahNui;H2yfomP+!5I6Q{F8rs(1BdYxo=dvWY7%H;vGV-r1`o|Fw=EBB$8$b8Y*HJ z`N_K)d_vsrofhn~CI%5mBOs#2*_NO-LahTU@$%!+CQ!mUG@7=#pV46007NILp~L1j z-*UF7GmqJ(wOuv!xK;wrJ-!A)nu8Y|r?f5>@+ z9U#q$6)T$`a8D~M_Yk#w1;ngIYIaxR+(8Y>%J{VO6V9AA!W?<9l@-*1VB}To9wA-# zSU=@~494Kl{C*x&F=^E=bI>mSX}cIPaxbPH_$8TSS>W0AL7c=LkM*nOef#eOrIjEw z$OHi~%gI>yuTZ*j9nNbAv}TN)boLB5r!^SgUnH{rlW%T8o zpSnS3Rk?75yf$SCxpntp4_JrlR3pz$=x506+3E4d(>Eu=Q|T&>+dK;Sd9aDm#3p)J zZTTs!e-V7kF3=uBc<2c9CKy?=xoX_sR`$i@}s3qPH z6n4QqUClk#P8ET&AHwO-(L1D{qrZ&Yrl#LY%4%W|;pRNxE(s~JCfG_k9jFWz#1GCg zOS`qGgmY1uc#d|ax&K0A_AZ>!3=Wq^F`R80fdFiKK&0L0-VcBG7|%sc_}bZ?8(~Vi z&|lf0^BiX**caY#K#+{R)-hnb`=sRgo8)u^YNy{SDa*?^B>WNPGQaUD8SiZ*>u;E! zrsc4Lx^T)g1nW%q^I;zS08YR<>YcQ5`zN>VJ|b;~=utuX0pHGTO{Q-iES1}wWK704 z4TO(xM#l^5BE?7}f8Rw0-=!niuz&)gyd|HpRv=DXf-?}@*VU@9?Zt|bIcob5kU@lS zP?kYNV>f`C*siuwwdEr%HuB`>t4Xi#JPj-^my06Hy0v%%?=ur?G2lMt-EN_^vUgHg zo-t<(v3OIS9cnWINxkQ<(JJTi{AEk#1*1U}Dg87Q0!!{>!Ymk8^R+73X^BKWPx$`F zVa-2-Y|TO_3DBC78cy8IwHVLw$~KX@h*nL&!dR(F(|VJ@9CndxlCevo&7Qb?#PWQ} z{4$XE4Y?-*Lcezq>vS{rlg4Vh42EJ$A+z49#)4~Ia#kMJAuwPubkjnHI`ls62g+P664qMw=MAewQXD4(>8d3c8@`Y>NVTii=>Pe&9R9tUkAs zdA-{5dOnke?G~ZK*ceFk!m^PdbVU_}u2=@i#>ZGp%A}!OGE3MOAnxKh;TsAGt<BSP^*u7T3NZrSZ1m0>jTbnS}N<6$>Lo5hM?ASahA1>O>{UonG0lT z3FTlOSh%sW*`nRT6-@tI+~Jf|AE6;#WoOp5_b&+Wbg?{&Jf30PMcYgb%PYLwf8B#+PCSw!Mj49Fm&(te4G#Yv3D1DAxHLFA2b zq(5l+R`U3qABe$rRjJHycn^KhwD$#TW?DQ6n*M(;yZ_A9CTjjX(Eg!KgQ#pv^6ojl zu~VX0!1qvHFOGksY3y<^^2&*Khsit}jJTjvbnQ$Kz>HD!HLSJr`0la~j%mj~v{HY; znbmkO<)frJ3WCJOM4HTFZGORDKq2qDr>v9ouyCG$^f8Sj$h z^j!=0q~8uf#uE@>=qkwc*B{`+n+*oTHaL5F!e6~;J@*hcvSOi7*IQ#D(oiI(ZfX<0Lo{oVUdFYBBN(;*gCSQkvg>sW`mK?9Wl5C=2BUTbzq=s_B`(vOMe%&Lxfp@*WC;wZK`nO3WV8pz;c06ap&*;AH5O zv!2{9B;<$W@<60ChG6a&x%^P1cltSpyzt=)LgA$;*sIW1F{EcGrTT|&aO;uW-(J9( z^@p(T?~*Fxo)&jreHS%r5E&WMnq=us9yd{btSgTP}>5 z+<66Ik8^)$JHwETlAVeqc$pOGx9LWV0a`~k>oyWLE_=0W$7?Ye$ciqv5GH8a?p(dYI@(n} z>UeksN6;|XZEV0tBN|@^0pt6w&iJ{hazVX`X?}DB$%~TQGUZ!h>}!OYQHfXO!8vpK zWexi;#+u}O4*1JR9?XI6&F-K6O~A5YEFhSL_aJP9hKhZ3aC{eitr;X3 zXV9L$7ifa_%BoWu9(Kt;{@l(KqNjd(tV3hR!*a-7kHB zU1aw_aCcO?l>VuB!}muEF~4%}!1_bKmJkguIa`MwYgzW-ba`tQXF>O0cv^?^n#Vvd zj)O2HFrVXIx8xpk8)Xm-Ux%7j#`u2oZXUKV*PcPd*LBri$GeAa&z)~d3ab{8+xu6H zj9mxpcDGwMT(@}P$3-h^zkh7P23KN;Znl#ljSTdr%^*ph0lt+)xZX{pe=8Bsuqn*_ zrlw(qdgq7p8FtyLKoI#5dIkfV5YB$+)fT*n%~AskFYIQ%VXCR~Z!FuIEYAlK_yN3x z#)DBzhJa<8UgP93+lDUvNjo@7Vu&g7ZGQmZ650ld??{~C9f1wr#Sr$rMjGUP*(xSYgA|L#ijz5$W=w|AQN^U5J}FM*&s z>z&S@@m(1|`p|3ut_z7~+PlrmV0>O1*^_K}mC{k~}tt!+7V|g~_=0;wp>FAR+Y&cv=JI?GW0?8R@X1{Au z|JiF5l@C*)M$@-+$6AMQid@Vi1+RsvS>WNtQE=wDAFla2|POOZdviVkZ z^^})pBJtg*b#_z({zc2>5nM$UKsgc>wiEpgC#`K{h$LHp;95&{sKTWy|A?&U_5biZai>O^Bm$E z8xlWtQ~N*waEA9&I}E6=qVa6kBozcLLKY7K^L1tcQXr1BdcJwu-Pc;!ExE`M@d zDqpWOj+7b;Q6B)&KBN>T5W#EYzwX@W5}DwrI>&WDDp zA3C=4Ibcmx43Dz=dS(u3vMhOyD#lVVd>sq+nLV@`H5=*@L97cigX%C9;=&pH(aJDI zsD!OO0?(Xz!Jk7)Y#@kLigHi{Fs^RMLp-NE&;b1J9KNNqIH$|CKz}0`@(H`G=(zK+ zH_U9-1KxU^t-T2h+62@?JWrrPjaO>+X7cRQ{a^x!)urV!(CH*CtrhydV(RvdZrvHQry@-Eb+2Usi{I^>Gwy1!%0{!MX%Z4CRu=G;9;CU6Z1%z7uQg%<}i z(^J(es;Z{=93o@CUnIw-ssbLMFvqhvijfTsfD#9IoK-muZ|Yqa4VV>jfp37W`((#= ztTh7ovnix5e--6gbe$)k#)3>pTsX81WJ0R}Mus_L-{!YC$I0g ze_W?O)Yq4x5LJEOQ2?8SA7>$ z=idb?7fe3)7=-yj5M7v?NnA~HPLK(ot|Z*;IXu%eXhSj%OB4$_X$KilQ=n$P!L?Na z3{trhl-VC>PUiB3S8A^&Gq!Lo;s}opGG>Fvr^Yzcd&DEXlt1u8*)cTcI1mYEl$2de z{Cr{3tcR5q4sj`%fh0CgZHV4C11HoB5~^lsu_-qAx)@Vbh+D^|$qGRDAHwE&0PFl9 zv`&BE-2R@#$@aXPw+fGgQ5=qR&WNhMJ{i!F{Ltt zKu%t2N6r`Az|->gjcg~0XI&LaZm?m0j5X+msCT>S!r|Ax`5o z7!}OG>m)g-rnqA>RxqqN$vP1f933k6&P;6%<;R!U>s#mmE+7cJbFMdiW6_>;Zg%Qd z;2IEE7z-q3rLN^nE^iY3-