Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -26,6 +26,7 @@ import jakarta.persistence.Id
import jakarta.persistence.OneToMany
import jakarta.persistence.SequenceGenerator
import jakarta.persistence.Table
import jakarta.persistence.UniqueConstraint
import org.hibernate.annotations.BatchSize
import org.hibernate.annotations.Cache
import org.hibernate.annotations.CacheConcurrencyStrategy
Expand All @@ -36,7 +37,7 @@ import java.util.Objects
import java.util.UUID

@Entity
@Table(name = "rest_source_user")
@Table(name = "rest_source_user", uniqueConstraints = [UniqueConstraint(columnNames = ["external_user_id", "source_type"])])
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
class RestSourceUser(
@Id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import org.radarbase.authorizer.api.StateCreateDTO
import org.radarbase.authorizer.api.TokenSecret
import org.radarbase.authorizer.api.toProject
import org.radarbase.authorizer.doa.RegistrationRepository
import org.radarbase.authorizer.doa.RestSourceUserRepository
import org.radarbase.authorizer.service.RegistrationService
import org.radarbase.authorizer.service.RestSourceAuthorizationService
import org.radarbase.authorizer.service.RestSourceUserService
Expand All @@ -45,7 +44,6 @@ class RegistrationResource(
@Context private val registrationRepository: RegistrationRepository,
@Context private val restSourceUserService: RestSourceUserService,
@Context private val authorizationService: RestSourceAuthorizationService,
@Context private val userRepository: RestSourceUserRepository,
@Context private val registrationService: RegistrationService,
@Context private val projectService: RadarProjectService,
@Context private val authService: AuthService,
Expand Down Expand Up @@ -154,7 +152,7 @@ class RegistrationResource(
) = asyncService.runAsCoroutine(asyncResponse) {
val registration = registrationService.ensureRegistration(token)
val accessToken = authorizationService.requestAccessToken(payload, registration.user.sourceType)
val user = userRepository.updateToken(accessToken, registration.user)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are replacing userRepository::updateToken, the variable userRepository is now unused and can be removed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reviewing this. Removed now.

val user = restSourceUserService.updateUserToken(accessToken, registration.user)
val project = registration.user.projectId?.let {
projectService.project(it).toProject()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import jakarta.ws.rs.core.Context
import jakarta.ws.rs.core.Response
import org.radarbase.auth.authorization.EntityDetails
import org.radarbase.auth.authorization.Permission
import org.radarbase.authorizer.api.RestOauth2AccessToken
import org.radarbase.authorizer.api.RestSourceUserDTO
import org.radarbase.authorizer.api.RestSourceUserMapper
import org.radarbase.authorizer.api.TokenDTO
Expand All @@ -13,6 +14,7 @@ import org.radarbase.authorizer.doa.entity.RestSourceUser
import org.radarbase.jersey.auth.AuthService
import org.radarbase.jersey.exception.HttpApplicationException
import org.radarbase.jersey.exception.HttpBadRequestException
import org.radarbase.jersey.exception.HttpConflictException
import org.radarbase.jersey.exception.HttpNotFoundException
import kotlin.time.Duration.Companion.seconds

Expand Down Expand Up @@ -106,6 +108,36 @@ class RestSourceUserService(
)
}

/**
* Validates that an external user ID is not already in use by another user.
* Should be called before updating a user's token with an external user ID.
*/
private suspend fun validateExternalUserId(token: RestOauth2AccessToken?, user: RestSourceUser) {
if (token?.externalUserId != null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an else block to handle when externalUserId is null?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added now 👍

val existingUser = userRepository.findByExternalId(token.externalUserId, user.sourceType)
if (existingUser != null && existingUser.id != user.id) {
throw HttpConflictException(
"external_user_id_already_exists",
"External user ID ${token.externalUserId} is already registered for another user of source type ${user.sourceType} and user id ${existingUser.userId}",
)
}
} else {
throw HttpBadRequestException(
"missing_external_user_id",
"External user ID cannot be empty",
)
}
}

/**
* Updates a user's token after validating the external user ID.
* This method ensures no duplicate external user IDs exist in the system.
*/
suspend fun updateUserToken(token: RestOauth2AccessToken?, user: RestSourceUser): RestSourceUser {
validateExternalUserId(token, user)
return userRepository.updateToken(token, user)
}

suspend fun ensureToken(userId: Long): TokenDTO {
ensureUser(userId, Permission.MEASUREMENT_CREATE)
return runLocked(userId) { user ->
Expand Down Expand Up @@ -151,7 +183,7 @@ class RestSourceUserService(
}

val token = authorizationService.refreshToken(user)
val updatedUser = userRepository.updateToken(token, user)
val updatedUser = updateUserToken(token, user)

if (!updatedUser.authorized) {
throw HttpApplicationException(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.10.xsd">

<changeSet author="yatharth" id="20240607120000-add-unique-constraint-external-user-id">
<!-- Update duplicates to a fixed value for investigation, appending the external_user_id, except the row with the lowest id -->
<preConditions onFail="CONTINUE">
<not>
<sqlCheck expectedResult="0">
SELECT COUNT(*) FROM (
SELECT external_user_id, source_type FROM rest_source_user
WHERE external_user_id IS NOT NULL
GROUP BY external_user_id, source_type
HAVING COUNT(*) > 1
) t;
</sqlCheck>
</not>
</preConditions>
<sql dbms="postgresql">
UPDATE rest_source_user a
SET external_user_id = 'DUPLICATE_INVESTIGATE_' || a.id || '_' || a.external_user_id
FROM rest_source_user b
WHERE a.external_user_id = b.external_user_id
AND a.source_type = b.source_type
AND a.id > b.id
AND a.external_user_id IS NOT NULL
AND b.id = (
SELECT MIN(id) FROM rest_source_user c
WHERE c.external_user_id = a.external_user_id
AND c.source_type = a.source_type
);
</sql>
<addUniqueConstraint
tableName="rest_source_user"
columnNames="external_user_id,source_type"
constraintName="uk_rest_source_user_external_user_id_source_type"/>
</changeSet>
</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
<include file="db/changelog/changes/00000000000003_add_created_at_column.xml"/>
<include file="db/changelog/changes/20210721100000_add_registration_table.xml"/>
<include file="db/changelog/changes/20211008113000_add_registration_created_at.xml"/>
<include file="db/changelog/changes/20240607120000_add_unique_constraint_external_user_id.xml"/>
</databaseChangeLog>
Loading