diff --git a/src/main/resources/swagger/api-docs.yaml b/src/main/resources/swagger/api-docs.yaml index ddb00ee07..74af23b20 100755 --- a/src/main/resources/swagger/api-docs.yaml +++ b/src/main/resources/swagger/api-docs.yaml @@ -3370,6 +3370,58 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorReport' + /termsOfService/v1/user/self/history: + get: + tags: + - TermsOfService + summary: gets a user's own terms of service acceptance/rejection action history + operationId: userTermsOfServiceSelfGetHistory + parameters: + - name: limit + in: query + description: limit on how many records to return, defaults to 100 + required: false + schema: + type: string + responses: + 200: + description: user has a history of terms of service actions + content: + application/json: + schema: + $ref: '#/components/schemas/UserTermsOfServiceHistory' + 404: + description: user has no history of terms of service actions + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorReport' + /termsOfService/v1/user/{sam_user_id}/history: + get: + tags: + - TermsOfService + summary: gets a user's terms of service acceptance/rejection action history, can only get another user's history if the requester is an admin. + operationId: userTermsOfServiceGetUserHistory + parameters: + - name: sam_user_id + in: path + description: the id of the sam user to get terms of service status for + required: true + schema: + type: string + - name: limit + in: query + description: limit on how many records to return, defaults to 100 + required: false + schema: + type: string + responses: + 200: + description: user has a history of terms of service actions + content: + application/json: + schema: + $ref: '#/components/schemas/UserTermsOfServiceHistory' /version: get: tags: @@ -4091,6 +4143,33 @@ components: type: boolean description: based on the user's currently accepted terms of service version and when the terms of service were last changed, should the user still be permitted to use the system? + UserTermsOfServiceHistory: + required: + - history + type: object + description: a user's terms of service action history + properties: + history: + type: array + items: + $ref: '#/components/schemas/UserTermsOfServiceHistoryRecord' + UserTermsOfServiceHistoryRecord: + required: + - action + - version + - timestamp + type: object + properties: + action: + type: string + description: the terms of service action the user took + version: + type: string + description: the version of the terms of service the user accepted/rejected + timestamp: + type: string + format: date-time + description: the timestamp of the action ManagedResourceGroupCoordinates: required: - tenantId diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutes.scala index 203d2c4c5..bb7b72389 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutes.scala @@ -66,7 +66,7 @@ trait OldUserRoutes extends SamUserDirectives with SamRequestContextDirectives { get { withUserAllowInactive(samRequestContext) { samUser => complete { - tosService.getTosComplianceStatus(samUser, samRequestContext).map { tosAcceptanceStatus => + tosService.getTermsOfServiceComplianceStatus(samUser, samRequestContext).map { tosAcceptanceStatus => StatusCodes.OK -> Option(JsBoolean(tosAcceptanceStatus.permitsSystemUsage)) } } @@ -144,7 +144,7 @@ trait OldUserRoutes extends SamUserDirectives with SamRequestContextDirectives { } ~ path("termsOfServiceComplianceStatus") { get { - complete(tosService.getTosComplianceStatus(user, samRequestContext)) + complete(tosService.getTermsOfServiceComplianceStatus(user, samRequestContext)) } } } diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRoutes.scala index 8b5f95296..a6a4cc680 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRoutes.scala @@ -97,6 +97,17 @@ trait TermsOfServiceRoutes extends SamUserDirectives with SamRequestContextDirec complete(tosService.rejectCurrentTermsOfService(samUser.id, samRequestContext).map(_ => StatusCodes.NoContent)) } } + } ~ + pathPrefix("history") { // api/termsOfService/v1/user/{userId}/history + pathEndOrSingleSlash { + get { + parameters("limit".as[Integer].withDefault(100)) { (limit: Int) => + complete { + tosService.getTermsOfServiceHistoryForUser(samUser.id, samRequestContext, limit) + } + } + } + } } } ~ // The {user_id} route must be last otherwise it will try to parse the other routes incorrectly as user id's @@ -113,7 +124,9 @@ trait TermsOfServiceRoutes extends SamUserDirectives with SamRequestContextDirec pathPrefix("history") { // api/termsOfService/v1/user/{userId}/history pathEndOrSingleSlash { getWithTelemetry(samRequestContext, userIdParam(requestUserId)) { - complete(StatusCodes.NotImplemented) + parameters("limit".as[Integer].withDefault(100)) { (limit: Int) => + complete(tosService.getTermsOfServiceHistoryForUser(requestUserId, samRequestContext, limit)) + } } } } 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 a76306f5b..5664bd166 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 @@ -76,8 +76,14 @@ trait DirectoryDAO { def acceptTermsOfService(userId: WorkbenchUserId, tosVersion: String, samRequestContext: SamRequestContext): IO[Boolean] def rejectTermsOfService(userId: WorkbenchUserId, tosVersion: String, samRequestContext: SamRequestContext): IO[Boolean] - def getUserTos(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Option[SamUserTos]] - def getUserTosVersion(userId: WorkbenchUserId, tosVersion: Option[String], samRequestContext: SamRequestContext): IO[Option[SamUserTos]] + def getUserTermsOfService(userId: WorkbenchUserId, samRequestContext: SamRequestContext, action: Option[String] = None): IO[Option[SamUserTos]] + def getUserTermsOfServiceVersion( + userId: WorkbenchUserId, + tosVersion: Option[String], + samRequestContext: SamRequestContext, + action: Option[String] = None + ): IO[Option[SamUserTos]] + def getUserTermsOfServiceHistory(userId: WorkbenchUserId, samRequestContext: SamRequestContext, limit: Integer): IO[List[SamUserTos]] def createPetManagedIdentity(petManagedIdentity: PetManagedIdentity, samRequestContext: SamRequestContext): IO[PetManagedIdentity] def loadPetManagedIdentity(petManagedIdentityId: PetManagedIdentityId, samRequestContext: SamRequestContext): IO[Option[PetManagedIdentity]] def getUserFromPetManagedIdentity(petManagedIdentityObjectId: ManagedIdentityObjectId, samRequestContext: SamRequestContext): IO[Option[SamUser]] 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 b2268ae1c..c14639420 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 @@ -649,20 +649,28 @@ class PostgresDirectoryDAO(protected val writeDbRef: DbReference, protected val } // When no tosVersion is specified, return the latest TosRecord for the user - override def getUserTos(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Option[SamUserTos]] = - getUserTosVersion(userId, None, samRequestContext) - - override def getUserTosVersion(userId: WorkbenchUserId, tosVersion: Option[String], samRequestContext: SamRequestContext): IO[Option[SamUserTos]] = - readOnlyTransaction("getUserTos", samRequestContext) { implicit session => + override def getUserTermsOfService(userId: WorkbenchUserId, samRequestContext: SamRequestContext, action: Option[String] = None): IO[Option[SamUserTos]] = + getUserTermsOfServiceVersion(userId, None, samRequestContext, action) + + override def getUserTermsOfServiceVersion( + userId: WorkbenchUserId, + tosVersion: Option[String], + samRequestContext: SamRequestContext, + action: Option[String] = None + ): IO[Option[SamUserTos]] = + readOnlyTransaction("getUserTermsOfService", samRequestContext) { implicit session => val tosTable = TosTable.syntax val column = TosTable.column - val versionConstraint = if (tosVersion.isDefined) samsqls"and ${column.version} = ${tosVersion.get}" else samsqls"" + val versionConstraint = tosVersion.map(v => samsqls"and ${column.version} = $v").getOrElse(samsqls"") + val actionConstraint = action.map(a => samsqls"and ${column.action} = $a").getOrElse(samsqls"") val loadUserTosQuery = samsql"""select ${tosTable.resultAll} from ${TosTable as tosTable} - where ${column.samUserId} = $userId $versionConstraint + where ${column.samUserId} = $userId + $versionConstraint + $actionConstraint order by ${column.createdAt} desc limit 1""" @@ -670,6 +678,22 @@ class PostgresDirectoryDAO(protected val writeDbRef: DbReference, protected val userTosRecordOpt.map(TosTable.unmarshalUserRecord) } + override def getUserTermsOfServiceHistory(userId: WorkbenchUserId, samRequestContext: SamRequestContext, limit: Integer): IO[List[SamUserTos]] = + readOnlyTransaction("getUserTermsOfServiceHistory", samRequestContext) { implicit session => + val tosTable = TosTable.syntax + val column = TosTable.column + + val loadUserTosQuery = + samsql"""select ${tosTable.resultAll} + from ${TosTable as tosTable} + where ${column.samUserId} = ${userId} + order by ${column.createdAt} desc + limit ${limit}""" + + val userTosRecordOpt: List[TosRecord] = loadUserTosQuery.map(TosTable(tosTable)).list().apply() + userTosRecordOpt.map(TosTable.unmarshalUserRecord) + } + override def isEnabled(subject: WorkbenchSubject, samRequestContext: SamRequestContext): IO[Boolean] = readOnlyTransaction("isEnabled", samRequestContext) { implicit session => val userIdOpt = subject match { 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 7fd501d5a..2fbf34844 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 @@ -85,6 +85,8 @@ object UserStatusDetails { ) @Lenses final case class TermsOfServiceDetails(latestAcceptedVersion: String, acceptedOn: Instant, permitsSystemUsage: Boolean, isCurrentVersion: Boolean) +@Lenses final case class TermsOfServiceHistory(history: List[TermsOfServiceHistoryRecord]) +@Lenses final case class TermsOfServiceHistoryRecord(action: String, version: String, timestamp: Instant) @Lenses final case class ResourceActionPattern(value: String, description: String, authDomainConstrainable: Boolean) { def matches(other: ResourceAction) = value.r.pattern.matcher(other.value).matches() @@ -254,6 +256,7 @@ final case class SamUserTos(id: WorkbenchUserId, version: String, action: String this.action == userTos.action case _ => false } + def toHistoryRecord: TermsOfServiceHistoryRecord = TermsOfServiceHistoryRecord(action, version, createdAt) } object SamLenses { val resourceIdentityAccessPolicy = AccessPolicy.id composeLens FullyQualifiedPolicyId.resource diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamJsonSupport.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamJsonSupport.scala index 4f8e1a257..f14907128 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamJsonSupport.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamJsonSupport.scala @@ -29,6 +29,8 @@ import org.broadinstitute.dsde.workbench.sam.model.{ TermsOfServiceAcceptance, TermsOfServiceComplianceStatus, TermsOfServiceDetails, + TermsOfServiceHistory, + TermsOfServiceHistoryRecord, UserIdInfo, UserPolicyResponse, UserResourcesResponse, @@ -73,6 +75,10 @@ object SamJsonSupport { implicit val TermsOfServiceDetailsFormat = jsonFormat4(TermsOfServiceDetails.apply) + implicit val termsOfServiceHistoryRecordFormat = jsonFormat3(TermsOfServiceHistoryRecord.apply) + + implicit val termsOfServiceHistory = jsonFormat1(TermsOfServiceHistory.apply) + implicit val SamUserTosFormat = jsonFormat4(SamUserTos.apply) implicit val termsOfAcceptanceStatusFormat = jsonFormat3(TermsOfServiceComplianceStatus.apply) diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/TosService.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/TosService.scala index 475ca6d34..7459260e8 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/TosService.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/TosService.scala @@ -13,7 +13,14 @@ import org.broadinstitute.dsde.workbench.sam.dataAccess.DirectoryDAO import org.broadinstitute.dsde.workbench.sam.db.tables.TosTable import org.broadinstitute.dsde.workbench.sam.errorReportSource import org.broadinstitute.dsde.workbench.sam.model.api.{SamUser, TermsOfServiceConfigResponse} -import org.broadinstitute.dsde.workbench.sam.model.{OldTermsOfServiceDetails, SamUserTos, TermsOfServiceComplianceStatus, TermsOfServiceDetails} +import org.broadinstitute.dsde.workbench.sam.model.{ + OldTermsOfServiceDetails, + SamUserTos, + TermsOfServiceComplianceStatus, + TermsOfServiceDetails, + TermsOfServiceHistory, + TermsOfServiceHistoryRecord +} import org.broadinstitute.dsde.workbench.sam.util.AsyncLogging.{FutureWithLogging, IOWithLogging} import org.broadinstitute.dsde.workbench.sam.util.{SamRequestContext, SupportsAdmin} @@ -79,7 +86,7 @@ class TosService( @Deprecated def getTosDetails(samUser: SamUser, samRequestContext: SamRequestContext): IO[OldTermsOfServiceDetails] = - directoryDao.getUserTos(samUser.id, samRequestContext).map { tos => + directoryDao.getUserTermsOfService(samUser.id, samRequestContext).map { tos => OldTermsOfServiceDetails(isEnabled = true, tosConfig.isGracePeriodEnabled, tosConfig.version, tos.map(_.version)) } @@ -100,7 +107,7 @@ class TosService( } private def ensureLatestTermsOfService(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[SamUserTos] = for { - maybeTermsOfServiceRecord <- directoryDao.getUserTos(userId, samRequestContext) + maybeTermsOfServiceRecord <- directoryDao.getUserTermsOfService(userId, samRequestContext, Option(TosTable.ACCEPT)) latestUserTermsOfService <- maybeTermsOfServiceRecord .map(IO.pure) .getOrElse( @@ -115,8 +122,20 @@ class TosService( case Some(samUser) => samUser case None => throw new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.NotFound, s"Could not find user:${userId}")) } - def getTosComplianceStatus(samUser: SamUser, samRequestContext: SamRequestContext): IO[TermsOfServiceComplianceStatus] = for { - latestUserTos <- directoryDao.getUserTos(samUser.id, samRequestContext) + + def getTermsOfServiceHistoryForUser(userId: WorkbenchUserId, samRequestContext: SamRequestContext, limit: Integer): IO[TermsOfServiceHistory] = + ensureAdminIfNeeded[TermsOfServiceHistory](userId, samRequestContext) { + directoryDao.getUserTermsOfServiceHistory(userId, samRequestContext, limit).map { + case samUserTosHistory if samUserTosHistory.isEmpty => TermsOfServiceHistory(List.empty) + case samUserTosHistory => + TermsOfServiceHistory( + samUserTosHistory.map(historyRecord => TermsOfServiceHistoryRecord(historyRecord.action, historyRecord.version, historyRecord.createdAt)) + ) + } + } + + def getTermsOfServiceComplianceStatus(samUser: SamUser, samRequestContext: SamRequestContext): IO[TermsOfServiceComplianceStatus] = for { + latestUserTos <- directoryDao.getUserTermsOfService(samUser.id, samRequestContext) userHasAcceptedLatestVersion = userHasAcceptedCurrentTermsOfService(latestUserTos) permitsSystemUsage = tosAcceptancePermitsSystemUsage(samUser, latestUserTos) } yield TermsOfServiceComplianceStatus(samUser.id, userHasAcceptedLatestVersion, permitsSystemUsage) 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 7b174da0c..3cce2f6ef 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 @@ -318,7 +318,7 @@ class UserService( allUsersStatus <- directoryDAO.isGroupMember(allUsersGroup.id, user.id, samRequestContext) recover { case _: NameNotFoundException => false } - tosComplianceStatus <- tosService.getTosComplianceStatus(user, samRequestContext) + tosComplianceStatus <- tosService.getTermsOfServiceComplianceStatus(user, samRequestContext) adminEnabled <- directoryDAO.isEnabled(user.id, samRequestContext) } yield { // We are removing references to LDAP but this will require an API version change here, so we are leaving @@ -360,14 +360,14 @@ class UserService( // Mixing up the endpoint to return user info AND status information is only causing problems and confusion def getUserStatusInfo(user: SamUser, samRequestContext: SamRequestContext): IO[UserStatusInfo] = for { - tosAcceptanceDetails <- tosService.getTosComplianceStatus(user, samRequestContext) + tosAcceptanceDetails <- tosService.getTermsOfServiceComplianceStatus(user, samRequestContext) } yield UserStatusInfo(user.id.value, user.email.value, tosAcceptanceDetails.permitsSystemUsage && user.enabled, user.enabled) def getUserStatusDiagnostics(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Option[UserStatusDiagnostics]] = directoryDAO.loadUser(userId, samRequestContext).flatMap { case Some(user) => // pulled out of for comprehension to allow concurrent execution - val tosAcceptanceStatus = tosService.getTosComplianceStatus(user, samRequestContext) + val tosAcceptanceStatus = tosService.getTermsOfServiceComplianceStatus(user, samRequestContext) val adminEnabledStatus = directoryDAO.isEnabled(user.id, samRequestContext) val allUsersStatus = cloudExtensions.getOrCreateAllUsersGroup(directoryDAO, samRequestContext).flatMap { allUsersGroup => directoryDAO.isGroupMember(allUsersGroup.id, user.id, samRequestContext) recover { case e: NameNotFoundException => false } @@ -457,7 +457,7 @@ class UserService( def getUserAllowances(samUser: SamUser, samRequestContext: SamRequestContext): IO[SamUserAllowances] = for { - tosStatus <- tosService.getTosComplianceStatus(samUser, samRequestContext) + tosStatus <- tosService.getTermsOfServiceComplianceStatus(samUser, samRequestContext) } yield SamUserAllowances( allowed = samUser.enabled && tosStatus.permitsSystemUsage, enabled = samUser.enabled, diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamRoutesBuilder.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamRoutesBuilder.scala index 9945ccfc1..590455442 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamRoutesBuilder.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamRoutesBuilder.scala @@ -5,6 +5,7 @@ import akka.http.scaladsl.server.Directives.{onSuccess, reject} import akka.http.scaladsl.server._ import akka.stream.Materializer import org.broadinstitute.dsde.workbench.model.{ErrorReportSource, WorkbenchGroup} +import org.broadinstitute.dsde.workbench.sam.model.TermsOfServiceHistory import org.broadinstitute.dsde.workbench.sam.model.api.{SamUser, SamUserAttributes} import org.broadinstitute.dsde.workbench.sam.service._ import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext @@ -74,6 +75,11 @@ class MockSamRoutesBuilder(allUsersGroup: WorkbenchGroup)(implicit system: Actor this } + def withTermsOfServiceHistoryForUser(samUser: SamUser, tosHistory: TermsOfServiceHistory): MockSamRoutesBuilder = { + mockTosServiceBuilder.withTermsOfServiceHistoryForUser(samUser, tosHistory) + this + } + def withUserAttributes(samUser: SamUser, userAttributes: SamUserAttributes): MockSamRoutesBuilder = { userServiceBuilder.withUserAttributes(samUser, userAttributes) this diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRouteSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRouteSpec.scala index 1629e00e0..c2c513b0f 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRouteSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/TermsOfServiceRouteSpec.scala @@ -6,15 +6,18 @@ import akka.http.scaladsl.server.Route import akka.http.scaladsl.testkit.ScalatestRouteTest import org.broadinstitute.dsde.workbench.model.WorkbenchEmail import org.broadinstitute.dsde.workbench.sam.TestSupport.{databaseEnabled, databaseEnabledClue} +import org.broadinstitute.dsde.workbench.sam.db.tables.TosTable import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model.api.{SamUser, TermsOfServiceConfigResponse} -import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, TermsOfServiceDetails} +import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, TermsOfServiceDetails, TermsOfServiceHistory, TermsOfServiceHistoryRecord} import org.broadinstitute.dsde.workbench.sam.service.CloudExtensions import org.broadinstitute.dsde.workbench.sam.{Generator, TestSupport} import org.scalatest.concurrent.Eventually.eventually import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers +import java.time.Instant + class TermsOfServiceRouteSpec extends AnyFunSpec with Matchers with ScalatestRouteTest with TestSupport { describe("GET /tos/text") { @@ -152,6 +155,72 @@ class TermsOfServiceRouteSpec extends AnyFunSpec with Matchers with ScalatestRou } } + describe("GET /api/termsOfService/v1/user/{USER_ID}/history") { + val samRoutes = TestSamRoutes(Map.empty) + it("should return a list of `TermsOfServiceHistoryRecord`") { + val allUsersGroup: BasicWorkbenchGroup = BasicWorkbenchGroup(CloudExtensions.allUsersGroupName, Set(), WorkbenchEmail("all_users@fake.com")) + val defaultUser: SamUser = Generator.genWorkbenchUserGoogle.sample.get + val record1 = TermsOfServiceHistoryRecord(TosTable.ACCEPT, "0", Instant.now) + val record2 = TermsOfServiceHistoryRecord(TosTable.REJECT, "0", Instant.now.minusSeconds(5)) + val mockSamRoutesBuilder = new MockSamRoutesBuilder(allUsersGroup) + .withEnabledUser(defaultUser) + .withTermsOfServiceHistoryForUser(defaultUser, TermsOfServiceHistory(List(record1, record2))) + + Get(s"/api/termsOfService/v1/user/${defaultUser.id}/history") ~> mockSamRoutesBuilder.build.route ~> check { + withClue(s"${responseAs[String]} is not parsable as an instance of `TermsOfServiceHistoryRecord`.") { + responseAs[ + String + ] shouldBe s"""{"history":[{"action":"${record1.action}","timestamp":"${record1.timestamp}","version":"${record1.version}"},{"action":"${record2.action}","timestamp":"${record2.timestamp}","version":"${record2.version}"}]}""" + responseAs[TermsOfServiceHistory] shouldBe TermsOfServiceHistory(List(record1, record2)) + } + status shouldEqual StatusCodes.OK + } + } + + it("should return 404 when user has no acceptance history") { + val allUsersGroup: BasicWorkbenchGroup = BasicWorkbenchGroup(CloudExtensions.allUsersGroupName, Set(), WorkbenchEmail("all_users@fake.com")) + val mockSamRoutesBuilder = new MockSamRoutesBuilder(allUsersGroup) + .withEnabledUser(Generator.genWorkbenchUserGoogle.sample.get) + + Get("/api/termsOfService/v1/user/12345abc/history") ~> mockSamRoutesBuilder.build.route ~> check { + status shouldEqual StatusCodes.NotFound + } + } + } + + describe("GET /api/termsOfService/v1/user/self/history") { + val samRoutes = TestSamRoutes(Map.empty) + it("should return a list of `TermsOfServiceHistoryRecord`") { + val allUsersGroup: BasicWorkbenchGroup = BasicWorkbenchGroup(CloudExtensions.allUsersGroupName, Set(), WorkbenchEmail("all_users@fake.com")) + val defaultUser: SamUser = Generator.genWorkbenchUserGoogle.sample.get + val record1 = TermsOfServiceHistoryRecord(TosTable.ACCEPT, "0", Instant.now) + val record2 = TermsOfServiceHistoryRecord(TosTable.REJECT, "0", Instant.now.minusSeconds(5)) + val mockSamRoutesBuilder = new MockSamRoutesBuilder(allUsersGroup) + .withEnabledUser(defaultUser) + .withTermsOfServiceHistoryForUser(defaultUser, TermsOfServiceHistory(List(record1, record2))) + + Get(s"/api/termsOfService/v1/user/self/history") ~> mockSamRoutesBuilder.build.route ~> check { + withClue(s"${responseAs[String]} is not parsable as an instance of `TermsOfServiceHistoryRecord`.") { + responseAs[ + String + ] shouldBe s"""{"history":[{"action":"${record1.action}","timestamp":"${record1.timestamp}","version":"${record1.version}"},{"action":"${record2.action}","timestamp":"${record2.timestamp}","version":"${record2.version}"}]}""" + responseAs[TermsOfServiceHistory] shouldBe TermsOfServiceHistory(List(record1, record2)) + } + status shouldEqual StatusCodes.OK + } + } + + it("should return 404 when user has no acceptance history") { + val allUsersGroup: BasicWorkbenchGroup = BasicWorkbenchGroup(CloudExtensions.allUsersGroupName, Set(), WorkbenchEmail("all_users@fake.com")) + val mockSamRoutesBuilder = new MockSamRoutesBuilder(allUsersGroup) + .withDisabledUser(Generator.genWorkbenchUserGoogle.sample.get) + + Get("/api/termsOfService/v1/user/self/history") ~> mockSamRoutesBuilder.build.route ~> check { + status shouldEqual StatusCodes.NotFound + } + } + } + it("should return 204 when tos accepted") { val samRoutes = TestSamRoutes(Map.empty) eventually { 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 b34e56bac..e94a28668 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 @@ -1,6 +1,5 @@ package org.broadinstitute.dsde.workbench.sam.dataAccess -import java.util.Date import akka.http.scaladsl.model.StatusCodes import cats.effect.IO import cats.implicits._ @@ -14,6 +13,7 @@ import org.broadinstitute.dsde.workbench.sam.model.{AccessPolicy, BasicWorkbench import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import java.time.Instant +import java.util.Date import scala.collection.concurrent.TrieMap import scala.collection.mutable @@ -22,7 +22,8 @@ import scala.collection.mutable class MockDirectoryDAO(val groups: mutable.Map[WorkbenchGroupIdentity, WorkbenchGroup] = new TrieMap(), passStatusCheck: Boolean = true) extends DirectoryDAO { private val groupSynchronizedDates: mutable.Map[WorkbenchGroupIdentity, Date] = new TrieMap() private val users: mutable.Map[WorkbenchUserId, SamUser] = new TrieMap() - private val userTos: mutable.Map[WorkbenchUserId, SamUserTos] = new TrieMap() + private val userTermsOfService: mutable.Map[WorkbenchUserId, SamUserTos] = new TrieMap() + private val userTermsOfServiceHistory: mutable.Map[WorkbenchUserId, List[SamUserTos]] = new TrieMap() private val userAttributes: mutable.Map[WorkbenchUserId, SamUserAttributes] = new TrieMap() private val usersWithEmails: mutable.Map[WorkbenchEmail, WorkbenchUserId] = new TrieMap() @@ -312,7 +313,9 @@ class MockDirectoryDAO(val groups: mutable.Map[WorkbenchGroupIdentity, Workbench case None => false case Some(user) => users.put(userId, user) - userTos.put(userId, SamUserTos(userId, tosVersion, TosTable.ACCEPT, Instant.now())) + userTermsOfService.put(userId, SamUserTos(userId, tosVersion, TosTable.ACCEPT, Instant.now())) + val userHistory = userTermsOfServiceHistory.getOrElse(userId, List.empty) + userTermsOfServiceHistory.put(userId, userHistory :+ SamUserTos(userId, tosVersion, TosTable.ACCEPT, Instant.now())) true } @@ -321,27 +324,43 @@ class MockDirectoryDAO(val groups: mutable.Map[WorkbenchGroupIdentity, Workbench case None => false case Some(user) => users.put(userId, user) - userTos.put(userId, SamUserTos(userId, tosVersion, TosTable.REJECT, Instant.now())) + userTermsOfService.put(userId, SamUserTos(userId, tosVersion, TosTable.REJECT, Instant.now())) + val userHistory = userTermsOfServiceHistory.getOrElse(userId, List.empty) + userTermsOfServiceHistory.put(userId, userHistory :+ SamUserTos(userId, tosVersion, TosTable.REJECT, Instant.now())) true } - override def getUserTos(userId: WorkbenchUserId, samRequestContext: SamRequestContext): IO[Option[SamUserTos]] = + override def getUserTermsOfService(userId: WorkbenchUserId, samRequestContext: SamRequestContext, action: Option[String]): IO[Option[SamUserTos]] = loadUser(userId, samRequestContext).map { case None => None case Some(_) => - userTos.get(userId) + if (action.isDefined) { + userTermsOfService.get(userId).filter(_.action == action.get) + } else + userTermsOfService.get(userId) } - override def getUserTosVersion(userId: WorkbenchUserId, tosVersion: Option[String], samRequestContext: SamRequestContext): IO[Option[SamUserTos]] = + override def getUserTermsOfServiceVersion( + userId: WorkbenchUserId, + tosVersion: Option[String], + samRequestContext: SamRequestContext, + action: Option[String] + ): IO[Option[SamUserTos]] = loadUser(userId, samRequestContext).map { case None => None case Some(_) => tosVersion match { - case Some(_) => userTos.get(userId) + case Some(_) => userTermsOfService.get(userId) case None => None } } + override def getUserTermsOfServiceHistory(userId: WorkbenchUserId, samRequestContext: SamRequestContext, limit: Integer): IO[List[SamUserTos]] = + loadUser(userId, samRequestContext).map { + case None => List.empty + case Some(_) => userTermsOfServiceHistory.getOrElse(userId, List.empty) + } + override def createPetManagedIdentity(petManagedIdentity: PetManagedIdentity, samRequestContext: SamRequestContext): IO[PetManagedIdentity] = { if (petManagedIdentitiesByUser.keySet.contains(petManagedIdentity.id)) { IO.raiseError(new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.Conflict, s"pet managed identity ${petManagedIdentity.id} already exists"))) diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockDirectoryDaoBuilder.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockDirectoryDaoBuilder.scala index c03d84ccd..ad8a09e40 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockDirectoryDaoBuilder.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/MockDirectoryDaoBuilder.scala @@ -3,8 +3,8 @@ package org.broadinstitute.dsde.workbench.sam.dataAccess import cats.effect.IO import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.sam.db.tables.TosTable -import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, SamUserTos} 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.util.SamRequestContext import org.mockito.ArgumentMatchersSugar.{any, eqTo} import org.mockito.{IdiomaticMockito, Strictness} @@ -41,7 +41,7 @@ case class MockDirectoryDaoBuilder() extends IdiomaticMockito { mockedDirectoryDAO.setUserRegisteredAt(any[WorkbenchUserId], any[Instant], any[SamRequestContext]) returns IO.unit mockedDirectoryDAO.setUserAttributes(any[SamUserAttributes], any[SamRequestContext]) returns IO.unit mockedDirectoryDAO.getUserAttributes(any[WorkbenchUserId], any[SamRequestContext]) returns IO(None) - mockedDirectoryDAO.getUserTosVersion(any[WorkbenchUserId], any[Some[String]], any[SamRequestContext]) returns IO(None) + mockedDirectoryDAO.getUserTermsOfServiceVersion(any[WorkbenchUserId], any[Some[String]], any[SamRequestContext]) returns IO(None) def withHealthyDatabase: MockDirectoryDaoBuilder = { mockedDirectoryDAO.checkStatus(any[SamRequestContext]) returns IO(true) @@ -116,16 +116,26 @@ case class MockDirectoryDaoBuilder() extends IdiomaticMockito { def withAcceptedTermsOfServiceForUser(samUser: SamUser, tosVersion: String): MockDirectoryDaoBuilder = { makeUserExist(samUser) val samUserTos = SamUserTos(samUser.id, tosVersion, TosTable.ACCEPT, Instant.now) - mockedDirectoryDAO.getUserTos(eqTo(samUser.id), any[SamRequestContext]) returns IO(Option(samUserTos)) - mockedDirectoryDAO.getUserTosVersion(eqTo(samUser.id), eqTo(Some(tosVersion)), any[SamRequestContext]) returns IO(Option(samUserTos)) + mockedDirectoryDAO.getUserTermsOfService(eqTo(samUser.id), any[SamRequestContext], any[Option[String]]) returns IO(Option(samUserTos)) + mockedDirectoryDAO.getUserTermsOfServiceVersion(eqTo(samUser.id), eqTo(Some(tosVersion)), any[SamRequestContext], any[Option[String]]) returns IO( + Option(samUserTos) + ) this } def withRejectedTermsOfServiceForUser(samUser: SamUser, tosVersion: String): MockDirectoryDaoBuilder = { makeUserExist(samUser) val samUserTos = SamUserTos(samUser.id, tosVersion, TosTable.REJECT, Instant.now) - mockedDirectoryDAO.getUserTos(eqTo(samUser.id), any[SamRequestContext]) returns IO(Option(samUserTos)) - mockedDirectoryDAO.getUserTosVersion(eqTo(samUser.id), eqTo(Some(tosVersion)), any[SamRequestContext]) returns IO(Option(samUserTos)) + mockedDirectoryDAO.getUserTermsOfService(eqTo(samUser.id), any[SamRequestContext]) returns IO(Option(samUserTos)) + mockedDirectoryDAO.getUserTermsOfServiceVersion(eqTo(samUser.id), eqTo(Some(tosVersion)), any[SamRequestContext]) returns IO(Option(samUserTos)) + this + } + + def withTermsOfServiceHistoryForUser(samUser: SamUser, tosHistory: List[SamUserTos]): MockDirectoryDaoBuilder = { + makeUserExist(samUser) + mockedDirectoryDAO.getUserTermsOfService(eqTo(samUser.id), any[SamRequestContext]) returns IO(Option(tosHistory.head)) + mockedDirectoryDAO.getUserTermsOfServiceVersion(eqTo(samUser.id), any[Option[String]], any[SamRequestContext]) returns IO(Option(tosHistory.head)) + mockedDirectoryDAO.getUserTermsOfServiceHistory(eqTo(samUser.id), any[SamRequestContext], any[Integer]) returns IO(tosHistory) this } diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAOSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAOSpec.scala index 9b52f29e5..5c5933625 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAOSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/PostgresDirectoryDAOSpec.scala @@ -1601,7 +1601,7 @@ class PostgresDirectoryDAOSpec extends RetryableAnyFreeSpec with Matchers with B dao.acceptTermsOfService(defaultUser.id, tosConfig.version, samRequestContext).unsafeRunSync() shouldBe true // Assert - val userTos = dao.getUserTos(defaultUser.id, samRequestContext).unsafeRunSync() + val userTos = dao.getUserTermsOfService(defaultUser.id, samRequestContext).unsafeRunSync() userTos should not be empty userTos.get.createdAt should beAround(Instant.now()) userTos.get.action shouldBe TosTable.ACCEPT @@ -1615,7 +1615,7 @@ class PostgresDirectoryDAOSpec extends RetryableAnyFreeSpec with Matchers with B dao.acceptTermsOfService(defaultUser.id, "2", samRequestContext).unsafeRunSync() shouldBe true // Assert - val userTos = dao.getUserTos(defaultUser.id, samRequestContext).unsafeRunSync() + val userTos = dao.getUserTermsOfService(defaultUser.id, samRequestContext).unsafeRunSync() userTos should not be empty userTos.get.createdAt should beAround(Instant.now()) userTos.get.action shouldBe TosTable.ACCEPT @@ -1631,7 +1631,7 @@ class PostgresDirectoryDAOSpec extends RetryableAnyFreeSpec with Matchers with B dao.rejectTermsOfService(user.id, tosConfig.version, samRequestContext).unsafeRunSync() shouldBe true // Assert - val userTos = dao.getUserTos(user.id, samRequestContext).unsafeRunSync() + val userTos = dao.getUserTermsOfService(user.id, samRequestContext).unsafeRunSync() userTos should not be empty userTos.get.createdAt should beAround(Instant.now()) userTos.get.action shouldBe TosTable.REJECT @@ -1646,7 +1646,7 @@ class PostgresDirectoryDAOSpec extends RetryableAnyFreeSpec with Matchers with B dao.rejectTermsOfService(user.id, tosConfig.version, samRequestContext).unsafeRunSync() shouldBe true // Assert - val userTos = dao.getUserTos(user.id, samRequestContext).unsafeRunSync() + val userTos = dao.getUserTermsOfService(user.id, samRequestContext).unsafeRunSync() userTos should not be empty userTos.get.createdAt should beAround(Instant.now()) userTos.get.action shouldBe TosTable.REJECT @@ -1661,9 +1661,21 @@ class PostgresDirectoryDAOSpec extends RetryableAnyFreeSpec with Matchers with B dao.createUser(user, samRequestContext).unsafeRunSync() // Assert - val userTos = dao.getUserTos(user.id, samRequestContext).unsafeRunSync() + val userTos = dao.getUserTermsOfService(user.id, samRequestContext).unsafeRunSync() userTos should be(None) } + "returns acceptances" in { + assume(databaseEnabled, databaseEnabledClue) + val user = Generator.genWorkbenchUserGoogle.sample.get + dao.createUser(user, samRequestContext).unsafeRunSync() + + dao.acceptTermsOfService(user.id, tosConfig.version, samRequestContext).unsafeRunSync() shouldBe true + dao.rejectTermsOfService(user.id, tosConfig.version, samRequestContext).unsafeRunSync() shouldBe true + + // Assert + val userTos = dao.getUserTermsOfService(user.id, samRequestContext, action = Option(TosTable.ACCEPT)).unsafeRunSync() + userTos should be(Some(SamUserTos(user.id, tosConfig.version, TosTable.ACCEPT, Instant.now()))) + } } "checkStatus" - { diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockTosServiceBuilder.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockTosServiceBuilder.scala index 82525a54d..370821aff 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockTosServiceBuilder.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockTosServiceBuilder.scala @@ -5,7 +5,7 @@ import cats.effect.IO import org.broadinstitute.dsde.workbench.model.{ErrorReport, ErrorReportSource, WorkbenchExceptionWithErrorReport, WorkbenchUserId} import org.broadinstitute.dsde.workbench.sam.db.tables.TosTable import org.broadinstitute.dsde.workbench.sam.model.api.SamUser -import org.broadinstitute.dsde.workbench.sam.model.{TermsOfServiceComplianceStatus, TermsOfServiceDetails} +import org.broadinstitute.dsde.workbench.sam.model.{TermsOfServiceComplianceStatus, TermsOfServiceDetails, TermsOfServiceHistory, TermsOfServiceHistoryRecord} import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.mockito.Mockito.{RETURNS_SMART_NULLS, lenient} import org.mockito.invocation.InvocationOnMock @@ -30,6 +30,14 @@ case class MockTosServiceBuilder() extends MockitoSugar { this } + def withTermsOfServiceHistoryForUser(samUser: SamUser, tosHistory: TermsOfServiceHistory): MockTosServiceBuilder = { + lenient() + .doReturn(IO.pure(tosHistory)) + .when(tosService) + .getTermsOfServiceHistoryForUser(ArgumentMatchers.eq(samUser.id), any[SamRequestContext], any[Integer]) + this + } + def withAcceptedStateForUser(samUser: SamUser, isAccepted: Boolean, version: String = "v1"): MockTosServiceBuilder = { setAcceptedStateForUserTo(samUser, isAccepted, version) this @@ -39,12 +47,16 @@ case class MockTosServiceBuilder() extends MockitoSugar { lenient() .doAnswer((i: InvocationOnMock) => IO.pure(TermsOfServiceComplianceStatus(i.getArgument[SamUser](0).id, isAccepted, isAccepted))) .when(tosService) - .getTosComplianceStatus(any[SamUser], any[SamRequestContext]) + .getTermsOfServiceComplianceStatus(any[SamUser], any[SamRequestContext]) lenient() .doReturn(IO.raiseError(new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.NotFound, s"")(new ErrorReportSource("MockTosServiceBuilder"))))) .when(tosService) .getTermsOfServiceDetailsForUser(any[WorkbenchUserId], any[SamRequestContext]) + lenient() + .doReturn(IO.raiseError(new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.NotFound, s"")(new ErrorReportSource("MockTosServiceBuilder"))))) + .when(tosService) + .getTermsOfServiceHistoryForUser(any[WorkbenchUserId], any[SamRequestContext], any[Integer]) } private def setAcceptedStateForUserTo(samUser: SamUser, isAccepted: Boolean, version: String) = { @@ -55,7 +67,7 @@ case class MockTosServiceBuilder() extends MockitoSugar { lenient() .doReturn(IO.pure(TermsOfServiceComplianceStatus(samUser.id, isAccepted, isAccepted))) .when(tosService) - .getTosComplianceStatus(ArgumentMatchers.argThat(matchesUser), any[SamRequestContext]) + .getTermsOfServiceComplianceStatus(ArgumentMatchers.argThat(matchesUser), any[SamRequestContext]) val action = if (isAccepted) TosTable.ACCEPT else TosTable.REJECT val rightNow = Instant.now @@ -63,6 +75,11 @@ case class MockTosServiceBuilder() extends MockitoSugar { .doReturn(IO.pure(TermsOfServiceDetails(version, rightNow, permitsSystemUsage = isAccepted, true))) .when(tosService) .getTermsOfServiceDetailsForUser(ArgumentMatchers.eq(samUser.id), any[SamRequestContext]) + + lenient() + .doReturn(IO.pure(TermsOfServiceHistory(List(TermsOfServiceHistoryRecord(action, version, rightNow))))) + .when(tosService) + .getTermsOfServiceHistoryForUser(ArgumentMatchers.eq(samUser.id), any[SamRequestContext], any[Integer]) } private def initializeDefaults(mockTosService: TosService): Unit = { diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/TosServiceSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/TosServiceSpec.scala index 0edc9cd1f..a0688a640 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/TosServiceSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/TosServiceSpec.scala @@ -11,7 +11,7 @@ import org.broadinstitute.dsde.workbench.sam.dataAccess.{DirectoryDAO, MockDirec import org.broadinstitute.dsde.workbench.sam.db.tables.TosTable import org.broadinstitute.dsde.workbench.sam.matchers.{TermsOfServiceDetailsMatchers, TimeMatchers} import org.broadinstitute.dsde.workbench.sam.model.api.TermsOfServiceConfigResponse -import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, SamUserTos, TermsOfServiceDetails} +import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, SamUserTos, TermsOfServiceDetails, TermsOfServiceHistory} import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.broadinstitute.dsde.workbench.sam.{Generator, PropertyBasedTesting, TestSupport} import org.mockito.Mockito.RETURNS_SMART_NULLS @@ -21,7 +21,6 @@ import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll, OptionValues} import java.time.Instant import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future class TosServiceSpec(_system: ActorSystem) extends TestKit(_system) @@ -152,11 +151,11 @@ class TosServiceSpec(_system: ActorSystem) val previousTosVersion = Option("1") val tosService = new TosService(NoExtensions, dirDAO, TestSupport.tosConfig.copy(version = tosVersion, previousVersion = previousTosVersion)) - when(dirDAO.getUserTos(serviceAccountUser.id, samRequestContext)).thenReturn(IO.pure(None)) + when(dirDAO.getUserTermsOfService(serviceAccountUser.id, samRequestContext)).thenReturn(IO.pure(None)) - when(dirDAO.getUserTosVersion(serviceAccountUser.id, previousTosVersion, samRequestContext)).thenReturn(IO.pure(None)) + when(dirDAO.getUserTermsOfServiceVersion(serviceAccountUser.id, previousTosVersion, samRequestContext)).thenReturn(IO.pure(None)) - val complianceStatus = tosService.getTosComplianceStatus(serviceAccountUser, samRequestContext).unsafeRunSync() + val complianceStatus = tosService.getTermsOfServiceComplianceStatus(serviceAccountUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe true } @@ -165,11 +164,11 @@ class TosServiceSpec(_system: ActorSystem) val previousTosVersion = Option("1") val tosService = new TosService(NoExtensions, dirDAO, TestSupport.tosConfig.copy(version = tosVersion, previousVersion = previousTosVersion)) - when(dirDAO.getUserTos(uamiUser.id, samRequestContext)).thenReturn(IO.pure(None)) + when(dirDAO.getUserTermsOfService(uamiUser.id, samRequestContext)).thenReturn(IO.pure(None)) - when(dirDAO.getUserTosVersion(uamiUser.id, previousTosVersion, samRequestContext)).thenReturn(IO.pure(None)) + when(dirDAO.getUserTermsOfServiceVersion(uamiUser.id, previousTosVersion, samRequestContext)).thenReturn(IO.pure(None)) - val complianceStatus = tosService.getTosComplianceStatus(uamiUser, samRequestContext).unsafeRunSync() + val complianceStatus = tosService.getTermsOfServiceComplianceStatus(uamiUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe true } } @@ -240,13 +239,13 @@ class TosServiceSpec(_system: ActorSystem) ) ) - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(None)) - when(dirDAO.getUserTosVersion(defaultUser.id, previousTosVersion, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousTosVersion, samRequestContext)) .thenReturn(IO.pure(None)) - val complianceStatus = tosService.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = tosService.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe false } } @@ -272,13 +271,13 @@ class TosServiceSpec(_system: ActorSystem) withoutGracePeriod - { withoutRollingAcceptanceWindow - { cannotUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(None)) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(None)) // CASE 1 val complianceStatus = - tosServiceV2GracePeriodDisabledAcceptanceWindowDisabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + tosServiceV2GracePeriodDisabledAcceptanceWindowDisabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe false } } @@ -286,12 +285,13 @@ class TosServiceSpec(_system: ActorSystem) withGracePeriod - { withoutRollingAcceptanceWindow - { cannotUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(None)) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(None)) // CASE 4 - val complianceStatus = tosServiceV2GracePeriodEnabledAcceptanceWindowDisabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = + tosServiceV2GracePeriodEnabledAcceptanceWindowDisabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe false } } @@ -299,12 +299,13 @@ class TosServiceSpec(_system: ActorSystem) withoutGracePeriod - { withRollingAcceptanceWindow - { cannotUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(None)) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(None)) // CASE 7 - val complianceStatus = tosServiceV2GracePeriodDisabledAcceptanceWindowEnabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = + tosServiceV2GracePeriodDisabledAcceptanceWindowEnabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe false } } @@ -312,12 +313,13 @@ class TosServiceSpec(_system: ActorSystem) withGracePeriod - { withRollingAcceptanceWindow - { cannotUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(None)) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(None)) // CASE 10 - val complianceStatus = tosServiceV2GracePeriodEnabledAcceptanceWindowEnabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = + tosServiceV2GracePeriodEnabledAcceptanceWindowEnabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe false } } @@ -327,13 +329,13 @@ class TosServiceSpec(_system: ActorSystem) withoutGracePeriod - { withoutRollingAcceptanceWindow - { cannotUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) // CASE 2 val complianceStatus = - tosServiceV2GracePeriodDisabledAcceptanceWindowDisabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + tosServiceV2GracePeriodDisabledAcceptanceWindowDisabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe false } } @@ -341,12 +343,13 @@ class TosServiceSpec(_system: ActorSystem) withGracePeriod - { withoutRollingAcceptanceWindow - { canUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) // CASE 5 - val complianceStatus = tosServiceV2GracePeriodEnabledAcceptanceWindowDisabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = + tosServiceV2GracePeriodEnabledAcceptanceWindowDisabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe true } } @@ -354,12 +357,13 @@ class TosServiceSpec(_system: ActorSystem) withoutGracePeriod - { withRollingAcceptanceWindow - { canUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) // CASE 8 - val complianceStatus = tosServiceV2GracePeriodDisabledAcceptanceWindowEnabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = + tosServiceV2GracePeriodDisabledAcceptanceWindowEnabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe true } } @@ -367,12 +371,13 @@ class TosServiceSpec(_system: ActorSystem) withGracePeriod - { withRollingAcceptanceWindow - { canUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) // CASE 11 - val complianceStatus = tosServiceV2GracePeriodEnabledAcceptanceWindowEnabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = + tosServiceV2GracePeriodEnabledAcceptanceWindowEnabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe true } } @@ -382,13 +387,13 @@ class TosServiceSpec(_system: ActorSystem) withoutGracePeriod - { withoutRollingAcceptanceWindow - { canUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, tosVersion, TosTable.ACCEPT, Instant.now())))) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) // CASE 3 val complianceStatus = - tosServiceV2GracePeriodDisabledAcceptanceWindowDisabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + tosServiceV2GracePeriodDisabledAcceptanceWindowDisabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe true } } @@ -396,12 +401,13 @@ class TosServiceSpec(_system: ActorSystem) withGracePeriod - { withoutRollingAcceptanceWindow - { canUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, tosVersion, TosTable.ACCEPT, Instant.now())))) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) // CASE 6 - val complianceStatus = tosServiceV2GracePeriodEnabledAcceptanceWindowDisabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = + tosServiceV2GracePeriodEnabledAcceptanceWindowDisabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe true } } @@ -409,12 +415,13 @@ class TosServiceSpec(_system: ActorSystem) withoutGracePeriod - { withRollingAcceptanceWindow - { canUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, tosVersion, TosTable.ACCEPT, Instant.now())))) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) // CASE 9 - val complianceStatus = tosServiceV2GracePeriodDisabledAcceptanceWindowEnabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = + tosServiceV2GracePeriodDisabledAcceptanceWindowEnabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe true } } @@ -422,12 +429,13 @@ class TosServiceSpec(_system: ActorSystem) withGracePeriod - { withRollingAcceptanceWindow - { canUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, tosVersion, TosTable.ACCEPT, Instant.now())))) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) // CASE 12 - val complianceStatus = tosServiceV2GracePeriodEnabledAcceptanceWindowEnabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = + tosServiceV2GracePeriodEnabledAcceptanceWindowEnabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe true } } @@ -438,13 +446,13 @@ class TosServiceSpec(_system: ActorSystem) withoutGracePeriod - { withoutRollingAcceptanceWindow - { cannotUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, tosVersion, TosTable.REJECT, Instant.now())))) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) val complianceStatus = - tosServiceV2GracePeriodDisabledAcceptanceWindowDisabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + tosServiceV2GracePeriodDisabledAcceptanceWindowDisabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe false } } @@ -452,12 +460,13 @@ class TosServiceSpec(_system: ActorSystem) withGracePeriod - { withoutRollingAcceptanceWindow - { cannotUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, tosVersion, TosTable.REJECT, Instant.now())))) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) - val complianceStatus = tosServiceV2GracePeriodEnabledAcceptanceWindowDisabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = + tosServiceV2GracePeriodEnabledAcceptanceWindowDisabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe false } } @@ -465,12 +474,13 @@ class TosServiceSpec(_system: ActorSystem) withoutGracePeriod - { withRollingAcceptanceWindow - { canUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, tosVersion, TosTable.REJECT, Instant.now())))) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) - val complianceStatus = tosServiceV2GracePeriodDisabledAcceptanceWindowEnabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = + tosServiceV2GracePeriodDisabledAcceptanceWindowEnabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe false } } @@ -478,12 +488,13 @@ class TosServiceSpec(_system: ActorSystem) withGracePeriod - { withRollingAcceptanceWindow - { cannotUseTheSystem in { - when(dirDAO.getUserTos(defaultUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(defaultUser.id, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, tosVersion, TosTable.REJECT, Instant.now())))) - when(dirDAO.getUserTosVersion(defaultUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(defaultUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(Option(SamUserTos(defaultUser.id, previousVersion, TosTable.ACCEPT, Instant.now())))) - val complianceStatus = tosServiceV2GracePeriodEnabledAcceptanceWindowEnabled.getTosComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() + val complianceStatus = + tosServiceV2GracePeriodEnabledAcceptanceWindowEnabled.getTermsOfServiceComplianceStatus(defaultUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe false } } @@ -492,12 +503,12 @@ class TosServiceSpec(_system: ActorSystem) "when a service account is using the api" - { "let it use the api regardless of tos status" in { - when(dirDAO.getUserTos(serviceAccountUser.id, samRequestContext)) + when(dirDAO.getUserTermsOfService(serviceAccountUser.id, samRequestContext)) .thenReturn(IO.pure(None)) - when(dirDAO.getUserTosVersion(serviceAccountUser.id, previousVersionOpt, samRequestContext)) + when(dirDAO.getUserTermsOfServiceVersion(serviceAccountUser.id, previousVersionOpt, samRequestContext)) .thenReturn(IO.pure(None)) val complianceStatus = - tosServiceV2GracePeriodDisabledAcceptanceWindowDisabled.getTosComplianceStatus(serviceAccountUser, samRequestContext).unsafeRunSync() + tosServiceV2GracePeriodDisabledAcceptanceWindowDisabled.getTermsOfServiceComplianceStatus(serviceAccountUser, samRequestContext).unsafeRunSync() complianceStatus.permitsSystemUsage shouldBe true } } @@ -511,13 +522,9 @@ class TosServiceSpec(_system: ActorSystem) .withAcceptedTermsOfServiceForUser(defaultUser, tosVersion) .build - val tosService = new TosService( - new NoExtensions { - override def isWorkbenchAdmin(memberEmail: WorkbenchEmail): Future[Boolean] = Future.successful(memberEmail == adminUser.email) - }, - directoryDao, - TestSupport.tosConfig - ) + val cloudExt = MockCloudExtensionsBuilder(allUsersGroup).withAdminUser().build + + val tosService = new TosService(cloudExt, directoryDao, TestSupport.tosConfig) // Act val userTosDetails: TermsOfServiceDetails = @@ -538,7 +545,9 @@ class TosServiceSpec(_system: ActorSystem) .withAcceptedTermsOfServiceForUser(defaultUser, tosVersion) .build - val tosService = new TosService(NoExtensions, directoryDao, TestSupport.tosConfig) + val cloudExt = MockCloudExtensionsBuilder(allUsersGroup).withNonAdminUser().build + + val tosService = new TosService(cloudExt, directoryDao, TestSupport.tosConfig) // Act val userTosDetails: TermsOfServiceDetails = @@ -574,5 +583,76 @@ class TosServiceSpec(_system: ActorSystem) assert(e.errorReport.statusCode.value == StatusCodes.Unauthorized, "User should not be authorized to see other users' Terms of Service details") } } + + "can retrieve Terms of Service history for a user" - { + "if the requesting user is an admin" in { + // Arrange + val tosVersion = "0" + val adminUser = Generator.genWorkbenchUserBoth.sample.get + val record1 = SamUserTos(adminUser.id, tosVersion, TosTable.ACCEPT, Instant.now()) + val record2 = SamUserTos(adminUser.id, tosVersion, TosTable.REJECT, Instant.now().minusSeconds(5)) + val directoryDao = new MockDirectoryDaoBuilder() + .withTermsOfServiceHistoryForUser(defaultUser, List(record1, record2)) + .build + val cloudExt = MockCloudExtensionsBuilder(allUsersGroup).withAdminUser().build + + val tosService = new TosService(cloudExt, directoryDao, TestSupport.tosConfig) + + // Act + val userTosDetails: TermsOfServiceHistory = + runAndWait(tosService.getTermsOfServiceHistoryForUser(defaultUser.id, SamRequestContext(None, None, Some(adminUser)), 5)) + + // Assert + userTosDetails.history.size shouldBe 2 + userTosDetails.history.head shouldBe record1.toHistoryRecord + userTosDetails.history.last shouldBe record2.toHistoryRecord + } + + "if the requesting user is not an admin but is the same as the requested user" in { + // Arrange + val tosVersion = "0" + val userTos1 = SamUserTos(defaultUser.id, tosVersion, TosTable.ACCEPT, Instant.now()) + val userTos2 = SamUserTos(defaultUser.id, tosVersion, TosTable.REJECT, Instant.now().minusSeconds(5)) + val directoryDao = new MockDirectoryDaoBuilder() + .withTermsOfServiceHistoryForUser(defaultUser, List(userTos1, userTos2)) + .build + + val cloudExt = MockCloudExtensionsBuilder(allUsersGroup).withNonAdminUser().build + + val tosService = new TosService(cloudExt, directoryDao, TestSupport.tosConfig) + + // Act + val userTosDetails: TermsOfServiceHistory = + runAndWait(tosService.getTermsOfServiceHistoryForUser(defaultUser.id, SamRequestContext(None, None, Some(defaultUser)), 5)) + + // Assert + userTosDetails.history.size shouldBe 2 + userTosDetails.history.head shouldBe userTos1.toHistoryRecord + userTosDetails.history.last shouldBe userTos2.toHistoryRecord + } + } + "cannot retrieve Terms of Service history for another user" - { + "if requesting user is not an admin and the requested user is a different user" in { + // Arrange + val tosVersion = "v1" + val nonAdminUser = Generator.genWorkbenchUserBoth.sample.get + val someRandoUser = Generator.genWorkbenchUserBoth.sample.get + val userTos1 = SamUserTos(someRandoUser.id, tosVersion, TosTable.ACCEPT, Instant.now()) + val userTos2 = SamUserTos(someRandoUser.id, tosVersion, TosTable.REJECT, Instant.now().minusSeconds(5)) + val directoryDao = new MockDirectoryDaoBuilder() + .withTermsOfServiceHistoryForUser(someRandoUser, List(userTos1, userTos2)) + .build + val cloudExt = MockCloudExtensionsBuilder(allUsersGroup).withNonAdminUser().build + + val tosService = new TosService(cloudExt, directoryDao, TestSupport.tosConfig) + + // Act and Assert + val e = intercept[WorkbenchExceptionWithErrorReport] { + runAndWait(tosService.getTermsOfServiceHistoryForUser(someRandoUser.id, SamRequestContext(None, None, Some(nonAdminUser)), 5)) + } + + assert(e.errorReport.statusCode.value == StatusCodes.Unauthorized, "User should not be authorized to see other users' Terms of Service details") + } + } } } diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpec.scala index 9d3b0896d..131f636d4 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpec.scala @@ -137,7 +137,7 @@ class OldUserServiceMockSpec(_system: ActorSystem) when(googleExtensions.onGroupUpdate(any[Seq[WorkbenchGroupIdentity]], any[SamRequestContext])).thenReturn(IO.unit) mockTosService = mock[TosService](RETURNS_SMART_NULLS) - when(mockTosService.getTosComplianceStatus(any[SamUser], any[SamRequestContext])) + when(mockTosService.getTermsOfServiceComplianceStatus(any[SamUser], any[SamRequestContext])) .thenAnswer((i: InvocationOnMock) => IO.pure(TermsOfServiceComplianceStatus(i.getArgument[SamUser](0).id, true, true))) service = Mockito.spy(new UserService(dirDAO, googleExtensions, Seq(blockedDomain), mockTosService)) @@ -168,7 +168,7 @@ class OldUserServiceMockSpec(_system: ActorSystem) } it should "return UserStatusDiagnostics.tosAccepted as false if user's TOS status is false" in { - when(mockTosService.getTosComplianceStatus(enabledUser, samRequestContext)) + when(mockTosService.getTermsOfServiceComplianceStatus(enabledUser, samRequestContext)) .thenReturn(IO.pure(TermsOfServiceComplianceStatus(enabledUser.id, false, false))) val status = service.getUserStatusDiagnostics(enabledUser.id, samRequestContext).unsafeRunSync() status.value.tosAccepted shouldBe false