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 kratos subject webhook endpoint #888

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
141ebaf
Add kratos subject webhook endpoint
mpgxvii Jul 8, 2024
9a4c541
Fix KratosEndpoint
mpgxvii Jul 8, 2024
84b4ffb
Update logger factory
mpgxvii Jul 15, 2024
2ad85e6
Specify subject id from kratos id
mpgxvii Jul 30, 2024
628f326
Merge branch 'add-kratos-endpoint' of https://github.com/RADAR-base/M…
mpgxvii Jul 30, 2024
5b3b7ab
Add back permission check
mpgxvii Jul 30, 2024
ce976fe
Update logger
mpgxvii Jul 30, 2024
e49c60a
Update AuthService import
mpgxvii Jul 30, 2024
73f9ced
Revert some services to previous implementation since identity creati…
mpgxvii Aug 7, 2024
289698a
Update KratosEndpoint subject creation webhook
mpgxvii Aug 7, 2024
c240dd3
Update KratosEndpoint
mpgxvii Aug 7, 2024
8f426c4
Fix tests
mpgxvii Aug 7, 2024
9270cba
Fix UserResource test
mpgxvii Aug 7, 2024
dd98cc4
Cleanup KratosEndpoint methods
mpgxvii Aug 8, 2024
3bee316
Separate out subject creation and activation webhooks
mpgxvii Aug 12, 2024
db50ee4
Merge branch 'dev' of https://github.com/RADAR-base/ManagementPortal …
mpgxvii Aug 13, 2024
527a193
Merge branch 'feature/ory-based-authorization' of https://github.com/…
mpgxvii Aug 15, 2024
33748f7
Merge branch 'feature/ory-based-authorization' of https://github.com/…
mpgxvii Aug 23, 2024
047c5a9
Fix merge conflicts
mpgxvii Aug 27, 2024
aa3fcc2
Fix security config for kratos endpoint
mpgxvii Aug 28, 2024
5104435
Add check if kratos schema id is subject before creating subject thro…
mpgxvii Aug 28, 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 @@ -45,42 +45,21 @@ class KratosSessionDTO(
)

@Serializable
class Identity(
val id: String? = null,
data class Identity(
var id: String? = null,
val schema_id: String? = null,
val schema_url: String? = null,
val state: String? = null,
@Serializable(with = InstantSerializer::class)
val state_changed_at: Instant? = null,
val traits: Traits? = null,
val metadata_public: Metadata? = null,
var metadata_public: Metadata? = null,
@Serializable(with = InstantSerializer::class)
val created_at: Instant? = null,
@Serializable(with = InstantSerializer::class)
val updated_at: Instant? = null,
)
{


fun parseRoles(): Set<AuthorityReference> = buildSet {
if (metadata_public?.authorities?.isNotEmpty() == true) {
for (roleValue in metadata_public.authorities) {
val authority = RoleAuthority.valueOfAuthorityOrNull(roleValue)
if (authority?.scope == RoleAuthority.Scope.GLOBAL) {
add(AuthorityReference(authority))
}
}
}
if (metadata_public?.roles?.isNotEmpty() == true) {
for (roleValue in metadata_public.roles) {
val role = RoleAuthority.valueOfAuthorityOrNull(roleValue)
if (role?.scope == RoleAuthority.Scope.GLOBAL) {
add(AuthorityReference(role))
}
}
}
}
}
{}


@Serializable
Expand All @@ -91,17 +70,17 @@ class KratosSessionDTO(

@Serializable
class Metadata (
val roles: List<String>,
val authorities: Set<String>,
val scope: List<String>,
val sources: List<String>,
val aud: List<String>,
val mp_login: String?
val roles: List<String> = emptyList(),
val authorities: Set<String> = emptySet(),
val scope: List<String> = emptyList(),
val sources: List<String> = emptyList(),
val aud: List<String> = emptyList(),
val mp_login: String? = null,
)

fun toDataRadarToken() : DataRadarToken {
return DataRadarToken(
roles = this.identity.parseRoles(),
roles = emptySet(),
scopes = this.identity.metadata_public?.scope?.toSet() ?: emptySet(),
sources = this.identity.metadata_public?.sources ?: emptyList(),
grantType = "session",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,6 @@ class ManagementPortalSecurityConfigLoader {
try {
if (!isAdminIdCreated && managementPortalProperties?.identityServer?.serverUrl != null && managementPortalProperties.identityServer.adminEmail != null) {
logger.info("Overriding admin email to ${managementPortalProperties.identityServer.adminEmail}")
val dto: UserDTO =
runBlocking { userService!!.addAdminEmail(managementPortalProperties.identityServer.adminEmail) }
runBlocking { userService?.updateUser(dto) }
isAdminIdCreated = true
} else if (!isAdminIdCreated) {
logger.warn("AdminEmail property is left empty, thus no admin identity could be created.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class OAuth2ServerConfiguration(
.skipUrlPattern(HttpMethod.GET, "/api/meta-token/*")
.skipUrlPattern(HttpMethod.GET, "/api/sitesettings")
.skipUrlPattern(HttpMethod.GET, "/api/redirect/**")
.skipUrlPattern(HttpMethod.GET, "/api/kratos/**")
.skipUrlPattern(HttpMethod.GET, "/api/logout-url")
.skipUrlPattern(HttpMethod.GET, "/oauth2/authorize")
.skipUrlPattern(HttpMethod.GET, "/images/**")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class SecurityConfiguration
.antMatchers("/api/profile-info")
.antMatchers("/api/activate")
.antMatchers("/api/redirect/**")
.antMatchers("/api/kratos/**")
.antMatchers("/api/account/reset_password/init")
.antMatchers("/api/account/reset_password/finish")
.antMatchers("/test/**")
Expand Down
4 changes: 0 additions & 4 deletions src/main/java/org/radarbase/management/domain/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,6 @@ class User : AbstractEntity(), Serializable {
@Column(name = "reset_date")
var resetDate: ZonedDateTime? = null

/** Identifier for association with the identity service provider.
* Null if not linked to an external identity. */
var identity: String? = null

/** Authorities that a user has. */
val authorities: Set<String>
get() {
Expand Down
156 changes: 42 additions & 114 deletions src/main/java/org/radarbase/management/service/IdentityService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.Duration
import org.radarbase.management.service.dto.UserDTO
import org.radarbase.management.service.dto.RoleDTO

/**
* Service class for managing identities.
Expand Down Expand Up @@ -58,67 +60,32 @@ class IdentityService(
log.debug("kratos serverAdminUrl set to ${managementPortalProperties.identityServer.adminUrl()}")
}

/** Save a [User] to the IDP as an identity. Returns the generated [KratosSessionDTO.Identity] */
@Throws(IdpException::class)
suspend fun saveAsIdentity(user: User): KratosSessionDTO.Identity? {
val kratosIdentity: KratosSessionDTO.Identity?

withContext(Dispatchers.IO) {
val identity = createIdentity(user)

val postRequestBuilder = HttpRequestBuilder().apply {
url("${adminUrl}/admin/identities")
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
setBody(identity)
}
val response = httpClient.post(postRequestBuilder)

if (response.status.isSuccess()) {
kratosIdentity = response.body<KratosSessionDTO.Identity>()
log.debug("saved identity for user ${user.login} to IDP as ${kratosIdentity.id}")
} else {
throw IdpException(
"couldn't save Kratos ID to server at " + adminUrl,
)
}
}

return kratosIdentity
}

/** Update a [User] as to the IDP as an identity. Returns the updated [KratosSessionDTO.Identity] */
@Throws(IdpException::class)
suspend fun updateAssociatedIdentity(user: User): KratosSessionDTO.Identity? {
val kratosIdentity: KratosSessionDTO.Identity?

user.identity ?: throw IdpException(
"user ${user.login} could not be updated on the IDP. No identity was set",
)

withContext(Dispatchers.IO) {
val identity = createIdentity(user)
val response = httpClient.put {
url("${adminUrl}/admin/identities/${user.identity}")
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
setBody(identity)
}


if (response.status.isSuccess()) {
kratosIdentity = response.body<KratosSessionDTO.Identity>()
log.debug("Updated identity for user ${user.login} to IDP as ${kratosIdentity.id}")
} else {
throw IdpException(
"Couldn't update identity to server at $adminUrl"
)
/** Update a [User] as to the IDP as an identity. Returns the updated [KratosSessionDTO.Identity] */
@Throws(IdpException::class)
suspend fun updateAssociatedIdentity(identity: KratosSessionDTO.Identity): KratosSessionDTO.Identity? {
val updatedIdentity: KratosSessionDTO.Identity?

withContext(Dispatchers.IO) {
val response = httpClient.put {
url("${adminUrl}/admin/identities/${identity.id}")
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
setBody(identity)
}

if (response.status.isSuccess()) {
updatedIdentity = response.body<KratosSessionDTO.Identity>()
log.debug("Updated identity for ${updatedIdentity.id}")
} else {
throw IdpException(
"Couldn't update identity to server at $adminUrl"
)
}
}

return updatedIdentity
}

return kratosIdentity
}

/** Delete a [User] as to the IDP as an identity. */
@Throws(IdpException::class)
suspend fun deleteAssociatedIdentity(userIdentity: String?) {
Expand All @@ -144,39 +111,40 @@ class IdentityService(
}
}

public suspend fun updateIdentityMetadataWithRoles(identity: KratosSessionDTO.Identity, user: UserDTO): KratosSessionDTO.Identity? {
val newIdentity = identity.copy(
metadata_public = getIdentityMetadata(user)
)
return updateAssociatedIdentity(newIdentity)
}

/**
* Convert a [User] to a [KratosSessionDTO.Identity] object.
* @param user The object to convert
* @return the newly created DTO object
*/
@Throws(IdpException::class)
private fun createIdentity(user: User): KratosSessionDTO.Identity {
public fun getIdentityMetadata(user: UserDTO): KratosSessionDTO.Metadata {
try {
return KratosSessionDTO.Identity(
schema_id = "user",
traits = KratosSessionDTO.Traits(email = user.email),
metadata_public = KratosSessionDTO.Metadata(
return KratosSessionDTO.Metadata(
aud = emptyList(),
sources = emptyList(), //empty at the time of creation
roles = user.roles.mapNotNull { role: Role ->
val auth = role.authority?.name
when (role.role?.scope) {
RoleAuthority.Scope.GLOBAL -> auth
RoleAuthority.Scope.ORGANIZATION -> role.organization!!.name + ":" + auth
RoleAuthority.Scope.PROJECT -> role.project!!.projectName + ":" + auth
null -> null
roles = user.roles.orEmpty().mapNotNull { role ->
val auth = role.authorityName
when {
role.projectName != null -> "${role.projectName}:$auth"
role.organizationName != null -> "${role.organizationName}:$auth"
else -> auth
}
}.toList(),
authorities = user.authorities,
authorities = user.authorities.orEmpty(),
scope = Permission.scopes().filter { scope ->
val permission = Permission.ofScope(scope)
val auths = user.roles.mapNotNull { it.role }

val auths = user.roles?.map { RoleAuthority.valueOfAuthority(it.authorityName!!) } ?: emptyList()
return@filter authService.mayBeGranted(auths, permission)
},
mp_login = user.login
)
)
}
catch (e: Throwable){
val message = "could not convert user ${user.login} to identity"
Expand All @@ -185,46 +153,6 @@ class IdentityService(
}
}

/**
* get a recovery link from the identityprovider in the response, which expires in 24 hours.
* @param user The user for whom the recovery link is requested.
* @return The recovery link obtained from the server response.
* @throws IdpException If there is an issue with the identity or if the recovery link cannot be obtained from the server.
*/
@Throws(IdpException::class)
suspend fun getRecoveryLink(user: User): String {
val recoveryLink: String

user.identity ?: throw IdpException(
"user ${user.login} could not be recovered on the IDP. No identity was set",
)

withContext(Dispatchers.IO) {
val response = httpClient.post {
url("${adminUrl}/admin/recovery/link")
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
setBody(
mapOf(
"expires_in" to "24h",
"identity_id" to user.identity
)
)
}

if (response.status.isSuccess()) {
recoveryLink = response.body<Map<String, String>>()["recovery_link"]!!
log.debug("recovery link for user ${user.login} is $recoveryLink")
} else {
throw IdpException(
"couldn't get recovery link from server at $adminUrl"
)
}
}

return recoveryLink
}

companion object {
private val log = LoggerFactory.getLogger(IdentityService::class.java)
}
Expand Down
16 changes: 8 additions & 8 deletions src/main/java/org/radarbase/management/service/RoleService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,21 @@ class RoleService(
*/
@Transactional(readOnly = true)
fun findAll(): List<RoleDTO> {
val optUser = userService.getUserWithAuthorities()
val optUser = userService.userWithAuthorities
?: // return an empty list if we do not have a current user (e.g. with client credentials
// oauth2 grant)
return emptyList()
val currentUserAuthorities = optUser.authorities
return if (currentUserAuthorities.contains(RoleAuthority.SYS_ADMIN.authority)) {
return if (currentUserAuthorities?.contains(RoleAuthority.SYS_ADMIN.authority) == true) {
log.debug("Request to get all Roles")
roleRepository.findAll().filterNotNull().map { role: Role -> roleMapper.roleToRoleDTO(role) }.toList()
} else (if (currentUserAuthorities.contains(RoleAuthority.PROJECT_ADMIN.authority)) {
} else (if (currentUserAuthorities?.contains(RoleAuthority.PROJECT_ADMIN.authority) == true) {
log.debug("Request to get project admin's project Projects")
optUser.roles.asSequence().filter { role: Role? ->
optUser.roles?.asSequence()?.filter { role: Role? ->
(RoleAuthority.PROJECT_ADMIN.authority == role?.authority?.name)
}.mapNotNull { r: Role -> r.project?.projectName }.distinct()
.flatMap { name: String -> roleRepository.findAllRolesByProjectName(name) }
.map { role -> roleMapper.roleToRoleDTO(role) }.toList()
}?.mapNotNull { r: Role -> r.project?.projectName }?.distinct()
?.flatMap { name: String -> roleRepository.findAllRolesByProjectName(name) }
?.map { role -> roleMapper.roleToRoleDTO(role) }?.toList()
} else {
emptyList()
}) as List<RoleDTO>
Expand Down Expand Up @@ -252,4 +252,4 @@ class RoleService(
return authority
}
}
}
}
Loading
Loading