Skip to content

Commit fc004da

Browse files
authored
Merge pull request #312 from RADAR-base/fix/mp-user-sync
Add periodic user cleanup
2 parents 7eccc21 + 2705101 commit fc004da

File tree

4 files changed

+125
-1
lines changed

4 files changed

+125
-1
lines changed

authorizer-app-backend/src/main/java/org/radarbase/authorizer/config/AuthorizerServiceConfig.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ data class AuthorizerServiceConfig(
1616
val resourceConfig: Class<out EnhancerFactory> = ManagementPortalEnhancerFactory::class.java,
1717
val enableCors: Boolean? = false,
1818
val syncProjectsIntervalMin: Long = 30,
19-
val syncParticipantsIntervalMin: Long = 30,
19+
val syncParticipantsIntervalMin: Long = 1440, // 1 day,
2020
val tokenExpiryTimeInMinutes: Long = 15,
2121
val persistentTokenExpiryInMin: Long = 3.days.inWholeMinutes,
2222
) {

authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepository.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface RestSourceUserRepository {
2626
suspend fun create(user: RestSourceUserDTO): RestSourceUser
2727
suspend fun updateToken(token: RestOauth2AccessToken?, user: RestSourceUser): RestSourceUser
2828
suspend fun read(id: Long): RestSourceUser?
29+
suspend fun listAll(): List<RestSourceUser>
2930
suspend fun update(userId: Long, user: RestSourceUserDTO): RestSourceUser
3031
suspend fun query(
3132
page: Page,

authorizer-app-backend/src/main/java/org/radarbase/authorizer/doa/RestSourceUserRepositoryImpl.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ class RestSourceUserRepositoryImpl(
9797

9898
override suspend fun read(id: Long): RestSourceUser? = transact { find(RestSourceUser::class.java, id) }
9999

100+
override suspend fun listAll(): List<RestSourceUser> = transact {
101+
createQuery(
102+
"""
103+
SELECT u
104+
FROM RestSourceUser u
105+
""".trimIndent(),
106+
RestSourceUser::class.java,
107+
).resultList
108+
}
109+
100110
override suspend fun update(userId: Long, user: RestSourceUserDTO): RestSourceUser = transact {
101111
val existingUser = find(RestSourceUser::class.java, userId)
102112
?: throw HttpNotFoundException("user_not_found", "User with ID $userId not found")
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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

Comments
 (0)