Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Ory auth #282

Merged
merged 27 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8d00f90
Add support for separate auth server (ory hydra)
mpgxvii Sep 25, 2024
5923ad3
Fix Management portal auth and resource enhancer
mpgxvii Sep 25, 2024
37f668c
Cleanup logs
mpgxvii Sep 26, 2024
d2649b4
Add back auth service check for project permissions
mpgxvii Sep 26, 2024
7b73d61
Fix lint errors
mpgxvii Sep 26, 2024
2b78f90
Fix MPClient missing imports
mpgxvii Sep 30, 2024
4edcf3a
Fix lint errors
mpgxvii Sep 30, 2024
139abe2
Fix MPClient auth config imports
mpgxvii Oct 12, 2024
207e6bb
Update user creation endpoint to return user if it exists
mpgxvii Oct 12, 2024
bcffef7
Fix RestSourceUserService
mpgxvii Oct 12, 2024
19e96f9
Fix creating rest source user permission
mpgxvii Oct 14, 2024
ff21a25
Cache access token in MPClient
mpgxvii Oct 14, 2024
f773bd6
Update default config
mpgxvii Oct 16, 2024
c6f2ba4
Inject ProjectService instead of constructing
mpgxvii Nov 27, 2024
bd177ef
Fix MPClient
mpgxvii Nov 27, 2024
fd19851
Update AuthConfig
mpgxvii Nov 27, 2024
65fd6d4
Reuse radar-jersey client and projectservice but override for hydra auth
mpgxvii Nov 28, 2024
258ca8d
Revert changes in resource due to update in ProjectService
mpgxvii Nov 28, 2024
3916c09
Revert formatting updates in Project class
mpgxvii Nov 28, 2024
1e66f71
Rever formatting changes
mpgxvii Nov 28, 2024
16f4e6d
Revert formatting changes
mpgxvii Nov 28, 2024
65cba74
Clean up comments
mpgxvii Nov 28, 2024
f003e00
Update ClientCredentialsConfig in MPClientFactory
mpgxvii Nov 28, 2024
ff46214
Bump radar commons and restore missing url in MPClient
mpgxvii Nov 29, 2024
2acd0e3
Update create user permission to SUBJECT_UPDATE
mpgxvii Dec 2, 2024
b0f9f7b
Bump radar commons version
mpgxvii Dec 3, 2024
4c4bf07
Fix lint errors
mpgxvii Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>? = null,
val jwtRSAPublicKeys: List<String>? = null,
val jwtIssuer: String? = null,
val jwtResourceName: String = "res_restAuthorizer",
val jwksUrls: List<String> = listOf("http://hydra-public:4444/.well-known/jwks.json"),
)
Original file line number Diff line number Diff line change
Expand Up @@ -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?
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}

Expand Down Expand Up @@ -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))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,32 +26,41 @@ 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(private val config: AuthorizerConfig) : EnhancerFactory {
class ManagementPortalEnhancerFactory(
private val config: AuthorizerConfig,
) : EnhancerFactory {
override fun createEnhancers(): List<JerseyResourceEnhancer> {
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),
Expand Down
Original file line number Diff line number Diff line change
@@ -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.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()

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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class RestSourceUserResource(
}

@POST
@NeedsPermission(Permission.SUBJECT_CREATE)
@NeedsPermission(Permission.SUBJECT_UPDATE)
fun create(
userDto: RestSourceUserDTO,
@Suspended asyncResponse: AsyncResponse,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<MPClient> {

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,
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)
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]}`;
}
}
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
mpgxvii marked this conversation as resolved.
Show resolved Hide resolved
const val radarJersey = "0.11.2"
const val postgresql = "42.6.1"
const val ktor = "2.3.11"
Expand Down
Loading