Skip to content

Commit

Permalink
[AN-385] new sam disk permissions (#4828)
Browse files Browse the repository at this point in the history
  • Loading branch information
LizBaldo authored Feb 19, 2025
1 parent 7e8b90d commit 431b9c3
Show file tree
Hide file tree
Showing 22 changed files with 594 additions and 562 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -458,24 +458,19 @@ object LeonardoApiClient {
} yield r

def listDisk(
googleProject: GoogleProject,
includeDeleted: Boolean = false
googleProject: GoogleProject
)(implicit client: Client[IO], authorization: IO[Authorization]): IO[List[ListPersistentDiskResponse]] = {
val uriWithoutQueryParam = rootUri
.withPath(Uri.Path.unsafeFromString(s"/api/google/v1/disks/${googleProject.value}"))

val uri =
if (includeDeleted) uriWithoutQueryParam.withQueryParam("includeDeleted", "true")
else uriWithoutQueryParam

for {
traceIdHeader <- genTraceIdHeader()
authHeader <- authorization
r <- client.expectOr[List[ListPersistentDiskResponse]](
Request[IO](
method = Method.GET,
headers = Headers(authHeader, traceIdHeader),
uri = uri
uri = uriWithoutQueryParam
)
)(onError(s"Failed to list disks in project ${googleProject.value}"))
} yield r
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,9 @@ class RuntimeCreationDiskSpec extends BillingProjectFixtureSpec with ParallelTes
_ <- IO(disk.status shouldBe DiskStatus.Ready)
_ <- IO(disk.size shouldBe diskSize)
_ <- LeonardoApiClient.deleteDiskWithWait(googleProject, diskName)
listofDisks <- LeonardoApiClient.listDisk(googleProject, true)
} yield listofDisks.collect { case resp if resp.name == diskName => resp.status } shouldBe List(
DiskStatus.Deleted
) // assume we won't have multiple disks with same name in the same project in tests
listofDisks <- LeonardoApiClient.listDisk(googleProject)
} yield listofDisks.collect { case resp if resp.name == diskName => resp.status } shouldBe List.empty
// assume we won't have multiple disks with same name in the same project in tests
}
res.unsafeRunSync()(cats.effect.unsafe.IORuntime.global)
}
Expand Down
21 changes: 0 additions & 21 deletions http/src/main/resources/swagger/api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -988,13 +988,6 @@ paths:
required: false
schema:
type: string
- in: query
name: includeDeleted
description: Optional filter that includes any persistent disks with a Deleted status.
required: false
schema:
type: boolean
default: false
- in: query
name: includeLabels
description: >
Expand Down Expand Up @@ -1058,13 +1051,6 @@ paths:
required: false
schema:
type: string
- in: query
name: includeDeleted
description: Optional filter that includes any persistent disks with a Deleted status.
required: false
schema:
type: boolean
default: false
- in: query
name: includeLabels
description: >
Expand Down Expand Up @@ -1357,13 +1343,6 @@ paths:
required: false
schema:
type: string
- in: query
name: includeDeleted
description: Optional filter that includes any apps with a Deleted status.
required: false
schema:
type: boolean
default: false
- in: query
name: includeLabels
description: >
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import akka.http.scaladsl.model.headers.OAuth2BearerToken
import cats.effect.Async
import cats.implicits.{catsSyntaxApplicativeError, toFlatMapOps}
import cats.mtl.Ask
import org.broadinstitute.dsde.workbench.google2.DiskName
import org.broadinstitute.dsde.workbench.leonardo.http.service.{DiskNotFoundByIdException, DiskNotFoundException}
import org.broadinstitute.dsde.workbench.leonardo.model.{
ForbiddenError,
LeoException,
Expand All @@ -14,66 +16,110 @@ import org.broadinstitute.dsde.workbench.leonardo.model.{
import org.broadinstitute.dsde.workbench.leonardo.{
AppContext,
CloudContext,
DiskId,
PersistentDiskAction,
RuntimeAction,
RuntimeName,
SamResourceAction,
SamResourceId,
WorkspaceId
}
import org.broadinstitute.dsde.workbench.model.{UserInfo, WorkbenchEmail}
import org.broadinstitute.dsde.workbench.model.{TraceId, UserInfo, WorkbenchEmail}

trait SamUtils[F[_]] {
val samService: SamService[F]

def checkRuntimeAction(userInfo: UserInfo,
cloudContext: CloudContext,
runtimeName: RuntimeName,
samResourceId: SamResourceId,
action: RuntimeAction,
userEmail: Option[WorkbenchEmail] = None
object SamUtils {
def checkRuntimeAction[F[_]](samService: SamService[F],
userInfo: UserInfo,
cloudContext: CloudContext,
runtimeName: RuntimeName,
samResourceId: SamResourceId,
action: RuntimeAction,
userEmail: Option[WorkbenchEmail] = None
)(implicit F: Async[F], as: Ask[F, AppContext]): F[Unit] =
checkRuntimeActionInternal(
checkActionInternal(
samService,
userInfo.accessToken,
userEmail.getOrElse(userInfo.userEmail),
samResourceId,
action,
RuntimeAction.GetRuntimeStatus,
RuntimeNotFoundException(cloudContext, runtimeName, "Not found in database")
)

def checkRuntimeAction(userInfo: UserInfo,
workspaceId: WorkspaceId,
runtimeName: RuntimeName,
samResourceId: SamResourceId,
action: RuntimeAction
def checkRuntimeAction[F[_]](samService: SamService[F],
userInfo: UserInfo,
workspaceId: WorkspaceId,
runtimeName: RuntimeName,
samResourceId: SamResourceId,
action: RuntimeAction
)(implicit F: Async[F], as: Ask[F, AppContext]): F[Unit] =
checkRuntimeActionInternal(
checkActionInternal(
samService,
userInfo.accessToken,
userInfo.userEmail,
samResourceId,
action,
RuntimeAction.GetRuntimeStatus,
RuntimeNotFoundByWorkspaceIdException(workspaceId, runtimeName, "Not found in database")
)

private def checkRuntimeActionInternal(userToken: OAuth2BearerToken,
userEmail: WorkbenchEmail,
samResourceId: SamResourceId,
action: RuntimeAction,
notFoundException: LeoException
def checkDiskAction[F[_]](samService: SamService[F],
userInfo: UserInfo,
cloudContext: CloudContext,
diskName: DiskName,
samResourceId: SamResourceId,
action: SamResourceAction,
traceId: TraceId
)(implicit F: Async[F], as: Ask[F, AppContext]): F[Unit] =
checkActionInternal(
samService,
userInfo.accessToken,
userInfo.userEmail,
samResourceId,
action,
PersistentDiskAction.ReadPersistentDisk,
DiskNotFoundException(cloudContext, diskName, traceId)
)

def checkDiskAction[F[_]](samService: SamService[F],
userInfo: UserInfo,
diskId: DiskId,
samResourceId: SamResourceId,
action: SamResourceAction,
traceId: TraceId
)(implicit F: Async[F], as: Ask[F, AppContext]): F[Unit] =
checkActionInternal(
samService,
userInfo.accessToken,
userInfo.userEmail,
samResourceId,
action,
PersistentDiskAction.ReadPersistentDisk,
DiskNotFoundByIdException(diskId, traceId)
)

private def checkActionInternal[F[_]](samService: SamService[F],
userToken: OAuth2BearerToken,
userEmail: WorkbenchEmail,
samResourceId: SamResourceId,
actionToCheck: SamResourceAction,
resourceReadAction: SamResourceAction,
notFoundException: LeoException
)(implicit F: Async[F], as: Ask[F, AppContext]): F[Unit] =
samService
.checkAuthorized(userToken.token, samResourceId, action)
.checkAuthorized(userToken.token, samResourceId, actionToCheck)
.handleErrorWith {
// If we've already checked read access and the user doesn't have it, pretend the runtime doesn't exist to avoid leaking its existence
case e: SamException if e.statusCode == StatusCodes.Forbidden && action == RuntimeAction.GetRuntimeStatus =>
// If we've already checked read access and the user doesn't have it, pretend the resource doesn't exist to avoid leaking its existence
case e: SamException if e.statusCode == StatusCodes.Forbidden && actionToCheck == resourceReadAction =>
F.raiseError(notFoundException)
// Check if the user can read the runtime to determine which error to raise
// Check if the user can read the resource to determine which error to raise
case e: SamException if e.statusCode == StatusCodes.Forbidden =>
samService
.checkAuthorized(userToken.token, samResourceId, RuntimeAction.GetRuntimeStatus)
.checkAuthorized(userToken.token, samResourceId, resourceReadAction)
.attempt
.flatMap {
// The user can read the runtime, but they don't have the required action. Raise the original Forbidden action from Sam
// The user can read the resource, but they don't have the required action. Raise the original Forbidden action from Sam
case Right(_) => F.raiseError(ForbiddenError(userEmail))
// The user can't read the runtime, pretend it doesn't exist to avoid leaking its existence
// The user can't read the resource, pretend it doesn't exist to avoid leaking its existence
case Left(_) => F.raiseError(notFoundException)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,52 @@ import org.broadinstitute.dsde.workbench.leonardo.db.persistentDiskQuery.unmarsh
import org.broadinstitute.dsde.workbench.leonardo.http.{GetPersistentDiskResponse, GetPersistentDiskV2Response}
import org.broadinstitute.dsde.workbench.leonardo.http.service.{DiskNotFoundByIdException, DiskNotFoundException}
import org.broadinstitute.dsde.workbench.model.{TraceId, WorkbenchEmail}
import org.broadinstitute.dsde.workbench.leonardo.SamResourceId.PersistentDiskSamResourceId

import scala.concurrent.ExecutionContext

object DiskServiceDbQueries {

def listDisks(labelMap: LabelMap,
includeDeleted: Boolean,
creatorOnly: Option[WorkbenchEmail],
cloudContextOpt: Option[CloudContext] = None,
workspaceOpt: Option[WorkspaceId] = None
def listDisksBySamIds(samDiskResourceIds: List[PersistentDiskSamResourceId],
labelMap: LabelMap,
creatorOnly: Option[WorkbenchEmail],
cloudContextOpt: Option[CloudContext] = None,
workspaceOpt: Option[WorkspaceId] = None
)(implicit
ec: ExecutionContext
): DBIO[List[PersistentDisk]] = {
val listDiskQuery = persistentDiskQuery.tableQuery.filter(_.samResourceId inSetBind samDiskResourceIds)

filterListDisks(listDiskQuery, labelMap, creatorOnly, cloudContextOpt, workspaceOpt)
}
def listDisks(
labelMap: LabelMap,
creatorOnly: Option[WorkbenchEmail],
cloudContextOpt: Option[CloudContext] = None,
workspaceOpt: Option[WorkspaceId] = None
)(implicit
ec: ExecutionContext
): DBIO[List[PersistentDisk]] =
filterListDisks(persistentDiskQuery.tableQuery, labelMap, creatorOnly, cloudContextOpt, workspaceOpt)

private def filterListDisks(
baseQuery: Query[PersistentDiskTable, PersistentDiskRecord, Seq],
labelMap: LabelMap,
creatorOnly: Option[WorkbenchEmail],
cloudContextOpt: Option[CloudContext] = None,
workspaceOpt: Option[WorkspaceId] = None
)(implicit
ec: ExecutionContext
): DBIO[List[PersistentDisk]] = {

// filtered by creator first as it may have great impact
val diskQueryFilteredByCreator = creatorOnly match {
case Some(email) => persistentDiskQuery.tableQuery.filter(_.creator === email)
case None => persistentDiskQuery.tableQuery
case Some(email) => baseQuery.filter(_.creator === email)
case None => baseQuery
}

val diskQueryFilteredByDeletion =
if (includeDeleted) diskQueryFilteredByCreator
else diskQueryFilteredByCreator.filterNot(_.status === (DiskStatus.Deleted: DiskStatus))
diskQueryFilteredByCreator.filterNot(_.status === (DiskStatus.Deleted: DiskStatus))

val diskQueryFilteredByProject =
cloudContextOpt.fold(diskQueryFilteredByDeletion)(p =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu
): Resource[IO, ServicesDependencies] = {
val statusService = new StatusService(baselineDependencies.samDAO, dbReference)
val diskV2Service = new DiskV2ServiceInterp[IO](
baselineDependencies.authProvider,
baselineDependencies.publisherQueue,
baselineDependencies.wsmClientProvider
baselineDependencies.wsmClientProvider,
baselineDependencies.samService
)

val azureService = new RuntimeV2ServiceInterp[IO](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ class AzureDependenciesBuilder extends CloudDependenciesBuilder {
// Needed for v1 APIs
val diskService = new DiskServiceInterp[IO](
ConfigReader.appConfig.persistentDisk,
baselineDependencies.authProvider,
baselineDependencies.publisherQueue,
None,
None,
Expand All @@ -103,7 +102,6 @@ class AzureDependenciesBuilder extends CloudDependenciesBuilder {
val runtimeService = RuntimeService(
baselineDependencies.runtimeServicesConfig,
ConfigReader.appConfig.persistentDisk,
baselineDependencies.authProvider,
baselineDependencies.dockerDAO,
None,
None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,6 @@ class GcpDependencyBuilder extends CloudDependenciesBuilder {

val diskService = new DiskServiceInterp[IO](
ConfigReader.appConfig.persistentDisk,
baselineDependencies.authProvider,
baselineDependencies.publisherQueue,
Some(gcpDependencies.googleDiskService),
Some(gcpDependencies.googleProjectDAO),
Expand All @@ -284,7 +283,6 @@ class GcpDependencyBuilder extends CloudDependenciesBuilder {
val runtimeService = RuntimeService(
baselineDependencies.runtimeServicesConfig,
ConfigReader.appConfig.persistentDisk,
baselineDependencies.authProvider,
baselineDependencies.dockerDAO,
Some(gcpDependencies.googleStorageService),
Some(gcpDependencies.googleComputeService),
Expand Down
Loading

0 comments on commit 431b9c3

Please sign in to comment.