Skip to content

Commit

Permalink
ID-816 New User Registration Endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
tlangs committed Nov 9, 2023
1 parent 0886960 commit 4be46c4
Show file tree
Hide file tree
Showing 12 changed files with 133 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ trait FireCloudApiService extends CookieAuthedApiService
billingServiceRoutes ~
shareLogServiceRoutes ~
staticNotebooksRoutes ~
perimeterServiceRoutes
perimeterServiceRoutes ~
newRegisterRoutes
}

val routeWrappers: Directive[Unit] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand All @@ -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"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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"
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.broadinstitute.dsde.firecloud.model

case class RegisterRequest(acceptsTermsOfService: Boolean, profile: BasicProfile)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.broadinstitute.dsde.firecloud.model

case class SamUserAttributesRequest(marketingConsent: Option[Boolean])
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.broadinstitute.dsde.firecloud.model

import org.broadinstitute.dsde.workbench.model.ErrorReport

case class SamUserRegistrationRequest(
acceptsTermsOfService: Boolean,
userAttributes: SamUserAttributesRequest
)
Original file line number Diff line number Diff line change
@@ -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
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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("[email protected]"),
azureB2CId = Some(AzureB2CId("baz")),
allowed = true,
createdAt = Instant.now(),
registeredAt = Some(Instant.now()),
updatedAt = Instant.now()
)
}

private val enabledUserInfo = Future.successful {
RegistrationInfo(
Expand Down Expand Up @@ -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)))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ 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
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
Expand Down Expand Up @@ -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("[email protected]"),
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" - {

Expand Down Expand Up @@ -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("[email protected]"),
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)
}
}
}

Expand Down

0 comments on commit 4be46c4

Please sign in to comment.