Skip to content

Commit

Permalink
Merge e4bbad7 into 62a3d10
Browse files Browse the repository at this point in the history
  • Loading branch information
Qi77Qi committed Aug 22, 2018
2 parents 62a3d10 + e4bbad7 commit 17b4ab6
Show file tree
Hide file tree
Showing 38 changed files with 1,024 additions and 1,039 deletions.
2 changes: 1 addition & 1 deletion automation/project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ object Dependencies {
val akkaV = "2.5.7"
val akkaHttpV = "10.0.10"

val workbenchModelV = "0.10-6800f3a"
val workbenchModelV = "0.11-7657779-SNAP"
val workbenchGoogleV = "0.16-847c3ff"
val workbenchServiceTestV = "0.10-61981d5"

Expand Down
2 changes: 1 addition & 1 deletion docker/stand-alone/sam.conf
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ schemaLock {
recheckTimeInterval = 5
maxTimeToWait = 60
instanceId = instance1
schemaVersion = 1
schemaVersion = 2
}

emailDomain = ${EMAIL_DOMAIN}
2 changes: 1 addition & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ object Dependencies {
val scalaTestV = "3.0.1"

val workbenchUtilV = "0.2-d6801ce"
val workbenchModelV = "0.10-8e9ac2a"
val workbenchModelV = "0.11-7657779-SNAP"
val workbenchGoogleV = "0.16-f339f30"
val workbenchNotificationsV = "0.1-21c2127"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ trait ResourceRoutes extends UserInfoDirectives with SecurityDirectives with Sam

def withResourceType(name: ResourceTypeName): Directive1[ResourceType] = {
onSuccess(resourceService.getResourceType(name)).map {
case Some(resourceType) => resourceType
case Some(resourceType) =>
resourceType
case None => throw new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.NotFound, s"resource type ${name.value} not found"))
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.broadinstitute.dsde.workbench.sam.api
package org.broadinstitute.dsde.workbench.sam
package api

import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.headers.OAuth2BearerToken
import akka.http.scaladsl.server.Directives.{headerValueByName, onSuccess, optionalHeaderValueByName}
import akka.http.scaladsl.server._
import akka.http.scaladsl.server.Directives.{headerValueByName, onSuccess}
import org.broadinstitute.dsde.workbench.model._
import akka.http.scaladsl.server.Directives.headerValueByName
import org.broadinstitute.dsde.workbench.model.google.ServiceAccountSubjectId
import org.broadinstitute.dsde.workbench.model.{WorkbenchEmail, WorkbenchUserId}

import org.broadinstitute.dsde.workbench.model.{WorkbenchEmail, _}
import akka.http.scaladsl.server.directives.OnSuccessMagnet._
import scala.concurrent.{ExecutionContext, Future}


Expand All @@ -17,30 +17,59 @@ trait StandardUserInfoDirectives extends UserInfoDirectives {

val petSAdomain = "\\S+@\\S+\\.iam\\.gserviceaccount\\.com".r

private def isPetSA(email: WorkbenchEmail) = {
def isPetSA(email: WorkbenchEmail) = {
petSAdomain.pattern.matcher(email.value).matches
}

def requireUserInfo: Directive1[UserInfo] = (
headerValueByName("OIDC_access_token") &
headerValueByName("OIDC_CLAIM_user_id") &
headerValueByName("OIDC_CLAIM_expires_in") &
headerValueByName("OIDC_CLAIM_email")
headerValueByName(accessTokenHeader) &
// headerValueByName(userIdHeader) & TODO: add this back once once https://broadinstitute.atlassian.net/browse/GAWB-3748 is done
headerValueByName(expiresInHeader) &
headerValueByName(emailHeader) &
optionalHeaderValueByName(googleSubjectIdHeader) //it's required for PET accounts, but optional for regular users.
// TODO: until https://broadinstitute.atlassian.net/browse/GAWB-3748 is done, googleSubjectIdHeader is required
) tflatMap {
case (token, userId, expiresIn, email) => {
val userInfo = UserInfo(OAuth2BearerToken(token), WorkbenchUserId(userId), WorkbenchEmail(email), expiresIn.toLong)
onSuccess(getUserFromPetServiceAccount(userInfo).map {
case Some(petOwnerUser) => UserInfo(OAuth2BearerToken(token), petOwnerUser.id, petOwnerUser.email, expiresIn.toLong)
case None => userInfo
})
case (token, expiresIn, email, googleSubjectId) => {
val em = WorkbenchEmail(email)
val gSid = googleSubjectId.map(GoogleSubjectId)
onSuccess{
val res: Future[UserInfo] = if (isPetSA(em)) {
// If it's a PET account, we treat it as its owner
googleSubjectId.fold[Future[UserInfo]](Future.failed(new WorkbenchException("google subject Id is required for PET account"))){
gid =>
directoryDAO.getUserFromPetServiceAccount(ServiceAccountSubjectId(gid)).flatMap(_.fold[Future[UserInfo]](Future.failed(new WorkbenchException("no owner found for this PET account")))(pet => Future.successful(UserInfo(OAuth2BearerToken(token), pet.id, gSid, pet.email, expiresIn.toLong))))
}
} else {
for{
subject <- directoryDAO.loadSubjectFromGoogleSubjectId(GoogleSubjectId(googleSubjectId.get)) //dirty temporary code TODO: this call should be removed after https://broadinstitute.atlassian.net/browse/GAWB-3747
userInfo <- subject match{
case Some(uid: WorkbenchUserId) => Future.successful(UserInfo(OAuth2BearerToken(token), uid, gSid, WorkbenchEmail(email), expiresIn.toLong))
case Some(_) => Future.failed(new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.Conflict, s"subjectId $googleSubjectId is not a WorkbenchUser")))
case None => Future.failed(new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.NotFound, s"user $googleSubjectId not found")))
}
}yield userInfo
}

res
}
}
}

private def getUserFromPetServiceAccount(userInfo: UserInfo):Future[Option[WorkbenchUser]] = {
if (isPetSA(userInfo.userEmail)) {
directoryDAO.getUserFromPetServiceAccount(ServiceAccountSubjectId(userInfo.userId.value))
} else {
Future.successful(None)
def requireCreateUser: Directive1[CreateWorkbenchUserAPI] = (
headerValueByName(googleSubjectIdHeader) &
headerValueByName(emailHeader)
) tflatMap {
case (googleSubjectId, email) => {
val em = WorkbenchEmail(email)
onSuccess{
val res: Future[CreateWorkbenchUserAPI] = if (isPetSA(em)) {
// If it's a PET account, we don't creat a user for it. Its owner should already exist in ldap
Future.failed[CreateWorkbenchUserAPI](new WorkbenchExceptionWithErrorReport(ErrorReport(StatusCodes.Conflict, s"owner for PET ${googleSubjectId} already exists")))
} else {
Future.successful(CreateWorkbenchUserAPI(genWorkbenchUserId(System.currentTimeMillis()), GoogleSubjectId(googleSubjectId), WorkbenchEmail(email)))
}
res
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package org.broadinstitute.dsde.workbench.sam.api
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives.onSuccess
import akka.http.scaladsl.server.{Directive0, Directive1, Directives}
import org.broadinstitute.dsde.workbench.model.{ErrorReport, UserInfo, WorkbenchExceptionWithErrorReport}
import org.broadinstitute.dsde.workbench.model._
import org.broadinstitute.dsde.workbench.sam.service.CloudExtensions
import org.broadinstitute.dsde.workbench.sam._
import org.broadinstitute.dsde.workbench.sam.directory.DirectoryDAO
Expand All @@ -17,6 +17,8 @@ trait UserInfoDirectives {

def requireUserInfo: Directive1[UserInfo]

def requireCreateUser: Directive1[CreateWorkbenchUserAPI]

def asWorkbenchAdmin(userInfo: UserInfo): Directive0 = {
Directives.mapInnerRoute { r =>
onSuccess(cloudExtensions.isWorkbenchAdmin(userInfo.userEmail)) { isAdmin =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package org.broadinstitute.dsde.workbench.sam.api
package org.broadinstitute.dsde.workbench.sam
package 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.{WorkbenchEmail, WorkbenchUser, WorkbenchUserId}
import org.broadinstitute.dsde.workbench.model.google.GoogleProject
import org.broadinstitute.dsde.workbench.model._
import org.broadinstitute.dsde.workbench.sam.model.SamJsonSupport._
import org.broadinstitute.dsde.workbench.sam.service.UserService

Expand All @@ -20,61 +21,64 @@ trait UserRoutes extends UserInfoDirectives {

def userRoutes: server.Route =
pathPrefix("user") {
requireUserInfo { userInfo =>
(pathPrefix("v1") | pathEndOrSingleSlash){
(pathPrefix("v1") | pathEndOrSingleSlash) {
pathEndOrSingleSlash {
post {
complete {
userService.createUser(WorkbenchUser(userInfo.userId, userInfo.userEmail)).map(userStatus => StatusCodes.Created -> userStatus)
}
} ~
get {
parameter("userDetailsOnly".?) { userDetailsOnly =>
complete {
userService.getUserStatus(userInfo.userId, userDetailsOnly.exists(_.equalsIgnoreCase("true"))).map { statusOption =>
statusOption.map { status =>
StatusCodes.OK -> Option(status)
}.getOrElse(StatusCodes.NotFound -> None)
}
}
}
}
}
} ~
pathPrefix("v2") {
pathPrefix("self") {
pathEndOrSingleSlash {
post {
requireCreateUser { createUser =>
complete {
userService.createUser(WorkbenchUser(userInfo.userId, userInfo.userEmail)).map(userStatus => StatusCodes.Created -> userStatus)
userService.createUser(createUser).map(userStatus => StatusCodes.Created -> userStatus)
}
}
} ~
path("info") {
} ~ requireUserInfo { user =>
get {
complete {
userService.getUserStatusInfo(userInfo.userId).map { statusOption =>
statusOption.map { status =>
StatusCodes.OK -> Option(status)
}.getOrElse(StatusCodes.NotFound -> None)
parameter("userDetailsOnly".?) { userDetailsOnly =>
complete {
userService.getUserStatus(user.userId, userDetailsOnly.exists(_.equalsIgnoreCase("true"))).map { statusOption =>
statusOption.map { status =>
StatusCodes.OK -> Option(status)
}.getOrElse(StatusCodes.NotFound -> None)
}
}
}
}
} ~
path("diagnostics") {
get {
complete {
userService.getUserStatusDiagnostics(userInfo.userId).map { statusOption =>
statusOption.map { status =>
StatusCodes.OK -> Option(status)
}.getOrElse(StatusCodes.NotFound -> None)
}
}
} ~ pathPrefix("v2") {
pathPrefix("self") {
pathEndOrSingleSlash {
post {
requireCreateUser { createUser =>
complete {
userService.createUser(createUser).map(userStatus => StatusCodes.Created -> userStatus)
}
}
}
}~ requireUserInfo {user =>
path("info") {
get {
complete {
userService.getUserStatusInfo(user.userId).map { statusOption =>
statusOption.map { status =>
StatusCodes.OK -> Option(status)
}.getOrElse(StatusCodes.NotFound -> None)
}
}
}
} ~
path("diagnostics") {
get {
complete {
userService.getUserStatusDiagnostics(user.userId).map { statusOption =>
statusOption.map { status =>
StatusCodes.OK -> Option(status)
}.getOrElse(StatusCodes.NotFound -> None)
}
}
}
}
}
}
}
}
}
}
}

def adminUserRoutes: server.Route =
Expand Down Expand Up @@ -149,3 +153,4 @@ trait UserRoutes extends UserInfoDirectives {
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.broadinstitute.dsde.workbench.sam

package object api {
val googleSubjectIdHeader = "OIDC_CLAIM_google_subject_id"
val accessTokenHeader = "OIDC_access_token"
val expiresInHeader = "OIDC_CLAIM_expires_in"
val emailHeader = "OIDC_CLAIM_email"
val userIdHeader = "OIDC_CLAIM_user_id"
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ trait DirectoryDAO {
def loadSubjectFromEmail(email: WorkbenchEmail): Future[Option[WorkbenchSubject]]
def loadSubjectEmail(subject: WorkbenchSubject): Future[Option[WorkbenchEmail]]
def loadSubjectEmails(subjects: Set[WorkbenchSubject]): Future[Set[WorkbenchEmail]]
def loadSubjectFromGoogleSubjectId(googleSubjectId: GoogleSubjectId): Future[Option[WorkbenchSubject]]

def createUser(user: WorkbenchUser): Future[WorkbenchUser]
def loadUser(userId: WorkbenchUserId): Future[Option[WorkbenchUser]]
Expand All @@ -57,4 +58,5 @@ trait DirectoryDAO {
def deletePetServiceAccount(petServiceAccountId: PetServiceAccountId): Future[Unit]
def getAllPetServiceAccountsForUser(userId: WorkbenchUserId): Future[Seq[PetServiceAccount]]
def updatePetServiceAccount(petServiceAccount: PetServiceAccount): Future[PetServiceAccount]
def updateGoogleSubjectId(user: WorkbenchUser): Future[WorkbenchUser]
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,23 @@ class LdapDirectoryDAO(protected val ldapConnectionPool: LDAPConnectionPool, pro
}

override def createUser(user: WorkbenchUser): Future[WorkbenchUser] = Future {
ldapConnectionPool.add(userDn(user.id),
user.googleSubjectId.fold(ldapConnectionPool.add(userDn(user.id),
new Attribute(Attr.email, user.email.value),
new Attribute(Attr.sn, user.id.value),
new Attribute(Attr.cn, user.id.value),
new Attribute(Attr.uid, user.id.value),
new Attribute("objectclass", Seq("top", "workbenchPerson").asJava)
)
)){
googleSubjectId =>
ldapConnectionPool.add(userDn(user.id),
new Attribute(Attr.email, user.email.value),
new Attribute(Attr.sn, user.id.value),
new Attribute(Attr.cn, user.id.value),
new Attribute(Attr.uid, user.id.value),
new Attribute(Attr.googleSubjectId, googleSubjectId.value),
new Attribute("objectclass", Seq("top", "workbenchPerson").asJava)
)
}

user
}.recover {
Expand All @@ -179,7 +189,7 @@ class LdapDirectoryDAO(protected val ldapConnectionPool: LDAPConnectionPool, pro
val uid = getAttribute(results, Attr.uid).getOrElse(throw new WorkbenchException(s"${Attr.uid} attribute missing"))
val email = getAttribute(results, Attr.email).getOrElse(throw new WorkbenchException(s"${Attr.email} attribute missing"))

WorkbenchUser(WorkbenchUserId(uid), WorkbenchEmail(email))
WorkbenchUser(WorkbenchUserId(uid), getAttribute(results, Attr.googleSubjectId).map(GoogleSubjectId), WorkbenchEmail(email))
}

override def loadUsers(userIds: Set[WorkbenchUserId]): Future[Seq[WorkbenchUser]] = Future {
Expand Down Expand Up @@ -327,4 +337,19 @@ class LdapDirectoryDAO(protected val ldapConnectionPool: LDAPConnectionPool, pro
ldapConnectionPool.modify(petDn(petServiceAccount.id), modifications.asJava)
petServiceAccount
}

override def loadSubjectFromGoogleSubjectId(googleSubjectId: GoogleSubjectId): Future[Option[WorkbenchSubject]] = Future {
ldapConnectionPool.search(directoryConfig.baseDn, SearchScope.SUB, Filter.createEqualityFilter(Attr.googleSubjectId, googleSubjectId.value)).getSearchEntries.asScala match {
case Seq() => None
case Seq(subject) => Option(dnToSubject(subject.getDN))
case subjects => throw new WorkbenchException(s"Database error: googleSubjectId $googleSubjectId refers to too many subjects: ${subjects.map(_.getDN)}")
}
}

override def updateGoogleSubjectId(user: WorkbenchUser): Future[WorkbenchUser] =
user.googleSubjectId match {
case None => Future.successful(user) //No op
case Some(sid) =>
Future{ldapConnectionPool.modify(userDn(user.id), new Modification(ModificationType.REPLACE, Attr.googleSubjectId, sid.value)); user}
}
}
Loading

0 comments on commit 17b4ab6

Please sign in to comment.