diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/entities/EntityService.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/entities/EntityService.scala index f23c51f4db..6e12a39b0e 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/rawls/entities/EntityService.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/entities/EntityService.scala @@ -17,19 +17,15 @@ import org.broadinstitute.dsde.rawls.entities.exceptions.{ import org.broadinstitute.dsde.rawls.expressions.ExpressionEvaluator import org.broadinstitute.dsde.rawls.metrics.RawlsInstrumented import org.broadinstitute.dsde.rawls.model.AttributeUpdateOperations.{AttributeUpdateOperation, EntityUpdateDefinition} -import org.broadinstitute.dsde.rawls.model.{ - AttributeEntityReference, - Entity, - EntityCopyDefinition, - EntityQuery, - ErrorReport, - SamResourceTypeNames, - SamWorkspaceActions, - WorkspaceName, - _ +import org.broadinstitute.dsde.rawls.model._ +import org.broadinstitute.dsde.rawls.util.{ + AttributeSupport, + AttributeUpdateOperationException, + EntitySupport, + JsonFilterUtils, + WorkspaceSupport } -import org.broadinstitute.dsde.rawls.util.{AttributeSupport, EntitySupport, JsonFilterUtils, WorkspaceSupport} -import org.broadinstitute.dsde.rawls.workspace.{AttributeUpdateOperationException, WorkspaceRepository} +import org.broadinstitute.dsde.rawls.workspace.WorkspaceRepository import org.broadinstitute.dsde.rawls.{RawlsException, RawlsExceptionWithErrorReport} import slick.jdbc.{ResultSetConcurrency, ResultSetType, TransactionIsolation} diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/metrics/MetricsHelper.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/metrics/MetricsHelper.scala index 4314958711..fc16570671 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/rawls/metrics/MetricsHelper.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/metrics/MetricsHelper.scala @@ -40,11 +40,12 @@ object MetricsHelper { def incrementFastPassRevokedCounter(memberType: IamMemberType): IO[Unit] = incrementFastPassUpdatedCounter(memberType, "revoke") - def incrementCounter(name: String, - count: Int = 1, - labels: Map[String, String] = Map.empty, - description: Option[String] = None - ): IO[Unit] = { + def incrementCounter( + name: String, + count: Int = 1, + labels: Map[String, String] = Map.empty, + description: Option[String] = None + ): Unit = { val metrics = meter .counterBuilder(s"$PREFIX/$name") .setDescription(description.getOrElse("none")) @@ -53,7 +54,7 @@ object MetricsHelper { labels.foreach { case (k, v) => labelBuilder.put(k, v) } - IO(metrics.add(count, labelBuilder.build())) + metrics.add(count, labelBuilder.build()) } private def incrementFastPassUpdatedCounter(memberType: IamMemberType, action: String) = diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/submissions/SubmissionsRepository.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/submissions/SubmissionsRepository.scala new file mode 100644 index 0000000000..4059028103 --- /dev/null +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/submissions/SubmissionsRepository.scala @@ -0,0 +1,53 @@ +package org.broadinstitute.dsde.rawls.submissions + +import org.broadinstitute.dsde.rawls.dataaccess.SlickDataSource +import org.broadinstitute.dsde.rawls.dataaccess.slick.WorkflowRecord +import org.broadinstitute.dsde.rawls.metrics.RawlsInstrumented +import org.broadinstitute.dsde.rawls.model.{WorkflowStatuses, Workspace, WorkspaceName} + +import scala.concurrent.{ExecutionContext, Future} + +/** + * Data access for workflow submissions + * + * The intention of this class is to hide direct dependencies on Slick behind a relatively clean interface + * to ease testability of higher level business logic. + */ +class SubmissionsRepository( + dataSource: SlickDataSource, + trackDetailedSubmissionMetrics: Boolean = true, + override val workbenchMetricBaseName: String +) extends RawlsInstrumented { + + import dataSource.dataAccess.driver.api._ + + def getActiveWorkflowsAndSetStatusToAborted( + workspace: Workspace + )(implicit ex: ExecutionContext): Future[Seq[WorkflowRecord]] = + dataSource + .inTransaction { dataAccess => + for { + // Gather any active workflows with external ids + workflowsToAbort <- dataAccess.workflowQuery.findActiveWorkflowsWithExternalIds(workspace) + + // If a workflow is not done, automatically change its status to Aborted + _ <- dataAccess.workflowQuery.findWorkflowsByWorkspace(workspace).result.map { workflowRecords => + workflowRecords + .filter(workflowRecord => !WorkflowStatuses.withName(workflowRecord.status).isDone) + .foreach { workflowRecord => + dataAccess.workflowQuery.updateStatus(workflowRecord, WorkflowStatuses.Aborted) { status => + if (trackDetailedSubmissionMetrics) + Option( + workflowStatusCounter( + workspaceSubmissionMetricBuilder(workspace.toWorkspaceName, workflowRecord.submissionId) + )( + status + ) + ) + else None + } + } + } + } yield workflowsToAbort + } +} diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/util/AttributeSupport.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/util/AttributeSupport.scala index 784e7c276f..ca94c62774 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/rawls/util/AttributeSupport.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/util/AttributeSupport.scala @@ -1,7 +1,7 @@ package org.broadinstitute.dsde.rawls.util import akka.http.scaladsl.model.StatusCodes -import org.broadinstitute.dsde.rawls.RawlsExceptionWithErrorReport +import org.broadinstitute.dsde.rawls.{RawlsException, RawlsExceptionWithErrorReport} import org.broadinstitute.dsde.rawls.model.Attributable.AttributeMap import org.broadinstitute.dsde.rawls.model.AttributeUpdateOperations.{ AddListMember, @@ -26,7 +26,6 @@ import org.broadinstitute.dsde.rawls.model.{ ErrorReport, MethodConfiguration } -import org.broadinstitute.dsde.rawls.workspace.{AttributeNotFoundException, AttributeUpdateOperationException} import scala.concurrent.Future @@ -172,10 +171,13 @@ trait AttributeSupport { * * @param entity to update * @param operations sequence of operations - * @throws org.broadinstitute.dsde.rawls.workspace.AttributeNotFoundException when removing from a list attribute that does not exist + * @throws AttributeNotFoundException when removing from a list attribute that does not exist * @throws AttributeUpdateOperationException when adding or removing from an attribute that is not a list * @return the updated entity */ def applyOperationsToEntity(entity: Entity, operations: Seq[AttributeUpdateOperation]): Entity = entity.copy(attributes = applyAttributeUpdateOperations(entity, operations)) } + +class AttributeUpdateOperationException(message: String) extends RawlsException(message) +class AttributeNotFoundException(message: String) extends AttributeUpdateOperationException(message) diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/util/TracingUtils.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/util/TracingUtils.scala index 542aeae2d1..53dadcc8cb 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/rawls/util/TracingUtils.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/util/TracingUtils.scala @@ -96,6 +96,33 @@ object TracingUtils { def traceFuture[T](name: String)(f: RawlsTracingContext => Future[T])(implicit ec: ExecutionContext): Future[T] = traceFutureWithParent(name, RawlsTracingContext(otelContext = Option(Context.root())))(f) + /** + * Trace a sync operation with the RawlsRequestContext + * + * @param name The name that will be used in the trace + * @param parentContext the RawlsRequestContext to use for tracing, if `parentContext.otelContext` is defined + * @param f the operation to execute within the trace + * @return the result of the operation `f` + */ + def trace[T](name: String, parentContext: RawlsRequestContext)(f: RawlsRequestContext => T): T = + parentContext.otelContext match { + case Some(otelContext) if instrumenter.shouldStart(otelContext, name) => + val childContext = instrumenter.start(otelContext, name) + val result = Try(f(parentContext.copy(otelContext = Option(childContext)))) + instrumenter.end(childContext, name, name, result.failed.toOption.orNull) + result.get + case _ => + f(parentContext) + } + + /** + * Trace a sync operation with the RawlsTracingContext + * + * @param name The name that will be used in the trace + * @param parentContext the RawlsTracingContext to use for tracing, if `parentContext.otelContext` is defined + * @param f the operation to execute within the trace + * @return the result of the operation `f` + */ def traceNakedWithParent[T](name: String, parentContext: RawlsTracingContext)( f: RawlsTracingContext => T ): T = diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceRepository.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceRepository.scala index 024d917aa6..48ba88b21c 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceRepository.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceRepository.scala @@ -3,6 +3,7 @@ package org.broadinstitute.dsde.rawls.workspace import akka.http.scaladsl.model.StatusCodes import org.broadinstitute.dsde.rawls.RawlsExceptionWithErrorReport import org.broadinstitute.dsde.rawls.dataaccess.SlickDataSource +import org.broadinstitute.dsde.rawls.dataaccess.slick.PendingBucketDeletionRecord import org.broadinstitute.dsde.rawls.model.Attributable.AttributeMap import org.broadinstitute.dsde.rawls.model.{ ErrorReport, @@ -18,7 +19,6 @@ import org.broadinstitute.dsde.rawls.model.{ import org.broadinstitute.dsde.rawls.model.WorkspaceState.WorkspaceState import org.broadinstitute.dsde.rawls.util.TracingUtils.traceDBIOWithParent import org.joda.time.DateTime -import slick.dbio.DBIO import java.util.UUID import scala.concurrent.{ExecutionContext, Future} @@ -51,7 +51,9 @@ class WorkspaceRepository(dataSource: SlickDataSource) { _.workspaceQuery.getV2WorkspaceId(workspaceName) } - def listWorkspacesByIds(workspaceIds: Seq[UUID], attributeSpecs: Option[WorkspaceAttributeSpecs] = None): Future[Seq[Workspace]] = dataSource.inTransaction { + def listWorkspacesByIds(workspaceIds: Seq[UUID], + attributeSpecs: Option[WorkspaceAttributeSpecs] = None + ): Future[Seq[Workspace]] = dataSource.inTransaction { _.workspaceQuery.listV2WorkspacesByIds(workspaceIds, attributeSpecs) } @@ -77,6 +79,22 @@ class WorkspaceRepository(dataSource: SlickDataSource) { access.workspaceQuery.delete(workspaceName) } + def deleteRawlsWorkspace(workspace: Workspace)(implicit ex: ExecutionContext): Future[Unit] = + dataSource.inTransaction { dataAccess => + for { + // Delete components of the workspace + _ <- dataAccess.submissionQuery.deleteFromDb(workspace.workspaceIdAsUUID) + _ <- dataAccess.methodConfigurationQuery.deleteFromDb(workspace.workspaceIdAsUUID) + _ <- dataAccess.entityQuery.deleteFromDb(workspace) + + // Schedule bucket for deletion + _ <- dataAccess.pendingBucketDeletionQuery.save(PendingBucketDeletionRecord(workspace.bucketName)) + + // Delete the workspace + _ <- dataAccess.workspaceQuery.delete(workspace.toWorkspaceName) + } yield () + } + def createMCWorkspace(workspaceId: UUID, workspaceName: WorkspaceName, attributes: AttributeMap, @@ -153,13 +171,13 @@ class WorkspaceRepository(dataSource: SlickDataSource) { def savePendingCloneWorkspaceFileTransfer(destWorkspace: UUID, sourceWorkspace: UUID, prefix: String): Future[Int] = dataSource.inTransaction(_.cloneWorkspaceFileTransferQuery.save(destWorkspace, sourceWorkspace, prefix)) - def getSubmissionSummaryStats(workspaceId: UUID)(implicit ex: ExecutionContext): Future[Option[WorkspaceSubmissionStats]] = + def getSubmissionSummaryStats(workspaceId: UUID)(implicit + ex: ExecutionContext + ): Future[Option[WorkspaceSubmissionStats]] = dataSource.inTransaction(_.workspaceQuery.listSubmissionSummaryStats(Seq(workspaceId))).map(_.values.headOption) def listSubmissionSummaryStats(workspaceIds: Seq[UUID]): Future[Map[UUID, WorkspaceSubmissionStats]] = - dataSource.inTransaction(_.workspaceQuery.listSubmissionSummaryStats(workspaceIds)) - - + dataSource.inTransaction(_.workspaceQuery.listSubmissionSummaryStats(workspaceIds)) def getTags(workspaceIds: Seq[UUID], query: Option[String], limit: Option[Int] = None): Future[Seq[WorkspaceTag]] = dataSource.inTransaction(_.workspaceQuery.getTags(query, limit, Some(workspaceIds))) diff --git a/core/src/main/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceService.scala b/core/src/main/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceService.scala index 0ebd70d163..e97fd9b9c7 100644 --- a/core/src/main/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceService.scala +++ b/core/src/main/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceService.scala @@ -3,7 +3,6 @@ package org.broadinstitute.dsde.rawls.workspace import akka.http.scaladsl.model.{StatusCode, StatusCodes} import akka.stream.Materializer import bio.terra.workspace.client.ApiException -import bio.terra.workspace.model.WorkspaceDescription import cats.implicits._ import cats.{Applicative, ApplicativeThrow} import com.google.api.client.googleapis.json.GoogleJsonResponseException @@ -26,7 +25,6 @@ import org.broadinstitute.dsde.rawls.model.WorkspaceAccessLevels._ import org.broadinstitute.dsde.rawls.model.WorkspaceJsonSupport._ import org.broadinstitute.dsde.rawls.model.WorkspaceState.WorkspaceState import org.broadinstitute.dsde.rawls.model.WorkspaceType.WorkspaceType -import org.broadinstitute.dsde.rawls.model.WorkspaceVersions.WorkspaceVersion import org.broadinstitute.dsde.rawls.model._ import org.broadinstitute.dsde.rawls.monitor.migration.MigrationUtils.Implicits.monadThrowDBIOAction import org.broadinstitute.dsde.rawls.resourcebuffer.ResourceBufferService @@ -34,7 +32,9 @@ import org.broadinstitute.dsde.rawls.serviceperimeter.ServicePerimeterService import org.broadinstitute.dsde.rawls.user.UserService import org.broadinstitute.dsde.rawls.util.TracingUtils._ import org.broadinstitute.dsde.rawls.util.{ + AttributeNotFoundException, AttributeSupport, + AttributeUpdateOperationException, BillingProjectSupport, JsonFilterUtils, LibraryPermissionsSupport, @@ -53,8 +53,8 @@ import org.joda.time.DateTime import spray.json.DefaultJsonProtocol._ import spray.json._ import org.broadinstitute.dsde.rawls.metrics.MetricsHelper -import cats.effect.unsafe.implicits.global import org.broadinstitute.dsde.rawls.billing.BillingRepository +import org.broadinstitute.dsde.rawls.submissions.SubmissionsRepository import java.io.IOException import java.util.UUID @@ -118,7 +118,8 @@ object WorkspaceService { multiCloudWorkspaceAclManager, (context: RawlsRequestContext) => fastPassServiceConstructor(context, dataSource), new WorkspaceRepository(dataSource), - new BillingRepository(dataSource) + new BillingRepository(dataSource), + new SubmissionsRepository(dataSource, config.trackDetailedSubmissionMetrics, workbenchMetricBaseName) ) val SECURITY_LABEL_KEY: String = "security" @@ -165,7 +166,8 @@ class WorkspaceService( multiCloudWorkspaceAclManager: MultiCloudWorkspaceAclManager, val fastPassServiceConstructor: RawlsRequestContext => FastPassService, val workspaceRepository: WorkspaceRepository, - val billingRepository: BillingRepository + val billingRepository: BillingRepository, + val submissionsRepository: SubmissionsRepository )(implicit protected val executionContext: ExecutionContext) extends LazyLogging with LibraryPermissionsSupport @@ -465,286 +467,116 @@ class WorkspaceService( .getResourceAuthDomain(resourceTypeName, resourceId, ctx) .map(_.map(g => ManagedGroupRef(RawlsGroupName(g))).toSet) - // Do not limit workspace deletion to V2 workspaces so that we can clean up old V1 workspaces as needed. - def deleteWorkspace(workspaceName: WorkspaceName): Future[WorkspaceDeletionResult] = { - def maybeLoadMcWorkspace(workspace: Workspace): Future[Option[WorkspaceDescription]] = - workspace.workspaceType match { - case WorkspaceType.McWorkspace => - Future(Option(workspaceManagerDAO.getWorkspace(workspace.workspaceIdAsUUID, ctx))) - case WorkspaceType.RawlsWorkspace => Future(None) - } - traceFutureWithParent("getWorkspaceContextAndPermissions", ctx)(_ => - getWorkspaceContextAndPermissions(workspaceName, SamWorkspaceActions.delete) flatMap { workspace => - traceFutureWithParent("maybeLoadMCWorkspace", ctx)(_ => maybeLoadMcWorkspace(workspace)) flatMap { - maybeMcWorkspace => - traceFutureWithParent("deleteWorkspaceInternal", ctx)(s1 => - deleteWorkspaceInternal(workspace, maybeMcWorkspace, s1) - ) - } - } - ) - } - - private def gatherWorkflowsToAbortAndSetStatusToAborted(workspaceName: WorkspaceName, workspaceContext: Workspace) = - dataSource - .inTransaction { dataAccess => - for { - // Gather any active workflows with external ids - workflowsToAbort <- dataAccess.workflowQuery.findActiveWorkflowsWithExternalIds(workspaceContext) - - // If a workflow is not done, automatically change its status to Aborted - _ <- dataAccess.workflowQuery.findWorkflowsByWorkspace(workspaceContext).result.map { workflowRecords => - workflowRecords - .filter(workflowRecord => !WorkflowStatuses.withName(workflowRecord.status).isDone) - .foreach { workflowRecord => - dataAccess.workflowQuery.updateStatus(workflowRecord, WorkflowStatuses.Aborted) { status => - if (config.trackDetailedSubmissionMetrics) - Option( - workflowStatusCounter( - workspaceSubmissionMetricBuilder(workspaceName, workflowRecord.submissionId) - )( - status - ) - ) - else None - } - } - } - } yield workflowsToAbort - } - .recover { case t: Throwable => - logger.warn( - s"Unexpected failure deleting workspace (while gathering workflows that need to be aborted) for workspace `${workspaceContext.toWorkspaceName}`", - t - ) - throw t - } - - private def deleteWorkspaceTransaction(workspaceContext: Workspace) = - dataSource.inTransaction { dataAccess => - for { - // Delete components of the workspace - _ <- dataAccess.submissionQuery.deleteFromDb(workspaceContext.workspaceIdAsUUID) - _ <- dataAccess.methodConfigurationQuery.deleteFromDb(workspaceContext.workspaceIdAsUUID) - _ <- dataAccess.entityQuery.deleteFromDb(workspaceContext) - - // Schedule bucket for deletion - _ <- dataAccess.pendingBucketDeletionQuery.save(PendingBucketDeletionRecord(workspaceContext.bucketName)) - - // Delete the workspace - _ <- dataAccess.workspaceQuery.delete(workspaceContext.toWorkspaceName) - } yield () - } - - def assertNoGoogleChildrenBlockingWorkspaceDeletion(workspace: Workspace): Future[Unit] = for { - _ <- ApplicativeThrow[Future].raiseWhen(workspace.googleProjectId.value.isEmpty) { - RawlsExceptionWithErrorReport( - ErrorReport( - StatusCodes.InternalServerError, - s"Cannot call this method on workspace ${workspace.workspaceId} with no googleProjectId" - ) - ) + def deleteWorkspace(workspaceName: WorkspaceName): Future[WorkspaceDeletionResult] = for { + workspace <- getWorkspaceContextAndPermissions(workspaceName, SamWorkspaceActions.delete) + _ = workspace.workspaceType match { + case WorkspaceType.McWorkspace => + throw RawlsExceptionWithErrorReport(StatusCodes.BadRequest, "Multi Cloud workspaces not supported") + case WorkspaceType.RawlsWorkspace => () } - workspaceChildren <- samDAO - .listResourceChildren(SamResourceTypeNames.workspace, workspace.workspaceId, ctx) - .map( - // a workspace may have a single child, if that child is the google project: this is deleted as part of the normal process - _.filter(c => - c.resourceTypeName != SamResourceTypeNames.googleProject.value || workspace.googleProjectId.value != c.resourceId - ) - ) - googleProjectChildren <- - samDAO.listResourceChildren(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx) - blockingChildren = workspaceChildren.toList ::: googleProjectChildren.toList - } yield - if (blockingChildren.nonEmpty) { - val reports = - blockingChildren.map(r => ErrorReport(s"Blocking resource: ${r.resourceTypeName} resource ${r.resourceId}")) - throw RawlsExceptionWithErrorReport( - ErrorReport(StatusCodes.BadRequest, "Workspace deletion blocked by child resources", reports) - ) - } - - private def deleteWorkspaceInternal(workspaceContext: Workspace, - maybeMcWorkspace: Option[WorkspaceDescription], - parentContext: RawlsRequestContext - ): Future[WorkspaceDeletionResult] = { - if (isAzureMcWorkspace(maybeMcWorkspace)) { - return Future.failed( - new RawlsExceptionWithErrorReport(ErrorReport(StatusCodes.InternalServerError, "MC workspaces not supported")) - ) + _ <- requesterPaysSetupService.deleteAllRecordsForWorkspace(workspace) + workflowsToAbort <- traceFutureWithParent("gatherWorkflowsToAbortAndSetStatusToAborted", ctx)(_ => + submissionsRepository.getActiveWorkflowsAndSetStatusToAborted(workspace) + ) + // Attempt to abort any running workflows so they don't write any more to the bucket. + // Notice that we're kicking off Futures to do the aborts concurrently, but we never collect their results! + // This is because there's nothing we can do if Cromwell fails, so we might as well move on and let the + // ExecutionContext run the futures whenever + aborts = traceFutureWithParent("abortRunningWorkflows", ctx)(_ => + Future.traverse(workflowsToAbort)(wf => executionServiceCluster.abort(wf, ctx.userInfo)) + ) + _ <- traceFutureWithParent("deleteFastPassGrantsTransaction", ctx)(childContext => + fastPassServiceConstructor(childContext).removeFastPassGrantsForWorkspace(workspace) + ) + // notify leonardo so it can cleanup any dangling sam resources and other non-cloud state + _ <- traceFutureWithParent("notifyLeonardo", ctx)(_ => + leonardoService.cleanupResources(workspace.googleProjectId, workspace.workspaceIdAsUUID, ctx) + ) + // Delete Google Project + _ <- traceFutureWithParent("deleteGoogleProject", ctx)(_ => deleteGoogleProject(workspace.googleProjectId)) + // attempt to delete workspace in WSM, in case thsi is a TDR snapshot - but don't fail on it + _ = Try(workspaceManagerDAO.deleteWorkspace(workspace.workspaceIdAsUUID, ctx)).recover { + case e: ApiException if e.getCode != StatusCodes.NotFound.intValue => + logger.warn(s"Unexpected failure deleting workspace in WSM for workspace `${workspace.toWorkspaceName}]", e) } - - for { - // just a simple db operation now - the extra logging is excessive - _ <- requesterPaysSetupService.deleteAllRecordsForWorkspace(workspaceContext) - workflowsToAbort <- traceFutureWithParent("gatherWorkflowsToAbortAndSetStatusToAborted", parentContext)(_ => - gatherWorkflowsToAbortAndSetStatusToAborted(workspaceContext.toWorkspaceName, workspaceContext) - ) - - // Attempt to abort any running workflows so they don't write any more to the bucket. - // Notice that we're kicking off Futures to do the aborts concurrently, but we never collect their results! - // This is because there's nothing we can do if Cromwell fails, so we might as well move on and let the - // ExecutionContext run the futures whenever - aborts = traceFutureWithParent("abortRunningWorkflows", parentContext)(_ => - Future.traverse(workflowsToAbort)(wf => executionServiceCluster.abort(wf, ctx.userInfo)) - ) - - _ <- traceFutureWithParent("deleteFastPassGrantsTransaction", parentContext)(childContext => - fastPassServiceConstructor(childContext).removeFastPassGrantsForWorkspace(workspaceContext) - ) - - // notify leonardo so it can cleanup any dangling sam resources and other non-cloud state - _ <- traceFutureWithParent("notifyLeonardo", parentContext)(_ => - leonardoService.cleanupResources(workspaceContext.googleProjectId, workspaceContext.workspaceIdAsUUID, ctx) - ) - - // Delete Google Project - _ <- traceFutureWithParent("maybeDeleteGoogleProject", parentContext)(_ => - maybeDeleteGoogleProject(workspaceContext.googleProjectId, workspaceContext.workspaceVersion) - ) - - _ <- traceFutureWithParent("deleteWorkspaceInWSM", parentContext) { _ => - maybeDeleteWsmWorkspace(workspaceContext) - } - - // Delete the workspace records in Rawls. Do this after deleting the google project to prevent service perimeter leaks. - _ <- traceFutureWithParent("deleteWorkspaceTransaction", parentContext)(_ => - deleteWorkspaceTransaction(workspaceContext) recoverWith { case t: Throwable => + // Delete the workspace records in Rawls. Do this after deleting the google project to prevent service perimeter leaks. + _ <- traceFutureWithParent("deleteWorkspaceTransaction", ctx)(_ => + workspaceRepository.deleteRawlsWorkspace(workspace) + ) + // Delete workflowCollection resource in sam outside of DB transaction + _ <- traceFutureWithParent("deleteWorkflowCollectionSamResource", ctx)(_ => + workspace.workflowCollectionName + .map(cn => samDAO.deleteResource(SamResourceTypeNames.workflowCollection, cn, ctx)) + .getOrElse(Future.successful(())) recover { + case t: RawlsExceptionWithErrorReport if t.errorReport.statusCode.contains(StatusCodes.NotFound) => + logger.warn( + s"Received 404 from delete workflowCollection resource in Sam (while deleting workspace) for workspace `${workspace.toWorkspaceName}`: [${t.errorReport.message}]" + ) + case t: RawlsExceptionWithErrorReport => logger.error( - s"Unexpected failure deleting workspace (while deleting workspace in Rawls DB) for workspace `${workspaceContext.toWorkspaceName}`", + s"Unexpected failure deleting workspace (while deleting workflowCollection in Sam) for workspace `${workspace.toWorkspaceName}`.", t ) - Future.failed(t) - } - ) - - // Delete workflowCollection resource in sam outside of DB transaction - _ <- traceFutureWithParent("deleteWorkflowCollectionSamResource", parentContext)(_ => - workspaceContext.workflowCollectionName - .map(cn => samDAO.deleteResource(SamResourceTypeNames.workflowCollection, cn, ctx)) - .getOrElse(Future.successful(())) recoverWith { - case t: RawlsExceptionWithErrorReport if t.errorReport.statusCode.contains(StatusCodes.NotFound) => - logger.warn( - s"Received 404 from delete workflowCollection resource in Sam (while deleting workspace) for workspace `${workspaceContext.toWorkspaceName}`: [${t.errorReport.message}]" - ) - Future.successful() - case t: RawlsExceptionWithErrorReport => - logger.error( - s"Unexpected failure deleting workspace (while deleting workflowCollection in Sam) for workspace `${workspaceContext.toWorkspaceName}`.", - t - ) - Future.failed(t) - } - ) - - _ <- traceFutureWithParent("deleteWorkspaceSamResource", parentContext)(_ => - if (workspaceContext.workspaceType != WorkspaceType.McWorkspace) { // WSM will delete Sam resources for McWorkspaces - samDAO.deleteResource(SamResourceTypeNames.workspace, - workspaceContext.workspaceIdAsUUID.toString, - ctx - ) recoverWith { - case t: RawlsExceptionWithErrorReport if t.errorReport.statusCode.contains(StatusCodes.NotFound) => - logger.warn( - s"Received 404 from delete workspace resource in Sam (while deleting workspace) for workspace `${workspaceContext.toWorkspaceName}`: [${t.errorReport.message}]" - ) - Future.successful() - case t: RawlsExceptionWithErrorReport => - logger.error( - s"Unexpected failure deleting workspace (while deleting workspace in Sam) for workspace `${workspaceContext.toWorkspaceName}`.", - t - ) - - if (t.errorReport.message.contains("Cannot delete a resource with children")) { - MetricsHelper - .incrementCounter("leakingSamResourceError", - labels = Map("cloud" -> "gcp", "projectType" -> workspaceContext.projectType) - ) - .unsafeToFuture() - } - Future.failed(t) - } - } else { Future.successful() } - ) - } yield { - aborts.onComplete { - case Failure(t) => - logger.info(s"failure aborting workflows while deleting workspace ${workspaceContext.toWorkspaceName}", t) - case _ => /* ok */ + throw t } - WorkspaceDeletionResult.fromGcpBucketName(workspaceContext.bucketName) - } - } - - private def maybeDeleteWsmWorkspace(workspaceContext: Workspace) = - Future(workspaceManagerDAO.deleteWorkspace(workspaceContext.workspaceIdAsUUID, ctx)).recoverWith { - case e: ApiException => - if (e.getCode != StatusCodes.NotFound.intValue) { + ) + _ <- traceFutureWithParent("deleteWorkspaceSamResource", ctx)(_ => + samDAO.deleteResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx) recover { + case t: RawlsExceptionWithErrorReport if t.errorReport.statusCode.contains(StatusCodes.NotFound) => logger.warn( - s"Unexpected failure deleting workspace (while deleting in Workspace Manager) for workspace `${workspaceContext.toWorkspaceName}. Received ${e.getCode}: [${e.getResponseBody}]" + s"Received 404 from delete workspace resource in Sam (while deleting workspace) for workspace `${workspace.toWorkspaceName}`: [${t.errorReport.message}]" ) - // fail out if this was an mc workspace (aka azure) - // if it's NOT an MC workspace, this will only ever succeed if it's a TDR snapshot so we handle all exceptions otherwise - if (workspaceContext.workspaceType == WorkspaceType.McWorkspace) { - Future.failed( - new RawlsExceptionWithErrorReport( - errorReport = ErrorReport(StatusCodes.InternalServerError, - s"Unable to delete ${workspaceContext.name}", - ErrorReport(e) - ) - ) - ) - } else { - Future.successful() - } - } else { - // 404 == workspace manager does not know about this workspace, move on - Future.successful() - } + case t: RawlsExceptionWithErrorReport + if t.errorReport.message.contains("Cannot delete a resource with children") => + MetricsHelper.incrementCounter( + "leakingSamResourceError", + labels = Map("cloud" -> "gcp", "projectType" -> workspace.projectType) + ) + throw t + } + ) + } yield { + aborts.onComplete { + case Failure(t) => + logger.info(s"failure aborting workflows while deleting workspace ${workspace.toWorkspaceName}", t) + case _ => /* ok */ } + WorkspaceDeletionResult.fromGcpBucketName(workspace.bucketName) + } - private def isAzureMcWorkspace(maybeMcWorkspace: Option[WorkspaceDescription]): Boolean = - maybeMcWorkspace.flatMap(mcWorkspace => Option(mcWorkspace.getAzureContext)).isDefined - - // TODO - once workspace migration is complete and there are no more v1 workspaces or v1 billing projects, we can remove this https://broadworkbench.atlassian.net/browse/CA-1118 - private def maybeDeleteGoogleProject(googleProjectId: GoogleProjectId, - workspaceVersion: WorkspaceVersion - ): Future[Unit] = - if (workspaceVersion == WorkspaceVersions.V2) deleteGoogleProject(googleProjectId) else Future.successful() + private def deleteGoogleProject(googleProjectId: GoogleProjectId): Future[Unit] = { + def destroyPet(userIdInfo: UserIdInfo, projectName: GoogleProjectId): Future[Unit] = + for { + petSAJson <- samDAO.getPetServiceAccountKeyForUser(projectName, RawlsUserEmail(userIdInfo.userEmail)) + petUserInfo <- gcsDAO.getUserInfoUsingJson(petSAJson) + _ <- samDAO.deleteUserPetServiceAccount(projectName, ctx.copy(userInfo = petUserInfo)) + } yield () - private def deleteGoogleProject(googleProjectId: GoogleProjectId): Future[Unit] = + def deletePetsInProject(projectName: GoogleProjectId): Future[Unit] = + for { + projectUsers <- samDAO + .listAllResourceMemberIds(SamResourceTypeNames.googleProject, projectName.value, ctx) + .recover { + case regrets: RawlsExceptionWithErrorReport + if regrets.errorReport.statusCode == Option(StatusCodes.NotFound) => + logger.info( + s"google-project resource ${projectName.value} not found in Sam. Continuing with workspace deletion" + ) + Set[UserIdInfo]() + } + _ <- projectUsers.toList.traverse(destroyPet(_, projectName)) + } yield () for { _ <- deletePetsInProject(googleProjectId) _ <- gcsDAO.deleteGoogleProject(googleProjectId) _ <- samDAO.deleteResource(SamResourceTypeNames.googleProject, googleProjectId.value, ctx).recover { - case regrets: RawlsExceptionWithErrorReport if regrets.errorReport.statusCode == Option(StatusCodes.NotFound) => + case regrets: RawlsExceptionWithErrorReport if regrets.errorReport.statusCode.contains(StatusCodes.NotFound) => logger.info( s"google-project resource ${googleProjectId.value} not found in Sam. Continuing with workspace deletion" ) } } yield () - - private def deletePetsInProject(projectName: GoogleProjectId): Future[Unit] = - for { - projectUsers <- samDAO - .listAllResourceMemberIds(SamResourceTypeNames.googleProject, projectName.value, ctx) - .recover { - case regrets: RawlsExceptionWithErrorReport - if regrets.errorReport.statusCode == Option(StatusCodes.NotFound) => - logger.info( - s"google-project resource ${projectName.value} not found in Sam. Continuing with workspace deletion" - ) - Set[UserIdInfo]() - } - _ <- projectUsers.toList.traverse(destroyPet(_, projectName)) - } yield () - - private def destroyPet(userIdInfo: UserIdInfo, projectName: GoogleProjectId): Future[Unit] = - for { - petSAJson <- samDAO.getPetServiceAccountKeyForUser(projectName, RawlsUserEmail(userIdInfo.userEmail)) - petUserInfo <- gcsDAO.getUserInfoUsingJson(petSAJson) - _ <- samDAO.deleteUserPetServiceAccount(projectName, ctx.copy(userInfo = petUserInfo)) - } yield () + } def updateLibraryAttributes(workspaceName: WorkspaceName, operations: Seq[AttributeUpdateOperation] @@ -822,13 +654,16 @@ class WorkspaceService( billingProject: RawlsBillingProject, destWorkspaceRequest: WorkspaceRequest, parentContext: RawlsRequestContext = ctx - ): Future[Workspace] = - for { - _ <- destWorkspaceRequest.copyFilesWithPrefix.traverse_(validateFileCopyPrefix) - - (libraryAttributeNames, workspaceAttributeNames) = - destWorkspaceRequest.attributes.keys.partition(_.namespace == AttributeName.libraryNamespace) + ): Future[Workspace] = { + if (destWorkspaceRequest.copyFilesWithPrefix.exists(_.isEmpty)) + throw RawlsExceptionWithErrorReport( + StatusCodes.BadRequest, + """You may not specify an empty string for `copyFilesWithPrefix`. Did you mean to specify "/" or leave the field out entirely?""" + ) + val (libraryAttributeNames, workspaceAttributeNames) = + destWorkspaceRequest.attributes.keys.partition(_.namespace == AttributeName.libraryNamespace) + for { _ <- withAttributeNamespaceCheck(workspaceAttributeNames)(Future.successful()) _ <- withLibraryAttributeNamespaceCheck(libraryAttributeNames)(Future.successful()) _ <- failUnlessBillingAccountHasAccess(billingProject, parentContext) @@ -951,16 +786,7 @@ class WorkspaceService( } .getOrElse(Future.successful()) } yield destWorkspaceContext - - private def validateFileCopyPrefix(copyFilesWithPrefix: String): Future[Unit] = - ApplicativeThrow[Future].raiseWhen(copyFilesWithPrefix.isEmpty) { - RawlsExceptionWithErrorReport( - ErrorReport( - StatusCodes.BadRequest, - """You may not specify an empty string for `copyFilesWithPrefix`. Did you mean to specify "/" or leave the field out entirely?""" - ) - ) - } + } def listPendingFileTransfersForWorkspace( workspaceName: WorkspaceName @@ -2216,6 +2042,4 @@ class WorkspaceService( } -class AttributeUpdateOperationException(message: String) extends RawlsException(message) -class AttributeNotFoundException(message: String) extends AttributeUpdateOperationException(message) class InvalidWorkspaceAclUpdateException(errorReport: ErrorReport) extends RawlsExceptionWithErrorReport(errorReport) diff --git a/core/src/test/scala/org/broadinstitute/dsde/rawls/entities/EntityServiceSpec.scala b/core/src/test/scala/org/broadinstitute/dsde/rawls/entities/EntityServiceSpec.scala index 42c462236c..9e4f9ad247 100644 --- a/core/src/test/scala/org/broadinstitute/dsde/rawls/entities/EntityServiceSpec.scala +++ b/core/src/test/scala/org/broadinstitute/dsde/rawls/entities/EntityServiceSpec.scala @@ -56,9 +56,12 @@ import org.broadinstitute.dsde.rawls.model.{ Workspace } import org.broadinstitute.dsde.rawls.openam.MockUserInfoDirectivesWithUser -import org.broadinstitute.dsde.rawls.util.MockitoTestUtils +import org.broadinstitute.dsde.rawls.util.{ + AttributeNotFoundException, + AttributeUpdateOperationException, + MockitoTestUtils +} import org.broadinstitute.dsde.rawls.webservice.EntityApiService -import org.broadinstitute.dsde.rawls.workspace.{AttributeNotFoundException, AttributeUpdateOperationException} import org.scalatest.BeforeAndAfterAll import org.scalatest.concurrent.{Eventually, ScalaFutures} import org.scalatest.flatspec.AnyFlatSpec diff --git a/core/src/test/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceServiceSpec.scala b/core/src/test/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceServiceSpec.scala index 0acf54015d..a8602c52ed 100644 --- a/core/src/test/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceServiceSpec.scala +++ b/core/src/test/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceServiceSpec.scala @@ -50,7 +50,12 @@ import org.broadinstitute.dsde.rawls.submissions.SubmissionsService import org.broadinstitute.dsde.rawls.user.UserService import org.broadinstitute.dsde.rawls.util.MockitoTestUtils import org.broadinstitute.dsde.rawls.webservice._ -import org.broadinstitute.dsde.rawls.{NoSuchWorkspaceException, RawlsExceptionWithErrorReport, RawlsTestUtils} +import org.broadinstitute.dsde.rawls.{ + NoSuchWorkspaceException, + RawlsExceptionWithErrorReport, + RawlsTestUtils, + TestExecutionContext +} import org.broadinstitute.dsde.workbench.dataaccess.{NotificationDAO, PubSubNotificationDAO} import org.broadinstitute.dsde.workbench.google.mock.{MockGoogleBigQueryDAO, MockGoogleIamDAO, MockGoogleStorageDAO} import org.broadinstitute.dsde.workbench.model.google.{GcsBucketName, GoogleProject, IamPermission} @@ -79,7 +84,6 @@ import scala.jdk.DurationConverters.JavaDurationOps import scala.language.postfixOps import scala.util.Try -//noinspection NameBooleanParameters,TypeAnnotation,EmptyParenMethodAccessedAsParameterless,ScalaUnnecessaryParentheses,RedundantNewCaseClass,ScalaUnusedSymbol class WorkspaceServiceSpec extends AnyFlatSpec with ScalatestRouteTest @@ -94,7 +98,7 @@ class WorkspaceServiceSpec with OptionValues { import driver.api._ - val workspace = Workspace( + val workspace: Workspace = Workspace( testData.wsName.namespace, testData.wsName.name, "aWorkspaceId", @@ -106,7 +110,7 @@ class WorkspaceServiceSpec Map.empty ) - val mockServer = RemoteServicesMockServer() + val mockServer: RemoteServicesMockServer = RemoteServicesMockServer() val leonardoDAO: MockLeonardoDAO = new MockLeonardoDAO() @@ -121,12 +125,14 @@ class WorkspaceServiceSpec } // noinspection TypeAnnotation,NameBooleanParameters,ConvertibleToMethodValue,UnitMethodIsParameterless - class TestApiService(dataSource: SlickDataSource, val user: RawlsUser)(implicit - override val executionContext: ExecutionContext - ) extends WorkspaceApiService + class TestApiService(dataSource: SlickDataSource, val user: RawlsUser) + extends WorkspaceApiService with MethodConfigApiService with SubmissionApiService with MockUserInfoDirectivesWithUser { + + implicit override val executionContext: TestExecutionContext = TestExecutionContext.testExecutionContext + val ctx1 = RawlsRequestContext(UserInfo(user.userEmail, OAuth2BearerToken("foo"), 0, user.userSubjectId)) lazy val workspaceService: WorkspaceService = workspaceServiceConstructor(ctx1) @@ -350,9 +356,8 @@ class WorkspaceServiceSpec submissionSupervisor ! PoisonPill } - class TestApiServiceWithCustomSamDAO(dataSource: SlickDataSource, override val user: RawlsUser)(implicit - override val executionContext: ExecutionContext - ) extends TestApiService(dataSource, user) { + class TestApiServiceWithCustomSamDAO(dataSource: SlickDataSource, override val user: RawlsUser) + extends TestApiService(dataSource, user) { override val samDAO: CustomizableMockSamDAO = Mockito.spy(new CustomizableMockSamDAO(dataSource)) // these need to be overridden to use the new samDAO @@ -374,7 +379,7 @@ class WorkspaceServiceSpec def withTestDataServicesCustomSam[T](testCode: TestApiServiceWithCustomSamDAO => T): T = withTestDataServicesCustomSamAndUser(testData.userOwner)(testCode) - def withServices[T](dataSource: SlickDataSource, user: RawlsUser)(testCode: (TestApiService) => T) = { + def withServices[T](dataSource: SlickDataSource, user: RawlsUser)(testCode: TestApiService => T): T = { val apiService = new TestApiService(dataSource, user) try testCode(apiService) @@ -383,7 +388,7 @@ class WorkspaceServiceSpec } private def withServicesCustomSam[T](dataSource: SlickDataSource, user: RawlsUser)( - testCode: (TestApiServiceWithCustomSamDAO) => T + testCode: TestApiServiceWithCustomSamDAO => T ) = { val apiService = new TestApiServiceWithCustomSamDAO(dataSource, user) @@ -415,7 +420,7 @@ class WorkspaceServiceSpec private def toRawlsRequestContext(user: RawlsUser) = RawlsRequestContext( UserInfo(user.userEmail, OAuth2BearerToken(""), 0, user.userSubjectId) ) - private def populateWorkspacePolicies(services: TestApiService, workspace: Workspace = testData.workspace) = { + private def populateWorkspacePolicies(services: TestApiService, workspace: Workspace = testData.workspace): Unit = { val populateAcl = for { _ <- services.samDAO.registerUser(toRawlsRequestContext(testData.userOwner)) _ <- services.samDAO.registerUser(toRawlsRequestContext(testData.userWriter)) @@ -951,7 +956,7 @@ class WorkspaceServiceSpec val except: RawlsExceptionWithErrorReport = intercept[RawlsExceptionWithErrorReport] { Await.result( services.workspaceService.lockWorkspace( - new WorkspaceName(testData.workspaceMixedSubmissions.namespace, testData.workspaceMixedSubmissions.name) + WorkspaceName(testData.workspaceMixedSubmissions.namespace, testData.workspaceMixedSubmissions.name) ), Duration.Inf ) @@ -1009,12 +1014,8 @@ class WorkspaceServiceSpec // delete the workspace Await.result(services.workspaceService.deleteWorkspace(testData.wsName3), Duration.Inf) - verify(services.workspaceManagerDAO, Mockito.atLeast(1)).deleteWorkspace(any[UUID], any[RawlsRequestContext]) - // check that the workspace has been deleted - assertResult(None) { - runAndWait(workspaceQuery.findByName(testData.wsName3)) - } + runAndWait(workspaceQuery.findByName(testData.wsName3)) shouldBe None } @@ -1027,13 +1028,8 @@ class WorkspaceServiceSpec // delete the workspace Await.result(services.workspaceService.deleteWorkspace(testData.wsName3), Duration.Inf) - verify(services.workspaceManagerDAO, Mockito.atLeast(1)).deleteWorkspace(any[UUID], any[RawlsRequestContext]) - // check that the workspace has been deleted - assertResult(None) { - runAndWait(workspaceQuery.findByName(testData.wsName3)) - } - + runAndWait(workspaceQuery.findByName(testData.wsName3)) shouldBe None } it should "delete a workspace with succeeded submission" in withTestDataServices { services => @@ -1346,17 +1342,6 @@ class WorkspaceServiceSpec workspaceName, Map.empty ) - when(services.workspaceManagerDAO.getWorkspace(any[UUID], any[RawlsRequestContext])).thenReturn( - new WorkspaceDescription() - .stage(WorkspaceStageModel.MC_WORKSPACE) - .azureContext( - new AzureContext() - .tenantId("fake_tenant_id") - .subscriptionId("fake_sub_id") - .resourceGroupId("fake_mrg_id") - ) - ) - val workspace = Await.result( services.mcWorkspaceService.createMultiCloudWorkspace(workspaceRequest, new ProfileModel().id(UUID.randomUUID())), Duration.Inf @@ -1372,9 +1357,9 @@ class WorkspaceServiceSpec Duration.Inf ) } - assertResult(Some(StatusCodes.InternalServerError)) { - error.errorReport.statusCode - } + + error.errorReport.statusCode shouldBe Some(StatusCodes.BadRequest) + } behavior of "getTags" @@ -1390,8 +1375,8 @@ class WorkspaceServiceSpec Await.result( services.workspaceService.updateWorkspace( testData.wsName, - Seq(AddListMember(AttributeName.withTagsNS, AttributeString("cancer")), - AddListMember(AttributeName.withTagsNS, AttributeString("cantaloupe")) + Seq(AddListMember(AttributeName.withTagsNS(), AttributeString("cancer")), + AddListMember(AttributeName.withTagsNS(), AttributeString("cantaloupe")) ) ), Duration.Inf @@ -1400,8 +1385,8 @@ class WorkspaceServiceSpec Await.result( services.workspaceService.updateWorkspace( testData.wsName7, - Seq(AddListMember(AttributeName.withTagsNS, AttributeString("cantaloupe")), - AddListMember(AttributeName.withTagsNS, AttributeString("buffalo")) + Seq(AddListMember(AttributeName.withTagsNS(), AttributeString("cantaloupe")), + AddListMember(AttributeName.withTagsNS(), AttributeString("buffalo")) ) ), Duration.Inf @@ -1445,11 +1430,11 @@ class WorkspaceServiceSpec // remove tags Await.result( - services.workspaceService.updateWorkspace(testData.wsName, Seq(RemoveAttribute(AttributeName.withTagsNS))), + services.workspaceService.updateWorkspace(testData.wsName, Seq(RemoveAttribute(AttributeName.withTagsNS()))), Duration.Inf ) Await.result( - services.workspaceService.updateWorkspace(testData.wsName7, Seq(RemoveAttribute(AttributeName.withTagsNS))), + services.workspaceService.updateWorkspace(testData.wsName7, Seq(RemoveAttribute(AttributeName.withTagsNS()))), Duration.Inf ) @@ -1484,13 +1469,13 @@ class WorkspaceServiceSpec email ) if (shouldShare) { - services.samDAO.callsToAddToPolicy should contain theSameElementsAs (Set(expectedPolicyEntry)) + services.samDAO.callsToAddToPolicy should contain theSameElementsAs Set(expectedPolicyEntry) } else { - services.samDAO.callsToAddToPolicy should contain theSameElementsAs (Set.empty) + services.samDAO.callsToAddToPolicy should contain theSameElementsAs Set.empty } } - val aclTestUser = + val aclTestUser: UserInfo = UserInfo(RawlsUserEmail("acl-test-user"), OAuth2BearerToken(""), 0, RawlsUserSubjectId("acl-test-user-subject-id")) def allWorkspaceAclUpdatePermutations(emailString: String): Seq[WorkspaceACLUpdate] = for { @@ -1644,7 +1629,10 @@ class WorkspaceServiceSpec } } - def addEmailToPolicy(services: TestApiServiceWithCustomSamDAO, policyName: SamResourcePolicyName, email: String) = { + def addEmailToPolicy(services: TestApiServiceWithCustomSamDAO, + policyName: SamResourcePolicyName, + email: String + ): Option[SamPolicyWithNameAndEmail] = { val policy = services.samDAO.policies((SamResourceTypeNames.workspace, testData.workspace.workspaceId))(policyName) val updateMembers = policy.policy.memberEmails + WorkbenchEmail(email) val updatedPolicy = policy.copy(policy = policy.policy.copy(memberEmails = updateMembers)) @@ -1653,7 +1641,7 @@ class WorkspaceServiceSpec .put(policyName, updatedPolicy) } - val testPolicyNames = Set( + val testPolicyNames: Set[SamResourcePolicyName] = Set( SamWorkspacePolicyNames.canCompute, SamWorkspacePolicyNames.writer, SamWorkspacePolicyNames.reader, @@ -1753,8 +1741,8 @@ class WorkspaceServiceSpec behavior of "RequesterPays" it should "return Unit when adding linked service accounts to workspace" in withTestDataServices { services => - withWorkspaceContext(testData.workspace) { ctx => - val rqComplete = + withWorkspaceContext(testData.workspace) { _ => + val rqComplete: Unit = Await.result(services.workspaceService.enableRequesterPaysForLinkedSAs(testData.workspace.toWorkspaceName), Duration.Inf ) @@ -1766,7 +1754,7 @@ class WorkspaceServiceSpec it should "return a 404 ErrorReport when adding linked service accounts to workspace which does not exist" in withTestDataServices { services => - withWorkspaceContext(testData.workspace) { ctx => + withWorkspaceContext(testData.workspace) { _ => val error = intercept[RawlsExceptionWithErrorReport] { Await.result(services.workspaceService.enableRequesterPaysForLinkedSAs( testData.workspace.toWorkspaceName.copy(name = "DNE") @@ -1784,7 +1772,7 @@ class WorkspaceServiceSpec RawlsUser(RawlsUserSubjectId("no-access"), RawlsUserEmail("no-access")) ) { services => populateWorkspacePolicies(services) - withWorkspaceContext(testData.workspace) { ctx => + withWorkspaceContext(testData.workspace) { _ => val error = intercept[RawlsExceptionWithErrorReport] { Await.result(services.workspaceService.enableRequesterPaysForLinkedSAs(testData.workspace.toWorkspaceName), Duration.Inf @@ -1800,7 +1788,7 @@ class WorkspaceServiceSpec testData.userReader ) { services => populateWorkspacePolicies(services) - withWorkspaceContext(testData.workspace) { ctx => + withWorkspaceContext(testData.workspace) { _ => val error = intercept[RawlsExceptionWithErrorReport] { Await.result(services.workspaceService.enableRequesterPaysForLinkedSAs(testData.workspace.toWorkspaceName), Duration.Inf @@ -1813,8 +1801,8 @@ class WorkspaceServiceSpec } it should "return Unit when removing linked service accounts from workspace" in withTestDataServices { services => - withWorkspaceContext(testData.workspace) { ctx => - val rqComplete = + withWorkspaceContext(testData.workspace) { _ => + val rqComplete: Unit = Await.result(services.workspaceService.disableRequesterPaysForLinkedSAs(testData.workspace.toWorkspaceName), Duration.Inf ) @@ -1826,11 +1814,11 @@ class WorkspaceServiceSpec it should "return Unit when removing linked service accounts from workspace which does not exist" in withTestDataServices { services => - withWorkspaceContext(testData.workspace) { ctx => - val rqComplete = Await.result(services.workspaceService.disableRequesterPaysForLinkedSAs( - testData.workspace.toWorkspaceName.copy(name = "DNE") - ), - Duration.Inf + withWorkspaceContext(testData.workspace) { _ => + val rqComplete: Unit = Await.result(services.workspaceService.disableRequesterPaysForLinkedSAs( + testData.workspace.toWorkspaceName.copy(name = "DNE") + ), + Duration.Inf ) assertResult(()) { rqComplete @@ -1842,8 +1830,8 @@ class WorkspaceServiceSpec RawlsUser(RawlsUserSubjectId("no-access"), RawlsUserEmail("no-access")) ) { services => populateWorkspacePolicies(services) - withWorkspaceContext(testData.workspace) { ctx => - val rqComplete = + withWorkspaceContext(testData.workspace) { _ => + val rqComplete: Unit = Await.result(services.workspaceService.disableRequesterPaysForLinkedSAs(testData.workspace.toWorkspaceName), Duration.Inf ) @@ -1857,8 +1845,8 @@ class WorkspaceServiceSpec testData.userReader ) { services => populateWorkspacePolicies(services) - withWorkspaceContext(testData.workspace) { ctx => - val rqComplete = + withWorkspaceContext(testData.workspace) { _ => + val rqComplete: Unit = Await.result(services.workspaceService.disableRequesterPaysForLinkedSAs(testData.workspace.toWorkspaceName), Duration.Inf ) @@ -2120,7 +2108,7 @@ class WorkspaceServiceSpec val newWorkspaceName = "space_for_workin" val workspaceRequest = WorkspaceRequest(testData.testProject1Name.value, newWorkspaceName, Map.empty) - val workspace = Await.result(services.workspaceService.createWorkspace(workspaceRequest), Duration.Inf) + Await.result(services.workspaceService.createWorkspace(workspaceRequest), Duration.Inf) verify(services.resourceBufferService).getGoogleProjectFromBuffer(any[ProjectPoolType], any[String]) } @@ -2139,7 +2127,7 @@ class WorkspaceServiceSpec val workspaceRequest = WorkspaceRequest(newWorkspaceNamespace, newWorkspaceName, Map.empty) val captor = ArgumentCaptor.forClass(classOf[Project]) - val workspace = Await.result(services.workspaceService.createWorkspace(workspaceRequest), Duration.Inf) + Await.result(services.workspaceService.createWorkspace(workspaceRequest), Duration.Inf) verify(services.gcsDAO).updateGoogleProject(ArgumentMatchers.eq(GoogleProjectId("project-from-buffer")), captor.capture() @@ -2226,9 +2214,6 @@ class WorkspaceServiceSpec // Use the WorkspaceServiceConfig to determine which static projects exist for which perimeter val servicePerimeterName: ServicePerimeterName = services.servicePerimeterServiceConfig.staticProjectsInPerimeters.keys.head - val staticProjectNumbersInPerimeter: Set[String] = - services.servicePerimeterServiceConfig.staticProjectsInPerimeters(servicePerimeterName).map(_.value).toSet - val billingProject1 = testData.testProject1 val billingProject2 = testData.testProject2 val billingProjects = Seq(billingProject1, billingProject2) @@ -2236,7 +2221,7 @@ class WorkspaceServiceSpec // Setup BillingProjects by updating their Service Perimeter fields, then pre-populate some Workspaces in each of // the Billing Projects and therefore in the Perimeter - val workspacesInPerimeter: Seq[Workspace] = billingProjects.flatMap { bp => + billingProjects.foreach { bp => runAndWait { for { _ <- slickDataSource.dataAccess.rawlsBillingProjectQuery.updateServicePerimeter(bp.projectName, @@ -2570,13 +2555,14 @@ class WorkspaceServiceSpec val newWorkspaceName = "cloned_space" val workspaceRequest = WorkspaceRequest(testData.testProject1Name.value, newWorkspaceName, Map.empty) - val workspace = - Await.result(services.mcWorkspaceService.cloneMultiCloudWorkspace(services.workspaceService, - baseWorkspace.toWorkspaceName, - workspaceRequest - ), - Duration.Inf - ) + Await.result( + services.mcWorkspaceService.cloneMultiCloudWorkspace( + services.workspaceService, + baseWorkspace.toWorkspaceName, + workspaceRequest + ), + Duration.Inf + ) verify(services.resourceBufferService).getGoogleProjectFromBuffer(any[ProjectPoolType], any[String]) } @@ -2693,13 +2679,14 @@ class WorkspaceServiceSpec val newWorkspaceName = "cloned_space" val workspaceRequest = WorkspaceRequest(testData.testProject1Name.value, newWorkspaceName, Map.empty) - val workspace = - Await.result(services.mcWorkspaceService.cloneMultiCloudWorkspace(services.workspaceService, - baseWorkspace.toWorkspaceName, - workspaceRequest - ), - Duration.Inf - ) + Await.result( + services.mcWorkspaceService.cloneMultiCloudWorkspace( + services.workspaceService, + baseWorkspace.toWorkspaceName, + workspaceRequest + ), + Duration.Inf + ) verify(services.workspaceService.workspaceManagerDAO).cloneWorkspace( ArgumentMatchers.eq(baseWorkspace.workspaceIdAsUUID), @@ -2728,13 +2715,14 @@ class WorkspaceServiceSpec ) ).thenThrow(new ApiException(StatusCodes.NotFound.intValue, "Rawls stage workspace not found")) - val workspace = - Await.result(services.mcWorkspaceService.cloneMultiCloudWorkspace(services.workspaceService, - baseWorkspace.toWorkspaceName, - workspaceRequest - ), - Duration.Inf - ) + Await.result( + services.mcWorkspaceService.cloneMultiCloudWorkspace( + services.workspaceService, + baseWorkspace.toWorkspaceName, + workspaceRequest + ), + Duration.Inf + ) verify(services.workspaceService.workspaceManagerDAO).cloneWorkspace( ArgumentMatchers.eq(baseWorkspace.workspaceIdAsUUID), @@ -2791,9 +2779,6 @@ class WorkspaceServiceSpec // Use the WorkspaceServiceConfig to determine which static projects exist for which perimeter val servicePerimeterName: ServicePerimeterName = services.servicePerimeterServiceConfig.staticProjectsInPerimeters.keys.head - val staticProjectNumbersInPerimeter: Set[String] = - services.servicePerimeterServiceConfig.staticProjectsInPerimeters(servicePerimeterName).map(_.value).toSet - val billingProject1 = testData.testProject1 val billingProject2 = testData.testProject2 val billingProjects = Seq(billingProject1, billingProject2) @@ -2801,7 +2786,7 @@ class WorkspaceServiceSpec // Setup BillingProjects by updating their Service Perimeter fields, then pre-populate some Workspaces in each of // the Billing Projects and therefore in the Perimeter - val workspacesInPerimeter: Seq[Workspace] = billingProjects.flatMap { bp => + billingProjects.flatMap { bp => runAndWait { for { _ <- slickDataSource.dataAccess.rawlsBillingProjectQuery.updateServicePerimeter(bp.projectName, @@ -3136,11 +3121,6 @@ class WorkspaceServiceSpec it should "return the policies of a GCP workspace" in withTestDataServices { services => val workspaceName = s"rawls-test-workspace-${UUID.randomUUID().toString}" - val workspaceRequest = WorkspaceRequest( - testData.testProject1Name.value, - workspaceName, - Map.empty - ) val wsmPolicyInput = new WsmPolicyInput() .name("test_name") .namespace("test_namespace") diff --git a/core/src/test/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceServiceUnitTests.scala b/core/src/test/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceServiceUnitTests.scala index df02b4be3f..5d6ac92899 100644 --- a/core/src/test/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceServiceUnitTests.scala +++ b/core/src/test/scala/org/broadinstitute/dsde/rawls/workspace/WorkspaceServiceUnitTests.scala @@ -3,6 +3,7 @@ package org.broadinstitute.dsde.rawls.workspace import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.headers.OAuth2BearerToken +import bio.terra.workspace.client.ApiException import bio.terra.workspace.model.{ AzureContext, IamRole, @@ -18,15 +19,19 @@ import org.broadinstitute.dsde.rawls.config._ import org.broadinstitute.dsde.rawls.dataaccess._ import org.broadinstitute.dsde.rawls.dataaccess.leonardo.LeonardoService import org.broadinstitute.dsde.rawls.dataaccess.workspacemanager.WorkspaceManagerDAO -import org.broadinstitute.dsde.rawls.fastpass.{FastPassService, FastPassServiceImpl} -import org.broadinstitute.dsde.rawls.model.WorkspaceAccessLevels.WorkspaceAccessLevel +import org.broadinstitute.dsde.rawls.fastpass.FastPassService import org.broadinstitute.dsde.rawls.model.WorkspaceType.WorkspaceType import org.broadinstitute.dsde.rawls.model._ -import org.broadinstitute.dsde.rawls.resourcebuffer.ResourceBufferServiceImpl -import org.broadinstitute.dsde.rawls.serviceperimeter.ServicePerimeterServiceImpl +import org.broadinstitute.dsde.rawls.resourcebuffer.ResourceBufferService +import org.broadinstitute.dsde.rawls.serviceperimeter.ServicePerimeterService import org.broadinstitute.dsde.rawls.user.UserService import org.broadinstitute.dsde.rawls.util.MockitoTestUtils -import org.broadinstitute.dsde.rawls.{NoSuchWorkspaceException, RawlsExceptionWithErrorReport, UserDisabledException} +import org.broadinstitute.dsde.rawls.{ + NoSuchWorkspaceException, + RawlsExceptionWithErrorReport, + UserDisabledException, + WorkspaceAccessDeniedException +} import org.broadinstitute.dsde.workbench.dataaccess.NotificationDAO import org.broadinstitute.dsde.workbench.google.GoogleIamDAO import org.broadinstitute.dsde.workbench.model.{WorkbenchEmail, WorkbenchGroupName} @@ -42,6 +47,7 @@ import org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper import org.scalatest.prop.TableDrivenPropertyChecks import spray.json.{JsArray, JsObject} import org.broadinstitute.dsde.rawls.model.WorkspaceJsonSupport._ +import org.broadinstitute.dsde.rawls.submissions.SubmissionsRepository import java.util.UUID import scala.concurrent.duration._ @@ -61,10 +67,15 @@ class WorkspaceServiceUnitTests implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global - val defaultRequestContext: RawlsRequestContext = - RawlsRequestContext( - UserInfo(RawlsUserEmail("test"), OAuth2BearerToken("Bearer 123"), 123, RawlsUserSubjectId("abc")) + val ctx: RawlsRequestContext = RawlsRequestContext( + UserInfo(RawlsUserEmail("user@example.com"), + OAuth2BearerToken("Bearer 123"), + 123, + RawlsUserSubjectId("fake_user_id") ) + ) + + val enabledUser: SamUserStatusResponse = SamUserStatusResponse("fake_user_id", "user@example.com", true) val workspace: Workspace = Workspace( "test-namespace", @@ -89,9 +100,9 @@ class WorkspaceServiceUnitTests userServiceConstructor: RawlsRequestContext => UserService = _ => mock[UserService](RETURNS_SMART_NULLS), workbenchMetricBaseName: String = "", config: WorkspaceServiceConfig = mock[WorkspaceServiceConfig](RETURNS_SMART_NULLS), - requesterPaysSetupService: RequesterPaysSetupServiceImpl = mock[RequesterPaysSetupServiceImpl](RETURNS_SMART_NULLS), - resourceBufferService: ResourceBufferServiceImpl = mock[ResourceBufferServiceImpl](RETURNS_SMART_NULLS), - servicePerimeterService: ServicePerimeterServiceImpl = mock[ServicePerimeterServiceImpl](RETURNS_SMART_NULLS), + requesterPaysSetupService: RequesterPaysSetupService = mock[RequesterPaysSetupService](RETURNS_SMART_NULLS), + resourceBufferService: ResourceBufferService = mock[ResourceBufferService](RETURNS_SMART_NULLS), + servicePerimeterService: ServicePerimeterService = mock[ServicePerimeterService](RETURNS_SMART_NULLS), googleIamDao: GoogleIamDAO = mock[GoogleIamDAO](RETURNS_SMART_NULLS), terraBillingProjectOwnerRole: String = "", terraWorkspaceCanComputeRole: String = "", @@ -103,7 +114,8 @@ class WorkspaceServiceUnitTests fastPassServiceConstructor: RawlsRequestContext => FastPassService = _ => mock[FastPassService](RETURNS_SMART_NULLS), workspaceRepository: WorkspaceRepository = mock[WorkspaceRepository](RETURNS_SMART_NULLS), - billingRepository: BillingRepository = mock[BillingRepository](RETURNS_SMART_NULLS) + billingRepository: BillingRepository = mock[BillingRepository](RETURNS_SMART_NULLS), + submissionsRepository: SubmissionsRepository = mock[SubmissionsRepository](RETURNS_SMART_NULLS) ): RawlsRequestContext => WorkspaceService = info => new WorkspaceService( info, @@ -130,22 +142,18 @@ class WorkspaceServiceUnitTests new MultiCloudWorkspaceAclManager(workspaceManagerDAO, samDAO, billingProfileManagerDAO, aclManagerDatasource), fastPassServiceConstructor, workspaceRepository, - billingRepository + billingRepository, + submissionsRepository )(scala.concurrent.ExecutionContext.global) behavior of "getWorkspaceById" it should "return the workspace on success" in { val sam = mock[SamDAO] - when(sam.getUserStatus(defaultRequestContext)).thenReturn(Future(Some(SamUserStatusResponse("", "", true)))) - when( - sam.userHasAction(SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspaceActions.read, - defaultRequestContext - ) - ).thenReturn(Future(true)) - when(sam.getResourceAuthDomain(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.read, ctx)) + .thenReturn(Future(true)) + when(sam.getResourceAuthDomain(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) .thenReturn(Future(Seq())) val repository = mock[WorkspaceRepository] when(repository.getWorkspace(workspace.workspaceIdAsUUID, Some(WorkspaceAttributeSpecs(false)))) @@ -157,7 +165,7 @@ class WorkspaceServiceUnitTests samDAO = sam, workspaceRepository = repository, workspaceManagerDAO = wsm - )(defaultRequestContext) + )(ctx) val result = Await.result( service.getWorkspaceById(workspace.workspaceId, WorkspaceFieldSpecs(Some(Set("workspace")))), @@ -174,16 +182,10 @@ class WorkspaceServiceUnitTests when(repository.getWorkspace(workspace.workspaceIdAsUUID, Some(WorkspaceAttributeSpecs(true)))) .thenReturn(Future(Some(workspace))) val sam = mock[SamDAO] - when(sam.getUserStatus(defaultRequestContext)).thenReturn(Future(Some(SamUserStatusResponse("", "", true)))) - when( - sam.userHasAction(SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspaceActions.read, - defaultRequestContext - ) - ).thenReturn(Future(false)) - - val service = workspaceServiceConstructor(samDAO = sam, workspaceRepository = repository)(defaultRequestContext) + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.read, ctx)) + .thenReturn(Future(false)) + val service = workspaceServiceConstructor(samDAO = sam, workspaceRepository = repository)(ctx) val exception = intercept[NoSuchWorkspaceException] { Await.result(service.getWorkspaceById(workspace.workspaceId, WorkspaceFieldSpecs()), Duration.Inf) @@ -192,22 +194,18 @@ class WorkspaceServiceUnitTests exception.workspace shouldBe workspace.workspaceId exception.getMessage should (not include workspace.name) exception.getMessage should (not include workspace.namespace) - verify(sam).userHasAction(SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspaceActions.read, - defaultRequestContext - ) + verify(sam).userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.read, ctx) } it should "return an exception with the workspaceId when no workspace is found" in { val sam = mock[SamDAO] - when(sam.getUserStatus(defaultRequestContext)).thenReturn(Future(Some(SamUserStatusResponse("", "", true)))) + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) val repository = mock[WorkspaceRepository] when(repository.getWorkspace(workspace.workspaceIdAsUUID, Some(WorkspaceAttributeSpecs(true)))) .thenReturn(Future(None)) val exception = intercept[NoSuchWorkspaceException] { - val service = workspaceServiceConstructor(samDAO = sam, workspaceRepository = repository)(defaultRequestContext) + val service = workspaceServiceConstructor(samDAO = sam, workspaceRepository = repository)(ctx) Await.result(service.getWorkspaceById(workspace.workspaceId, WorkspaceFieldSpecs()), Duration.Inf) } @@ -218,15 +216,10 @@ class WorkspaceServiceUnitTests it should "return the workspace on success" in { val sam = mock[SamDAO] - when(sam.getUserStatus(defaultRequestContext)).thenReturn(Future(Some(SamUserStatusResponse("", "", true)))) - when( - sam.userHasAction(SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspaceActions.read, - defaultRequestContext - ) - ).thenReturn(Future(true)) - when(sam.getResourceAuthDomain(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.read, ctx)) + .thenReturn(Future(true)) + when(sam.getResourceAuthDomain(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) .thenReturn(Future(Seq())) val repository = mock[WorkspaceRepository] when(repository.getWorkspace(workspace.toWorkspaceName, Some(WorkspaceAttributeSpecs(false)))) @@ -237,19 +230,20 @@ class WorkspaceServiceUnitTests samDAO = sam, workspaceRepository = repository, workspaceManagerDAO = wsm - )(defaultRequestContext) + )(ctx) val result = Await.result( service.getWorkspace(workspace.toWorkspaceName, WorkspaceFieldSpecs(Some(Set("workspace")))), Duration.Inf ) + val fields = result.fields("workspace").asJsObject.getFields("name", "namespace") fields should contain(workspace.name) fields should contain(workspace.namespace) } it should "throw an exception when invalid fields are requested" in { - val service = workspaceServiceConstructor()(defaultRequestContext) + val service = workspaceServiceConstructor()(ctx) val invalidField = "thisFieldIsInvalid" val fields = WorkspaceFieldSpecs(Some(Set(invalidField))) @@ -262,13 +256,10 @@ class WorkspaceServiceUnitTests it should "return an unauthorized error if the user is disabled" in { val samDAO = mock[SamDAO](RETURNS_SMART_NULLS) - val samUserStatus = SamUserStatusResponse("sub", "email", enabled = false) - when(samDAO.getUserStatus(ArgumentMatchers.eq(defaultRequestContext))).thenReturn( - Future.successful(Some(samUserStatus)) - ) + when(samDAO.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser.copy(enabled = false)))) val exception = intercept[UserDisabledException] { - val service = workspaceServiceConstructor(samDAO = samDAO)(defaultRequestContext) + val service = workspaceServiceConstructor(samDAO = samDAO)(ctx) Await.result(service.getWorkspace(WorkspaceName("fake_namespace", "fake_name"), WorkspaceFieldSpecs()), Duration.Inf ) @@ -281,9 +272,8 @@ class WorkspaceServiceUnitTests it should "not preform operations for fields that are not requested" in { val options = WorkspaceService.QueryOptions(Set(), WorkspaceAttributeSpecs(false)) val wsmDao = mock[WorkspaceManagerDAO] - when(wsmDao.getWorkspace(any, any)) - .thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) - val service = workspaceServiceConstructor(workspaceManagerDAO = wsmDao)(defaultRequestContext) + when(wsmDao.getWorkspace(any, any)).thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsmDao)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) @@ -297,40 +287,29 @@ class WorkspaceServiceUnitTests val wsm = mock[WorkspaceManagerDAO] when(wsm.getWorkspace(any, any)).thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) val sam = mock[SamDAO] - when( - sam.userHasAction( - SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspaceActions.catalog, - defaultRequestContext - ) - ).thenReturn(Future(true)) - val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(defaultRequestContext) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.catalog, ctx)) + .thenReturn(Future(true)) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) result.catalog shouldBe Some(true) - verify(sam).userHasAction(SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspaceActions.catalog, - defaultRequestContext - ) + verify(sam).userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.catalog, ctx) } it should "return the highest access level in accessLevel" in { val options = WorkspaceService.QueryOptions(Set("accessLevel"), WorkspaceAttributeSpecs(false)) val wsm = mock[WorkspaceManagerDAO] - when(wsm.getWorkspace(any, any)) - .thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) + when(wsm.getWorkspace(any, any)).thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) val sam = mock[SamDAO] - when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) + when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) .thenReturn(Future(Set(SamResourceRole("READER"), SamResourceRole("OWNER")))) - val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(defaultRequestContext) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) result.accessLevel shouldBe Some(WorkspaceAccessLevels.Owner) - verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext) + verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx) } it should "return noaccess for accessLevel when sam return no roles for the user" in { @@ -340,39 +319,38 @@ class WorkspaceServiceUnitTests val wsm = mock[WorkspaceManagerDAO] when(wsm.getWorkspace(any, any)).thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) val sam = mock[SamDAO] - when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) + when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) .thenReturn(Future(Set())) - val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(defaultRequestContext) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) result.accessLevel shouldBe Some(WorkspaceAccessLevels.NoAccess) - verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext) + verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx) } it should "return true for canCompute if the user is an owner" in { val options = WorkspaceService.QueryOptions(Set("canCompute"), WorkspaceAttributeSpecs(false)) val wsm = mock[WorkspaceManagerDAO] - when(wsm.getWorkspace(any, any)) - .thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) + when(wsm.getWorkspace(any, any)).thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) val sam = mock[SamDAO] - when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) + when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) .thenReturn(Future(Set(SamResourceRole("OWNER")))) - val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(defaultRequestContext) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) result.workspace.name shouldBe workspace.name result.workspace.namespace shouldBe workspace.namespace result.canCompute shouldBe Some(true) - verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext) + verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx) } it should "return true for canCompute if the user is a writer on an azure workspace" in { val workspace = this.workspace.copy(workspaceType = WorkspaceType.McWorkspace) val options = WorkspaceService.QueryOptions(Set("canCompute"), WorkspaceAttributeSpecs(false)) val wsmDao = mock[WorkspaceManagerDAO] - when(wsmDao.getWorkspace(workspace.workspaceIdAsUUID, defaultRequestContext)) + when(wsmDao.getWorkspace(workspace.workspaceIdAsUUID, ctx)) .thenReturn( new WorkspaceDescription() .azureContext( @@ -385,67 +363,54 @@ class WorkspaceServiceUnitTests .stage(WorkspaceStageModel.MC_WORKSPACE) ) val sam = mock[SamDAO] - when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) + when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) .thenReturn(Future(Set(SamResourceRole("OWNER")))) - val service = workspaceServiceConstructor(workspaceManagerDAO = wsmDao, samDAO = sam)(defaultRequestContext) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsmDao, samDAO = sam)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) result.workspace.name shouldBe workspace.name result.workspace.namespace shouldBe workspace.namespace result.canCompute shouldBe Some(true) - verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext) + verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx) } it should "query sam for canCompute if the user is not an owner on a gcp workspace" in { val options = WorkspaceService.QueryOptions(Set("canCompute"), WorkspaceAttributeSpecs(false)) val wsmDao = mock[WorkspaceManagerDAO] - when(wsmDao.getWorkspace(any, any)) - .thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) + when(wsmDao.getWorkspace(any, any)).thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) val sam = mock[SamDAO] - when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) + when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) .thenReturn(Future(Set(SamResourceRole("WRITER")))) - when( - sam.userHasAction( - SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspaceActions.compute, - defaultRequestContext - ) - ).thenReturn(Future(true)) - val service = workspaceServiceConstructor(workspaceManagerDAO = wsmDao, samDAO = sam)(defaultRequestContext) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.compute, ctx)) + .thenReturn(Future(true)) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsmDao, samDAO = sam)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) result.workspace.name shouldBe workspace.name result.workspace.namespace shouldBe workspace.namespace result.canCompute shouldBe Some(true) - verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext) - verify(sam).userHasAction( - SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspaceActions.compute, - defaultRequestContext - ) + verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx) + verify(sam).userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.compute, ctx) } it should "return true for canShare if the user is a workspace or project owner" in { forAll(Table("role", "OWNER", "PROJECT_OWNER")) { (role: String) => val options = WorkspaceService.QueryOptions(Set("canShare"), WorkspaceAttributeSpecs(false)) val wsm = mock[WorkspaceManagerDAO] - when(wsm.getWorkspace(any, any)) - .thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) + when(wsm.getWorkspace(any, any)).thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) val sam = mock[SamDAO] - when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) + when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) .thenReturn(Future(Set(SamResourceRole(role)))) - val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(defaultRequestContext) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) result.workspace.name shouldBe workspace.name result.workspace.namespace shouldBe workspace.namespace result.canShare shouldBe Some(true) - verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext) + verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx) } } @@ -463,29 +428,29 @@ class WorkspaceServiceUnitTests when(wsmDao.getWorkspace(any, any)) .thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) val sam = mock[SamDAO] - when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) + when(sam.listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) .thenReturn(Future(Set(SamResourceRole(role)))) when( sam.userHasAction( SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.sharePolicy(role.toLowerCase), - defaultRequestContext + ctx ) ).thenReturn(Future(samAnswer)) - val service = workspaceServiceConstructor(workspaceManagerDAO = wsmDao, samDAO = sam)(defaultRequestContext) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsmDao, samDAO = sam)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) result.workspace.name shouldBe workspace.name result.workspace.namespace shouldBe workspace.namespace result.canShare shouldBe Some(samAnswer) - verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext) + verify(sam).listUserRolesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx) verify(sam).userHasAction( SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.sharePolicy(role.toLowerCase), - defaultRequestContext + ctx ) } } @@ -497,7 +462,7 @@ class WorkspaceServiceUnitTests val gcs = mock[GoogleServicesDAO] val bucketDetails = WorkspaceBucketOptions(true) when(gcs.getBucketDetails(workspace.bucketName, workspace.googleProjectId)).thenReturn(Future(bucketDetails)) - val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, gcsDAO = gcs)(defaultRequestContext) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, gcsDAO = gcs)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) @@ -512,15 +477,9 @@ class WorkspaceServiceUnitTests val sam = mock[SamDAO] val ownerEmails = Set("user1@test.com", "user2@test.com") val owners = SamPolicy(ownerEmails.map(WorkbenchEmail), Set(), Set()) - when( - sam.getPolicy(SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspacePolicyNames.owner, - defaultRequestContext - ) - ) + when(sam.getPolicy(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspacePolicyNames.owner, ctx)) .thenReturn(Future(owners)) - val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(defaultRequestContext) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) @@ -533,9 +492,9 @@ class WorkspaceServiceUnitTests when(wsm.getWorkspace(any, any)).thenAnswer(_ => throw new AggregateWorkspaceNotFoundException(ErrorReport(""))) val sam = mock[SamDAO] val authDomains = Seq("some-auth-domain") - when(sam.getResourceAuthDomain(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) + when(sam.getResourceAuthDomain(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) .thenReturn(Future(authDomains)) - val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(defaultRequestContext) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, samDAO = sam)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) @@ -550,10 +509,7 @@ class WorkspaceServiceUnitTests val stats = WorkspaceSubmissionStats(None, None, 3) val workspaceRepository = mock[WorkspaceRepository] when(workspaceRepository.getSubmissionSummaryStats(workspace.workspaceIdAsUUID)).thenReturn(Future(Some(stats))) - val service = workspaceServiceConstructor( - workspaceManagerDAO = wsm, - workspaceRepository = workspaceRepository - )(defaultRequestContext) + val service = workspaceServiceConstructor(workspaceManagerDAO = wsm, workspaceRepository = workspaceRepository)(ctx) val result = Await.result(service.getWorkspaceDetails(workspace, options), Duration.Inf) @@ -563,18 +519,15 @@ class WorkspaceServiceUnitTests behavior of "listWorkspaces" it should "return an empty response when the user has no workspaces" in { val sam = mock[SamDAO] - when(sam.listUserResources(SamResourceTypeNames.workspace, defaultRequestContext)) + when(sam.listUserResources(SamResourceTypeNames.workspace, ctx)) .thenReturn(Future(Seq())) val wsRepo = mock[WorkspaceRepository] when(wsRepo.listWorkspacesByIds(ArgumentMatchers.eq(Seq()), any)).thenReturn(Future(Seq())) when(wsRepo.listSubmissionSummaryStats(Seq())).thenReturn(Future(Map())) val wsm = mock[WorkspaceManagerDAO] - when(wsm.listWorkspaces(defaultRequestContext)).thenReturn(List()) - val service = workspaceServiceConstructor( - samDAO = sam, - workspaceManagerDAO = wsm, - workspaceRepository = wsRepo - )(defaultRequestContext) + when(wsm.listWorkspaces(ctx)).thenReturn(List()) + val service = + workspaceServiceConstructor(samDAO = sam, workspaceManagerDAO = wsm, workspaceRepository = wsRepo)(ctx) val result = Await.result(service.listWorkspaces(WorkspaceFieldSpecs(), -1), Duration.Inf) @@ -604,8 +557,7 @@ class WorkspaceServiceUnitTests Set() ) val sam = mock[SamDAO] - when(sam.listUserResources(SamResourceTypeNames.workspace, defaultRequestContext)) - .thenReturn(Future(Seq(workspaceSamResource))) + when(sam.listUserResources(SamResourceTypeNames.workspace, ctx)).thenReturn(Future(Seq(workspaceSamResource))) val wsRepo = mock[WorkspaceRepository] if (hasAccess) { when(wsRepo.listWorkspacesByIds(ArgumentMatchers.eq(Seq(workspace.workspaceIdAsUUID)), any)) @@ -614,12 +566,12 @@ class WorkspaceServiceUnitTests when(wsRepo.listWorkspacesByIds(ArgumentMatchers.eq(Seq()), any)).thenReturn(Future(Seq())) } val wsm = mock[WorkspaceManagerDAO] - when(wsm.listWorkspaces(defaultRequestContext)).thenReturn(List()) + when(wsm.listWorkspaces(ctx)).thenReturn(List()) val service = workspaceServiceConstructor( samDAO = sam, workspaceManagerDAO = wsm, workspaceRepository = wsRepo - )(defaultRequestContext) + )(ctx) val params = WorkspaceFieldSpecs(fields = Some(Set("workspace", "accessLevel", "public"))) val result = Await.result(service.listWorkspaces(params, -1), Duration.Inf) @@ -651,18 +603,14 @@ class WorkspaceServiceUnitTests Set() ) val sam = mock[SamDAO] - when(sam.listUserResources(SamResourceTypeNames.workspace, defaultRequestContext)) - .thenReturn(Future(Seq(workspaceSamResource))) + when(sam.listUserResources(SamResourceTypeNames.workspace, ctx)).thenReturn(Future(Seq(workspaceSamResource))) val wsRepo = mock[WorkspaceRepository] when(wsRepo.listWorkspacesByIds(ArgumentMatchers.eq(Seq(workspace.workspaceIdAsUUID)), any)) .thenReturn(Future(Seq(workspace))) val wsm = mock[WorkspaceManagerDAO] - when(wsm.listWorkspaces(defaultRequestContext)).thenReturn(List()) - val service = workspaceServiceConstructor( - samDAO = sam, - workspaceManagerDAO = wsm, - workspaceRepository = wsRepo - )(defaultRequestContext) + when(wsm.listWorkspaces(ctx)).thenReturn(List()) + val service = + workspaceServiceConstructor(samDAO = sam, workspaceManagerDAO = wsm, workspaceRepository = wsRepo)(ctx) val params = WorkspaceFieldSpecs(fields = Some(Set("workspace", "accessLevel", "public"))) val result = Await.result(service.listWorkspaces(params, -1), Duration.Inf) @@ -695,21 +643,19 @@ class WorkspaceServiceUnitTests Set() ) val sam = mock[SamDAO] - when(sam.listUserResources(SamResourceTypeNames.workspace, defaultRequestContext)) - .thenReturn(Future(Seq(workspaceSamResource))) + when(sam.listUserResources(SamResourceTypeNames.workspace, ctx)).thenReturn(Future(Seq(workspaceSamResource))) val wsRepo = mock[WorkspaceRepository] when(wsRepo.listWorkspacesByIds(ArgumentMatchers.eq(Seq(workspace.workspaceIdAsUUID)), any)) .thenReturn(Future(Seq(workspace))) val wsm = mock[WorkspaceManagerDAO] - when(wsm.listWorkspaces(defaultRequestContext)).thenReturn(List()) - val service = workspaceServiceConstructor( - samDAO = sam, - workspaceManagerDAO = wsm, - workspaceRepository = wsRepo - )(defaultRequestContext) - + when(wsm.listWorkspaces(ctx)).thenReturn(List()) val params = WorkspaceFieldSpecs(fields = Some(Set("workspace", "public", "accessLevel", "canShare"))) - val result = Await.result(service.listWorkspaces(params, -1), Duration.Inf) + + val result = Await.result( + workspaceServiceConstructor(samDAO = sam, workspaceManagerDAO = wsm, workspaceRepository = wsRepo)(ctx) + .listWorkspaces(params, -1), + Duration.Inf + ) val resultWorkspace = result match { case jsa: JsArray => @@ -755,10 +701,8 @@ class WorkspaceServiceUnitTests Set(), Set() ) - val sam = mock[SamDAO] - when(sam.listUserResources(SamResourceTypeNames.workspace, defaultRequestContext)) - .thenReturn(Future(Seq(workspaceSamResource))) + when(sam.listUserResources(SamResourceTypeNames.workspace, ctx)).thenReturn(Future(Seq(workspaceSamResource))) val wsRepo = mock[WorkspaceRepository] when(wsRepo.listWorkspacesByIds(ArgumentMatchers.eq(Seq(workspace.workspaceIdAsUUID)), any)) .thenReturn(Future(Seq(workspace))) @@ -778,15 +722,14 @@ class WorkspaceServiceUnitTests .stage(WorkspaceStageModel.MC_WORKSPACE) ) } - when(wsm.listWorkspaces(defaultRequestContext)).thenReturn(wsmWorkspaces) - val service = workspaceServiceConstructor( - samDAO = sam, - workspaceManagerDAO = wsm, - workspaceRepository = wsRepo - )(defaultRequestContext) + when(wsm.listWorkspaces(ctx)).thenReturn(wsmWorkspaces) val params = WorkspaceFieldSpecs(fields = Some(Set("workspace", "public", "accessLevel", "canCompute"))) - val result = Await.result(service.listWorkspaces(params, -1), Duration.Inf) + val result = Await.result( + workspaceServiceConstructor(samDAO = sam, workspaceManagerDAO = wsm, workspaceRepository = wsRepo)(ctx) + .listWorkspaces(params, -1), + Duration.Inf + ) val resultWorkspace = result match { case jsa: JsArray => @@ -811,21 +754,19 @@ class WorkspaceServiceUnitTests ) val sam = mock[SamDAO] - when(sam.listUserResources(SamResourceTypeNames.workspace, defaultRequestContext)) - .thenReturn(Future(Seq(workspaceSamResource))) + when(sam.listUserResources(SamResourceTypeNames.workspace, ctx)).thenReturn(Future(Seq(workspaceSamResource))) val wsRepo = mock[WorkspaceRepository] when(wsRepo.listWorkspacesByIds(ArgumentMatchers.eq(Seq(workspace.workspaceIdAsUUID)), any)) .thenReturn(Future(Seq(workspace))) val wsm = mock[WorkspaceManagerDAO] - when(wsm.listWorkspaces(defaultRequestContext)).thenReturn(List()) - val service = workspaceServiceConstructor( - samDAO = sam, - workspaceManagerDAO = wsm, - workspaceRepository = wsRepo - )(defaultRequestContext) - + when(wsm.listWorkspaces(ctx)).thenReturn(List()) val params = WorkspaceFieldSpecs(fields = Some(Set("workspace", "accessLevel", "public"))) - val result = Await.result(service.listWorkspaces(params, -1), Duration.Inf) + + val result = Await.result( + workspaceServiceConstructor(samDAO = sam, workspaceManagerDAO = wsm, workspaceRepository = wsRepo)(ctx) + .listWorkspaces(params, -1), + Duration.Inf + ) val resultWorkspace = result match { case jsa: JsArray => @@ -854,7 +795,7 @@ class WorkspaceServiceUnitTests direct = SamRolesAndActions(Set(SamWorkspaceRoles.writer), Set(SamWorkspaceActions.compute)) ) val sam = mock[SamDAO] - when(sam.listUserResources(SamResourceTypeNames.workspace, defaultRequestContext)) + when(sam.listUserResources(SamResourceTypeNames.workspace, ctx)) .thenReturn(Future(Seq(workspace2SamResource, workspace1SamResource))) val wsRepo = mock[WorkspaceRepository] when(wsRepo.listWorkspacesByIds(ArgumentMatchers.eq(workspaceIds), any)) @@ -870,12 +811,12 @@ class WorkspaceServiceUnitTests ) ) val wsm = mock[WorkspaceManagerDAO] - when(wsm.listWorkspaces(defaultRequestContext)).thenReturn(List()) + when(wsm.listWorkspaces(ctx)).thenReturn(List()) val service = workspaceServiceConstructor( samDAO = sam, workspaceManagerDAO = wsm, workspaceRepository = wsRepo - )(defaultRequestContext) + )(ctx) val params = WorkspaceFieldSpecs(fields = Some(Set("workspace", "accessLevel", "public", "workspaceSubmissionStats"))) @@ -919,7 +860,7 @@ class WorkspaceServiceUnitTests direct = SamRolesAndActions(Set(SamWorkspaceRoles.writer), Set(SamWorkspaceActions.compute)) ) val sam = mock[SamDAO] - when(sam.listUserResources(SamResourceTypeNames.workspace, defaultRequestContext)) + when(sam.listUserResources(SamResourceTypeNames.workspace, ctx)) .thenReturn(Future(Seq(workspace2SamResource, workspace1SamResource))) val wsRepo = mock[WorkspaceRepository] when(wsRepo.listWorkspacesByIds(ArgumentMatchers.eq(workspaceIds), any)) @@ -935,12 +876,12 @@ class WorkspaceServiceUnitTests .id(workspace1.workspaceIdAsUUID) .stage(WorkspaceStageModel.MC_WORKSPACE) - when(wsm.listWorkspaces(defaultRequestContext)).thenReturn(List(workspace1WSMDescription)) + when(wsm.listWorkspaces(ctx)).thenReturn(List(workspace1WSMDescription)) val service = workspaceServiceConstructor( samDAO = sam, workspaceManagerDAO = wsm, workspaceRepository = wsRepo - )(defaultRequestContext) + )(ctx) val params = WorkspaceFieldSpecs(fields = Some(Set("workspace", "accessLevel", "public"))) @@ -967,133 +908,526 @@ class WorkspaceServiceUnitTests resultWorkspaces(workspace2.workspaceId) shouldBe expectedResult2 } - behavior of "assertNoGoogleChildrenBlockingWorkspaceDeletion" + behavior of "deleteWorkspace" - it should "not error if the only child is the google project" in { - val samDAO = mock[SamDAO] - when(samDAO.listResourceChildren(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) - .thenReturn( - Future( - Seq( - SamFullyQualifiedResourceId(workspace.googleProjectId.value, SamResourceTypeNames.googleProject.value) - ) - ) - ) - when( - samDAO.listResourceChildren( - SamResourceTypeNames.googleProject, - workspace.googleProjectId.value, - defaultRequestContext - ) - ) - .thenReturn(Future(Seq())) - val workspaceService = workspaceServiceConstructor(samDAO = samDAO)(defaultRequestContext) + it should "fail if the user does not have the delete permission for the workspace" in { + val sam = mock[SamDAO] + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.delete, ctx)) + .thenReturn(Future(false)) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.read, ctx)) + .thenReturn(Future(true)) + val repo = mock[WorkspaceRepository] + when(repo.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) + val service = workspaceServiceConstructor(samDAO = sam, workspaceRepository = repo)(ctx) + + intercept[WorkspaceAccessDeniedException] { + Await.result(service.deleteWorkspace(workspace.toWorkspaceName), Duration.Inf) + } - Await.result(workspaceService.assertNoGoogleChildrenBlockingWorkspaceDeletion(workspace), Duration.Inf) shouldBe () + verify(sam).userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.delete, ctx) } - it should "error if the workspace google project has a child resource" in { - val samDAO = mock[SamDAO] - when(samDAO.listResourceChildren(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) - .thenReturn(Future(Seq())) - when( - samDAO.listResourceChildren( - SamResourceTypeNames.googleProject, - workspace.googleProjectId.value, - defaultRequestContext - ) - ) - .thenReturn(Future(Seq(SamFullyQualifiedResourceId("some-child", SamResourceTypeNames.googleProject.value)))) - val workspaceService = workspaceServiceConstructor(samDAO = samDAO)(defaultRequestContext) + it should "fail if called for a multi cloud workspace" in { + val workspace = this.workspace.copy(workspaceType = WorkspaceType.McWorkspace) + val sam = mock[SamDAO] + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.delete, ctx)) + .thenReturn(Future(true)) + val repo = mock[WorkspaceRepository] + when(repo.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) + val service = workspaceServiceConstructor(samDAO = sam, workspaceRepository = repo)(ctx) - val error = intercept[RawlsExceptionWithErrorReport] { - Await.result(workspaceService.assertNoGoogleChildrenBlockingWorkspaceDeletion(workspace), Duration.Inf) + val exception = intercept[RawlsExceptionWithErrorReport] { + Await.result(service.deleteWorkspace(workspace.toWorkspaceName), Duration.Inf) } - error.errorReport.statusCode.get shouldBe StatusCodes.BadRequest - error.errorReport.message shouldBe "Workspace deletion blocked by child resources" - error.errorReport.causes.size shouldBe 1 + exception.errorReport.statusCode shouldBe Some(StatusCodes.BadRequest) } - it should "error if the workspace has a child resource besides it's google project" in { - val samDAO = mock[SamDAO] - when(samDAO.listResourceChildren(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) - .thenReturn( - Future( - Seq( - SamFullyQualifiedResourceId(workspace.googleProjectId.value, SamResourceTypeNames.googleProject.value) - ) - ) - ) - when( - samDAO.listResourceChildren( - SamResourceTypeNames.googleProject, - workspace.googleProjectId.value, - defaultRequestContext - ) - ) - .thenReturn(Future(Seq(SamFullyQualifiedResourceId("some-child", SamResourceTypeNames.googleProject.value)))) - val workspaceService = workspaceServiceConstructor(samDAO = samDAO)(defaultRequestContext) + it should "delete the workspace in sam and the database" in { + val sam = mock[SamDAO] + val repo = mock[WorkspaceRepository] + val requesterPaysService = mock[RequesterPaysSetupService] + val submissionsRepository = mock[SubmissionsRepository] + val leo = mock[LeonardoService] + val fastPass = mock[FastPassService] + val gcs = mock[GoogleServicesDAO] + // mocked operations are defined in the order they are called by the service + // initial auth checks/workspace retrieval + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.delete, ctx)) + .thenReturn(Future(true)) + when(repo.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) + // delete requester pays records + when(requesterPaysService.deleteAllRecordsForWorkspace(workspace)).thenReturn(Future(1)) + // abort workflows + when(submissionsRepository.getActiveWorkflowsAndSetStatusToAborted(workspace)).thenReturn(Future(Seq())) + // delete fast pass grants + when(fastPass.removeFastPassGrantsForWorkspace(workspace)).thenReturn(Future()) + // notify leo to clean up resources + when(leo.cleanupResources(workspace.googleProjectId, workspace.workspaceIdAsUUID, ctx)).thenReturn(Future()) + // delete google project + when(sam.listAllResourceMemberIds(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future(Set())) + when(gcs.deleteGoogleProject(workspace.googleProjectId)).thenReturn(Future()) + when(sam.deleteResource(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future()) + // delete workspace and associated records + when(repo.deleteRawlsWorkspace(workspace)).thenReturn(Future()) + // delete workflow collection in sam + when(sam.deleteResource(SamResourceTypeNames.workflowCollection, workspace.workflowCollectionName.get, ctx)) + .thenReturn(Future()) + // delete workspace in sam + when(sam.deleteResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)).thenReturn(Future()) + val service = workspaceServiceConstructor( + samDAO = sam, + requesterPaysSetupService = requesterPaysService, + fastPassServiceConstructor = _ => fastPass, + leonardoService = leo, + workspaceRepository = repo, + gcsDAO = gcs, + submissionsRepository = submissionsRepository + )(ctx) + + val result = Await.result(service.deleteWorkspace(workspace.toWorkspaceName), Duration.Inf) + + result shouldBe WorkspaceDeletionResult.fromGcpBucketName(workspace.bucketName) + verify(repo).deleteRawlsWorkspace(workspace) + verify(sam).deleteResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx) + } - val error = intercept[RawlsExceptionWithErrorReport] { - Await.result(workspaceService.assertNoGoogleChildrenBlockingWorkspaceDeletion(workspace), Duration.Inf) - } + it should "attempt to delete the workspace in wsm, but never fail because of it" in { + val sam = mock[SamDAO] + val repo = mock[WorkspaceRepository] + val requesterPaysService = mock[RequesterPaysSetupService] + val submissionsRepository = mock[SubmissionsRepository] + val leo = mock[LeonardoService] + val fastPass = mock[FastPassService] + val gcs = mock[GoogleServicesDAO] + val wsm = mock[WorkspaceManagerDAO] + // mocked operations are defined in the order they are called by the service + // initial auth checks/workspace retrieval + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.delete, ctx)) + .thenReturn(Future(true)) + when(repo.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) + // delete requester pays records + when(requesterPaysService.deleteAllRecordsForWorkspace(workspace)).thenReturn(Future(1)) + // abort workflows + when(submissionsRepository.getActiveWorkflowsAndSetStatusToAborted(workspace)).thenReturn(Future(Seq())) + // delete fast pass grants + when(fastPass.removeFastPassGrantsForWorkspace(workspace)).thenReturn(Future()) + // notify leo to clean up resources + when(leo.cleanupResources(workspace.googleProjectId, workspace.workspaceIdAsUUID, ctx)).thenReturn(Future()) + // delete google project + when(sam.listAllResourceMemberIds(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future(Set())) + when(gcs.deleteGoogleProject(workspace.googleProjectId)).thenReturn(Future()) + when(sam.deleteResource(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future()) + // delete workspace in wsm + when(wsm.deleteWorkspace(workspace.workspaceIdAsUUID, ctx)).thenAnswer(_ => throw new ApiException(500, "failed")) + // delete workspace and associated records + when(repo.deleteRawlsWorkspace(workspace)).thenReturn(Future()) + // delete workflow collection in sam + when(sam.deleteResource(SamResourceTypeNames.workflowCollection, workspace.workflowCollectionName.get, ctx)) + .thenReturn(Future()) + // delete workspace in sam + when(sam.deleteResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)).thenReturn(Future()) + val service = workspaceServiceConstructor( + samDAO = sam, + requesterPaysSetupService = requesterPaysService, + fastPassServiceConstructor = _ => fastPass, + leonardoService = leo, + workspaceRepository = repo, + workspaceManagerDAO = wsm, + gcsDAO = gcs, + submissionsRepository = submissionsRepository + )(ctx) + + val result = Await.result(service.deleteWorkspace(workspace.toWorkspaceName), Duration.Inf) - error.errorReport.statusCode.get shouldBe StatusCodes.BadRequest - error.errorReport.message shouldBe "Workspace deletion blocked by child resources" - error.errorReport.causes.size shouldBe 1 + result shouldBe WorkspaceDeletionResult.fromGcpBucketName(workspace.bucketName) + verify(wsm).deleteWorkspace(workspace.workspaceIdAsUUID, ctx) } - it should "return an error for each blocking child resource in the error report" in { - val samDAO = mock[SamDAO] - when(samDAO.listResourceChildren(SamResourceTypeNames.workspace, workspace.workspaceId, defaultRequestContext)) - .thenReturn( - Future( - Seq( - SamFullyQualifiedResourceId(workspace.googleProjectId.value, SamResourceTypeNames.googleProject.value), - SamFullyQualifiedResourceId("another-resource", SamResourceTypeNames.googleProject.value) - ) - ) - ) - when( - samDAO.listResourceChildren( - SamResourceTypeNames.googleProject, - workspace.googleProjectId.value, - defaultRequestContext - ) - ) - .thenReturn(Future(Seq(SamFullyQualifiedResourceId("some-child", SamResourceTypeNames.googleProject.value)))) - val workspaceService = workspaceServiceConstructor(samDAO = samDAO)(defaultRequestContext) + it should "ignore 404 errors from sam when deleting the google project resource" in { + val sam = mock[SamDAO] + val repo = mock[WorkspaceRepository] + val requesterPaysService = mock[RequesterPaysSetupService] + val submissionsRepository = mock[SubmissionsRepository] + val leo = mock[LeonardoService] + val fastPass = mock[FastPassService] + val gcs = mock[GoogleServicesDAO] + // mocked operations are defined in the order they are called by the service + // initial auth checks/workspace retrieval + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.delete, ctx)) + .thenReturn(Future(true)) + when(repo.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) + // delete requester pays records + when(requesterPaysService.deleteAllRecordsForWorkspace(workspace)).thenReturn(Future(1)) + // abort workflows + when(submissionsRepository.getActiveWorkflowsAndSetStatusToAborted(workspace)).thenReturn(Future(Seq())) + // delete fast pass grants + when(fastPass.removeFastPassGrantsForWorkspace(workspace)).thenReturn(Future()) + // notify leo to clean up resources + when(leo.cleanupResources(workspace.googleProjectId, workspace.workspaceIdAsUUID, ctx)).thenReturn(Future()) + // delete google project + when(sam.listAllResourceMemberIds(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future(Set())) + when(gcs.deleteGoogleProject(workspace.googleProjectId)).thenReturn(Future()) + // throw 404 when deleting the google project resource in sam + when(sam.deleteResource(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future.failed(RawlsExceptionWithErrorReport(StatusCodes.NotFound, ""))) + // delete workspace and associated records + when(repo.deleteRawlsWorkspace(workspace)).thenReturn(Future()) + // delete workflow collection in sam + when(sam.deleteResource(SamResourceTypeNames.workflowCollection, workspace.workflowCollectionName.get, ctx)) + .thenReturn(Future()) + when(sam.deleteResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)).thenReturn(Future()) + val service = workspaceServiceConstructor( + samDAO = sam, + requesterPaysSetupService = requesterPaysService, + fastPassServiceConstructor = _ => fastPass, + leonardoService = leo, + workspaceRepository = repo, + gcsDAO = gcs, + submissionsRepository = submissionsRepository + )(ctx) + + val result = Await.result(service.deleteWorkspace(workspace.toWorkspaceName), Duration.Inf) + + result shouldBe WorkspaceDeletionResult.fromGcpBucketName(workspace.bucketName) + verify(sam).deleteResource(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx) + } - val error = intercept[RawlsExceptionWithErrorReport] { - Await.result(workspaceService.assertNoGoogleChildrenBlockingWorkspaceDeletion(workspace), Duration.Inf) + it should "ignore 404 errors from sam when deleting the workflow collection resource" in { + val sam = mock[SamDAO] + val repo = mock[WorkspaceRepository] + val requesterPaysService = mock[RequesterPaysSetupService] + val submissionsRepository = mock[SubmissionsRepository] + val leo = mock[LeonardoService] + val fastPass = mock[FastPassService] + val gcs = mock[GoogleServicesDAO] + // mocked operations are defined in the order they are called by the service + // initial auth checks/workspace retrieval + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.delete, ctx)) + .thenReturn(Future(true)) + when(repo.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) + // delete requester pays records + when(requesterPaysService.deleteAllRecordsForWorkspace(workspace)).thenReturn(Future(1)) + // abort workflows + when(submissionsRepository.getActiveWorkflowsAndSetStatusToAborted(workspace)).thenReturn(Future(Seq())) + // delete fast pass grants + when(fastPass.removeFastPassGrantsForWorkspace(workspace)).thenReturn(Future()) + // notify leo to clean up resources + when(leo.cleanupResources(workspace.googleProjectId, workspace.workspaceIdAsUUID, ctx)).thenReturn(Future()) + // delete google project + when(sam.listAllResourceMemberIds(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future(Set())) + when(gcs.deleteGoogleProject(workspace.googleProjectId)).thenReturn(Future()) + when(sam.deleteResource(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future()) + // delete workspace and associated records + when(repo.deleteRawlsWorkspace(workspace)).thenReturn(Future()) + // throw 404 when deleting workflow collection in sam + when(sam.deleteResource(SamResourceTypeNames.workflowCollection, workspace.workflowCollectionName.get, ctx)) + .thenReturn(Future.failed(RawlsExceptionWithErrorReport(StatusCodes.NotFound, ""))) + when(sam.deleteResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)).thenReturn(Future()) + val service = workspaceServiceConstructor( + samDAO = sam, + requesterPaysSetupService = requesterPaysService, + fastPassServiceConstructor = _ => fastPass, + leonardoService = leo, + workspaceRepository = repo, + gcsDAO = gcs, + submissionsRepository = submissionsRepository + )(ctx) + + val result = Await.result(service.deleteWorkspace(workspace.toWorkspaceName), Duration.Inf) + + result shouldBe WorkspaceDeletionResult.fromGcpBucketName(workspace.bucketName) + verify(sam).deleteResource(SamResourceTypeNames.workflowCollection, workspace.workflowCollectionName.get, ctx) + } + + it should "throw non 404 errors from sam when deleting the workflow collection resource" in { + val sam = mock[SamDAO] + val repo = mock[WorkspaceRepository] + val requesterPaysService = mock[RequesterPaysSetupService] + val submissionsRepository = mock[SubmissionsRepository] + val leo = mock[LeonardoService] + val fastPass = mock[FastPassService] + val gcs = mock[GoogleServicesDAO] + // mocked operations are defined in the order they are called by the service + // initial auth checks/workspace retrieval + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.delete, ctx)) + .thenReturn(Future(true)) + when(repo.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) + // delete requester pays records + when(requesterPaysService.deleteAllRecordsForWorkspace(workspace)).thenReturn(Future(1)) + // abort workflows + when(submissionsRepository.getActiveWorkflowsAndSetStatusToAborted(workspace)).thenReturn(Future(Seq())) + // delete fast pass grants + when(fastPass.removeFastPassGrantsForWorkspace(workspace)).thenReturn(Future()) + // notify leo to clean up resources + when(leo.cleanupResources(workspace.googleProjectId, workspace.workspaceIdAsUUID, ctx)).thenReturn(Future()) + // delete google project + when(sam.listAllResourceMemberIds(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future(Set())) + when(gcs.deleteGoogleProject(workspace.googleProjectId)).thenReturn(Future()) + when(sam.deleteResource(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future()) + // delete workspace and associated records + when(repo.deleteRawlsWorkspace(workspace)).thenReturn(Future()) + // throw exception when deleting workflow collection in sam + val workflowDeletionException = RawlsExceptionWithErrorReport(StatusCodes.BadGateway, "") + when(sam.deleteResource(SamResourceTypeNames.workflowCollection, workspace.workflowCollectionName.get, ctx)) + .thenReturn(Future.failed(workflowDeletionException)) + when(sam.deleteResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)).thenReturn(Future()) + val service = workspaceServiceConstructor( + samDAO = sam, + requesterPaysSetupService = requesterPaysService, + fastPassServiceConstructor = _ => fastPass, + leonardoService = leo, + workspaceRepository = repo, + gcsDAO = gcs, + submissionsRepository = submissionsRepository + )(ctx) + + val exception = intercept[RawlsExceptionWithErrorReport] { + Await.result(service.deleteWorkspace(workspace.toWorkspaceName), Duration.Inf) } + exception shouldBe workflowDeletionException + verify(sam).deleteResource(SamResourceTypeNames.workflowCollection, workspace.workflowCollectionName.get, ctx) + } + + it should "ignore 404 errors when deleting the workspace resource in sam" in { + val sam = mock[SamDAO] + val repo = mock[WorkspaceRepository] + val requesterPaysService = mock[RequesterPaysSetupService] + val submissionsRepository = mock[SubmissionsRepository] + val leo = mock[LeonardoService] + val fastPass = mock[FastPassService] + val gcs = mock[GoogleServicesDAO] + // mocked operations are defined in the order they are called by the service + // initial auth checks/workspace retrieval + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.delete, ctx)) + .thenReturn(Future(true)) + when(repo.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) + // delete requester pays records + when(requesterPaysService.deleteAllRecordsForWorkspace(workspace)).thenReturn(Future(1)) + // abort workflows + when(submissionsRepository.getActiveWorkflowsAndSetStatusToAborted(workspace)).thenReturn(Future(Seq())) + // delete fast pass grants + when(fastPass.removeFastPassGrantsForWorkspace(workspace)).thenReturn(Future()) + // notify leo to clean up resources + when(leo.cleanupResources(workspace.googleProjectId, workspace.workspaceIdAsUUID, ctx)).thenReturn(Future()) + // delete google project + when(sam.listAllResourceMemberIds(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future(Set())) + when(gcs.deleteGoogleProject(workspace.googleProjectId)).thenReturn(Future()) + when(sam.deleteResource(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future()) + // delete workspace and associated records + when(repo.deleteRawlsWorkspace(workspace)).thenReturn(Future()) + // delete workflow collection in sam + when(sam.deleteResource(SamResourceTypeNames.workflowCollection, workspace.workflowCollectionName.get, ctx)) + .thenReturn(Future()) + // throw 404 when deleting workspace in sam + when(sam.deleteResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) + .thenReturn(Future.failed(RawlsExceptionWithErrorReport(StatusCodes.NotFound, ""))) - error.errorReport.statusCode.get shouldBe StatusCodes.BadRequest - error.errorReport.message shouldBe "Workspace deletion blocked by child resources" - error.errorReport.causes.size shouldBe 2 + val service = workspaceServiceConstructor( + samDAO = sam, + requesterPaysSetupService = requesterPaysService, + fastPassServiceConstructor = _ => fastPass, + leonardoService = leo, + workspaceRepository = repo, + gcsDAO = gcs, + submissionsRepository = submissionsRepository + )(ctx) + + val result = Await.result(service.deleteWorkspace(workspace.toWorkspaceName), Duration.Inf) + + result shouldBe WorkspaceDeletionResult.fromGcpBucketName(workspace.bucketName) + verify(sam).deleteResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx) } - it should "error if there is no googleProjectId" in { - val samDAO = mock[SamDAO] - val workspaceService = workspaceServiceConstructor(samDAO = samDAO)(defaultRequestContext) - val wsId = UUID.randomUUID().toString - val azureWorkspace = Workspace.buildReadyMcWorkspace( - namespace = "test-azure-bp", - name = s"test-azure-ws-$wsId", - workspaceId = wsId, - createdDate = DateTime.now, - lastModified = DateTime.now, - createdBy = "testuser@example.com", - attributes = Map() - ) + it should "rethrow errors for resource children when deleting the workspace resource in sam" in { + val sam = mock[SamDAO] + val repo = mock[WorkspaceRepository] + val requesterPaysService = mock[RequesterPaysSetupService] + val submissionsRepository = mock[SubmissionsRepository] + val leo = mock[LeonardoService] + val fastPass = mock[FastPassService] + val gcs = mock[GoogleServicesDAO] + // mocked operations are defined in the order they are called by the service + // initial auth checks/workspace retrieval + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.delete, ctx)) + .thenReturn(Future(true)) + when(repo.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) + // delete requester pays records + when(requesterPaysService.deleteAllRecordsForWorkspace(workspace)).thenReturn(Future(1)) + // abort workflows + when(submissionsRepository.getActiveWorkflowsAndSetStatusToAborted(workspace)).thenReturn(Future(Seq())) + // delete fast pass grants + when(fastPass.removeFastPassGrantsForWorkspace(workspace)).thenReturn(Future()) + // notify leo to clean up resources + when(leo.cleanupResources(workspace.googleProjectId, workspace.workspaceIdAsUUID, ctx)).thenReturn(Future()) + // delete google project + when(sam.listAllResourceMemberIds(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future(Set())) + when(gcs.deleteGoogleProject(workspace.googleProjectId)).thenReturn(Future()) + when(sam.deleteResource(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future()) + // delete workspace and associated records + when(repo.deleteRawlsWorkspace(workspace)).thenReturn(Future()) + // delete workflow collection in sam + when(sam.deleteResource(SamResourceTypeNames.workflowCollection, workspace.workflowCollectionName.get, ctx)) + .thenReturn(Future()) + // throw exception for child resources when deleting workspace in sam + val samError = RawlsExceptionWithErrorReport(StatusCodes.BadRequest, "Cannot delete a resource with children") + when(sam.deleteResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) + .thenReturn(Future.failed(samError)) - val error = intercept[RawlsExceptionWithErrorReport] { - Await.result(workspaceService.assertNoGoogleChildrenBlockingWorkspaceDeletion(azureWorkspace), Duration.Inf) + val service = workspaceServiceConstructor( + samDAO = sam, + requesterPaysSetupService = requesterPaysService, + fastPassServiceConstructor = _ => fastPass, + leonardoService = leo, + workspaceRepository = repo, + gcsDAO = gcs, + submissionsRepository = submissionsRepository + )(ctx) + + val exception = intercept[RawlsExceptionWithErrorReport] { + val result = Await.result(service.deleteWorkspace(workspace.toWorkspaceName), Duration.Inf) + result shouldBe WorkspaceDeletionResult.fromGcpBucketName(workspace.bucketName) } - error.errorReport.statusCode.get shouldBe StatusCodes.InternalServerError - assert(error.errorReport.message contains "with no googleProjectId") + exception shouldBe samError + } + + it should "delete pets when deleting the google project" in { + val sam = mock[SamDAO] + val repo = mock[WorkspaceRepository] + val requesterPaysService = mock[RequesterPaysSetupService] + val submissionsRepository = mock[SubmissionsRepository] + val leo = mock[LeonardoService] + val fastPass = mock[FastPassService] + val gcs = mock[GoogleServicesDAO] + // mocked operations are defined in the order they are called by the service + // initial auth checks/workspace retrieval + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.delete, ctx)) + .thenReturn(Future(true)) + when(repo.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) + // delete requester pays records + when(requesterPaysService.deleteAllRecordsForWorkspace(workspace)).thenReturn(Future(1)) + // abort workflows + when(submissionsRepository.getActiveWorkflowsAndSetStatusToAborted(workspace)).thenReturn(Future(Seq())) + // delete fast pass grants + when(fastPass.removeFastPassGrantsForWorkspace(workspace)).thenReturn(Future()) + // notify leo to clean up resources + when(leo.cleanupResources(workspace.googleProjectId, workspace.workspaceIdAsUUID, ctx)).thenReturn(Future()) + // delete pets in project + val pet = UserIdInfo(UUID.randomUUID().toString, "pet-email", None) + when(sam.listAllResourceMemberIds(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future(Set(pet))) + val petKeyJson = "fake-json" + when(sam.getPetServiceAccountKeyForUser(workspace.googleProjectId, RawlsUserEmail(pet.userEmail))) + .thenReturn(Future(petKeyJson)) + val petUserInfo = mock[UserInfo] + when(gcs.getUserInfoUsingJson(petKeyJson)).thenReturn(Future(petUserInfo)) + when(sam.deleteUserPetServiceAccount(workspace.googleProjectId, ctx.copy(userInfo = petUserInfo))) + .thenReturn(Future()) + // delete google project + when(gcs.deleteGoogleProject(workspace.googleProjectId)).thenReturn(Future()) + when(sam.deleteResource(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future()) + // delete workspace and associated records + when(repo.deleteRawlsWorkspace(workspace)).thenReturn(Future()) + // delete workflow collection in sam + when(sam.deleteResource(SamResourceTypeNames.workflowCollection, workspace.workflowCollectionName.get, ctx)) + .thenReturn(Future()) + // delete workspace in sam + when(sam.deleteResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)).thenReturn(Future()) + val service = workspaceServiceConstructor( + samDAO = sam, + requesterPaysSetupService = requesterPaysService, + fastPassServiceConstructor = _ => fastPass, + leonardoService = leo, + workspaceRepository = repo, + gcsDAO = gcs, + submissionsRepository = submissionsRepository + )(ctx) + + val result = Await.result(service.deleteWorkspace(workspace.toWorkspaceName), Duration.Inf) + + result shouldBe WorkspaceDeletionResult.fromGcpBucketName(workspace.bucketName) + verify(sam).listAllResourceMemberIds(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx) + verify(sam).getPetServiceAccountKeyForUser(workspace.googleProjectId, RawlsUserEmail(pet.userEmail)) + verify(gcs).getUserInfoUsingJson(petKeyJson) + verify(sam).deleteUserPetServiceAccount(workspace.googleProjectId, ctx.copy(userInfo = petUserInfo)) + } + + it should "ignore 404s from Sam when retrieving pets" in { + val sam = mock[SamDAO] + val repo = mock[WorkspaceRepository] + val requesterPaysService = mock[RequesterPaysSetupService] + val submissionsRepository = mock[SubmissionsRepository] + val leo = mock[LeonardoService] + val fastPass = mock[FastPassService] + val gcs = mock[GoogleServicesDAO] + // mocked operations are defined in the order they are called by the service + // initial auth checks/workspace retrieval + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.delete, ctx)) + .thenReturn(Future(true)) + when(repo.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) + // delete requester pays records + when(requesterPaysService.deleteAllRecordsForWorkspace(workspace)).thenReturn(Future(1)) + // abort workflows + when(submissionsRepository.getActiveWorkflowsAndSetStatusToAborted(workspace)).thenReturn(Future(Seq())) + // delete fast pass grants + when(fastPass.removeFastPassGrantsForWorkspace(workspace)).thenReturn(Future()) + // notify leo to clean up resources + when(leo.cleanupResources(workspace.googleProjectId, workspace.workspaceIdAsUUID, ctx)).thenReturn(Future()) + // delete pets in project + val pet = UserIdInfo(UUID.randomUUID().toString, "pet-email", None) + when(sam.listAllResourceMemberIds(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future.failed(RawlsExceptionWithErrorReport(StatusCodes.NotFound, ""))) + // delete google project + when(gcs.deleteGoogleProject(workspace.googleProjectId)).thenReturn(Future()) + when(sam.deleteResource(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx)) + .thenReturn(Future()) + // delete workspace and associated records + when(repo.deleteRawlsWorkspace(workspace)).thenReturn(Future()) + // delete workflow collection in sam + when(sam.deleteResource(SamResourceTypeNames.workflowCollection, workspace.workflowCollectionName.get, ctx)) + .thenReturn(Future()) + // delete workspace in sam + when(sam.deleteResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)).thenReturn(Future()) + val service = workspaceServiceConstructor( + samDAO = sam, + requesterPaysSetupService = requesterPaysService, + fastPassServiceConstructor = _ => fastPass, + leonardoService = leo, + workspaceRepository = repo, + gcsDAO = gcs, + submissionsRepository = submissionsRepository + )(ctx) + + val result = Await.result(service.deleteWorkspace(workspace.toWorkspaceName), Duration.Inf) + + result shouldBe WorkspaceDeletionResult.fromGcpBucketName(workspace.bucketName) + verify(sam).listAllResourceMemberIds(SamResourceTypeNames.googleProject, workspace.googleProjectId.value, ctx) } behavior of "getAcl" @@ -1127,39 +1461,20 @@ class WorkspaceServiceUnitTests def mockSamForAclTests(): SamDAO = { val samDAO = mock[SamDAO](RETURNS_SMART_NULLS) - when(samDAO.getUserIdInfo(any(), any())).thenReturn( - Future.successful(SamDAO.User(UserIdInfo("fake_user_id", "user@example.com", Option("fake_google_subject_id")))) - ) - when(samDAO.getUserStatus(any())) - .thenReturn(Future.successful(Option(SamUserStatusResponse("fake_user_id", "user@example.com", true)))) + when(samDAO.getUserIdInfo(any(), any())) + .thenReturn(Future(SamDAO.User(UserIdInfo("fake_user_id", "user@example.com", Option("fake_google_subject_id"))))) + when(samDAO.getUserStatus(any())).thenReturn(Future(Option(enabledUser))) samDAO } - def mockWorkspaceRepositoryForAclTests(workspaceType: WorkspaceType, - workspaceId: UUID = UUID.randomUUID() - ): WorkspaceRepository = { + def mockWorkspaceRepositoryForAclTests(workspaceType: WorkspaceType): WorkspaceRepository = { val workspaceRepository = mock[WorkspaceRepository](RETURNS_SMART_NULLS) val googleProjectId = workspaceType match { case WorkspaceType.McWorkspace => GoogleProjectId("") case WorkspaceType.RawlsWorkspace => GoogleProjectId("fake-project-id") } - - when(workspaceRepository.getWorkspace(any[WorkspaceName](), any())).thenReturn( - Future.successful( - Option( - Workspace("fake_namespace", - "fake_name", - workspaceId.toString, - "fake_bucket", - None, - DateTime.now(), - DateTime.now(), - "creator@example.com", - Map.empty - ).copy(workspaceType = workspaceType, googleProjectId = googleProjectId) - ) - ) - ) + val workspace = this.workspace.copy(workspaceType = workspaceType, googleProjectId = googleProjectId) + when(workspaceRepository.getWorkspace(any[WorkspaceName](), any())).thenReturn(Future(Option(workspace))) workspaceRepository } @@ -1205,14 +1520,12 @@ class WorkspaceServiceUnitTests val writerEmail = "writer@example.com" val readerEmail = "reader@example.com" val samDAO = mockSamForAclTests() - when(samDAO.listPoliciesForResource(ArgumentMatchers.eq(SamResourceTypeNames.workspace), any(), any())).thenReturn( - Future.successful(samWorkspacePoliciesForAclTests(projectOwnerEmail, ownerEmail, writerEmail, readerEmail)) - ) + when(samDAO.listPoliciesForResource(SamResourceTypeNames.workspace, workspace.workspaceId, ctx)) + .thenReturn(Future(samWorkspacePoliciesForAclTests(projectOwnerEmail, ownerEmail, writerEmail, readerEmail))) val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.RawlsWorkspace) - val service = - workspaceServiceConstructor(workspaceRepository = workspaceRepository, samDAO = samDAO)(defaultRequestContext) + val service = workspaceServiceConstructor(workspaceRepository = workspaceRepository, samDAO = samDAO)(ctx) val result = Await.result(service.getACL(WorkspaceName("fake_namespace", "fake_name")), Duration.Inf) val expected = WorkspaceACL( @@ -1236,11 +1549,11 @@ class WorkspaceServiceUnitTests val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.McWorkspace) val samDAO = mockSamForAclTests() - val service = - workspaceServiceConstructor(workspaceRepository = workspaceRepository, - samDAO = samDAO, - workspaceManagerDAO = wsmDAO - )(defaultRequestContext) + val service = workspaceServiceConstructor( + workspaceRepository = workspaceRepository, + samDAO = samDAO, + workspaceManagerDAO = wsmDAO + )(ctx) val expected = WorkspaceACL( Map( @@ -1265,31 +1578,27 @@ class WorkspaceServiceUnitTests val readerEmail = "reader@example.com" val samDAO = mockSamForAclTests() - when(samDAO.listPoliciesForResource(ArgumentMatchers.eq(SamResourceTypeNames.workspace), any(), any())).thenReturn( - Future.successful(samWorkspacePoliciesForAclTests(projectOwnerEmail, ownerEmail, writerEmail, readerEmail)) - ) - when(samDAO.addUserToPolicy(any(), any(), any(), any(), any())).thenReturn(Future.successful()) - when(samDAO.removeUserFromPolicy(any(), any(), any(), any(), any())).thenReturn(Future.successful()) + when(samDAO.listPoliciesForResource(ArgumentMatchers.eq(SamResourceTypeNames.workspace), any(), any())) + .thenReturn(Future(samWorkspacePoliciesForAclTests(projectOwnerEmail, ownerEmail, writerEmail, readerEmail))) + when(samDAO.addUserToPolicy(any(), any(), any(), any(), any())).thenReturn(Future()) + when(samDAO.removeUserFromPolicy(any(), any(), any(), any(), any())).thenReturn(Future()) - val workspaceId = UUID.randomUUID() - val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.RawlsWorkspace, workspaceId) + val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.RawlsWorkspace) - val requesterPaysSetupService = mock[RequesterPaysSetupServiceImpl](RETURNS_SMART_NULLS) - when(requesterPaysSetupService.revokeUserFromWorkspace(any(), any())).thenReturn(Future.successful(Seq.empty)) + val requesterPaysSetupService = mock[RequesterPaysSetupService](RETURNS_SMART_NULLS) + when(requesterPaysSetupService.revokeUserFromWorkspace(any(), any())).thenReturn(Future(Seq.empty)) - val mockFastPassService = mock[FastPassServiceImpl] - when(mockFastPassService.syncFastPassesForUserInWorkspace(any[Workspace], any[String])) - .thenReturn(Future.successful()) + val mockFastPassService = mock[FastPassService] + when(mockFastPassService.syncFastPassesForUserInWorkspace(any[Workspace], any[String])).thenReturn(Future()) - val service = - workspaceServiceConstructor( - workspaceRepository = workspaceRepository, - samDAO = samDAO, - requesterPaysSetupService = requesterPaysSetupService, - fastPassServiceConstructor = _ => mockFastPassService - )( - defaultRequestContext - ) + val service = workspaceServiceConstructor( + workspaceRepository = workspaceRepository, + samDAO = samDAO, + requesterPaysSetupService = requesterPaysSetupService, + fastPassServiceConstructor = _ => mockFastPassService + )( + ctx + ) val aclUpdates = Set( WorkspaceACLUpdate(writerEmail, WorkspaceAccessLevels.NoAccess, Option(false), Option(false)), @@ -1298,23 +1607,25 @@ class WorkspaceServiceUnitTests Await.result(service.updateACL(WorkspaceName("fake_namespace", "fake_name"), aclUpdates, true), Duration.Inf) - verify(samDAO).addUserToPolicy(ArgumentMatchers.eq(SamResourceTypeNames.workspace), - any(), - ArgumentMatchers.eq(SamWorkspacePolicyNames.writer), - ArgumentMatchers.eq(readerEmail), - any() + verify(samDAO).addUserToPolicy(SamResourceTypeNames.workspace, + workspace.workspaceId, + SamWorkspacePolicyNames.writer, + readerEmail, + ctx ) - verify(samDAO).removeUserFromPolicy(ArgumentMatchers.eq(SamResourceTypeNames.workspace), - any(), - ArgumentMatchers.eq(SamWorkspacePolicyNames.reader), - ArgumentMatchers.eq(readerEmail), - any() + verify(samDAO).removeUserFromPolicy( + SamResourceTypeNames.workspace, + workspace.workspaceId, + SamWorkspacePolicyNames.reader, + readerEmail, + ctx ) - verify(samDAO).removeUserFromPolicy(ArgumentMatchers.eq(SamResourceTypeNames.workspace), - any(), - ArgumentMatchers.eq(SamWorkspacePolicyNames.writer), - ArgumentMatchers.eq(writerEmail), - any() + verify(samDAO).removeUserFromPolicy( + SamResourceTypeNames.workspace, + workspace.workspaceId, + SamWorkspacePolicyNames.writer, + writerEmail, + ctx ) } @@ -1322,15 +1633,14 @@ class WorkspaceServiceUnitTests val ownerEmail = "owner@example.com" val writerEmail = "writer@example.com" val readerEmail = "reader@example.com" - val workspaceId = UUID.randomUUID() val wsmDAO = mockWsmForAclTests(ownerEmail, writerEmail, readerEmail) - val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.McWorkspace, workspaceId) + val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.McWorkspace) val samDAO = mockSamForAclTests() val aclManagerDatasource = mock[SlickDataSource] when(aclManagerDatasource.inTransaction[Option[RawlsBillingProject]](any(), any())).thenReturn( - Future.successful( + Future( Option( RawlsBillingProject( RawlsBillingProjectName("fake_namespace"), @@ -1343,12 +1653,8 @@ class WorkspaceServiceUnitTests ) ) - val mockFastPassService = mock[FastPassServiceImpl] - when(mockFastPassService.syncFastPassesForUserInWorkspace(any[Workspace], any[String])) - .thenReturn( - Future.successful( - ) - ) + val mockFastPassService = mock[FastPassService] + when(mockFastPassService.syncFastPassesForUserInWorkspace(any[Workspace], any[String])).thenReturn(Future()) val service = workspaceServiceConstructor( workspaceRepository = workspaceRepository, @@ -1356,7 +1662,7 @@ class WorkspaceServiceUnitTests workspaceManagerDAO = wsmDAO, aclManagerDatasource = aclManagerDatasource, fastPassServiceConstructor = _ => mockFastPassService - )(defaultRequestContext) + )(ctx) val aclUpdates = Set( WorkspaceACLUpdate(writerEmail, WorkspaceAccessLevels.NoAccess, Option(false), Option(false)), @@ -1365,23 +1671,11 @@ class WorkspaceServiceUnitTests Await.result(service.updateACL(WorkspaceName("fake_namespace", "fake_name"), aclUpdates, true), Duration.Inf) - verify(samDAO, never).addUserToPolicy(any(), any(), any(), any(), any()) - verify(samDAO, never).removeUserFromPolicy(any(), any(), any(), any(), any()) - verify(wsmDAO).removeRole(ArgumentMatchers.eq(workspaceId), - ArgumentMatchers.eq(WorkbenchEmail(writerEmail)), - ArgumentMatchers.eq(IamRole.WRITER), - any() - ) - verify(wsmDAO).removeRole(ArgumentMatchers.eq(workspaceId), - ArgumentMatchers.eq(WorkbenchEmail(readerEmail)), - ArgumentMatchers.eq(IamRole.READER), - any() - ) - verify(wsmDAO).grantRole(ArgumentMatchers.eq(workspaceId), - ArgumentMatchers.eq(WorkbenchEmail(readerEmail)), - ArgumentMatchers.eq(IamRole.WRITER), - any() - ) + verify(samDAO, never).addUserToPolicy(any, any, any, any, any) + verify(samDAO, never).removeUserFromPolicy(any, any, any, any, any) + verify(wsmDAO).removeRole(workspace.workspaceIdAsUUID, WorkbenchEmail(writerEmail), IamRole.WRITER, ctx) + verify(wsmDAO).removeRole(workspace.workspaceIdAsUUID, WorkbenchEmail(readerEmail), IamRole.READER, ctx) + verify(wsmDAO).grantRole(workspace.workspaceIdAsUUID, WorkbenchEmail(readerEmail), IamRole.WRITER, ctx) } it should "not allow share writers for McWorkspaces" in { @@ -1391,18 +1685,16 @@ class WorkspaceServiceUnitTests val workspaceId = UUID.randomUUID() val wsmDAO = mockWsmForAclTests(ownerEmail, writerEmail, readerEmail) - val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.McWorkspace, workspaceId) + val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.McWorkspace) val samDAO = mockSamForAclTests() - val aclUpdates = Set( - WorkspaceACLUpdate(writerEmail, WorkspaceAccessLevels.Write, Option(true), Option(false)) - ) + val aclUpdates = Set(WorkspaceACLUpdate(writerEmail, WorkspaceAccessLevels.Write, Option(true), Option(false))) - val service = - workspaceServiceConstructor(workspaceRepository = workspaceRepository, - samDAO = samDAO, - workspaceManagerDAO = wsmDAO - )(defaultRequestContext) + val service = workspaceServiceConstructor( + workspaceRepository = workspaceRepository, + samDAO = samDAO, + workspaceManagerDAO = wsmDAO + )(ctx) val exception = intercept[InvalidWorkspaceAclUpdateException] { Await.result(service.updateACL(WorkspaceName("fake_namespace", "fake_name"), aclUpdates, true), Duration.Inf) } @@ -1417,18 +1709,18 @@ class WorkspaceServiceUnitTests val workspaceId = UUID.randomUUID() val wsmDAO = mockWsmForAclTests(ownerEmail, writerEmail, readerEmail) - val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.McWorkspace, workspaceId) + val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.McWorkspace) val samDAO = mockSamForAclTests() val aclUpdates = Set( WorkspaceACLUpdate(readerEmail, WorkspaceAccessLevels.Read, Option(true), Option(false)) ) - val service = - workspaceServiceConstructor(workspaceRepository = workspaceRepository, - samDAO = samDAO, - workspaceManagerDAO = wsmDAO - )(defaultRequestContext) + val service = workspaceServiceConstructor( + workspaceRepository = workspaceRepository, + samDAO = samDAO, + workspaceManagerDAO = wsmDAO + )(ctx) val exception = intercept[InvalidWorkspaceAclUpdateException] { Await.result(service.updateACL(WorkspaceName("fake_namespace", "fake_name"), aclUpdates, true), Duration.Inf) } @@ -1440,21 +1732,18 @@ class WorkspaceServiceUnitTests val ownerEmail = "owner@example.com" val writerEmail = "writer@example.com" val readerEmail = "reader@example.com" - val workspaceId = UUID.randomUUID() val wsmDAO = mockWsmForAclTests(ownerEmail, writerEmail, readerEmail) - val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.McWorkspace, workspaceId) + val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.McWorkspace) val samDAO = mockSamForAclTests() - val aclUpdates = Set( - WorkspaceACLUpdate(writerEmail, WorkspaceAccessLevels.Write, Option(false), Option(true)) - ) + val aclUpdates = Set(WorkspaceACLUpdate(writerEmail, WorkspaceAccessLevels.Write, Option(false), Option(true))) - val service = - workspaceServiceConstructor(workspaceRepository = workspaceRepository, - samDAO = samDAO, - workspaceManagerDAO = wsmDAO - )(defaultRequestContext) + val service = workspaceServiceConstructor( + workspaceRepository = workspaceRepository, + samDAO = samDAO, + workspaceManagerDAO = wsmDAO + )(ctx) val exception = intercept[InvalidWorkspaceAclUpdateException] { Await.result(service.updateACL(WorkspaceName("fake_namespace", "fake_name"), aclUpdates, true), Duration.Inf) } @@ -1469,30 +1758,23 @@ class WorkspaceServiceUnitTests val readerEmail = "reader@example.com" val samDAO = mockSamForAclTests() - when(samDAO.listPoliciesForResource(ArgumentMatchers.eq(SamResourceTypeNames.workspace), any(), any())).thenReturn( - Future.successful(samWorkspacePoliciesForAclTests(projectOwnerEmail, ownerEmail, writerEmail, readerEmail)) - ) - when(samDAO.addUserToPolicy(any(), any(), any(), any(), any())).thenReturn(Future.successful()) + when(samDAO.listPoliciesForResource(ArgumentMatchers.eq(SamResourceTypeNames.workspace), any(), any())) + .thenReturn(Future(samWorkspacePoliciesForAclTests(projectOwnerEmail, ownerEmail, writerEmail, readerEmail))) + when(samDAO.addUserToPolicy(any(), any(), any(), any(), any())).thenReturn(Future()) - val workspaceId = UUID.randomUUID() - val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.RawlsWorkspace, workspaceId) - val mockFastPassService = mock[FastPassServiceImpl] - when(mockFastPassService.syncFastPassesForUserInWorkspace(any[Workspace], any[String])) - .thenReturn(Future.successful()) + val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.RawlsWorkspace) + val mockFastPassService = mock[FastPassService] + when(mockFastPassService.syncFastPassesForUserInWorkspace(any[Workspace], any[String])).thenReturn(Future()) val service = workspaceServiceConstructor(workspaceRepository = workspaceRepository, samDAO = samDAO, fastPassServiceConstructor = _ => mockFastPassService - )(defaultRequestContext) + )(ctx) - val writerAclUpdate = Set( - WorkspaceACLUpdate(writerEmail, WorkspaceAccessLevels.Write, Option(false), Option(true)) - ) + val writerAclUpdate = Set(WorkspaceACLUpdate(writerEmail, WorkspaceAccessLevels.Write, Option(false), Option(true))) Await.result(service.updateACL(WorkspaceName("fake_namespace", "fake_name"), writerAclUpdate, true), Duration.Inf) - val readerAclUpdate = Set( - WorkspaceACLUpdate(readerEmail, WorkspaceAccessLevels.Read, Option(false), Option(true)) - ) + val readerAclUpdate = Set(WorkspaceACLUpdate(readerEmail, WorkspaceAccessLevels.Read, Option(false), Option(true))) val thrown = intercept[RawlsExceptionWithErrorReport] { Await.result(service.updateACL(WorkspaceName("fake_namespace", "fake_name"), readerAclUpdate, true), Duration.Inf) @@ -1508,20 +1790,18 @@ class WorkspaceServiceUnitTests val samDAO = mockSamForAclTests() when(samDAO.listPoliciesForResource(ArgumentMatchers.eq(SamResourceTypeNames.workspace), any(), any())).thenReturn( - Future.successful(samWorkspacePoliciesForAclTests(projectOwnerEmail, ownerEmail, writerEmail, readerEmail)) + Future(samWorkspacePoliciesForAclTests(projectOwnerEmail, ownerEmail, writerEmail, readerEmail)) ) - when(samDAO.addUserToPolicy(any(), any(), any(), any(), any())).thenReturn(Future.successful()) + when(samDAO.addUserToPolicy(any(), any(), any(), any(), any())).thenReturn(Future()) - val workspaceId = UUID.randomUUID() - val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.RawlsWorkspace, workspaceId) - val mockFastPassService = mock[FastPassServiceImpl] - when(mockFastPassService.syncFastPassesForUserInWorkspace(any[Workspace], any[String])) - .thenReturn(Future.successful()) + val workspaceRepository = mockWorkspaceRepositoryForAclTests(WorkspaceType.RawlsWorkspace) + val mockFastPassService = mock[FastPassService] + when(mockFastPassService.syncFastPassesForUserInWorkspace(any[Workspace], any[String])).thenReturn(Future()) val service = workspaceServiceConstructor(workspaceRepository = workspaceRepository, samDAO = samDAO, fastPassServiceConstructor = _ => mockFastPassService - )(defaultRequestContext) + )(ctx) val aclUpdate = Set( WorkspaceACLUpdate(readerEmail, WorkspaceAccessLevels.Read, Option(true), Option(false)), @@ -1543,35 +1823,19 @@ class WorkspaceServiceUnitTests it should "get the bucket usage for a gcp workspace" in { val workspace = this.workspace.copy(googleProjectId = GoogleProjectId("project-id"), bucketName = "test-bucket") val repository = mock[WorkspaceRepository] - when(repository.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future.successful(Some(workspace))) + when(repository.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) val sam = mock[SamDAO] - when(sam.getUserStatus(defaultRequestContext)).thenReturn( - Future.successful( - Some( - SamUserStatusResponse( - defaultRequestContext.userInfo.userSubjectId.value, - defaultRequestContext.userInfo.userEmail.value, - true - ) - ) - ) - ) - when( - sam.userHasAction(SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspaceActions.read, - defaultRequestContext - ) - ).thenReturn(Future(true)) + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.read, ctx)) + .thenReturn(Future(true)) val bucketUsage = mock[BucketUsageResponse] val gcs = mock[GoogleServicesDAO](RETURNS_SMART_NULLS) - when(gcs.getBucketUsage(workspace.googleProjectId, workspace.bucketName, None)) - .thenReturn(Future.successful(bucketUsage)) + when(gcs.getBucketUsage(workspace.googleProjectId, workspace.bucketName, None)).thenReturn(Future(bucketUsage)) val service = workspaceServiceConstructor( samDAO = sam, workspaceRepository = repository, gcsDAO = gcs - )(defaultRequestContext) + )(ctx) Await.result(service.getBucketUsage(workspace.toWorkspaceName), Duration.Inf) shouldBe bucketUsage verify(gcs).getBucketUsage(workspace.googleProjectId, workspace.bucketName, None) @@ -1580,35 +1844,15 @@ class WorkspaceServiceUnitTests it should "work on a locked workspace" in { val workspace = this.workspace.copy(isLocked = true, googleProjectId = GoogleProjectId("project-id")) val repository = mock[WorkspaceRepository] - when(repository.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future.successful(Some(workspace))) + when(repository.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) val sam = mock[SamDAO] - when(sam.getUserStatus(defaultRequestContext)).thenReturn( - Future.successful( - Some( - SamUserStatusResponse( - defaultRequestContext.userInfo.userSubjectId.value, - defaultRequestContext.userInfo.userEmail.value, - true - ) - ) - ) - ) - when( - sam.userHasAction(SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspaceActions.read, - defaultRequestContext - ) - ).thenReturn(Future(true)) + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.read, ctx)) + .thenReturn(Future(true)) val bucketUsage = mock[BucketUsageResponse] val gcs = mock[GoogleServicesDAO](RETURNS_SMART_NULLS) - when(gcs.getBucketUsage(workspace.googleProjectId, workspace.bucketName, None)) - .thenReturn(Future.successful(bucketUsage)) - val service = workspaceServiceConstructor( - samDAO = sam, - workspaceRepository = repository, - gcsDAO = gcs - )(defaultRequestContext) + when(gcs.getBucketUsage(workspace.googleProjectId, workspace.bucketName, None)).thenReturn(Future(bucketUsage)) + val service = workspaceServiceConstructor(samDAO = sam, workspaceRepository = repository, gcsDAO = gcs)(ctx) Await.result(service.getBucketUsage(workspace.toWorkspaceName), Duration.Inf) shouldBe bucketUsage verify(gcs).getBucketUsage(workspace.googleProjectId, workspace.bucketName, None) @@ -1616,26 +1860,11 @@ class WorkspaceServiceUnitTests it should "map non-standard codes from a GoogleJsonResponseException to a rawls exception" in { val repository = mock[WorkspaceRepository] - when(repository.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future.successful(Some(workspace))) + when(repository.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) val sam = mock[SamDAO] - when(sam.getUserStatus(defaultRequestContext)).thenReturn( - Future.successful( - Some( - SamUserStatusResponse( - defaultRequestContext.userInfo.userSubjectId.value, - defaultRequestContext.userInfo.userEmail.value, - true - ) - ) - ) - ) - when( - sam.userHasAction(SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspaceActions.read, - defaultRequestContext - ) - ).thenReturn(Future(true)) + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.read, ctx)) + .thenReturn(Future(true)) val bucketUsage = mock[BucketUsageResponse] val gcs = mock[GoogleServicesDAO](RETURNS_SMART_NULLS) doAnswer { _ => @@ -1644,12 +1873,7 @@ class WorkspaceServiceUnitTests new GoogleJsonError() ) }.when(gcs).getBucketUsage(workspace.googleProjectId, workspace.bucketName, None) - - val service = workspaceServiceConstructor( - samDAO = sam, - workspaceRepository = repository, - gcsDAO = gcs - )(defaultRequestContext) + val service = workspaceServiceConstructor(samDAO = sam, workspaceRepository = repository, gcsDAO = gcs)(ctx) val error = intercept[RawlsExceptionWithErrorReport] { Await.result(service.getBucketUsage(workspace.toWorkspaceName), Duration.Inf) shouldBe bucketUsage @@ -1663,35 +1887,15 @@ class WorkspaceServiceUnitTests behavior of "getBucketOptions" it should "get the bucket options for a gcp workspace" in { val repository = mock[WorkspaceRepository] - when(repository.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future.successful(Some(workspace))) + when(repository.getWorkspace(workspace.toWorkspaceName, None)).thenReturn(Future(Some(workspace))) val sam = mock[SamDAO] - when(sam.getUserStatus(defaultRequestContext)).thenReturn( - Future.successful( - Some( - SamUserStatusResponse( - defaultRequestContext.userInfo.userSubjectId.value, - defaultRequestContext.userInfo.userEmail.value, - true - ) - ) - ) - ) - when( - sam.userHasAction(SamResourceTypeNames.workspace, - workspace.workspaceId, - SamWorkspaceActions.read, - defaultRequestContext - ) - ).thenReturn(Future(true)) + when(sam.getUserStatus(ctx)).thenReturn(Future(Some(enabledUser))) + when(sam.userHasAction(SamResourceTypeNames.workspace, workspace.workspaceId, SamWorkspaceActions.read, ctx)) + .thenReturn(Future(true)) val bucketDetails = mock[WorkspaceBucketOptions] val gcs = mock[GoogleServicesDAO](RETURNS_SMART_NULLS) - when(gcs.getBucketDetails(workspace.bucketName, workspace.googleProjectId)) - .thenReturn(Future.successful(bucketDetails)) - val service = workspaceServiceConstructor( - samDAO = sam, - workspaceRepository = repository, - gcsDAO = gcs - )(defaultRequestContext) + when(gcs.getBucketDetails(workspace.bucketName, workspace.googleProjectId)).thenReturn(Future(bucketDetails)) + val service = workspaceServiceConstructor(samDAO = sam, workspaceRepository = repository, gcsDAO = gcs)(ctx) Await.result(service.getBucketOptions(workspace.toWorkspaceName), Duration.Inf) shouldBe bucketDetails verify(gcs).getBucketDetails(workspace.bucketName, workspace.googleProjectId) diff --git a/model/src/main/scala/org/broadinstitute/dsde/rawls/RawlsException.scala b/model/src/main/scala/org/broadinstitute/dsde/rawls/RawlsException.scala index 1bdd354ca3..171792d85c 100644 --- a/model/src/main/scala/org/broadinstitute/dsde/rawls/RawlsException.scala +++ b/model/src/main/scala/org/broadinstitute/dsde/rawls/RawlsException.scala @@ -21,6 +21,9 @@ object RawlsExceptionWithErrorReport { def apply(status: StatusCode, t: Throwable)(implicit source: ErrorReportSource): RawlsExceptionWithErrorReport = RawlsExceptionWithErrorReport(ErrorReport(status, t)) + + def apply(status: StatusCode, message: String, t: Throwable)(implicit source: ErrorReportSource): RawlsExceptionWithErrorReport = + RawlsExceptionWithErrorReport(ErrorReport(status, message, t)) } /**