diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/FireCloudApiService.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/FireCloudApiService.scala index 8438aab9e..06dfa3602 100644 --- a/src/main/scala/org/broadinstitute/dsde/firecloud/FireCloudApiService.scala +++ b/src/main/scala/org/broadinstitute/dsde/firecloud/FireCloudApiService.scala @@ -184,7 +184,8 @@ trait FireCloudApiService extends CookieAuthedApiService billingServiceRoutes ~ shareLogServiceRoutes ~ staticNotebooksRoutes ~ - perimeterServiceRoutes + perimeterServiceRoutes ~ + newRegisterRoutes } val routeWrappers: Directive[Unit] = diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/dataaccess/HttpSamDAO.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/dataaccess/HttpSamDAO.scala index 8c92ee0ec..3cd207726 100644 --- a/src/main/scala/org/broadinstitute/dsde/firecloud/dataaccess/HttpSamDAO.scala +++ b/src/main/scala/org/broadinstitute/dsde/firecloud/dataaccess/HttpSamDAO.scala @@ -12,7 +12,7 @@ import org.broadinstitute.dsde.firecloud.model.ErrorReportExtensions.FCErrorRepo import org.broadinstitute.dsde.firecloud.model.ManagedGroupRoles.ManagedGroupRole import org.broadinstitute.dsde.firecloud.model.ModelJsonProtocol._ import org.broadinstitute.dsde.firecloud.model.SamResource.UserPolicy -import org.broadinstitute.dsde.firecloud.model.{AccessToken, FireCloudManagedGroupMembership, ManagedGroupRoles, RegistrationInfo, RegistrationInfoV2, UserIdInfo, UserInfo, WithAccessToken} +import org.broadinstitute.dsde.firecloud.model.{AccessToken, FireCloudManagedGroupMembership, ManagedGroupRoles, RegistrationInfo, RegistrationInfoV2, SamUserAttributesRequest, SamUserRegistrationRequest, SamUserResponse, UserIdInfo, UserInfo, WithAccessToken} import org.broadinstitute.dsde.firecloud.utils.RestJsonClient import org.broadinstitute.dsde.rawls.model.RawlsUserEmail import org.broadinstitute.dsde.workbench.model.WorkbenchIdentityJsonSupport._ @@ -38,6 +38,10 @@ class HttpSamDAO( implicit val system: ActorSystem, val materializer: Materializ authedRequestToObject[RegistrationInfo](Post(samUserRegistrationUrl, termsOfService), label=Some("HttpSamDAO.registerUser")) } + override def registerUserSelf(termsOfService: Boolean)(implicit userInfo: WithAccessToken): Future[SamUserResponse] = { + authedRequestToObject[SamUserResponse](Post(samUserRegisterSelfUrl, SamUserRegistrationRequest(termsOfService, SamUserAttributesRequest(marketingConsent = Some(false)))), label = Some("HttpSamDAO.registerUserSelf")) + } + override def getRegistrationStatus(implicit userInfo: WithAccessToken): Future[RegistrationInfo] = { authedRequestToObject[RegistrationInfo](Get(samUserRegistrationUrl), label=Some("HttpSamDAO.getRegistrationStatus")) } diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/dataaccess/SamDAO.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/dataaccess/SamDAO.scala index 0a6e47856..46d71cce3 100644 --- a/src/main/scala/org/broadinstitute/dsde/firecloud/dataaccess/SamDAO.scala +++ b/src/main/scala/org/broadinstitute/dsde/firecloud/dataaccess/SamDAO.scala @@ -6,7 +6,7 @@ import com.typesafe.scalalogging.LazyLogging import org.broadinstitute.dsde.firecloud.FireCloudConfig import org.broadinstitute.dsde.firecloud.model.ManagedGroupRoles.ManagedGroupRole import org.broadinstitute.dsde.firecloud.model.SamResource.UserPolicy -import org.broadinstitute.dsde.firecloud.model.{AccessToken, FireCloudManagedGroupMembership, RegistrationInfo, RegistrationInfoV2, UserIdInfo, UserInfo, WithAccessToken} +import org.broadinstitute.dsde.firecloud.model.{AccessToken, FireCloudManagedGroupMembership, RegistrationInfo, RegistrationInfoV2, SamUserResponse, UserIdInfo, UserInfo, WithAccessToken} import org.broadinstitute.dsde.rawls.model.{ErrorReportSource, RawlsUserEmail} import org.broadinstitute.dsde.workbench.model.google.GoogleProject import org.broadinstitute.dsde.workbench.model.{WorkbenchEmail, WorkbenchGroupName} @@ -29,6 +29,7 @@ trait SamDAO extends LazyLogging with ReportsSubsystemStatus { val managedGroupResourceTypeName = "managed-group" val samUserRegistrationUrl = FireCloudConfig.Sam.baseUrl + "/register/user" + val samUserRegisterSelfUrl = FireCloudConfig.Sam.baseUrl + "/api/users/v2/self/register" val samStatusUrl = FireCloudConfig.Sam.baseUrl + "/status" val samGetUserIdsUrl = FireCloudConfig.Sam.baseUrl + "/api/users/v1/%s" val samArbitraryPetTokenUrl = FireCloudConfig.Sam.baseUrl + "/api/google/v1/user/petServiceAccount/token" @@ -53,6 +54,8 @@ trait SamDAO extends LazyLogging with ReportsSubsystemStatus { def samListResources(resourceTypeName: String): String = samResourcesBase + s"/$resourceTypeName" def registerUser(termsOfService: Option[String])(implicit userInfo: WithAccessToken): Future[RegistrationInfo] + + def registerUserSelf(acceptsTermsOfService: Boolean)(implicit userInfo: WithAccessToken): Future[SamUserResponse] def getRegistrationStatus(implicit userInfo: WithAccessToken): Future[RegistrationInfo] def getUserIds(email: RawlsUserEmail)(implicit userInfo: WithAccessToken): Future[UserIdInfo] diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/model/ModelJsonProtocol.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/model/ModelJsonProtocol.scala index b047a549f..82b4c9bcd 100644 --- a/src/main/scala/org/broadinstitute/dsde/firecloud/model/ModelJsonProtocol.scala +++ b/src/main/scala/org/broadinstitute/dsde/firecloud/model/ModelJsonProtocol.scala @@ -220,6 +220,10 @@ object ModelJsonProtocol extends WorkspaceJsonSupport with SprayJsonSupport { implicit val impTerraPreference: RootJsonFormat[TerraPreference] = jsonFormat2(TerraPreference) implicit val impShibbolethToken: RootJsonFormat[ShibbolethToken] = jsonFormat2(ShibbolethToken) + implicit val impRegisterRequest: RootJsonFormat[RegisterRequest] = jsonFormat2(RegisterRequest) + implicit val impSamUserRegistrationRequest: RootJsonFormat[SamUserRegistrationRequest] = jsonFormat2(SamUserRegistrationRequest) + implicit val impSamUserAttributesRequest: RootJsonFormat[SamUserAttributesRequest] = jsonFormat1(SamUserAttributesRequest) + implicit val impJWTWrapper: RootJsonFormat[JWTWrapper] = jsonFormat1(JWTWrapper) implicit val impOAuthUser: RootJsonFormat[OAuthUser] = jsonFormat2(OAuthUser) @@ -229,6 +233,7 @@ object ModelJsonProtocol extends WorkspaceJsonSupport with SprayJsonSupport { implicit val impWorkbenchEnabledV2: RootJsonFormat[WorkbenchEnabledV2] = jsonFormat3(WorkbenchEnabledV2) implicit val impRegistrationInfo: RootJsonFormat[RegistrationInfo] = jsonFormat3(RegistrationInfo) implicit val impRegistrationInfoV2: RootJsonFormat[RegistrationInfoV2] = jsonFormat3(RegistrationInfoV2) + implicit val impSamUserResponse: RootJsonFormat[SamUserResponse] = jsonFormat8(SamUserResponse) implicit val impUserIdInfo: RootJsonFormat[UserIdInfo] = jsonFormat3(UserIdInfo) implicit val impCurator: RootJsonFormat[Curator] = jsonFormat1(Curator) implicit val impUserImportPermission: RootJsonFormat[UserImportPermission] = jsonFormat2(UserImportPermission) diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/model/RegisterRequest.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/model/RegisterRequest.scala new file mode 100644 index 000000000..3618da58d --- /dev/null +++ b/src/main/scala/org/broadinstitute/dsde/firecloud/model/RegisterRequest.scala @@ -0,0 +1,3 @@ +package org.broadinstitute.dsde.firecloud.model + +case class RegisterRequest(acceptsTermsOfService: Boolean, profile: BasicProfile) diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/model/SamUserAttributesRequest.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/model/SamUserAttributesRequest.scala new file mode 100644 index 000000000..7faa34da4 --- /dev/null +++ b/src/main/scala/org/broadinstitute/dsde/firecloud/model/SamUserAttributesRequest.scala @@ -0,0 +1,3 @@ +package org.broadinstitute.dsde.firecloud.model + +case class SamUserAttributesRequest(marketingConsent: Option[Boolean]) diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/model/SamUserRegistrationRequest.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/model/SamUserRegistrationRequest.scala new file mode 100644 index 000000000..97d59c6fc --- /dev/null +++ b/src/main/scala/org/broadinstitute/dsde/firecloud/model/SamUserRegistrationRequest.scala @@ -0,0 +1,8 @@ +package org.broadinstitute.dsde.firecloud.model + +import org.broadinstitute.dsde.workbench.model.ErrorReport + +case class SamUserRegistrationRequest( + acceptsTermsOfService: Boolean, + userAttributes: SamUserAttributesRequest + ) diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/model/SamUserResponse.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/model/SamUserResponse.scala new file mode 100644 index 000000000..8328b91f0 --- /dev/null +++ b/src/main/scala/org/broadinstitute/dsde/firecloud/model/SamUserResponse.scala @@ -0,0 +1,16 @@ +package org.broadinstitute.dsde.firecloud.model + +import org.broadinstitute.dsde.workbench.model.{AzureB2CId, GoogleSubjectId, WorkbenchEmail, WorkbenchUserId} + +import java.time.Instant + +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/firecloud/service/RegisterService.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/service/RegisterService.scala index 4f22e825b..9b5fa3974 100644 --- a/src/main/scala/org/broadinstitute/dsde/firecloud/service/RegisterService.scala +++ b/src/main/scala/org/broadinstitute/dsde/firecloud/service/RegisterService.scala @@ -28,6 +28,16 @@ object RegisterService { class RegisterService(val rawlsDao: RawlsDAO, val samDao: SamDAO, val thurloeDao: ThurloeDAO, val googleServicesDAO: GoogleServicesDAO) (implicit protected val executionContext: ExecutionContext) extends LazyLogging { + def createUserWithProfile(userInfo: UserInfo, registerRequest: RegisterRequest): Future[PerRequestMessage] = + for { + registerResult <- registerUser(userInfo, registerRequest.acceptsTermsOfService) + _ <- thurloeDao.saveProfile(userInfo, registerRequest.profile) + _ <- thurloeDao.saveKeyValues(userInfo, Map("isRegistrationComplete" -> Profile.currentVersion.toString)) + _ <- if (!registerResult.allowed) { + thurloeDao.saveKeyValues(userInfo, Map("email" -> userInfo.userEmail)) + } else Future.successful() + } yield RequestComplete(StatusCodes.OK, registerResult) + def createUpdateProfile(userInfo: UserInfo, basicProfile: BasicProfile): Future[PerRequestMessage] = { for { _ <- thurloeDao.saveProfile(userInfo, basicProfile) @@ -70,6 +80,13 @@ class RegisterService(val rawlsDao: RawlsDAO, val samDao: SamDAO, val thurloeDao } } + private def registerUser(userInfo: UserInfo, acceptsTermsOfService: Boolean): Future[SamUserResponse] = { + for { + userResponse <- samDao.registerUserSelf(acceptsTermsOfService)(userInfo) + _ <- googleServicesDAO.publishMessages(FireCloudConfig.Notification.fullyQualifiedNotificationTopic, Seq(NotificationFormat.write(generateWelcomeEmail(userInfo)).compactPrint)) + } yield userResponse + } + // utility method to determine if a preferences key headed to Thurloe is valid for user input. private def isValidPreferenceKey(key: String): Boolean = { val validKeyPrefixes = FireCloudConfig.Thurloe.validPreferenceKeyPrefixes diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/RegisterApiService.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/RegisterApiService.scala index bcaf5cef8..8c4603893 100644 --- a/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/RegisterApiService.scala +++ b/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/RegisterApiService.scala @@ -18,6 +18,19 @@ trait RegisterApiService extends FireCloudDirectives with EnabledUserDirectives val registerServiceConstructor: () => RegisterService + val newRegisterRoutes: Route = + pathPrefix("users" / "v1" / "registerWithProfile") { + post { + requireUserInfo() { userInfo => + entity(as[RegisterRequest]) { registerRequest => + complete { + registerServiceConstructor().createUserWithProfile(userInfo, registerRequest) + } + } + } + } + } + val registerRoutes: Route = pathPrefix("register") { path("profile") { diff --git a/src/test/scala/org/broadinstitute/dsde/firecloud/dataaccess/MockSamDAO.scala b/src/test/scala/org/broadinstitute/dsde/firecloud/dataaccess/MockSamDAO.scala index 3135f2f93..5ee1b8ce0 100644 --- a/src/test/scala/org/broadinstitute/dsde/firecloud/dataaccess/MockSamDAO.scala +++ b/src/test/scala/org/broadinstitute/dsde/firecloud/dataaccess/MockSamDAO.scala @@ -4,12 +4,13 @@ import akka.http.scaladsl.model.StatusCodes import org.broadinstitute.dsde.firecloud.{FireCloudException, FireCloudExceptionWithErrorReport} import org.broadinstitute.dsde.firecloud.HealthChecks.termsOfServiceUrl import org.broadinstitute.dsde.firecloud.model.ManagedGroupRoles.ManagedGroupRole -import org.broadinstitute.dsde.firecloud.model.{AccessToken, FireCloudManagedGroupMembership, RegistrationInfo, RegistrationInfoV2, SamResource, UserIdInfo, UserInfo, WithAccessToken, WorkbenchEnabled, WorkbenchUserInfo} +import org.broadinstitute.dsde.firecloud.model.{AccessToken, FireCloudManagedGroupMembership, RegistrationInfo, RegistrationInfoV2, SamResource, SamUserResponse, UserIdInfo, UserInfo, WithAccessToken, WorkbenchEnabled, WorkbenchUserInfo} import org.broadinstitute.dsde.workbench.util.health.SubsystemStatus import org.broadinstitute.dsde.rawls.model.{ErrorReport, RawlsUserEmail} import org.broadinstitute.dsde.workbench.model.google.GoogleProject -import org.broadinstitute.dsde.workbench.model.{WorkbenchEmail, WorkbenchGroupName} +import org.broadinstitute.dsde.workbench.model.{AzureB2CId, GoogleSubjectId, WorkbenchEmail, WorkbenchGroupName, WorkbenchUserId} +import java.time.Instant import scala.concurrent.Future /** @@ -82,6 +83,18 @@ class MockSamDAO extends SamDAO { Future.successful(()) } + private val registeredUser = Future.successful { + SamUserResponse( + id = WorkbenchUserId("foo"), + googleSubjectId = Some(GoogleSubjectId("bar")), + email = WorkbenchEmail("ltcommanderdata@neighborhood.horse"), + azureB2CId = Some(AzureB2CId("baz")), + allowed = true, + createdAt = Instant.now(), + registeredAt = Some(Instant.now()), + updatedAt = Instant.now() + ) + } private val enabledUserInfo = Future.successful { RegistrationInfo( @@ -121,4 +134,12 @@ class MockSamDAO extends SamDAO { override def getPetServiceAccountKeyForUser(user: WithAccessToken, project: GoogleProject): Future[String] = Future.successful("""{"fake":"key""}""") override def setPolicyPublic(resourceTypeName: String, resourceId: String, policyName: String, public: Boolean)(implicit userInfo: WithAccessToken): Future[Unit] = Future.successful(()) + + override def registerUserSelf(acceptsTermsOfService: Boolean)(implicit userInfo: WithAccessToken): Future[SamUserResponse] = { + if (acceptsTermsOfService) { + registeredUser + } else { + Future.failed(new FireCloudExceptionWithErrorReport(new ErrorReport("sam", "invalid", Some(StatusCodes.BadRequest), Seq.empty, Seq.empty, None))) + } + } } diff --git a/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/RegisterApiServiceSpec.scala b/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/RegisterApiServiceSpec.scala index d51f3c151..771f3d8b1 100644 --- a/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/RegisterApiServiceSpec.scala +++ b/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/RegisterApiServiceSpec.scala @@ -2,7 +2,7 @@ package org.broadinstitute.dsde.firecloud.webservice import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import org.broadinstitute.dsde.firecloud.dataaccess.MockThurloeDAO -import org.broadinstitute.dsde.firecloud.model.{BasicProfile, UserInfo} +import org.broadinstitute.dsde.firecloud.model.{BasicProfile, RegisterRequest, UserInfo, WithAccessToken} import org.broadinstitute.dsde.firecloud.service.{BaseServiceSpec, RegisterService, UserService} import akka.http.scaladsl.model.StatusCodes.{BadRequest, Forbidden, NoContent, NotFound, OK} import akka.http.scaladsl.model.StatusCode @@ -10,6 +10,7 @@ import akka.http.scaladsl.server.Route.{seal => sealRoute} import org.broadinstitute.dsde.firecloud.HealthChecks.termsOfServiceUrl import org.broadinstitute.dsde.firecloud.mock.{MockUtils, SamMockserverUtils} import org.broadinstitute.dsde.firecloud.model.ModelJsonProtocol.impBasicProfile +import org.broadinstitute.dsde.firecloud.model.ModelJsonProtocol.impRegisterRequest import org.mockserver.integration.ClientAndServer import org.mockserver.integration.ClientAndServer.startClientAndServer import org.mockserver.model.Header @@ -78,6 +79,24 @@ final class RegisterApiServiceSpec extends BaseServiceSpec with RegisterApiServi override val userServiceConstructor:(UserInfo) => UserService = UserService.constructor(app.copy(thurloeDAO = new RegisterApiServiceSpecThurloeDAO)) + def makeBasicProfile(hasTermsOfService: Boolean): BasicProfile = { + val randomString = MockUtils.randomAlpha() + BasicProfile( + firstName = randomString, + lastName = randomString, + title = randomString, + contactEmail = Some("me@abc.com"), + institute = randomString, + researchArea = Some(randomString), + programLocationCity = randomString, + programLocationState = randomString, + programLocationCountry = randomString, + termsOfService = if (hasTermsOfService) Some(termsOfServiceUrl) else None, + department = Some(randomString), + interestInTerra = Some(randomString) + ) + } + "RegisterApiService" - { "update-preferences API" - { @@ -151,23 +170,21 @@ final class RegisterApiServiceSpec extends BaseServiceSpec with RegisterApiServi status should be(OK) } } + } - def makeBasicProfile(hasTermsOfService: Boolean): BasicProfile = { - val randomString = MockUtils.randomAlpha() - BasicProfile( - firstName = randomString, - lastName = randomString, - title = randomString, - contactEmail = Some("me@abc.com"), - institute = randomString, - researchArea = Some(randomString), - programLocationCity = randomString, - programLocationState = randomString, - programLocationCountry = randomString, - termsOfService = if (hasTermsOfService) Some(termsOfServiceUrl) else None, - department = Some(randomString), - interestInTerra = Some(randomString) - ) + "register-with-profile API POST" - { + "should fail if Sam does not register the user" in { + val payload = makeBasicProfile(false) + Post("/users/v1/registerWithProfile", RegisterRequest(acceptsTermsOfService = false, profile = payload)) ~> dummyUserIdHeaders("RegisterApiServiceSpec", "new") ~> sealRoute(newRegisterRoutes) ~> check { + status should be(BadRequest) + } + } + + "should succeed if Sam does register the user" in { + val payload = makeBasicProfile(true) + Post("/users/v1/registerWithProfile", RegisterRequest(acceptsTermsOfService = true, profile = payload)) ~> dummyUserIdHeaders("RegisterApiServiceSpec", "new") ~> sealRoute(newRegisterRoutes) ~> check { + status should be(OK) + } } }