From 5807030563930d9640b0015ca1aca938f4ffa4eb Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Thu, 23 Mar 2023 13:19:21 +0100 Subject: [PATCH 01/35] Bump dev versions --- 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 905f1121..8a081a6e 100644 --- a/authorizer-app/package.json +++ b/authorizer-app/package.json @@ -1,6 +1,6 @@ { "name": "authorizer-app", - "version": "4.3.0", + "version": "4.3.1", "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 0dab2002..a6066df9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { allprojects { group = "org.radarbase" - version = "4.3.0" + version = "4.3.1-SNAPSHOT" repositories { mavenCentral() From 51b1c916f93255052168c0250f1ca9809e163ad0 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Thu, 30 Mar 2023 16:06:52 +0200 Subject: [PATCH 02/35] Readability fixes --- .../ManagementPortalEnhancerFactory.kt | 1 + .../OAuth1RestSourceAuthorizationService.kt | 80 ++++++++++--------- 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalEnhancerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalEnhancerFactory.kt index 59c353dc..149006b5 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalEnhancerFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalEnhancerFactory.kt @@ -39,6 +39,7 @@ class ManagementPortalEnhancerFactory(private val config: AuthorizerConfig) : En ), jwtResourceName = config.auth.jwtResourceName, ) + val dbConfig = config.database.copy( managedClasses = listOf( RestSourceUser::class.qualifiedName!!, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt index 6d29fdb7..5870d1d2 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt @@ -126,17 +126,22 @@ abstract class OAuth1RestSourceAuthorizationService( val authConfig = clientService.forSourceType(sourceType) val tokens = this.requestToken(authConfig.preAuthorizationEndpoint, RestOauth1AccessToken(""), sourceType) - val params = mapOf( - OAUTH_ACCESS_TOKEN to tokens?.token, - OAUTH_ACCESS_TOKEN_SECRET to tokens?.tokenSecret, - OAUTH_CALLBACK to config.service.callbackUrl - .newBuilder() - .addQueryParameter("state", state) - .build() - .toString(), - ) - return Url(authConfig.authorizationEndpoint, params).getUrl() + return Url( + authConfig.authorizationEndpoint, + buildMap { + put(OAUTH_ACCESS_TOKEN, tokens?.token) + put(OAUTH_ACCESS_TOKEN_SECRET, tokens?.tokenSecret) + put( + OAUTH_CALLBACK, + config.service.callbackUrl + .newBuilder() + .addQueryParameter("state", state) + .build() + .toString(), + ) + }, + ).getUrl() } private fun requestToken( @@ -183,16 +188,21 @@ abstract class OAuth1RestSourceAuthorizationService( val accessToken = user.accessToken ?: throw HttpBadRequestException("access-token-not-found", "No access token available for user") - val signedParams = payload.parameters.toMutableMap() - signedParams[OAUTH_ACCESS_TOKEN] = accessToken - signedParams[OAUTH_SIGNATURE_METHOD] = OAUTH_SIGNATURE_METHOD_VALUE - signedParams[OAUTH_SIGNATURE] = OauthSignature( - payload.url, - signedParams.toSortedMap(), - payload.method, - authConfig.clientSecret, - user.refreshToken, - ).getEncodedSignature() + val signedParams = buildMap(payload.parameters.size + 3) { + putAll(payload.parameters) + put(OAUTH_ACCESS_TOKEN, accessToken) + put(OAUTH_SIGNATURE_METHOD, OAUTH_SIGNATURE_METHOD_VALUE) + put( + OAUTH_SIGNATURE, + OauthSignature( + payload.url, + toSortedMap(), + payload.method, + authConfig.clientSecret, + user.refreshToken, + ).getEncodedSignature(), + ) + } return SignRequestParams(payload.url, payload.method, signedParams) } @@ -219,27 +229,23 @@ abstract class OAuth1RestSourceAuthorizationService( private fun parseParams(input: String): String { val params = input - .replace("=".toRegex(), "\":\"") - .replace("&".toRegex(), "\",\"") + .replace("=", "\":\"") + .replace("&", "\",\"") return "{\"$params\"}" } - private fun RestOauth1AccessToken.toOAuth2(sourceType: String): RestOauth2AccessToken { - // This maps the OAuth1 properties to OAuth2 for backwards compatibility in the repository - // Also, an additional request for getting the external ID is made here to pull the external id - val tokens = this - return RestOauth2AccessToken( - tokens.token, - tokens.tokenSecret, - Integer.MAX_VALUE, - "", - tokens.getExternalId(sourceType), - ) - } + // This maps the OAuth1 properties to OAuth2 for backwards compatibility in the repository + // Also, an additional request for getting the external ID is made here to pull the external id + private fun RestOauth1AccessToken.toOAuth2(sourceType: String) = RestOauth2AccessToken( + token, + tokenSecret, + Integer.MAX_VALUE, + "", + getExternalId(sourceType), + ) - private fun Map.toFormattedHeader(): String = this - .map { (k, v) -> "$k=\"$v\"" } - .joinToString() + private fun Map.toFormattedHeader(): String = + entries.joinToString { (k, v) -> "$k=\"$v\"" } abstract fun RestOauth1AccessToken.getExternalId(sourceType: String): String? From a43e0cde225d5148698862d5be7f6a275c3bf19a Mon Sep 17 00:00:00 2001 From: mpgxvii Date: Wed, 10 May 2023 17:17:26 +0100 Subject: [PATCH 03/35] Store auth params in authorization page as well --- .../authorization-page/authorization-page.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/authorizer-app/src/app/shared/containers/authorization-page/authorization-page.component.ts b/authorizer-app/src/app/shared/containers/authorization-page/authorization-page.component.ts index 3d528177..f9577a1d 100644 --- a/authorizer-app/src/app/shared/containers/authorization-page/authorization-page.component.ts +++ b/authorizer-app/src/app/shared/containers/authorization-page/authorization-page.component.ts @@ -37,6 +37,7 @@ export class AuthorizationPageComponent implements OnInit { this.project = resp.project.id; this.authEndpointUrl = resp.authEndpointUrl; this.isLoading = false; + this.userService.storeUserAuthParams(resp.authEndpointUrl); } }, error: (error) => { From 0b05de8ee2ebd27e6d2b929588fd66617b868e82 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Tue, 23 May 2023 16:56:25 +0200 Subject: [PATCH 04/35] Update to latest radar-auth --- authorizer-app-backend/Dockerfile | 3 +- authorizer-app-backend/build.gradle.kts | 77 ++------ .../authorizer/api/ApiDeclarations.kt | 35 ++-- .../authorizer/api/RestSourceUserMapper.kt | 7 +- .../config/AuthorizerServiceConfig.kt | 27 ++- .../authorizer/doa/RegistrationRepository.kt | 18 +- .../doa/RestSourceUserRepository.kt | 18 +- .../doa/RestSourceUserRepositoryImpl.kt | 22 ++- .../lifecycle/RegistrationLifecycleManager.kt | 55 +++--- .../authorizer/resources/ProjectResource.kt | 34 ++-- .../resources/RegistrationResource.kt | 46 +++-- .../resources/RestSourceUserResource.kt | 74 +++++--- .../resources/SourceClientResource.kt | 42 ++++- ...DelegatedRestSourceAuthorizationService.kt | 12 +- .../GarminSourceAuthorizationService.kt | 46 ++--- .../authorizer/service/LockService.kt | 2 +- .../OAuth1RestSourceAuthorizationService.kt | 174 +++++++++--------- .../OAuth2RestSourceAuthorizationService.kt | 116 ++++++------ .../authorizer/service/RedisLockService.kt | 22 ++- .../authorizer/service/RegistrationService.kt | 6 +- .../service/RestSourceAuthorizationService.kt | 37 +++- .../service/RestSourceClientService.kt | 3 +- .../service/RestSourceUserService.kt | 71 +++---- .../authorizer/util/OauthSignature.kt | 5 +- .../authorizer/AuthorizerServiceConfigTest.kt | 8 +- build.gradle.kts | 69 ++----- buildSrc/build.gradle.kts | 21 +++ buildSrc/src/main/kotlin/Versions.kt | 23 +++ gradle.properties | 31 +--- gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 +- settings.gradle.kts | 18 +- 33 files changed, 612 insertions(+), 519 deletions(-) create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/kotlin/Versions.kt diff --git a/authorizer-app-backend/Dockerfile b/authorizer-app-backend/Dockerfile index 4f322aa9..6180e1b2 100644 --- a/authorizer-app-backend/Dockerfile +++ b/authorizer-app-backend/Dockerfile @@ -10,7 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM --platform=$BUILDPLATFORM gradle:7.6-jdk17 as builder +FROM --platform=$BUILDPLATFORM gradle:8.1-jdk17 as builder RUN mkdir /code WORKDIR /code @@ -19,6 +19,7 @@ ENV GRADLE_USER_HOME=/code/.gradlecache \ GRADLE_OPTS="-Djdk.lang.Process.launchMechanism=vfork -Dorg.gradle.vfs.watch=false" COPY ./build.gradle.kts ./settings.gradle.kts ./gradle.properties /code/ +COPY ./buildSrc /code/buildSrc COPY authorizer-app-backend/build.gradle.kts /code/authorizer-app-backend/ RUN gradle downloadDependencies copyDependencies startScripts diff --git a/authorizer-app-backend/build.gradle.kts b/authorizer-app-backend/build.gradle.kts index 8665aa18..ec13d552 100644 --- a/authorizer-app-backend/build.gradle.kts +++ b/authorizer-app-backend/build.gradle.kts @@ -1,9 +1,7 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { application kotlin("jvm") + kotlin("plugin.serialization") version Versions.kotlin id("org.jetbrains.kotlin.plugin.noarg") id("org.jetbrains.kotlin.plugin.jpa") id("org.jetbrains.kotlin.plugin.allopen") @@ -13,74 +11,35 @@ application { mainClass.set("org.radarbase.authorizer.Main") applicationDefaultJvmArgs = listOf( "-Djava.security.egd=file:/dev/./urandom", - "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager", ) } -repositories { - maven(url = "https://oss.sonatype.org/content/repositories/snapshots") -} - dependencies { api(kotlin("stdlib-jdk8")) implementation(kotlin("reflect")) - val radarJerseyVersion: String by project - implementation("org.radarbase:radar-jersey:$radarJerseyVersion") - implementation("org.radarbase:radar-jersey-hibernate:$radarJerseyVersion") { - val postgresVersion: String by project - runtimeOnly("org.postgresql:postgresql:$postgresVersion") + implementation("org.radarbase:radar-commons-kotlin:${Versions.radarCommons}") + implementation("org.radarbase:radar-jersey:${Versions.radarJersey}") + implementation("org.radarbase:radar-jersey-hibernate:${Versions.radarJersey}") { + runtimeOnly("org.postgresql:postgresql:${Versions.postgres}") } - val slf4jVersion: String by project - implementation("org.slf4j:slf4j-api:$slf4jVersion") - - val okhttpVersion: String by project - implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") + implementation("redis.clients:jedis:${Versions.jedis}") - val jedisVersion: String by project - implementation("redis.clients:jedis:$jedisVersion") + implementation(platform("io.ktor:ktor-bom:${Versions.ktor}")) + implementation("io.ktor:ktor-client-core") + implementation("io.ktor:ktor-client-auth") + implementation("io.ktor:ktor-client-cio") + implementation("io.ktor:ktor-client-content-negotiation") + implementation("io.ktor:ktor-serialization-kotlinx-json") - val log4j2Version: String by project - runtimeOnly("org.apache.logging.log4j:log4j-core:$log4j2Version") - runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:$log4j2Version") - runtimeOnly("org.apache.logging.log4j:log4j-jul:$log4j2Version") - - val junitVersion: String by project - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") - testImplementation("org.hamcrest:hamcrest-all:1.3") - - val mockitoKotlinVersion: String by project - testImplementation("org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion") - - val jerseyVersion: String by project - testImplementation("org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2:$jerseyVersion") -} - -tasks.withType { - kotlinOptions { - jvmTarget = "17" - apiVersion = "1.7" - languageVersion = "1.7" - } -} - -tasks.withType { - options.release.set(17) -} - -tasks.withType { - useJUnitPlatform() - testLogging { - events("passed", "skipped", "failed") - showStandardStreams = true - exceptionFormat = FULL - } - systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager") + testImplementation("org.hamcrest:hamcrest:${Versions.hamcrest}") + testImplementation("org.mockito.kotlin:mockito-kotlin:${Versions.mockitoKotlin}") + testImplementation("org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2:${Versions.jersey}") } allOpen { - annotation("javax.persistence.Entity") - annotation("javax.persistence.MappedSuperclass") - annotation("javax.persistence.Embeddable") + annotation("jakarta.persistence.Entity") + annotation("jakarta.persistence.MappedSuperclass") + annotation("jakarta.persistence.Embeddable") } 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 80f81419..357a6e55 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 @@ -16,27 +16,38 @@ package org.radarbase.authorizer.api -import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import com.fasterxml.jackson.annotation.JsonProperty +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import java.time.Instant -@JsonIgnoreProperties(ignoreUnknown = true) +@Serializable data class RestOauth2AccessToken( - @JsonProperty("access_token") val accessToken: String, - @JsonProperty("refresh_token") val refreshToken: String? = null, - @JsonProperty("expires_in") val expiresIn: Int = 0, - @JsonProperty("token_type") val tokenType: String? = null, - @JsonProperty("user_id") val externalUserId: String? = null, + @SerialName("access_token") + val accessToken: String, + @SerialName("refresh_token") + val refreshToken: String? = null, + @SerialName("expires_in") + val expiresIn: Int = 0, + @SerialName("token_type") + val tokenType: String? = null, + @SerialName("user_id") + val externalUserId: String? = null, ) +@Serializable data class RestOauth1AccessToken( - @JsonProperty("oauth_token") val token: String, - @JsonProperty("oauth_token_secret") val tokenSecret: String? = null, - @JsonProperty("oauth_verifier") val tokenVerifier: String? = null, + @SerialName("oauth_token") + val token: String, + @SerialName("oauth_token_secret") + val tokenSecret: String? = null, + @SerialName("oauth_verifier") + val tokenVerifier: String? = null, ) +@Serializable data class RestOauth1UserId( - @JsonProperty("userId") val userId: String, + @SerialName("userId") + val userId: String, ) data class SignRequestParams( 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 13cce8a2..eedab90b 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 @@ -19,11 +19,12 @@ package org.radarbase.authorizer.api import jakarta.ws.rs.core.Context import org.radarbase.authorizer.doa.entity.RestSourceUser import org.radarbase.jersey.service.managementportal.RadarProjectService +import org.radarbase.kotlin.coroutines.forkJoin class RestSourceUserMapper( @Context private val projectService: RadarProjectService, ) { - fun fromEntity(user: RestSourceUser): RestSourceUserDTO { + suspend fun fromEntity(user: RestSourceUser): RestSourceUserDTO { val mpUser = user.projectId?.let { p -> user.userId?.let { u -> projectService.subject(p, u) } } @@ -48,8 +49,8 @@ class RestSourceUserMapper( ) } - fun fromRestSourceUsers(records: List, page: Page?) = RestSourceUsers( - users = records.map(::fromEntity), + suspend fun fromRestSourceUsers(records: List, page: Page?) = RestSourceUsers( + users = records.forkJoin { fromEntity(it) }, metadata = page, ) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt index ec211e14..3afe9376 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt @@ -1,7 +1,6 @@ package org.radarbase.authorizer.config -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import io.ktor.http.* import org.radarbase.authorizer.enhancer.ManagementPortalEnhancerFactory import org.radarbase.jersey.enhancer.EnhancerFactory import java.net.URI @@ -18,26 +17,22 @@ data class AuthorizerServiceConfig( val tokenExpiryTimeInMinutes: Long = 15, val persistentTokenExpiryInMin: Long = 3.days.inWholeMinutes, ) { - val callbackUrl: HttpUrl by lazy { + val callbackUrl: Url by lazy { val frontendBaseUrlBuilder = when { - frontendBaseUri != null -> frontendBaseUri.toHttpUrlOrNull()?.newBuilder() + frontendBaseUri != null -> URLBuilder().takeFrom(frontendBaseUri) advertisedBaseUri != null -> { - advertisedBaseUri.toHttpUrlOrNull()?.let { advertisedUrl -> - advertisedUrl.newBuilder().apply { - advertisedUrl.pathSegments.asReversed() - .forEachIndexed { idx, segment -> - if (segment.isEmpty() || segment == "backend") { - removePathSegment(advertisedUrl.pathSize - 1 - idx) - } - } - addPathSegment("authorizer") + URLBuilder().apply { + takeFrom(advertisedBaseUri) + pathSegments = buildList(pathSegments.size) { + addAll(pathSegments.dropLastWhile { it.isEmpty() || it == "backend" }) + add("authorizer") } } } - else -> null + else -> throw IllegalStateException("Frontend URL parameter is not a valid HTTP URL.") } - checkNotNull(frontendBaseUrlBuilder) { "Frontend URL parameter $frontendBaseUri is not a valid HTTP URL." } - .addPathSegment("users:new") + frontendBaseUrlBuilder + .appendPathSegments("users:new") .build() } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RegistrationRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RegistrationRepository.kt index 669be649..0c541101 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RegistrationRepository.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RegistrationRepository.kt @@ -11,18 +11,20 @@ import org.radarbase.authorizer.util.Hmac256Secret import org.radarbase.authorizer.util.Hmac256Secret.Companion.encodeToBase64 import org.radarbase.authorizer.util.Hmac256Secret.Companion.randomize import org.radarbase.jersey.hibernate.HibernateRepository +import org.radarbase.jersey.service.AsyncCoroutineService import java.time.Instant import kotlin.time.Duration.Companion.minutes class RegistrationRepository( @Context private val config: AuthorizerConfig, @Context em: Provider, -) : HibernateRepository(em) { + @Context asyncService: AsyncCoroutineService, +) : HibernateRepository(em, asyncService) { private val tokenExpiryTime = config.service.tokenExpiryTimeInMinutes.minutes private val persistentTokenExpiryTime = config.service.persistentTokenExpiryInMin.minutes - fun generate( + suspend fun generate( user: RestSourceUser, secret: Hmac256Secret?, persistent: Boolean, @@ -53,11 +55,11 @@ class RegistrationRepository( } } - operator fun get(token: String): RegistrationState? = transact { + suspend fun get(token: String): RegistrationState? = transact { find(RegistrationState::class.java, token) } - fun cleanUp(): Int = transact { + suspend fun cleanUp(): Int = transact { val cb = criteriaBuilder // create delete @@ -70,15 +72,11 @@ class RegistrationRepository( createQuery(deleteQuery).executeUpdate() } - operator fun minusAssign(token: String) = remove(token) - - operator fun minusAssign(registrationState: RegistrationState) = remove(registrationState) - - fun remove(registrationState: RegistrationState): Unit = transact { + suspend fun remove(registrationState: RegistrationState): Unit = transact { remove(registrationState) } - fun remove(token: String): Unit = transact { + suspend fun remove(token: String): Unit = transact { val state = find(RegistrationState::class.java, token) remove(state) } 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 008b5a15..d9513cf5 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,11 +23,11 @@ import org.radarbase.authorizer.doa.entity.RestSourceUser import java.time.Instant interface RestSourceUserRepository { - fun create(user: RestSourceUserDTO): RestSourceUser - fun updateToken(token: RestOauth2AccessToken?, user: RestSourceUser): RestSourceUser - fun read(id: Long): RestSourceUser? - fun update(userId: Long, user: RestSourceUserDTO): RestSourceUser - fun query( + suspend fun create(user: RestSourceUserDTO): RestSourceUser + suspend fun updateToken(token: RestOauth2AccessToken?, user: RestSourceUser): RestSourceUser + suspend fun read(id: Long): RestSourceUser? + suspend fun update(userId: Long, user: RestSourceUserDTO): RestSourceUser + suspend fun query( page: Page, projectIds: List, sourceType: String? = null, @@ -35,8 +35,8 @@ interface RestSourceUserRepository { userIds: List, isAuthorized: Boolean?, ): Pair, Page> - fun queryAllWithElapsedEndDate(sourceType: String? = null): List - fun delete(user: RestSourceUser) - fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?): RestSourceUser - fun findByExternalId(externalId: String, sourceType: String): RestSourceUser? + suspend fun queryAllWithElapsedEndDate(sourceType: String? = null): List + suspend fun delete(user: RestSourceUser) + suspend fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?): RestSourceUser + suspend fun findByExternalId(externalId: String, sourceType: String): 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 a1b87b03..5deb8e88 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 @@ -26,6 +26,7 @@ import org.radarbase.authorizer.doa.entity.RestSourceUser import org.radarbase.jersey.exception.HttpConflictException import org.radarbase.jersey.exception.HttpNotFoundException import org.radarbase.jersey.hibernate.HibernateRepository +import org.radarbase.jersey.service.AsyncCoroutineService import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit @@ -33,9 +34,10 @@ import java.util.* class RestSourceUserRepositoryImpl( @Context em: Provider, -) : RestSourceUserRepository, HibernateRepository(em) { + @Context asyncService: AsyncCoroutineService, +) : RestSourceUserRepository, HibernateRepository(em, asyncService) { - override fun create(user: RestSourceUserDTO): RestSourceUser = transact { + override suspend fun create(user: RestSourceUserDTO): RestSourceUser = transact { val existingUser = createQuery( """ SELECT u @@ -86,16 +88,16 @@ class RestSourceUserRepositoryImpl( } } - override fun updateToken(token: RestOauth2AccessToken?, user: RestSourceUser): RestSourceUser = transact { + override suspend fun updateToken(token: RestOauth2AccessToken?, user: RestSourceUser): RestSourceUser = transact { user.apply { setToken(token) merge(this) } } - override fun read(id: Long): RestSourceUser? = transact { find(RestSourceUser::class.java, id) } + override suspend fun read(id: Long): RestSourceUser? = transact { find(RestSourceUser::class.java, id) } - override fun update(userId: Long, user: RestSourceUserDTO): RestSourceUser = transact { + override suspend fun update(userId: Long, user: RestSourceUserDTO): RestSourceUser = transact { val existingUser = find(RestSourceUser::class.java, userId) ?: throw HttpNotFoundException("user_not_found", "User with ID $userId not found") @@ -108,7 +110,7 @@ class RestSourceUserRepositoryImpl( } } - override fun query( + override suspend fun query( page: Page, projectIds: List, sourceType: String?, @@ -165,7 +167,7 @@ class RestSourceUserRepositoryImpl( } } - override fun queryAllWithElapsedEndDate( + override suspend fun queryAllWithElapsedEndDate( sourceType: String?, ): List { var queryString = """ @@ -188,7 +190,7 @@ class RestSourceUserRepositoryImpl( } } - override fun findByExternalId( + override suspend fun findByExternalId( externalId: String, sourceType: String, ): RestSourceUser? { @@ -209,11 +211,11 @@ class RestSourceUserRepositoryImpl( return if (result.isEmpty()) null else result[0] } - override fun delete(user: RestSourceUser) = transact { + override suspend fun delete(user: RestSourceUser) = transact { remove(merge(user)) } - override fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?) = transact { + override suspend 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/lifecycle/RegistrationLifecycleManager.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/lifecycle/RegistrationLifecycleManager.kt index b1000291..253c0fb3 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/lifecycle/RegistrationLifecycleManager.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/lifecycle/RegistrationLifecycleManager.kt @@ -4,6 +4,10 @@ import jakarta.inject.Singleton import jakarta.persistence.EntityManagerFactory import jakarta.ws.rs.core.Context import jakarta.ws.rs.ext.Provider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import org.glassfish.jersey.server.BackgroundScheduler import org.glassfish.jersey.server.monitoring.ApplicationEvent import org.glassfish.jersey.server.monitoring.ApplicationEventListener @@ -11,6 +15,7 @@ import org.glassfish.jersey.server.monitoring.RequestEvent import org.glassfish.jersey.server.monitoring.RequestEventListener import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.RegistrationRepository +import org.radarbase.jersey.service.AsyncCoroutineService import org.slf4j.LoggerFactory import java.time.Duration import java.util.concurrent.Future @@ -25,10 +30,11 @@ class RegistrationLifecycleManager( private val executor: ScheduledExecutorService, @Context private val entityManagerFactory: EntityManagerFactory, @Context private val config: AuthorizerConfig, + @Context private val asyncService: AsyncCoroutineService, ) : ApplicationEventListener { private val expiryTime = Duration.ofMinutes(config.service.tokenExpiryTimeInMinutes) .coerceAtLeast(Duration.ofMinutes(1)) - private val lock: Any = Any() + private val mutex = Mutex() private var checkTask: Future<*>? = null @@ -64,30 +70,37 @@ class RegistrationLifecycleManager( } private fun runStaleCheck() { - try { - val entityManager = entityManagerFactory.createEntityManager() + asyncService.runBlocking { try { - val registrationRepository = RegistrationRepository( - config, - em = { entityManager }, - ) - synchronized(lock) { - logger.debug("Cleaning up expired registrations.") - val numUpdated = registrationRepository.cleanUp() - if (numUpdated == 0) { - logger.debug("Did not clean up any registrations.") - } else { - logger.info("Removed {} expired registrations.", numUpdated) + val entityManager = withContext(Dispatchers.IO) { + entityManagerFactory.createEntityManager() + } + try { + val registrationRepository = RegistrationRepository( + config, + em = { entityManager }, + asyncService, + ) + mutex.withLock { + logger.debug("Cleaning up expired registrations.") + val numUpdated = registrationRepository.cleanUp() + if (numUpdated == 0) { + logger.debug("Did not clean up any registrations.") + } else { + logger.info("Removed {} expired registrations.", numUpdated) + } + } + } catch (ex: Exception) { + logger.error("Failed to run reset of stale processing", ex) + } finally { + withContext(Dispatchers.IO) { + entityManager.close() } } - } catch (ex: Exception) { - logger.error("Failed to run reset of stale processing", ex) - } finally { - entityManager.close() + } catch (ex: Throwable) { + // catch all exceptions to ensure that this job keeps repeating + logger.error("Failed to run reset of stale processing.", ex) } - } catch (ex: Throwable) { - // catch all exceptions to ensure that this job keeps repeating - logger.error("Failed to run reset of stale processing.", ex) } } 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 b2e7dd7e..09743801 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 @@ -19,15 +19,17 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource import jakarta.inject.Singleton import jakarta.ws.rs.* +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION import jakarta.ws.rs.core.MediaType import org.radarbase.auth.authorization.Permission 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.cache.Cache +import org.radarbase.jersey.service.AsyncCoroutineService import org.radarbase.jersey.service.managementportal.RadarProjectService @Path("projects") @@ -38,16 +40,18 @@ import org.radarbase.jersey.service.managementportal.RadarProjectService @Singleton class ProjectResource( @Context private val projectService: RadarProjectService, - @Context private val auth: Auth, + @Context private val asyncService: AsyncCoroutineService, ) { @GET @NeedsPermission(Permission.PROJECT_READ) @Cache(maxAge = 300, isPrivate = true, vary = [AUTHORIZATION]) - fun projects() = ProjectList( - projectService.userProjects(auth) - .map { it.toProject() }, - ) + fun projects(@Suspended asyncResponse: AsyncResponse) = asyncService.runAsCoroutine(asyncResponse) { + ProjectList( + projectService.userProjects() + .map { it.toProject() }, + ) + } @GET @Path("{projectId}/users") @@ -55,16 +59,22 @@ class ProjectResource( @Cache(maxAge = 60, isPrivate = true, vary = [AUTHORIZATION]) fun users( @PathParam("projectId") projectId: String, - ) = UserList( - projectService.projectSubjects(projectId) - .map { it.toUser() }, - ) + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { + UserList( + projectService.projectSubjects(projectId) + .map { it.toUser() }, + ) + } @GET @Path("{projectId}") @NeedsPermission(Permission.PROJECT_READ, "projectId") @Cache(maxAge = 300, isPrivate = true, vary = [AUTHORIZATION]) - fun project(@PathParam("projectId") projectId: String): Project { - return projectService.project(projectId).toProject() + fun project( + @PathParam("projectId") projectId: String, + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { + projectService.project(projectId).toProject() } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt index b72e581e..a9e918d0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt @@ -3,9 +3,12 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource import jakarta.inject.Singleton import jakarta.ws.rs.* +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.core.Response +import org.radarbase.auth.authorization.EntityDetails import org.radarbase.auth.authorization.Permission import org.radarbase.authorizer.api.* import org.radarbase.authorizer.doa.RegistrationRepository @@ -14,11 +17,12 @@ import org.radarbase.authorizer.service.RegistrationService import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceUserService import org.radarbase.authorizer.util.Hmac256Secret -import org.radarbase.jersey.auth.Auth +import org.radarbase.jersey.auth.AuthService 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.service.AsyncCoroutineService import org.radarbase.jersey.service.managementportal.RadarProjectService import java.net.URI @@ -34,16 +38,24 @@ class RegistrationResource( @Context private val userRepository: RestSourceUserRepository, @Context private val registrationService: RegistrationService, @Context private val projectService: RadarProjectService, + @Context private val authService: AuthService, + @Context private val asyncService: AsyncCoroutineService, ) { @POST @Authenticated @NeedsPermission(Permission.SUBJECT_UPDATE) fun createState( - @Context auth: Auth, createState: StateCreateDTO, - ): Response { + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { val user = restSourceUserService.ensureUser(createState.userId.toLong()) - auth.checkPermissionOnSubject(Permission.SUBJECT_UPDATE, user.projectId, user.userId) + authService.checkPermission( + Permission.SUBJECT_UPDATE, + EntityDetails( + project = user.projectId, + subject = user.userId, + ), + ) var tokenState = registrationService.generate(user, createState.persistent) if (!createState.persistent) { tokenState = tokenState.copy( @@ -54,7 +66,7 @@ class RegistrationResource( ), ) } - return Response.created(URI("tokens/${tokenState.token}")) + Response.created(URI("tokens/${tokenState.token}")) .entity(tokenState) .build() } @@ -63,9 +75,10 @@ class RegistrationResource( @Path("{token}") fun state( @PathParam("token") token: String, - ): RegistrationResponse { + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { val registration = registrationService.ensureRegistration(token) - return RegistrationResponse( + RegistrationResponse( token = registration.token, userId = registration.user.id!!.toString(), createdAt = registration.createdAt, @@ -81,9 +94,10 @@ class RegistrationResource( @Path("{token}") fun deleteState( @PathParam("token") token: String, - ): Response { - registrationRepository -= token - return Response.noContent().build() + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { + registrationRepository.remove(token) + Response.noContent().build() } @POST @@ -91,7 +105,8 @@ class RegistrationResource( fun authEndpoint( @PathParam("token") token: String, tokenSecret: TokenSecret, - ): RegistrationResponse { + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { val registration = registrationService.ensureRegistration(token) if (registration.user.authorized) throw HttpConflictException("user_already_authorized", "User was already authorized for this service.") val salt = registration.salt @@ -103,7 +118,7 @@ class RegistrationResource( projectService.project(it).toProject() } - return RegistrationResponse( + RegistrationResponse( token = registration.token, authEndpointUrl = authorizationService.getAuthorizationEndpointWithParams( sourceType = registration.user.sourceType, @@ -125,7 +140,8 @@ class RegistrationResource( fun addAccount( @PathParam("token") token: String, payload: RequestTokenPayload, - ): Response { + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { val registration = registrationService.ensureRegistration(token) val accessToken = authorizationService.requestAccessToken(payload, registration.user.sourceType) val user = userRepository.updateToken(accessToken, registration.user) @@ -143,9 +159,9 @@ class RegistrationResource( sourceType = registration.user.sourceType, ) - registrationRepository -= registration + registrationRepository.remove(registration) - return Response.created(URI("source-clients/${user.sourceType}/authorization/${user.externalUserId}")) + Response.created(URI("source-clients/${user.sourceType}/authorization/${user.externalUserId}")) .entity(tokenEntity) .build() } 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 fd5f6cd5..c29ef035 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,21 +19,25 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource import jakarta.inject.Singleton import jakarta.ws.rs.* +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.HttpHeaders import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.core.Response +import org.radarbase.auth.authorization.EntityDetails import org.radarbase.auth.authorization.Permission import org.radarbase.authorizer.api.* import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceClientService import org.radarbase.authorizer.service.RestSourceUserService -import org.radarbase.jersey.auth.Auth +import org.radarbase.jersey.auth.AuthService import org.radarbase.jersey.auth.Authenticated import org.radarbase.jersey.auth.NeedsPermission import org.radarbase.jersey.cache.Cache import org.radarbase.jersey.exception.HttpBadRequestException +import org.radarbase.jersey.service.AsyncCoroutineService import org.radarbase.jersey.service.managementportal.RadarProjectService import java.net.URI @@ -47,10 +51,11 @@ class RestSourceUserResource( @Context private val userRepository: RestSourceUserRepository, @Context private val userMapper: RestSourceUserMapper, @Context private val projectService: RadarProjectService, - @Context private val auth: Auth, @Context private val authorizationService: RestSourceAuthorizationService, @Context private val sourceClientService: RestSourceClientService, @Context private val userService: RestSourceUserService, + @Context private val asyncService: AsyncCoroutineService, + @Context private val authService: AuthService, ) { @GET @NeedsPermission(Permission.SUBJECT_READ) @@ -67,22 +72,21 @@ class RestSourceUserResource( @DefaultValue("1") @QueryParam("page") pageNumber: Int, - ): RestSourceUsers { + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { val projectIds = if (projectId == null) { - projectService.userProjects(auth, Permission.SUBJECT_READ) - .also { projects -> - if (projects.isEmpty()) return emptyUsers(pageNumber, pageSize) - } + projectService.userProjects(Permission.SUBJECT_READ) + .also { projects -> if (projects.isEmpty()) return@runAsCoroutine emptyUsers(pageNumber, pageSize) } .map { it.id } } else { - auth.checkPermissionOnProject(Permission.SUBJECT_READ, projectId) + authService.checkPermission(Permission.SUBJECT_READ, EntityDetails(project = projectId)) listOf(projectId) } val sanitizedSourceType = when (sourceType) { null -> null in sourceClientService -> sourceType - else -> return emptyUsers(pageNumber, pageSize) + else -> return@runAsCoroutine emptyUsers(pageNumber, pageSize) } val sanitizedSearch = search?.takeIf { it.length >= 2 } @@ -117,17 +121,18 @@ class RestSourceUserResource( authorizedBoolean, ) - return userMapper.fromRestSourceUsers(records, page) + userMapper.fromRestSourceUsers(records, page) } @POST @NeedsPermission(Permission.SUBJECT_CREATE) fun create( userDto: RestSourceUserDTO, - ): Response { + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { val user = userService.create(userDto) - return Response.created(URI("users/${user.id}")) + Response.created(URI("users/${user.id}")) .entity(user) .build() } @@ -138,20 +143,33 @@ class RestSourceUserResource( fun update( @PathParam("id") userId: Long, user: RestSourceUserDTO, - ): RestSourceUserDTO = userService.update(userId, user) + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { + userService.update(userId, user) + } @GET @Path("{id}") @NeedsPermission(Permission.SUBJECT_READ) @Cache(maxAge = 300, isPrivate = true, vary = [HttpHeaders.AUTHORIZATION]) - fun readUser(@PathParam("id") userId: Long): RestSourceUserDTO = userService.get(userId) + fun readUser( + @PathParam("id") userId: Long, + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { + userService.get(userId) + } @DELETE @Path("{id}") @NeedsPermission(Permission.SUBJECT_UPDATE) - fun deleteUser(@PathParam("id") userId: Long): Response { + fun deleteUser( + @PathParam("id") userId: Long, + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { userService.delete(userId) - return Response.noContent().header("user-removed", userId).build() + Response.noContent() + .header("user-removed", userId) + .build() } @POST @@ -160,17 +178,30 @@ class RestSourceUserResource( fun reset( @PathParam("id") userId: Long, user: RestSourceUserDTO, - ): RestSourceUserDTO = userService.reset(userId, user) + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { + userService.reset(userId, user) + } @GET @Path("{id}/token") @NeedsPermission(Permission.MEASUREMENT_CREATE) - fun requestToken(@PathParam("id") userId: Long): TokenDTO = userService.ensureToken(userId) + fun requestToken( + @PathParam("id") userId: Long, + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { + userService.ensureToken(userId) + } @POST @Path("{id}/token") @NeedsPermission(Permission.MEASUREMENT_CREATE) - fun refreshToken(@PathParam("id") userId: Long): TokenDTO = userService.refreshToken(userId) + fun refreshToken( + @PathParam("id") userId: Long, + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { + userService.refreshToken(userId) + } @POST @Path("{id}/token/sign") @@ -178,9 +209,10 @@ class RestSourceUserResource( fun signRequest( @PathParam("id") userId: Long, payload: SignRequestParams, - ): SignRequestParams { + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { val user = userService.ensureUser(userId, Permission.MEASUREMENT_READ) - return authorizationService.signRequest(user, payload) + authorizationService.signRequest(user, payload) } companion object { 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 9ca238f7..5bc88829 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 @@ -19,19 +19,24 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource import jakarta.inject.Singleton import jakarta.ws.rs.* +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.HttpHeaders import jakarta.ws.rs.core.MediaType +import org.radarbase.auth.authorization.EntityDetails import org.radarbase.auth.authorization.Permission import org.radarbase.authorizer.api.* import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceClientService -import org.radarbase.jersey.auth.Auth +import org.radarbase.jersey.auth.AuthService import org.radarbase.jersey.auth.Authenticated import org.radarbase.jersey.auth.NeedsPermission import org.radarbase.jersey.cache.Cache import org.radarbase.jersey.exception.HttpNotFoundException +import org.radarbase.jersey.service.AsyncCoroutineService +import org.radarbase.kotlin.coroutines.forkJoin import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -42,7 +47,8 @@ import org.slf4j.LoggerFactory class SourceClientResource( @Context private val restSourceClients: RestSourceClientService, @Context private val clientMapper: RestSourceClientMapper, - @Context private val auth: Auth, + @Context private val authService: AuthService, + @Context private val asyncService: AsyncCoroutineService, @Context private val authorizationService: RestSourceAuthorizationService, @Context private val userRepository: RestSourceUserRepository, @Context private val userMapper: RestSourceUserMapper, @@ -72,15 +78,22 @@ class SourceClientResource( @PathParam("serviceUserId") serviceUserId: String, @PathParam("type") sourceType: String, @QueryParam("accessToken") accessToken: String?, - ): Boolean { + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { restSourceClients.ensureSourceType(sourceType) val user = userRepository.findByExternalId(serviceUserId, sourceType) - return if (user == null) { + if (user == null) { if (accessToken.isNullOrEmpty()) throw HttpNotFoundException("user-not-found", "User and access token not valid") logger.info("No user found for external ID provided. Continuing deregistration..") authorizationService.revokeToken(serviceUserId, sourceType, accessToken) } else { - auth.checkPermissionOnSubject(Permission.SUBJECT_UPDATE, user.projectId, user.userId) + authService.checkPermission( + Permission.SUBJECT_UPDATE, + EntityDetails( + project = user.projectId, + subject = user.userId, + ), + ) authorizationService.revokeToken(user) } } @@ -92,13 +105,21 @@ class SourceClientResource( fun getUserByServiceUserId( @PathParam("serviceUserId") serviceUserId: String, @PathParam("type") sourceType: String, - ): RestSourceUserDTO { + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { restSourceClients.ensureSourceType(sourceType) val user = userRepository.findByExternalId(serviceUserId, sourceType) ?: throw HttpNotFoundException("user-not-found", "User with service user id not found.") - auth.checkPermissionOnSubject(Permission.MEASUREMENT_READ, user.projectId, user.userId) - return userMapper.fromEntity(user) + authService.withPermission( + Permission.MEASUREMENT_READ, + EntityDetails( + project = user.projectId, + subject = user.userId, + ), + ) { + userMapper.fromEntity(user) + } } /** @@ -117,10 +138,11 @@ class SourceClientResource( fun reportDeregistration( @PathParam("type") sourceType: String, body: DeregistrationsDTO, - ) { + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { restSourceClients.ensureSourceType(sourceType) - body.deregistrations.forEach { deregistration -> + body.deregistrations.forkJoin { deregistration -> val user = userRepository.findByExternalId(deregistration.userId, sourceType) if (user != null && user.accessToken == deregistration.userAccessToken) { authorizationService.deregisterUser(user) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/DelegatedRestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/DelegatedRestSourceAuthorizationService.kt index 1300628b..05525636 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/DelegatedRestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/DelegatedRestSourceAuthorizationService.kt @@ -33,19 +33,19 @@ class DelegatedRestSourceAuthorizationService( return provider.get() } - override fun requestAccessToken(payload: RequestTokenPayload, sourceType: String): RestOauth2AccessToken = + override suspend fun requestAccessToken(payload: RequestTokenPayload, sourceType: String): RestOauth2AccessToken = delegate(sourceType).requestAccessToken(payload, sourceType) - override fun refreshToken(user: RestSourceUser): RestOauth2AccessToken? = + override suspend fun refreshToken(user: RestSourceUser): RestOauth2AccessToken? = delegate(user.sourceType).refreshToken(user) - override fun revokeToken(user: RestSourceUser): Boolean = + override suspend fun revokeToken(user: RestSourceUser): Boolean = delegate(user.sourceType).revokeToken(user) - override fun revokeToken(externalId: String, sourceType: String, token: String): Boolean = + override suspend fun revokeToken(externalId: String, sourceType: String, token: String): Boolean = delegate(sourceType).revokeToken(externalId, sourceType, token) - override fun getAuthorizationEndpointWithParams( + override suspend fun getAuthorizationEndpointWithParams( sourceType: String, userId: Long, state: String, @@ -56,7 +56,7 @@ class DelegatedRestSourceAuthorizationService( override fun signRequest(user: RestSourceUser, payload: SignRequestParams): SignRequestParams = delegate(user.sourceType).signRequest(user, payload) - override fun deregisterUser(user: RestSourceUser) = + override suspend fun deregisterUser(user: RestSourceUser) = delegate(user.sourceType).deregisterUser(user) companion object { diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt index 8bae9828..e17e0b14 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt @@ -16,10 +16,12 @@ package org.radarbase.authorizer.service -import com.fasterxml.jackson.databind.ObjectMapper +import io.ktor.client.call.* +import io.ktor.client.statement.* +import io.ktor.http.* import jakarta.ws.rs.core.Context -import okhttp3.OkHttpClient -import org.glassfish.jersey.process.internal.RequestScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.glassfish.jersey.server.BackgroundScheduler import org.radarbase.authorizer.api.RestOauth1AccessToken import org.radarbase.authorizer.api.RestOauth1UserId @@ -28,28 +30,24 @@ import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.doa.entity.RestSourceUser import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.GARMIN_AUTH import org.radarbase.jersey.exception.HttpBadGatewayException +import org.radarbase.jersey.service.AsyncCoroutineService +import org.radarbase.kotlin.coroutines.forkJoin import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit class GarminSourceAuthorizationService( @Context private val clientService: RestSourceClientService, - @Context private val httpClient: OkHttpClient, - @Context private val objectMapper: ObjectMapper, @Context private val userRepository: RestSourceUserRepository, - @Context private val requestScope: RequestScope, + @Context private val asyncService: AsyncCoroutineService, @Context @BackgroundScheduler private val scheduler: ScheduledExecutorService, @Context private val config: AuthorizerConfig, ) : OAuth1RestSourceAuthorizationService( clientService, - httpClient, - objectMapper, userRepository, config, ) { - private val oauth1Reader = objectMapper.readerFor(RestOauth1UserId::class.java) - init { // This schedules a task that periodically checks users with elapsed end dates and deregisters them. scheduler.scheduleAtFixedRate( @@ -60,34 +58,26 @@ class GarminSourceAuthorizationService( ) } - override fun deregisterUser(user: RestSourceUser) { + override suspend fun deregisterUser(user: RestSourceUser) { userRepository.delete(user) } - override fun RestOauth1AccessToken.getExternalId(sourceType: String): String { + override suspend fun RestOauth1AccessToken.getExternalId(sourceType: String): String = withContext(Dispatchers.IO) { // Garmin does not provide the service/external id with the token payload, so an additional // request to pull the external id is needed. - val req = createRequest("GET", GARMIN_USER_ID_ENDPOINT, this, sourceType) - return httpClient.newCall(req) - .execute() - .use { response -> - when (response.code) { - 200 -> response.body?.byteStream() - ?.let { - oauth1Reader.readValue(it).userId - } - ?: throw HttpBadGatewayException("Service did not provide a result") - 400, 401, 403 -> throw HttpBadGatewayException("Service was unable to fetch the external ID") - else -> throw HttpBadGatewayException("Cannot connect to $GARMIN_USER_ID_ENDPOINT: HTTP status ${response.code}") - } - } + val response = request(HttpMethod.Get, GARMIN_USER_ID_ENDPOINT, this@getExternalId, sourceType) + when (response.status) { + HttpStatusCode.OK -> response.body().userId + HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden -> throw HttpBadGatewayException("Service was unable to fetch the external ID") + else -> throw HttpBadGatewayException("Cannot connect to ${response.request.url}: HTTP status ${response.status}") + } } private fun checkForUsersWithElapsedEndDateAndDeregister() { - requestScope.runInScope { + asyncService.runBlocking { userRepository .queryAllWithElapsedEndDate(GARMIN_AUTH) - .forEach { revokeToken(it) } + .forkJoin { revokeToken(it) } } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/LockService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/LockService.kt index 7a6fba0a..b2c5bdb1 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/LockService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/LockService.kt @@ -3,5 +3,5 @@ package org.radarbase.authorizer.service import kotlin.time.Duration interface LockService { - fun runLocked(lockName: String, timeout: Duration, doRun: () -> T): T + suspend fun runLocked(lockName: String, timeout: Duration, doRun: suspend () -> T): T } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt index 5870d1d2..becac0c4 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt @@ -16,13 +16,19 @@ package org.radarbase.authorizer.service -import com.fasterxml.jackson.databind.ObjectMapper +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.Response -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import org.radarbase.authorizer.api.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import org.radarbase.authorizer.api.RequestTokenPayload +import org.radarbase.authorizer.api.RestOauth1AccessToken +import org.radarbase.authorizer.api.RestOauth2AccessToken +import org.radarbase.authorizer.api.SignRequestParams import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.config.RestSourceClient import org.radarbase.authorizer.doa.RestSourceUserRepository @@ -35,18 +41,16 @@ import org.radarbase.jersey.exception.HttpBadRequestException import org.slf4j.Logger import org.slf4j.LoggerFactory import java.time.Instant -import kotlin.math.floor +import java.util.concurrent.ThreadLocalRandom abstract class OAuth1RestSourceAuthorizationService( @Context private val clientService: RestSourceClientService, - @Context private val httpClient: OkHttpClient, - @Context private val objectMapper: ObjectMapper, @Context private val userRepository: RestSourceUserRepository, @Context private val config: AuthorizerConfig, ) : RestSourceAuthorizationService { - private val tokenReader = objectMapper.readerFor(RestOauth1AccessToken::class.java) + private val httpClient = RestSourceAuthorizationService.httpClient() - override fun requestAccessToken(payload: RequestTokenPayload, sourceType: String): RestOauth2AccessToken { + override suspend fun requestAccessToken(payload: RequestTokenPayload, sourceType: String): RestOauth2AccessToken { val authConfig = clientService.forSourceType(sourceType) logger.info("Requesting access token..") @@ -62,11 +66,11 @@ abstract class OAuth1RestSourceAuthorizationService( return token.toOAuth2(sourceType) } - override fun refreshToken(user: RestSourceUser): RestOauth2AccessToken? { + override suspend fun refreshToken(user: RestSourceUser): RestOauth2AccessToken? { return user.refreshToken?.let { RestOauth2AccessToken(it, user.refreshToken) } } - override fun revokeToken(user: RestSourceUser): Boolean { + override suspend fun revokeToken(user: RestSourceUser): Boolean { val accessToken = user.accessToken if (accessToken == null || !user.authorized) { throw HttpBadRequestException( @@ -76,48 +80,46 @@ abstract class OAuth1RestSourceAuthorizationService( } val authConfig = clientService.forSourceType(user.sourceType) - val req = createRequest( - "DELETE", - authConfig.deregistrationEndpoint!!, - RestOauth1AccessToken(accessToken, user.refreshToken), - user.sourceType, - ) - - return httpClient.newCall(req) - .execute() - .use { response -> - when (response.code) { - 200, 204 -> { - this.userRepository.updateToken(null, user) - true - } - 400, 401, 403 -> false - else -> throw HttpBadGatewayException("Cannot connect to ${authConfig.deregistrationEndpoint}: HTTP status ${response.code}") + return withContext(Dispatchers.IO) { + val response = request( + HttpMethod.Delete, + authConfig.deregistrationEndpoint!!, + RestOauth1AccessToken(accessToken, user.refreshToken), + user.sourceType, + ) + when (response.status) { + HttpStatusCode.OK, HttpStatusCode.NoContent -> { + userRepository.updateToken(null, user) + true } + + HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden -> false + else -> throw HttpBadGatewayException("Cannot connect to ${response.request.url}: HTTP status ${response.status}") } + } } - override fun revokeToken(externalId: String, sourceType: String, token: String): Boolean { + override suspend fun revokeToken(externalId: String, sourceType: String, token: String): Boolean { val authConfig = clientService.forSourceType(sourceType) if (token.isEmpty()) throw HttpBadRequestException("token-empty", "Token cannot be null or empty") - val req = createRequest( - "DELETE", - authConfig.deregistrationEndpoint!!, - RestOauth1AccessToken(token, ""), - sourceType, - ) + val response = withContext(Dispatchers.IO) { + request( + HttpMethod.Delete, + authConfig.deregistrationEndpoint!!, + RestOauth1AccessToken(token, ""), + sourceType, + ) + } - return httpClient.newCall(req).execute().use { response -> - when (response.code) { - 200, 204 -> true - 400, 401, 403 -> false - else -> throw HttpBadGatewayException("Cannot connect to ${authConfig.deregistrationEndpoint}: HTTP status ${response.code}") - } + return when (response.status) { + HttpStatusCode.OK, HttpStatusCode.NoContent -> true + HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden -> false + else -> throw HttpBadGatewayException("Cannot connect to ${response.request.url}: HTTP status ${response.status}") } } - override fun getAuthorizationEndpointWithParams( + override suspend fun getAuthorizationEndpointWithParams( sourceType: String, userId: Long, state: String, @@ -125,7 +127,7 @@ abstract class OAuth1RestSourceAuthorizationService( logger.info("Getting auth endpoint..") val authConfig = clientService.forSourceType(sourceType) - val tokens = this.requestToken(authConfig.preAuthorizationEndpoint, RestOauth1AccessToken(""), sourceType) + val tokens = requestToken(authConfig.preAuthorizationEndpoint, RestOauth1AccessToken(""), sourceType) return Url( authConfig.authorizationEndpoint, @@ -134,53 +136,53 @@ abstract class OAuth1RestSourceAuthorizationService( put(OAUTH_ACCESS_TOKEN_SECRET, tokens?.tokenSecret) put( OAUTH_CALLBACK, - config.service.callbackUrl - .newBuilder() - .addQueryParameter("state", state) - .build() - .toString(), + URLBuilder(config.service.callbackUrl).run { + parameters.append("state", state) + build() + }.toString(), ) }, ).getUrl() } - private fun requestToken( + private suspend fun requestToken( tokenEndpoint: String?, tokens: RestOauth1AccessToken, sourceType: String, - ): RestOauth1AccessToken? { - val req = createRequest( - "POST", + ): RestOauth1AccessToken? = withContext(Dispatchers.IO) { + val response = request( + HttpMethod.Post, tokenEndpoint.orEmpty(), tokens, sourceType, ) - return httpClient.newCall(req) - .execute() - .use { response -> - when (response.code) { - 200 -> response.body?.string() - ?.let { tokenReader.readValue(parseParams(it)) } - ?: throw HttpBadGatewayException("Service did not provide a result") - 400, 401, 403 -> null - else -> throw HttpBadGatewayException("Cannot connect to $tokenEndpoint: HTTP status ${response.code}") - } + when (response.status) { + HttpStatusCode.OK -> try { + Json.decodeFromString(response.bodyAsText().toJsonString()) + } catch (ex: IllegalArgumentException) { + throw HttpBadGatewayException("Service did not provide a result: $ex") } + HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden -> null + else -> throw HttpBadGatewayException("Cannot connect to ${response.request.url}: HTTP status ${response.status}") + } } - fun createRequest(method: String, url: String, tokens: RestOauth1AccessToken, sourceType: String): Request { + suspend fun request(method: HttpMethod, url: String, tokens: RestOauth1AccessToken, sourceType: String): HttpResponse { val authConfig = clientService.forSourceType(sourceType) val params = this.getAuthParams(authConfig, tokens.token, tokens.tokenVerifier) params[OAUTH_SIGNATURE] = OauthSignature(url, params, method, authConfig.clientSecret, tokens.tokenSecret).getEncodedSignature() - val headers = params.toFormattedHeader() - return Request.Builder() - .url(url) - .header("Authorization", "OAuth $headers") - .method(method, if (method == "POST") "".toRequestBody(null) else null) - .build() + return httpClient.request(url = Url(url)) { + headers { + append("Authorization", "OAuth ${params.toFormattedHeader()}") + } + this.method = method + if (method == HttpMethod.Post) { + setBody("") + } + } } override fun signRequest(user: RestSourceUser, payload: SignRequestParams): SignRequestParams { @@ -197,7 +199,7 @@ abstract class OAuth1RestSourceAuthorizationService( OauthSignature( payload.url, toSortedMap(), - payload.method, + HttpMethod.parse(payload.method), authConfig.clientSecret, user.refreshToken, ).getEncodedSignature(), @@ -211,24 +213,22 @@ abstract class OAuth1RestSourceAuthorizationService( authConfig: RestSourceClient, accessToken: String?, tokenVerifier: String?, - ): MutableMap { - return mutableMapOf( - OAUTH_CONSUMER_KEY to authConfig.clientId, - OAUTH_NONCE to this.generateNonce(), - OAUTH_SIGNATURE_METHOD to OAUTH_SIGNATURE_METHOD_VALUE, - OAUTH_TIMESTAMP to Instant.now().epochSecond.toString(), - OAUTH_ACCESS_TOKEN to accessToken, - OAUTH_VERIFIER to tokenVerifier, - OAUTH_VERSION to OAUTH_VERSION_VALUE, - ) - } + ): MutableMap = mutableMapOf( + OAUTH_CONSUMER_KEY to authConfig.clientId, + OAUTH_NONCE to this.generateNonce(), + OAUTH_SIGNATURE_METHOD to OAUTH_SIGNATURE_METHOD_VALUE, + OAUTH_TIMESTAMP to Instant.now().epochSecond.toString(), + OAUTH_ACCESS_TOKEN to accessToken, + OAUTH_VERIFIER to tokenVerifier, + OAUTH_VERSION to OAUTH_VERSION_VALUE, + ) private fun generateNonce(): String { - return floor(Math.random() * 1000000000).toInt().toString() + return ThreadLocalRandom.current().nextInt(1000000000).toString() } - private fun parseParams(input: String): String { - val params = input + private fun String.toJsonString(): String { + val params = this .replace("=", "\":\"") .replace("&", "\",\"") return "{\"$params\"}" @@ -236,7 +236,7 @@ abstract class OAuth1RestSourceAuthorizationService( // This maps the OAuth1 properties to OAuth2 for backwards compatibility in the repository // Also, an additional request for getting the external ID is made here to pull the external id - private fun RestOauth1AccessToken.toOAuth2(sourceType: String) = RestOauth2AccessToken( + private suspend fun RestOauth1AccessToken.toOAuth2(sourceType: String) = RestOauth2AccessToken( token, tokenSecret, Integer.MAX_VALUE, @@ -247,7 +247,7 @@ abstract class OAuth1RestSourceAuthorizationService( private fun Map.toFormattedHeader(): String = entries.joinToString { (k, v) -> "$k=\"$v\"" } - abstract fun RestOauth1AccessToken.getExternalId(sourceType: String): String? + abstract suspend fun RestOauth1AccessToken.getExternalId(sourceType: String): String? companion object { val logger: Logger = LoggerFactory.getLogger(OAuth1RestSourceAuthorizationService::class.java) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt index 996097a8..4166599d 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt @@ -16,84 +16,81 @@ package org.radarbase.authorizer.service -import com.fasterxml.jackson.databind.ObjectMapper +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.client.statement.* +import io.ktor.http.* import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.UriBuilder -import okhttp3.Credentials -import okhttp3.FormBody -import okhttp3.OkHttpClient -import okhttp3.Request +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.radarbase.authorizer.api.RequestTokenPayload import org.radarbase.authorizer.api.RestOauth2AccessToken import org.radarbase.authorizer.api.SignRequestParams import org.radarbase.authorizer.config.AuthorizerConfig +import org.radarbase.authorizer.config.RestSourceClient import org.radarbase.authorizer.doa.entity.RestSourceUser 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 class OAuth2RestSourceAuthorizationService( @Context private val clients: RestSourceClientService, - @Context private val httpClient: OkHttpClient, - @Context private val objectMapper: ObjectMapper, @Context private val config: AuthorizerConfig, ) : RestSourceAuthorizationService { - private val tokenReader = objectMapper.readerFor(RestOauth2AccessToken::class.java) + private val httpClient = RestSourceAuthorizationService.httpClient() - override fun requestAccessToken(payload: RequestTokenPayload, sourceType: String): RestOauth2AccessToken { - val authorizationConfig = clients.forSourceType(sourceType) - val clientId = checkNotNull(authorizationConfig.clientId) - - val form = FormBody.Builder().apply { - payload.code?.let { add("code", it) } - add("grant_type", "authorization_code") - add("client_id", clientId) - add("redirect_uri", config.service.callbackUrl.toString()) - }.build() + override suspend fun requestAccessToken(payload: RequestTokenPayload, sourceType: String): RestOauth2AccessToken = withContext(Dispatchers.IO) { logger.info("Requesting access token with authorization code") - return httpClient.requestJson(post(form, sourceType), tokenReader) + val response = submitForm(sourceType) { authorizationConfig -> + payload.code?.let { append("code", it) } + append("grant_type", "authorization_code") + append("client_id", checkNotNull(authorizationConfig.clientId)) + append("redirect_uri", config.service.callbackUrl.toString()) + } + if (!response.status.isSuccess()) { + throw HttpBadGatewayException("Failed to request access token (HTTP status code ${response.status}): ${response.bodyAsText()}") + } + response.body() } - override fun refreshToken(user: RestSourceUser): RestOauth2AccessToken? { - val refreshToken = user.refreshToken ?: return null - val form = FormBody.Builder().apply { - add("grant_type", "refresh_token") - add("refresh_token", refreshToken) - }.build() - logger.info("Requesting to refreshToken") - val request = post(form, user.sourceType) - return httpClient.newCall(request).execute().use { response -> - when (response.code) { - 200 -> response.body?.byteStream() - ?.let { tokenReader.readValue(it) } - ?: throw HttpBadGatewayException("Service ${user.sourceType} did not provide a result") - 400, 401, 403 -> { - val body = response.body?.string() - logger.error("Failed to refresh token (HTTP status code {}): {}", response.code, body) - null - } - else -> throw HttpBadGatewayException("Cannot connect to ${request.url}: HTTP status ${response.code}") + override suspend fun refreshToken(user: RestSourceUser): RestOauth2AccessToken? = withContext(Dispatchers.IO) { + val refreshToken = user.refreshToken ?: return@withContext null + logger.info("Requesting to refresh token") + val response = submitForm(user.sourceType) { + append("grant_type", "refresh_token") + append("refresh_token", refreshToken) + } + when (response.status) { + HttpStatusCode.OK -> response.body() + HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden -> { + logger.error("Failed to refresh token (HTTP status code {}): {}", response.status, response.bodyAsText()) + null } + else -> throw HttpBadGatewayException( + "Cannot connect to ${response.request.url} (HTTP status ${response.status}): ${response.bodyAsText()}" + ) } } - override fun revokeToken(user: RestSourceUser): Boolean { + override suspend fun revokeToken(user: RestSourceUser): Boolean = withContext(Dispatchers.IO) { val accessToken = user.accessToken ?: run { logger.error("Cannot revoke token of user {} without an access token", user.userId) - return false + return@withContext false } - val form = FormBody.Builder().add("token", accessToken).build() logger.info("Requesting to revoke access token") - return httpClient.request(post(form, user.sourceType)) + val response = submitForm(user.sourceType) { + append("token", accessToken) + } + response.status.isSuccess() } - override fun revokeToken(externalId: String, sourceType: String, token: String): Boolean = + override suspend fun revokeToken(externalId: String, sourceType: String, token: String): Boolean = throw HttpBadRequestException("", "Not available for auth type") - override fun getAuthorizationEndpointWithParams( + override suspend fun getAuthorizationEndpointWithParams( sourceType: String, userId: Long, state: String, @@ -109,27 +106,26 @@ class OAuth2RestSourceAuthorizationService( .build().toString() } - override fun deregisterUser(user: RestSourceUser) = + override suspend fun deregisterUser(user: RestSourceUser) = throw HttpBadRequestException("", "Not available for auth type") override fun signRequest(user: RestSourceUser, payload: SignRequestParams): SignRequestParams = throw HttpBadRequestException("", "Not available for auth type") - private fun post(form: FormBody, sourceType: String): Request { + private suspend fun submitForm(sourceType: String, builder: ParametersBuilder.(RestSourceClient) -> Unit): HttpResponse { val authorizationConfig = clients.forSourceType(sourceType) - val credentials = Credentials.basic( - checkNotNull(authorizationConfig.clientId), - checkNotNull(authorizationConfig.clientSecret), - ) - - return Request.Builder().apply { - url(authorizationConfig.tokenEndpoint) - post(form) - header("Authorization", credentials) - header("Content-Type", "application/x-www-form-urlencoded") - header("Accept", "application/json") - }.build() + return httpClient.submitForm( + url = authorizationConfig.tokenEndpoint, + formParameters = Parameters.build { + builder(authorizationConfig) + } + ) { + basicAuth( + checkNotNull(authorizationConfig.clientId), + checkNotNull(authorizationConfig.clientSecret), + ) + } } companion object { diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RedisLockService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RedisLockService.kt index a657ed9b..7f86b447 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RedisLockService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RedisLockService.kt @@ -2,6 +2,10 @@ package org.radarbase.authorizer.service import jakarta.persistence.LockTimeoutException import jakarta.ws.rs.core.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext import org.radarbase.authorizer.config.AuthorizerConfig import org.slf4j.LoggerFactory import redis.clients.jedis.Jedis @@ -11,6 +15,7 @@ import redis.clients.jedis.params.SetParams import java.io.IOException import java.util.* import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds class RedisLockService( @Context config: AuthorizerConfig, @@ -26,7 +31,7 @@ class RedisLockService( /** * @throws LockTimeoutException if the lock cannot be acquired. */ - override fun runLocked(lockName: String, timeout: Duration, doRun: () -> T): T { + override suspend fun runLocked(lockName: String, timeout: Duration, doRun: suspend () -> T): T { val lockKey = "$lockPrefix/$lockName.lock" val setParams = SetParams() .nx() // only set if not already set @@ -35,20 +40,23 @@ class RedisLockService( val startTime = System.nanoTime() val totalTime = timeout.inWholeNanoseconds var didAcquire = false + val callerCoroutineContext = currentCoroutineContext() return withJedis { while (System.nanoTime() - startTime < totalTime) { didAcquire = set(lockKey, uuid, setParams) != null if (didAcquire) { break } else { - Thread.sleep(POLL_PERIOD) + delay(POLL_PERIOD) } } if (!didAcquire) { throw LockTimeoutException() } try { - doRun() + withContext(callerCoroutineContext) { + doRun() + } } finally { if (get(lockKey) == uuid) { del(lockKey) @@ -58,8 +66,10 @@ class RedisLockService( } @Throws(IOException::class) - fun withJedis(routine: Jedis.() -> T): T { - return try { + suspend fun withJedis( + routine: suspend Jedis.() -> T + ): T = withContext(Dispatchers.IO) { + try { jedisPool.resource.use { it.routine() } @@ -70,6 +80,6 @@ class RedisLockService( companion object { private val logger = LoggerFactory.getLogger(RedisLockService::class.java) - private const val POLL_PERIOD = 250L + private val POLL_PERIOD = 250.milliseconds } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RegistrationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RegistrationService.kt index eaac0d58..b0b8d907 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RegistrationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RegistrationService.kt @@ -12,7 +12,7 @@ import org.radarbase.jersey.exception.HttpInternalServerException class RegistrationService( @Context private val registrationRepository: RegistrationRepository, ) { - fun generate(user: RestSourceUser, persistent: Boolean): RegistrationResponse { + suspend fun generate(user: RestSourceUser, persistent: Boolean): RegistrationResponse { val secret = if (persistent) { Hmac256Secret.generate(secretLength = 12, saltLength = 6) } else { @@ -31,8 +31,8 @@ class RegistrationService( ) } - fun ensureRegistration(token: String): RegistrationState { - val registration = registrationRepository[token] + suspend fun ensureRegistration(token: String): RegistrationState { + val registration = registrationRepository.get(token) ?: throw HttpBadRequestException("registration_not_found", "State has expired or not found") if (!registration.isValid) throw HttpBadRequestException("registration_expired", "Token has expired") return registration 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 52de6414..dfaf14a4 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 @@ -16,21 +16,28 @@ package org.radarbase.authorizer.service +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json import org.radarbase.authorizer.api.RequestTokenPayload import org.radarbase.authorizer.api.RestOauth2AccessToken import org.radarbase.authorizer.api.SignRequestParams import org.radarbase.authorizer.doa.entity.RestSourceUser +import kotlin.time.Duration.Companion.seconds interface RestSourceAuthorizationService { - fun requestAccessToken(payload: RequestTokenPayload, sourceType: String): RestOauth2AccessToken + suspend fun requestAccessToken(payload: RequestTokenPayload, sourceType: String): RestOauth2AccessToken - fun refreshToken(user: RestSourceUser): RestOauth2AccessToken? + suspend fun refreshToken(user: RestSourceUser): RestOauth2AccessToken? - fun revokeToken(user: RestSourceUser): Boolean + suspend fun revokeToken(user: RestSourceUser): Boolean - fun revokeToken(externalId: String, sourceType: String, token: String): Boolean + suspend fun revokeToken(externalId: String, sourceType: String, token: String): Boolean - fun getAuthorizationEndpointWithParams( + suspend fun getAuthorizationEndpointWithParams( sourceType: String, userId: Long, state: String, @@ -38,5 +45,23 @@ interface RestSourceAuthorizationService { fun signRequest(user: RestSourceUser, payload: SignRequestParams): SignRequestParams - fun deregisterUser(user: RestSourceUser) + suspend fun deregisterUser(user: RestSourceUser) + + companion object { + fun httpClient(builder: HttpClientConfig.() -> Unit = {}) = + HttpClient(CIO) { + install(HttpTimeout) { + val millis = 10.seconds.inWholeMilliseconds + connectTimeoutMillis = millis + socketTimeoutMillis = millis + requestTimeoutMillis = millis + } + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + }) + } + builder() + } + } } 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 index c4941341..873b0836 100644 --- 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 @@ -1,6 +1,7 @@ package org.radarbase.authorizer.service import jakarta.ws.rs.core.Context +import org.radarbase.authorizer.config.RestSourceClient import org.radarbase.authorizer.config.RestSourceClients import org.radarbase.jersey.exception.HttpBadRequestException import org.radarbase.jersey.exception.HttpNotFoundException @@ -11,7 +12,7 @@ class RestSourceClientService( val clients = restSourceClients.clients private val configMap = clients.associateBy { it.sourceType } - fun forSourceType(sourceType: String) = configMap[sourceType] + fun forSourceType(sourceType: String): RestSourceClient = configMap[sourceType] ?: throw HttpBadRequestException( "client-config-not-found", "Cannot find client configurations for source-type $sourceType", diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.kt index bd35fff1..c48da663 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.kt @@ -2,17 +2,17 @@ package org.radarbase.authorizer.service import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.Response +import org.radarbase.auth.authorization.EntityDetails import org.radarbase.auth.authorization.Permission import org.radarbase.authorizer.api.RestSourceUserDTO import org.radarbase.authorizer.api.RestSourceUserMapper import org.radarbase.authorizer.api.TokenDTO import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.doa.entity.RestSourceUser -import org.radarbase.jersey.auth.Auth +import org.radarbase.jersey.auth.AuthService import org.radarbase.jersey.exception.HttpApplicationException import org.radarbase.jersey.exception.HttpBadRequestException import org.radarbase.jersey.exception.HttpNotFoundException -import org.radarbase.jersey.service.managementportal.RadarProjectService import kotlin.time.Duration.Companion.seconds class RestSourceUserService( @@ -20,28 +20,33 @@ class RestSourceUserService( @Context private val userMapper: RestSourceUserMapper, @Context private val lockService: LockService, @Context private val authorizationService: RestSourceAuthorizationService, - @Context private val projectService: RadarProjectService, - @Context private val auth: Auth, + @Context private val authService: AuthService, ) { - fun ensureUser(userId: Long, permission: Permission? = null): RestSourceUser { + suspend fun ensureUser(userId: Long, permission: Permission? = null): RestSourceUser { val user = userRepository.read(userId) ?: throw HttpNotFoundException("user_not_found", "Rest-Source-User with ID $userId does not exist") if (permission != null) { - auth.checkPermissionOnSubject(permission, user.projectId, user.userId) + authService.checkPermission( + permission, + EntityDetails( + project = user.projectId, + subject = user.userId, + ), + ) } return user } - fun create(userDto: RestSourceUserDTO): RestSourceUserDTO { - validate(userDto) + suspend fun create(userDto: RestSourceUserDTO): RestSourceUserDTO { + userDto.ensure() val user = userRepository.create(userDto) return userMapper.fromEntity(user) } - fun get(userId: Long): RestSourceUserDTO = + suspend fun get(userId: Long): RestSourceUserDTO = userMapper.fromEntity(ensureUser(userId, Permission.SUBJECT_READ)) - fun delete(userId: Long) { + suspend fun delete(userId: Long) { ensureUser(userId, Permission.SUBJECT_UPDATE) runLocked(userId) { user -> if (user.accessToken != null) authorizationService.revokeToken(user) @@ -49,8 +54,8 @@ class RestSourceUserService( } } - fun update(userId: Long, user: RestSourceUserDTO): RestSourceUserDTO { - validate(user) + suspend fun update(userId: Long, user: RestSourceUserDTO): RestSourceUserDTO { + user.ensure() return userMapper.fromEntity( runLocked(userId) { userRepository.update(userId, user) @@ -58,8 +63,8 @@ class RestSourceUserService( ) } - fun reset(userId: Long, user: RestSourceUserDTO): RestSourceUserDTO { - validate(user) + suspend fun reset(userId: Long, user: RestSourceUserDTO): RestSourceUserDTO { + user.ensure() val existingUser = ensureUser(userId) return userMapper.fromEntity( userRepository.reset( @@ -70,26 +75,25 @@ class RestSourceUserService( ) } - private fun validate( - user: RestSourceUserDTO, - ) { - val projectId = user.projectId + private suspend fun RestSourceUserDTO.ensure() { + val projectId = projectId ?: throw HttpBadRequestException("missing_project_id", "project cannot be empty") - val userId = user.userId + val subjectId = userId ?: throw HttpBadRequestException( "missing_user_id", "subject-id/user-id cannot be empty", ) - auth.checkPermissionOnSubject(Permission.SUBJECT_UPDATE, projectId, userId) - projectService.projectSubjects(projectId).find { it.id == userId } - ?: throw HttpBadRequestException( - "user_not_found", - "user $userId not found in project $projectId", - ) + authService.checkPermission( + Permission.SUBJECT_UPDATE, + EntityDetails( + project = projectId, + subject = subjectId, + ), + ) } - fun ensureToken(userId: Long): TokenDTO { + suspend fun ensureToken(userId: Long): TokenDTO { ensureUser(userId, Permission.MEASUREMENT_CREATE) return runLocked(userId) { user -> if (user.hasValidToken()) { @@ -101,20 +105,23 @@ class RestSourceUserService( } } - fun refreshToken(userId: Long): TokenDTO { + suspend fun refreshToken(userId: Long): TokenDTO { ensureUser(userId, Permission.MEASUREMENT_CREATE) return runLocked(userId) { user -> doRefreshToken(user) } } - private inline fun runLocked(userId: Long, crossinline doRun: (RestSourceUser) -> T): T { - return lockService.runLocked("token-$userId", 10.seconds) { - doRun(ensureUser(userId)) + private suspend inline fun runLocked( + userId: Long, + crossinline doRun: suspend (RestSourceUser) -> T, + ): T = + lockService.runLocked("token-$userId", 10.seconds) { + val user = ensureUser(userId) + doRun(user) } - } - private fun doRefreshToken(user: RestSourceUser): TokenDTO { + private suspend fun doRefreshToken(user: RestSourceUser): TokenDTO { if (!user.authorized) { throw HttpApplicationException( Response.Status.PROXY_AUTHENTICATION_REQUIRED, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OauthSignature.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OauthSignature.kt index 310b24ed..ed91c9e0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OauthSignature.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OauthSignature.kt @@ -1,5 +1,6 @@ package org.radarbase.authorizer.util +import io.ktor.http.* import java.net.URLEncoder import java.util.* import javax.crypto.Mac @@ -9,14 +10,14 @@ import kotlin.text.Charsets.UTF_8 data class OauthSignature( var endPoint: String, var params: Map, - var method: String, + var method: HttpMethod, var clientSecret: String?, var tokenSecret: String?, ) { fun getEncodedSignature(): String { val encodedUrl = URLEncoder.encode(this.endPoint, UTF_8) val encodedParams = URLEncoder.encode(this.params.toQueryFormat(), UTF_8) - val signatureBase = "$method&$encodedUrl&$encodedParams" + val signatureBase = "${method.value}&$encodedUrl&$encodedParams" val key = "${this.clientSecret.orEmpty()}&${this.tokenSecret.orEmpty()}" return URLEncoder.encode(encodeSHA(key, signatureBase), UTF_8) } diff --git a/authorizer-app-backend/src/test/java/org/radarbase/authorizer/AuthorizerServiceConfigTest.kt b/authorizer-app-backend/src/test/java/org/radarbase/authorizer/AuthorizerServiceConfigTest.kt index 1541f6ea..d41c39f5 100644 --- a/authorizer-app-backend/src/test/java/org/radarbase/authorizer/AuthorizerServiceConfigTest.kt +++ b/authorizer-app-backend/src/test/java/org/radarbase/authorizer/AuthorizerServiceConfigTest.kt @@ -1,6 +1,6 @@ package org.radarbase.authorizer -import okhttp3.HttpUrl.Companion.toHttpUrl +import io.ktor.http.* import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.junit.jupiter.api.Test @@ -15,7 +15,7 @@ internal class AuthorizerServiceConfigTest { advertisedBaseUri = URI("http://something/"), ) - assertThat(config.callbackUrl, equalTo("http://something/authorizer/users:new".toHttpUrl())) + assertThat(config.callbackUrl, equalTo(Url("http://something/authorizer/users:new"))) } @Test @@ -23,7 +23,7 @@ internal class AuthorizerServiceConfigTest { val config = AuthorizerServiceConfig( advertisedBaseUri = URI("http://something//backend//"), ) - assertThat(config.callbackUrl, equalTo("http://something/authorizer/users:new".toHttpUrl())) + assertThat(config.callbackUrl, equalTo(Url("http://something/authorizer/users:new"))) } @Test @@ -31,6 +31,6 @@ internal class AuthorizerServiceConfigTest { val config = AuthorizerServiceConfig( frontendBaseUri = URI("http://something/authorizer"), ) - assertThat(config.callbackUrl, equalTo("http://something/authorizer/users:new".toHttpUrl())) + assertThat(config.callbackUrl, equalTo(Url("http://something/authorizer/users:new"))) } } diff --git a/build.gradle.kts b/build.gradle.kts index a6066df9..6d65fbee 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,60 +1,25 @@ -import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask +import org.radarbase.gradle.plugin.radarKotlin plugins { - kotlin("jvm") - id("org.jlleitschuh.gradle.ktlint") version "11.3.1" apply false - id("com.github.ben-manes.versions") version "0.46.0" + id("org.radarbase.radar-root-project") version Versions.radarCommons + id("org.radarbase.radar-dependency-management") version Versions.radarCommons + id("org.radarbase.radar-kotlin") version Versions.radarCommons apply false } -allprojects { - group = "org.radarbase" - version = "4.3.1-SNAPSHOT" - - repositories { - mavenCentral() - mavenLocal() -// maven(url = "https://oss.sonatype.org/content/repositories/snapshots") - } -} - -subprojects { - apply(plugin = "org.jlleitschuh.gradle.ktlint") - - configure { - val ktlintVersion: String by project - version.set(ktlintVersion) - } - - tasks.register("downloadDependencies") { - doLast { - configurations["runtimeClasspath"].files - configurations["compileClasspath"].files - println("Downloaded all dependencies") - } - } - - tasks.register("copyDependencies") { - from(configurations.runtimeClasspath.map { it.files }) - into("$buildDir/third-party/") - doLast { - println("Copied third-party runtime dependencies") - } - } -} - -fun isNonStable(version: String): Boolean { - val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) } - val regex = "^[0-9,.v-]+(-r)?$".toRegex() - val isStable = stableKeyword || regex.matches(version) - return isStable.not() +radarRootProject { + projectVersion.set(Versions.project) + gradleVersion.set(Versions.gradle) } -tasks.named("dependencyUpdates").configure { - rejectVersionIf { - isNonStable(candidate.version) +allprojects { + apply(plugin = "org.radarbase.radar-kotlin") + + radarKotlin { + javaVersion.set(Versions.java) + kotlinVersion.set(Versions.kotlin) + slf4jVersion.set(Versions.slf4j) + log4j2Version.set(Versions.log4j2) + junitVersion.set(Versions.junit) + ktlintVersion.set(Versions.ktlint) } } - -tasks.wrapper { - gradleVersion = "8.0.2" -} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..a180307e --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,21 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") version "1.8.21" +} + +repositories { + mavenCentral() +} + +tasks.withType { + sourceCompatibility = "17" + targetCompatibility = "17" +} + +tasks.withType { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt new file mode 100644 index 00000000..6b99161c --- /dev/null +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -0,0 +1,23 @@ +object Versions { + const val project = "4.3.1-SNAPSHOT" + + const val java = 17 + + const val kotlin = "1.8.21" + const val ktlint = "0.48.2" + + const val radarJersey = "0.11.0-SNAPSHOT" + const val postgres = "42.6.0" + const val slf4j = "2.0.7" + const val log4j2 = "2.20.0" + const val jersey = "3.1.1" + const val junit = "5.9.3" + const val mockitoKotlin = "4.1.0" + const val jedis = "4.4.0" + + const val radarCommons = "1.0.0" + const val ktor: String = "2.3.0" + + const val hamcrest: String = "2.2" + const val gradle = "8.1.1" +} diff --git a/gradle.properties b/gradle.properties index 62afa364..705fb677 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,29 +1,6 @@ -# -# 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. -# - kotlin.code.style=official -kotlinVersion=1.8.10 -ktlintVersion=0.48.2 -okhttpVersion=4.10.0 -radarJerseyVersion=0.10.0 -postgresVersion=42.6.0 -slf4jVersion=2.0.7 -log4j2Version=2.20.0 -jerseyVersion=3.1.1 -junitVersion=5.9.2 -mockitoKotlinVersion=4.1.0 -jedisVersion=4.3.2 +public.gpr.user=radar-public +public.gpr.token=Z2hwX0h0d0FHSmJzeEpjenBlUVIycVhWb0RpNGdZdHZnZzJTMFVJZA== + +kotlinVersion=1.8.21 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 8979 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 F{TFmrHLU;u delta 8469 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 Date: Wed, 24 May 2023 10:14:47 +0200 Subject: [PATCH 05/35] Code cleanup --- .../config/AuthorizerServiceConfig.kt | 12 ++++---- .../OAuth1RestSourceAuthorizationService.kt | 29 +++++++++---------- .../OAuth2RestSourceAuthorizationService.kt | 24 ++++++++------- .../authorizer/service/RedisLockService.kt | 2 +- .../service/RestSourceAuthorizationService.kt | 8 +++-- .../java/org/radarbase/authorizer/util/Url.kt | 16 ---------- 6 files changed, 38 insertions(+), 53 deletions(-) delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Url.kt diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt index 3afe9376..fb4d31ca 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt @@ -20,13 +20,11 @@ data class AuthorizerServiceConfig( val callbackUrl: Url by lazy { val frontendBaseUrlBuilder = when { frontendBaseUri != null -> URLBuilder().takeFrom(frontendBaseUri) - advertisedBaseUri != null -> { - URLBuilder().apply { - takeFrom(advertisedBaseUri) - pathSegments = buildList(pathSegments.size) { - addAll(pathSegments.dropLastWhile { it.isEmpty() || it == "backend" }) - add("authorizer") - } + advertisedBaseUri != null -> URLBuilder().apply { + takeFrom(advertisedBaseUri) + pathSegments = buildList(pathSegments.size) { + addAll(pathSegments.dropLastWhile { it.isEmpty() || it == "backend" }) + add("authorizer") } } else -> throw IllegalStateException("Frontend URL parameter is not a valid HTTP URL.") diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt index becac0c4..9a7b2aa7 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt @@ -34,7 +34,6 @@ import org.radarbase.authorizer.config.RestSourceClient import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.doa.entity.RestSourceUser import org.radarbase.authorizer.util.OauthSignature -import org.radarbase.authorizer.util.Url import org.radarbase.jersey.exception.HttpApplicationException import org.radarbase.jersey.exception.HttpBadGatewayException import org.radarbase.jersey.exception.HttpBadRequestException @@ -129,20 +128,20 @@ abstract class OAuth1RestSourceAuthorizationService( val tokens = requestToken(authConfig.preAuthorizationEndpoint, RestOauth1AccessToken(""), sourceType) - return Url( - authConfig.authorizationEndpoint, - buildMap { - put(OAUTH_ACCESS_TOKEN, tokens?.token) - put(OAUTH_ACCESS_TOKEN_SECRET, tokens?.tokenSecret) - put( - OAUTH_CALLBACK, - URLBuilder(config.service.callbackUrl).run { - parameters.append("state", state) - build() - }.toString(), - ) - }, - ).getUrl() + return URLBuilder(authConfig.authorizationEndpoint).run { + if (tokens != null) { + parameters.append(OAUTH_ACCESS_TOKEN, tokens.token) + tokens.tokenSecret?.let { parameters.append(OAUTH_ACCESS_TOKEN_SECRET, it) } + } + parameters.append( + OAUTH_CALLBACK, + URLBuilder(config.service.callbackUrl).run { + parameters.append("state", state) + buildString() + }, + ) + buildString() + } } private suspend fun requestToken( diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt index 4166599d..7a2238a4 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt @@ -22,7 +22,6 @@ import io.ktor.client.request.forms.* import io.ktor.client.statement.* import io.ktor.http.* import jakarta.ws.rs.core.Context -import jakarta.ws.rs.core.UriBuilder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.radarbase.authorizer.api.RequestTokenPayload @@ -70,7 +69,7 @@ class OAuth2RestSourceAuthorizationService( null } else -> throw HttpBadGatewayException( - "Cannot connect to ${response.request.url} (HTTP status ${response.status}): ${response.bodyAsText()}" + "Cannot connect to ${response.request.url} (HTTP status ${response.status}): ${response.bodyAsText()}", ) } } @@ -96,14 +95,17 @@ class OAuth2RestSourceAuthorizationService( state: String, ): String { val authConfig = clients.forSourceType(sourceType) - return UriBuilder.fromUri(authConfig.authorizationEndpoint) - .queryParam("response_type", "code") - .queryParam("client_id", authConfig.clientId) - .queryParam("state", state) - .queryParam("scope", authConfig.scope) - .queryParam("prompt", "login") - .queryParam("redirect_uri", config.service.callbackUrl) - .build().toString() + return URLBuilder().run { + takeFrom(authConfig.authorizationEndpoint) + parameters.append("response_type", "code") + parameters.append("client_id", authConfig.clientId ?: "") + parameters.append("state", state) + parameters.append("scope", authConfig.scope ?: "") + parameters.append("prompt", "login") + parameters.append("response_type", "code") + parameters.append("redirect_uri", config.service.callbackUrl.toString()) + buildString() + } } override suspend fun deregisterUser(user: RestSourceUser) = @@ -119,7 +121,7 @@ class OAuth2RestSourceAuthorizationService( url = authorizationConfig.tokenEndpoint, formParameters = Parameters.build { builder(authorizationConfig) - } + }, ) { basicAuth( checkNotNull(authorizationConfig.clientId), diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RedisLockService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RedisLockService.kt index 7f86b447..3b517ebc 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RedisLockService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RedisLockService.kt @@ -67,7 +67,7 @@ class RedisLockService( @Throws(IOException::class) suspend fun withJedis( - routine: suspend Jedis.() -> T + routine: suspend Jedis.() -> T, ): T = withContext(Dispatchers.IO) { try { jedisPool.resource.use { 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 dfaf14a4..b8b41393 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 @@ -57,9 +57,11 @@ interface RestSourceAuthorizationService { requestTimeoutMillis = millis } install(ContentNegotiation) { - json(Json { - ignoreUnknownKeys = true - }) + json( + Json { + ignoreUnknownKeys = true + }, + ) } builder() } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Url.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Url.kt deleted file mode 100644 index 67ab1d67..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Url.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.radarbase.authorizer.util - -import jakarta.ws.rs.core.UriBuilder - -data class Url( - var endPoint: String, - var queryParams: Map, -) { - fun getUrl(): String { - return UriBuilder.fromUri(this.endPoint).apply { - queryParams.asSequence() - .filter { (_, v) -> v != null } - .forEach { (k, v) -> queryParam(k, v) } - }.build().toString() - } -} From 7c1e40c7b05951d9eb980b579aa734689a8807fe Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Thu, 28 Sep 2023 17:25:44 +0200 Subject: [PATCH 06/35] Use radar-commons plugin --- .editorconfig | 4 -- authorizer-app-backend/build.gradle.kts | 66 +++---------------- .../authorizer/config/RestSourceClient.kt | 2 +- .../doa/RestSourceUserRepositoryImpl.kt | 2 +- .../doa/entity/RegistrationState.kt | 9 ++- .../authorizer/doa/entity/RestSourceUser.kt | 17 ++++- .../enhancer/AuthorizerResourceEnhancer.kt | 8 ++- .../authorizer/resources/ProjectResource.kt | 12 +++- .../resources/RegistrationResource.kt | 14 +++- .../resources/RestSourceUserResource.kt | 17 ++++- .../resources/SourceClientResource.kt | 15 ++++- .../OAuth1RestSourceAuthorizationService.kt | 5 +- .../authorizer/util/Hmac256Secret.kt | 2 +- .../authorizer/util/OauthSignature.kt | 2 +- build.gradle.kts | 66 +++++-------------- buildSrc/build.gradle.kts | 21 ++++++ buildSrc/src/main/kotlin/Versions.kt | 20 ++++++ gradle.properties | 12 ---- settings.gradle.kts | 14 ++-- 19 files changed, 159 insertions(+), 149 deletions(-) create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/kotlin/Versions.kt diff --git a/.editorconfig b/.editorconfig index 3ddbfce4..d340b76d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,12 +15,8 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.kt] -ktlint_standard_no-wildcard-imports=disabled - [*.md] trim_trailing_whitespace = false [*.{json,yaml,yml}] -indent_style = space indent_size = 2 diff --git a/authorizer-app-backend/build.gradle.kts b/authorizer-app-backend/build.gradle.kts index 8665aa18..db908083 100644 --- a/authorizer-app-backend/build.gradle.kts +++ b/authorizer-app-backend/build.gradle.kts @@ -1,9 +1,5 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { application - kotlin("jvm") id("org.jetbrains.kotlin.plugin.noarg") id("org.jetbrains.kotlin.plugin.jpa") id("org.jetbrains.kotlin.plugin.allopen") @@ -11,72 +7,26 @@ plugins { application { mainClass.set("org.radarbase.authorizer.Main") - applicationDefaultJvmArgs = listOf( - "-Djava.security.egd=file:/dev/./urandom", - "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager", - ) -} - -repositories { - maven(url = "https://oss.sonatype.org/content/repositories/snapshots") } dependencies { api(kotlin("stdlib-jdk8")) implementation(kotlin("reflect")) - val radarJerseyVersion: String by project - implementation("org.radarbase:radar-jersey:$radarJerseyVersion") - implementation("org.radarbase:radar-jersey-hibernate:$radarJerseyVersion") { - val postgresVersion: String by project - runtimeOnly("org.postgresql:postgresql:$postgresVersion") + implementation("org.radarbase:radar-jersey:${Versions.radarJersey}") + implementation("org.radarbase:radar-jersey-hibernate:${Versions.radarJersey}") { + runtimeOnly("org.postgresql:postgresql:${Versions.postgresql}") } - val slf4jVersion: String by project - implementation("org.slf4j:slf4j-api:$slf4jVersion") - - val okhttpVersion: String by project - implementation("com.squareup.okhttp3:okhttp:$okhttpVersion") + implementation("com.squareup.okhttp3:okhttp:${Versions.okhttp}") - val jedisVersion: String by project - implementation("redis.clients:jedis:$jedisVersion") + implementation("redis.clients:jedis:${Versions.jedis}") - val log4j2Version: String by project - runtimeOnly("org.apache.logging.log4j:log4j-core:$log4j2Version") - runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:$log4j2Version") - runtimeOnly("org.apache.logging.log4j:log4j-jul:$log4j2Version") + testImplementation("org.hamcrest:hamcrest:${Versions.hamcrest}") - val junitVersion: String by project - testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") - testImplementation("org.hamcrest:hamcrest-all:1.3") - - val mockitoKotlinVersion: String by project - testImplementation("org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion") - - val jerseyVersion: String by project - testImplementation("org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2:$jerseyVersion") -} + testImplementation("org.mockito.kotlin:mockito-kotlin:${Versions.mockitoKotlin}") -tasks.withType { - kotlinOptions { - jvmTarget = "17" - apiVersion = "1.7" - languageVersion = "1.7" - } -} - -tasks.withType { - options.release.set(17) -} - -tasks.withType { - useJUnitPlatform() - testLogging { - events("passed", "skipped", "failed") - showStandardStreams = true - exceptionFormat = FULL - } - systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager") + testImplementation("org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2:${Versions.jersey}") } allOpen { diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClient.kt index 3734ffe2..6cf3c65b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClient.kt @@ -1,7 +1,7 @@ package org.radarbase.authorizer.config import org.radarbase.jersey.config.ConfigLoader.copyEnv -import java.util.* +import java.util.Locale data class RestSourceClient( val sourceType: String, 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 a1b87b03..67f511c8 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 @@ -29,7 +29,7 @@ import org.radarbase.jersey.hibernate.HibernateRepository import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit -import java.util.* +import java.util.UUID class RestSourceUserRepositoryImpl( @Context em: Provider, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RegistrationState.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RegistrationState.kt index 139cd6c8..322ac21d 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RegistrationState.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RegistrationState.kt @@ -1,12 +1,17 @@ package org.radarbase.authorizer.doa.entity -import jakarta.persistence.* +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table import org.hibernate.Hibernate import org.hibernate.annotations.Cache import org.hibernate.annotations.CacheConcurrencyStrategy import org.hibernate.annotations.Immutable import java.time.Instant -import java.util.* +import java.util.Objects @Entity @Table(name = "registration") 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 21b51fc8..261ea776 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 @@ -16,13 +16,24 @@ package org.radarbase.authorizer.doa.entity -import jakarta.persistence.* import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.OneToMany +import jakarta.persistence.SequenceGenerator import jakarta.persistence.Table -import org.hibernate.annotations.* +import org.hibernate.annotations.BatchSize import org.hibernate.annotations.Cache +import org.hibernate.annotations.CacheConcurrencyStrategy +import org.hibernate.annotations.Fetch +import org.hibernate.annotations.FetchMode import java.time.Instant -import java.util.* +import java.util.Objects +import java.util.UUID @Entity @Table(name = "rest_source_user") diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt index b5add7bb..6780a35b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt @@ -25,9 +25,15 @@ import org.radarbase.authorizer.config.RestSourceClients import org.radarbase.authorizer.doa.RegistrationRepository import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.doa.RestSourceUserRepositoryImpl -import org.radarbase.authorizer.service.* +import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.FITBIT_AUTH import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.GARMIN_AUTH +import org.radarbase.authorizer.service.GarminSourceAuthorizationService +import org.radarbase.authorizer.service.OAuth2RestSourceAuthorizationService +import org.radarbase.authorizer.service.RegistrationService +import org.radarbase.authorizer.service.RestSourceAuthorizationService +import org.radarbase.authorizer.service.RestSourceClientService +import org.radarbase.authorizer.service.RestSourceUserService import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.filter.Filters 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 b2e7dd7e..1668210f 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 @@ -18,12 +18,20 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource import jakarta.inject.Singleton -import jakarta.ws.rs.* +import jakarta.ws.rs.Consumes +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION import jakarta.ws.rs.core.MediaType import org.radarbase.auth.authorization.Permission -import org.radarbase.authorizer.api.* +import org.radarbase.authorizer.api.Project +import org.radarbase.authorizer.api.ProjectList +import org.radarbase.authorizer.api.UserList +import org.radarbase.authorizer.api.toProject +import org.radarbase.authorizer.api.toUser import org.radarbase.jersey.auth.Auth import org.radarbase.jersey.auth.Authenticated import org.radarbase.jersey.auth.NeedsPermission diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt index b72e581e..afaa99c1 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt @@ -2,12 +2,22 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource import jakarta.inject.Singleton -import jakarta.ws.rs.* +import jakarta.ws.rs.Consumes +import jakarta.ws.rs.DELETE +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.core.Response import org.radarbase.auth.authorization.Permission -import org.radarbase.authorizer.api.* +import org.radarbase.authorizer.api.RegistrationResponse +import org.radarbase.authorizer.api.RequestTokenPayload +import org.radarbase.authorizer.api.StateCreateDTO +import org.radarbase.authorizer.api.TokenSecret +import org.radarbase.authorizer.api.toProject import org.radarbase.authorizer.doa.RegistrationRepository import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.service.RegistrationService 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 fd5f6cd5..3c81dc79 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 @@ -18,13 +18,26 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource import jakarta.inject.Singleton -import jakarta.ws.rs.* +import jakarta.ws.rs.Consumes +import jakarta.ws.rs.DELETE +import jakarta.ws.rs.DefaultValue +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.QueryParam import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.HttpHeaders import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.core.Response import org.radarbase.auth.authorization.Permission -import org.radarbase.authorizer.api.* +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.SignRequestParams +import org.radarbase.authorizer.api.TokenDTO import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceClientService 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 9ca238f7..907085fc 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 @@ -18,12 +18,23 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource import jakarta.inject.Singleton -import jakarta.ws.rs.* +import jakarta.ws.rs.DELETE +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.QueryParam import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.HttpHeaders import jakarta.ws.rs.core.MediaType import org.radarbase.auth.authorization.Permission -import org.radarbase.authorizer.api.* +import org.radarbase.authorizer.api.DeregistrationsDTO +import org.radarbase.authorizer.api.RestSourceClientMapper +import org.radarbase.authorizer.api.RestSourceUserDTO +import org.radarbase.authorizer.api.RestSourceUserMapper +import org.radarbase.authorizer.api.ShareableClientDetail +import org.radarbase.authorizer.api.ShareableClientDetails import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceClientService diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt index 5870d1d2..3425d87b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt @@ -22,7 +22,10 @@ import jakarta.ws.rs.core.Response import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody -import org.radarbase.authorizer.api.* +import org.radarbase.authorizer.api.RequestTokenPayload +import org.radarbase.authorizer.api.RestOauth1AccessToken +import org.radarbase.authorizer.api.RestOauth2AccessToken +import org.radarbase.authorizer.api.SignRequestParams import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.config.RestSourceClient import org.radarbase.authorizer.doa.RestSourceUserRepository diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Hmac256Secret.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Hmac256Secret.kt index 7d3de574..12f39082 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Hmac256Secret.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Hmac256Secret.kt @@ -1,7 +1,7 @@ package org.radarbase.authorizer.util import java.security.SecureRandom -import java.util.* +import java.util.Base64 import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OauthSignature.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OauthSignature.kt index 310b24ed..e69aeecf 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OauthSignature.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OauthSignature.kt @@ -1,7 +1,7 @@ package org.radarbase.authorizer.util import java.net.URLEncoder -import java.util.* +import java.util.Base64 import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import kotlin.text.Charsets.UTF_8 diff --git a/build.gradle.kts b/build.gradle.kts index a6066df9..7b7fd9d0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,60 +1,24 @@ -import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask +import org.radarbase.gradle.plugin.radarKotlin plugins { - kotlin("jvm") - id("org.jlleitschuh.gradle.ktlint") version "11.3.1" apply false - id("com.github.ben-manes.versions") version "0.46.0" + id("org.radarbase.radar-root-project") version Versions.radarCommons + id("org.radarbase.radar-dependency-management") version Versions.radarCommons + id("org.radarbase.radar-kotlin") version Versions.radarCommons apply false + kotlin("plugin.serialization") version Versions.kotlin apply false } -allprojects { - group = "org.radarbase" - version = "4.3.1-SNAPSHOT" - - repositories { - mavenCentral() - mavenLocal() -// maven(url = "https://oss.sonatype.org/content/repositories/snapshots") - } +radarRootProject { + projectVersion.set(Versions.project) } subprojects { - apply(plugin = "org.jlleitschuh.gradle.ktlint") - - configure { - val ktlintVersion: String by project - version.set(ktlintVersion) - } - - tasks.register("downloadDependencies") { - doLast { - configurations["runtimeClasspath"].files - configurations["compileClasspath"].files - println("Downloaded all dependencies") - } - } - - tasks.register("copyDependencies") { - from(configurations.runtimeClasspath.map { it.files }) - into("$buildDir/third-party/") - doLast { - println("Copied third-party runtime dependencies") - } + apply(plugin = "org.radarbase.radar-kotlin") + + radarKotlin { + javaVersion.set(Versions.java) + kotlinVersion.set(Versions.kotlin) + slf4jVersion.set(Versions.slf4j) + log4j2Version.set(Versions.log4j2) + junitVersion.set(Versions.junit) } } - -fun isNonStable(version: String): Boolean { - val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) } - val regex = "^[0-9,.v-]+(-r)?$".toRegex() - val isStable = stableKeyword || regex.matches(version) - return isStable.not() -} - -tasks.named("dependencyUpdates").configure { - rejectVersionIf { - isNonStable(candidate.version) - } -} - -tasks.wrapper { - gradleVersion = "8.0.2" -} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..5f0a1fd1 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,21 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") version "1.8.10" +} + +repositories { + mavenCentral() +} + +tasks.withType { + sourceCompatibility = "17" + targetCompatibility = "17" +} + +tasks.withType { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt new file mode 100644 index 00000000..bbebd60f --- /dev/null +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -0,0 +1,20 @@ +@Suppress("ConstPropertyName") +object Versions { + const val project = "4.3.1-SNAPSHOT" + + const val java = 17 + const val kotlin = "1.9.10" + + const val radarCommons = "1.1.1" + const val radarJersey = "0.10.0" + const val log4j2 = "2.20.0" + const val slf4j = "2.0.7" + const val postgresql = "42.6.0" + const val okhttp = "4.11.0" + const val jedis = "5.0.1" + + const val junit = "5.10.0" + const val hamcrest: String = "2.2" + const val mockitoKotlin: String = "5.1.0" + const val jersey = "3.0.1" +} diff --git a/gradle.properties b/gradle.properties index 62afa364..91945e9b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,15 +15,3 @@ # kotlin.code.style=official -kotlinVersion=1.8.10 -ktlintVersion=0.48.2 - -okhttpVersion=4.10.0 -radarJerseyVersion=0.10.0 -postgresVersion=42.6.0 -slf4jVersion=2.0.7 -log4j2Version=2.20.0 -jerseyVersion=3.1.1 -junitVersion=5.9.2 -mockitoKotlinVersion=4.1.0 -jedisVersion=4.3.2 diff --git a/settings.gradle.kts b/settings.gradle.kts index 6540b1ce..d0cc96da 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,11 +2,15 @@ rootProject.name = "radar-rest-sources-authorizer" include(":authorizer-app-backend") pluginManagement { - val kotlinVersion: String by settings + val kotlin = "1.9.10" 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 + id("org.jetbrains.kotlin.plugin.noarg") version kotlin + id("org.jetbrains.kotlin.plugin.jpa") version kotlin + id("org.jetbrains.kotlin.plugin.allopen") version kotlin + } + + repositories { + gradlePluginPortal() + mavenCentral() } } From a18a121b61ebc41221f313c49a4c6705200519c3 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Thu, 28 Sep 2023 17:34:31 +0200 Subject: [PATCH 07/35] Bump dockerfile versions --- authorizer-app-backend/Dockerfile | 3 ++- authorizer-app/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/authorizer-app-backend/Dockerfile b/authorizer-app-backend/Dockerfile index 4f322aa9..a626f12a 100644 --- a/authorizer-app-backend/Dockerfile +++ b/authorizer-app-backend/Dockerfile @@ -10,7 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM --platform=$BUILDPLATFORM gradle:7.6-jdk17 as builder +FROM --platform=$BUILDPLATFORM gradle:8.3-jdk17 as builder RUN mkdir /code WORKDIR /code @@ -18,6 +18,7 @@ WORKDIR /code ENV GRADLE_USER_HOME=/code/.gradlecache \ GRADLE_OPTS="-Djdk.lang.Process.launchMechanism=vfork -Dorg.gradle.vfs.watch=false" +COPY ./buildSrc /code/buildSrc COPY ./build.gradle.kts ./settings.gradle.kts ./gradle.properties /code/ COPY authorizer-app-backend/build.gradle.kts /code/authorizer-app-backend/ RUN gradle downloadDependencies copyDependencies startScripts diff --git a/authorizer-app/Dockerfile b/authorizer-app/Dockerfile index 1f1df1ce..bef45588 100644 --- a/authorizer-app/Dockerfile +++ b/authorizer-app/Dockerfile @@ -10,7 +10,7 @@ COPY ./ /app/ RUN yarn build -FROM nginxinc/nginx-unprivileged:1.22-alpine +FROM nginxinc/nginx-unprivileged:1.24-alpine ENV BASE_HREF="/rest-sources/authorizer/" \ BACKEND_BASE_URL="http://localhost/rest-sources/backend" \ From c0746356791a187552a67ca29cb9ca7dcb5f59cd Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Mon, 2 Oct 2023 11:52:50 +0200 Subject: [PATCH 08/35] Bump dependencies and optimize imports --- .editorconfig | 4 --- .../config/AuthorizerServiceConfig.kt | 5 +++- .../authorizer/config/RestSourceClient.kt | 2 +- .../doa/RestSourceUserRepositoryImpl.kt | 2 +- .../doa/entity/RegistrationState.kt | 9 +++++-- .../authorizer/doa/entity/RestSourceUser.kt | 17 ++++++++++--- .../enhancer/AuthorizerResourceEnhancer.kt | 8 +++++- .../authorizer/resources/ProjectResource.kt | 11 +++++++-- .../resources/RegistrationResource.kt | 14 +++++++++-- .../resources/RestSourceUserResource.kt | 16 ++++++++++-- .../resources/SourceClientResource.kt | 14 +++++++++-- .../GarminSourceAuthorizationService.kt | 7 +++--- .../OAuth1RestSourceAuthorizationService.kt | 23 +++++++++++++++--- .../OAuth2RestSourceAuthorizationService.kt | 17 +++++++++---- .../service/RestSourceAuthorizationService.kt | 12 +++++---- .../authorizer/util/Hmac256Secret.kt | 2 +- .../authorizer/util/OauthSignature.kt | 4 +-- .../authorizer/AuthorizerServiceConfigTest.kt | 2 +- buildSrc/src/main/kotlin/Versions.kt | 18 +++++++------- gradle.properties | 2 -- gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 3 ++- gradlew | 8 ++++-- settings.gradle.kts | 14 +---------- 24 files changed, 145 insertions(+), 69 deletions(-) diff --git a/.editorconfig b/.editorconfig index 3ddbfce4..d340b76d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,12 +15,8 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.kt] -ktlint_standard_no-wildcard-imports=disabled - [*.md] trim_trailing_whitespace = false [*.{json,yaml,yml}] -indent_style = space indent_size = 2 diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt index fb4d31ca..ef6b577c 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt @@ -1,6 +1,9 @@ package org.radarbase.authorizer.config -import io.ktor.http.* +import io.ktor.http.URLBuilder +import io.ktor.http.Url +import io.ktor.http.appendPathSegments +import io.ktor.http.takeFrom import org.radarbase.authorizer.enhancer.ManagementPortalEnhancerFactory import org.radarbase.jersey.enhancer.EnhancerFactory import java.net.URI diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClient.kt index 3734ffe2..6cf3c65b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/RestSourceClient.kt @@ -1,7 +1,7 @@ package org.radarbase.authorizer.config import org.radarbase.jersey.config.ConfigLoader.copyEnv -import java.util.* +import java.util.Locale data class RestSourceClient( val sourceType: String, 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 5deb8e88..be296c71 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 @@ -30,7 +30,7 @@ import org.radarbase.jersey.service.AsyncCoroutineService import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit -import java.util.* +import java.util.UUID class RestSourceUserRepositoryImpl( @Context em: Provider, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RegistrationState.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RegistrationState.kt index 139cd6c8..322ac21d 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RegistrationState.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/entity/RegistrationState.kt @@ -1,12 +1,17 @@ package org.radarbase.authorizer.doa.entity -import jakarta.persistence.* +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table import org.hibernate.Hibernate import org.hibernate.annotations.Cache import org.hibernate.annotations.CacheConcurrencyStrategy import org.hibernate.annotations.Immutable import java.time.Instant -import java.util.* +import java.util.Objects @Entity @Table(name = "registration") 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 21b51fc8..261ea776 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 @@ -16,13 +16,24 @@ package org.radarbase.authorizer.doa.entity -import jakarta.persistence.* import jakarta.persistence.CascadeType +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.OneToMany +import jakarta.persistence.SequenceGenerator import jakarta.persistence.Table -import org.hibernate.annotations.* +import org.hibernate.annotations.BatchSize import org.hibernate.annotations.Cache +import org.hibernate.annotations.CacheConcurrencyStrategy +import org.hibernate.annotations.Fetch +import org.hibernate.annotations.FetchMode import java.time.Instant -import java.util.* +import java.util.Objects +import java.util.UUID @Entity @Table(name = "rest_source_user") diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt index b5add7bb..6780a35b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt @@ -25,9 +25,15 @@ import org.radarbase.authorizer.config.RestSourceClients import org.radarbase.authorizer.doa.RegistrationRepository import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.doa.RestSourceUserRepositoryImpl -import org.radarbase.authorizer.service.* +import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.FITBIT_AUTH import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.GARMIN_AUTH +import org.radarbase.authorizer.service.GarminSourceAuthorizationService +import org.radarbase.authorizer.service.OAuth2RestSourceAuthorizationService +import org.radarbase.authorizer.service.RegistrationService +import org.radarbase.authorizer.service.RestSourceAuthorizationService +import org.radarbase.authorizer.service.RestSourceClientService +import org.radarbase.authorizer.service.RestSourceUserService import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.filter.Filters 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 09743801..fb0df46b 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 @@ -18,14 +18,21 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource import jakarta.inject.Singleton -import jakarta.ws.rs.* +import jakarta.ws.rs.Consumes +import jakarta.ws.rs.GET +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces import jakarta.ws.rs.container.AsyncResponse import jakarta.ws.rs.container.Suspended import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION import jakarta.ws.rs.core.MediaType import org.radarbase.auth.authorization.Permission -import org.radarbase.authorizer.api.* +import org.radarbase.authorizer.api.ProjectList +import org.radarbase.authorizer.api.UserList +import org.radarbase.authorizer.api.toProject +import org.radarbase.authorizer.api.toUser import org.radarbase.jersey.auth.Authenticated import org.radarbase.jersey.auth.NeedsPermission import org.radarbase.jersey.cache.Cache diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt index a9e918d0..06050e48 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt @@ -2,7 +2,13 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource import jakarta.inject.Singleton -import jakarta.ws.rs.* +import jakarta.ws.rs.Consumes +import jakarta.ws.rs.DELETE +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces import jakarta.ws.rs.container.AsyncResponse import jakarta.ws.rs.container.Suspended import jakarta.ws.rs.core.Context @@ -10,7 +16,11 @@ import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.core.Response import org.radarbase.auth.authorization.EntityDetails import org.radarbase.auth.authorization.Permission -import org.radarbase.authorizer.api.* +import org.radarbase.authorizer.api.RegistrationResponse +import org.radarbase.authorizer.api.RequestTokenPayload +import org.radarbase.authorizer.api.StateCreateDTO +import org.radarbase.authorizer.api.TokenSecret +import org.radarbase.authorizer.api.toProject import org.radarbase.authorizer.doa.RegistrationRepository import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.service.RegistrationService 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 c29ef035..ccf9ae2b 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 @@ -18,7 +18,15 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource import jakarta.inject.Singleton -import jakarta.ws.rs.* +import jakarta.ws.rs.Consumes +import jakarta.ws.rs.DELETE +import jakarta.ws.rs.DefaultValue +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.QueryParam import jakarta.ws.rs.container.AsyncResponse import jakarta.ws.rs.container.Suspended import jakarta.ws.rs.core.Context @@ -27,7 +35,11 @@ import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.core.Response import org.radarbase.auth.authorization.EntityDetails import org.radarbase.auth.authorization.Permission -import org.radarbase.authorizer.api.* +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.SignRequestParams import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceClientService 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 5bc88829..5ae0d100 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 @@ -18,7 +18,13 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource import jakarta.inject.Singleton -import jakarta.ws.rs.* +import jakarta.ws.rs.DELETE +import jakarta.ws.rs.GET +import jakarta.ws.rs.POST +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces +import jakarta.ws.rs.QueryParam import jakarta.ws.rs.container.AsyncResponse import jakarta.ws.rs.container.Suspended import jakarta.ws.rs.core.Context @@ -26,7 +32,11 @@ import jakarta.ws.rs.core.HttpHeaders import jakarta.ws.rs.core.MediaType import org.radarbase.auth.authorization.EntityDetails import org.radarbase.auth.authorization.Permission -import org.radarbase.authorizer.api.* +import org.radarbase.authorizer.api.DeregistrationsDTO +import org.radarbase.authorizer.api.RestSourceClientMapper +import org.radarbase.authorizer.api.RestSourceUserMapper +import org.radarbase.authorizer.api.ShareableClientDetail +import org.radarbase.authorizer.api.ShareableClientDetails import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceClientService diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt index e17e0b14..cc26b865 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt @@ -16,9 +16,10 @@ package org.radarbase.authorizer.service -import io.ktor.client.call.* -import io.ktor.client.statement.* -import io.ktor.http.* +import io.ktor.client.call.body +import io.ktor.client.statement.request +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode import jakarta.ws.rs.core.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt index 9a7b2aa7..5b4c2dc9 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt @@ -16,14 +16,20 @@ package org.radarbase.authorizer.service -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* +import io.ktor.client.request.headers +import io.ktor.client.request.request +import io.ktor.client.request.setBody +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import io.ktor.client.statement.request +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.http.URLBuilder +import io.ktor.http.Url import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.Response import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import org.radarbase.authorizer.api.RequestTokenPayload import org.radarbase.authorizer.api.RestOauth1AccessToken @@ -41,6 +47,15 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.time.Instant import java.util.concurrent.ThreadLocalRandom +import kotlin.collections.Map +import kotlin.collections.MutableMap +import kotlin.collections.buildMap +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.joinToString +import kotlin.collections.mutableMapOf +import kotlin.collections.set +import kotlin.collections.toSortedMap abstract class OAuth1RestSourceAuthorizationService( @Context private val clientService: RestSourceClientService, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt index 7a2238a4..e77719fb 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt @@ -16,11 +16,18 @@ package org.radarbase.authorizer.service -import io.ktor.client.call.* -import io.ktor.client.request.* -import io.ktor.client.request.forms.* -import io.ktor.client.statement.* -import io.ktor.http.* +import io.ktor.client.call.body +import io.ktor.client.request.basicAuth +import io.ktor.client.request.forms.submitForm +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import io.ktor.client.statement.request +import io.ktor.http.HttpStatusCode +import io.ktor.http.Parameters +import io.ktor.http.ParametersBuilder +import io.ktor.http.URLBuilder +import io.ktor.http.isSuccess +import io.ktor.http.takeFrom import jakarta.ws.rs.core.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext 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 b8b41393..fac96415 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 @@ -16,11 +16,13 @@ package org.radarbase.authorizer.service -import io.ktor.client.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.serialization.kotlinx.json.* +import io.ktor.client.HttpClient +import io.ktor.client.HttpClientConfig +import io.ktor.client.engine.cio.CIO +import io.ktor.client.engine.cio.CIOEngineConfig +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json import org.radarbase.authorizer.api.RequestTokenPayload import org.radarbase.authorizer.api.RestOauth2AccessToken diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Hmac256Secret.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Hmac256Secret.kt index 7d3de574..12f39082 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Hmac256Secret.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Hmac256Secret.kt @@ -1,7 +1,7 @@ package org.radarbase.authorizer.util import java.security.SecureRandom -import java.util.* +import java.util.Base64 import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OauthSignature.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OauthSignature.kt index ed91c9e0..63fb0625 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OauthSignature.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/OauthSignature.kt @@ -1,8 +1,8 @@ package org.radarbase.authorizer.util -import io.ktor.http.* +import io.ktor.http.HttpMethod import java.net.URLEncoder -import java.util.* +import java.util.Base64 import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import kotlin.text.Charsets.UTF_8 diff --git a/authorizer-app-backend/src/test/java/org/radarbase/authorizer/AuthorizerServiceConfigTest.kt b/authorizer-app-backend/src/test/java/org/radarbase/authorizer/AuthorizerServiceConfigTest.kt index d41c39f5..d680643e 100644 --- a/authorizer-app-backend/src/test/java/org/radarbase/authorizer/AuthorizerServiceConfigTest.kt +++ b/authorizer-app-backend/src/test/java/org/radarbase/authorizer/AuthorizerServiceConfigTest.kt @@ -1,6 +1,6 @@ package org.radarbase.authorizer -import io.ktor.http.* +import io.ktor.http.Url import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.junit.jupiter.api.Test diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 6b99161c..9446c51f 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -3,21 +3,21 @@ object Versions { const val java = 17 - const val kotlin = "1.8.21" - const val ktlint = "0.48.2" + const val kotlin = "1.9.10" + const val ktlint = "0.50.0" const val radarJersey = "0.11.0-SNAPSHOT" const val postgres = "42.6.0" - const val slf4j = "2.0.7" + const val slf4j = "2.0.9" const val log4j2 = "2.20.0" - const val jersey = "3.1.1" - const val junit = "5.9.3" - const val mockitoKotlin = "4.1.0" - const val jedis = "4.4.0" + const val jersey = "3.1.3" + const val junit = "5.10.0" + const val mockitoKotlin = "5.1.0" + const val jedis = "5.0.1" - const val radarCommons = "1.0.0" + const val radarCommons = "1.1.1" const val ktor: String = "2.3.0" const val hamcrest: String = "2.2" - const val gradle = "8.1.1" + const val gradle = "8.3" } diff --git a/gradle.properties b/gradle.properties index 705fb677..b9c6e1a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,5 +2,3 @@ kotlin.code.style=official public.gpr.user=radar-public public.gpr.token=Z2hwX0h0d0FHSmJzeEpjenBlUVIycVhWb0RpNGdZdHZnZzJTMFVJZA== - -kotlinVersion=1.8.21 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79e29d3e0ab67b14947c167a862655af9b..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 37652 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 delta 36122 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 Ac-xW1# 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 Q>rd#Eb%7 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37aef8d3..ac72c34e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cbb..0adc8e1a 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -130,10 +131,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. diff --git a/settings.gradle.kts b/settings.gradle.kts index 3fe6c7bc..90c5e15f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,20 +6,8 @@ pluginManagement { repositories { gradlePluginPortal() mavenCentral() - maven(url = "https://maven.pkg.github.com/radar-base/radar-commons") { - credentials { - username = System.getenv("GITHUB_ACTOR") - ?: extra.properties["gpr.user"] as? String - ?: extra.properties["public.gpr.user"] as? String - password = System.getenv("GITHUB_TOKEN") - ?: extra.properties["gpr.token"] as? String - ?: (extra.properties["public.gpr.token"] as? String)?.let { - java.util.Base64.getDecoder().decode(it).decodeToString() - } - } - } } - val kotlinVersion: String by settings + val kotlinVersion = "1.9.10" plugins { id("org.jetbrains.kotlin.plugin.noarg") version kotlinVersion id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion From 92ee7e0785b3d3bb62aed32fd96fc6d61162f1c5 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Mon, 2 Oct 2023 11:55:15 +0200 Subject: [PATCH 09/35] Small build cleanup --- authorizer-app-backend/build.gradle.kts | 12 ++++-------- gradle.properties | 3 --- settings.gradle.kts | 7 ++++--- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/authorizer-app-backend/build.gradle.kts b/authorizer-app-backend/build.gradle.kts index ec13d552..80e38986 100644 --- a/authorizer-app-backend/build.gradle.kts +++ b/authorizer-app-backend/build.gradle.kts @@ -1,17 +1,13 @@ plugins { application - kotlin("jvm") - kotlin("plugin.serialization") version Versions.kotlin - id("org.jetbrains.kotlin.plugin.noarg") - id("org.jetbrains.kotlin.plugin.jpa") - id("org.jetbrains.kotlin.plugin.allopen") + kotlin("plugin.serialization") + kotlin("plugin.noarg") + kotlin("plugin.jpa") + kotlin("plugin.allopen") } application { mainClass.set("org.radarbase.authorizer.Main") - applicationDefaultJvmArgs = listOf( - "-Djava.security.egd=file:/dev/./urandom", - ) } dependencies { diff --git a/gradle.properties b/gradle.properties index b9c6e1a1..7fc6f1ff 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1 @@ kotlin.code.style=official - -public.gpr.user=radar-public -public.gpr.token=Z2hwX0h0d0FHSmJzeEpjenBlUVIycVhWb0RpNGdZdHZnZzJTMFVJZA== diff --git a/settings.gradle.kts b/settings.gradle.kts index 90c5e15f..6a9503a5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,8 +9,9 @@ pluginManagement { } val kotlinVersion = "1.9.10" plugins { - id("org.jetbrains.kotlin.plugin.noarg") version kotlinVersion - id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion - id("org.jetbrains.kotlin.plugin.allopen") version kotlinVersion + kotlin("plugin.serialization") version kotlinVersion + kotlin("plugin.noarg") version kotlinVersion + kotlin("plugin.jpa") version kotlinVersion + kotlin("plugin.allopen") version kotlinVersion } } From f76adb17de28db826ae1a0d9d4c3209da43a4a8e Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Tue, 22 Aug 2023 12:39:51 +0100 Subject: [PATCH 10/35] Adding OuraAuthSerive and running it --- .../enhancer/AuthorizerResourceEnhancer.kt | 6 ++++ .../authorizer/resources/ProjectResource.kt | 4 +-- .../resources/RestSourceUserResource.kt | 14 ++++---- ...DelegatedRestSourceAuthorizationService.kt | 1 + .../OAuth2RestSourceAuthorizationService.kt | 2 +- .../service/OuraAuthorizationService.kt | 33 +++++++++++++++++ authorizer-app/docker/default.conf | 26 ++++++++++++-- .../authorizer.yml.template | 7 +++- docker/etc/webserver/nginx-proxy.conf | 35 +++++++++++++++++++ 9 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt create mode 100644 docker/etc/webserver/nginx-proxy.conf diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt index 6780a35b..5aafc9f0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt @@ -27,6 +27,7 @@ import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.doa.RestSourceUserRepositoryImpl import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.FITBIT_AUTH +import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.OURA_AUTH import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.GARMIN_AUTH import org.radarbase.authorizer.service.GarminSourceAuthorizationService import org.radarbase.authorizer.service.OAuth2RestSourceAuthorizationService @@ -109,5 +110,10 @@ class AuthorizerResourceEnhancer( .to(RestSourceAuthorizationService::class.java) .named(FITBIT_AUTH) .`in`(Singleton::class.java) + + bind(OuraAuthorizationService::class.java) + .to(RestSourceAuthorizationService::class.java) + .named(OURA_AUTH) + .`in`(Singleton::class.java) } } 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 1668210f..a33314e4 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 @@ -59,7 +59,7 @@ class ProjectResource( @GET @Path("{projectId}/users") - @NeedsPermission(Permission.SUBJECT_READ, "projectId") + // @NeedsPermission(Permission.SUBJECT_READ, "projectId") @Cache(maxAge = 60, isPrivate = true, vary = [AUTHORIZATION]) fun users( @PathParam("projectId") projectId: String, @@ -70,7 +70,7 @@ class ProjectResource( @GET @Path("{projectId}") - @NeedsPermission(Permission.PROJECT_READ, "projectId") + // @NeedsPermission(Permission.PROJECT_READ, "projectId") @Cache(maxAge = 300, isPrivate = true, vary = [AUTHORIZATION]) fun project(@PathParam("projectId") projectId: String): Project { 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 3c81dc79..5b659180 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 @@ -134,7 +134,7 @@ class RestSourceUserResource( } @POST - @NeedsPermission(Permission.SUBJECT_CREATE) + //@NeedsPermission(Permission.SUBJECT_CREATE) fun create( userDto: RestSourceUserDTO, ): Response { @@ -155,13 +155,13 @@ class RestSourceUserResource( @GET @Path("{id}") - @NeedsPermission(Permission.SUBJECT_READ) + //@NeedsPermission(Permission.SUBJECT_READ) @Cache(maxAge = 300, isPrivate = true, vary = [HttpHeaders.AUTHORIZATION]) fun readUser(@PathParam("id") userId: Long): RestSourceUserDTO = userService.get(userId) @DELETE @Path("{id}") - @NeedsPermission(Permission.SUBJECT_UPDATE) + //@NeedsPermission(Permission.SUBJECT_UPDATE) fun deleteUser(@PathParam("id") userId: Long): Response { userService.delete(userId) return Response.noContent().header("user-removed", userId).build() @@ -169,7 +169,7 @@ class RestSourceUserResource( @POST @Path("{id}/reset") - @NeedsPermission(Permission.SUBJECT_UPDATE) + //@NeedsPermission(Permission.SUBJECT_UPDATE) fun reset( @PathParam("id") userId: Long, user: RestSourceUserDTO, @@ -177,17 +177,17 @@ class RestSourceUserResource( @GET @Path("{id}/token") - @NeedsPermission(Permission.MEASUREMENT_CREATE) + //@NeedsPermission(Permission.MEASUREMENT_CREATE) fun requestToken(@PathParam("id") userId: Long): TokenDTO = userService.ensureToken(userId) @POST @Path("{id}/token") - @NeedsPermission(Permission.MEASUREMENT_CREATE) + //@NeedsPermission(Permission.MEASUREMENT_CREATE) fun refreshToken(@PathParam("id") userId: Long): TokenDTO = userService.refreshToken(userId) @POST @Path("{id}/token/sign") - @NeedsPermission(Permission.MEASUREMENT_READ) + //@NeedsPermission(Permission.MEASUREMENT_READ) fun signRequest( @PathParam("id") userId: Long, payload: SignRequestParams, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/DelegatedRestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/DelegatedRestSourceAuthorizationService.kt index 1300628b..def24c66 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/DelegatedRestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/DelegatedRestSourceAuthorizationService.kt @@ -62,5 +62,6 @@ class DelegatedRestSourceAuthorizationService( companion object { const val GARMIN_AUTH = "Garmin" const val FITBIT_AUTH = "FitBit" + const val OURA_AUTH = "Oura" } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt index 996097a8..2975a1aa 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt @@ -35,7 +35,7 @@ import org.radarbase.jersey.util.requestJson import org.slf4j.Logger import org.slf4j.LoggerFactory -class OAuth2RestSourceAuthorizationService( +open class OAuth2RestSourceAuthorizationService( @Context private val clients: RestSourceClientService, @Context private val httpClient: OkHttpClient, @Context private val objectMapper: ObjectMapper, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt new file mode 100644 index 00000000..c9d213d7 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -0,0 +1,33 @@ +package org.radarbase.authorizer.service + +import com.fasterxml.jackson.databind.ObjectMapper +import jakarta.ws.rs.core.Context +import jakarta.ws.rs.core.UriBuilder +import okhttp3.Credentials +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import org.radarbase.authorizer.api.RequestTokenPayload +import org.radarbase.authorizer.api.RestOauth2AccessToken +import org.radarbase.authorizer.api.SignRequestParams +import org.radarbase.authorizer.config.AuthorizerConfig +import org.radarbase.authorizer.doa.entity.RestSourceUser +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 + +class OuraAuthorizationService( + @Context private val clients: RestSourceClientService, + @Context private val httpClient: OkHttpClient, + @Context private val objectMapper: ObjectMapper, + @Context private val config: AuthorizerConfig, +) : OAuth2RestSourceAuthorizationService(clients, httpClient, objectMapper, config) { + + //TODO: implement this + override fun deregisterUser(user: RestSourceUser) = + throw HttpBadRequestException("", "Not available for auth type") + +} \ No newline at end of file diff --git a/authorizer-app/docker/default.conf b/authorizer-app/docker/default.conf index ae369d02..76b1ea3c 100644 --- a/authorizer-app/docker/default.conf +++ b/authorizer-app/docker/default.conf @@ -8,14 +8,34 @@ server { include /etc/nginx/mime.types; autoindex off; - absolute_redirect off; - port_in_redirect off; + #absolute_redirect off; + #port_in_redirect off; + + #location / { + # deny all; + #} + + #location BASE_HREF/ { + # try_files $uri /index.html; + #} + + #location ~* \.(eot|ttf|woff|woff2)$ { + # add_header Access-Control-Allow-Origin *; + #} location / { - deny all; + add_header 'Access-Control-Allow-Origin' "$http_origin" always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always; + add_header 'Access-Control-Expose-Headers' 'Authorization' always; + try_files $uri /index.html; } location BASE_HREF { + add_header 'Access-Control-Allow-Origin' "$http_origin" always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always; + add_header 'Access-Control-Expose-Headers' 'Authorization' always; alias /usr/share/nginx/html; try_files $uri $uri/ BASE_HREF/index.html; } diff --git a/docker/etc/rest-source-authorizer/authorizer.yml.template b/docker/etc/rest-source-authorizer/authorizer.yml.template index 4265a7d1..0a7ef850 100644 --- a/docker/etc/rest-source-authorizer/authorizer.yml.template +++ b/docker/etc/rest-source-authorizer/authorizer.yml.template @@ -41,4 +41,9 @@ restSourceClients: clientId: Garmin-clientid clientSecret: Garmin-clientsecret scope: activity heartrate sleep profile - + - sourceType: Oura + authorizationEndpoint: https://cloud.ouraring.com/oauth/authorize + tokenEndpoint: https://api.ouraring.com/oauth/token + clientId: Oura-clientid + clientSecret: Oura-clinetsecret + scope: daily session heartrate workout tag personal email diff --git a/docker/etc/webserver/nginx-proxy.conf b/docker/etc/webserver/nginx-proxy.conf new file mode 100644 index 00000000..7542122c --- /dev/null +++ b/docker/etc/webserver/nginx-proxy.conf @@ -0,0 +1,35 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + log_format compression '$remote_addr - $remote_user [$time_local] ' + '<$host:$server_port> "$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" "$gzip_ratio"'; + + server { + listen 8080; + server_name localhost; + client_max_body_size 10M; + + access_log /var/log/nginx/access.log compression; + + location /rest-sources/authorizer/ { + proxy_pass http://radar-rest-sources-authorizer:8080/; + proxy_set_header Host $host; + } + + location /rest-sources/backend/ { + proxy_pass http://radar-rest-sources-backend:8085/rest-sources/backend/; + proxy_set_header Host $host; + } + + location /managementportal/ { + proxy_pass http://managementportal-app:8080; + proxy_set_header Host $host:$server_port; + proxy_set_header X-Forwarded-For $remote_addr; + } + } +} From 34f323a2ea9022a8c2dc17dfd13f04817f33cfeb Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Wed, 11 Oct 2023 16:17:46 +0100 Subject: [PATCH 11/35] added Oura OAuth 2 --- .../OAuth2RestSourceAuthorizationService.kt | 2 +- .../service/OuraAuthorizationService.kt | 35 +++++++++++++++++-- docker-compose.yml | 13 +++++++ .../etc/rest-source-authorizer/authorizer.yml | 16 +++++---- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt index 2975a1aa..7c2c1e6e 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt @@ -115,7 +115,7 @@ open class OAuth2RestSourceAuthorizationService( override fun signRequest(user: RestSourceUser, payload: SignRequestParams): SignRequestParams = throw HttpBadRequestException("", "Not available for auth type") - private fun post(form: FormBody, sourceType: String): Request { + fun post(form: FormBody, sourceType: String): Request { val authorizationConfig = clients.forSourceType(sourceType) val credentials = Credentials.basic( diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index c9d213d7..64a5e4c0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -7,6 +7,7 @@ import okhttp3.Credentials import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.Response import org.radarbase.authorizer.api.RequestTokenPayload import org.radarbase.authorizer.api.RestOauth2AccessToken import org.radarbase.authorizer.api.SignRequestParams @@ -26,8 +27,36 @@ class OuraAuthorizationService( @Context private val config: AuthorizerConfig, ) : OAuth2RestSourceAuthorizationService(clients, httpClient, objectMapper, config) { - //TODO: implement this - override fun deregisterUser(user: RestSourceUser) = - throw HttpBadRequestException("", "Not available for auth type") + override fun revokeToken(user: RestSourceUser): Boolean { + val accessToken = user.accessToken ?: run { + logger.error("Cannot revoke token of user {} without an access token", user.userId) + return false + } + // revoke token using the deregistrationEndpoint token endpoint + val authConfig = clients.forSourceType(user.sourceType) + val deregistrationEndpoint = checkNotNull(authConfig.deregistrationEndpoint) + val revokeURI = UriBuilder.fromUri(deregistrationEndpoint) + .queryParam("access_token", accessToken) + .build() + .toString() + + val credentials = Credentials.basic( + checkNotNull(authConfig.clientId), + checkNotNull(authConfig.clientSecret), + ) + val form = FormBody.Builder().add("token", accessToken).build() + + var requestObj = Request.Builder().apply { + url(revokeURI) + }.post(form).build() + val response = httpClient.request(requestObj) + if (response) { + logger.info("Successfully revoked token for user {}", user.userId) + } else { + logger.error("Failed to revoke token for user {}", user.userId) + return false + } + return httpClient.request(post(form, user.sourceType)) + } } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 41499752..cc6a867e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,6 +75,10 @@ services: depends_on: - radar-rest-sources-backend - managementportal + - radarbase-postgresql + - redis +# ports: +# - "8080:80" environment: BASE_HREF: "/rest-sources/authorizer/" BACKEND_BASE_URL: "http://localhost:8080/rest-sources/backend" @@ -100,3 +104,12 @@ services: command: --api.insecure=true --providers.docker ports: - "8080:80" + + redis: + image: redis:latest + restart: on-failure + command: ["redis-server","--bind","redis","127.0.0.1", "--port","6379"] + ports: + - "6379:6379" + environment: + - ALLOW_EMPTY_PASSWORD=yes diff --git a/docker/etc/rest-source-authorizer/authorizer.yml b/docker/etc/rest-source-authorizer/authorizer.yml index e3eb19c3..b9dc816e 100644 --- a/docker/etc/rest-source-authorizer/authorizer.yml +++ b/docker/etc/rest-source-authorizer/authorizer.yml @@ -19,10 +19,14 @@ database: password: radarcns dialect: org.hibernate.dialect.PostgreSQLDialect +redis: + uri: redis://redis:6379 + 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 + - sourceType: Oura + authorizationEndpoint: https://cloud.ouraring.com/oauth/authorize + tokenEndpoint: https://api.ouraring.com/oauth/token + deregistrationEndpoint: https://api.ouraring.com/oauth/revoke + clientId: client-id + clientSecret: client-secret + scope: daily session heartrate workout tag personal email spo2 \ No newline at end of file From a1c9c7c1b6efa1220ad6e25fb96d8c103f6bafe8 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Thu, 12 Oct 2023 15:33:53 +0100 Subject: [PATCH 12/35] merged dev and updated code --- .../radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt | 1 + docker-compose.yml | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt index 5aafc9f0..a4013c2f 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt @@ -31,6 +31,7 @@ import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService. import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.GARMIN_AUTH import org.radarbase.authorizer.service.GarminSourceAuthorizationService import org.radarbase.authorizer.service.OAuth2RestSourceAuthorizationService +import org.radarbase.authorizer.service.OuraAuthorizationService import org.radarbase.authorizer.service.RegistrationService import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceClientService diff --git a/docker-compose.yml b/docker-compose.yml index cc6a867e..3b49ccda 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,7 +75,6 @@ services: depends_on: - radar-rest-sources-backend - managementportal - - radarbase-postgresql - redis # ports: # - "8080:80" From bd7ebb1990eb99086dc9e72c5d19057ab5fdbc27 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Thu, 12 Oct 2023 15:44:36 +0100 Subject: [PATCH 13/35] updated Resources --- .../authorizer/resources/ProjectResource.kt | 4 +-- .../resources/RestSourceUserResource.kt | 12 ++++---- .../service/OuraAuthorizationService.kt | 7 ++--- authorizer-app/docker/default.conf | 28 +++---------------- 4 files changed, 14 insertions(+), 37 deletions(-) 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 a33314e4..1668210f 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 @@ -59,7 +59,7 @@ class ProjectResource( @GET @Path("{projectId}/users") - // @NeedsPermission(Permission.SUBJECT_READ, "projectId") + @NeedsPermission(Permission.SUBJECT_READ, "projectId") @Cache(maxAge = 60, isPrivate = true, vary = [AUTHORIZATION]) fun users( @PathParam("projectId") projectId: String, @@ -70,7 +70,7 @@ class ProjectResource( @GET @Path("{projectId}") - // @NeedsPermission(Permission.PROJECT_READ, "projectId") + @NeedsPermission(Permission.PROJECT_READ, "projectId") @Cache(maxAge = 300, isPrivate = true, vary = [AUTHORIZATION]) fun project(@PathParam("projectId") projectId: String): Project { 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 5b659180..85b20c87 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 @@ -155,13 +155,13 @@ class RestSourceUserResource( @GET @Path("{id}") - //@NeedsPermission(Permission.SUBJECT_READ) + @NeedsPermission(Permission.SUBJECT_READ) @Cache(maxAge = 300, isPrivate = true, vary = [HttpHeaders.AUTHORIZATION]) fun readUser(@PathParam("id") userId: Long): RestSourceUserDTO = userService.get(userId) @DELETE @Path("{id}") - //@NeedsPermission(Permission.SUBJECT_UPDATE) + @NeedsPermission(Permission.SUBJECT_UPDATE) fun deleteUser(@PathParam("id") userId: Long): Response { userService.delete(userId) return Response.noContent().header("user-removed", userId).build() @@ -169,7 +169,7 @@ class RestSourceUserResource( @POST @Path("{id}/reset") - //@NeedsPermission(Permission.SUBJECT_UPDATE) + @NeedsPermission(Permission.SUBJECT_UPDATE) fun reset( @PathParam("id") userId: Long, user: RestSourceUserDTO, @@ -177,17 +177,17 @@ class RestSourceUserResource( @GET @Path("{id}/token") - //@NeedsPermission(Permission.MEASUREMENT_CREATE) + @NeedsPermission(Permission.MEASUREMENT_CREATE) fun requestToken(@PathParam("id") userId: Long): TokenDTO = userService.ensureToken(userId) @POST @Path("{id}/token") - //@NeedsPermission(Permission.MEASUREMENT_CREATE) + @NeedsPermission(Permission.MEASUREMENT_CREATE) fun refreshToken(@PathParam("id") userId: Long): TokenDTO = userService.refreshToken(userId) @POST @Path("{id}/token/sign") - //@NeedsPermission(Permission.MEASUREMENT_READ) + @NeedsPermission(Permission.MEASUREMENT_READ) fun signRequest( @PathParam("id") userId: Long, payload: SignRequestParams, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index 64a5e4c0..f158fe35 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -36,10 +36,7 @@ class OuraAuthorizationService( val authConfig = clients.forSourceType(user.sourceType) val deregistrationEndpoint = checkNotNull(authConfig.deregistrationEndpoint) - val revokeURI = UriBuilder.fromUri(deregistrationEndpoint) - .queryParam("access_token", accessToken) - .build() - .toString() + val revokeURI = UriBuilder.fromUri(deregistrationEndpoint).queryParam("access_token", accessToken).build().toString() val credentials = Credentials.basic( checkNotNull(authConfig.clientId), @@ -59,4 +56,4 @@ class OuraAuthorizationService( } return httpClient.request(post(form, user.sourceType)) } -} \ No newline at end of file +} diff --git a/authorizer-app/docker/default.conf b/authorizer-app/docker/default.conf index 76b1ea3c..276eac34 100644 --- a/authorizer-app/docker/default.conf +++ b/authorizer-app/docker/default.conf @@ -8,38 +8,18 @@ server { include /etc/nginx/mime.types; autoindex off; - #absolute_redirect off; - #port_in_redirect off; - - #location / { - # deny all; - #} - - #location BASE_HREF/ { - # try_files $uri /index.html; - #} - - #location ~* \.(eot|ttf|woff|woff2)$ { - # add_header Access-Control-Allow-Origin *; - #} + absolute_redirect off; + port_in_redirect off; location / { - add_header 'Access-Control-Allow-Origin' "$http_origin" always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always; - add_header 'Access-Control-Expose-Headers' 'Authorization' always; - try_files $uri /index.html; + deny all; } location BASE_HREF { - add_header 'Access-Control-Allow-Origin' "$http_origin" always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always; - add_header 'Access-Control-Expose-Headers' 'Authorization' always; alias /usr/share/nginx/html; try_files $uri $uri/ BASE_HREF/index.html; } # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; -} +} \ No newline at end of file From d70867af752242bc3eecd320f26e33e6467a3705 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Thu, 12 Oct 2023 16:17:01 +0100 Subject: [PATCH 14/35] fixed lint issues and solve permission error --- .../authorizer/resources/RestSourceUserResource.kt | 2 +- .../authorizer/service/OuraAuthorizationService.kt | 9 --------- docker/etc/managementportal/oauth_client_details.csv | 2 +- 3 files changed, 2 insertions(+), 11 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 85b20c87..3c81dc79 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 @@ -134,7 +134,7 @@ class RestSourceUserResource( } @POST - //@NeedsPermission(Permission.SUBJECT_CREATE) + @NeedsPermission(Permission.SUBJECT_CREATE) fun create( userDto: RestSourceUserDTO, ): Response { diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index f158fe35..2a3a032c 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -7,18 +7,9 @@ import okhttp3.Credentials import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.Response -import org.radarbase.authorizer.api.RequestTokenPayload -import org.radarbase.authorizer.api.RestOauth2AccessToken -import org.radarbase.authorizer.api.SignRequestParams import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.entity.RestSourceUser -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 class OuraAuthorizationService( @Context private val clients: RestSourceClientService, diff --git a/docker/etc/managementportal/oauth_client_details.csv b/docker/etc/managementportal/oauth_client_details.csv index a7095d45..30aefa04 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/rest-sources/authorizer/login;3600;78000;; +radar_rest_sources_authorizer;res_restAuthorizer;;SOURCETYPE.READ,PROJECT.READ,SUBJECT.READ,SUBJECT.UPDATE,SUBJECT.CREATE;authorization_code;http://localhost:8080/rest-sources/authorizer/login;3600;78000;; From 10e041b5e4e76febfd4afa55aebdb2fb394e613a Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Thu, 12 Oct 2023 16:23:31 +0100 Subject: [PATCH 15/35] imports are in lexicographic order now --- .../radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt index a4013c2f..556ed2b5 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt @@ -27,8 +27,8 @@ import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.doa.RestSourceUserRepositoryImpl import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.FITBIT_AUTH -import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.OURA_AUTH import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.GARMIN_AUTH +import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService.Companion.OURA_AUTH import org.radarbase.authorizer.service.GarminSourceAuthorizationService import org.radarbase.authorizer.service.OAuth2RestSourceAuthorizationService import org.radarbase.authorizer.service.OuraAuthorizationService From 43f851949217e4e76d616681d247d3128c6f7d09 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Mon, 16 Oct 2023 13:26:27 +0100 Subject: [PATCH 16/35] minor changes from the PR review --- .../authorizer/service/OuraAuthorizationService.kt | 10 +++++++--- docker/etc/rest-source-authorizer/authorizer.yml | 13 ++++++------- .../rest-source-authorizer/authorizer.yml.template | 3 ++- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index 2a3a032c..6379923d 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -33,18 +33,22 @@ class OuraAuthorizationService( checkNotNull(authConfig.clientId), checkNotNull(authConfig.clientSecret), ) - val form = FormBody.Builder().add("token", accessToken).build() var requestObj = Request.Builder().apply { url(revokeURI) - }.post(form).build() + post(FormBody.Builder().build()) + header("Authorization", credentials) + header("Content-Type", "application/x-www-form-urlencoded") + header("Accept", "application/json") + }.build() + val response = httpClient.request(requestObj) if (response) { logger.info("Successfully revoked token for user {}", user.userId) + return true } else { logger.error("Failed to revoke token for user {}", user.userId) return false } - return httpClient.request(post(form, user.sourceType)) } } diff --git a/docker/etc/rest-source-authorizer/authorizer.yml b/docker/etc/rest-source-authorizer/authorizer.yml index b9dc816e..6535af5f 100644 --- a/docker/etc/rest-source-authorizer/authorizer.yml +++ b/docker/etc/rest-source-authorizer/authorizer.yml @@ -23,10 +23,9 @@ redis: uri: redis://redis:6379 restSourceClients: - - sourceType: Oura - authorizationEndpoint: https://cloud.ouraring.com/oauth/authorize - tokenEndpoint: https://api.ouraring.com/oauth/token - deregistrationEndpoint: https://api.ouraring.com/oauth/revoke - clientId: client-id - clientSecret: client-secret - scope: daily session heartrate workout tag personal email spo2 \ No newline at end of file + - 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 \ No newline at end of file diff --git a/docker/etc/rest-source-authorizer/authorizer.yml.template b/docker/etc/rest-source-authorizer/authorizer.yml.template index 0a7ef850..c78fad3e 100644 --- a/docker/etc/rest-source-authorizer/authorizer.yml.template +++ b/docker/etc/rest-source-authorizer/authorizer.yml.template @@ -44,6 +44,7 @@ restSourceClients: - sourceType: Oura authorizationEndpoint: https://cloud.ouraring.com/oauth/authorize tokenEndpoint: https://api.ouraring.com/oauth/token + deregistrationEndpoint: https://api.ouraring.com/oauth/revoke clientId: Oura-clientid clientSecret: Oura-clinetsecret - scope: daily session heartrate workout tag personal email + scope: daily session heartrate workout tag personal email spo2 From 0d66fd9f3ce70e65646b98145a15cd4872b74f85 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Mon, 16 Oct 2023 14:45:02 +0100 Subject: [PATCH 17/35] updated authorizer.yml --- docker/etc/rest-source-authorizer/authorizer.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docker/etc/rest-source-authorizer/authorizer.yml b/docker/etc/rest-source-authorizer/authorizer.yml index 6535af5f..1b7bb625 100644 --- a/docker/etc/rest-source-authorizer/authorizer.yml +++ b/docker/etc/rest-source-authorizer/authorizer.yml @@ -28,4 +28,11 @@ restSourceClients: tokenEndpoint: https://api.fitbit.com/oauth2/token clientId: Fitbit-clientid clientSecret: Fitbit-clientsecret - scope: activity heartrate sleep profile \ No newline at end of file + scope: activity heartrate sleep profile + - sourceType: Oura + authorizationEndpoint: https://cloud.ouraring.com/oauth/authorize + tokenEndpoint: https://api.ouraring.com/oauth/token + deregistrationEndpoint: https://api.ouraring.com/oauth/revoke + clientId: Oura-clientid + clientSecret: Oura-clinetsecret + scope: daily session heartrate workout tag personal email spo2 \ No newline at end of file From b35367adf68525d673fb147a5abe4d5f2105ff45 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Mon, 16 Oct 2023 15:17:57 +0100 Subject: [PATCH 18/35] added ring_configuration scope in Oura --- docker/etc/rest-source-authorizer/authorizer.yml | 2 +- docker/etc/rest-source-authorizer/authorizer.yml.template | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/etc/rest-source-authorizer/authorizer.yml b/docker/etc/rest-source-authorizer/authorizer.yml index 1b7bb625..0b4eff17 100644 --- a/docker/etc/rest-source-authorizer/authorizer.yml +++ b/docker/etc/rest-source-authorizer/authorizer.yml @@ -35,4 +35,4 @@ restSourceClients: deregistrationEndpoint: https://api.ouraring.com/oauth/revoke clientId: Oura-clientid clientSecret: Oura-clinetsecret - scope: daily session heartrate workout tag personal email spo2 \ No newline at end of file + scope: daily session heartrate workout tag personal email spo2 ring_configuration \ No newline at end of file diff --git a/docker/etc/rest-source-authorizer/authorizer.yml.template b/docker/etc/rest-source-authorizer/authorizer.yml.template index c78fad3e..f2fcf95c 100644 --- a/docker/etc/rest-source-authorizer/authorizer.yml.template +++ b/docker/etc/rest-source-authorizer/authorizer.yml.template @@ -47,4 +47,4 @@ restSourceClients: deregistrationEndpoint: https://api.ouraring.com/oauth/revoke clientId: Oura-clientid clientSecret: Oura-clinetsecret - scope: daily session heartrate workout tag personal email spo2 + scope: daily session heartrate workout tag personal email spo2 ring_configuration From 3e56a2e02e52c59d3eb9551bef539e1b4af3aecc Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Wed, 18 Oct 2023 15:00:02 +0100 Subject: [PATCH 19/35] Added code to get external id during auth Oura --- .../authorizer/api/ApiDeclarations.kt | 9 +++++ .../resources/SourceClientResource.kt | 1 + .../service/OuraAuthorizationService.kt | 37 +++++++++++++++---- 3 files changed, 39 insertions(+), 8 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 80f81419..73254349 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 @@ -39,6 +39,15 @@ data class RestOauth1UserId( @JsonProperty("userId") val userId: String, ) +data class OuraAuthUserId( + @JsonProperty("age") val age: String, + @JsonProperty("weight") val weight: String, + @JsonProperty("height") val height: String, + @JsonProperty("gender") val gender: String, + @JsonProperty("email") val email: String, + @JsonProperty("user_id") val userId: String, +) + data class SignRequestParams( val url: String, val method: String, 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 907085fc..a8608034 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 @@ -30,6 +30,7 @@ import jakarta.ws.rs.core.HttpHeaders import jakarta.ws.rs.core.MediaType import org.radarbase.auth.authorization.Permission import org.radarbase.authorizer.api.DeregistrationsDTO +import org.radarbase.authorizer.api.OuraAuthUserId import org.radarbase.authorizer.api.RestSourceClientMapper import org.radarbase.authorizer.api.RestSourceUserDTO import org.radarbase.authorizer.api.RestSourceUserMapper diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index 6379923d..ac05ad2b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -7,9 +7,13 @@ import okhttp3.Credentials import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request +import org.radarbase.authorizer.api.OuraAuthUserId +import org.radarbase.authorizer.api.RequestTokenPayload +import org.radarbase.authorizer.api.RestOauth2AccessToken import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.entity.RestSourceUser import org.radarbase.jersey.util.request +import org.radarbase.jersey.util.requestJson class OuraAuthorizationService( @Context private val clients: RestSourceClientService, @@ -17,6 +21,28 @@ class OuraAuthorizationService( @Context private val objectMapper: ObjectMapper, @Context private val config: AuthorizerConfig, ) : OAuth2RestSourceAuthorizationService(clients, httpClient, objectMapper, config) { + private val tokenReader = objectMapper.readerFor(RestOauth2AccessToken::class.java) + private val oauthUserReader = objectMapper.readerFor(OuraAuthUserId::class.java) + + override fun requestAccessToken(payload: RequestTokenPayload, sourceType: String): RestOauth2AccessToken { + val authorizationConfig = clients.forSourceType(sourceType) + val clientId = checkNotNull(authorizationConfig.clientId) + + val form = FormBody.Builder().apply { + payload.code?.let { add("code", it) } + add("grant_type", "authorization_code") + add("client_id", clientId) + add("redirect_uri", config.service.callbackUrl.toString()) + }.build() + val accessToken: RestOauth2AccessToken = httpClient.requestJson(post(form, sourceType), tokenReader) + val ouraUserUri = UriBuilder.fromUri(Oura_USER_ID_ENDPOINT).queryParam("access_token", accessToken.accessToken).build().toString() + val userReq = Request.Builder().apply { + url(ouraUserUri) + }.build() + val userIdObj: OuraAuthUserId = httpClient.requestJson(userReq, oauthUserReader) + val userId = userIdObj.userId + return accessToken.copy(externalUserId = userId) + } override fun revokeToken(user: RestSourceUser): Boolean { val accessToken = user.accessToken ?: run { @@ -29,17 +55,9 @@ class OuraAuthorizationService( val revokeURI = UriBuilder.fromUri(deregistrationEndpoint).queryParam("access_token", accessToken).build().toString() - val credentials = Credentials.basic( - checkNotNull(authConfig.clientId), - checkNotNull(authConfig.clientSecret), - ) - var requestObj = Request.Builder().apply { url(revokeURI) post(FormBody.Builder().build()) - header("Authorization", credentials) - header("Content-Type", "application/x-www-form-urlencoded") - header("Accept", "application/json") }.build() val response = httpClient.request(requestObj) @@ -51,4 +69,7 @@ class OuraAuthorizationService( return false } } + companion object { + private const val Oura_USER_ID_ENDPOINT = "https://api.ouraring.com/v1/userinfo?" + } } From f53462eaa78ad5ed4dafe4d7d1ed339759e9ee78 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Wed, 18 Oct 2023 15:24:02 +0100 Subject: [PATCH 20/35] added exception handling --- .../authorizer/resources/SourceClientResource.kt | 1 - .../authorizer/service/OuraAuthorizationService.kt | 12 ++++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) 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 a8608034..907085fc 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 @@ -30,7 +30,6 @@ import jakarta.ws.rs.core.HttpHeaders import jakarta.ws.rs.core.MediaType import org.radarbase.auth.authorization.Permission import org.radarbase.authorizer.api.DeregistrationsDTO -import org.radarbase.authorizer.api.OuraAuthUserId import org.radarbase.authorizer.api.RestSourceClientMapper import org.radarbase.authorizer.api.RestSourceUserDTO import org.radarbase.authorizer.api.RestSourceUserMapper diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index ac05ad2b..5b5e536a 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -3,7 +3,6 @@ package org.radarbase.authorizer.service import com.fasterxml.jackson.databind.ObjectMapper import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.UriBuilder -import okhttp3.Credentials import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request @@ -12,6 +11,7 @@ import org.radarbase.authorizer.api.RequestTokenPayload import org.radarbase.authorizer.api.RestOauth2AccessToken import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.entity.RestSourceUser +import org.radarbase.jersey.exception.HttpBadGatewayException import org.radarbase.jersey.util.request import org.radarbase.jersey.util.requestJson @@ -35,11 +35,19 @@ class OuraAuthorizationService( add("redirect_uri", config.service.callbackUrl.toString()) }.build() val accessToken: RestOauth2AccessToken = httpClient.requestJson(post(form, sourceType), tokenReader) + if (accessToken.accessToken == null) { + logger.error("Failed to get access token for user {}", clientId) + throw HttpBadGatewayException("Service ${sourceType} did not provide a result") + } val ouraUserUri = UriBuilder.fromUri(Oura_USER_ID_ENDPOINT).queryParam("access_token", accessToken.accessToken).build().toString() val userReq = Request.Builder().apply { url(ouraUserUri) }.build() - val userIdObj: OuraAuthUserId = httpClient.requestJson(userReq, oauthUserReader) + val userIdObj: OuraAuthUserId = httpClient.requestJson(userReq, oauthUserReader) + if (userIdObj.userId == null) { + logger.error("Failed to get user id for user {}", clientId) + throw HttpBadGatewayException("Service ${sourceType} did not provide a result") + } val userId = userIdObj.userId return accessToken.copy(externalUserId = userId) } From b09b453a373406b8397be61cdb48988c16bb9a31 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Wed, 18 Oct 2023 15:29:17 +0100 Subject: [PATCH 21/35] fixed kotlin lint issue --- .../radarbase/authorizer/service/OuraAuthorizationService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index 5b5e536a..aaad005c 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -37,7 +37,7 @@ class OuraAuthorizationService( val accessToken: RestOauth2AccessToken = httpClient.requestJson(post(form, sourceType), tokenReader) if (accessToken.accessToken == null) { logger.error("Failed to get access token for user {}", clientId) - throw HttpBadGatewayException("Service ${sourceType} did not provide a result") + throw HttpBadGatewayException("Service {} did not provide a result", sourceType) } val ouraUserUri = UriBuilder.fromUri(Oura_USER_ID_ENDPOINT).queryParam("access_token", accessToken.accessToken).build().toString() val userReq = Request.Builder().apply { @@ -46,7 +46,7 @@ class OuraAuthorizationService( val userIdObj: OuraAuthUserId = httpClient.requestJson(userReq, oauthUserReader) if (userIdObj.userId == null) { logger.error("Failed to get user id for user {}", clientId) - throw HttpBadGatewayException("Service ${sourceType} did not provide a result") + throw HttpBadGatewayException("Service {} did not provide a result", sourceType) } val userId = userIdObj.userId return accessToken.copy(externalUserId = userId) From a2f316ac54484861093f048975cc2d4353896a38 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Wed, 18 Oct 2023 15:33:20 +0100 Subject: [PATCH 22/35] fixed few other klint issue --- .../radarbase/authorizer/service/OuraAuthorizationService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index aaad005c..bd4049cc 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -37,7 +37,7 @@ class OuraAuthorizationService( val accessToken: RestOauth2AccessToken = httpClient.requestJson(post(form, sourceType), tokenReader) if (accessToken.accessToken == null) { logger.error("Failed to get access token for user {}", clientId) - throw HttpBadGatewayException("Service {} did not provide a result", sourceType) + throw HttpBadGatewayException("Service $sourceType did not provide a result") } val ouraUserUri = UriBuilder.fromUri(Oura_USER_ID_ENDPOINT).queryParam("access_token", accessToken.accessToken).build().toString() val userReq = Request.Builder().apply { @@ -46,7 +46,7 @@ class OuraAuthorizationService( val userIdObj: OuraAuthUserId = httpClient.requestJson(userReq, oauthUserReader) if (userIdObj.userId == null) { logger.error("Failed to get user id for user {}", clientId) - throw HttpBadGatewayException("Service {} did not provide a result", sourceType) + throw HttpBadGatewayException("Service $sourceType did not provide a result") } val userId = userIdObj.userId return accessToken.copy(externalUserId = userId) From c28db9fb42fb1dc041d1ce8bf7bfbafd00a4da22 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Wed, 18 Oct 2023 16:37:43 +0100 Subject: [PATCH 23/35] simplify oura requestAccessToken function --- .../service/OuraAuthorizationService.kt | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index bd4049cc..0e16804e 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -21,34 +21,21 @@ class OuraAuthorizationService( @Context private val objectMapper: ObjectMapper, @Context private val config: AuthorizerConfig, ) : OAuth2RestSourceAuthorizationService(clients, httpClient, objectMapper, config) { - private val tokenReader = objectMapper.readerFor(RestOauth2AccessToken::class.java) private val oauthUserReader = objectMapper.readerFor(OuraAuthUserId::class.java) override fun requestAccessToken(payload: RequestTokenPayload, sourceType: String): RestOauth2AccessToken { val authorizationConfig = clients.forSourceType(sourceType) val clientId = checkNotNull(authorizationConfig.clientId) - - val form = FormBody.Builder().apply { - payload.code?.let { add("code", it) } - add("grant_type", "authorization_code") - add("client_id", clientId) - add("redirect_uri", config.service.callbackUrl.toString()) - }.build() - val accessToken: RestOauth2AccessToken = httpClient.requestJson(post(form, sourceType), tokenReader) + val accessToken: RestOauth2AccessToken = super.requestAccessToken(payload, sourceType) if (accessToken.accessToken == null) { logger.error("Failed to get access token for user {}", clientId) throw HttpBadGatewayException("Service $sourceType did not provide a result") } - val ouraUserUri = UriBuilder.fromUri(Oura_USER_ID_ENDPOINT).queryParam("access_token", accessToken.accessToken).build().toString() - val userReq = Request.Builder().apply { - url(ouraUserUri) - }.build() - val userIdObj: OuraAuthUserId = httpClient.requestJson(userReq, oauthUserReader) - if (userIdObj.userId == null) { + val userId = getExternalId(accessToken.accessToken) + if (userId == null) { logger.error("Failed to get user id for user {}", clientId) throw HttpBadGatewayException("Service $sourceType did not provide a result") } - val userId = userIdObj.userId return accessToken.copy(externalUserId = userId) } @@ -77,7 +64,17 @@ class OuraAuthorizationService( return false } } + + private fun getExternalId(accessToken: String): String { + val ouraUserUri = UriBuilder.fromUri(OURA_USER_ID_ENDPOINT).queryParam("access_token", accessToken).build().toString() + val userReq = Request.Builder().apply { + url(ouraUserUri) + }.build() + val userIdObj: OuraAuthUserId = httpClient.requestJson(userReq, oauthUserReader) + return userIdObj.userId + } + companion object { - private const val Oura_USER_ID_ENDPOINT = "https://api.ouraring.com/v1/userinfo?" + private const val OURA_USER_ID_ENDPOINT = "https://api.ouraring.com/v1/userinfo?" } } From c9b0c3ab99e7281177b07e7e53d5d5596757260e Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Mon, 23 Oct 2023 10:49:13 +0200 Subject: [PATCH 24/35] Bumped dependencies --- buildSrc/src/main/kotlin/Versions.kt | 10 +++++----- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 14 +++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 19a65808..51b8df4b 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -7,18 +7,18 @@ object Versions { const val kotlin = "1.9.10" const val radarCommons = "1.1.1" - const val radarJersey = "0.11.0-SNAPSHOT" + const val radarJersey = "0.11.0" const val postgresql = "42.6.0" - const val ktor = "2.3.0" - const val jedis = "5.0.1" + const val ktor = "2.3.5" + const val jedis = "5.0.2" const val slf4j = "2.0.9" - const val log4j2 = "2.20.0" + const val log4j2 = "2.21.0" const val jersey = "3.1.3" const val junit = "5.10.0" const val mockitoKotlin = "5.1.0" const val hamcrest = "2.2" - const val gradle = "8.3" + const val gradle = "8.4" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac72c34e..3fa8f862 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 0adc8e1a..1aa94a42 100755 --- a/gradlew +++ b/gradlew @@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +202,11 @@ fi # 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" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ From 9357146e5e87f6b75b72a37d047f5393fc96476f Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Tue, 24 Oct 2023 11:02:45 +0100 Subject: [PATCH 25/35] updated Oura getExternalId fun --- .../service/OuraAuthorizationService.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index 0e16804e..f637fd8d 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -31,12 +31,7 @@ class OuraAuthorizationService( logger.error("Failed to get access token for user {}", clientId) throw HttpBadGatewayException("Service $sourceType did not provide a result") } - val userId = getExternalId(accessToken.accessToken) - if (userId == null) { - logger.error("Failed to get user id for user {}", clientId) - throw HttpBadGatewayException("Service $sourceType did not provide a result") - } - return accessToken.copy(externalUserId = userId) + return accessToken.copy(externalUserId = getExternalId(accessToken.accessToken)) } override fun revokeToken(user: RestSourceUser): Boolean { @@ -70,8 +65,20 @@ class OuraAuthorizationService( val userReq = Request.Builder().apply { url(ouraUserUri) }.build() - val userIdObj: OuraAuthUserId = httpClient.requestJson(userReq, oauthUserReader) - return userIdObj.userId + return httpClient.newCall(userReq) + .execute() + .use { response -> + when (response.code) { + 200 -> + response.body?.byteStream() + ?.let { + oauthUserReader.readValue(it).userId + } + ?: throw HttpBadGatewayException("Service did not provide a result") + 400, 401, 403 -> throw HttpBadGatewayException("Service was unable to fetch the external ID") + else -> throw HttpBadGatewayException("Cannot connect to $OURA_USER_ID_ENDPOINT: HTTP status ${response.code}") + } + } } companion object { From 99a043111f5809253f4baa1e8a502a52c747dc76 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Tue, 24 Oct 2023 11:14:17 +0100 Subject: [PATCH 26/35] solved lint issues I think --- .../radarbase/authorizer/service/OuraAuthorizationService.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index f637fd8d..c457d027 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -13,7 +13,6 @@ import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.entity.RestSourceUser import org.radarbase.jersey.exception.HttpBadGatewayException import org.radarbase.jersey.util.request -import org.radarbase.jersey.util.requestJson class OuraAuthorizationService( @Context private val clients: RestSourceClientService, @@ -69,8 +68,7 @@ class OuraAuthorizationService( .execute() .use { response -> when (response.code) { - 200 -> - response.body?.byteStream() + 200 -> response.body?.byteStream() ?.let { oauthUserReader.readValue(it).userId } From 9b6b703788c9b1d3c7fe1c203e8fbe1920675130 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Tue, 24 Oct 2023 11:17:14 +0100 Subject: [PATCH 27/35] changed OURA_USER_ID_ENDPOINT url --- .../radarbase/authorizer/service/OuraAuthorizationService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index c457d027..d26b7c80 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -80,6 +80,6 @@ class OuraAuthorizationService( } companion object { - private const val OURA_USER_ID_ENDPOINT = "https://api.ouraring.com/v1/userinfo?" + private const val OURA_USER_ID_ENDPOINT = "https://api.ouraring.com/v1/userinfo" } } From 54d5c9020ff0538c57806375d09edd6968f7d67c Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Wed, 25 Oct 2023 11:37:22 +0200 Subject: [PATCH 28/35] More consistent use of IO context --- .../GarminSourceAuthorizationService.kt | 4 +- .../OAuth1RestSourceAuthorizationService.kt | 70 +++++++++---------- .../OAuth2RestSourceAuthorizationService.kt | 5 +- .../service/OuraAuthorizationService.kt | 36 ++++++---- 4 files changed, 63 insertions(+), 52 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt index cc26b865..6191ad91 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt @@ -68,7 +68,9 @@ class GarminSourceAuthorizationService( // request to pull the external id is needed. val response = request(HttpMethod.Get, GARMIN_USER_ID_ENDPOINT, this@getExternalId, sourceType) when (response.status) { - HttpStatusCode.OK -> response.body().userId + HttpStatusCode.OK -> withContext(Dispatchers.IO) { + response.body().userId + } HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden -> throw HttpBadGatewayException("Service was unable to fetch the external ID") else -> throw HttpBadGatewayException("Cannot connect to ${response.request.url}: HTTP status ${response.status}") } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt index dfa64b81..64607228 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt @@ -47,15 +47,9 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.time.Instant import java.util.concurrent.ThreadLocalRandom -import kotlin.collections.Map -import kotlin.collections.MutableMap -import kotlin.collections.buildMap import kotlin.collections.component1 import kotlin.collections.component2 -import kotlin.collections.joinToString -import kotlin.collections.mutableMapOf import kotlin.collections.set -import kotlin.collections.toSortedMap abstract class OAuth1RestSourceAuthorizationService( @Context private val clientService: RestSourceClientService, @@ -94,22 +88,21 @@ abstract class OAuth1RestSourceAuthorizationService( } val authConfig = clientService.forSourceType(user.sourceType) - return withContext(Dispatchers.IO) { - val response = request( - HttpMethod.Delete, - authConfig.deregistrationEndpoint!!, - RestOauth1AccessToken(accessToken, user.refreshToken), - user.sourceType, - ) - when (response.status) { - HttpStatusCode.OK, HttpStatusCode.NoContent -> { - userRepository.updateToken(null, user) - true - } - HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden -> false - else -> throw HttpBadGatewayException("Cannot connect to ${response.request.url}: HTTP status ${response.status}") + val response = request( + HttpMethod.Delete, + authConfig.deregistrationEndpoint!!, + RestOauth1AccessToken(accessToken, user.refreshToken), + user.sourceType, + ) + return when (response.status) { + HttpStatusCode.OK, HttpStatusCode.NoContent -> { + userRepository.updateToken(null, user) + true } + + HttpStatusCode.BadRequest, HttpStatusCode.Unauthorized, HttpStatusCode.Forbidden -> false + else -> throw HttpBadGatewayException("Cannot connect to ${response.request.url}: HTTP status ${response.status}") } } @@ -117,14 +110,12 @@ abstract class OAuth1RestSourceAuthorizationService( val authConfig = clientService.forSourceType(sourceType) if (token.isEmpty()) throw HttpBadRequestException("token-empty", "Token cannot be null or empty") - val response = withContext(Dispatchers.IO) { - request( - HttpMethod.Delete, - authConfig.deregistrationEndpoint!!, - RestOauth1AccessToken(token, ""), - sourceType, - ) - } + val response = request( + HttpMethod.Delete, + authConfig.deregistrationEndpoint!!, + RestOauth1AccessToken(token, ""), + sourceType, + ) return when (response.status) { HttpStatusCode.OK, HttpStatusCode.NoContent -> true @@ -185,19 +176,26 @@ abstract class OAuth1RestSourceAuthorizationService( } } - suspend fun request(method: HttpMethod, url: String, tokens: RestOauth1AccessToken, sourceType: String): HttpResponse { + suspend fun request( + method: HttpMethod, + url: String, + tokens: RestOauth1AccessToken, + sourceType: String, + ): HttpResponse { val authConfig = clientService.forSourceType(sourceType) val params = this.getAuthParams(authConfig, tokens.token, tokens.tokenVerifier) params[OAUTH_SIGNATURE] = OauthSignature(url, params, method, authConfig.clientSecret, tokens.tokenSecret).getEncodedSignature() - return httpClient.request(url = Url(url)) { - headers { - append("Authorization", "OAuth ${params.toFormattedHeader()}") - } - this.method = method - if (method == HttpMethod.Post) { - setBody("") + return withContext(Dispatchers.IO){ + httpClient.request(url = Url(url)) { + headers { + append("Authorization", "OAuth ${params.toFormattedHeader()}") + } + this.method = method + if (method == HttpMethod.Post) { + setBody("") + } } } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt index 3baa8a6b..77f4e5f1 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth2RestSourceAuthorizationService.kt @@ -121,7 +121,10 @@ open class OAuth2RestSourceAuthorizationService( override fun signRequest(user: RestSourceUser, payload: SignRequestParams): SignRequestParams = throw HttpBadRequestException("", "Not available for auth type") - private suspend fun submitForm(sourceType: String, builder: ParametersBuilder.(RestSourceClient) -> Unit): HttpResponse { + private suspend fun submitForm( + sourceType: String, + builder: ParametersBuilder.(RestSourceClient) -> Unit, + ): HttpResponse { val authorizationConfig = clients.forSourceType(sourceType) return httpClient.submitForm( diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index 2d9c21b7..5e2865b6 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -6,6 +6,8 @@ import io.ktor.client.statement.bodyAsText import io.ktor.http.isSuccess import io.ktor.http.takeFrom import jakarta.ws.rs.core.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.entity.RestSourceUser @@ -24,21 +26,27 @@ class OuraAuthorizationService( val deregistrationEndpoint = checkNotNull(authConfig.deregistrationEndpoint) val isSuccess = try { - val response = httpClient.submitForm { - url { - takeFrom(deregistrationEndpoint) - parameters.append("access_token", accessToken) + withContext(Dispatchers.IO) { + val response = httpClient.submitForm { + url { + takeFrom(deregistrationEndpoint) + parameters.append("access_token", accessToken) + } + basicAuth( + username = checkNotNull(authConfig.clientId), + password = checkNotNull(authConfig.clientSecret), + ) + } + if (response.status.isSuccess()) { + true + } else { + logger.error( + "Failed to revoke token for user {}: {}", + user.userId, + response.bodyAsText().take(512) + ) + false } - basicAuth( - username = checkNotNull(authConfig.clientId), - password = checkNotNull(authConfig.clientSecret), - ) - } - if (response.status.isSuccess()) { - true - } else { - logger.error("Failed to revoke token for user {}: {}", user.userId, response.bodyAsText().take(512)) - false } } catch (ex: Exception) { logger.warn("Revoke endpoint error: {}", ex.toString()) From 53096086edc820b2777b7e99eaed3b1400f74852 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Wed, 25 Oct 2023 11:42:24 +0200 Subject: [PATCH 29/35] Code style --- .../authorizer/service/OAuth1RestSourceAuthorizationService.kt | 2 +- .../radarbase/authorizer/service/OuraAuthorizationService.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt index 64607228..b40d10f5 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OAuth1RestSourceAuthorizationService.kt @@ -187,7 +187,7 @@ abstract class OAuth1RestSourceAuthorizationService( params[OAUTH_SIGNATURE] = OauthSignature(url, params, method, authConfig.clientSecret, tokens.tokenSecret).getEncodedSignature() - return withContext(Dispatchers.IO){ + return withContext(Dispatchers.IO) { httpClient.request(url = Url(url)) { headers { append("Authorization", "OAuth ${params.toFormattedHeader()}") diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index 5e2865b6..f4b80001 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -43,7 +43,7 @@ class OuraAuthorizationService( logger.error( "Failed to revoke token for user {}: {}", user.userId, - response.bodyAsText().take(512) + response.bodyAsText().take(512), ) false } From 26be7d54dc1562e715a70406f4546007a84e8d19 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 25 Oct 2023 12:33:40 +0100 Subject: [PATCH 30/35] Bump versions --- authorizer-app/package.json | 2 +- buildSrc/src/main/kotlin/Versions.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/authorizer-app/package.json b/authorizer-app/package.json index 8a081a6e..3e0a7a8b 100644 --- a/authorizer-app/package.json +++ b/authorizer-app/package.json @@ -1,6 +1,6 @@ { "name": "authorizer-app", - "version": "4.3.1", + "version": "4.4.0", "description": "Simple app to authorize to collect data from third party services ", "repository": { "type": "git", diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 51b8df4b..5b0fd5c3 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -1,6 +1,6 @@ @Suppress("ConstPropertyName") object Versions { - const val project = "4.3.1-SNAPSHOT" + const val project = "4.4.0" const val java = 17 From c2742f1b5826e876ada25fa86b1688903fbd8408 Mon Sep 17 00:00:00 2001 From: Joris Borgdorff Date: Wed, 25 Oct 2023 14:29:21 +0200 Subject: [PATCH 31/35] Add message to checkNotNull --- .../radarbase/authorizer/service/OuraAuthorizationService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index 135a231b..9d80384f 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -35,7 +35,7 @@ class OuraAuthorizationService( } // revoke token using the deregistrationEndpoint token endpoint val authConfig = clients.forSourceType(user.sourceType) - val deregistrationEndpoint = checkNotNull(authConfig.deregistrationEndpoint) + val deregistrationEndpoint = checkNotNull(authConfig.deregistrationEndpoint) { "Missing Oura deregistration endpoint configuration" } val isSuccess = try { withContext(Dispatchers.IO) { From c592c7d8257977408773d0a1be5edab6591a5cd0 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Wed, 25 Oct 2023 15:05:06 +0100 Subject: [PATCH 32/35] solved error of fetching external id in Oura --- .../java/org/radarbase/authorizer/api/ApiDeclarations.kt | 6 +++--- .../authorizer/service/OuraAuthorizationService.kt | 7 ++++--- gradle/wrapper/gradle-wrapper.properties | 1 + 3 files changed, 8 insertions(+), 6 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 f35557da..30cc62cc 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 @@ -52,9 +52,9 @@ data class RestOauth1UserId( @Serializable data class OuraAuthUserId( - val age: String, - val weight: String, - val height: String, + val age: Int, + val weight: Float, + val height: Int, val gender: String, val email: String, @SerialName("user_id") val userId: String, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index 9d80384f..f28088b4 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -77,9 +77,10 @@ class OuraAuthorizationService( private suspend fun getExternalId(accessToken: String): String = withContext(Dispatchers.IO) { try { val response = httpClient.get { - url(OURA_USER_ID_ENDPOINT) { - parameters.append("access_token", accessToken) - } + url { + takeFrom(OURA_USER_ID_ENDPOINT) + parameters.append("access_token", accessToken) + } } if (response.status.isSuccess()) { response.body().userId diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fa8f862..c40a836f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -5,3 +5,4 @@ networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME From 638bfe568ea8bf25107d3d29d6cb38872c466623 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Wed, 25 Oct 2023 15:08:32 +0100 Subject: [PATCH 33/35] removed repeated properties in gradle wrapper --- gradle/wrapper/gradle-wrapper.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c40a836f..3fa8f862 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -5,4 +5,3 @@ networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME From 7271c5726e97664936f93a8c3b0a5ad62a9b7b86 Mon Sep 17 00:00:00 2001 From: Heet Sankesara Date: Wed, 25 Oct 2023 15:22:57 +0100 Subject: [PATCH 34/35] changed height into float --- .../main/java/org/radarbase/authorizer/api/ApiDeclarations.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 30cc62cc..1253d53b 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 @@ -54,7 +54,7 @@ data class RestOauth1UserId( data class OuraAuthUserId( val age: Int, val weight: Float, - val height: Int, + val height: Float, val gender: String, val email: String, @SerialName("user_id") val userId: String, From 7ed4f5e6debc55f1251532677a1b2814547a0ef3 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 25 Oct 2023 15:29:42 +0100 Subject: [PATCH 35/35] Fix lint errors --- .../authorizer/service/OuraAuthorizationService.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt index f28088b4..b1479a69 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -78,9 +78,9 @@ class OuraAuthorizationService( try { val response = httpClient.get { url { - takeFrom(OURA_USER_ID_ENDPOINT) - parameters.append("access_token", accessToken) - } + takeFrom(OURA_USER_ID_ENDPOINT) + parameters.append("access_token", accessToken) + } } if (response.status.isSuccess()) { response.body().userId