From 8d00f908a610b3e7afbd152559cc9e3fa6d6b0ba Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 25 Sep 2024 21:25:03 +0100 Subject: [PATCH 01/27] Add support for separate auth server (ory hydra) --- .../authorizer/api/RestSourceUserMapper.kt | 8 +- .../radarbase/authorizer/config/AuthConfig.kt | 6 +- .../ManagementPortalEnhancerFactory.kt | 1 + .../authorizer/resources/ProjectResource.kt | 24 ++--- .../resources/RegistrationResource.kt | 6 +- .../resources/RestSourceUserResource.kt | 6 +- .../radarbase/authorizer/service/MPClient.kt | 96 ++++++++++++++++++ .../authorizer/service/ProjectService.kt | 97 +++++++++++++++++++ .../login-page/login-page.component.ts | 5 +- 9 files changed, 228 insertions(+), 21 deletions(-) create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/ProjectService.kt 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 eedab90b..3501c10e 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 @@ -18,12 +18,16 @@ 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.authorizer.service.ProjectService +import org.radarbase.authorizer.service.MPClient import org.radarbase.kotlin.coroutines.forkJoin +import org.radarbase.jersey.auth.AuthService class RestSourceUserMapper( - @Context private val projectService: RadarProjectService, + @Context private val authService: AuthService, ) { + private val projectService: ProjectService = ProjectService(MPClient(), authService) + suspend fun fromEntity(user: RestSourceUser): RestSourceUserDTO { val mpUser = user.projectId?.let { p -> user.userId?.let { u -> projectService.subject(p, u) } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt index 34915edb..7669f38b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt @@ -1,11 +1,13 @@ package org.radarbase.authorizer.config data class AuthConfig( - val managementPortalUrl: String = "http://managementportal-app:8080/managementportal", + val managementPortalUrl: String = "http://host.docker.internal:8080/managementportal", + val authUrl: String = "http://host.docker.internal:4444/oauth2/token", val clientId: String = "radar_rest_sources_auth_backend", - val clientSecret: String? = null, + val clientSecret: String? = "secret", val jwtECPublicKeys: List? = null, val jwtRSAPublicKeys: List? = null, val jwtIssuer: String? = null, val jwtResourceName: String = "res_restAuthorizer", + val jwksUrls: List = listOf("http://host.docker.internal:4445/admin/keys/hydra.jwt.access-token"), ) 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 149006b5..623db71c 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 @@ -38,6 +38,7 @@ class ManagementPortalEnhancerFactory(private val config: AuthorizerConfig) : En syncParticipantsIntervalMin = config.service.syncParticipantsIntervalMin, ), jwtResourceName = config.auth.jwtResourceName, + jwksUrls = config.auth.jwksUrls, ) val dbConfig = config.database.copy( 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 fb0df46b..53521f37 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 @@ -17,12 +17,9 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource +import jakarta.inject.Provider import jakarta.inject.Singleton -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.* import jakarta.ws.rs.container.AsyncResponse import jakarta.ws.rs.container.Suspended import jakarta.ws.rs.core.Context @@ -33,11 +30,13 @@ 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.authorizer.service.MPClient +import org.radarbase.authorizer.service.ProjectService +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.service.AsyncCoroutineService -import org.radarbase.jersey.service.managementportal.RadarProjectService @Path("projects") @Authenticated @@ -46,17 +45,19 @@ import org.radarbase.jersey.service.managementportal.RadarProjectService @Resource @Singleton class ProjectResource( - @Context private val projectService: RadarProjectService, @Context private val asyncService: AsyncCoroutineService, + @Context private val authService: AuthService, ) { + private val projectService: ProjectService = ProjectService(MPClient(), authService) @GET @NeedsPermission(Permission.PROJECT_READ) @Cache(maxAge = 300, isPrivate = true, vary = [AUTHORIZATION]) - fun projects(@Suspended asyncResponse: AsyncResponse) = asyncService.runAsCoroutine(asyncResponse) { + fun projects( + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { ProjectList( - projectService.userProjects() - .map { it.toProject() }, + projectService.getProjects().map { it.toProject() }, ) } @@ -69,7 +70,8 @@ class ProjectResource( @Suspended asyncResponse: AsyncResponse, ) = asyncService.runAsCoroutine(asyncResponse) { UserList( - projectService.projectSubjects(projectId) + projectService + .projectSubjects(projectId) .map { it.toUser() }, ) } 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 06050e48..92091b53 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 @@ -33,8 +33,9 @@ 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 +import org.radarbase.authorizer.service.MPClient +import org.radarbase.authorizer.service.ProjectService @Path("registrations") @Produces(MediaType.APPLICATION_JSON) @@ -47,10 +48,11 @@ class RegistrationResource( @Context private val authorizationService: RestSourceAuthorizationService, @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, ) { + private val projectService: ProjectService = ProjectService(MPClient(), authService) + @POST @Authenticated @NeedsPermission(Permission.SUBJECT_UPDATE) 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 ccf9ae2b..546c58af 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 @@ -50,8 +50,9 @@ 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 +import org.radarbase.authorizer.service.MPClient +import org.radarbase.authorizer.service.ProjectService @Path("users") @Produces(MediaType.APPLICATION_JSON) @@ -62,13 +63,14 @@ import java.net.URI class RestSourceUserResource( @Context private val userRepository: RestSourceUserRepository, @Context private val userMapper: RestSourceUserMapper, - @Context private val projectService: RadarProjectService, @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, ) { + private val projectService: ProjectService = ProjectService(MPClient(), authService) + @GET @NeedsPermission(Permission.SUBJECT_READ) fun query( diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt new file mode 100644 index 00000000..0d7355a9 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt @@ -0,0 +1,96 @@ +package org.radarbase.authorizer.service + +import jakarta.inject.Singleton +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.Serializable +import org.slf4j.LoggerFactory +import kotlinx.serialization.json.Json +import org.radarbase.management.client.MPProject +import org.radarbase.management.client.MPOrganization +import org.radarbase.management.client.MPSubject +import io.ktor.client.request.forms.* +import io.ktor.client.call.* +import org.radarbase.authorizer.config.AuthorizerConfig + +class MPClient { + private val config: AuthorizerConfig = AuthorizerConfig() + private val logger = LoggerFactory.getLogger(MPClient::class.java) + + private val httpClient = HttpClient(CIO) { + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true }) + } + install(HttpTimeout) { + requestTimeoutMillis = 30_000 + } + } + + suspend fun getAccessToken(): String { + val response: HttpResponse = httpClient.submitForm( + url = config.auth.authUrl, + formParameters = Parameters.build { + append("grant_type", "client_credentials") + append("client_id", "radar_rest_sources_auth_backend") + append("client_secret", "secret") + append("scope", "SUBJECT.READ PROJECT.READ") + append("audience", "res_ManagementPortal") + } + ) + + if (!response.status.isSuccess()) { + logger.error("Failed to acquire access token: ${response.status}") + throw RuntimeException("Unable to retrieve access token") + } + + val tokenResponse = response.body() + return tokenResponse.access_token + } + + suspend fun requestProjects(page: Int = 0, size: Int = Int.MAX_VALUE): List { + val accessToken = getAccessToken() + val response: HttpResponse = httpClient.get("${config.auth.managementPortalUrl}/api/projects") { + headers { + append(HttpHeaders.Authorization, "Bearer $accessToken") + } + } + + if (!response.status.isSuccess()) { + logger.error("Failed to fetch projects: ${response.status}") + throw RuntimeException("Failed to fetch projects") + } + + return response.body() + } + + suspend fun requestSubjects(projectId: String, page: Int = 0, size: Int = Int.MAX_VALUE,): List { + logger.info("Fetching subjects for project $projectId") + val accessToken = getAccessToken() + logger.info("Access token: $accessToken") + val response: HttpResponse = httpClient.get("${config.auth.managementPortalUrl}/api/projects/${projectId}/subjects") { + headers { + append(HttpHeaders.Authorization, "Bearer $accessToken") + } + } + + if (!response.status.isSuccess()) { + logger.error("Failed to fetch projects: ${response.status}") + throw RuntimeException("Failed to fetch projects") + } + + return response.body() + } +} + +@Serializable +data class TokenResponse( + val access_token: String, + val token_type: String, + val expires_in: Long +) \ No newline at end of file diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/ProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/ProjectService.kt new file mode 100644 index 00000000..bd5d5070 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/ProjectService.kt @@ -0,0 +1,97 @@ +package org.radarbase.authorizer.service + +import jakarta.inject.Singleton +import org.radarbase.jersey.auth.AuthService +import org.radarbase.kotlin.coroutines.CacheConfig +import org.radarbase.kotlin.coroutines.CachedMap +import org.radarbase.management.client.MPProject +import org.radarbase.management.client.MPSubject +import org.radarbase.management.client.MPOrganization +import org.slf4j.LoggerFactory +import java.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.toKotlinDuration +import jakarta.ws.rs.core.Context +import org.radarbase.auth.authorization.EntityDetails +import org.radarbase.auth.authorization.Permission +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import org.radarbase.jersey.exception.HttpNotFoundException + +class ProjectService( + private val mpClient: MPClient, + private val authService: AuthService, +) { + private val projects: CachedMap + private val participants: ConcurrentMap> = ConcurrentHashMap() + + private val projectCacheConfig = CacheConfig( + refreshDuration = Duration.ofMinutes(5).toKotlinDuration(), + retryDuration = RETRY_INTERVAL, + ) + + init { + val cacheConfig = + CacheConfig( + refreshDuration = Duration.ofMinutes(5).toKotlinDuration(), + retryDuration = 1.minutes, + ) + + projects = + CachedMap(cacheConfig) { + try { + val projectList = mpClient.requestProjects() + projectList.associateBy { it.id } + } catch (e: Exception) { + logger.error("Failed to fetch projects from Management Portal", e) + throw RuntimeException("Unable to fetch projects", e) + } + } + } + + suspend fun getProjects(): List = projects.get().values.toList() + + suspend fun userProjects(permission: Permission): List { + return projects.get() + .values + .filter { + authService.hasPermission( + permission, + EntityDetails( + organization = it.organization?.id, + project = it.id, + ), + ) + } + } + + suspend fun ensureProject(projectId: String) { + if (!projects.contains(projectId)) { + throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") + } + } + + suspend fun project(projectId: String): MPProject = projects.get(projectId) + ?: throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") + + suspend fun projectSubjects(projectId: String): List = projectUserCache(projectId).get().values.toList() + + private suspend fun projectUserCache(projectId: String) = participants.computeIfAbsent(projectId) { + CachedMap(projectCacheConfig) { + mpClient.requestSubjects(projectId) + .associateBy { checkNotNull(it.id) } + } + } + + suspend fun subject(projectId: String, userId: String): MPSubject? { + ensureProject(projectId) + return projectUserCache(projectId).get(userId) + } + + companion object { + private val RETRY_INTERVAL = 1.minutes + private val logger = LoggerFactory.getLogger(ProjectService::class.java) + private val defaultOrganization = MPOrganization("main") + } + +} diff --git a/authorizer-app/src/app/auth/containers/login-page/login-page.component.ts b/authorizer-app/src/app/auth/containers/login-page/login-page.component.ts index 4af01b7a..22584952 100644 --- a/authorizer-app/src/app/auth/containers/login-page/login-page.component.ts +++ b/authorizer-app/src/app/auth/containers/login-page/login-page.component.ts @@ -81,8 +81,9 @@ export class LoginPageComponent implements OnInit, OnDestroy { } redirectToAuthRequestLink() { - window.location.href = `${environment.authBaseUrl}/authorize?client_id=${ + const scopes = "SOURCETYPE.READ%20PROJECT.READ%20SUBJECT.READ%20SUBJECT.UPDATE%20SUBJECT.CREATE" + window.location.href = `${environment.authBaseUrl}/auth?client_id=${ environment.appClientId - }&response_type=code&redirect_uri=${window.location.href.split('?')[0]}`; + }&response_type=code&state=${Date.now()}&audience=res_restAuthorizer&scope=${scopes}&redirect_uri=${window.location.href.split('?')[0]}`; } } From 5923ad38502e8a2077ad9b6ba729569627315a44 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 25 Sep 2024 22:54:11 +0100 Subject: [PATCH 02/27] Fix Management portal auth and resource enhancer --- .../org/radarbase/authorizer/api/Project.kt | 4 +- .../authorizer/api/RestSourceUserMapper.kt | 5 +- .../enhancer/AuthorizerResourceEnhancer.kt | 6 ++ .../ManagementPortalEnhancerFactory.kt | 3 +- .../authorizer/resources/ProjectResource.kt | 7 +- .../resources/RegistrationResource.kt | 6 +- .../resources/RestSourceUserResource.kt | 4 +- .../radarbase/authorizer/service/MPClient.kt | 17 +++++ ...ojectService.kt => RadarProjectService.kt} | 76 +++++++++++++++---- 9 files changed, 96 insertions(+), 32 deletions(-) rename authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/{ProjectService.kt => RadarProjectService.kt} (54%) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt index 0472713b..70066beb 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt @@ -41,9 +41,9 @@ data class UserList(val users: List) data class User(val id: String, val projectId: String, val externalId: String? = null, val status: String) -fun MPSubject.toUser() = User( +fun MPSubject.toUser(projectId: String) = User( id = checkNotNull(id) { "User must have a login" }, - projectId = checkNotNull(projectId) { "User must have a project ID" }, + projectId = projectId, externalId = externalId, status = status, ) 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 3501c10e..3d08e04e 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 @@ -18,15 +18,14 @@ package org.radarbase.authorizer.api import jakarta.ws.rs.core.Context import org.radarbase.authorizer.doa.entity.RestSourceUser -import org.radarbase.authorizer.service.ProjectService +import org.radarbase.authorizer.service.RadarProjectService import org.radarbase.authorizer.service.MPClient import org.radarbase.kotlin.coroutines.forkJoin import org.radarbase.jersey.auth.AuthService class RestSourceUserMapper( - @Context private val authService: AuthService, ) { - private val projectService: ProjectService = ProjectService(MPClient(), authService) + private val projectService: RadarProjectService = RadarProjectService() suspend fun fromEntity(user: RestSourceUser): RestSourceUserDTO { val mpUser = user.projectId?.let { p -> 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 556ed2b5..6ec81174 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 @@ -36,8 +36,10 @@ 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.authorizer.service.RadarProjectService import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.filter.Filters +import org.radarbase.jersey.service.ProjectService class AuthorizerResourceEnhancer( private val config: AuthorizerConfig, @@ -116,5 +118,9 @@ class AuthorizerResourceEnhancer( .to(RestSourceAuthorizationService::class.java) .named(OURA_AUTH) .`in`(Singleton::class.java) + + bind(RadarProjectService::class.java) + .to(ProjectService::class.java) + .`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 623db71c..6e8b0c01 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 @@ -32,8 +32,6 @@ class ManagementPortalEnhancerFactory(private val config: AuthorizerConfig) : En val authConfig = AuthConfig( managementPortal = MPConfig( url = config.auth.managementPortalUrl.trimEnd('/'), - clientId = config.auth.clientId, - clientSecret = config.auth.clientSecret, syncProjectsIntervalMin = config.service.syncProjectsIntervalMin, syncParticipantsIntervalMin = config.service.syncParticipantsIntervalMin, ), @@ -52,6 +50,7 @@ class ManagementPortalEnhancerFactory(private val config: AuthorizerConfig) : En Enhancers.health, HibernateResourceEnhancer(dbConfig), Enhancers.managementPortal(authConfig), + Enhancers.ecdsa, JedisResourceEnhancer(), Enhancers.exception, AuthorizerResourceEnhancer(config), 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 53521f37..2bb16f61 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 @@ -31,7 +31,7 @@ import org.radarbase.authorizer.api.UserList import org.radarbase.authorizer.api.toProject import org.radarbase.authorizer.api.toUser import org.radarbase.authorizer.service.MPClient -import org.radarbase.authorizer.service.ProjectService +import org.radarbase.authorizer.service.RadarProjectService import org.radarbase.jersey.auth.AuthService import org.radarbase.jersey.auth.Authenticated import org.radarbase.jersey.auth.NeedsPermission @@ -46,9 +46,8 @@ import org.radarbase.jersey.service.AsyncCoroutineService @Singleton class ProjectResource( @Context private val asyncService: AsyncCoroutineService, - @Context private val authService: AuthService, ) { - private val projectService: ProjectService = ProjectService(MPClient(), authService) + private val projectService: RadarProjectService = RadarProjectService() @GET @NeedsPermission(Permission.PROJECT_READ) @@ -72,7 +71,7 @@ class ProjectResource( UserList( projectService .projectSubjects(projectId) - .map { it.toUser() }, + .map { it.toUser(projectId) }, ) } 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 92091b53..63de730c 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 @@ -35,7 +35,7 @@ import org.radarbase.jersey.exception.HttpConflictException import org.radarbase.jersey.service.AsyncCoroutineService import java.net.URI import org.radarbase.authorizer.service.MPClient -import org.radarbase.authorizer.service.ProjectService +import org.radarbase.authorizer.service.RadarProjectService @Path("registrations") @Produces(MediaType.APPLICATION_JSON) @@ -48,10 +48,10 @@ class RegistrationResource( @Context private val authorizationService: RestSourceAuthorizationService, @Context private val userRepository: RestSourceUserRepository, @Context private val registrationService: RegistrationService, - @Context private val authService: AuthService, @Context private val asyncService: AsyncCoroutineService, + @Context private val authService: AuthService, ) { - private val projectService: ProjectService = ProjectService(MPClient(), authService) + private val projectService: RadarProjectService = RadarProjectService() @POST @Authenticated 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 546c58af..63bd2ba4 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 @@ -52,7 +52,7 @@ import org.radarbase.jersey.exception.HttpBadRequestException import org.radarbase.jersey.service.AsyncCoroutineService import java.net.URI import org.radarbase.authorizer.service.MPClient -import org.radarbase.authorizer.service.ProjectService +import org.radarbase.authorizer.service.RadarProjectService @Path("users") @Produces(MediaType.APPLICATION_JSON) @@ -69,7 +69,7 @@ class RestSourceUserResource( @Context private val asyncService: AsyncCoroutineService, @Context private val authService: AuthService, ) { - private val projectService: ProjectService = ProjectService(MPClient(), authService) + private val projectService: RadarProjectService = RadarProjectService() @GET @NeedsPermission(Permission.SUBJECT_READ) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt index 0d7355a9..7ad03339 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt @@ -19,6 +19,7 @@ import io.ktor.client.request.forms.* import io.ktor.client.call.* import org.radarbase.authorizer.config.AuthorizerConfig +@Singleton class MPClient { private val config: AuthorizerConfig = AuthorizerConfig() private val logger = LoggerFactory.getLogger(MPClient::class.java) @@ -53,6 +54,22 @@ class MPClient { return tokenResponse.access_token } + suspend fun requestOrganizations(page: Int = 0, size: Int = Int.MAX_VALUE): List { + val accessToken = getAccessToken() + val response: HttpResponse = httpClient.get("${config.auth.managementPortalUrl}/api/organizations") { + headers { + append(HttpHeaders.Authorization, "Bearer $accessToken") + } + } + + if (!response.status.isSuccess()) { + logger.error("Failed to fetch projects: ${response.status}") + throw RuntimeException("Failed to fetch projects") + } + + return response.body() + } + suspend fun requestProjects(page: Int = 0, size: Int = Int.MAX_VALUE): List { val accessToken = getAccessToken() val response: HttpResponse = httpClient.get("${config.auth.managementPortalUrl}/api/projects") { diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/ProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt similarity index 54% rename from authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/ProjectService.kt rename to authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt index bd5d5070..f1b295b3 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/ProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt @@ -1,12 +1,14 @@ package org.radarbase.authorizer.service import jakarta.inject.Singleton +import io.ktor.http.HttpStatusCode import org.radarbase.jersey.auth.AuthService import org.radarbase.kotlin.coroutines.CacheConfig import org.radarbase.kotlin.coroutines.CachedMap import org.radarbase.management.client.MPProject import org.radarbase.management.client.MPSubject import org.radarbase.management.client.MPOrganization +import org.radarbase.management.client.HttpStatusException import org.slf4j.LoggerFactory import java.time.Duration import kotlin.time.Duration.Companion.minutes @@ -17,12 +19,14 @@ import org.radarbase.auth.authorization.Permission import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap import org.radarbase.jersey.exception.HttpNotFoundException +import org.radarbase.jersey.service.ProjectService + +class RadarProjectService( +): ProjectService { + private val mpClient: MPClient = MPClient() -class ProjectService( - private val mpClient: MPClient, - private val authService: AuthService, -) { private val projects: CachedMap + private val organizations: CachedMap private val participants: ConcurrentMap> = ConcurrentHashMap() private val projectCacheConfig = CacheConfig( @@ -47,34 +51,74 @@ class ProjectService( throw RuntimeException("Unable to fetch projects", e) } } + + organizations = CachedMap(cacheConfig) { + try { + mpClient.requestOrganizations() + .associateBy { it.id } + .also { logger.debug("Fetched organizations {}", it) } + } catch (ex: HttpStatusException) { + if (ex.code == HttpStatusCode.NotFound) { + logger.warn("Target ManagementPortal does not support organizations. Using default organization main.") + mapOf("main" to defaultOrganization) + } else { + throw ex + } + } + } } suspend fun getProjects(): List = projects.get().values.toList() suspend fun userProjects(permission: Permission): List { return projects.get() - .values - .filter { - authService.hasPermission( - permission, - EntityDetails( - organization = it.organization?.id, - project = it.id, - ), - ) - } + .values.toList() + // .filter { + // authService.hasPermission( + // permission, + // EntityDetails( + // organization = it.organization?.id, + // project = it.id, + // ), + // ) + // } } - suspend fun ensureProject(projectId: String) { + override suspend fun ensureSubject(projectId: String, userId: String) { + ensureProject(projectId) + if (!projectUserCache(projectId).contains(userId)) { + throw HttpNotFoundException("user_not_found", "User $userId not found in project $projectId of ManagementPortal.") + } + } + + override suspend fun ensureOrganization(organizationId: String) { + if (!organizations.contains(organizationId)) { + throw HttpNotFoundException("organization_not_found", "Organization $organizationId not found in Management Portal.") + } + } + + override suspend fun ensureProject(projectId: String) { if (!projects.contains(projectId)) { throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") } } + override suspend fun projectOrganization(projectId: String): String = + projects.get(projectId)?.organization?.id + ?: throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") + suspend fun project(projectId: String): MPProject = projects.get(projectId) ?: throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") - suspend fun projectSubjects(projectId: String): List = projectUserCache(projectId).get().values.toList() + override suspend fun listProjects(organizationId: String): List = projects.get().asSequence() + .filter { it.value.organization?.id == organizationId } + .mapTo(ArrayList()) { it.key } + + suspend fun projectSubjects(projectId: String): List { + logger.info("Fetching subjects for project $projectId") + logger.info(projectUserCache(projectId).get().values.toList().toString()) + return projectUserCache(projectId).get().values.toList() + } private suspend fun projectUserCache(projectId: String) = participants.computeIfAbsent(projectId) { CachedMap(projectCacheConfig) { From 37f668c7e4d68e795f2d1d577096b7170a1ccd40 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 26 Sep 2024 11:25:02 +0100 Subject: [PATCH 03/27] Cleanup logs --- .../src/main/java/org/radarbase/authorizer/service/MPClient.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt index 7ad03339..2ebca737 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt @@ -87,9 +87,7 @@ class MPClient { } suspend fun requestSubjects(projectId: String, page: Int = 0, size: Int = Int.MAX_VALUE,): List { - logger.info("Fetching subjects for project $projectId") val accessToken = getAccessToken() - logger.info("Access token: $accessToken") val response: HttpResponse = httpClient.get("${config.auth.managementPortalUrl}/api/projects/${projectId}/subjects") { headers { append(HttpHeaders.Authorization, "Bearer $accessToken") From d2649b496c12ab4aea5ca2faab299dc16632fc2c Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 26 Sep 2024 14:56:26 +0100 Subject: [PATCH 04/27] Add back auth service check for project permissions --- .../org/radarbase/authorizer/api/Project.kt | 43 +++-- .../authorizer/api/RestSourceUserMapper.kt | 25 +-- .../enhancer/AuthorizerResourceEnhancer.kt | 41 ++--- .../ManagementPortalEnhancerFactory.kt | 38 +++-- .../authorizer/resources/ProjectResource.kt | 24 ++- .../resources/RegistrationResource.kt | 84 ++++++---- .../resources/RestSourceUserResource.kt | 156 +++++++++--------- .../radarbase/authorizer/service/MPClient.kt | 98 ++++++----- .../authorizer/service/RadarProjectService.kt | 141 ++++++++-------- 9 files changed, 361 insertions(+), 289 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt index 70066beb..7aaccb5f 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt @@ -19,7 +19,9 @@ package org.radarbase.authorizer.api import org.radarbase.management.client.MPProject import org.radarbase.management.client.MPSubject -data class ProjectList(val projects: List) +data class ProjectList( + val projects: List, +) data class Project( val id: String, @@ -29,21 +31,30 @@ data class Project( val description: String? = null, ) -fun MPProject.toProject() = Project( - id = id, - name = name, - location = location, - organization = organization?.id, - description = description, +fun MPProject.toProject() = + Project( + id = id, + name = name, + location = location, + organization = organization?.id, + description = description, + ) + +data class UserList( + val users: List, ) -data class UserList(val users: List) - -data class User(val id: String, val projectId: String, val externalId: String? = null, val status: String) - -fun MPSubject.toUser(projectId: String) = User( - id = checkNotNull(id) { "User must have a login" }, - projectId = projectId, - externalId = externalId, - status = status, +data class User( + val id: String, + val projectId: String, + val externalId: String? = null, + val status: String, ) + +fun MPSubject.toUser(projectId: String) = + User( + id = checkNotNull(id) { "User must have a login" }, + projectId = projectId, + externalId = externalId, + status = status, + ) 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 3d08e04e..396b7612 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 @@ -16,28 +16,28 @@ package org.radarbase.authorizer.api -import jakarta.ws.rs.core.Context import org.radarbase.authorizer.doa.entity.RestSourceUser import org.radarbase.authorizer.service.RadarProjectService -import org.radarbase.authorizer.service.MPClient import org.radarbase.kotlin.coroutines.forkJoin -import org.radarbase.jersey.auth.AuthService -class RestSourceUserMapper( -) { +class RestSourceUserMapper { private val projectService: RadarProjectService = RadarProjectService() suspend fun fromEntity(user: RestSourceUser): RestSourceUserDTO { - val mpUser = user.projectId?.let { p -> - user.userId?.let { u -> projectService.subject(p, u) } - } + val mpUser = + user.projectId?.let { p -> + user.userId?.let { u -> projectService.subject(p, u) } + } return RestSourceUserDTO( id = user.id.toString(), createdAt = user.createdAt, projectId = user.projectId, userId = user.userId, - humanReadableUserId = mpUser?.attributes?.get("Human-readable-identifier") - ?.takeIf { it.isNotBlank() && it != "null" }, + humanReadableUserId = + mpUser + ?.attributes + ?.get("Human-readable-identifier") + ?.takeIf { it.isNotBlank() && it != "null" }, externalId = mpUser?.externalId, sourceId = user.sourceId, isAuthorized = user.authorized, @@ -52,7 +52,10 @@ class RestSourceUserMapper( ) } - suspend fun fromRestSourceUsers(records: List, page: Page?) = RestSourceUsers( + 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/enhancer/AuthorizerResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/AuthorizerResourceEnhancer.kt index 6ec81174..fac81767 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 @@ -32,11 +32,11 @@ import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService. import org.radarbase.authorizer.service.GarminSourceAuthorizationService import org.radarbase.authorizer.service.OAuth2RestSourceAuthorizationService import org.radarbase.authorizer.service.OuraAuthorizationService +import org.radarbase.authorizer.service.RadarProjectService 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.authorizer.service.RadarProjectService import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.filter.Filters import org.radarbase.jersey.service.ProjectService @@ -44,26 +44,29 @@ import org.radarbase.jersey.service.ProjectService class AuthorizerResourceEnhancer( private val config: AuthorizerConfig, ) : JerseyResourceEnhancer { - private val restSourceClients = RestSourceClients( - config.restSourceClients - .map { it.withEnv() } - .onEach { - requireNotNull(it.clientId) { "Client ID of ${it.sourceType} is missing" } - requireNotNull(it.clientSecret) { "Client secret of ${it.sourceType} is missing" } - }, - ) + private val restSourceClients = + RestSourceClients( + config.restSourceClients + .map { it.withEnv() } + .onEach { + requireNotNull(it.clientId) { "Client ID of ${it.sourceType} is missing" } + requireNotNull(it.clientSecret) { "Client secret of ${it.sourceType} is missing" } + }, + ) override val classes: Array> - get() = listOfNotNull( - Filters.cache, - Filters.logResponse, - if (config.service.enableCors == true) Filters.cors else null, - ).toTypedArray() - - override val packages: Array = arrayOf( - "org.radarbase.authorizer.resources", - "org.radarbase.authorizer.lifecycle", - ) + get() = + listOfNotNull( + Filters.cache, + Filters.logResponse, + if (config.service.enableCors == true) Filters.cors else null, + ).toTypedArray() + + override val packages: Array = + arrayOf( + "org.radarbase.authorizer.resources", + "org.radarbase.authorizer.lifecycle", + ) override fun AbstractBinder.enhance() { // Bind instances. These cannot use any injects themselves 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 6e8b0c01..b568ea14 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 @@ -27,24 +27,30 @@ import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.hibernate.config.HibernateResourceEnhancer /** This binder needs to register all non-Jersey classes, otherwise initialization fails. */ -class ManagementPortalEnhancerFactory(private val config: AuthorizerConfig) : EnhancerFactory { +class ManagementPortalEnhancerFactory( + private val config: AuthorizerConfig, +) : EnhancerFactory { override fun createEnhancers(): List { - val authConfig = AuthConfig( - managementPortal = MPConfig( - url = config.auth.managementPortalUrl.trimEnd('/'), - syncProjectsIntervalMin = config.service.syncProjectsIntervalMin, - syncParticipantsIntervalMin = config.service.syncParticipantsIntervalMin, - ), - jwtResourceName = config.auth.jwtResourceName, - jwksUrls = config.auth.jwksUrls, - ) + val authConfig = + AuthConfig( + managementPortal = + MPConfig( + url = config.auth.managementPortalUrl.trimEnd('/'), + syncProjectsIntervalMin = config.service.syncProjectsIntervalMin, + syncParticipantsIntervalMin = config.service.syncParticipantsIntervalMin, + ), + jwtResourceName = config.auth.jwtResourceName, + jwksUrls = config.auth.jwksUrls, + ) - val dbConfig = config.database.copy( - managedClasses = listOf( - RestSourceUser::class.qualifiedName!!, - RegistrationState::class.qualifiedName!!, - ), - ) + val dbConfig = + config.database.copy( + managedClasses = + listOf( + RestSourceUser::class.qualifiedName!!, + RegistrationState::class.qualifiedName!!, + ), + ) return listOf( Enhancers.radar(authConfig), Enhancers.health, 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 2bb16f61..a7717a33 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 @@ -17,7 +17,6 @@ package org.radarbase.authorizer.resources import jakarta.annotation.Resource -import jakarta.inject.Provider import jakarta.inject.Singleton import jakarta.ws.rs.* import jakarta.ws.rs.container.AsyncResponse @@ -25,12 +24,12 @@ 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.EntityDetails import org.radarbase.auth.authorization.Permission 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.authorizer.service.MPClient import org.radarbase.authorizer.service.RadarProjectService import org.radarbase.jersey.auth.AuthService import org.radarbase.jersey.auth.Authenticated @@ -46,6 +45,7 @@ import org.radarbase.jersey.service.AsyncCoroutineService @Singleton class ProjectResource( @Context private val asyncService: AsyncCoroutineService, + @Context private val authService: AuthService, ) { private val projectService: RadarProjectService = RadarProjectService() @@ -56,7 +56,17 @@ class ProjectResource( @Suspended asyncResponse: AsyncResponse, ) = asyncService.runAsCoroutine(asyncResponse) { ProjectList( - projectService.getProjects().map { it.toProject() }, + projectService + .userProjects() + .filter { + authService.hasPermission( + Permission.SUBJECT_READ, + EntityDetails( + organization = it.organization?.id, + project = it.id, + ), + ) + }.map { it.toProject() }, ) } @@ -69,9 +79,7 @@ class ProjectResource( @Suspended asyncResponse: AsyncResponse, ) = asyncService.runAsCoroutine(asyncResponse) { UserList( - projectService - .projectSubjects(projectId) - .map { it.toUser(projectId) }, + projectService.projectSubjects(projectId).map { it.toUser(projectId) }, ) } @@ -82,7 +90,5 @@ class ProjectResource( fun project( @PathParam("projectId") projectId: String, @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { - projectService.project(projectId).toProject() - } + ) = 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 63de730c..733e39dc 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 @@ -23,6 +23,7 @@ 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.RadarProjectService import org.radarbase.authorizer.service.RegistrationService import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceUserService @@ -34,8 +35,6 @@ import org.radarbase.jersey.exception.HttpBadRequestException import org.radarbase.jersey.exception.HttpConflictException import org.radarbase.jersey.service.AsyncCoroutineService import java.net.URI -import org.radarbase.authorizer.service.MPClient -import org.radarbase.authorizer.service.RadarProjectService @Path("registrations") @Produces(MediaType.APPLICATION_JSON) @@ -52,7 +51,7 @@ class RegistrationResource( @Context private val authService: AuthService, ) { private val projectService: RadarProjectService = RadarProjectService() - + @POST @Authenticated @NeedsPermission(Permission.SUBJECT_UPDATE) @@ -70,15 +69,18 @@ class RegistrationResource( ) var tokenState = registrationService.generate(user, createState.persistent) if (!createState.persistent) { - tokenState = tokenState.copy( - authEndpointUrl = authorizationService.getAuthorizationEndpointWithParams( - sourceType = user.sourceType, - userId = user.id!!, - state = tokenState.token, - ), - ) + tokenState = + tokenState.copy( + authEndpointUrl = + authorizationService.getAuthorizationEndpointWithParams( + sourceType = user.sourceType, + userId = user.id!!, + state = tokenState.token, + ), + ) } - Response.created(URI("tokens/${tokenState.token}")) + Response + .created(URI("tokens/${tokenState.token}")) .entity(tokenState) .build() } @@ -120,23 +122,34 @@ class RegistrationResource( @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.") + if (registration.user.authorized) { + throw HttpConflictException( + "user_already_authorized", + "User was already authorized for this service.", + ) + } val salt = registration.salt val secretHash = registration.secretHash - if (salt == null || secretHash == null) throw HttpBadRequestException("registration_invalid", "Cannot retrieve authentication endpoint token without credentials.") + if (salt == null || + secretHash == null + ) { + throw HttpBadRequestException("registration_invalid", "Cannot retrieve authentication endpoint token without credentials.") + } val hmac256Secret = Hmac256Secret(tokenSecret.secret, salt, secretHash) if (!hmac256Secret.isValid) throw HttpBadRequestException("bad_secret", "Secret does not match token") - val project = registration.user.projectId?.let { - projectService.project(it).toProject() - } + val project = + registration.user.projectId?.let { + projectService.project(it).toProject() + } RegistrationResponse( token = registration.token, - authEndpointUrl = authorizationService.getAuthorizationEndpointWithParams( - sourceType = registration.user.sourceType, - userId = registration.user.id!!, - state = registration.token, - ), + authEndpointUrl = + authorizationService.getAuthorizationEndpointWithParams( + sourceType = registration.user.sourceType, + userId = registration.user.id!!, + state = registration.token, + ), userId = registration.user.id!!.toString(), project = project, createdAt = registration.createdAt, @@ -157,23 +170,26 @@ class RegistrationResource( val registration = registrationService.ensureRegistration(token) val accessToken = authorizationService.requestAccessToken(payload, registration.user.sourceType) val user = userRepository.updateToken(accessToken, registration.user) - val project = registration.user.projectId?.let { - projectService.project(it).toProject() - } + val project = + registration.user.projectId?.let { + projectService.project(it).toProject() + } - val tokenEntity = RegistrationResponse( - token = registration.token, - userId = registration.user.id!!.toString(), - project = project, - createdAt = registration.createdAt, - expiresAt = registration.expiresAt, - persistent = registration.persistent, - sourceType = registration.user.sourceType, - ) + val tokenEntity = + RegistrationResponse( + token = registration.token, + userId = registration.user.id!!.toString(), + project = project, + createdAt = registration.createdAt, + expiresAt = registration.expiresAt, + persistent = registration.persistent, + sourceType = registration.user.sourceType, + ) registrationRepository.remove(registration) - 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 63bd2ba4..cd98ff84 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 @@ -41,6 +41,7 @@ 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.RadarProjectService import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceClientService import org.radarbase.authorizer.service.RestSourceUserService @@ -51,8 +52,6 @@ import org.radarbase.jersey.cache.Cache import org.radarbase.jersey.exception.HttpBadRequestException import org.radarbase.jersey.service.AsyncCoroutineService import java.net.URI -import org.radarbase.authorizer.service.MPClient -import org.radarbase.authorizer.service.RadarProjectService @Path("users") @Produces(MediaType.APPLICATION_JSON) @@ -77,63 +76,80 @@ class RestSourceUserResource( @QueryParam("project-id") projectId: String?, @QueryParam("source-type") sourceType: String?, @QueryParam("search") search: String?, - @DefaultValue("true") - @QueryParam("authorized") - isAuthorized: String, - @DefaultValue(Integer.MAX_VALUE.toString()) - @QueryParam("size") - pageSize: Int, - @DefaultValue("1") - @QueryParam("page") - pageNumber: Int, + @DefaultValue("true") @QueryParam("authorized") isAuthorized: String, + @DefaultValue(Integer.MAX_VALUE.toString()) @QueryParam("size") pageSize: Int, + @DefaultValue("1") @QueryParam("page") pageNumber: Int, @Suspended asyncResponse: AsyncResponse, ) = asyncService.runAsCoroutine(asyncResponse) { - val projectIds = if (projectId == null) { - projectService.userProjects(Permission.SUBJECT_READ) - .also { projects -> if (projects.isEmpty()) return@runAsCoroutine emptyUsers(pageNumber, pageSize) } - .map { it.id } - } else { - authService.checkPermission(Permission.SUBJECT_READ, EntityDetails(project = projectId)) - listOf(projectId) - } - - val sanitizedSourceType = when (sourceType) { - null -> null - in sourceClientService -> sourceType - else -> return@runAsCoroutine emptyUsers(pageNumber, pageSize) - } + val projectIds = + if (projectId == null) { + projectService + .userProjects() + .filter { + authService.hasPermission( + Permission.SUBJECT_READ, + EntityDetails( + organization = it.organization?.id, + project = it.id, + ), + ) + }.also { projects -> + if (projects.isEmpty()) { + return@runAsCoroutine emptyUsers( + pageNumber, + pageSize, + ) + } + }.map { it.id } + } else { + authService.checkPermission( + Permission.SUBJECT_READ, + EntityDetails(project = projectId), + ) + listOf(projectId) + } + + val sanitizedSourceType = + when (sourceType) { + null -> null + in sourceClientService -> sourceType + else -> return@runAsCoroutine emptyUsers(pageNumber, pageSize) + } val sanitizedSearch = search?.takeIf { it.length >= 2 } - val userIds = if (sanitizedSearch != null) { - projectId ?: throw HttpBadRequestException( - "missing_project_id", - "Cannot search without a fixed project ID.", - ) - projectService.projectSubjects(projectId) - .mapNotNull { sub -> + val userIds = + if (sanitizedSearch != null) { + projectId + ?: throw HttpBadRequestException( + "missing_project_id", + "Cannot search without a fixed project ID.", + ) + projectService.projectSubjects(projectId).mapNotNull { sub -> val externalId = sub.externalId ?: return@mapNotNull null sub.id.takeIf { sanitizedSearch in externalId } } - } else { - emptyList() - } + } else { + emptyList() + } - val authorizedBoolean = when (isAuthorized) { - "true", "yes" -> true - "false", "no" -> false - else -> null - } + val authorizedBoolean = + when (isAuthorized) { + "true", "yes" -> true + "false", "no" -> false + else -> null + } val queryPage = Page(pageNumber = pageNumber, pageSize = pageSize) - val (records, page) = userRepository.query( - queryPage, - projectIds, - sanitizedSourceType, - sanitizedSearch, - userIds, - authorizedBoolean, - ) + val (records, page) = + userRepository.query( + queryPage, + projectIds, + sanitizedSourceType, + sanitizedSearch, + userIds, + authorizedBoolean, + ) userMapper.fromRestSourceUsers(records, page) } @@ -146,9 +162,7 @@ class RestSourceUserResource( ) = asyncService.runAsCoroutine(asyncResponse) { val user = userService.create(userDto) - Response.created(URI("users/${user.id}")) - .entity(user) - .build() + Response.created(URI("users/${user.id}")).entity(user).build() } @POST @@ -158,9 +172,7 @@ class RestSourceUserResource( @PathParam("id") userId: Long, user: RestSourceUserDTO, @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { - userService.update(userId, user) - } + ) = asyncService.runAsCoroutine(asyncResponse) { userService.update(userId, user) } @GET @Path("{id}") @@ -169,9 +181,7 @@ class RestSourceUserResource( fun readUser( @PathParam("id") userId: Long, @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { - userService.get(userId) - } + ) = asyncService.runAsCoroutine(asyncResponse) { userService.get(userId) } @DELETE @Path("{id}") @@ -181,9 +191,7 @@ class RestSourceUserResource( @Suspended asyncResponse: AsyncResponse, ) = asyncService.runAsCoroutine(asyncResponse) { userService.delete(userId) - Response.noContent() - .header("user-removed", userId) - .build() + Response.noContent().header("user-removed", userId).build() } @POST @@ -193,9 +201,7 @@ class RestSourceUserResource( @PathParam("id") userId: Long, user: RestSourceUserDTO, @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { - userService.reset(userId, user) - } + ) = asyncService.runAsCoroutine(asyncResponse) { userService.reset(userId, user) } @GET @Path("{id}/token") @@ -203,9 +209,7 @@ class RestSourceUserResource( fun requestToken( @PathParam("id") userId: Long, @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { - userService.ensureToken(userId) - } + ) = asyncService.runAsCoroutine(asyncResponse) { userService.ensureToken(userId) } @POST @Path("{id}/token") @@ -213,9 +217,7 @@ class RestSourceUserResource( fun refreshToken( @PathParam("id") userId: Long, @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { - userService.refreshToken(userId) - } + ) = asyncService.runAsCoroutine(asyncResponse) { userService.refreshToken(userId) } @POST @Path("{id}/token/sign") @@ -230,13 +232,17 @@ class RestSourceUserResource( } companion object { - private fun emptyUsers(pageNumber: Int, pageSize: Int) = RestSourceUsers( + private fun emptyUsers( + pageNumber: Int, + pageSize: Int, + ) = RestSourceUsers( users = listOf(), - metadata = Page( - pageNumber = pageNumber, - pageSize = pageSize, - totalElements = 0, - ), + metadata = + Page( + pageNumber = pageNumber, + pageSize = pageSize, + totalElements = 0, + ), ) } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt index 2ebca737..19ed7bb8 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt @@ -1,49 +1,52 @@ package org.radarbase.authorizer.service -import jakarta.inject.Singleton import io.ktor.client.* +import io.ktor.client.call.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* +import io.ktor.client.request.forms.* import io.ktor.client.statement.* import io.ktor.http.* -import io.ktor.client.plugins.contentnegotiation.* import io.ktor.serialization.kotlinx.json.* +import jakarta.inject.Singleton import kotlinx.serialization.Serializable -import org.slf4j.LoggerFactory import kotlinx.serialization.json.Json -import org.radarbase.management.client.MPProject +import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.management.client.MPOrganization +import org.radarbase.management.client.MPProject import org.radarbase.management.client.MPSubject -import io.ktor.client.request.forms.* -import io.ktor.client.call.* -import org.radarbase.authorizer.config.AuthorizerConfig +import org.slf4j.LoggerFactory @Singleton class MPClient { private val config: AuthorizerConfig = AuthorizerConfig() private val logger = LoggerFactory.getLogger(MPClient::class.java) - private val httpClient = HttpClient(CIO) { - install(ContentNegotiation) { - json(Json { ignoreUnknownKeys = true }) - } - install(HttpTimeout) { - requestTimeoutMillis = 30_000 + private val httpClient = + HttpClient(CIO) { + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true }) + } + install(HttpTimeout) { + requestTimeoutMillis = 30_000 + } } - } suspend fun getAccessToken(): String { - val response: HttpResponse = httpClient.submitForm( - url = config.auth.authUrl, - formParameters = Parameters.build { - append("grant_type", "client_credentials") - append("client_id", "radar_rest_sources_auth_backend") - append("client_secret", "secret") - append("scope", "SUBJECT.READ PROJECT.READ") - append("audience", "res_ManagementPortal") - } - ) + val response: HttpResponse = + httpClient.submitForm( + url = config.auth.authUrl, + formParameters = + Parameters.build { + append("grant_type", "client_credentials") + append("client_id", "radar_rest_sources_auth_backend") + append("client_secret", "secret") + append("scope", "SUBJECT.READ PROJECT.READ") + append("audience", "res_ManagementPortal") + }, + ) if (!response.status.isSuccess()) { logger.error("Failed to acquire access token: ${response.status}") @@ -54,13 +57,17 @@ class MPClient { return tokenResponse.access_token } - suspend fun requestOrganizations(page: Int = 0, size: Int = Int.MAX_VALUE): List { + suspend fun requestOrganizations( + page: Int = 0, + size: Int = Int.MAX_VALUE, + ): List { val accessToken = getAccessToken() - val response: HttpResponse = httpClient.get("${config.auth.managementPortalUrl}/api/organizations") { - headers { - append(HttpHeaders.Authorization, "Bearer $accessToken") + val response: HttpResponse = + httpClient.get("${config.auth.managementPortalUrl}/api/organizations") { + headers { + append(HttpHeaders.Authorization, "Bearer $accessToken") + } } - } if (!response.status.isSuccess()) { logger.error("Failed to fetch projects: ${response.status}") @@ -70,13 +77,17 @@ class MPClient { return response.body() } - suspend fun requestProjects(page: Int = 0, size: Int = Int.MAX_VALUE): List { + suspend fun requestProjects( + page: Int = 0, + size: Int = Int.MAX_VALUE, + ): List { val accessToken = getAccessToken() - val response: HttpResponse = httpClient.get("${config.auth.managementPortalUrl}/api/projects") { - headers { - append(HttpHeaders.Authorization, "Bearer $accessToken") + val response: HttpResponse = + httpClient.get("${config.auth.managementPortalUrl}/api/projects") { + headers { + append(HttpHeaders.Authorization, "Bearer $accessToken") + } } - } if (!response.status.isSuccess()) { logger.error("Failed to fetch projects: ${response.status}") @@ -86,13 +97,18 @@ class MPClient { return response.body() } - suspend fun requestSubjects(projectId: String, page: Int = 0, size: Int = Int.MAX_VALUE,): List { + suspend fun requestSubjects( + projectId: String, + page: Int = 0, + size: Int = Int.MAX_VALUE, + ): List { val accessToken = getAccessToken() - val response: HttpResponse = httpClient.get("${config.auth.managementPortalUrl}/api/projects/${projectId}/subjects") { - headers { - append(HttpHeaders.Authorization, "Bearer $accessToken") + val response: HttpResponse = + httpClient.get("${config.auth.managementPortalUrl}/api/projects/$projectId/subjects") { + headers { + append(HttpHeaders.Authorization, "Bearer $accessToken") + } } - } if (!response.status.isSuccess()) { logger.error("Failed to fetch projects: ${response.status}") @@ -107,5 +123,5 @@ class MPClient { data class TokenResponse( val access_token: String, val token_type: String, - val expires_in: Long -) \ No newline at end of file + val expires_in: Long, +) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt index f1b295b3..05a53b1c 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt @@ -1,38 +1,34 @@ package org.radarbase.authorizer.service -import jakarta.inject.Singleton import io.ktor.http.HttpStatusCode -import org.radarbase.jersey.auth.AuthService +import org.radarbase.jersey.exception.HttpNotFoundException +import org.radarbase.jersey.service.ProjectService import org.radarbase.kotlin.coroutines.CacheConfig import org.radarbase.kotlin.coroutines.CachedMap +import org.radarbase.management.client.HttpStatusException +import org.radarbase.management.client.MPOrganization import org.radarbase.management.client.MPProject import org.radarbase.management.client.MPSubject -import org.radarbase.management.client.MPOrganization -import org.radarbase.management.client.HttpStatusException import org.slf4j.LoggerFactory import java.time.Duration -import kotlin.time.Duration.Companion.minutes -import kotlin.time.toKotlinDuration -import jakarta.ws.rs.core.Context -import org.radarbase.auth.authorization.EntityDetails -import org.radarbase.auth.authorization.Permission import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap -import org.radarbase.jersey.exception.HttpNotFoundException -import org.radarbase.jersey.service.ProjectService +import kotlin.time.Duration.Companion.minutes +import kotlin.time.toKotlinDuration -class RadarProjectService( -): ProjectService { +class RadarProjectService : ProjectService { private val mpClient: MPClient = MPClient() private val projects: CachedMap private val organizations: CachedMap - private val participants: ConcurrentMap> = ConcurrentHashMap() + private val participants: ConcurrentMap> = + ConcurrentHashMap() - private val projectCacheConfig = CacheConfig( - refreshDuration = Duration.ofMinutes(5).toKotlinDuration(), - retryDuration = RETRY_INTERVAL, - ) + private val projectCacheConfig = + CacheConfig( + refreshDuration = Duration.ofMinutes(5).toKotlinDuration(), + retryDuration = RETRY_INTERVAL, + ) init { val cacheConfig = @@ -51,83 +47,93 @@ class RadarProjectService( throw RuntimeException("Unable to fetch projects", e) } } - - organizations = CachedMap(cacheConfig) { - try { - mpClient.requestOrganizations() - .associateBy { it.id } - .also { logger.debug("Fetched organizations {}", it) } - } catch (ex: HttpStatusException) { - if (ex.code == HttpStatusCode.NotFound) { - logger.warn("Target ManagementPortal does not support organizations. Using default organization main.") - mapOf("main" to defaultOrganization) - } else { - throw ex + + organizations = + CachedMap(cacheConfig) { + try { + mpClient.requestOrganizations().associateBy { it.id }.also { + logger.debug("Fetched organizations {}", it) + } + } catch (ex: HttpStatusException) { + if (ex.code == HttpStatusCode.NotFound) { + logger.warn( + "Target ManagementPortal does not support organizations. Using default organization main.", + ) + mapOf("main" to defaultOrganization) + } else { + throw ex + } } } - } } - suspend fun getProjects(): List = projects.get().values.toList() - - suspend fun userProjects(permission: Permission): List { - return projects.get() - .values.toList() - // .filter { - // authService.hasPermission( - // permission, - // EntityDetails( - // organization = it.organization?.id, - // project = it.id, - // ), - // ) - // } - } + suspend fun userProjects(): List = projects.get().values.toList() - override suspend fun ensureSubject(projectId: String, userId: String) { + override suspend fun ensureSubject( + projectId: String, + userId: String, + ) { ensureProject(projectId) if (!projectUserCache(projectId).contains(userId)) { - throw HttpNotFoundException("user_not_found", "User $userId not found in project $projectId of ManagementPortal.") + throw HttpNotFoundException( + "user_not_found", + "User $userId not found in project $projectId of ManagementPortal.", + ) } } override suspend fun ensureOrganization(organizationId: String) { if (!organizations.contains(organizationId)) { - throw HttpNotFoundException("organization_not_found", "Organization $organizationId not found in Management Portal.") + throw HttpNotFoundException( + "organization_not_found", + "Organization $organizationId not found in Management Portal.", + ) } } override suspend fun ensureProject(projectId: String) { if (!projects.contains(projectId)) { - throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") + throw HttpNotFoundException( + "project_not_found", + "Project $projectId not found in Management Portal.", + ) } } override suspend fun projectOrganization(projectId: String): String = projects.get(projectId)?.organization?.id - ?: throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") + ?: throw HttpNotFoundException( + "project_not_found", + "Project $projectId not found in Management Portal.", + ) - suspend fun project(projectId: String): MPProject = projects.get(projectId) - ?: throw HttpNotFoundException("project_not_found", "Project $projectId not found in Management Portal.") + suspend fun project(projectId: String): MPProject = + projects.get(projectId) + ?: throw HttpNotFoundException( + "project_not_found", + "Project $projectId not found in Management Portal.", + ) - override suspend fun listProjects(organizationId: String): List = projects.get().asSequence() - .filter { it.value.organization?.id == organizationId } - .mapTo(ArrayList()) { it.key } + override suspend fun listProjects(organizationId: String): List = + projects + .get() + .asSequence() + .filter { it.value.organization?.id == organizationId } + .mapTo(ArrayList()) { it.key } - suspend fun projectSubjects(projectId: String): List { - logger.info("Fetching subjects for project $projectId") - logger.info(projectUserCache(projectId).get().values.toList().toString()) - return projectUserCache(projectId).get().values.toList() - } + suspend fun projectSubjects(projectId: String): List = projectUserCache(projectId).get().values.toList() - private suspend fun projectUserCache(projectId: String) = participants.computeIfAbsent(projectId) { - CachedMap(projectCacheConfig) { - mpClient.requestSubjects(projectId) - .associateBy { checkNotNull(it.id) } + private suspend fun projectUserCache(projectId: String) = + participants.computeIfAbsent(projectId) { + CachedMap(projectCacheConfig) { + mpClient.requestSubjects(projectId).associateBy { checkNotNull(it.id) } + } } - } - suspend fun subject(projectId: String, userId: String): MPSubject? { + suspend fun subject( + projectId: String, + userId: String, + ): MPSubject? { ensureProject(projectId) return projectUserCache(projectId).get(userId) } @@ -137,5 +143,4 @@ class RadarProjectService( private val logger = LoggerFactory.getLogger(ProjectService::class.java) private val defaultOrganization = MPOrganization("main") } - } From 7b73d61716961b77ee9a8b78e172108ee9ebfce7 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 26 Sep 2024 16:38:01 +0100 Subject: [PATCH 05/27] Fix lint errors --- .../authorizer/api/RestSourceUserMapper.kt | 8 ++-- .../ManagementPortalEnhancerFactory.kt | 18 ++++----- .../authorizer/resources/ProjectResource.kt | 6 ++- .../resources/RegistrationResource.kt | 20 +++++----- .../resources/RestSourceUserResource.kt | 10 ++--- .../radarbase/authorizer/service/MPClient.kt | 37 ++++++++++--------- 6 files changed, 53 insertions(+), 46 deletions(-) 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 396b7612..8b23ad7f 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 @@ -34,10 +34,10 @@ class RestSourceUserMapper { projectId = user.projectId, userId = user.userId, humanReadableUserId = - mpUser - ?.attributes - ?.get("Human-readable-identifier") - ?.takeIf { it.isNotBlank() && it != "null" }, + mpUser + ?.attributes + ?.get("Human-readable-identifier") + ?.takeIf { it.isNotBlank() && it != "null" }, externalId = mpUser?.externalId, sourceId = user.sourceId, isAuthorized = user.authorized, 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 b568ea14..b5710da0 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 @@ -34,11 +34,11 @@ class ManagementPortalEnhancerFactory( val authConfig = AuthConfig( managementPortal = - MPConfig( - url = config.auth.managementPortalUrl.trimEnd('/'), - syncProjectsIntervalMin = config.service.syncProjectsIntervalMin, - syncParticipantsIntervalMin = config.service.syncParticipantsIntervalMin, - ), + MPConfig( + url = config.auth.managementPortalUrl.trimEnd('/'), + syncProjectsIntervalMin = config.service.syncProjectsIntervalMin, + syncParticipantsIntervalMin = config.service.syncParticipantsIntervalMin, + ), jwtResourceName = config.auth.jwtResourceName, jwksUrls = config.auth.jwksUrls, ) @@ -46,10 +46,10 @@ class ManagementPortalEnhancerFactory( val dbConfig = config.database.copy( managedClasses = - listOf( - RestSourceUser::class.qualifiedName!!, - RegistrationState::class.qualifiedName!!, - ), + listOf( + RestSourceUser::class.qualifiedName!!, + RegistrationState::class.qualifiedName!!, + ), ) return listOf( Enhancers.radar(authConfig), 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 a7717a33..13697f3f 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,7 +18,11 @@ 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 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 733e39dc..675fdb11 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 @@ -72,11 +72,11 @@ class RegistrationResource( tokenState = tokenState.copy( authEndpointUrl = - authorizationService.getAuthorizationEndpointWithParams( - sourceType = user.sourceType, - userId = user.id!!, - state = tokenState.token, - ), + authorizationService.getAuthorizationEndpointWithParams( + sourceType = user.sourceType, + userId = user.id!!, + state = tokenState.token, + ), ) } Response @@ -145,11 +145,11 @@ class RegistrationResource( RegistrationResponse( token = registration.token, authEndpointUrl = - authorizationService.getAuthorizationEndpointWithParams( - sourceType = registration.user.sourceType, - userId = registration.user.id!!, - state = registration.token, - ), + authorizationService.getAuthorizationEndpointWithParams( + sourceType = registration.user.sourceType, + userId = registration.user.id!!, + state = registration.token, + ), userId = registration.user.id!!.toString(), project = project, createdAt = registration.createdAt, 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 cd98ff84..7d485b4b 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 @@ -238,11 +238,11 @@ class RestSourceUserResource( ) = RestSourceUsers( users = listOf(), metadata = - Page( - pageNumber = pageNumber, - pageSize = pageSize, - totalElements = 0, - ), + Page( + pageNumber = pageNumber, + pageSize = pageSize, + totalElements = 0, + ), ) } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt index 19ed7bb8..35316584 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt @@ -1,15 +1,18 @@ package org.radarbase.authorizer.service -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.request.* -import io.ktor.client.request.forms.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.http.HttpHeaders +import io.ktor.http.Parameters +import io.ktor.http.headers +import io.ktor.http.isSuccess +import io.ktor.serialization.kotlinx.json.json import jakarta.inject.Singleton import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -39,13 +42,13 @@ class MPClient { httpClient.submitForm( url = config.auth.authUrl, formParameters = - Parameters.build { - append("grant_type", "client_credentials") - append("client_id", "radar_rest_sources_auth_backend") - append("client_secret", "secret") - append("scope", "SUBJECT.READ PROJECT.READ") - append("audience", "res_ManagementPortal") - }, + Parameters.build { + append("grant_type", "client_credentials") + append("client_id", config.auth.clientId) + append("client_secret", config.auth.clientSecret!!) + append("scope", "SUBJECT.READ PROJECT.READ") + append("audience", "res_ManagementPortal") + }, ) if (!response.status.isSuccess()) { From 2b78f900b9340f7a2a08daaab3ab441220a9ec05 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 1 Oct 2024 00:24:34 +0100 Subject: [PATCH 06/27] Fix MPClient missing imports --- .../src/main/java/org/radarbase/authorizer/service/MPClient.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt index 35316584..bd824a9d 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt @@ -7,6 +7,8 @@ import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.forms.submitForm import io.ktor.client.request.get +import io.ktor.client.request.url +import io.ktor.client.request.headers import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpHeaders import io.ktor.http.Parameters From 4edcf3a2a414e2eb0a668d42dbba3594c9bf4c6b Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 1 Oct 2024 00:30:04 +0100 Subject: [PATCH 07/27] Fix lint errors --- .../src/main/java/org/radarbase/authorizer/service/MPClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt index bd824a9d..76adb724 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt @@ -7,8 +7,8 @@ import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.forms.submitForm import io.ktor.client.request.get -import io.ktor.client.request.url import io.ktor.client.request.headers +import io.ktor.client.request.url import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpHeaders import io.ktor.http.Parameters From 139abe218c269244d361f16878e2da9d3d643158 Mon Sep 17 00:00:00 2001 From: Pauline Date: Sun, 13 Oct 2024 03:28:59 +0800 Subject: [PATCH 08/27] Fix MPClient auth config imports --- .../authorizer/api/RestSourceUserMapper.kt | 8 ++++++-- .../org/radarbase/authorizer/config/AuthConfig.kt | 6 +++--- .../authorizer/resources/ProjectResource.kt | 4 +++- .../authorizer/resources/RegistrationResource.kt | 4 +++- .../resources/RestSourceUserResource.kt | 4 +++- .../org/radarbase/authorizer/service/MPClient.kt | 15 ++++++++------- .../authorizer/service/RadarProjectService.kt | 8 ++++++-- 7 files changed, 32 insertions(+), 17 deletions(-) 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 8b23ad7f..d5e6e974 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 @@ -16,12 +16,16 @@ package org.radarbase.authorizer.api +import jakarta.ws.rs.core.Context +import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.entity.RestSourceUser import org.radarbase.authorizer.service.RadarProjectService import org.radarbase.kotlin.coroutines.forkJoin -class RestSourceUserMapper { - private val projectService: RadarProjectService = RadarProjectService() +class RestSourceUserMapper( + @Context private val config: AuthorizerConfig, +) { + private val projectService: RadarProjectService = RadarProjectService(config) suspend fun fromEntity(user: RestSourceUser): RestSourceUserDTO { val mpUser = diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt index 7669f38b..59798505 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt @@ -1,10 +1,10 @@ package org.radarbase.authorizer.config data class AuthConfig( - val managementPortalUrl: String = "http://host.docker.internal:8080/managementportal", - val authUrl: String = "http://host.docker.internal:4444/oauth2/token", + val managementPortalUrl: String = "http://managementportal-app:8080/managementportal", + val authUrl: String = "", val clientId: String = "radar_rest_sources_auth_backend", - val clientSecret: String? = "secret", + val clientSecret: String? = "", val jwtECPublicKeys: List? = null, val jwtRSAPublicKeys: List? = null, val jwtIssuer: String? = null, 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 13697f3f..75a8c90c 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 @@ -34,6 +34,7 @@ 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.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.service.RadarProjectService import org.radarbase.jersey.auth.AuthService import org.radarbase.jersey.auth.Authenticated @@ -50,8 +51,9 @@ import org.radarbase.jersey.service.AsyncCoroutineService class ProjectResource( @Context private val asyncService: AsyncCoroutineService, @Context private val authService: AuthService, + @Context private val config: AuthorizerConfig, ) { - private val projectService: RadarProjectService = RadarProjectService() + private val projectService: RadarProjectService = RadarProjectService(config) @GET @NeedsPermission(Permission.PROJECT_READ) 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 675fdb11..a8f7bbf5 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 @@ -21,6 +21,7 @@ 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.config.AuthorizerConfig import org.radarbase.authorizer.doa.RegistrationRepository import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.service.RadarProjectService @@ -49,8 +50,9 @@ class RegistrationResource( @Context private val registrationService: RegistrationService, @Context private val asyncService: AsyncCoroutineService, @Context private val authService: AuthService, + @Context private val config: AuthorizerConfig, ) { - private val projectService: RadarProjectService = RadarProjectService() + private val projectService: RadarProjectService = RadarProjectService(config) @POST @Authenticated 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 7d485b4b..b12cf598 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 @@ -40,6 +40,7 @@ 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.config.AuthorizerConfig import org.radarbase.authorizer.doa.RestSourceUserRepository import org.radarbase.authorizer.service.RadarProjectService import org.radarbase.authorizer.service.RestSourceAuthorizationService @@ -67,8 +68,9 @@ class RestSourceUserResource( @Context private val userService: RestSourceUserService, @Context private val asyncService: AsyncCoroutineService, @Context private val authService: AuthService, + @Context private val config: AuthorizerConfig, ) { - private val projectService: RadarProjectService = RadarProjectService() + private val projectService: RadarProjectService = RadarProjectService(config) @GET @NeedsPermission(Permission.SUBJECT_READ) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt index 76adb724..7e77b9d0 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt @@ -16,6 +16,7 @@ import io.ktor.http.headers import io.ktor.http.isSuccess import io.ktor.serialization.kotlinx.json.json import jakarta.inject.Singleton +import jakarta.ws.rs.core.Context import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import org.radarbase.authorizer.config.AuthorizerConfig @@ -25,10 +26,10 @@ import org.radarbase.management.client.MPSubject import org.slf4j.LoggerFactory @Singleton -class MPClient { - private val config: AuthorizerConfig = AuthorizerConfig() +class MPClient( + @Context private val config: AuthorizerConfig, +) { private val logger = LoggerFactory.getLogger(MPClient::class.java) - private val httpClient = HttpClient(CIO) { install(ContentNegotiation) { @@ -75,8 +76,8 @@ class MPClient { } if (!response.status.isSuccess()) { - logger.error("Failed to fetch projects: ${response.status}") - throw RuntimeException("Failed to fetch projects") + logger.error("Failed to fetch organizations: ${response.status}") + throw RuntimeException("Failed to fetch organizations") } return response.body() @@ -116,8 +117,8 @@ class MPClient { } if (!response.status.isSuccess()) { - logger.error("Failed to fetch projects: ${response.status}") - throw RuntimeException("Failed to fetch projects") + logger.error("Failed to fetch subjects: ${response.status}") + throw RuntimeException("Failed to fetch subjects") } return response.body() diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt index 05a53b1c..640f3461 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt @@ -1,6 +1,8 @@ package org.radarbase.authorizer.service import io.ktor.http.HttpStatusCode +import jakarta.ws.rs.core.Context +import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.jersey.exception.HttpNotFoundException import org.radarbase.jersey.service.ProjectService import org.radarbase.kotlin.coroutines.CacheConfig @@ -16,8 +18,10 @@ import java.util.concurrent.ConcurrentMap import kotlin.time.Duration.Companion.minutes import kotlin.time.toKotlinDuration -class RadarProjectService : ProjectService { - private val mpClient: MPClient = MPClient() +class RadarProjectService( + @Context private val config: AuthorizerConfig, +) : ProjectService { + private val mpClient: MPClient = MPClient(config) private val projects: CachedMap private val organizations: CachedMap From 207e6bbeda503a28803064bd54234ebe6c1b6698 Mon Sep 17 00:00:00 2001 From: Pauline Date: Sun, 13 Oct 2024 04:04:19 +0800 Subject: [PATCH 09/27] Update user creation endpoint to return user if it exists --- .../doa/RestSourceUserRepository.kt | 1 + .../doa/RestSourceUserRepositoryImpl.kt | 49 +++++++++++++------ .../service/RestSourceUserService.kt | 15 ++++++ 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt index d9513cf5..2d7182fe 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 @@ -39,4 +39,5 @@ interface RestSourceUserRepository { suspend fun delete(user: RestSourceUser) suspend fun reset(user: RestSourceUser, startDate: Instant, endDate: Instant?): RestSourceUser suspend fun findByExternalId(externalId: String, sourceType: String): RestSourceUser? + suspend fun findByUserIdProjectIdSourceType(userId: String, projectId: 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 be296c71..d11d2397 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 @@ -23,7 +23,6 @@ import org.radarbase.authorizer.api.Page import org.radarbase.authorizer.api.RestOauth2AccessToken import org.radarbase.authorizer.api.RestSourceUserDTO import org.radarbase.authorizer.doa.entity.RestSourceUser -import org.radarbase.jersey.exception.HttpConflictException import org.radarbase.jersey.exception.HttpNotFoundException import org.radarbase.jersey.hibernate.HibernateRepository import org.radarbase.jersey.service.AsyncCoroutineService @@ -52,19 +51,20 @@ class RestSourceUserRepositoryImpl( .resultList.firstOrNull() if (existingUser != null) { - throw HttpConflictException("user_exists", "User ${user.userId} already exists.") - } - RestSourceUser( - projectId = user.projectId, - userId = user.userId, - sourceId = user.sourceId ?: UUID.randomUUID().toString(), - sourceType = user.sourceType, - createdAt = Instant.now(), - version = Instant.now().toString(), - startDate = user.startDate, - endDate = user.endDate, - ).also { - persist(it) + existingUser + } else { + RestSourceUser( + projectId = user.projectId, + userId = user.userId, + sourceId = user.sourceId ?: UUID.randomUUID().toString(), + sourceType = user.sourceType, + createdAt = Instant.now(), + version = Instant.now().toString(), + startDate = user.startDate, + endDate = user.endDate, + ).also { + persist(it) + } } } @@ -211,6 +211,27 @@ class RestSourceUserRepositoryImpl( return if (result.isEmpty()) null else result[0] } + override suspend fun findByUserIdProjectIdSourceType( + userId: String, + projectId: String, + sourceType: String, + ): RestSourceUser? = transact { + createQuery( + """ + SELECT u + FROM RestSourceUser u + WHERE u.userId = :userId + AND u.projectId = :projectId + AND u.sourceType = :sourceType + """.trimIndent(), + RestSourceUser::class.java, + ).apply { + setParameter("userId", userId) + setParameter("projectId", projectId) + setParameter("sourceType", sourceType) + }.resultList.firstOrNull() + } + override suspend fun delete(user: RestSourceUser) = transact { remove(merge(user)) } 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 c48da663..00ff4fb4 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 @@ -1,5 +1,6 @@ package org.radarbase.authorizer.service +import jakarta.ws.rs.WebApplicationException import jakarta.ws.rs.core.Context import jakarta.ws.rs.core.Response import org.radarbase.auth.authorization.EntityDetails @@ -39,6 +40,20 @@ class RestSourceUserService( suspend fun create(userDto: RestSourceUserDTO): RestSourceUserDTO { userDto.ensure() + val existingUserDto = userMapper.fromEntity( + userRepository.findByUserIdProjectIdSourceType( + userId = userDto.userId!!, + projectId = userDto.projectId!!, + sourceType = userDto.sourceType, + )!!, + ) + if (existingUserDto != null) { + val response = Response.status(Response.Status.CONFLICT) + .entity(mapOf("status" to 409, "message" to "User already exists.", "user" to existingUserDto)) + .build() + + throw WebApplicationException(response) + } val user = userRepository.create(userDto) return userMapper.fromEntity(user) } From bcffef7e2d0e5b163cdea98c47718d3d6eee2c5c Mon Sep 17 00:00:00 2001 From: Pauline Date: Sun, 13 Oct 2024 04:19:15 +0800 Subject: [PATCH 10/27] Fix RestSourceUserService --- .../authorizer/service/RestSourceUserService.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 00ff4fb4..abdde814 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 @@ -40,16 +40,14 @@ class RestSourceUserService( suspend fun create(userDto: RestSourceUserDTO): RestSourceUserDTO { userDto.ensure() - val existingUserDto = userMapper.fromEntity( - userRepository.findByUserIdProjectIdSourceType( - userId = userDto.userId!!, - projectId = userDto.projectId!!, - sourceType = userDto.sourceType, - )!!, + val existingUser = userRepository.findByUserIdProjectIdSourceType( + userId = userDto.userId!!, + projectId = userDto.projectId!!, + sourceType = userDto.sourceType, ) - if (existingUserDto != null) { + if (existingUser != null) { val response = Response.status(Response.Status.CONFLICT) - .entity(mapOf("status" to 409, "message" to "User already exists.", "user" to existingUserDto)) + .entity(mapOf("status" to 409, "message" to "User already exists.", "user" to userMapper.fromEntity(existingUser))) .build() throw WebApplicationException(response) From 19e96f9f1053ce76c131ebe7d756d5688792cd43 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 14 Oct 2024 15:56:23 +0800 Subject: [PATCH 11/27] Fix creating rest source user permission --- .../radarbase/authorizer/resources/RestSourceUserResource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b12cf598..d3d6d630 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 @@ -157,7 +157,7 @@ class RestSourceUserResource( } @POST - @NeedsPermission(Permission.SUBJECT_CREATE) + @NeedsPermission(Permission.SUBJECT_UPDATE) fun create( userDto: RestSourceUserDTO, @Suspended asyncResponse: AsyncResponse, From ff21a25d3a827f4534d922338921214793345bb1 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 14 Oct 2024 15:57:39 +0800 Subject: [PATCH 12/27] Cache access token in MPClient --- .../radarbase/authorizer/service/MPClient.kt | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt index 7e77b9d0..c6de7057 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt @@ -17,6 +17,8 @@ import io.ktor.http.isSuccess import io.ktor.serialization.kotlinx.json.json import jakarta.inject.Singleton import jakarta.ws.rs.core.Context +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import org.radarbase.authorizer.config.AuthorizerConfig @@ -40,7 +42,12 @@ class MPClient( } } - suspend fun getAccessToken(): String { + private var accessToken: String? = null + private var tokenExpiration: Long = 0 + + private val mutex = Mutex() + + private suspend fun fetchAccessToken(): String { val response: HttpResponse = httpClient.submitForm( url = config.auth.authUrl, @@ -60,7 +67,20 @@ class MPClient( } val tokenResponse = response.body() - return tokenResponse.access_token + accessToken = tokenResponse.access_token + tokenExpiration = System.currentTimeMillis() + (tokenResponse.expires_in * 1000) // Convert seconds to milliseconds + + return accessToken!! + } + + private suspend fun getAccessToken(): String { + mutex.withLock { + return if (accessToken == null || System.currentTimeMillis() >= tokenExpiration) { + fetchAccessToken() + } else { + accessToken!! + } + } } suspend fun requestOrganizations( From f773bd6da95bedc21116b47b0381bd72e17dac86 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 16 Oct 2024 12:55:05 +0800 Subject: [PATCH 13/27] Update default config --- .../main/java/org/radarbase/authorizer/config/AuthConfig.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt index 59798505..20636f7c 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt @@ -2,12 +2,12 @@ package org.radarbase.authorizer.config data class AuthConfig( val managementPortalUrl: String = "http://managementportal-app:8080/managementportal", - val authUrl: String = "", + val authUrl: String = "http://hydra-public:4444/oauth2/token", val clientId: String = "radar_rest_sources_auth_backend", val clientSecret: String? = "", val jwtECPublicKeys: List? = null, val jwtRSAPublicKeys: List? = null, val jwtIssuer: String? = null, val jwtResourceName: String = "res_restAuthorizer", - val jwksUrls: List = listOf("http://host.docker.internal:4445/admin/keys/hydra.jwt.access-token"), + val jwksUrls: List = listOf("http://hydra-admin:4445/admin/keys/hydra.jwt.access-token"), ) From c6f2ba4db6a0b8fad805134468746bd445c123d0 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 27 Nov 2024 19:31:23 +0000 Subject: [PATCH 14/27] Inject ProjectService instead of constructing --- .../org/radarbase/authorizer/api/RestSourceUserMapper.kt | 2 +- .../authorizer/enhancer/AuthorizerResourceEnhancer.kt | 9 ++++++++- .../radarbase/authorizer/resources/ProjectResource.kt | 2 +- .../authorizer/resources/RegistrationResource.kt | 2 +- .../authorizer/resources/RestSourceUserResource.kt | 2 +- .../radarbase/authorizer/service/RadarProjectService.kt | 2 +- 6 files changed, 13 insertions(+), 6 deletions(-) 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 d5e6e974..3bd3deed 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/RestSourceUserMapper.kt @@ -24,8 +24,8 @@ import org.radarbase.kotlin.coroutines.forkJoin class RestSourceUserMapper( @Context private val config: AuthorizerConfig, + @Context private val projectService: RadarProjectService ) { - private val projectService: RadarProjectService = RadarProjectService(config) suspend fun fromEntity(user: RestSourceUser): RestSourceUserDTO { val mpUser = 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 fac81767..428d449a 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 @@ -30,6 +30,7 @@ import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService. 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.MPClient import org.radarbase.authorizer.service.OAuth2RestSourceAuthorizationService import org.radarbase.authorizer.service.OuraAuthorizationService import org.radarbase.authorizer.service.RadarProjectService @@ -54,6 +55,8 @@ class AuthorizerResourceEnhancer( }, ) + private val mpClient = MPClient(config) + override val classes: Array> get() = listOfNotNull( @@ -73,6 +76,9 @@ class AuthorizerResourceEnhancer( bind(config) .to(AuthorizerConfig::class.java) + bind(mpClient) + .to(MPClient::class.java) + bind(restSourceClients) .to(RestSourceClients::class.java) @@ -123,7 +129,8 @@ class AuthorizerResourceEnhancer( .`in`(Singleton::class.java) bind(RadarProjectService::class.java) - .to(ProjectService::class.java) + .to(ProjectService::class.java) // For injecting as ProjectService + .to(RadarProjectService::class.java) // For injecting as RadarProjectService .`in`(Singleton::class.java) } } diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt index 75a8c90c..18ca7907 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 @@ -52,8 +52,8 @@ class ProjectResource( @Context private val asyncService: AsyncCoroutineService, @Context private val authService: AuthService, @Context private val config: AuthorizerConfig, + @Context private val projectService: RadarProjectService ) { - private val projectService: RadarProjectService = RadarProjectService(config) @GET @NeedsPermission(Permission.PROJECT_READ) 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 a8f7bbf5..82524bbf 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 @@ -51,8 +51,8 @@ class RegistrationResource( @Context private val asyncService: AsyncCoroutineService, @Context private val authService: AuthService, @Context private val config: AuthorizerConfig, + @Context private val projectService: RadarProjectService ) { - private val projectService: RadarProjectService = RadarProjectService(config) @POST @Authenticated 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 d3d6d630..ee044dc5 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 @@ -69,8 +69,8 @@ class RestSourceUserResource( @Context private val asyncService: AsyncCoroutineService, @Context private val authService: AuthService, @Context private val config: AuthorizerConfig, + @Context private val projectService: RadarProjectService ) { - private val projectService: RadarProjectService = RadarProjectService(config) @GET @NeedsPermission(Permission.SUBJECT_READ) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt index 640f3461..59b8d8e8 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt @@ -20,8 +20,8 @@ import kotlin.time.toKotlinDuration class RadarProjectService( @Context private val config: AuthorizerConfig, + @Context private val mpClient: MPClient, ) : ProjectService { - private val mpClient: MPClient = MPClient(config) private val projects: CachedMap private val organizations: CachedMap From bd177efe9d6b4d5b2a35a610c6e8513811a1fb1d Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 27 Nov 2024 19:32:22 +0000 Subject: [PATCH 15/27] Fix MPClient --- .../radarbase/authorizer/service/MPClient.kt | 71 +++++++++---------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt index c6de7057..1ce7d35f 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt @@ -16,9 +16,8 @@ import io.ktor.http.headers import io.ktor.http.isSuccess import io.ktor.serialization.kotlinx.json.json import jakarta.inject.Singleton -import jakarta.ws.rs.core.Context -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import org.radarbase.authorizer.config.AuthorizerConfig @@ -26,10 +25,11 @@ import org.radarbase.management.client.MPOrganization import org.radarbase.management.client.MPProject import org.radarbase.management.client.MPSubject import org.slf4j.LoggerFactory +import kotlin.time.Duration.Companion.seconds @Singleton class MPClient( - @Context private val config: AuthorizerConfig, + private val config: AuthorizerConfig, ) { private val logger = LoggerFactory.getLogger(MPClient::class.java) private val httpClient = @@ -38,49 +38,48 @@ class MPClient( json(Json { ignoreUnknownKeys = true }) } install(HttpTimeout) { - requestTimeoutMillis = 30_000 + val millis = 30.seconds.inWholeMilliseconds + connectTimeoutMillis = millis + socketTimeoutMillis = millis + requestTimeoutMillis = millis } } - private var accessToken: String? = null + private var accessToken: String = "" private var tokenExpiration: Long = 0 - private val mutex = Mutex() + private suspend fun fetchAccessToken(): String = + withContext(Dispatchers.IO) { + val response: HttpResponse = + httpClient.submitForm( + url = config.auth.authUrl, + formParameters = + Parameters.build { + append("grant_type", "client_credentials") + append("client_id", config.auth.clientId) + append("client_secret", config.auth.clientSecret!!) + append("scope", "SUBJECT.READ PROJECT.READ") + append("audience", "res_ManagementPortal") + }, + ) + + if (!response.status.isSuccess()) { + logger.error("Failed to acquire access token: ${response.status}") + throw RuntimeException("Unable to retrieve access token") + } - private suspend fun fetchAccessToken(): String { - val response: HttpResponse = - httpClient.submitForm( - url = config.auth.authUrl, - formParameters = - Parameters.build { - append("grant_type", "client_credentials") - append("client_id", config.auth.clientId) - append("client_secret", config.auth.clientSecret!!) - append("scope", "SUBJECT.READ PROJECT.READ") - append("audience", "res_ManagementPortal") - }, - ) + val tokenResponse = response.body() + accessToken = tokenResponse.access_token + tokenExpiration = System.currentTimeMillis() + (tokenResponse.expires_in * 1000) // Convert seconds to milliseconds - if (!response.status.isSuccess()) { - logger.error("Failed to acquire access token: ${response.status}") - throw RuntimeException("Unable to retrieve access token") + accessToken } - val tokenResponse = response.body() - accessToken = tokenResponse.access_token - tokenExpiration = System.currentTimeMillis() + (tokenResponse.expires_in * 1000) // Convert seconds to milliseconds - - return accessToken!! - } - private suspend fun getAccessToken(): String { - mutex.withLock { - return if (accessToken == null || System.currentTimeMillis() >= tokenExpiration) { - fetchAccessToken() - } else { - accessToken!! - } + if (accessToken.isEmpty() || System.currentTimeMillis() >= tokenExpiration) { + this.accessToken = fetchAccessToken() } + return this.accessToken } suspend fun requestOrganizations( From fd19851ed39398e810ccfe476ff2074e812510e7 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 27 Nov 2024 19:32:35 +0000 Subject: [PATCH 16/27] Update AuthConfig --- .../src/main/java/org/radarbase/authorizer/config/AuthConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt index 20636f7c..cd5913d8 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthConfig.kt @@ -9,5 +9,5 @@ data class AuthConfig( val jwtRSAPublicKeys: List? = null, val jwtIssuer: String? = null, val jwtResourceName: String = "res_restAuthorizer", - val jwksUrls: List = listOf("http://hydra-admin:4445/admin/keys/hydra.jwt.access-token"), + val jwksUrls: List = listOf("http://hydra-public:4444/.well-known/jwks.json"), ) From 65fd6d4a00f3c57f86bed4dea4a4b32b7701bd83 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 28 Nov 2024 00:52:30 +0000 Subject: [PATCH 17/27] Reuse radar-jersey client and projectservice but override for hydra auth --- .../authorizer/api/RestSourceUserMapper.kt | 2 +- .../enhancer/AuthorizerResourceEnhancer.kt | 11 +- .../ManagementPortalEnhancerFactory.kt | 6 +- .../ManagementPortalResourceEnhancer.kt | 53 ++++++ .../authorizer/resources/ProjectResource.kt | 2 +- .../resources/RegistrationResource.kt | 2 +- .../resources/RestSourceUserResource.kt | 2 +- .../radarbase/authorizer/service/MPClient.kt | 152 ------------------ .../authorizer/service/MPClientFactory.kt | 47 ++++++ .../authorizer/service/RadarProjectService.kt | 150 ----------------- 10 files changed, 110 insertions(+), 317 deletions(-) create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt create mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt delete mode 100644 authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt 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 3bd3deed..e91a3f34 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,7 +19,7 @@ package org.radarbase.authorizer.api import jakarta.ws.rs.core.Context import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.entity.RestSourceUser -import org.radarbase.authorizer.service.RadarProjectService +import org.radarbase.jersey.service.managementportal.RadarProjectService import org.radarbase.kotlin.coroutines.forkJoin class RestSourceUserMapper( 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 428d449a..a496708e 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 @@ -30,10 +30,9 @@ import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService. 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.MPClient import org.radarbase.authorizer.service.OAuth2RestSourceAuthorizationService import org.radarbase.authorizer.service.OuraAuthorizationService -import org.radarbase.authorizer.service.RadarProjectService +import org.radarbase.jersey.service.managementportal.RadarProjectService import org.radarbase.authorizer.service.RegistrationService import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceClientService @@ -76,9 +75,6 @@ class AuthorizerResourceEnhancer( bind(config) .to(AuthorizerConfig::class.java) - bind(mpClient) - .to(MPClient::class.java) - bind(restSourceClients) .to(RestSourceClients::class.java) @@ -127,10 +123,5 @@ class AuthorizerResourceEnhancer( .to(RestSourceAuthorizationService::class.java) .named(OURA_AUTH) .`in`(Singleton::class.java) - - bind(RadarProjectService::class.java) - .to(ProjectService::class.java) // For injecting as ProjectService - .to(RadarProjectService::class.java) // For injecting as RadarProjectService - .`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 b5710da0..a7db63bd 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 @@ -16,6 +16,7 @@ package org.radarbase.authorizer.enhancer +import jakarta.inject.Singleton import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.entity.RegistrationState import org.radarbase.authorizer.doa.entity.RestSourceUser @@ -25,6 +26,7 @@ import org.radarbase.jersey.enhancer.EnhancerFactory import org.radarbase.jersey.enhancer.Enhancers import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.hibernate.config.HibernateResourceEnhancer +import org.radarbase.management.client.MPClient /** This binder needs to register all non-Jersey classes, otherwise initialization fails. */ class ManagementPortalEnhancerFactory( @@ -36,6 +38,8 @@ class ManagementPortalEnhancerFactory( managementPortal = MPConfig( url = config.auth.managementPortalUrl.trimEnd('/'), + clientId = config.auth.clientId, + clientSecret = config.auth.clientSecret, syncProjectsIntervalMin = config.service.syncProjectsIntervalMin, syncParticipantsIntervalMin = config.service.syncParticipantsIntervalMin, ), @@ -55,7 +59,7 @@ class ManagementPortalEnhancerFactory( Enhancers.radar(authConfig), Enhancers.health, HibernateResourceEnhancer(dbConfig), - Enhancers.managementPortal(authConfig), + ManagementPortalResourceEnhancer(authConfig), Enhancers.ecdsa, JedisResourceEnhancer(), Enhancers.exception, diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt new file mode 100644 index 00000000..373763a5 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt @@ -0,0 +1,53 @@ +package org.radarbase.authorizer.enhancer + +import jakarta.inject.Singleton +import org.glassfish.jersey.internal.inject.AbstractBinder +import org.radarbase.auth.authentication.TokenValidator +import org.radarbase.auth.authorization.AuthorizationOracle +import org.radarbase.jersey.auth.AuthConfig +import org.radarbase.jersey.auth.AuthValidator +import org.radarbase.jersey.auth.jwt.AuthorizationOracleFactory +import org.radarbase.jersey.auth.managementportal.ManagementPortalTokenValidator +import org.radarbase.jersey.auth.jwt.TokenValidatorFactory +import org.radarbase.jersey.enhancer.JerseyResourceEnhancer +import org.radarbase.jersey.service.ProjectService +import org.radarbase.jersey.service.managementportal.MPProjectService +import org.radarbase.jersey.service.managementportal.ProjectServiceWrapper +import org.radarbase.jersey.service.managementportal.RadarProjectService +import org.radarbase.management.client.MPClient +import org.radarbase.authorizer.service.MPClientFactory + +class ManagementPortalResourceEnhancer(private val config: AuthConfig) : JerseyResourceEnhancer { + override fun AbstractBinder.enhance() { + val config = config.withEnv() + + // Bind other necessary services + bindFactory(TokenValidatorFactory::class.java) + .to(TokenValidator::class.java) + .`in`(Singleton::class.java) + + bind(ManagementPortalTokenValidator::class.java) + .to(AuthValidator::class.java) + .`in`(Singleton::class.java) + + bindFactory(AuthorizationOracleFactory::class.java) + .to(AuthorizationOracle::class.java) + .`in`(Singleton::class.java) + + // If managementPortal.clientId is available, bind your custom MPClient + if (config.managementPortal.clientId != null) { + bindFactory(MPClientFactory::class.java) // Bind your custom MPClientFactory + .to(MPClient::class.java) + .`in`(Singleton::class.java) + + // Bind other services related to ProjectService + bind(ProjectServiceWrapper::class.java) + .to(ProjectService::class.java) + .`in`(Singleton::class.java) + + bind(MPProjectService::class.java) + .to(RadarProjectService::class.java) + .`in`(Singleton::class.java) + } + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/ProjectResource.kt index 18ca7907..02276b10 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 @@ -35,7 +35,7 @@ import org.radarbase.authorizer.api.UserList import org.radarbase.authorizer.api.toProject import org.radarbase.authorizer.api.toUser import org.radarbase.authorizer.config.AuthorizerConfig -import org.radarbase.authorizer.service.RadarProjectService +import org.radarbase.jersey.service.managementportal.RadarProjectService import org.radarbase.jersey.auth.AuthService import org.radarbase.jersey.auth.Authenticated import org.radarbase.jersey.auth.NeedsPermission diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/resources/RegistrationResource.kt index 82524bbf..bc909a97 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 @@ -24,7 +24,7 @@ import org.radarbase.authorizer.api.toProject import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.RegistrationRepository import org.radarbase.authorizer.doa.RestSourceUserRepository -import org.radarbase.authorizer.service.RadarProjectService +import org.radarbase.jersey.service.managementportal.RadarProjectService import org.radarbase.authorizer.service.RegistrationService import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceUserService 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 ee044dc5..bbdcabc3 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 @@ -42,7 +42,7 @@ import org.radarbase.authorizer.api.RestSourceUsers import org.radarbase.authorizer.api.SignRequestParams import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.RestSourceUserRepository -import org.radarbase.authorizer.service.RadarProjectService +import org.radarbase.jersey.service.managementportal.RadarProjectService import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceClientService import org.radarbase.authorizer.service.RestSourceUserService diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt deleted file mode 100644 index 1ce7d35f..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClient.kt +++ /dev/null @@ -1,152 +0,0 @@ -package org.radarbase.authorizer.service - -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.engine.cio.CIO -import io.ktor.client.plugins.HttpTimeout -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.request.forms.submitForm -import io.ktor.client.request.get -import io.ktor.client.request.headers -import io.ktor.client.request.url -import io.ktor.client.statement.HttpResponse -import io.ktor.http.HttpHeaders -import io.ktor.http.Parameters -import io.ktor.http.headers -import io.ktor.http.isSuccess -import io.ktor.serialization.kotlinx.json.json -import jakarta.inject.Singleton -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import org.radarbase.authorizer.config.AuthorizerConfig -import org.radarbase.management.client.MPOrganization -import org.radarbase.management.client.MPProject -import org.radarbase.management.client.MPSubject -import org.slf4j.LoggerFactory -import kotlin.time.Duration.Companion.seconds - -@Singleton -class MPClient( - private val config: AuthorizerConfig, -) { - private val logger = LoggerFactory.getLogger(MPClient::class.java) - private val httpClient = - HttpClient(CIO) { - install(ContentNegotiation) { - json(Json { ignoreUnknownKeys = true }) - } - install(HttpTimeout) { - val millis = 30.seconds.inWholeMilliseconds - connectTimeoutMillis = millis - socketTimeoutMillis = millis - requestTimeoutMillis = millis - } - } - - private var accessToken: String = "" - private var tokenExpiration: Long = 0 - - private suspend fun fetchAccessToken(): String = - withContext(Dispatchers.IO) { - val response: HttpResponse = - httpClient.submitForm( - url = config.auth.authUrl, - formParameters = - Parameters.build { - append("grant_type", "client_credentials") - append("client_id", config.auth.clientId) - append("client_secret", config.auth.clientSecret!!) - append("scope", "SUBJECT.READ PROJECT.READ") - append("audience", "res_ManagementPortal") - }, - ) - - if (!response.status.isSuccess()) { - logger.error("Failed to acquire access token: ${response.status}") - throw RuntimeException("Unable to retrieve access token") - } - - val tokenResponse = response.body() - accessToken = tokenResponse.access_token - tokenExpiration = System.currentTimeMillis() + (tokenResponse.expires_in * 1000) // Convert seconds to milliseconds - - accessToken - } - - private suspend fun getAccessToken(): String { - if (accessToken.isEmpty() || System.currentTimeMillis() >= tokenExpiration) { - this.accessToken = fetchAccessToken() - } - return this.accessToken - } - - suspend fun requestOrganizations( - page: Int = 0, - size: Int = Int.MAX_VALUE, - ): List { - val accessToken = getAccessToken() - val response: HttpResponse = - httpClient.get("${config.auth.managementPortalUrl}/api/organizations") { - headers { - append(HttpHeaders.Authorization, "Bearer $accessToken") - } - } - - if (!response.status.isSuccess()) { - logger.error("Failed to fetch organizations: ${response.status}") - throw RuntimeException("Failed to fetch organizations") - } - - return response.body() - } - - suspend fun requestProjects( - page: Int = 0, - size: Int = Int.MAX_VALUE, - ): List { - val accessToken = getAccessToken() - val response: HttpResponse = - httpClient.get("${config.auth.managementPortalUrl}/api/projects") { - headers { - append(HttpHeaders.Authorization, "Bearer $accessToken") - } - } - - if (!response.status.isSuccess()) { - logger.error("Failed to fetch projects: ${response.status}") - throw RuntimeException("Failed to fetch projects") - } - - return response.body() - } - - suspend fun requestSubjects( - projectId: String, - page: Int = 0, - size: Int = Int.MAX_VALUE, - ): List { - val accessToken = getAccessToken() - val response: HttpResponse = - httpClient.get("${config.auth.managementPortalUrl}/api/projects/$projectId/subjects") { - headers { - append(HttpHeaders.Authorization, "Bearer $accessToken") - } - } - - if (!response.status.isSuccess()) { - logger.error("Failed to fetch subjects: ${response.status}") - throw RuntimeException("Failed to fetch subjects") - } - - return response.body() - } -} - -@Serializable -data class TokenResponse( - val access_token: String, - val token_type: String, - val expires_in: Long, -) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt new file mode 100644 index 00000000..3eb9564a --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt @@ -0,0 +1,47 @@ +package org.radarbase.authorizer.service + +import jakarta.ws.rs.core.Context +import org.radarbase.authorizer.config.AuthorizerConfig +import org.radarbase.ktor.auth.ClientCredentialsConfig +import org.radarbase.ktor.auth.clientCredentials +import org.radarbase.management.client.MPClient +import org.radarbase.management.client.mpClient +import org.slf4j.LoggerFactory +import java.net.URI +import java.util.function.Supplier + +class MPClientFactory( + @Context private val config: AuthorizerConfig, +) : Supplier { + + override fun get(): MPClient { + val baseUrl = config.auth.managementPortalUrl + val clientId = config.auth.clientId ?: throw IllegalArgumentException("Client ID is required") + val clientSecret = config.auth.clientSecret!! ?: throw IllegalArgumentException("Client Secret is required") + val customTokenUrl = config.auth.authUrl + + val mpClientConfig = MPClient.Config().apply { + url = baseUrl + + auth { + val authConfig = ClientCredentialsConfig( + tokenUrl = customTokenUrl, + clientId = clientId, + clientSecret = clientSecret, + additionalParameters = mapOf("audience" to "res_ManagementPortal"), + ).copyWithEnv() + + return@auth clientCredentials( + authConfig = authConfig, + targetHost = URI.create(baseUrl).host + ) + } + } + + return MPClient(mpClientConfig) + } + + companion object { + private val logger = LoggerFactory.getLogger(MPClientFactory::class.java) + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt deleted file mode 100644 index 59b8d8e8..00000000 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RadarProjectService.kt +++ /dev/null @@ -1,150 +0,0 @@ -package org.radarbase.authorizer.service - -import io.ktor.http.HttpStatusCode -import jakarta.ws.rs.core.Context -import org.radarbase.authorizer.config.AuthorizerConfig -import org.radarbase.jersey.exception.HttpNotFoundException -import org.radarbase.jersey.service.ProjectService -import org.radarbase.kotlin.coroutines.CacheConfig -import org.radarbase.kotlin.coroutines.CachedMap -import org.radarbase.management.client.HttpStatusException -import org.radarbase.management.client.MPOrganization -import org.radarbase.management.client.MPProject -import org.radarbase.management.client.MPSubject -import org.slf4j.LoggerFactory -import java.time.Duration -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentMap -import kotlin.time.Duration.Companion.minutes -import kotlin.time.toKotlinDuration - -class RadarProjectService( - @Context private val config: AuthorizerConfig, - @Context private val mpClient: MPClient, -) : ProjectService { - - private val projects: CachedMap - private val organizations: CachedMap - private val participants: ConcurrentMap> = - ConcurrentHashMap() - - private val projectCacheConfig = - CacheConfig( - refreshDuration = Duration.ofMinutes(5).toKotlinDuration(), - retryDuration = RETRY_INTERVAL, - ) - - init { - val cacheConfig = - CacheConfig( - refreshDuration = Duration.ofMinutes(5).toKotlinDuration(), - retryDuration = 1.minutes, - ) - - projects = - CachedMap(cacheConfig) { - try { - val projectList = mpClient.requestProjects() - projectList.associateBy { it.id } - } catch (e: Exception) { - logger.error("Failed to fetch projects from Management Portal", e) - throw RuntimeException("Unable to fetch projects", e) - } - } - - organizations = - CachedMap(cacheConfig) { - try { - mpClient.requestOrganizations().associateBy { it.id }.also { - logger.debug("Fetched organizations {}", it) - } - } catch (ex: HttpStatusException) { - if (ex.code == HttpStatusCode.NotFound) { - logger.warn( - "Target ManagementPortal does not support organizations. Using default organization main.", - ) - mapOf("main" to defaultOrganization) - } else { - throw ex - } - } - } - } - - suspend fun userProjects(): List = projects.get().values.toList() - - override suspend fun ensureSubject( - projectId: String, - userId: String, - ) { - ensureProject(projectId) - if (!projectUserCache(projectId).contains(userId)) { - throw HttpNotFoundException( - "user_not_found", - "User $userId not found in project $projectId of ManagementPortal.", - ) - } - } - - override suspend fun ensureOrganization(organizationId: String) { - if (!organizations.contains(organizationId)) { - throw HttpNotFoundException( - "organization_not_found", - "Organization $organizationId not found in Management Portal.", - ) - } - } - - override suspend fun ensureProject(projectId: String) { - if (!projects.contains(projectId)) { - throw HttpNotFoundException( - "project_not_found", - "Project $projectId not found in Management Portal.", - ) - } - } - - override suspend fun projectOrganization(projectId: String): String = - projects.get(projectId)?.organization?.id - ?: throw HttpNotFoundException( - "project_not_found", - "Project $projectId not found in Management Portal.", - ) - - suspend fun project(projectId: String): MPProject = - projects.get(projectId) - ?: throw HttpNotFoundException( - "project_not_found", - "Project $projectId not found in Management Portal.", - ) - - override suspend fun listProjects(organizationId: String): List = - projects - .get() - .asSequence() - .filter { it.value.organization?.id == organizationId } - .mapTo(ArrayList()) { it.key } - - suspend fun projectSubjects(projectId: String): List = projectUserCache(projectId).get().values.toList() - - private suspend fun projectUserCache(projectId: String) = - participants.computeIfAbsent(projectId) { - CachedMap(projectCacheConfig) { - mpClient.requestSubjects(projectId).associateBy { checkNotNull(it.id) } - } - } - - suspend fun subject( - projectId: String, - userId: String, - ): MPSubject? { - ensureProject(projectId) - return projectUserCache(projectId).get(userId) - } - - companion object { - private val RETRY_INTERVAL = 1.minutes - private val logger = LoggerFactory.getLogger(ProjectService::class.java) - private val defaultOrganization = MPOrganization("main") - } -} From 258ca8d5474eddd2580e5b5555b815f52608e0c2 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 28 Nov 2024 01:00:17 +0000 Subject: [PATCH 18/27] Revert changes in resource due to update in ProjectService --- .../enhancer/AuthorizerResourceEnhancer.kt | 2 - .../authorizer/resources/ProjectResource.kt | 33 ++-- .../resources/RegistrationResource.kt | 80 ++++----- .../resources/RestSourceUserResource.kt | 154 ++++++++---------- 4 files changed, 112 insertions(+), 157 deletions(-) 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 a496708e..406076ba 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 @@ -54,8 +54,6 @@ class AuthorizerResourceEnhancer( }, ) - private val mpClient = MPClient(config) - override val classes: Array> get() = listOfNotNull( 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 02276b10..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 @@ -28,19 +28,16 @@ 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.EntityDetails import org.radarbase.auth.authorization.Permission 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.authorizer.config.AuthorizerConfig -import org.radarbase.jersey.service.managementportal.RadarProjectService -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.service.AsyncCoroutineService +import org.radarbase.jersey.service.managementportal.RadarProjectService @Path("projects") @Authenticated @@ -49,30 +46,17 @@ import org.radarbase.jersey.service.AsyncCoroutineService @Resource @Singleton class ProjectResource( + @Context private val projectService: RadarProjectService, @Context private val asyncService: AsyncCoroutineService, - @Context private val authService: AuthService, - @Context private val config: AuthorizerConfig, - @Context private val projectService: RadarProjectService ) { @GET @NeedsPermission(Permission.PROJECT_READ) @Cache(maxAge = 300, isPrivate = true, vary = [AUTHORIZATION]) - fun projects( - @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { + fun projects(@Suspended asyncResponse: AsyncResponse) = asyncService.runAsCoroutine(asyncResponse) { ProjectList( - projectService - .userProjects() - .filter { - authService.hasPermission( - Permission.SUBJECT_READ, - EntityDetails( - organization = it.organization?.id, - project = it.id, - ), - ) - }.map { it.toProject() }, + projectService.userProjects() + .map { it.toProject() }, ) } @@ -85,7 +69,8 @@ class ProjectResource( @Suspended asyncResponse: AsyncResponse, ) = asyncService.runAsCoroutine(asyncResponse) { UserList( - projectService.projectSubjects(projectId).map { it.toUser(projectId) }, + projectService.projectSubjects(projectId) + .map { it.toUser() }, ) } @@ -96,5 +81,7 @@ class ProjectResource( fun project( @PathParam("projectId") projectId: String, @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { projectService.project(projectId).toProject() } + ) = 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 bc909a97..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 @@ -21,10 +21,8 @@ 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.config.AuthorizerConfig import org.radarbase.authorizer.doa.RegistrationRepository import org.radarbase.authorizer.doa.RestSourceUserRepository -import org.radarbase.jersey.service.managementportal.RadarProjectService import org.radarbase.authorizer.service.RegistrationService import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceUserService @@ -35,6 +33,7 @@ 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 @Path("registrations") @@ -48,12 +47,10 @@ class RegistrationResource( @Context private val authorizationService: RestSourceAuthorizationService, @Context private val userRepository: RestSourceUserRepository, @Context private val registrationService: RegistrationService, - @Context private val asyncService: AsyncCoroutineService, + @Context private val projectService: RadarProjectService, @Context private val authService: AuthService, - @Context private val config: AuthorizerConfig, - @Context private val projectService: RadarProjectService + @Context private val asyncService: AsyncCoroutineService, ) { - @POST @Authenticated @NeedsPermission(Permission.SUBJECT_UPDATE) @@ -71,18 +68,15 @@ class RegistrationResource( ) var tokenState = registrationService.generate(user, createState.persistent) if (!createState.persistent) { - tokenState = - tokenState.copy( - authEndpointUrl = - authorizationService.getAuthorizationEndpointWithParams( - sourceType = user.sourceType, - userId = user.id!!, - state = tokenState.token, - ), - ) + tokenState = tokenState.copy( + authEndpointUrl = authorizationService.getAuthorizationEndpointWithParams( + sourceType = user.sourceType, + userId = user.id!!, + state = tokenState.token, + ), + ) } - Response - .created(URI("tokens/${tokenState.token}")) + Response.created(URI("tokens/${tokenState.token}")) .entity(tokenState) .build() } @@ -124,30 +118,19 @@ class RegistrationResource( @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.", - ) - } + if (registration.user.authorized) throw HttpConflictException("user_already_authorized", "User was already authorized for this service.") val salt = registration.salt val secretHash = registration.secretHash - if (salt == null || - secretHash == null - ) { - throw HttpBadRequestException("registration_invalid", "Cannot retrieve authentication endpoint token without credentials.") - } + if (salt == null || secretHash == null) throw HttpBadRequestException("registration_invalid", "Cannot retrieve authentication endpoint token without credentials.") val hmac256Secret = Hmac256Secret(tokenSecret.secret, salt, secretHash) if (!hmac256Secret.isValid) throw HttpBadRequestException("bad_secret", "Secret does not match token") - val project = - registration.user.projectId?.let { - projectService.project(it).toProject() - } + val project = registration.user.projectId?.let { + projectService.project(it).toProject() + } RegistrationResponse( token = registration.token, - authEndpointUrl = - authorizationService.getAuthorizationEndpointWithParams( + authEndpointUrl = authorizationService.getAuthorizationEndpointWithParams( sourceType = registration.user.sourceType, userId = registration.user.id!!, state = registration.token, @@ -172,26 +155,23 @@ class RegistrationResource( val registration = registrationService.ensureRegistration(token) val accessToken = authorizationService.requestAccessToken(payload, registration.user.sourceType) val user = userRepository.updateToken(accessToken, registration.user) - val project = - registration.user.projectId?.let { - projectService.project(it).toProject() - } + val project = registration.user.projectId?.let { + projectService.project(it).toProject() + } - val tokenEntity = - RegistrationResponse( - token = registration.token, - userId = registration.user.id!!.toString(), - project = project, - createdAt = registration.createdAt, - expiresAt = registration.expiresAt, - persistent = registration.persistent, - sourceType = registration.user.sourceType, - ) + val tokenEntity = RegistrationResponse( + token = registration.token, + userId = registration.user.id!!.toString(), + project = project, + createdAt = registration.createdAt, + expiresAt = registration.expiresAt, + persistent = registration.persistent, + sourceType = registration.user.sourceType, + ) registrationRepository.remove(registration) - 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 bbdcabc3..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 @@ -40,9 +40,7 @@ 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.config.AuthorizerConfig import org.radarbase.authorizer.doa.RestSourceUserRepository -import org.radarbase.jersey.service.managementportal.RadarProjectService import org.radarbase.authorizer.service.RestSourceAuthorizationService import org.radarbase.authorizer.service.RestSourceClientService import org.radarbase.authorizer.service.RestSourceUserService @@ -52,6 +50,7 @@ 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 @Path("users") @@ -63,108 +62,91 @@ import java.net.URI class RestSourceUserResource( @Context private val userRepository: RestSourceUserRepository, @Context private val userMapper: RestSourceUserMapper, + @Context private val projectService: RadarProjectService, @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, - @Context private val config: AuthorizerConfig, - @Context private val projectService: RadarProjectService ) { - @GET @NeedsPermission(Permission.SUBJECT_READ) fun query( @QueryParam("project-id") projectId: String?, @QueryParam("source-type") sourceType: String?, @QueryParam("search") search: String?, - @DefaultValue("true") @QueryParam("authorized") isAuthorized: String, - @DefaultValue(Integer.MAX_VALUE.toString()) @QueryParam("size") pageSize: Int, - @DefaultValue("1") @QueryParam("page") pageNumber: Int, + @DefaultValue("true") + @QueryParam("authorized") + isAuthorized: String, + @DefaultValue(Integer.MAX_VALUE.toString()) + @QueryParam("size") + pageSize: Int, + @DefaultValue("1") + @QueryParam("page") + pageNumber: Int, @Suspended asyncResponse: AsyncResponse, ) = asyncService.runAsCoroutine(asyncResponse) { - val projectIds = - if (projectId == null) { - projectService - .userProjects() - .filter { - authService.hasPermission( - Permission.SUBJECT_READ, - EntityDetails( - organization = it.organization?.id, - project = it.id, - ), - ) - }.also { projects -> - if (projects.isEmpty()) { - return@runAsCoroutine emptyUsers( - pageNumber, - pageSize, - ) - } - }.map { it.id } - } else { - authService.checkPermission( - Permission.SUBJECT_READ, - EntityDetails(project = projectId), - ) - listOf(projectId) - } - - val sanitizedSourceType = - when (sourceType) { - null -> null - in sourceClientService -> sourceType - else -> return@runAsCoroutine emptyUsers(pageNumber, pageSize) - } + val projectIds = if (projectId == null) { + projectService.userProjects(Permission.SUBJECT_READ) + .also { projects -> if (projects.isEmpty()) return@runAsCoroutine emptyUsers(pageNumber, pageSize) } + .map { it.id } + } else { + authService.checkPermission(Permission.SUBJECT_READ, EntityDetails(project = projectId)) + listOf(projectId) + } + + val sanitizedSourceType = when (sourceType) { + null -> null + in sourceClientService -> sourceType + else -> return@runAsCoroutine emptyUsers(pageNumber, pageSize) + } val sanitizedSearch = search?.takeIf { it.length >= 2 } - val userIds = - if (sanitizedSearch != null) { - projectId - ?: throw HttpBadRequestException( - "missing_project_id", - "Cannot search without a fixed project ID.", - ) - projectService.projectSubjects(projectId).mapNotNull { sub -> + val userIds = if (sanitizedSearch != null) { + projectId ?: throw HttpBadRequestException( + "missing_project_id", + "Cannot search without a fixed project ID.", + ) + projectService.projectSubjects(projectId) + .mapNotNull { sub -> val externalId = sub.externalId ?: return@mapNotNull null sub.id.takeIf { sanitizedSearch in externalId } } - } else { - emptyList() - } + } else { + emptyList() + } - val authorizedBoolean = - when (isAuthorized) { - "true", "yes" -> true - "false", "no" -> false - else -> null - } + val authorizedBoolean = when (isAuthorized) { + "true", "yes" -> true + "false", "no" -> false + else -> null + } val queryPage = Page(pageNumber = pageNumber, pageSize = pageSize) - val (records, page) = - userRepository.query( - queryPage, - projectIds, - sanitizedSourceType, - sanitizedSearch, - userIds, - authorizedBoolean, - ) + val (records, page) = userRepository.query( + queryPage, + projectIds, + sanitizedSourceType, + sanitizedSearch, + userIds, + authorizedBoolean, + ) userMapper.fromRestSourceUsers(records, page) } @POST - @NeedsPermission(Permission.SUBJECT_UPDATE) + @NeedsPermission(Permission.SUBJECT_CREATE) fun create( userDto: RestSourceUserDTO, @Suspended asyncResponse: AsyncResponse, ) = asyncService.runAsCoroutine(asyncResponse) { val user = userService.create(userDto) - Response.created(URI("users/${user.id}")).entity(user).build() + Response.created(URI("users/${user.id}")) + .entity(user) + .build() } @POST @@ -174,7 +156,9 @@ class RestSourceUserResource( @PathParam("id") userId: Long, user: RestSourceUserDTO, @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { userService.update(userId, user) } + ) = asyncService.runAsCoroutine(asyncResponse) { + userService.update(userId, user) + } @GET @Path("{id}") @@ -183,7 +167,9 @@ class RestSourceUserResource( fun readUser( @PathParam("id") userId: Long, @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { userService.get(userId) } + ) = asyncService.runAsCoroutine(asyncResponse) { + userService.get(userId) + } @DELETE @Path("{id}") @@ -193,7 +179,9 @@ class RestSourceUserResource( @Suspended asyncResponse: AsyncResponse, ) = asyncService.runAsCoroutine(asyncResponse) { userService.delete(userId) - Response.noContent().header("user-removed", userId).build() + Response.noContent() + .header("user-removed", userId) + .build() } @POST @@ -203,7 +191,9 @@ class RestSourceUserResource( @PathParam("id") userId: Long, user: RestSourceUserDTO, @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { userService.reset(userId, user) } + ) = asyncService.runAsCoroutine(asyncResponse) { + userService.reset(userId, user) + } @GET @Path("{id}/token") @@ -211,7 +201,9 @@ class RestSourceUserResource( fun requestToken( @PathParam("id") userId: Long, @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { userService.ensureToken(userId) } + ) = asyncService.runAsCoroutine(asyncResponse) { + userService.ensureToken(userId) + } @POST @Path("{id}/token") @@ -219,7 +211,9 @@ class RestSourceUserResource( fun refreshToken( @PathParam("id") userId: Long, @Suspended asyncResponse: AsyncResponse, - ) = asyncService.runAsCoroutine(asyncResponse) { userService.refreshToken(userId) } + ) = asyncService.runAsCoroutine(asyncResponse) { + userService.refreshToken(userId) + } @POST @Path("{id}/token/sign") @@ -234,13 +228,9 @@ class RestSourceUserResource( } companion object { - private fun emptyUsers( - pageNumber: Int, - pageSize: Int, - ) = RestSourceUsers( + private fun emptyUsers(pageNumber: Int, pageSize: Int) = RestSourceUsers( users = listOf(), - metadata = - Page( + metadata = Page( pageNumber = pageNumber, pageSize = pageSize, totalElements = 0, From 3916c095e766edc67292a37540d16b9a389a0366 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 28 Nov 2024 11:35:52 +0000 Subject: [PATCH 19/27] Revert formatting updates in Project class --- .../org/radarbase/authorizer/api/Project.kt | 43 +++++++------------ 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt index 7aaccb5f..0472713b 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/api/Project.kt @@ -19,9 +19,7 @@ package org.radarbase.authorizer.api import org.radarbase.management.client.MPProject import org.radarbase.management.client.MPSubject -data class ProjectList( - val projects: List, -) +data class ProjectList(val projects: List) data class Project( val id: String, @@ -31,30 +29,21 @@ data class Project( val description: String? = null, ) -fun MPProject.toProject() = - Project( - id = id, - name = name, - location = location, - organization = organization?.id, - description = description, - ) - -data class UserList( - val users: List, +fun MPProject.toProject() = Project( + id = id, + name = name, + location = location, + organization = organization?.id, + description = description, ) -data class User( - val id: String, - val projectId: String, - val externalId: String? = null, - val status: String, -) +data class UserList(val users: List) -fun MPSubject.toUser(projectId: String) = - User( - id = checkNotNull(id) { "User must have a login" }, - projectId = projectId, - externalId = externalId, - status = status, - ) +data class User(val id: String, val projectId: String, val externalId: String? = null, val status: String) + +fun MPSubject.toUser() = User( + id = checkNotNull(id) { "User must have a login" }, + projectId = checkNotNull(projectId) { "User must have a project ID" }, + externalId = externalId, + status = status, +) From 1e66f717ca657bcf959f870edda10df04ed5c2c9 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 28 Nov 2024 11:48:07 +0000 Subject: [PATCH 20/27] Rever formatting changes --- .../authorizer/api/RestSourceUserMapper.kt | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) 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 e91a3f34..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 @@ -17,30 +17,23 @@ package org.radarbase.authorizer.api import jakarta.ws.rs.core.Context -import org.radarbase.authorizer.config.AuthorizerConfig 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 config: AuthorizerConfig, - @Context private val projectService: RadarProjectService + @Context private val projectService: RadarProjectService, ) { - suspend fun fromEntity(user: RestSourceUser): RestSourceUserDTO { - val mpUser = - user.projectId?.let { p -> - user.userId?.let { u -> projectService.subject(p, u) } - } + val mpUser = user.projectId?.let { p -> + user.userId?.let { u -> projectService.subject(p, u) } + } return RestSourceUserDTO( id = user.id.toString(), createdAt = user.createdAt, projectId = user.projectId, userId = user.userId, - humanReadableUserId = - mpUser - ?.attributes - ?.get("Human-readable-identifier") + humanReadableUserId = mpUser?.attributes?.get("Human-readable-identifier") ?.takeIf { it.isNotBlank() && it != "null" }, externalId = mpUser?.externalId, sourceId = user.sourceId, @@ -56,10 +49,7 @@ class RestSourceUserMapper( ) } - suspend fun fromRestSourceUsers( - records: List, - page: Page?, - ) = RestSourceUsers( + suspend fun fromRestSourceUsers(records: List, page: Page?) = RestSourceUsers( users = records.forkJoin { fromEntity(it) }, metadata = page, ) From 16f4e6df51747351727c52881dafe0d08b73d8f3 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 28 Nov 2024 11:53:54 +0000 Subject: [PATCH 21/27] Revert formatting changes --- .../enhancer/AuthorizerResourceEnhancer.kt | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) 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 406076ba..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 @@ -32,41 +32,36 @@ import org.radarbase.authorizer.service.DelegatedRestSourceAuthorizationService. import org.radarbase.authorizer.service.GarminSourceAuthorizationService import org.radarbase.authorizer.service.OAuth2RestSourceAuthorizationService import org.radarbase.authorizer.service.OuraAuthorizationService -import org.radarbase.jersey.service.managementportal.RadarProjectService 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 -import org.radarbase.jersey.service.ProjectService class AuthorizerResourceEnhancer( private val config: AuthorizerConfig, ) : JerseyResourceEnhancer { - private val restSourceClients = - RestSourceClients( - config.restSourceClients - .map { it.withEnv() } - .onEach { - requireNotNull(it.clientId) { "Client ID of ${it.sourceType} is missing" } - requireNotNull(it.clientSecret) { "Client secret of ${it.sourceType} is missing" } - }, - ) + private val restSourceClients = RestSourceClients( + config.restSourceClients + .map { it.withEnv() } + .onEach { + requireNotNull(it.clientId) { "Client ID of ${it.sourceType} is missing" } + requireNotNull(it.clientSecret) { "Client secret of ${it.sourceType} is missing" } + }, + ) override val classes: Array> - get() = - listOfNotNull( - Filters.cache, - Filters.logResponse, - if (config.service.enableCors == true) Filters.cors else null, - ).toTypedArray() - - override val packages: Array = - arrayOf( - "org.radarbase.authorizer.resources", - "org.radarbase.authorizer.lifecycle", - ) + get() = listOfNotNull( + Filters.cache, + Filters.logResponse, + if (config.service.enableCors == true) Filters.cors else null, + ).toTypedArray() + + override val packages: Array = arrayOf( + "org.radarbase.authorizer.resources", + "org.radarbase.authorizer.lifecycle", + ) override fun AbstractBinder.enhance() { // Bind instances. These cannot use any injects themselves From 65cba7402a26d6c1d54a9c6c398008ea358db761 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 28 Nov 2024 11:55:04 +0000 Subject: [PATCH 22/27] Clean up comments --- .../authorizer/enhancer/ManagementPortalResourceEnhancer.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt index 373763a5..ac778717 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt @@ -21,7 +21,6 @@ class ManagementPortalResourceEnhancer(private val config: AuthConfig) : JerseyR override fun AbstractBinder.enhance() { val config = config.withEnv() - // Bind other necessary services bindFactory(TokenValidatorFactory::class.java) .to(TokenValidator::class.java) .`in`(Singleton::class.java) @@ -34,13 +33,11 @@ class ManagementPortalResourceEnhancer(private val config: AuthConfig) : JerseyR .to(AuthorizationOracle::class.java) .`in`(Singleton::class.java) - // If managementPortal.clientId is available, bind your custom MPClient if (config.managementPortal.clientId != null) { - bindFactory(MPClientFactory::class.java) // Bind your custom MPClientFactory + bindFactory(MPClientFactory::class.java) .to(MPClient::class.java) .`in`(Singleton::class.java) - // Bind other services related to ProjectService bind(ProjectServiceWrapper::class.java) .to(ProjectService::class.java) .`in`(Singleton::class.java) From f003e001784709d4d3b043835393759e269c8129 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 28 Nov 2024 12:56:28 +0000 Subject: [PATCH 23/27] Update ClientCredentialsConfig in MPClientFactory --- .../java/org/radarbase/authorizer/service/MPClientFactory.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt index 3eb9564a..49549966 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt @@ -21,14 +21,13 @@ class MPClientFactory( val customTokenUrl = config.auth.authUrl val mpClientConfig = MPClient.Config().apply { - url = baseUrl auth { val authConfig = ClientCredentialsConfig( tokenUrl = customTokenUrl, clientId = clientId, clientSecret = clientSecret, - additionalParameters = mapOf("audience" to "res_ManagementPortal"), + audience = "res_ManagementPortal", ).copyWithEnv() return@auth clientCredentials( From ff46214487f62f93e9849e26e56b22870d6fef86 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 29 Nov 2024 15:02:19 +0000 Subject: [PATCH 24/27] Bump radar commons and restore missing url in MPClient --- .../java/org/radarbase/authorizer/service/MPClientFactory.kt | 1 + buildSrc/src/main/kotlin/Versions.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt index 49549966..d0c38869 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt @@ -21,6 +21,7 @@ class MPClientFactory( val customTokenUrl = config.auth.authUrl val mpClientConfig = MPClient.Config().apply { + url = baseUrl auth { val authConfig = ClientCredentialsConfig( diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 7a0ac474..216cb596 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -6,7 +6,7 @@ object Versions { const val kotlin = "1.9.23" - const val radarCommons = "1.1.2" + const val radarCommons = "1.1.3-SNAPSHOT" const val radarJersey = "0.11.2" const val postgresql = "42.6.1" const val ktor = "2.3.11" From 2acd0e39731d77d59813aa5290fcebb295bb6af1 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 2 Dec 2024 12:31:20 +0000 Subject: [PATCH 25/27] Update create user permission to SUBJECT_UPDATE --- .../radarbase/authorizer/resources/RestSourceUserResource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ccf9ae2b..e3f58ae9 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 @@ -137,7 +137,7 @@ class RestSourceUserResource( } @POST - @NeedsPermission(Permission.SUBJECT_CREATE) + @NeedsPermission(Permission.SUBJECT_UPDATE) fun create( userDto: RestSourceUserDTO, @Suspended asyncResponse: AsyncResponse, From b0f9f7ba4f268cf8fb55d480a3ab723c86a510a2 Mon Sep 17 00:00:00 2001 From: Pauline Conde Date: Tue, 3 Dec 2024 11:57:17 +0000 Subject: [PATCH 26/27] Bump radar commons version --- buildSrc/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 216cb596..1eabf10a 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -6,7 +6,7 @@ object Versions { const val kotlin = "1.9.23" - const val radarCommons = "1.1.3-SNAPSHOT" + const val radarCommons = "1.1.3" const val radarJersey = "0.11.2" const val postgresql = "42.6.1" const val ktor = "2.3.11" From 4c4bf07ff0294dbe78ebff7fe6c04d2132d04de4 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 3 Dec 2024 12:24:20 +0000 Subject: [PATCH 27/27] Fix lint errors --- .../authorizer/enhancer/ManagementPortalEnhancerFactory.kt | 2 -- .../enhancer/ManagementPortalResourceEnhancer.kt | 4 ++-- .../org/radarbase/authorizer/service/MPClientFactory.kt | 7 +++---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalEnhancerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalEnhancerFactory.kt index a7db63bd..b39e3114 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 @@ -16,7 +16,6 @@ package org.radarbase.authorizer.enhancer -import jakarta.inject.Singleton import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.authorizer.doa.entity.RegistrationState import org.radarbase.authorizer.doa.entity.RestSourceUser @@ -26,7 +25,6 @@ import org.radarbase.jersey.enhancer.EnhancerFactory import org.radarbase.jersey.enhancer.Enhancers import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.hibernate.config.HibernateResourceEnhancer -import org.radarbase.management.client.MPClient /** This binder needs to register all non-Jersey classes, otherwise initialization fails. */ class ManagementPortalEnhancerFactory( diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt index ac778717..1d86d08e 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt @@ -4,18 +4,18 @@ import jakarta.inject.Singleton import org.glassfish.jersey.internal.inject.AbstractBinder import org.radarbase.auth.authentication.TokenValidator import org.radarbase.auth.authorization.AuthorizationOracle +import org.radarbase.authorizer.service.MPClientFactory import org.radarbase.jersey.auth.AuthConfig import org.radarbase.jersey.auth.AuthValidator import org.radarbase.jersey.auth.jwt.AuthorizationOracleFactory -import org.radarbase.jersey.auth.managementportal.ManagementPortalTokenValidator import org.radarbase.jersey.auth.jwt.TokenValidatorFactory +import org.radarbase.jersey.auth.managementportal.ManagementPortalTokenValidator import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.service.ProjectService import org.radarbase.jersey.service.managementportal.MPProjectService import org.radarbase.jersey.service.managementportal.ProjectServiceWrapper import org.radarbase.jersey.service.managementportal.RadarProjectService import org.radarbase.management.client.MPClient -import org.radarbase.authorizer.service.MPClientFactory class ManagementPortalResourceEnhancer(private val config: AuthConfig) : JerseyResourceEnhancer { override fun AbstractBinder.enhance() { diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt index d0c38869..b2eb0216 100644 --- a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt @@ -5,7 +5,6 @@ import org.radarbase.authorizer.config.AuthorizerConfig import org.radarbase.ktor.auth.ClientCredentialsConfig import org.radarbase.ktor.auth.clientCredentials import org.radarbase.management.client.MPClient -import org.radarbase.management.client.mpClient import org.slf4j.LoggerFactory import java.net.URI import java.util.function.Supplier @@ -16,8 +15,8 @@ class MPClientFactory( override fun get(): MPClient { val baseUrl = config.auth.managementPortalUrl - val clientId = config.auth.clientId ?: throw IllegalArgumentException("Client ID is required") - val clientSecret = config.auth.clientSecret!! ?: throw IllegalArgumentException("Client Secret is required") + val clientId = config.auth.clientId + val clientSecret = config.auth.clientSecret ?: throw IllegalArgumentException("Client Secret is required") val customTokenUrl = config.auth.authUrl val mpClientConfig = MPClient.Config().apply { @@ -33,7 +32,7 @@ class MPClientFactory( return@auth clientCredentials( authConfig = authConfig, - targetHost = URI.create(baseUrl).host + targetHost = URI.create(baseUrl).host, ) } }