From 46a4dc8bb579802e1d45b6fdbbe9300e391ec9e9 Mon Sep 17 00:00:00 2001 From: Tristan Garwood Date: Tue, 30 Jan 2024 13:01:26 -0500 Subject: [PATCH] Add new updateuser DirectoryDAO method --- .../sam/dataAccess/DirectoryDAO.scala | 3 +- .../sam/dataAccess/PostgresDirectoryDAO.scala | 31 +++++++++++++++++++ .../dsde/workbench/sam/model/SamModel.scala | 5 +++ .../workbench/sam/service/UserService.scala | 21 +++---------- .../sam/dataAccess/MockDirectoryDAO.scala | 18 ++++++++++- 5 files changed, 59 insertions(+), 19 deletions(-) diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/DirectoryDAO.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/DirectoryDAO.scala index 5664bd166..77feb48b3 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/DirectoryDAO.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/DirectoryDAO.scala @@ -5,7 +5,7 @@ import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.model.google.ServiceAccountSubjectId import org.broadinstitute.dsde.workbench.sam.azure.{ManagedIdentityObjectId, PetManagedIdentity, PetManagedIdentityId} import org.broadinstitute.dsde.workbench.sam.model.api.{SamUser, SamUserAttributes} -import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, SamUserTos} +import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, SamUserTos, UserUpdate} import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import java.time.Instant @@ -53,6 +53,7 @@ trait DirectoryDAO { def setUserAzureB2CId(userId: WorkbenchUserId, b2cId: AzureB2CId, samRequestContext: SamRequestContext): IO[Unit] def updateUserEmail(userId: WorkbenchUserId, email: WorkbenchEmail, samRequestContext: SamRequestContext): IO[Unit] def deleteUser(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Unit] + def updateUser(userId: WorkbenchUserId, userUpdate: UserUpdate, samRequestContext: SamRequestContext): IO[Option[SamUser]] def listUsersGroups(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Set[WorkbenchGroupIdentity]] def listUserDirectMemberships(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[LazyList[WorkbenchGroupIdentity]] diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAO.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAO.scala index 943dd4635..404f6cbcc 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAO.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAO.scala @@ -479,6 +479,37 @@ class PostgresDirectoryDAO(protected val writeDbRef: DbReference, protected val override def updateUserEmail(userId: WorkbenchUserId, email: WorkbenchEmail, samRequestContext: SamRequestContext): IO[Unit] = IO.unit + override def updateUser(userId: WorkbenchUserId, userUpdate: UserUpdate, samRequestContext: SamRequestContext): IO[Option[SamUser]] = + serializableWriteTransaction("updateUser", samRequestContext) { implicit session => + val u = UserTable.column + val setColumnsClause = userUpdate match { + case UserUpdate(None, None) => throw new WorkbenchException("Cannot update user with no values.") + case UserUpdate(Some(newGoogleSubjectId), None) => + s"set (${u.googleSubjectId}, ${u.updatedAt})" + case UserUpdate(None, Some(newAzureB2CId)) => + s"set (${u.azureB2cId}, ${u.updatedAt})" + case UserUpdate(Some(newGoogleSubjectId), Some(newAzureB2CId)) => + s"set (${u.googleSubjectId}, ${u.azureB2cId}, ${u.updatedAt})" + } + + val updateGoogleSubjectIdClause = if (userUpdate.newGoogleSubjectId.isDefined) s"${userUpdate.newGoogleSubjectId}," else "" + val updateAzureB2CIDClause = if (userUpdate.newAzureB2CId.isDefined) s"${userUpdate.newAzureB2CId}," else "" + + samsql"""update ${UserTable.table} + ${setColumnsClause} = + ( + ${updateGoogleSubjectIdClause} + ${updateAzureB2CIDClause} + ${Instant.now()} + ) + where ${u.id} = $userId + returning ${u.id}, ${u.googleSubjectId}, ${u.email}, ${u.azureB2cId}, ${u.enabled}, ${u.createdAt}, ${u.registeredAt}, ${u.updatedAt}""" + .map(r => UserTable.unmarshalUserRecord(UserTable(UserTable.syntax)(r))) + .single() + .apply() + + } + override def deleteUser(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Unit] = serializableWriteTransaction("deleteUser", samRequestContext) { implicit session => val userTable = UserTable.syntax diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/SamModel.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/SamModel.scala index 2fbf34844..a89b7b1e6 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/SamModel.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/SamModel.scala @@ -72,6 +72,11 @@ object UserStatusDetails { tosAccepted: Boolean, adminEnabled: Boolean ) + +@Lenses final case class UserUpdate( + newAzureB2CId: Option[String], + newGoogleSubjectId: Option[String] +) @Lenses final case class TermsOfServiceAcceptance(value: String) extends ValueObject @Lenses final case class TermsOfServiceComplianceStatus(userId: WorkbenchUserId, userHasAcceptedLatestTos: Boolean, permitsSystemUsage: Boolean) diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/UserService.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/UserService.scala index e2d285db5..464522b94 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/UserService.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/UserService.scala @@ -155,7 +155,7 @@ class UserService( } else None private def validateAzureB2CId(azureB2CId: AzureB2CId): Option[ErrorReport] = - if (!UUID_REGEX.matcher(azureB2CId.value).matches() && !(azureB2CId.value == "")) { + if (!UUID_REGEX.matcher(azureB2CId.value).matches() && !(azureB2CId.value == "null")) { Option(ErrorReport(s"invalid azureB2CId [${azureB2CId.value}]")) } else None @@ -210,7 +210,7 @@ class UserService( request.azureB2CId.foreach(azureB2CId => errorReports = errorReports ++ validateAzureB2CId(azureB2CId)) if (errorReports.nonEmpty) { IO.raiseError(new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.BadRequest, "invalid user update", errorReports))) - } else if (request.googleSubjectId.contains(GoogleSubjectId("")) && request.azureB2CId.contains(AzureB2CId(""))) { + } else if (request.googleSubjectId.contains(GoogleSubjectId("null")) && request.azureB2CId.contains(AzureB2CId("null"))) { IO.raiseError( new WorkbenchExceptionWithErrorReport( ErrorReport(StatusCodes.BadRequest, "unable to null both azureB2CId and googleSubjectId in the same request", errorReports) @@ -222,21 +222,8 @@ class UserService( directoryDAO.updateUserEmail(userId, email, samRequestContext) updatedUser = user.copy(email = email) } - request.googleSubjectId.foreach { googleSubjectId => - directoryDAO.setGoogleSubjectId(userId, googleSubjectId, samRequestContext) - if (request.googleSubjectId.contains(GoogleSubjectId(""))) { - updatedUser = updatedUser.copy(googleSubjectId = None) - } else - updatedUser = updatedUser.copy(googleSubjectId = Option(googleSubjectId)) - } - request.azureB2CId.foreach { azureB2CId => - directoryDAO.setUserAzureB2CId(userId, azureB2CId, samRequestContext) - if (request.azureB2CId.contains(AzureB2CId(""))) { - updatedUser = updatedUser.copy(azureB2CId = None) - } else - updatedUser = updatedUser.copy(azureB2CId = Option(azureB2CId)) - } - IO(Some(updatedUser)) + val userUpdate = UserUpdate(request.azureB2CId.map(_.value), request.googleSubjectId.map(_.value)) + directoryDAO.updateUser(userId, userUpdate, samRequestContext) } case None => IO(None) } diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockDirectoryDAO.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockDirectoryDAO.scala index e94a28668..ca56cb37d 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockDirectoryDAO.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockDirectoryDAO.scala @@ -9,7 +9,7 @@ import org.broadinstitute.dsde.workbench.sam._ import org.broadinstitute.dsde.workbench.sam.azure.{ManagedIdentityObjectId, PetManagedIdentity, PetManagedIdentityId} import org.broadinstitute.dsde.workbench.sam.db.tables.TosTable import org.broadinstitute.dsde.workbench.sam.model.api.{SamUser, SamUserAttributes} -import org.broadinstitute.dsde.workbench.sam.model.{AccessPolicy, BasicWorkbenchGroup, SamUserTos} +import org.broadinstitute.dsde.workbench.sam.model.{AccessPolicy, BasicWorkbenchGroup, SamUserTos, UserUpdate} import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import java.time.Instant @@ -129,6 +129,22 @@ class MockDirectoryDAO(val groups: mutable.Map[WorkbenchGroupIdentity, Workbench users -= userId } + override def updateUser(userId: WorkbenchUserId, userUpdate: UserUpdate, samRequestContext: SamRequestContext): IO[Option[SamUser]] = { + val updatedUser = for { + user <- users.get(userId) + updatedUser = user.copy( + googleSubjectId = if (userUpdate.newGoogleSubjectId.isDefined) userUpdate.newGoogleSubjectId.map(GoogleSubjectId) else user.googleSubjectId, + azureB2CId = if (userUpdate.newAzureB2CId.isDefined) userUpdate.newAzureB2CId.map(AzureB2CId) else user.azureB2CId, + updatedAt = Instant.now() + ) + } yield updatedUser + + IO.pure(updatedUser.map { user => + users.put(userId, user) + user + }) + } + override def listUsersGroups(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Set[WorkbenchGroupIdentity]] = IO { listSubjectsGroups(userId, Set.empty).map(_.id) }