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/Dockerfile b/authorizer-app-backend/Dockerfile index 4f322aa9..1161d4a4 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,7 +18,9 @@ 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 ./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..5f1b9613 100644 --- a/authorizer-app-backend/build.gradle.kts +++ b/authorizer-app-backend/build.gradle.kts @@ -1,86 +1,41 @@ -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") + 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", - "-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}") } + implementation("org.radarbase:radar-commons-kotlin:${Versions.radarCommons}") - 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(enforcedPlatform("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..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 @@ -16,27 +16,48 @@ 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, +) + +@Serializable +data class OuraAuthUserId( + val age: Int, + val weight: Float, + val height: Float, + val gender: String, + val email: String, + @SerialName("user_id") 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..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,7 +1,9 @@ package org.radarbase.authorizer.config -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +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 @@ -18,26 +20,20 @@ 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() - 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") - } + frontendBaseUri != null -> URLBuilder().takeFrom(frontendBaseUri) + advertisedBaseUri != null -> 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/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/RegistrationRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RegistrationRepository.kt index 669be649..cf356a05 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 @@ -8,21 +8,23 @@ import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.entity.RegistrationState import org.radarbase.authorizer.doa.entity.RestSourceUser 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.authorizer.util.encodeToBase64 +import org.radarbase.authorizer.util.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..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 @@ -26,16 +26,18 @@ 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 -import java.util.* +import java.util.UUID 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/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..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 @@ -25,9 +25,17 @@ 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.DelegatedRestSourceAuthorizationService.Companion.OURA_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 +import org.radarbase.authorizer.service.RestSourceUserService import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.filter.Filters @@ -103,5 +111,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/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/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..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,16 +18,25 @@ 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.jersey.auth.Auth +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 +import org.radarbase.jersey.service.AsyncCoroutineService import org.radarbase.jersey.service.managementportal.RadarProjectService @Path("projects") @@ -38,16 +47,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 +66,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..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,23 +2,37 @@ 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 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 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 +48,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 +76,7 @@ class RegistrationResource( ), ) } - return Response.created(URI("tokens/${tokenState.token}")) + Response.created(URI("tokens/${tokenState.token}")) .entity(tokenState) .build() } @@ -63,9 +85,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 +104,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 +115,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 +128,7 @@ class RegistrationResource( projectService.project(it).toProject() } - return RegistrationResponse( + RegistrationResponse( token = registration.token, authEndpointUrl = authorizationService.getAuthorizationEndpointWithParams( sourceType = registration.user.sourceType, @@ -125,7 +150,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 +169,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..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,22 +18,38 @@ 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 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.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 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 +63,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 +84,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 +133,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 +155,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 +190,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 +221,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..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,20 +18,35 @@ 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 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 -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 +57,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 +88,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 +115,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 +148,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..1e2cd143 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,11 +56,12 @@ 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 { 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/GarminSourceAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/GarminSourceAuthorizationService.kt index 8bae9828..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 @@ -16,10 +16,13 @@ package org.radarbase.authorizer.service -import com.fasterxml.jackson.databind.ObjectMapper +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 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 +31,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 +59,28 @@ 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 -> 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}") + } } 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 6d29fdb7..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 @@ -16,37 +16,49 @@ package org.radarbase.authorizer.service -import com.fasterxml.jackson.databind.ObjectMapper +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 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.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 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 import org.slf4j.Logger import org.slf4j.LoggerFactory import java.time.Instant -import kotlin.math.floor +import java.util.concurrent.ThreadLocalRandom +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set 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 +74,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 +88,43 @@ abstract class OAuth1RestSourceAuthorizationService( } val authConfig = clientService.forSourceType(user.sourceType) - val req = createRequest( - "DELETE", + + val response = request( + HttpMethod.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 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", + val response = 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,57 +132,72 @@ abstract class OAuth1RestSourceAuthorizationService( logger.info("Getting auth endpoint..") 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(), - ) + val tokens = requestToken(authConfig.preAuthorizationEndpoint, RestOauth1AccessToken(""), sourceType) - return Url(authConfig.authorizationEndpoint, params).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 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( + RestOauth1AccessToken.serializer(), + 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 withContext(Dispatchers.IO) { + 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 { @@ -183,16 +205,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(), + HttpMethod.parse(payload.method), + authConfig.clientSecret, + user.refreshToken, + ).getEncodedSignature(), + ) + } return SignRequestParams(payload.url, payload.method, signedParams) } @@ -201,47 +228,41 @@ 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 - .replace("=".toRegex(), "\":\"") - .replace("&".toRegex(), "\",\"") + private fun String.toJsonString(): String { + val params = this + .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 suspend 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? + 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..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 @@ -16,120 +16,128 @@ package org.radarbase.authorizer.service -import com.fasterxml.jackson.databind.ObjectMapper +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 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( +open 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) + protected 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, ): 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 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/OuraAuthorizationService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt new file mode 100644 index 00000000..b1479a69 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/OuraAuthorizationService.kt @@ -0,0 +1,104 @@ +package org.radarbase.authorizer.service + +import io.ktor.client.call.body +import io.ktor.client.request.basicAuth +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.get +import io.ktor.client.request.url +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.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.exception.HttpBadGatewayException +import java.io.IOException + +class OuraAuthorizationService( + @Context private val clients: RestSourceClientService, + @Context private val config: AuthorizerConfig, +) : OAuth2RestSourceAuthorizationService(clients, config) { + override suspend fun requestAccessToken(payload: RequestTokenPayload, sourceType: String): RestOauth2AccessToken { + val accessToken: RestOauth2AccessToken = super.requestAccessToken(payload, sourceType) + return accessToken.copy(externalUserId = getExternalId(accessToken.accessToken)) + } + + override suspend 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) { "Missing Oura deregistration endpoint configuration" } + + val isSuccess = try { + 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 + } + } + } catch (ex: Exception) { + logger.warn("Revoke endpoint error: {}", ex.toString()) + false + } + + return if (isSuccess) { + logger.info("Successfully revoked token for user {}", user.userId) + true + } else { + logger.error("Failed to revoke token for user {}", user.userId) + false + } + } + + private suspend fun getExternalId(accessToken: String): String = withContext(Dispatchers.IO) { + try { + val response = httpClient.get { + url { + takeFrom(OURA_USER_ID_ENDPOINT) + parameters.append("access_token", accessToken) + } + } + if (response.status.isSuccess()) { + response.body().userId + } else { + logger.error( + "Unable to fetch data from Oura $OURA_USER_ID_ENDPOINT (Http Status {}): {}", + response.status, + response.bodyAsText().take(512), + ) + throw HttpBadGatewayException("Cannot connect to $OURA_USER_ID_ENDPOINT: HTTP status ${response.status}") + } + } catch (ex: IOException) { + logger.error("Unable to fetch data from Oura $OURA_USER_ID_ENDPOINT: {}", ex.toString()) + throw HttpBadGatewayException("Cannot connect to $OURA_USER_ID_ENDPOINT: I/O error") + } + } + + companion object { + private const val OURA_USER_ID_ENDPOINT = "https://api.ouraring.com/v1/userinfo" + } +} 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..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 @@ -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..cecc607b 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,13 +12,14 @@ 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) + Hmac256Secret(secretLength = 12, saltLength = 6) } else { - Hmac256Secret.generate(secretLength = 6, saltLength = 3) + Hmac256Secret(secretLength = 6, saltLength = 3) } - val tokenState = registrationRepository.generate(user, secret, persistent) ?: throw HttpInternalServerException("token_not_generated", "Failed to generate token.") + val tokenState = registrationRepository.generate(user, secret, persistent) + ?: throw HttpInternalServerException("token_not_generated", "Failed to generate token.") return RegistrationResponse( token = tokenState.token, @@ -31,8 +32,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..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,21 +16,30 @@ package org.radarbase.authorizer.service +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 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 +47,25 @@ 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/Extensions.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Extensions.kt new file mode 100644 index 00000000..58f1ad0d --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/util/Extensions.kt @@ -0,0 +1,14 @@ +package org.radarbase.authorizer.util + +import java.security.SecureRandom +import java.util.Base64 + +private val RANDOM = SecureRandom() +private val STATE_ENCODER: Base64.Encoder = Base64.getUrlEncoder().withoutPadding() + +fun ByteArray.randomize(): ByteArray = apply { + RANDOM.nextBytes(this) +} + +fun ByteArray.encodeToBase64(): String = STATE_ENCODER.encodeToString(this) +fun String.decodeFromBase64(): ByteArray = Base64.getUrlDecoder().decode(this) 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..10dcde54 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,14 +1,13 @@ package org.radarbase.authorizer.util -import java.security.SecureRandom -import java.util.* import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec class Hmac256Secret(val secret: String, val salt: ByteArray, val secretHash: ByteArray) { - val isValid: Boolean - get() = runHmac256(salt = salt, secret = secret.decodeFromBase64()) + val isValid: Boolean by lazy { + runHmac256(salt = salt, secret = secret.decodeFromBase64()) .contentEquals(secretHash) + } override fun equals(other: Any?): Boolean { if (this === other) return true @@ -27,35 +26,22 @@ class Hmac256Secret(val secret: String, val salt: ByteArray, val secretHash: Byt result = 31 * result + secretHash.contentHashCode() return result } +} - companion object { - private const val HMAC_SHA256 = "HmacSHA256" - - private val RANDOM = SecureRandom() - private val STATE_ENCODER: Base64.Encoder = Base64.getUrlEncoder().withoutPadding() - private val STATE_DECODER: Base64.Decoder = Base64.getUrlDecoder() - - private fun runHmac256(salt: ByteArray, secret: ByteArray) = Mac.getInstance(HMAC_SHA256).run { - init(SecretKeySpec(salt, HMAC_SHA256)) - doFinal(secret) - } - - fun generate(secretLength: Int, saltLength: Int = 6): Hmac256Secret { - val secret = ByteArray(secretLength).randomize() - val salt = ByteArray(saltLength).randomize() +private const val HMAC_SHA256 = "HmacSHA256" - return Hmac256Secret( - secret = secret.encodeToBase64(), - salt = salt, - secretHash = runHmac256(salt = salt, secret = secret), - ) - } +private fun runHmac256(salt: ByteArray, secret: ByteArray) = Mac.getInstance(HMAC_SHA256).run { + init(SecretKeySpec(salt, HMAC_SHA256)) + doFinal(secret) +} - fun ByteArray.randomize(): ByteArray = apply { - RANDOM.nextBytes(this) - } +fun Hmac256Secret(secretLength: Int, saltLength: Int = 6): Hmac256Secret { + val secret = ByteArray(secretLength).randomize() + val salt = ByteArray(saltLength).randomize() - fun ByteArray.encodeToBase64(): String = STATE_ENCODER.encodeToString(this) - fun String.decodeFromBase64(): ByteArray = STATE_DECODER.decode(this) - } + return Hmac256Secret( + secret = secret.encodeToBase64(), + salt = salt, + secretHash = runHmac256(salt = salt, secret = secret), + ) } 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..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,7 +1,8 @@ package org.radarbase.authorizer.util +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 @@ -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/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() - } -} 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..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 okhttp3.HttpUrl.Companion.toHttpUrl +import io.ktor.http.Url 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/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" \ diff --git a/authorizer-app/docker/default.conf b/authorizer-app/docker/default.conf index ae369d02..276eac34 100644 --- a/authorizer-app/docker/default.conf +++ b/authorizer-app/docker/default.conf @@ -22,4 +22,4 @@ server { # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; -} +} \ No newline at end of file diff --git a/authorizer-app/package.json b/authorizer-app/package.json index 905f1121..3e0a7a8b 100644 --- a/authorizer-app/package.json +++ b/authorizer-app/package.json @@ -1,6 +1,6 @@ { "name": "authorizer-app", - "version": "4.3.0", + "version": "4.4.0", "description": "Simple app to authorize to collect data from third party services ", "repository": { "type": "git", 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) => { diff --git a/build.gradle.kts b/build.gradle.kts index 0dab2002..8a4e778e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,60 +1,28 @@ -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 + kotlin("plugin.noarg") version Versions.kotlin apply false + kotlin("plugin.jpa") version Versions.kotlin apply false + kotlin("plugin.allopen") version Versions.kotlin apply false } -allprojects { - group = "org.radarbase" - version = "4.3.0" - - repositories { - mavenCentral() - mavenLocal() -// maven(url = "https://oss.sonatype.org/content/repositories/snapshots") - } +radarRootProject { + projectVersion.set(Versions.project) + gradleVersion.set(Versions.gradle) } 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..1854997d --- /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.9.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..5b0fd5c3 --- /dev/null +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -0,0 +1,24 @@ +@Suppress("ConstPropertyName") +object Versions { + const val project = "4.4.0" + + const val java = 17 + + const val kotlin = "1.9.10" + + const val radarCommons = "1.1.1" + const val radarJersey = "0.11.0" + const val postgresql = "42.6.0" + const val ktor = "2.3.5" + const val jedis = "5.0.2" + + const val slf4j = "2.0.9" + 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.4" +} diff --git a/docker-compose.yml b/docker-compose.yml index 41499752..3b49ccda 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,6 +75,9 @@ services: depends_on: - radar-rest-sources-backend - managementportal + - redis +# ports: +# - "8080:80" environment: BASE_HREF: "/rest-sources/authorizer/" BACKEND_BASE_URL: "http://localhost:8080/rest-sources/backend" @@ -100,3 +103,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/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;; diff --git a/docker/etc/rest-source-authorizer/authorizer.yml b/docker/etc/rest-source-authorizer/authorizer.yml index e3eb19c3..0b4eff17 100644 --- a/docker/etc/rest-source-authorizer/authorizer.yml +++ b/docker/etc/rest-source-authorizer/authorizer.yml @@ -19,6 +19,9 @@ database: password: radarcns dialect: org.hibernate.dialect.PostgreSQLDialect +redis: + uri: redis://redis:6379 + restSourceClients: - sourceType: FitBit authorizationEndpoint: https://www.fitbit.com/oauth2/authorize @@ -26,3 +29,10 @@ restSourceClients: 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: Oura-clientid + clientSecret: Oura-clinetsecret + 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 4265a7d1..f2fcf95c 100644 --- a/docker/etc/rest-source-authorizer/authorizer.yml.template +++ b/docker/etc/rest-source-authorizer/authorizer.yml.template @@ -41,4 +41,10 @@ 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 + deregistrationEndpoint: https://api.ouraring.com/oauth/revoke + clientId: Oura-clientid + clientSecret: Oura-clinetsecret + scope: daily session heartrate workout tag personal email spo2 ring_configuration 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; + } + } +} diff --git a/gradle.properties b/gradle.properties index 62afa364..7fc6f1ff 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,29 +1 @@ -# -# 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 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba77..7f93135c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bdc9a83b..3fa8f862 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.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d42..1aa94a42 100755 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# 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"' +# 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 @@ -133,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. @@ -144,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 @@ -152,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 @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# 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. + +# 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, 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" \ diff --git a/settings.gradle.kts b/settings.gradle.kts index 6540b1ce..b2226682 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,12 +1,10 @@ rootProject.name = "radar-rest-sources-authorizer" + include(":authorizer-app-backend") pluginManagement { - val kotlinVersion: String by settings - plugins { - kotlin("jvm") version kotlinVersion - id("org.jetbrains.kotlin.plugin.noarg") version kotlinVersion - id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion - id("org.jetbrains.kotlin.plugin.allopen") version kotlinVersion + repositories { + gradlePluginPortal() + mavenCentral() } }