diff --git a/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/sam/MockTestSupport.scala b/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/sam/MockTestSupport.scala index 12c217041..c10db52af 100644 --- a/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/sam/MockTestSupport.scala +++ b/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/sam/MockTestSupport.scala @@ -28,6 +28,7 @@ import org.broadinstitute.dsde.workbench.sam.db.TestDbReference import org.broadinstitute.dsde.workbench.sam.db.tables._ import org.broadinstitute.dsde.workbench.sam.google.{GoogleExtensionRoutes, GoogleExtensions, GoogleGroupSynchronizer, GoogleKeyCache} import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.UserService._ import org.broadinstitute.dsde.workbench.sam.service._ import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext diff --git a/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/sam/provider/SamProviderSpec.scala b/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/sam/provider/SamProviderSpec.scala index 52fd70079..a883cf9bc 100644 --- a/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/sam/provider/SamProviderSpec.scala +++ b/pact4s/src/test/scala/org/broadinstitute/dsde/workbench/sam/provider/SamProviderSpec.scala @@ -14,6 +14,7 @@ import org.broadinstitute.dsde.workbench.sam.azure.AzureService import org.broadinstitute.dsde.workbench.sam.dataAccess.{AccessPolicyDAO, DirectoryDAO, StatefulMockAccessPolicyDaoBuilder} import org.broadinstitute.dsde.workbench.sam.google.GoogleExtensions import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service._ import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.broadinstitute.dsde.workbench.sam.{Generator, MockSamDependencies, MockTestSupport} diff --git a/src/main/resources/swagger/api-docs.yaml b/src/main/resources/swagger/api-docs.yaml index c53d56e48..b7242cdb7 100755 --- a/src/main/resources/swagger/api-docs.yaml +++ b/src/main/resources/swagger/api-docs.yaml @@ -2793,6 +2793,102 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorReport' + /api/users/v2/self: + get: + tags: + - Users + summary: gets the user + operationId: getSamUserSelf + responses: + 200: + description: user exists + content: + application/json: + schema: + $ref: '#/components/schemas/SamUserResponse' + 404: + description: user not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorReport' + /api/users/v2/self/allowed: + get: + tags: + - Users + summary: gets the user allowances + operationId: getSamUserSelfAllowances + responses: + 200: + description: user exists + content: + application/json: + schema: + $ref: '#/components/schemas/SamUserAllowances' + 404: + description: user not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorReport' + /api/users/v2/{sam_user_id}: + get: + tags: + - Admin + summary: gets a user + description: Gets a SamUser by their id. This endpoint is scoped to the permissions of the caller. + A normal user can call the endpoint with their own id, but trying to get another user will result in a 404. + Admin permissions grant the caller the ability to get any id. + operationId: getSamUserById + parameters: + - name: sam_user_id + in: path + description: the id of the sam user to get + required: true + schema: + type: string + responses: + 200: + description: user exists + content: + application/json: + schema: + $ref: '#/components/schemas/SamUserResponse' + 404: + description: user not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorReport' + /api/users/v2/{sam_user_id}/allowed: + get: + tags: + - Admin + summary: gets a user + description: Gets a users allowances by their id. This endpoint is scoped to the permissions of the caller. + A normal user can call the endpoint with their own id, but trying to get another user will result in a 404. + Admin permissions grant the caller the ability to get any id. + operationId: getSamUserAllowancesById + parameters: + - name: sam_user_id + in: path + description: the id of the sam user to get + required: true + schema: + type: string + responses: + 200: + description: user exists + content: + application/json: + schema: + $ref: '#/components/schemas/SamUserAllowances' + 404: + description: user not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorReport' /register/user/v1: get: tags: @@ -3799,6 +3895,53 @@ components: format: date-time description: User's time of last update description: specification of a User + SamUserResponse: + type: object + required: + - id + - email + - allowed + - createdAt + - updatedAt + properties: + id: + type: string + description: User's Id + googleSubjectId: + type: string + description: User's Google subject Id + email: + type: string + description: User's email address + format: email + azureB2CId: + type: string + description: User's Azure B2C Id + allowed: + type: boolean + description: Whether or not the user allowed to use the system + createdAt: + type: string + format: date-time + description: User's time of creation + registeredAt: + type: string + format: date-time + description: User's time of registration + updatedAt: + type: string + format: date-time + description: User's time of last update + description: specification of a User + SamUserAllowances: + type: object + properties: + allowed: + type: boolean + description: is the user allowed to use the system + details: + type: object + description: details of the user's allowances UpdateUserRequest: type: object properties: diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/AdminRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/AdminRoutes.scala index 4730ee170..fce64b8d6 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/AdminRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/AdminRoutes.scala @@ -9,11 +9,11 @@ import akka.http.scaladsl.server.Directives._ import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.sam.config.LiquibaseConfig -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model.SamResourceActions.{adminAddMember, adminReadPolicies, adminRemoveMember} import org.broadinstitute.dsde.workbench.sam.model.SamResourceTypes.resourceTypeAdminName import org.broadinstitute.dsde.workbench.sam.model._ -import org.broadinstitute.dsde.workbench.sam.model.api.{AccessPolicyMembershipRequest, AdminUpdateUserRequest} +import org.broadinstitute.dsde.workbench.sam.model.api.{AccessPolicyMembershipRequest, AdminUpdateUserRequest, SamUser} import org.broadinstitute.dsde.workbench.sam.service.ResourceService import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import spray.json.DefaultJsonProtocol._ diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ExtensionRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ExtensionRoutes.scala index 57a49f889..b89d75d45 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ExtensionRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ExtensionRoutes.scala @@ -1,7 +1,7 @@ package org.broadinstitute.dsde.workbench.sam.api import akka.http.scaladsl.server -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext trait ExtensionRoutes { diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ManagedGroupRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ManagedGroupRoutes.scala index c1d35d0b9..7fd7d4b8e 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ManagedGroupRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ManagedGroupRoutes.scala @@ -10,7 +10,8 @@ import org.broadinstitute.dsde.workbench.model.WorkbenchIdentityJsonSupport._ import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.sam._ import org.broadinstitute.dsde.workbench.sam.model.{ResourceId, _} -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.ManagedGroupService import org.broadinstitute.dsde.workbench.sam.service.ManagedGroupService.ManagedGroupPolicyName import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutes.scala similarity index 82% rename from src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutes.scala rename to src/main/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutes.scala index 0a9fbe4cc..60e8492c1 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutes.scala @@ -7,8 +7,7 @@ import akka.http.scaladsl.server import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.{Directive0, ExceptionHandler} import org.broadinstitute.dsde.workbench.model._ -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.service.UserService import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import spray.json.JsBoolean @@ -17,7 +16,7 @@ import scala.concurrent.ExecutionContext /** Created by mbemis on 5/22/17. */ -trait UserRoutes extends SamUserDirectives with SamRequestContextDirectives { +trait OldUserRoutes extends SamUserDirectives with SamRequestContextDirectives { implicit val executionContext: ExecutionContext val userService: UserService @@ -33,7 +32,7 @@ trait UserRoutes extends SamUserDirectives with SamRequestContextDirectives { }) } - def userRoutes(samRequestContext: SamRequestContext): server.Route = + def oldUserRoutes(samRequestContext: SamRequestContext): server.Route = pathPrefix("user") { (pathPrefix("v1") | pathEndOrSingleSlash) { pathEndOrSingleSlash { @@ -151,33 +150,4 @@ trait UserRoutes extends SamUserDirectives with SamRequestContextDirectives { } } } - - def apiUserRoutes(samUser: SamUser, samRequestContext: SamRequestContext): server.Route = pathPrefix("users") { - pathPrefix("v1") { - get { - path(Segment) { email => - pathEnd { - complete { - userService.getUserIdInfoFromEmail(WorkbenchEmail(email), samRequestContext).map { - case Left(_) => StatusCodes.NotFound -> None - case Right(None) => StatusCodes.NoContent -> None - case Right(Some(userIdInfo)) => StatusCodes.OK -> Some(userIdInfo) - } - } - } - } - } ~ - pathPrefix("invite") { - post { - path(Segment) { inviteeEmail => - complete { - userService - .inviteUser(WorkbenchEmail(inviteeEmail.trim), samRequestContext) - .map(userStatus => StatusCodes.Created -> userStatus) - } - } - } - } - } - } } diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutes.scala index fbb0339b0..d566b74b5 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutes.scala @@ -11,9 +11,9 @@ import org.broadinstitute.dsde.workbench.model.WorkbenchIdentityJsonSupport._ import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.sam.config.LiquibaseConfig import org.broadinstitute.dsde.workbench.sam.model.RootPrimitiveJsonSupport._ -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ -import org.broadinstitute.dsde.workbench.sam.model.api.AccessPolicyMembershipRequest +import org.broadinstitute.dsde.workbench.sam.model.api.{AccessPolicyMembershipRequest, SamUser} import org.broadinstitute.dsde.workbench.sam.service.ResourceService import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import spray.json.DefaultJsonProtocol._ diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SamRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SamRoutes.scala index f4128dcd9..c57a18edb 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SamRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SamRoutes.scala @@ -48,14 +48,16 @@ abstract class SamRoutes( val openTelemetry: OpenTelemetryMetrics[IO] ) extends LazyLogging with ResourceRoutes - with UserRoutes + with OldUserRoutes with StatusRoutes with TermsOfServiceRoutes with ExtensionRoutes with ManagedGroupRoutes with AdminRoutes with AzureRoutes - with ServiceAdminRoutes { + with ServiceAdminRoutes + with UserRoutesV1 + with UserRoutesV2 { def route: server.Route = (logRequestResult & handleExceptions(myExceptionHandler)) { oidcConfig.swaggerRoutes("swagger/api-docs.yaml") ~ @@ -64,19 +66,20 @@ abstract class SamRoutes( termsOfServiceRoutes ~ withExecutionContext(ExecutionContext.global) { withSamRequestContext { samRequestContext => - pathPrefix("register")(userRoutes(samRequestContext)) ~ + pathPrefix("register")(oldUserRoutes(samRequestContext)) ~ pathPrefix("api") { // these routes are for machine to machine authorized requests // the whitelisted service admin account email is in the header of the request serviceAdminRoutes(samRequestContext) ~ + userRoutesV2(samRequestContext) ~ withActiveUser(samRequestContext) { samUser => val samRequestContextWithUser = samRequestContext.copy(samUser = Option(samUser)) resourceRoutes(samUser, samRequestContextWithUser) ~ adminRoutes(samUser, samRequestContextWithUser) ~ extensionRoutes(samUser, samRequestContextWithUser) ~ groupRoutes(samUser, samRequestContextWithUser) ~ - apiUserRoutes(samUser, samRequestContextWithUser) ~ - azureRoutes(samUser, samRequestContextWithUser) + azureRoutes(samUser, samRequestContextWithUser) ~ + userRoutesV1(samUser, samRequestContextWithUser) } } } diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SamUserDirectives.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SamUserDirectives.scala index 5f59fe130..c48253b6f 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SamUserDirectives.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/SamUserDirectives.scala @@ -10,8 +10,9 @@ import org.broadinstitute.dsde.workbench.sam._ import org.broadinstitute.dsde.workbench.sam.api.RejectionHandlers.termsOfServiceRejectionHandler import org.broadinstitute.dsde.workbench.sam.config.AppConfig.AdminConfig import org.broadinstitute.dsde.workbench.sam.config.TermsOfServiceConfig -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ -import org.broadinstitute.dsde.workbench.sam.model.{SamUser, TermsOfServiceAcceptance} +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.TermsOfServiceAcceptance +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext /** Directives to get user information. diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ServiceAdminRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ServiceAdminRoutes.scala index ccc3d83f8..adb7c99b5 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ServiceAdminRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/ServiceAdminRoutes.scala @@ -5,7 +5,7 @@ import akka.http.scaladsl.model.StatusCodes.{NotFound, OK} import akka.http.scaladsl.server import akka.http.scaladsl.server.Directives._ import org.broadinstitute.dsde.workbench.model._ -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.service.ResourceService import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import spray.json.DefaultJsonProtocol._ diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/StandardSamUserDirectives.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/StandardSamUserDirectives.scala index 4cdd6034d..36108ff31 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/StandardSamUserDirectives.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/StandardSamUserDirectives.scala @@ -13,9 +13,9 @@ import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.model.google.ServiceAccountSubjectId import org.broadinstitute.dsde.workbench.sam.api.StandardSamUserDirectives._ import org.broadinstitute.dsde.workbench.sam.azure.ManagedIdentityObjectId -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.UserService._ -import org.broadinstitute.dsde.workbench.sam.service.{TosService, UserService} +import org.broadinstitute.dsde.workbench.sam.service.UserService import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import scala.concurrent.ExecutionContext @@ -27,7 +27,7 @@ trait StandardSamUserDirectives extends SamUserDirectives with LazyLogging with def withActiveUser(samRequestContext: SamRequestContext): Directive1[SamUser] = requireOidcHeaders.flatMap { oidcHeaders => onSuccess { - getActiveSamUser(oidcHeaders, userService, tosService, samRequestContext).unsafeToFuture() + getActiveSamUser(oidcHeaders, userService, samRequestContext).unsafeToFuture() }.tmap { samUser => logger.debug(s"Handling request for active Sam User: $samUser") samUser @@ -119,15 +119,15 @@ object StandardSamUserDirectives { loadUserMaybeUpdateAzureB2CId(azureB2CId, oidcHeaders.googleSubjectIdFromAzure, userService, samRequestContext) } - def getActiveSamUser(oidcHeaders: OIDCHeaders, userService: UserService, tosService: TosService, samRequestContext: SamRequestContext): IO[SamUser] = + def getActiveSamUser(oidcHeaders: OIDCHeaders, userService: UserService, samRequestContext: SamRequestContext): IO[SamUser] = for { user <- getSamUser(oidcHeaders, userService, samRequestContext) - tosComplianceDetails <- tosService.getTosComplianceStatus(user, samRequestContext) + allowances <- userService.getUserAllowances(user, samRequestContext) } yield { - if (!tosComplianceDetails.permitsSystemUsage) { + if (!allowances.getTermsOfServiceCompliance) { throw new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.Unauthorized, "User must accept the latest terms of service.")) } - if (!user.enabled) { + if (!allowances.getEnabled) { throw new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.Unauthorized, "User is disabled.")) } diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV1.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV1.scala new file mode 100644 index 000000000..fc6da229f --- /dev/null +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV1.scala @@ -0,0 +1,45 @@ +package org.broadinstitute.dsde.workbench.sam.api + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.server +import akka.http.scaladsl.server.Directives._ +import org.broadinstitute.dsde.workbench.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser +import org.broadinstitute.dsde.workbench.sam.service.UserService +import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext + +trait UserRoutesV1 extends SamUserDirectives with SamRequestContextDirectives { + val userService: UserService + + def userRoutesV1(samUser: SamUser, samRequestContext: SamRequestContext): server.Route = pathPrefix("users") { + pathPrefix("v1") { + get { + path(Segment) { email => + pathEnd { + complete { + userService.getUserIdInfoFromEmail(WorkbenchEmail(email), samRequestContext).map { + case Left(_) => StatusCodes.NotFound -> None + case Right(None) => StatusCodes.NoContent -> None + case Right(Some(userIdInfo)) => StatusCodes.OK -> Some(userIdInfo) + } + } + } + } + } ~ + pathPrefix("invite") { + post { + path(Segment) { inviteeEmail => + complete { + userService + .inviteUser(WorkbenchEmail(inviteeEmail.trim), samRequestContext) + .map(userStatus => StatusCodes.Created -> userStatus) + } + } + } + } + } + } + +} diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2.scala new file mode 100644 index 000000000..057b9952e --- /dev/null +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2.scala @@ -0,0 +1,126 @@ +package org.broadinstitute.dsde.workbench.sam.api + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.StatusCodes.{NotFound, OK} +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.{Directive0, ExceptionHandler, Route} +import cats.effect.IO +import org.broadinstitute.dsde.workbench.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUserResponse._ +import org.broadinstitute.dsde.workbench.sam.model.api.{SamUser, SamUserResponse} +import org.broadinstitute.dsde.workbench.sam.service.UserService +import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext + +/** Created by tlangs on 10/12/2023. + */ +trait UserRoutesV2 extends SamUserDirectives with SamRequestContextDirectives { + val userService: UserService + + /** Changes a 403 error to a 404 error. Used when `UserInfoDirectives` throws a 403 in the case where a user is not found. In most routes that is appropriate + * but in the user routes it should be a 404. + */ + private val changeForbiddenToNotFound: Directive0 = { + import org.broadinstitute.dsde.workbench.model.ErrorReportJsonSupport._ + + handleExceptions(ExceptionHandler { + case withErrorReport: WorkbenchExceptionWithErrorReport if withErrorReport.errorReport.statusCode.contains(StatusCodes.Forbidden) => + complete((StatusCodes.NotFound, withErrorReport.errorReport.copy(statusCode = Option(StatusCodes.NotFound)))) + }) + } + + // These routes are wrapped in `withUserAllowInactive` because a user should be able to get info about themselves + // Routes that need the user to be active should be wrapped in a directive, such as `withActiveUser`, to ensure + // that the user is allowed to use the system. + def userRoutesV2(samRequestContextWithoutUser: SamRequestContext): Route = + withUserAllowInactive(samRequestContextWithoutUser) { samUser: SamUser => + val samRequestContext = samRequestContextWithoutUser.copy(samUser = Some(samUser)) + pathPrefix("users") { + pathPrefix("v2") { + pathPrefix("self") { + // api/users/v2/self + pathEndOrSingleSlash { + getSamUserResponse(samUser, samRequestContext) + } ~ + // api/users/v2/self/allowed + pathPrefix("allowed") { + pathEndOrSingleSlash { + getSamUserAllowances(samUser, samRequestContext) + } + } + } ~ + pathPrefix(Segment) { samUserId => + val workbenchUserId = WorkbenchUserId(samUserId) + // api/users/v2/{sam_user_id} + pathEndOrSingleSlash { + regularUserOrAdmin(samUser, workbenchUserId, samRequestContext)(getSamUserResponse)(getAdminSamUserResponse) + } ~ + // api/users/v2/{sam_user_id}/allowed + pathPrefix("allowed") { + pathEndOrSingleSlash { + regularUserOrAdmin(samUser, workbenchUserId, samRequestContext)(getSamUserAllowances)(getAdminSamUserAllowances) + } + } + } + } + } + } + + private def regularUserOrAdmin(callingUser: SamUser, requestedUserId: WorkbenchUserId, samRequestContext: SamRequestContext)( + asRegular: (SamUser, SamRequestContext) => Route + )(asAdmin: (WorkbenchUserId, SamRequestContext) => Route): Route = + if (callingUser.id.equals(requestedUserId)) { + asRegular(callingUser, samRequestContext) + } else { + (changeForbiddenToNotFound & asWorkbenchAdmin(callingUser)) { + asAdmin(requestedUserId, samRequestContext) + } + } + + // Get Sam User + private def getAdminSamUserResponse(samUserId: WorkbenchUserId, samRequestContext: SamRequestContext): Route = + get { + complete { + for { + user <- userService.getUser(samUserId, samRequestContext) + response <- user match { + case Some(value) => samUserResponse(value, samRequestContext).map(Some(_)) + case None => IO(None) + } + } yield (if (response.isDefined) OK else NotFound) -> response + } + } + + private def getSamUserResponse(samUser: SamUser, samRequestContext: SamRequestContext): Route = + get { + complete { + samUserResponse(samUser, samRequestContext).map(response => StatusCodes.OK -> response) + } + } + private def samUserResponse(samUser: SamUser, samRequestContext: SamRequestContext): IO[SamUserResponse] = + for { + allowances <- userService.getUserAllowances(samUser, samRequestContext) + } yield SamUserResponse(samUser, allowances.allowed) + + // Get Sam User Allowed + private def getSamUserAllowances(samUser: SamUser, samRequestContext: SamRequestContext): Route = + get { + complete { + userService.getUserAllowances(samUser, samRequestContext).map(StatusCodes.OK -> _) + } + } + + private def getAdminSamUserAllowances(samUserId: WorkbenchUserId, samRequestContext: SamRequestContext): Route = + get { + complete { + for { + user <- userService.getUser(samUserId, samRequestContext) + response <- user match { + case Some(value) => userService.getUserAllowances(value, samRequestContext).map(Some(_)) + case None => IO(None) + } + } yield (if (response.isDefined) OK else NotFound) -> response + } + } + +} diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/audit/SamAuditModel.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/audit/SamAuditModel.scala index 3bec90aff..335b0e963 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/audit/SamAuditModel.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/audit/SamAuditModel.scala @@ -41,7 +41,7 @@ final case class AccessChangeEvent(eventType: AccessChangeEventType, resource: F object SamAuditModelJsonSupport { import DefaultJsonProtocol._ import org.broadinstitute.dsde.workbench.model.WorkbenchIdentityJsonSupport._ - import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ + import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ implicit object InetAddressFormat extends RootJsonFormat[InetAddress] { def write(ip: InetAddress) = JsString(ip.getHostAddress) diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/azure/AzureRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/azure/AzureRoutes.scala index 054a179d9..f1c53b321 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/azure/AzureRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/azure/AzureRoutes.scala @@ -12,6 +12,7 @@ import org.broadinstitute.dsde.workbench.sam._ import org.broadinstitute.dsde.workbench.sam.api.{SecurityDirectives, _} import org.broadinstitute.dsde.workbench.sam.azure.AzureJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.CloudExtensions import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import spray.json.JsString diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/azure/AzureService.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/azure/AzureService.scala index c92202e55..c4b20c2ab 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/azure/AzureService.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/azure/AzureService.scala @@ -14,7 +14,7 @@ import org.broadinstitute.dsde.workbench.model.{ErrorReport, WorkbenchEmail, Wor import org.broadinstitute.dsde.workbench.sam._ import org.broadinstitute.dsde.workbench.sam.config.ManagedAppPlan import org.broadinstitute.dsde.workbench.sam.dataAccess.{AzureManagedResourceGroupDAO, DirectoryDAO} -import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.util.OpenCensusIOUtils._ import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/AccessPolicyDAO.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/AccessPolicyDAO.scala index 127842aa6..0d49e8ee6 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/AccessPolicyDAO.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/AccessPolicyDAO.scala @@ -3,7 +3,7 @@ package org.broadinstitute.dsde.workbench.sam.dataAccess import cats.data.NonEmptyList import cats.effect.IO import org.broadinstitute.dsde.workbench.model._ -import org.broadinstitute.dsde.workbench.sam.model.api.AccessPolicyMembershipResponse +import org.broadinstitute.dsde.workbench.sam.model.api.{AccessPolicyMembershipResponse, SamUser} import org.broadinstitute.dsde.workbench.sam.model.{FullyQualifiedResourceId, _} import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext 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 910133d99..2f6fda99f 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 @@ -4,7 +4,8 @@ import cats.effect.IO 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.{BasicWorkbenchGroup, SamUser, SamUserTos} +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser +import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, SamUserTos} import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import java.time.Instant 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 726e9bde3..1f2acadb1 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 @@ -12,6 +12,7 @@ import org.broadinstitute.dsde.workbench.sam.db.SamTypeBinders._ import org.broadinstitute.dsde.workbench.sam.db._ import org.broadinstitute.dsde.workbench.sam.db.tables._ import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.util.{DatabaseSupport, SamRequestContext} import org.postgresql.util.PSQLException import scalikejdbc._ diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/db/tables/UserTable.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/db/tables/UserTable.scala index 5e30bd33e..95079b395 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/db/tables/UserTable.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/db/tables/UserTable.scala @@ -2,7 +2,7 @@ package org.broadinstitute.dsde.workbench.sam.db.tables import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.sam.db.SamTypeBinders -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import scalikejdbc._ import java.time.Instant diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensionRoutes.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensionRoutes.scala index 3ac13f953..c031fdfa7 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensionRoutes.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensionRoutes.scala @@ -17,8 +17,9 @@ import org.broadinstitute.dsde.workbench.sam.api.{ SecurityDirectives, ioMarshaller } -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.CloudExtensions import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import spray.json.DefaultJsonProtocol._ diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensions.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensions.scala index 775039eba..c6a7beedf 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensions.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensions.scala @@ -25,8 +25,9 @@ import org.broadinstitute.dsde.workbench.model.google._ import org.broadinstitute.dsde.workbench.sam._ import org.broadinstitute.dsde.workbench.sam.config.{GoogleServicesConfig, PetServiceAccountConfig} import org.broadinstitute.dsde.workbench.sam.dataAccess.{AccessPolicyDAO, DirectoryDAO, LockDetails, PostgresDistributedLockDAO} -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.UserService._ import org.broadinstitute.dsde.workbench.sam.service.{CloudExtensions, CloudExtensionsInitializer, ManagedGroupService, SamApplication} import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleGroupSyncMessageReceiver.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleGroupSyncMessageReceiver.scala index f6f8e85c0..de091ebb6 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleGroupSyncMessageReceiver.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleGroupSyncMessageReceiver.scala @@ -78,7 +78,7 @@ class GoogleGroupSyncMessageReceiver(groupSynchronizer: GoogleGroupSynchronizer) private[google] def parseMessage(message: PubsubMessage): WorkbenchGroupIdentity = { val messageJson = message.getData.toStringUtf8.parseJson (Try { - import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport.FullyQualifiedPolicyIdFormat + import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport.FullyQualifiedPolicyIdFormat messageJson.convertTo[FullyQualifiedPolicyId] } recover { case _: DeserializationException => import WorkbenchIdentityJsonSupport.WorkbenchGroupNameFormat 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 1a2b5c373..6d6a35bdb 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 @@ -2,9 +2,7 @@ package org.broadinstitute.dsde.workbench.sam.model import monocle.macros.Lenses import org.broadinstitute.dsde.workbench.model._ -import org.broadinstitute.dsde.workbench.model.google.GoogleModelJsonSupport.InstantFormat -import org.broadinstitute.dsde.workbench.sam.model.api.SamApiJsonProtocol.PolicyInfoResponseBodyJsonFormat -import org.broadinstitute.dsde.workbench.sam.model.api.{AccessPolicyMembershipRequest, AccessPolicyMembershipResponse, AdminUpdateUserRequest} +import org.broadinstitute.dsde.workbench.sam.model.api.{AccessPolicyMembershipRequest, AccessPolicyMembershipResponse, SamUser} import org.broadinstitute.dsde.workbench.sam.service.ManagedGroupService.MangedGroupRoleName import spray.json.{DefaultJsonProtocol, JsValue, RootJsonFormat} @@ -12,82 +10,6 @@ import java.time.Instant /** Created by dvoet on 5/26/17. */ -object SamJsonSupport { - import DefaultJsonProtocol._ - import org.broadinstitute.dsde.workbench.model.WorkbenchIdentityJsonSupport._ - - implicit val ResourceActionPatternFormat = jsonFormat3(ResourceActionPattern.apply) - - implicit val ResourceActionFormat = ValueObjectFormat(ResourceAction.apply) - - implicit val ResourceRoleNameFormat = ValueObjectFormat(ResourceRoleName.apply) - - implicit val ResourceTypeNameFormat = ValueObjectFormat(ResourceTypeName.apply) - - implicit val ResourceRoleFormat = jsonFormat4(ResourceRole.apply) - - implicit val ResourceTypeFormat = jsonFormat6(ResourceType.apply) - - implicit val SamUserFormat = jsonFormat8(SamUser.apply) - - implicit val UserStatusDetailsFormat = jsonFormat2(UserStatusDetails.apply) - - implicit val UserStatusFormat = jsonFormat2(UserStatus.apply) - - implicit val UserStatusInfoFormat = jsonFormat4(UserStatusInfo.apply) - - implicit val UserIdInfoFormat = jsonFormat3(UserIdInfo.apply) - - implicit val TermsOfServiceAcceptanceFormat = ValueObjectFormat(TermsOfServiceAcceptance.apply) - - implicit val termsOfServiceDetailsFormat = jsonFormat4(TermsOfServiceDetails.apply) - - implicit val termsOfAcceptanceStatusFormat = jsonFormat3(TermsOfServiceComplianceStatus.apply) - - implicit val UserStatusDiagnosticsFormat = jsonFormat5(UserStatusDiagnostics.apply) - - implicit val AccessPolicyNameFormat = ValueObjectFormat(AccessPolicyName.apply) - - implicit val ResourceIdFormat = ValueObjectFormat(ResourceId.apply) - - implicit val FullyQualifiedResourceIdFormat = jsonFormat2(FullyQualifiedResourceId.apply) - - implicit val AccessPolicyDescendantPermissionsFormat: RootJsonFormat[AccessPolicyDescendantPermissions] = jsonFormat3(AccessPolicyDescendantPermissions.apply) - - implicit val PolicyIdentifiersFormat = jsonFormat3(PolicyIdentifiers.apply) - - implicit val AccessPolicyMembershipResponseFormat = jsonFormat5(AccessPolicyMembershipResponse.apply) - - implicit val AccessPolicyResponseEntryFormat = jsonFormat3(AccessPolicyResponseEntry.apply) - - implicit val UserPolicyResponseFormat = jsonFormat5(UserPolicyResponse.apply) - - implicit val UserUpdateRequestFormat = jsonFormat3(AdminUpdateUserRequest.apply) - - implicit val RolesAndActionsFormat = jsonFormat2(RolesAndActions.apply) - - implicit val UserResourcesResponseFormat = jsonFormat6(UserResourcesResponse.apply) - - implicit val FullyQualifiedPolicyIdFormat = jsonFormat2(FullyQualifiedPolicyId.apply) - - implicit val ManagedGroupMembershipEntryFormat = jsonFormat3(ManagedGroupMembershipEntry.apply) - - implicit val ManagedGroupAccessInstructionsFormat = ValueObjectFormat(ManagedGroupAccessInstructions.apply) - - implicit val GroupSyncResponseFormat = jsonFormat2(GroupSyncResponse.apply) - - implicit val AccessPolicyMembershipRequestFormat = jsonFormat5(AccessPolicyMembershipRequest.apply) - - implicit val CreateResourceRequestFormat = jsonFormat5(CreateResourceRequest.apply) - - implicit val CreateResourcePolicyResponseFormat = jsonFormat2(CreateResourcePolicyResponse.apply) - - implicit val CreateResourceResponseFormat = jsonFormat4(CreateResourceResponse.apply) - - implicit val SignedUrlRequestFormat = jsonFormat4(SignedUrlRequest.apply) - - implicit val RequesterPaysSignedUrlRequestFormat = jsonFormat3(RequesterPaysSignedUrlRequest.apply) -} object RootPrimitiveJsonSupport { implicit val rootBooleanJsonFormat: RootJsonFormat[Boolean] = new RootJsonFormat[Boolean] { @@ -316,40 +238,6 @@ object BasicWorkbenchGroup { requesterPaysProject: Option[String] = None ) -object SamUser { - def apply( - id: WorkbenchUserId, - googleSubjectId: Option[GoogleSubjectId], - email: WorkbenchEmail, - azureB2CId: Option[AzureB2CId], - enabled: Boolean - ): SamUser = - SamUser(id, googleSubjectId, email, azureB2CId, enabled, Instant.EPOCH, None, Instant.EPOCH) -} - -final case class SamUser( - id: WorkbenchUserId, - googleSubjectId: Option[GoogleSubjectId], - email: WorkbenchEmail, - azureB2CId: Option[AzureB2CId], - enabled: Boolean, - createdAt: Instant, - registeredAt: Option[Instant], - updatedAt: Instant -) { - def toUserIdInfo = UserIdInfo(id, email, googleSubjectId) - - override def equals(other: Any): Boolean = other match { - case user: SamUser => - this.id == user.id && - this.googleSubjectId == user.googleSubjectId && - this.email == user.email && - this.azureB2CId == user.azureB2CId && - this.enabled == user.enabled - case _ => false - } -} - final case class SamUserTos(id: WorkbenchUserId, version: String, action: String, createdAt: Instant) { override def equals(other: Any): Boolean = other match { case userTos: SamUserTos => @@ -359,7 +247,6 @@ final case class SamUserTos(id: WorkbenchUserId, version: String, action: String case _ => false } } - object SamLenses { val resourceIdentityAccessPolicy = AccessPolicy.id composeLens FullyQualifiedPolicyId.resource val resourceTypeNameInAccessPolicy = resourceIdentityAccessPolicy composeLens FullyQualifiedResourceId.resourceTypeName 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 new file mode 100644 index 000000000..257b3383e --- /dev/null +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamJsonSupport.scala @@ -0,0 +1,117 @@ +package org.broadinstitute.dsde.workbench.sam.model.api + +import org.broadinstitute.dsde.workbench.model.ValueObjectFormat +import org.broadinstitute.dsde.workbench.sam.model.{ + AccessPolicyDescendantPermissions, + AccessPolicyName, + AccessPolicyResponseEntry, + CreateResourcePolicyResponse, + CreateResourceRequest, + CreateResourceResponse, + FullyQualifiedPolicyId, + FullyQualifiedResourceId, + GroupSyncResponse, + ManagedGroupAccessInstructions, + ManagedGroupMembershipEntry, + PolicyIdentifiers, + RequesterPaysSignedUrlRequest, + ResourceAction, + ResourceActionPattern, + ResourceId, + ResourceRole, + ResourceRoleName, + ResourceType, + ResourceTypeName, + RolesAndActions, + SignedUrlRequest, + TermsOfServiceAcceptance, + TermsOfServiceComplianceStatus, + TermsOfServiceDetails, + UserIdInfo, + UserPolicyResponse, + UserResourcesResponse, + UserStatus, + UserStatusDetails, + UserStatusDiagnostics, + UserStatusInfo +} +import org.broadinstitute.dsde.workbench.model.google.GoogleModelJsonSupport.InstantFormat +import spray.json.{DefaultJsonProtocol, RootJsonFormat} +import org.broadinstitute.dsde.workbench.sam.model.api.SamApiJsonProtocol.PolicyInfoResponseBodyJsonFormat + +object SamJsonSupport { + import DefaultJsonProtocol._ + import org.broadinstitute.dsde.workbench.model.WorkbenchIdentityJsonSupport._ + + implicit val ResourceActionPatternFormat = jsonFormat3(ResourceActionPattern.apply) + + implicit val ResourceActionFormat = ValueObjectFormat(ResourceAction.apply) + + implicit val ResourceRoleNameFormat = ValueObjectFormat(ResourceRoleName.apply) + + implicit val ResourceTypeNameFormat = ValueObjectFormat(ResourceTypeName.apply) + + implicit val ResourceRoleFormat = jsonFormat4(ResourceRole.apply) + + implicit val ResourceTypeFormat = jsonFormat6(ResourceType.apply) + + implicit val SamUserFormat = jsonFormat8(SamUser.apply) + + implicit val UserStatusDetailsFormat = jsonFormat2(UserStatusDetails.apply) + + implicit val UserStatusFormat = jsonFormat2(UserStatus.apply) + + implicit val UserStatusInfoFormat = jsonFormat4(UserStatusInfo.apply) + + implicit val UserIdInfoFormat = jsonFormat3(UserIdInfo.apply) + + implicit val TermsOfServiceAcceptanceFormat = ValueObjectFormat(TermsOfServiceAcceptance.apply) + + implicit val termsOfServiceDetailsFormat = jsonFormat4(TermsOfServiceDetails.apply) + + implicit val termsOfAcceptanceStatusFormat = jsonFormat3(TermsOfServiceComplianceStatus.apply) + + implicit val UserStatusDiagnosticsFormat = jsonFormat5(UserStatusDiagnostics.apply) + + implicit val AccessPolicyNameFormat = ValueObjectFormat(AccessPolicyName.apply) + + implicit val ResourceIdFormat = ValueObjectFormat(ResourceId.apply) + + implicit val FullyQualifiedResourceIdFormat = jsonFormat2(FullyQualifiedResourceId.apply) + + implicit val AccessPolicyDescendantPermissionsFormat: RootJsonFormat[AccessPolicyDescendantPermissions] = jsonFormat3(AccessPolicyDescendantPermissions.apply) + + implicit val PolicyIdentifiersFormat = jsonFormat3(PolicyIdentifiers.apply) + + implicit val AccessPolicyMembershipResponseFormat = jsonFormat5(AccessPolicyMembershipResponse.apply) + + implicit val AccessPolicyResponseEntryFormat = jsonFormat3(AccessPolicyResponseEntry.apply) + + implicit val UserPolicyResponseFormat = jsonFormat5(UserPolicyResponse.apply) + + implicit val UserUpdateRequestFormat = jsonFormat3(AdminUpdateUserRequest.apply) + + implicit val RolesAndActionsFormat = jsonFormat2(RolesAndActions.apply) + + implicit val UserResourcesResponseFormat = jsonFormat6(UserResourcesResponse.apply) + + implicit val FullyQualifiedPolicyIdFormat = jsonFormat2(FullyQualifiedPolicyId.apply) + + implicit val ManagedGroupMembershipEntryFormat = jsonFormat3(ManagedGroupMembershipEntry.apply) + + implicit val ManagedGroupAccessInstructionsFormat = ValueObjectFormat(ManagedGroupAccessInstructions.apply) + + implicit val GroupSyncResponseFormat = jsonFormat2(GroupSyncResponse.apply) + + implicit val AccessPolicyMembershipRequestFormat = jsonFormat5(AccessPolicyMembershipRequest.apply) + + implicit val CreateResourceRequestFormat = jsonFormat5(CreateResourceRequest.apply) + + implicit val CreateResourcePolicyResponseFormat = jsonFormat2(CreateResourcePolicyResponse.apply) + + implicit val CreateResourceResponseFormat = jsonFormat4(CreateResourceResponse.apply) + + implicit val SignedUrlRequestFormat = jsonFormat4(SignedUrlRequest.apply) + + implicit val RequesterPaysSignedUrlRequestFormat = jsonFormat3(RequesterPaysSignedUrlRequest.apply) +} diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUser.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUser.scala new file mode 100644 index 000000000..6c8138287 --- /dev/null +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUser.scala @@ -0,0 +1,40 @@ +package org.broadinstitute.dsde.workbench.sam.model.api + +import org.broadinstitute.dsde.workbench.model.{AzureB2CId, GoogleSubjectId, WorkbenchEmail, WorkbenchUserId} +import org.broadinstitute.dsde.workbench.sam.model.UserIdInfo + +import java.time.Instant + +object SamUser { + def apply( + id: WorkbenchUserId, + googleSubjectId: Option[GoogleSubjectId], + email: WorkbenchEmail, + azureB2CId: Option[AzureB2CId], + enabled: Boolean + ): SamUser = + SamUser(id, googleSubjectId, email, azureB2CId, enabled, Instant.EPOCH, None, Instant.EPOCH) +} + +final case class SamUser( + id: WorkbenchUserId, + googleSubjectId: Option[GoogleSubjectId], + email: WorkbenchEmail, + azureB2CId: Option[AzureB2CId], + enabled: Boolean, + createdAt: Instant, + registeredAt: Option[Instant], + updatedAt: Instant +) { + def toUserIdInfo = UserIdInfo(id, email, googleSubjectId) + + override def equals(other: Any): Boolean = other match { + case user: SamUser => + this.id == user.id && + this.googleSubjectId == user.googleSubjectId && + this.email == user.email && + this.azureB2CId == user.azureB2CId && + this.enabled == user.enabled + case _ => false + } +} diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUserAllowances.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUserAllowances.scala new file mode 100644 index 000000000..6cbaf7c9b --- /dev/null +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUserAllowances.scala @@ -0,0 +1,20 @@ +package org.broadinstitute.dsde.workbench.sam.model.api + +import spray.json.DefaultJsonProtocol._ +import spray.json.RootJsonFormat + +object SamUserAllowances { + implicit val SamUserAllowedResponseFormat: RootJsonFormat[SamUserAllowances] = jsonFormat2(SamUserAllowances.apply) + + def apply(allowed: Boolean, enabled: Boolean, termsOfService: Boolean): SamUserAllowances = + SamUserAllowances(allowed, Map("enabled" -> enabled, "termsOfService" -> termsOfService)) +} +final case class SamUserAllowances( + allowed: Boolean, + details: Map[String, Boolean] +) { + + def getEnabled: Boolean = details.get("enabled").exists(identity) + def getTermsOfServiceCompliance: Boolean = details.get("termsOfService").exists(identity) + +} diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUserResponse.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUserResponse.scala new file mode 100644 index 000000000..924776240 --- /dev/null +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/model/api/SamUserResponse.scala @@ -0,0 +1,37 @@ +package org.broadinstitute.dsde.workbench.sam.model.api + +import org.broadinstitute.dsde.workbench.model.{AzureB2CId, GoogleSubjectId, WorkbenchEmail, WorkbenchUserId} +import spray.json.DefaultJsonProtocol.jsonFormat8 +import spray.json.DefaultJsonProtocol._ +import org.broadinstitute.dsde.workbench.model.WorkbenchIdentityJsonSupport._ +import org.broadinstitute.dsde.workbench.model.google.GoogleModelJsonSupport.InstantFormat +import spray.json.RootJsonFormat + +import java.time.Instant + +object SamUserResponse { + + implicit val SamUserFormat: RootJsonFormat[SamUserResponse] = jsonFormat8(SamUserResponse.apply) + + def apply(samUser: SamUser, allowed: Boolean): SamUserResponse = + SamUserResponse( + samUser.id, + samUser.googleSubjectId, + samUser.email, + samUser.azureB2CId, + allowed = allowed, + samUser.createdAt, + samUser.registeredAt, + samUser.updatedAt + ) +} +final case class SamUserResponse( + id: WorkbenchUserId, + googleSubjectId: Option[GoogleSubjectId], + email: WorkbenchEmail, + azureB2CId: Option[AzureB2CId], + allowed: Boolean, + createdAt: Instant, + registeredAt: Option[Instant], + updatedAt: Instant +) {} diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/CloudExtensions.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/CloudExtensions.scala index e727807e6..66b929eff 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/CloudExtensions.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/CloudExtensions.scala @@ -10,7 +10,8 @@ import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.sam.api.ExtensionRoutes import org.broadinstitute.dsde.workbench.sam.dataAccess.DirectoryDAO -import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, ResourceTypeName, SamUser} +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser +import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, ResourceTypeName} import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.broadinstitute.dsde.workbench.util.health.SubsystemStatus import org.broadinstitute.dsde.workbench.util.health.Subsystems.Subsystem diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceService.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceService.scala index 56d93bd5d..d9f11d0fc 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceService.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/service/ResourceService.scala @@ -14,7 +14,7 @@ import org.broadinstitute.dsde.workbench.sam.audit.SamAuditModelJsonSupport._ import org.broadinstitute.dsde.workbench.sam.audit._ import org.broadinstitute.dsde.workbench.sam.dataAccess.{AccessPolicyDAO, DirectoryDAO, LoadResourceAuthDomainResult} import org.broadinstitute.dsde.workbench.sam.model._ -import org.broadinstitute.dsde.workbench.sam.model.api.{AccessPolicyMembershipRequest, AccessPolicyMembershipResponse} +import org.broadinstitute.dsde.workbench.sam.model.api.{AccessPolicyMembershipRequest, AccessPolicyMembershipResponse, SamUser} import org.broadinstitute.dsde.workbench.sam.util.{API_TIMING_DURATION_BUCKET, SamRequestContext} import java.util.UUID 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 a9cbdbc56..6d8eca720 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 @@ -15,7 +15,8 @@ import org.broadinstitute.dsde.workbench.sam.errorReportSource import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.broadinstitute.dsde.workbench.sam.config.TermsOfServiceConfig import org.broadinstitute.dsde.workbench.sam.db.tables.TosTable -import org.broadinstitute.dsde.workbench.sam.model.{SamUser, SamUserTos, TermsOfServiceComplianceStatus, TermsOfServiceDetails} +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser +import org.broadinstitute.dsde.workbench.sam.model.{SamUserTos, TermsOfServiceComplianceStatus, TermsOfServiceDetails} import java.io.{FileNotFoundException, IOException} import scala.concurrent.{Await, ExecutionContext} 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 8d192cdb0..f00a4442e 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 @@ -12,7 +12,7 @@ import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics import org.broadinstitute.dsde.workbench.sam.azure.ManagedIdentityObjectId import org.broadinstitute.dsde.workbench.sam.dataAccess.DirectoryDAO import org.broadinstitute.dsde.workbench.sam.model._ -import org.broadinstitute.dsde.workbench.sam.model.api.AdminUpdateUserRequest +import org.broadinstitute.dsde.workbench.sam.model.api.{AdminUpdateUserRequest, SamUser, SamUserAllowances} import org.broadinstitute.dsde.workbench.sam.service.UserService.genWorkbenchUserId import org.broadinstitute.dsde.workbench.sam.util.AsyncLogging.IOWithLogging import org.broadinstitute.dsde.workbench.sam.util.{API_TIMING_DURATION_BUCKET, SamRequestContext} @@ -427,6 +427,15 @@ class UserService(val directoryDAO: DirectoryDAO, val cloudExtensions: CloudExte case UserService.emailRegex() => IO.unit case _ => IO.raiseError(new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.BadRequest, s"invalid email address [${email.value}]"))) } + + def getUserAllowances(samUser: SamUser, samRequestContext: SamRequestContext): IO[SamUserAllowances] = + for { + tosStatus <- tosService.getTosComplianceStatus(samUser, samRequestContext) + } yield SamUserAllowances( + allowed = samUser.enabled && tosStatus.permitsSystemUsage, + enabled = samUser.enabled, + termsOfService = tosStatus.permitsSystemUsage + ) } object UserService { diff --git a/src/main/scala/org/broadinstitute/dsde/workbench/sam/util/SamRequestContext.scala b/src/main/scala/org/broadinstitute/dsde/workbench/sam/util/SamRequestContext.scala index 59359ad74..4690c9917 100644 --- a/src/main/scala/org/broadinstitute/dsde/workbench/sam/util/SamRequestContext.scala +++ b/src/main/scala/org/broadinstitute/dsde/workbench/sam/util/SamRequestContext.scala @@ -2,7 +2,7 @@ package org.broadinstitute.dsde.workbench.sam.util import io.opencensus.trace.Span import org.broadinstitute.dsde.workbench.sam.audit.AuditInfo -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import java.net.InetAddress diff --git a/src/test/scala/Generator.scala b/src/test/scala/Generator.scala index 65b8953c1..2ea1fe295 100644 --- a/src/test/scala/Generator.scala +++ b/src/test/scala/Generator.scala @@ -10,6 +10,7 @@ import org.broadinstitute.dsde.workbench.sam.azure._ import org.broadinstitute.dsde.workbench.sam.dataAccess.LockDetails import org.broadinstitute.dsde.workbench.sam.model.SamResourceActions._ import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.UserService import org.scalacheck._ diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/TestSupport.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/TestSupport.scala index e206845d1..76034f97b 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/TestSupport.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/TestSupport.scala @@ -25,6 +25,7 @@ import org.broadinstitute.dsde.workbench.sam.db.TestDbReference import org.broadinstitute.dsde.workbench.sam.db.tables._ import org.broadinstitute.dsde.workbench.sam.google.{GoogleExtensionRoutes, GoogleExtensions, GoogleGroupSynchronizer, GoogleKeyCache} import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.UserService._ import org.broadinstitute.dsde.workbench.sam.service._ import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/AdminResourceTypesRoutesSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/AdminResourceTypesRoutesSpec.scala index b68309d96..b49394abe 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/AdminResourceTypesRoutesSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/AdminResourceTypesRoutesSpec.scala @@ -8,7 +8,7 @@ import cats.implicits.catsSyntaxOptionId import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.sam.api.TestSamRoutes.resourceTypeAdmin import org.broadinstitute.dsde.workbench.sam.dataAccess.{MockAccessPolicyDAO, MockDirectoryDAO} -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model.SamResourceActions._ import org.broadinstitute.dsde.workbench.sam.model._ import org.broadinstitute.dsde.workbench.sam.model.api._ diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/AdminServiceUserRoutesSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/AdminServiceUserRoutesSpec.scala index 1b9d3f659..114754b38 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/AdminServiceUserRoutesSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/AdminServiceUserRoutesSpec.scala @@ -4,8 +4,9 @@ import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.testkit.ScalatestRouteTest import org.broadinstitute.dsde.workbench.model.{WorkbenchEmail, WorkbenchUserId} -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service._ import org.broadinstitute.dsde.workbench.sam.{Generator, TestSupport} import org.mockito.scalatest.MockitoSugar diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/AdminUserRoutesSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/AdminUserRoutesSpec.scala index cfb216fce..f3dcdb5ad 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/AdminUserRoutesSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/AdminUserRoutesSpec.scala @@ -6,9 +6,9 @@ import akka.http.scaladsl.testkit.ScalatestRouteTest import org.broadinstitute.dsde.workbench.model.{WorkbenchEmail, WorkbenchUserId} import org.broadinstitute.dsde.workbench.sam.TestSupport.enabledMapNoTosAccepted import org.broadinstitute.dsde.workbench.sam.matchers.BeForUserMatcher.beForUser -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ -import org.broadinstitute.dsde.workbench.sam.model.api.AdminUpdateUserRequest +import org.broadinstitute.dsde.workbench.sam.model.api.{AdminUpdateUserRequest, SamUser} import org.broadinstitute.dsde.workbench.sam.service._ import org.broadinstitute.dsde.workbench.sam.{Generator, TestSupport} import org.mockito.scalatest.MockitoSugar diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ManagedGroupRoutesSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ManagedGroupRoutesSpec.scala index 5108715ca..441059c70 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ManagedGroupRoutesSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ManagedGroupRoutesSpec.scala @@ -14,8 +14,9 @@ import org.broadinstitute.dsde.workbench.openTelemetry.{FakeOpenTelemetryMetrics import org.broadinstitute.dsde.workbench.sam.TestSupport.samRequestContext import org.broadinstitute.dsde.workbench.sam.api.ManagedGroupRoutesSpec._ import org.broadinstitute.dsde.workbench.sam.dataAccess.{MockAccessPolicyDAO, MockDirectoryDAO} -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.ManagedGroupService import org.broadinstitute.dsde.workbench.sam.service.UserService.genRandom import org.scalatest.BeforeAndAfter diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ManagedGroupRoutesV1Spec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ManagedGroupRoutesV1Spec.scala index 3d4b9fcc1..210a71d9e 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ManagedGroupRoutesV1Spec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ManagedGroupRoutesV1Spec.scala @@ -7,8 +7,9 @@ import akka.http.scaladsl.testkit.ScalatestRouteTest import org.broadinstitute.dsde.workbench.model.WorkbenchIdentityJsonSupport._ import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.sam.api.ManagedGroupRoutesSpec._ -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.ManagedGroupService import org.scalatest.BeforeAndAfter import org.scalatest.concurrent.ScalaFutures diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamRoutes.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamRoutes.scala index 6ce8954cd..e2aefe7ab 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamRoutes.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamRoutes.scala @@ -47,7 +47,8 @@ abstract class MockSamRoutes( val openTelemetry: OpenTelemetryMetrics[IO] ) extends LazyLogging with ResourceRoutes - with UserRoutes + with OldUserRoutes + with UserRoutesV1 with MockStatusRoutes with TermsOfServiceRoutes with ExtensionRoutes @@ -62,7 +63,7 @@ abstract class MockSamRoutes( termsOfServiceRoutes ~ withExecutionContext(ExecutionContext.global) { withSamRequestContext { samRequestContext => - pathPrefix("register")(userRoutes(samRequestContext)) ~ + pathPrefix("register")(oldUserRoutes(samRequestContext)) ~ pathPrefix("api") { // IMPORTANT - all routes under /api must have an active user withActiveUser(samRequestContext) { samUser => @@ -71,7 +72,7 @@ abstract class MockSamRoutes( adminRoutes(samUser, samRequestContextWithUser) ~ extensionRoutes(samUser, samRequestContextWithUser) ~ groupRoutes(samUser, samRequestContextWithUser) ~ - apiUserRoutes(samUser, samRequestContextWithUser) ~ + userRoutesV1(samUser, samRequestContextWithUser) ~ azureRoutes(samUser, samRequestContextWithUser) } } 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 133000a5c..04901a65a 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 @@ -7,7 +7,7 @@ import akka.stream.Materializer import cats.effect.IO import org.broadinstitute.dsde.workbench.model.{ErrorReportSource, WorkbenchGroup} import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service._ import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext @@ -30,6 +30,7 @@ class MockSamRoutesBuilder(allUsersGroup: WorkbenchGroup)(implicit system: Actor private var disabledUser: Option[SamUser] = None private var adminUser: Option[SamUser] = None private var asServiceAdminUser = false + private var callingUser: Option[SamUser] = None // TODO: *sniff sniff* I can't help but notice we're coordinating state between the userService and the // cloudExtensions. Same for other methods too. @@ -54,18 +55,30 @@ class MockSamRoutesBuilder(allUsersGroup: WorkbenchGroup)(implicit system: Actor this } - def callAsAdminUser(): MockSamRoutesBuilder = { + def withAllowedUser(samUser: SamUser): MockSamRoutesBuilder = { + userServiceBuilder.withAllowedUser(samUser) + this + } + def withAllowedUsers(samUsers: Iterable[SamUser]): MockSamRoutesBuilder = { + userServiceBuilder.withAllowedUsers(samUsers) + this + } + + def callAsAdminUser(samUser: Option[SamUser] = None): MockSamRoutesBuilder = { cloudExtensionsBuilder.withAdminUser() + callingUser = samUser this } - def callAsAdminServiceUser(): MockSamRoutesBuilder = { + def callAsAdminServiceUser(samUser: Option[SamUser] = None): MockSamRoutesBuilder = { asServiceAdminUser = true + callingUser = samUser this } - def callAsNonAdminUser(): MockSamRoutesBuilder = { + def callAsNonAdminUser(samUser: Option[SamUser] = None): MockSamRoutesBuilder = { cloudExtensionsBuilder.withNonAdminUser() + callingUser = samUser this } @@ -77,7 +90,8 @@ class MockSamRoutesBuilder(allUsersGroup: WorkbenchGroup)(implicit system: Actor // Can only have 1 active user when making a request. If the adminUser is set, that takes precedence, otherwise try // to get the enabledUser. private def getActiveUser: SamUser = - enabledUser + callingUser + .orElse(enabledUser) .orElse(disabledUser) .getOrElse(throw new Exception("Try building MockSamRoutes .withAdminUser(), .withEnabledUser(), withDisabledUser() first")) @@ -110,7 +124,7 @@ class MockSamRoutesBuilder(allUsersGroup: WorkbenchGroup)(implicit system: Actor } override def withUserAllowInactive(samRequestContext: SamRequestContext): Directive1[SamUser] = onSuccess { - Future.successful(disabledUser.getOrElse(throw new Exception("Try building MockSamRoutes .withDisabledUser() first"))) + Future.successful(getActiveUser) } // We should really not be testing this, the routes should work identically whether the user diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamUserDirectives.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamUserDirectives.scala index dd7018e94..3b738b870 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamUserDirectives.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/MockSamUserDirectives.scala @@ -5,7 +5,7 @@ import akka.http.scaladsl.model.headers.OAuth2BearerToken import akka.http.scaladsl.server.Directive1 import akka.http.scaladsl.server.Directives._ import cats.effect.unsafe.implicits.global -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext /** Created by dvoet on 6/7/17. @@ -18,7 +18,7 @@ trait MockSamUserDirectives extends SamUserDirectives { OIDCHeaders(OAuth2BearerToken("dummy token"), user.googleSubjectId.toLeft(user.azureB2CId.get), user.email, user.googleSubjectId) override def withActiveUser(samRequestContext: SamRequestContext): Directive1[SamUser] = onSuccess { - StandardSamUserDirectives.getActiveSamUser(fakeOidcHeaders, userService, tosService, samRequestContext).unsafeToFuture() + StandardSamUserDirectives.getActiveSamUser(fakeOidcHeaders, userService, samRequestContext).unsafeToFuture() } override def withUserAllowInactive(samRequestContext: SamRequestContext): Directive1[SamUser] = onSuccess { diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutesSpec.scala similarity index 92% rename from src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesSpec.scala rename to src/test/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutesSpec.scala index 89f032aff..0c1b04e70 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutesSpec.scala @@ -7,8 +7,9 @@ import akka.http.scaladsl.testkit.ScalatestRouteTest import org.broadinstitute.dsde.workbench.google.GoogleDirectoryDAO import org.broadinstitute.dsde.workbench.sam.TestSupport.{genSamDependencies, genSamRoutes} import org.broadinstitute.dsde.workbench.sam.dataAccess.MockDirectoryDAO -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service._ import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers @@ -16,7 +17,7 @@ import org.mockito.scalatest.MockitoSugar /** Created by dvoet on 6/7/17. */ -class UserRoutesSpec extends UserRoutesSpecHelper { +class OldUserRoutesSpec extends OldUserRoutesSpecHelper { "POST /register/user" should "create user" in withDefaultRoutes { samRoutes => Post("/register/user") ~> samRoutes.route ~> check { status shouldEqual StatusCodes.Created @@ -41,7 +42,7 @@ class UserRoutesSpec extends UserRoutesSpecHelper { } } -trait UserRoutesSpecHelper extends AnyFlatSpec with Matchers with ScalatestRouteTest with MockitoSugar with TestSupport { +trait OldUserRoutesSpecHelper extends AnyFlatSpec with Matchers with ScalatestRouteTest with MockitoSugar with TestSupport { val defaultUser = Generator.genWorkbenchUserGoogle.sample.get val defaultUserId = defaultUser.id val defaultUserEmail = defaultUser.email diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV1Spec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutesV1Spec.scala similarity index 97% rename from src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV1Spec.scala rename to src/test/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutesV1Spec.scala index e3784a0f0..c4f641559 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV1Spec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutesV1Spec.scala @@ -8,13 +8,13 @@ import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.sam.Generator.genNonPetEmail import org.broadinstitute.dsde.workbench.sam.dataAccess.MockDirectoryDAO import org.broadinstitute.dsde.workbench.sam.model.RootPrimitiveJsonSupport.rootBooleanJsonFormat -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ import org.broadinstitute.dsde.workbench.sam.service.{NoExtensions, StatusService, TosService, UserService} /** Created by dvoet on 6/7/17. */ -class UserRoutesV1Spec extends UserRoutesSpecHelper { +class OldUserRoutesV1Spec extends OldUserRoutesSpecHelper { def withSARoutes[T](testCode: (TestSamRoutes, TestSamRoutes) => T): T = { val directoryDAO = new MockDirectoryDAO() diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutesV2Spec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutesV2Spec.scala new file mode 100644 index 000000000..c7014f8ae --- /dev/null +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/OldUserRoutesV2Spec.scala @@ -0,0 +1,115 @@ +package org.broadinstitute.dsde.workbench.sam +package api + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ +import akka.http.scaladsl.model.StatusCodes +import org.broadinstitute.dsde.workbench.model._ +import org.broadinstitute.dsde.workbench.sam.dataAccess.MockDirectoryDAO +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.service.UserService._ +import org.broadinstitute.dsde.workbench.sam.service.{NoExtensions, StatusService, TosService, UserService} + +/** Created by mtalbott on 8/8/18. + */ +class OldUserRoutesV2Spec extends OldUserRoutesSpecHelper { + def withSARoutes[T](testCode: (TestSamRoutes, TestSamRoutes) => T): T = { + val directoryDAO = new MockDirectoryDAO() + + val tosService = new TosService(directoryDAO, TestSupport.tosConfig) + val samRoutes = new TestSamRoutes( + null, + null, + new UserService(directoryDAO, NoExtensions, Seq.empty, tosService), + new StatusService(directoryDAO, NoExtensions), + null, + defaultUser, + NoExtensions, + tosService = tosService + ) + val SARoutes = new TestSamRoutes( + null, + null, + new UserService(directoryDAO, NoExtensions, Seq.empty, tosService), + new StatusService(directoryDAO, NoExtensions), + null, + petSAUser, + NoExtensions, + tosService = tosService + ) + testCode(samRoutes, SARoutes) + } + + "POST /register/user/v2/self" should "create user" in withDefaultRoutes { samRoutes => + Post("/register/user/v2/self") ~> samRoutes.route ~> check { + status shouldEqual StatusCodes.Created + val res = responseAs[UserStatus] + res.userInfo.userSubjectId.value.length shouldBe 21 + res.userInfo.userEmail shouldBe defaultUserEmail + res.enabled shouldBe TestSupport.enabledMapNoTosAccepted + } + + Post("/register/user/v2/self") ~> samRoutes.route ~> check { + status shouldEqual StatusCodes.Conflict + } + } + + "GET /register/user/v2/self/info" should "get the status of an enabled user" in withDefaultRoutes { samRoutes => + Get("/register/user/v2/self/info") ~> samRoutes.route ~> check { + status shouldEqual StatusCodes.NotFound + } + val (user, samDep, routes) = createTestUser(tosAccepted = true) + Get("/register/user/v2/self/info") ~> routes.route ~> check { + status shouldEqual StatusCodes.OK + responseAs[UserStatusInfo] shouldEqual UserStatusInfo(user.id.value, user.email.value, true, true) + } + } + + "GET /register/user/v2/self/diagnostics" should "get the diagnostic info for an enabled user" in withDefaultRoutes { samRoutes => + val googleSubjectId = GoogleSubjectId(genRandom(System.currentTimeMillis())) + Get("/register/user/v2/self/diagnostics") ~> samRoutes.route ~> check { + status shouldEqual StatusCodes.NotFound + } + val (user, samDep, routes) = createTestUser(tosAccepted = true) + + Get("/register/user/v2/self/diagnostics") ~> routes.route ~> check { + status shouldEqual StatusCodes.OK + responseAs[UserStatusDiagnostics] shouldEqual UserStatusDiagnostics(true, true, true, true, true) + } + } + + it should "get user's diagnostics after accepting the tos" in { + val (user, _, routes) = createTestUser(tosAccepted = true) + + Get("/register/user/v2/self/diagnostics") ~> routes.route ~> check { + status shouldEqual StatusCodes.OK + val res = responseAs[UserStatusDiagnostics] + res.tosAccepted shouldBe true + } + } + + "GET /register/user/v2/self/termsOfServiceDetails" should "get the user's Terms of Service details" in { + val (_, _, routes) = createTestUser(tosAccepted = true) + + Get("/register/user/v2/self/termsOfServiceDetails") ~> routes.route ~> check { + status shouldEqual StatusCodes.OK + val details = responseAs[TermsOfServiceDetails] + details.isEnabled should be(true) + details.isGracePeriodEnabled should be(TestSupport.tosConfig.isGracePeriodEnabled) + details.userAcceptedVersion should not be empty + details.currentVersion should be(details.userAcceptedVersion.get) + } + } + + "GET /register/user/v2/self/termsOfServiceComplianceStatus" should "get the user's Terms of Service Compliance Status" in { + val (user, _, routes) = createTestUser(tosAccepted = true) + + Get("/register/user/v2/self/termsOfServiceComplianceStatus") ~> routes.route ~> check { + status shouldEqual StatusCodes.OK + val complianceStatus = responseAs[TermsOfServiceComplianceStatus] + complianceStatus.userId shouldBe user.id + complianceStatus.permitsSystemUsage shouldBe true + complianceStatus.userHasAcceptedLatestTos shouldBe true + } + } +} diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesSpec.scala index 4cf65dd7f..4c9172a9d 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesSpec.scala @@ -9,7 +9,7 @@ import org.broadinstitute.dsde.workbench.model.ErrorReportJsonSupport._ import org.broadinstitute.dsde.workbench.model.WorkbenchIdentityJsonSupport._ import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.sam.dataAccess.{MockAccessPolicyDAO, MockDirectoryDAO} -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ import org.broadinstitute.dsde.workbench.sam.model.api._ import org.broadinstitute.dsde.workbench.sam.service._ diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesV1Spec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesV1Spec.scala index 4e762f805..8a392c69e 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesV1Spec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesV1Spec.scala @@ -12,7 +12,7 @@ import org.broadinstitute.dsde.workbench.sam.TestSupport._ import org.broadinstitute.dsde.workbench.sam.api.TestSamRoutes.SamResourceActionPatterns import org.broadinstitute.dsde.workbench.sam.dataAccess.{MockAccessPolicyDAO, MockDirectoryDAO} import org.broadinstitute.dsde.workbench.sam.model.RootPrimitiveJsonSupport._ -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ import org.broadinstitute.dsde.workbench.sam.model.api._ import org.broadinstitute.dsde.workbench.sam.service._ diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesV2Spec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesV2Spec.scala index eff039bf9..d1e06e39d 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesV2Spec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/ResourceRoutesV2Spec.scala @@ -11,7 +11,7 @@ import org.broadinstitute.dsde.workbench.sam.TestSupport.configResourceTypes import org.broadinstitute.dsde.workbench.sam.api.TestSamRoutes.SamResourceActionPatterns import org.broadinstitute.dsde.workbench.sam.dataAccess.{MockAccessPolicyDAO, MockDirectoryDAO} import org.broadinstitute.dsde.workbench.sam.model.RootPrimitiveJsonSupport._ -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ import org.broadinstitute.dsde.workbench.sam.model.api._ import org.broadinstitute.dsde.workbench.sam.service._ diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/RouteSecuritySpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/RouteSecuritySpec.scala index cc2841fd2..fceea1ec0 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/RouteSecuritySpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/RouteSecuritySpec.scala @@ -2,7 +2,7 @@ package org.broadinstitute.dsde.workbench.sam.api import akka.http.scaladsl.testkit.ScalatestRouteTest import org.broadinstitute.dsde.workbench.sam.TestSupport -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.mockito.ArgumentMatchers.any import org.mockito.internal.verification.AtLeast diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/StandardSamUserDirectivesSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/StandardSamUserDirectivesSpec.scala index d773e69b6..170c64703 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/StandardSamUserDirectivesSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/StandardSamUserDirectivesSpec.scala @@ -15,7 +15,7 @@ import org.broadinstitute.dsde.workbench.sam.api.StandardSamUserDirectives._ import org.broadinstitute.dsde.workbench.sam.config.AppConfig.AdminConfig import org.broadinstitute.dsde.workbench.sam.config.{AppConfig, TermsOfServiceConfig} import org.broadinstitute.dsde.workbench.sam.dataAccess.{DirectoryDAO, MockDirectoryDAO} -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.{CloudExtensions, MockUserService, TosService, UserService} import org.scalatest.concurrent.ScalaFutures import org.scalatest.flatspec.AnyFlatSpec @@ -35,8 +35,8 @@ class StandardSamUserDirectivesSpec extends AnyFlatSpec with PropertyBasedTestin override implicit val executionContext: ExecutionContext = null override val cloudExtensions: CloudExtensions = null override val termsOfServiceConfig: TermsOfServiceConfig = null - override val userService: UserService = new MockUserService(directoryDAO = dirDAO) override val tosService: TosService = new TosService(dirDAO, tosConfig) + override val userService: UserService = new MockUserService(directoryDAO = dirDAO, tosService = tosService) override val adminConfig: AppConfig.AdminConfig = testAdminConfig } diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/TestSamRoutes.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/TestSamRoutes.scala index 2fef13923..a55f8ceae 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/TestSamRoutes.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/TestSamRoutes.scala @@ -19,6 +19,7 @@ import org.broadinstitute.dsde.workbench.sam.config.{LiquibaseConfig, TermsOfSer import org.broadinstitute.dsde.workbench.sam.dataAccess._ import org.broadinstitute.dsde.workbench.sam.model.SamResourceActions.{adminAddMember, adminReadPolicies, adminRemoveMember} import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service._ import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.broadinstitute.dsde.workbench.sam.{Generator, TestSupport} diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2Spec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2Spec.scala index cf6b9b47d..9380f39a8 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2Spec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/api/UserRoutesV2Spec.scala @@ -1,115 +1,157 @@ -package org.broadinstitute.dsde.workbench.sam -package api +package org.broadinstitute.dsde.workbench.sam.api import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ import akka.http.scaladsl.model.StatusCodes -import org.broadinstitute.dsde.workbench.model._ -import org.broadinstitute.dsde.workbench.sam.dataAccess.MockDirectoryDAO -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import akka.http.scaladsl.testkit.ScalatestRouteTest +import org.broadinstitute.dsde.workbench.model.{ErrorReport, WorkbenchEmail} +import org.broadinstitute.dsde.workbench.model.ErrorReportJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.matchers.BeForSamUserResponseMatcher.beForUser import org.broadinstitute.dsde.workbench.sam.model._ -import org.broadinstitute.dsde.workbench.sam.service.UserService._ -import org.broadinstitute.dsde.workbench.sam.service.{NoExtensions, StatusService, TosService, UserService} - -/** Created by mtalbott on 8/8/18. - */ -class UserRoutesV2Spec extends UserRoutesSpecHelper { - def withSARoutes[T](testCode: (TestSamRoutes, TestSamRoutes) => T): T = { - val directoryDAO = new MockDirectoryDAO() - - val tosService = new TosService(directoryDAO, TestSupport.tosConfig) - val samRoutes = new TestSamRoutes( - null, - null, - new UserService(directoryDAO, NoExtensions, Seq.empty, tosService), - new StatusService(directoryDAO, NoExtensions), - null, - defaultUser, - NoExtensions, - tosService = tosService - ) - val SARoutes = new TestSamRoutes( - null, - null, - new UserService(directoryDAO, NoExtensions, Seq.empty, tosService), - new StatusService(directoryDAO, NoExtensions), - null, - petSAUser, - NoExtensions, - tosService = tosService - ) - testCode(samRoutes, SARoutes) - } +import org.broadinstitute.dsde.workbench.sam.model.api.{SamUser, SamUserAllowances, SamUserResponse} +import org.broadinstitute.dsde.workbench.sam.service._ +import org.broadinstitute.dsde.workbench.sam.{Generator, TestSupport} +import org.mockito.scalatest.MockitoSugar +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers - "POST /register/user/v2/self" should "create user" in withDefaultRoutes { samRoutes => - Post("/register/user/v2/self") ~> samRoutes.route ~> check { - status shouldEqual StatusCodes.Created - val res = responseAs[UserStatus] - res.userInfo.userSubjectId.value.length shouldBe 21 - res.userInfo.userEmail shouldBe defaultUserEmail - res.enabled shouldBe TestSupport.enabledMapNoTosAccepted - } +class UserRoutesV2Spec extends AnyFlatSpec with Matchers with ScalatestRouteTest with MockitoSugar with TestSupport { + val defaultUser: SamUser = Generator.genWorkbenchUserGoogle.sample.get + val otherUser: SamUser = Generator.genWorkbenchUserGoogle.sample.get + val thirdUser: SamUser = Generator.genWorkbenchUserGoogle.sample.get + val adminGroupEmail: WorkbenchEmail = Generator.genFirecloudEmail.sample.get + val allUsersGroup: BasicWorkbenchGroup = BasicWorkbenchGroup(CloudExtensions.allUsersGroupName, Set(), WorkbenchEmail("all_users@fake.com")) + + "GET /api/users/v2/self" should "get the user object of the requesting user" in { + // Arrange + val samRoutes = new MockSamRoutesBuilder(allUsersGroup) + .withEnabledUser(defaultUser) // "persisted/enabled" user we will check the status of + .withAllowedUser(defaultUser) // "allowed" user we will check the status of + .callAsNonAdminUser() + .build - Post("/register/user/v2/self") ~> samRoutes.route ~> check { - status shouldEqual StatusCodes.Conflict + // Act and Assert + Get(s"/api/users/v2/self") ~> samRoutes.route ~> check { + status shouldEqual StatusCodes.OK + responseAs[SamUserResponse] should beForUser(defaultUser) } } - "GET /register/user/v2/self/info" should "get the status of an enabled user" in withDefaultRoutes { samRoutes => - Get("/register/user/v2/self/info") ~> samRoutes.route ~> check { - status shouldEqual StatusCodes.NotFound - } - val (user, samDep, routes) = createTestUser(tosAccepted = true) - Get("/register/user/v2/self/info") ~> routes.route ~> check { + "GET /api/users/v2/{sam_user_id}" should "return the regular user if they're getting themselves" in { + // Arrange + val samRoutes = new MockSamRoutesBuilder(allUsersGroup) + .withEnabledUsers(Seq(defaultUser, otherUser)) + .withAllowedUsers(Seq(defaultUser, otherUser)) + .callAsNonAdminUser() + .build + + // Act and Assert + Get(s"/api/users/v2/${defaultUser.id}") ~> samRoutes.route ~> check { status shouldEqual StatusCodes.OK - responseAs[UserStatusInfo] shouldEqual UserStatusInfo(user.id.value, user.email.value, true, true) + responseAs[SamUserResponse] should beForUser(defaultUser) } } - "GET /register/user/v2/self/diagnostics" should "get the diagnostic info for an enabled user" in withDefaultRoutes { samRoutes => - val googleSubjectId = GoogleSubjectId(genRandom(System.currentTimeMillis())) - Get("/register/user/v2/self/diagnostics") ~> samRoutes.route ~> check { + it should "fail with Not Found if a regular user is getting another user" in { + // Arrange + val samRoutes = new MockSamRoutesBuilder(allUsersGroup) + .withEnabledUsers(Seq(defaultUser, otherUser)) + .withAllowedUsers(Seq(defaultUser, otherUser)) + .callAsNonAdminUser() + .build + + // Act and Assert + Get(s"/api/users/v2/${otherUser.id}") ~> samRoutes.route ~> check { status shouldEqual StatusCodes.NotFound + val foo = responseAs[ErrorReport] + foo.message contains "You must be an admin" } - val (user, samDep, routes) = createTestUser(tosAccepted = true) + } - Get("/register/user/v2/self/diagnostics") ~> routes.route ~> check { + it should "succeed if an admin user is getting another user" in { + // Arrange + val samRoutes = new MockSamRoutesBuilder(allUsersGroup) + .withEnabledUsers(Seq(defaultUser, otherUser)) + .withAllowedUser(defaultUser) + .callAsAdminUser() + .build + + // Act and Assert + Get(s"/api/users/v2/${defaultUser.id}") ~> samRoutes.route ~> check { status shouldEqual StatusCodes.OK - responseAs[UserStatusDiagnostics] shouldEqual UserStatusDiagnostics(true, true, true, true, true) + responseAs[SamUserResponse] should beForUser(defaultUser) + responseAs[SamUserResponse].allowed should be(true) + } + + Get(s"/api/users/v2/${otherUser.id}") ~> samRoutes.route ~> check { + status shouldEqual StatusCodes.OK + responseAs[SamUserResponse] should beForUser(otherUser) + responseAs[SamUserResponse].allowed should be(false) } } - it should "get user's diagnostics after accepting the tos" in { - val (user, _, routes) = createTestUser(tosAccepted = true) + "GET /api/users/v2/self/allowed" should "get the user allowances of the calling user" in { + // Arrange + val samRoutes = new MockSamRoutesBuilder(allUsersGroup) + .withEnabledUser(defaultUser) // "persisted/enabled" user we will check the status of + .withAllowedUser(defaultUser) // "allowed" user we will check the status of + .callAsNonAdminUser() + .build - Get("/register/user/v2/self/diagnostics") ~> routes.route ~> check { + // Act and Assert + Get(s"/api/users/v2/self/allowed") ~> samRoutes.route ~> check { status shouldEqual StatusCodes.OK - val res = responseAs[UserStatusDiagnostics] - res.tosAccepted shouldBe true + responseAs[SamUserAllowances] should be(SamUserAllowances(allowed = true, enabled = true, termsOfService = true)) } } - "GET /register/user/v2/self/termsOfServiceDetails" should "get the user's Terms of Service details" in { - val (_, _, routes) = createTestUser(tosAccepted = true) + "GET /api/users/v2/{sam_user_id}/allowed" should "return the allowances of a regular user if they're getting themselves" in { + // Arrange + val samRoutes = new MockSamRoutesBuilder(allUsersGroup) + .withEnabledUsers(Seq(defaultUser, otherUser)) + .withAllowedUsers(Seq(defaultUser, otherUser)) + .callAsNonAdminUser() + .build - Get("/register/user/v2/self/termsOfServiceDetails") ~> routes.route ~> check { + // Act and Assert + Get(s"/api/users/v2/${defaultUser.id}/allowed") ~> samRoutes.route ~> check { status shouldEqual StatusCodes.OK - val details = responseAs[TermsOfServiceDetails] - details.isEnabled should be(true) - details.isGracePeriodEnabled should be(TestSupport.tosConfig.isGracePeriodEnabled) - details.userAcceptedVersion should not be empty - details.currentVersion should be(details.userAcceptedVersion.get) + responseAs[SamUserAllowances] should be(SamUserAllowances(allowed = true, enabled = true, termsOfService = true)) + } + } + + it should "fail with Not Found if a regular user is getting another user" in { + // Arrange + val samRoutes = new MockSamRoutesBuilder(allUsersGroup) + .withEnabledUsers(Seq(defaultUser, otherUser)) + .withAllowedUsers(Seq(defaultUser, otherUser)) + .callAsNonAdminUser(Some(defaultUser)) + .build + + // Act and Assert + Get(s"/api/users/v2/${otherUser.id}/allowed") ~> samRoutes.route ~> check { + status shouldEqual StatusCodes.NotFound + val foo = responseAs[ErrorReport] + foo.message contains "You must be an admin" } } - "GET /register/user/v2/self/termsOfServiceComplianceStatus" should "get the user's Terms of Service Compliance Status" in { - val (user, _, routes) = createTestUser(tosAccepted = true) + it should "succeed if an admin user is getting another user" in { + // Arrange + val samRoutes = new MockSamRoutesBuilder(allUsersGroup) + .withEnabledUsers(Seq(defaultUser, otherUser, thirdUser)) + .withAllowedUser(otherUser) + .callAsAdminUser(Some(defaultUser)) + .build + + // Act and Assert + Get(s"/api/users/v2/${otherUser.id}/allowed") ~> samRoutes.route ~> check { + status shouldEqual StatusCodes.OK + responseAs[SamUserAllowances] should be(SamUserAllowances(allowed = true, enabled = true, termsOfService = true)) + } - Get("/register/user/v2/self/termsOfServiceComplianceStatus") ~> routes.route ~> check { + Get(s"/api/users/v2/${thirdUser.id}/allowed") ~> samRoutes.route ~> check { status shouldEqual StatusCodes.OK - val complianceStatus = responseAs[TermsOfServiceComplianceStatus] - complianceStatus.userId shouldBe user.id - complianceStatus.permitsSystemUsage shouldBe true - complianceStatus.userHasAcceptedLatestTos shouldBe true + responseAs[SamUserAllowances] should be(SamUserAllowances(allowed = false, enabled = false, termsOfService = false)) } } } diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/azure/AzureRoutesSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/azure/AzureRoutesSpec.scala index acbb04c53..5d82b2f1b 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/azure/AzureRoutesSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/azure/AzureRoutesSpec.scala @@ -10,8 +10,8 @@ import org.broadinstitute.dsde.workbench.sam.api.TestSamRoutes import org.broadinstitute.dsde.workbench.sam.azure.AzureJsonSupport._ import org.broadinstitute.dsde.workbench.sam.azure.MockCrlService.mockSamSpendProfileResource import org.broadinstitute.dsde.workbench.sam.config.ManagedAppPlan -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ -import org.broadinstitute.dsde.workbench.sam.model.{SamResourceTypes, SamUser} +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.SamResourceTypes import org.broadinstitute.dsde.workbench.sam.model.api._ import org.broadinstitute.dsde.workbench.sam.service.CloudExtensions import org.broadinstitute.dsde.workbench.sam.{Generator, TestSupport} diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/azure/MockCrlService.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/azure/MockCrlService.scala index 5922809b5..cc61bbbc4 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/azure/MockCrlService.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/azure/MockCrlService.scala @@ -12,7 +12,8 @@ import com.azure.resourcemanager.msi.models.{Identities, Identity} import com.azure.resourcemanager.resources.ResourceManager import com.azure.resourcemanager.resources.models.{ResourceGroup, ResourceGroups} import org.broadinstitute.dsde.workbench.sam.config.ManagedAppPlan -import org.broadinstitute.dsde.workbench.sam.model.{FullyQualifiedResourceId, ResourceId, SamResourceTypes, SamUser} +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser +import org.broadinstitute.dsde.workbench.sam.model.{FullyQualifiedResourceId, ResourceId, SamResourceTypes} import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito.{RETURNS_SMART_NULLS, lenient} 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 00836d980..47971a081 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,8 @@ import org.broadinstitute.dsde.workbench.model.google.ServiceAccountSubjectId 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.{AccessPolicy, BasicWorkbenchGroup, SamUser, SamUserTos} +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser +import org.broadinstitute.dsde.workbench.sam.model.{AccessPolicy, BasicWorkbenchGroup, SamUserTos} import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import java.time.Instant 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 5ebbeedc3..9e87c2004 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 @@ -2,7 +2,8 @@ package org.broadinstitute.dsde.workbench.sam.dataAccess import cats.effect.IO import org.broadinstitute.dsde.workbench.model._ -import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, SamUser} +import org.broadinstitute.dsde.workbench.sam.model.BasicWorkbenchGroup +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.mockito.ArgumentMatchersSugar.{any, eqTo} import org.mockito.{IdiomaticMockito, Strictness} diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/StatefulMockDirectoryDaoBuilder.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/StatefulMockDirectoryDaoBuilder.scala index a084ab3b9..923605c9f 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/StatefulMockDirectoryDaoBuilder.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/dataAccess/StatefulMockDirectoryDaoBuilder.scala @@ -3,7 +3,8 @@ package org.broadinstitute.dsde.workbench.sam.dataAccess import cats.effect.IO import cats.effect.unsafe.implicits.global import org.broadinstitute.dsde.workbench.model._ -import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, SamUser} +import org.broadinstitute.dsde.workbench.sam.model.BasicWorkbenchGroup +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.mockito.ArgumentMatchers import org.mockito.IdiomaticMockito.StubbingOps diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensionRoutesSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensionRoutesSpec.scala index 3f995f92b..bc05a7bee 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensionRoutesSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensionRoutesSpec.scala @@ -17,7 +17,7 @@ import org.broadinstitute.dsde.workbench.sam.TestSupport.{genSamDependencies, ge import org.broadinstitute.dsde.workbench.sam.api.SamRoutes import org.broadinstitute.dsde.workbench.sam.config.GoogleServicesConfig import org.broadinstitute.dsde.workbench.sam.mock.RealKeyMockGoogleIamDAO -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ import org.broadinstitute.dsde.workbench.sam.model.api._ import org.broadinstitute.dsde.workbench.sam.service._ diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensionRoutesV1Spec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensionRoutesV1Spec.scala index 2d4bca345..5f1e391cd 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensionRoutesV1Spec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleExtensionRoutesV1Spec.scala @@ -8,7 +8,7 @@ import cats.effect.unsafe.implicits.global import org.broadinstitute.dsde.workbench.model.WorkbenchIdentityJsonSupport._ import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.sam.TestSupport._ -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport._ import org.broadinstitute.dsde.workbench.sam.model._ import org.broadinstitute.dsde.workbench.sam.model.api._ import org.broadinstitute.dsde.workbench.sam.service._ diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleGroupSyncMessageReceiverSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleGroupSyncMessageReceiverSpec.scala index 056accba6..a8f11745b 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleGroupSyncMessageReceiverSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/google/GoogleGroupSyncMessageReceiverSpec.scala @@ -9,7 +9,7 @@ import com.google.pubsub.v1.PubsubMessage import org.broadinstitute.dsde.workbench.model.WorkbenchIdentityJsonSupport.WorkbenchGroupNameFormat import org.broadinstitute.dsde.workbench.model.{ErrorReport, WorkbenchEmail, WorkbenchExceptionWithErrorReport, WorkbenchGroupName} import org.broadinstitute.dsde.workbench.sam._ -import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport.FullyQualifiedPolicyIdFormat +import org.broadinstitute.dsde.workbench.sam.model.api.SamJsonSupport.FullyQualifiedPolicyIdFormat import org.broadinstitute.dsde.workbench.sam.model._ import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.mockito.ArgumentMatchers diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/matchers/BeForSamUserResponseMatcher.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/matchers/BeForSamUserResponseMatcher.scala new file mode 100644 index 000000000..a7fc08ab7 --- /dev/null +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/matchers/BeForSamUserResponseMatcher.scala @@ -0,0 +1,41 @@ +package org.broadinstitute.dsde.workbench.sam.matchers + +import org.broadinstitute.dsde.workbench.sam.model.api.{SamUser, SamUserResponse} +import org.scalatest.matchers.{MatchResult, Matcher} + +import scala.collection.mutable.ListBuffer + +/** Asserts that the passed UserStatus.userInfo matches the passed in SamUser Id and Email + * + * @param expectedUser: + * user to expect + */ +class BeForSamUserResponseMatcher(expectedUser: SamUser) extends Matcher[SamUserResponse] { + def apply(samUserResponse: SamUserResponse): MatchResult = { + val doEmailsMatch = samUserResponse.email == expectedUser.email + val doIdsMatch = samUserResponse.id == expectedUser.id + + val failureMessageList: ListBuffer[String] = ListBuffer.empty + val failureMessageNegatedList: ListBuffer[String] = ListBuffer.empty + + if (!doEmailsMatch) { + failureMessageList += s"""SamUserResponse email ${samUserResponse.email} did not equal ${expectedUser.email}""" + failureMessageNegatedList += s"""SamUserResponse email ${samUserResponse.email} equals ${expectedUser.email}""" + } + + if (!doIdsMatch) { + failureMessageList += s"""SamUserResponse id ${samUserResponse.id} did not equal ${expectedUser.id}""" + failureMessageNegatedList += s"""SamUserResponse id ${samUserResponse.id} equals ${expectedUser.id}""" + } + + MatchResult( + doEmailsMatch && doIdsMatch, + failureMessageList.mkString(" and "), + failureMessageNegatedList.mkString(" and ") + ) + } +} + +object BeForSamUserResponseMatcher { + def beForUser(expectedUser: SamUser) = new BeForSamUserResponseMatcher(expectedUser) +} diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/matchers/BeForUserMatcher.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/matchers/BeForUserMatcher.scala index 1aa873224..14d6a775c 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/matchers/BeForUserMatcher.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/matchers/BeForUserMatcher.scala @@ -1,6 +1,7 @@ package org.broadinstitute.dsde.workbench.sam.matchers -import org.broadinstitute.dsde.workbench.sam.model.{SamUser, UserStatus} +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser +import org.broadinstitute.dsde.workbench.sam.model.UserStatus import org.scalatest.matchers.{MatchResult, Matcher} import scala.collection.mutable.ListBuffer diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/matchers/BeSameUserMatcher.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/matchers/BeSameUserMatcher.scala index e3d7d18a8..c8547b920 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/matchers/BeSameUserMatcher.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/matchers/BeSameUserMatcher.scala @@ -1,6 +1,6 @@ package org.broadinstitute.dsde.workbench.sam.matchers -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.scalatest.matchers.{MatchResult, Matcher} import scala.collection.mutable.ListBuffer diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/model/UserStatusBuilder.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/model/UserStatusBuilder.scala index 8b3ffe03d..38cf105ca 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/model/UserStatusBuilder.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/model/UserStatusBuilder.scala @@ -1,5 +1,7 @@ package org.broadinstitute.dsde.workbench.sam.model +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser + import scala.collection.mutable case class UserStatusBuilder(user: SamUser) { diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ManagedGroupServiceSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ManagedGroupServiceSpec.scala index e3aba157a..85b90173a 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ManagedGroupServiceSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/ManagedGroupServiceSpec.scala @@ -8,6 +8,7 @@ import org.broadinstitute.dsde.workbench.sam.TestSupport.{databaseEnabled, datab import org.broadinstitute.dsde.workbench.sam.dataAccess.{AccessPolicyDAO, DirectoryDAO, PostgresAccessPolicyDAO, PostgresDirectoryDAO} import org.broadinstitute.dsde.workbench.sam.google.GoogleExtensions import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.{Generator, TestSupport} import org.mockito.ArgumentMatchers import org.mockito.Mockito._ diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockCloudExtensionsBuilder.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockCloudExtensionsBuilder.scala index e28c2fdf5..dbba80809 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockCloudExtensionsBuilder.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockCloudExtensionsBuilder.scala @@ -4,7 +4,7 @@ import cats.effect.IO import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.model.{WorkbenchEmail, WorkbenchGroup, WorkbenchGroupIdentity, WorkbenchUserId} import org.broadinstitute.dsde.workbench.sam.dataAccess.DirectoryDAO -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.broadinstitute.dsde.workbench.util.health.{SubsystemStatus, Subsystems} import org.mockito.ArgumentMatchersSugar.{any, argThat} 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 e54d1c767..b26620abd 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 @@ -1,7 +1,8 @@ package org.broadinstitute.dsde.workbench.sam.service import cats.effect.IO -import org.broadinstitute.dsde.workbench.sam.model.{SamUser, TermsOfServiceComplianceStatus} +import org.broadinstitute.dsde.workbench.sam.model.TermsOfServiceComplianceStatus +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.mockito.Mockito.{RETURNS_SMART_NULLS, lenient} import org.mockito.invocation.InvocationOnMock diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockUserService.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockUserService.scala index eb949f7c4..06e35cdf2 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockUserService.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockUserService.scala @@ -6,7 +6,8 @@ import org.broadinstitute.dsde.workbench.model.google.ServiceAccountSubjectId import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics import org.broadinstitute.dsde.workbench.sam.azure.{ManagedIdentityObjectId, PetManagedIdentity, PetManagedIdentityId} import org.broadinstitute.dsde.workbench.sam.dataAccess.{DirectoryDAO, MockDirectoryDAO} -import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, SamUser} +import org.broadinstitute.dsde.workbench.sam.model.BasicWorkbenchGroup +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import java.util.Date diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockUserServiceBuilder.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockUserServiceBuilder.scala index 4cb11deff..45294e5b6 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockUserServiceBuilder.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/MockUserServiceBuilder.scala @@ -5,8 +5,8 @@ import cats.effect.IO import org.broadinstitute.dsde.workbench.google.errorReportSource import org.broadinstitute.dsde.workbench.model._ import org.broadinstitute.dsde.workbench.sam.TestSupport.enabledMapNoTosAccepted -import org.broadinstitute.dsde.workbench.sam.model.api.AdminUpdateUserRequest -import org.broadinstitute.dsde.workbench.sam.model.{SamUser, UserStatus, UserStatusDetails} +import org.broadinstitute.dsde.workbench.sam.model.api.{AdminUpdateUserRequest, SamUser, SamUserAllowances} +import org.broadinstitute.dsde.workbench.sam.model.{UserStatus, UserStatusDetails} import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.mockito.ArgumentMatchersSugar.{any, eqTo} import org.mockito.{IdiomaticMockito, Strictness} @@ -17,6 +17,7 @@ case class MockUserServiceBuilder() extends IdiomaticMockito { private val enabledUsers: mutable.Set[SamUser] = mutable.Set.empty private val disabledUsers: mutable.Set[SamUser] = mutable.Set.empty + private val allowedUsers: mutable.Set[SamUser] = mutable.Set.empty private var isBadEmail = false private def existingUsers: mutable.Set[SamUser] = @@ -36,6 +37,13 @@ case class MockUserServiceBuilder() extends IdiomaticMockito { this } + def withAllowedUser(samUser: SamUser): MockUserServiceBuilder = withAllowedUsers(Set(samUser)) + + def withAllowedUsers(samUsers: Iterable[SamUser]): MockUserServiceBuilder = { + allowedUsers.addAll(samUsers) + this + } + // TODO: Need to figure out how to have a matcher accept an update user request with a bad email def withBadEmail(): MockUserServiceBuilder = { isBadEmail = true @@ -62,6 +70,10 @@ case class MockUserServiceBuilder() extends IdiomaticMockito { ) returns { IO(Set.empty) } + + mockUserService.getUserAllowances(any[SamUser], any[SamRequestContext]) returns IO( + SamUserAllowances(allowed = false, enabled = false, termsOfService = false) + ) } private def makeUser(samUser: SamUser, mockUserService: UserService): Unit = { @@ -151,6 +163,11 @@ case class MockUserServiceBuilder() extends IdiomaticMockito { IO(Option(UserStatus(UserStatusDetails(samUser.id, samUser.email), enabledMapNoTosAccepted))) } + private def makeUserAppearAllowed(samUser: SamUser, mockUserService: UserService): Unit = + mockUserService.getUserAllowances(eqTo(samUser), any[SamRequestContext]) returns IO( + SamUserAllowances(allowed = true, enabled = true, termsOfService = true) + ) + private def handleMalformedEmail(mockUserService: UserService): Unit = if (isBadEmail) { mockUserService.updateUserCrud(any[WorkbenchUserId], any[AdminUpdateUserRequest], any[SamRequestContext]) returns { @@ -167,6 +184,7 @@ case class MockUserServiceBuilder() extends IdiomaticMockito { makeUsers(existingUsers, mockUserService) enabledUsers.foreach(u => makeUserAppearEnabled(u, mockUserService)) disabledUsers.foreach(u => makeUserAppearDisabled(u, mockUserService)) + allowedUsers.foreach(u => makeUserAppearAllowed(u, mockUserService)) handleMalformedEmail(mockUserService) mockUserService } diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/StatefulMockCloudExtensionsBuilder.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/StatefulMockCloudExtensionsBuilder.scala index d603c2f5b..8cbe8d59b 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/StatefulMockCloudExtensionsBuilder.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/StatefulMockCloudExtensionsBuilder.scala @@ -4,7 +4,7 @@ import cats.effect.IO import cats.effect.unsafe.implicits.global import org.broadinstitute.dsde.workbench.model.{WorkbenchGroup, WorkbenchGroupIdentity, WorkbenchUserId} import org.broadinstitute.dsde.workbench.sam.dataAccess.DirectoryDAO -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.mockito.Mockito.{RETURNS_SMART_NULLS, lenient} import org.mockito.invocation.InvocationOnMock diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/TestUserServiceBuilder.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/TestUserServiceBuilder.scala index f8de89186..089478219 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/TestUserServiceBuilder.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/TestUserServiceBuilder.scala @@ -4,7 +4,7 @@ import cats.effect.IO import org.broadinstitute.dsde.workbench.model.WorkbenchGroup import org.broadinstitute.dsde.workbench.openTelemetry.OpenTelemetryMetrics import org.broadinstitute.dsde.workbench.sam.dataAccess.{DirectoryDAO, StatefulMockDirectoryDaoBuilder} -import org.broadinstitute.dsde.workbench.sam.model.SamUser +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import scala.collection.mutable import scala.concurrent.ExecutionContext 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 7b6a287e7..5040270c5 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 @@ -14,6 +14,7 @@ import org.broadinstitute.dsde.workbench.sam.google.GoogleExtensions import org.broadinstitute.dsde.workbench.sam.matchers.BeSameUserMatcher.beSameUserAs import org.broadinstitute.dsde.workbench.sam.matchers.TimeMatchers import org.broadinstitute.dsde.workbench.sam.model._ +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.UserServiceSpecs.{CreateUserSpec, GetUserStatusSpec, InviteUserSpec} import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.mockito.Mockito diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpecs/AllowedUserSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpecs/AllowedUserSpec.scala new file mode 100644 index 000000000..b6cf56218 --- /dev/null +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpecs/AllowedUserSpec.scala @@ -0,0 +1,74 @@ +package org.broadinstitute.dsde.workbench.sam.service.UserServiceSpecs + +import org.broadinstitute.dsde.workbench.model.WorkbenchEmail +import org.broadinstitute.dsde.workbench.sam.Generator.genWorkbenchUserBoth +import org.broadinstitute.dsde.workbench.sam.dataAccess.{DirectoryDAO, MockDirectoryDaoBuilder} +import org.broadinstitute.dsde.workbench.sam.model.BasicWorkbenchGroup +import org.broadinstitute.dsde.workbench.sam.model.api.SamUserAllowances +import org.broadinstitute.dsde.workbench.sam.service.{CloudExtensions, MockCloudExtensionsBuilder, MockTosServiceBuilder, TosService, UserService} + +import scala.concurrent.ExecutionContextExecutor + +class AllowedUserSpec extends UserServiceTestTraits { + implicit val ec: ExecutionContextExecutor = scala.concurrent.ExecutionContext.global + + val allUsersGroup: BasicWorkbenchGroup = BasicWorkbenchGroup(CloudExtensions.allUsersGroupName, Set(), WorkbenchEmail("all_users@fake.com")) + + val directoryDAO: DirectoryDAO = MockDirectoryDaoBuilder(allUsersGroup).build + val cloudExtensions: CloudExtensions = MockCloudExtensionsBuilder(allUsersGroup).build + + describe("an enabled user") { + describe("who has accepted the Terms of Service") { + val userWithBothIds = genWorkbenchUserBoth.sample.get.copy(enabled = true) + val tosService: TosService = MockTosServiceBuilder().withAllAccepted().build + val userService: UserService = new UserService(directoryDAO, cloudExtensions, Seq.empty, tosService) + it("should be allowed to use the system") { + // Arrange + // Act + val response = runAndWait(userService.getUserAllowances(userWithBothIds, samRequestContext)) + + // Assert + response should be(SamUserAllowances(allowed = true, enabled = true, termsOfService = true)) + } + } + describe("who has not accepted the Terms of Service") { + val userWithBothIds = genWorkbenchUserBoth.sample.get.copy(enabled = true) + it("should not be allowed to use the system") { + // Arrange + val tosService: TosService = MockTosServiceBuilder().withNoneAccepted().build + val userService: UserService = new UserService(directoryDAO, cloudExtensions, Seq.empty, tosService) + // Act + val response = runAndWait(userService.getUserAllowances(userWithBothIds, samRequestContext)) + + // Assert + response should be(SamUserAllowances(allowed = false, enabled = true, termsOfService = false)) + } + } + } + describe("a disabled user") { + val userWithBothIds = genWorkbenchUserBoth.sample.get.copy(enabled = false) + it("should not be able to use the system") { + // Arrange + val tosService: TosService = MockTosServiceBuilder().withNoneAccepted().build + val userService: UserService = new UserService(directoryDAO, cloudExtensions, Seq.empty, tosService) + + // Act + val response = runAndWait(userService.getUserAllowances(userWithBothIds, samRequestContext)) + + // Assert + response should be(SamUserAllowances(allowed = false, enabled = false, termsOfService = false)) + } + it("should not be able to use the system even if the Terms of Service permits them to") { + // Arrange + val tosService: TosService = MockTosServiceBuilder().withAllAccepted().build + val userService: UserService = new UserService(directoryDAO, cloudExtensions, Seq.empty, tosService) + + // Act + val response = runAndWait(userService.getUserAllowances(userWithBothIds, samRequestContext)) + + // Assert + response should be(SamUserAllowances(allowed = false, enabled = false, termsOfService = true)) + } + } + +} diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpecs/CreateUserSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpecs/CreateUserSpec.scala index 9616276dd..12a28e64d 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpecs/CreateUserSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpecs/CreateUserSpec.scala @@ -5,7 +5,8 @@ import org.broadinstitute.dsde.workbench.sam.Generator.{genWorkbenchUserAzure, g import org.broadinstitute.dsde.workbench.sam.dataAccess.{DirectoryDAO, MockDirectoryDaoBuilder} import org.broadinstitute.dsde.workbench.sam.matchers.BeEnabledInMatcher.beEnabledIn import org.broadinstitute.dsde.workbench.sam.matchers.BeForUserMatcher.beForUser -import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, SamUser} +import org.broadinstitute.dsde.workbench.sam.model.BasicWorkbenchGroup +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service._ import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext import org.mockito.ArgumentMatchersSugar.{any, eqTo} diff --git a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpecs/InviteUserSpec.scala b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpecs/InviteUserSpec.scala index 2b9d1bd33..c8a06cf3f 100644 --- a/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpecs/InviteUserSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/workbench/sam/service/UserServiceSpecs/InviteUserSpec.scala @@ -2,7 +2,8 @@ package org.broadinstitute.dsde.workbench.sam.service.UserServiceSpecs import org.broadinstitute.dsde.workbench.model.WorkbenchEmail import org.broadinstitute.dsde.workbench.sam.Generator.{genWorkbenchUserBoth, genWorkbenchUserGoogle} -import org.broadinstitute.dsde.workbench.sam.model.{BasicWorkbenchGroup, SamUser} +import org.broadinstitute.dsde.workbench.sam.model.BasicWorkbenchGroup +import org.broadinstitute.dsde.workbench.sam.model.api.SamUser import org.broadinstitute.dsde.workbench.sam.service.GenEmail.genBadChar import org.broadinstitute.dsde.workbench.sam.service.{CloudExtensions, TestUserServiceBuilder} import org.broadinstitute.dsde.workbench.sam.util.SamRequestContext