|
| 1 | +package org.radarbase.authorizer.lifecycle |
| 2 | + |
| 3 | +import jakarta.inject.Singleton |
| 4 | +import jakarta.ws.rs.core.Context |
| 5 | +import jakarta.ws.rs.ext.Provider |
| 6 | +import org.glassfish.jersey.server.BackgroundScheduler |
| 7 | +import org.glassfish.jersey.server.monitoring.ApplicationEvent |
| 8 | +import org.glassfish.jersey.server.monitoring.ApplicationEventListener |
| 9 | +import org.glassfish.jersey.server.monitoring.RequestEvent |
| 10 | +import org.glassfish.jersey.server.monitoring.RequestEventListener |
| 11 | +import org.radarbase.authorizer.config.AuthorizerConfig |
| 12 | +import org.radarbase.authorizer.doa.RestSourceUserRepository |
| 13 | +import org.radarbase.authorizer.doa.entity.RestSourceUser |
| 14 | +import org.radarbase.jersey.service.AsyncCoroutineService |
| 15 | +import org.radarbase.jersey.service.managementportal.RadarProjectService |
| 16 | +import org.slf4j.LoggerFactory |
| 17 | +import java.util.concurrent.Future |
| 18 | +import java.util.concurrent.ScheduledExecutorService |
| 19 | +import java.util.concurrent.TimeUnit |
| 20 | + |
| 21 | +@Provider |
| 22 | +@Singleton |
| 23 | +class UserSyncLifecycleManager( |
| 24 | + @BackgroundScheduler |
| 25 | + @Context |
| 26 | + private val scheduler: ScheduledExecutorService, |
| 27 | + @Context private val userRepository: RestSourceUserRepository, |
| 28 | + @Context private val projectService: RadarProjectService, |
| 29 | + @Context private val asyncService: AsyncCoroutineService, |
| 30 | + @Context private val config: AuthorizerConfig, |
| 31 | +) : ApplicationEventListener { |
| 32 | + |
| 33 | + private var cleanupTask: Future<*>? = null |
| 34 | + |
| 35 | + override fun onEvent(event: ApplicationEvent?) { |
| 36 | + event ?: return |
| 37 | + when (event.type) { |
| 38 | + ApplicationEvent.Type.INITIALIZATION_APP_FINISHED -> schedulePeriodicCleanup() |
| 39 | + ApplicationEvent.Type.DESTROY_FINISHED -> cancelPeriodicCleanup() |
| 40 | + else -> Unit |
| 41 | + } |
| 42 | + } |
| 43 | + |
| 44 | + @Synchronized |
| 45 | + private fun schedulePeriodicCleanup() { |
| 46 | + if (cleanupTask != null) return |
| 47 | + |
| 48 | + val intervalMin = config.service.syncParticipantsIntervalMin |
| 49 | + cleanupTask = scheduler.scheduleAtFixedRate( |
| 50 | + ::runCleanup, |
| 51 | + intervalMin, // initial delay |
| 52 | + intervalMin, |
| 53 | + TimeUnit.MINUTES, |
| 54 | + ) |
| 55 | + logger.info("Scheduled Management Portal user synchronization every {} minutes.", intervalMin) |
| 56 | + } |
| 57 | + |
| 58 | + @Synchronized |
| 59 | + private fun cancelPeriodicCleanup() { |
| 60 | + cleanupTask?.let { |
| 61 | + it.cancel(true) |
| 62 | + cleanupTask = null |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + private fun runCleanup() { |
| 67 | + asyncService.runBlocking { |
| 68 | + try { |
| 69 | + val allUsers = userRepository.listAll() |
| 70 | + if (allUsers.isEmpty()) return@runBlocking |
| 71 | + |
| 72 | + val usersByProject = allUsers |
| 73 | + .filter { it.projectId != null && it.userId != null } |
| 74 | + .groupBy { it.projectId!! } |
| 75 | + |
| 76 | + for ((projectId, users) in usersByProject) { |
| 77 | + val mpSubjects = try { |
| 78 | + projectService.projectSubjects(projectId) |
| 79 | + } catch (ex: Exception) { |
| 80 | + logger.warn("Skipping cleanup for project {} due to MP error: {}", projectId, ex.message) |
| 81 | + continue |
| 82 | + } |
| 83 | + val mpUserIds = mpSubjects.mapNotNull { it.id }.toHashSet() |
| 84 | + |
| 85 | + for (user in users) { |
| 86 | + val subjectId = user.userId |
| 87 | + if (subjectId != null && subjectId !in mpUserIds) { |
| 88 | + removeUser(user) |
| 89 | + } |
| 90 | + } |
| 91 | + } |
| 92 | + } catch (ex: Throwable) { |
| 93 | + logger.error("Failed to run Management Portal user cleanup.", ex) |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + private suspend fun removeUser(user: RestSourceUser) { |
| 99 | + try { |
| 100 | + logger.info("Removing user {} from project {} (sourceType={}) as it no longer exists in Management Portal.", user.userId, user.projectId, user.sourceType) |
| 101 | + // TODO: Delegate to the source-specific authorization service to ensure proper deregistration. |
| 102 | + userRepository.delete(user) |
| 103 | + } catch (ex: Exception) { |
| 104 | + logger.error("Failed to remove user {} in project {}: {}", user.userId, user.projectId, ex.message) |
| 105 | + } |
| 106 | + } |
| 107 | + |
| 108 | + override fun onRequest(requestEvent: RequestEvent?): RequestEventListener? = null |
| 109 | + |
| 110 | + companion object { |
| 111 | + private val logger = LoggerFactory.getLogger(UserSyncLifecycleManager::class.java) |
| 112 | + } |
| 113 | +} |
0 commit comments