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..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 @@ -2,10 +2,12 @@ package org.radarbase.authorizer.config data class AuthConfig( val managementPortalUrl: String = "http://managementportal-app:8080/managementportal", + val authUrl: String = "http://hydra-public:4444/oauth2/token", val clientId: String = "radar_rest_sources_auth_backend", - val clientSecret: String? = null, + 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://hydra-public:4444/.well-known/jwks.json"), ) 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/enhancer/ManagementPortalEnhancerFactory.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalEnhancerFactory.kt index 149006b5..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 @@ -27,30 +27,38 @@ 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('/'), - clientId = config.auth.clientId, - clientSecret = config.auth.clientSecret, - syncProjectsIntervalMin = config.service.syncProjectsIntervalMin, - syncParticipantsIntervalMin = config.service.syncParticipantsIntervalMin, - ), - jwtResourceName = config.auth.jwtResourceName, - ) + 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, + ), + 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, HibernateResourceEnhancer(dbConfig), - Enhancers.managementPortal(authConfig), + ManagementPortalResourceEnhancer(authConfig), + Enhancers.ecdsa, JedisResourceEnhancer(), Enhancers.exception, AuthorizerResourceEnhancer(config), 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..1d86d08e --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/enhancer/ManagementPortalResourceEnhancer.kt @@ -0,0 +1,50 @@ +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.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.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 + +class ManagementPortalResourceEnhancer(private val config: AuthConfig) : JerseyResourceEnhancer { + override fun AbstractBinder.enhance() { + val config = config.withEnv() + + 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 (config.managementPortal.clientId != null) { + bindFactory(MPClientFactory::class.java) + .to(MPClient::class.java) + .`in`(Singleton::class.java) + + bind(ProjectServiceWrapper::class.java) + .to(ProjectService::class.java) + .`in`(Singleton::class.java) + + bind(MPProjectService::class.java) + .to(RadarProjectService::class.java) + .`in`(Singleton::class.java) + } + } +} diff --git a/authorizer-app-backend/src/main/java/org/radarbase/authorizer/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, 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..b2eb0216 --- /dev/null +++ b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/MPClientFactory.kt @@ -0,0 +1,46 @@ +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.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 + 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, + audience = "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/RestSourceUserService.kt b/authorizer-app-backend/src/main/java/org/radarbase/authorizer/service/RestSourceUserService.kt index c48da663..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 @@ -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,18 @@ class RestSourceUserService( suspend fun create(userDto: RestSourceUserDTO): RestSourceUserDTO { userDto.ensure() + val existingUser = userRepository.findByUserIdProjectIdSourceType( + userId = userDto.userId!!, + projectId = userDto.projectId!!, + sourceType = userDto.sourceType, + ) + if (existingUser != null) { + val response = Response.status(Response.Status.CONFLICT) + .entity(mapOf("status" to 409, "message" to "User already exists.", "user" to userMapper.fromEntity(existingUser))) + .build() + + throw WebApplicationException(response) + } val user = userRepository.create(userDto) return userMapper.fromEntity(user) } 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]}`; } } diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 7a0ac474..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.2" + const val radarCommons = "1.1.3" const val radarJersey = "0.11.2" const val postgresql = "42.6.1" const val ktor = "2.3.11"