Skip to content

Commit

Permalink
ID-816 ID-817 New User Registration Endpoint (#1240)
Browse files Browse the repository at this point in the history
* ID-816 New User Registration Endpoint
  • Loading branch information
tlangs authored Nov 16, 2023
1 parent 655636e commit 37cbf6e
Show file tree
Hide file tree
Showing 13 changed files with 168 additions and 21 deletions.
36 changes: 36 additions & 0 deletions src/main/resources/swagger/api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5807,6 +5807,34 @@ paths:
description: OK
content: {}
x-passthrough: false
/api/users/v1/registerWithProfile:
post:
tags:
- Users
summary: Registers the user in Sam and saves the user profile in Thurloe
operationId: registerUserWithProfile
requestBody:
description: A registration request
content:
'application/json':
schema:
$ref: '#/components/schemas/RegisterRequest'
required: false
responses:
200:
description: OK
content: {}
400:
description: Bad request
content: {}
403:
description: Forbidden
content: {}
500:
description: Internal server error
content: {}
x-passthrough: false
x-codegen-request-body-name: profile
/tos/text:
get:
tags:
Expand Down Expand Up @@ -7880,6 +7908,14 @@ components:
type: array
items:
$ref: '#/components/schemas/KeyValuePair'
RegisterRequest:
type: object
properties:
acceptsTermsOfService:
type: boolean
description: Does the user accept the Terra Terms of Service?
profile:
$ref: '#/components/schemas/Profile'
ResearchPurpose:
required:
- DS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ trait FireCloudApiService extends CookieAuthedApiService
def apiRoutes: server.Route =
options { complete(StatusCodes.OK) } ~
withExecutionContext(ExecutionContext.global) {
v1RegisterRoutes ~
methodsApiServiceRoutes ~
profileRoutes ~
cromIamApiServiceRoutes ~
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(acceptsTermsOfService: Boolean)(implicit userInfo: WithAccessToken): Future[SamUserResponse] = {
authedRequestToObject[SamUserResponse](Post(samUserRegisterSelfUrl, SamUserRegistrationRequest(acceptsTermsOfService, 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 impSamUserAttributesRequest: RootJsonFormat[SamUserAttributesRequest] = jsonFormat1(SamUserAttributesRequest)
implicit val impSamUserRegistrationRequest: RootJsonFormat[SamUserRegistrationRequest] = jsonFormat2(SamUserRegistrationRequest)

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 v1RegisterRoutes: 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(v1RegisterRoutes) ~> 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(v1RegisterRoutes) ~> check {
status should be(OK)
}
}
}

Expand Down

0 comments on commit 37cbf6e

Please sign in to comment.