Skip to content

Commit

Permalink
[CORE-182] Convert remaining runtime operations to new Sam permission…
Browse files Browse the repository at this point in the history
…s model (#4820)
  • Loading branch information
marctalbott authored Jan 10, 2025
1 parent 59aaba2 commit dce08ef
Show file tree
Hide file tree
Showing 12 changed files with 420 additions and 863 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ object WorkspaceAction {
final case object Delete extends WorkspaceAction {
val asString = "delete"
}
final case object Compute extends WorkspaceAction {
val asString = "compute"
}

val allActions = sealerate.values[WorkspaceAction]
val stringToAction: Map[String, WorkspaceAction] =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.broadinstitute.dsde.workbench.leonardo.dao.sam

import akka.http.scaladsl.model.StatusCodes
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.leonardo.model.{
ForbiddenError,
LeoException,
RuntimeNotFoundByWorkspaceIdException,
RuntimeNotFoundException
}
import org.broadinstitute.dsde.workbench.leonardo.{
AppContext,
CloudContext,
RuntimeAction,
RuntimeName,
SamResourceId,
WorkspaceId
}
import org.broadinstitute.dsde.workbench.model.{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
)(implicit F: Async[F], as: Ask[F, AppContext]): F[Unit] =
checkRuntimeActionInternal(
userInfo.accessToken,
userEmail.getOrElse(userInfo.userEmail),
samResourceId,
action,
RuntimeNotFoundException(cloudContext, runtimeName, "Not found in database")
)

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

private def checkRuntimeActionInternal(userToken: OAuth2BearerToken,
userEmail: WorkbenchEmail,
samResourceId: SamResourceId,
action: RuntimeAction,
notFoundException: LeoException
)(implicit F: Async[F], as: Ask[F, AppContext]): F[Unit] =
samService
.checkAuthorized(userToken.token, samResourceId, action)
.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 =>
F.raiseError(notFoundException)
// Check if the user can read the runtime to determine which error to raise
case e: SamException if e.statusCode == StatusCodes.Forbidden =>
samService
.checkAuthorized(userToken.token, samResourceId, RuntimeAction.GetRuntimeStatus)
.attempt
.flatMap {
// The user can read the runtime, 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
case Left(_) => F.raiseError(notFoundException)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ class AppDependenciesBuilder(baselineDependenciesBuilder: BaselineDependenciesBu

val azureService = new RuntimeV2ServiceInterp[IO](
baselineDependencies.runtimeServicesConfig,
baselineDependencies.authProvider,
baselineDependencies.publisherQueue,
baselineDependencies.dateAccessedUpdaterQueue,
baselineDependencies.wsmClientProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ class GcpDependencyBuilder extends CloudDependenciesBuilder {
baselineDependencies.proxyResolver,
baselineDependencies.samDAO,
baselineDependencies.googleTokenCache,
baselineDependencies.samResourceCache
baselineDependencies.samResourceCache,
baselineDependencies.samService
)

val diskService = new DiskServiceInterp[IO](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.broadinstitute.dsde.workbench.leonardo.SamResourceId._
import org.broadinstitute.dsde.workbench.leonardo.config.ProxyConfig
import org.broadinstitute.dsde.workbench.leonardo.dao.HostStatus._
import org.broadinstitute.dsde.workbench.leonardo.dao.google.GoogleOAuth2Service
import org.broadinstitute.dsde.workbench.leonardo.dao.sam.{SamService, SamUtils}
import org.broadinstitute.dsde.workbench.leonardo.dao.{HostStatus, JupyterDAO, Proxy, SamDAO, TerminalName}
import org.broadinstitute.dsde.workbench.leonardo.db.{appQuery, clusterQuery, DbReference, KubernetesServiceDbQueries}
import org.broadinstitute.dsde.workbench.leonardo.dns.{KubernetesDnsCache, ProxyResolver, RuntimeDnsCache}
Expand Down Expand Up @@ -91,14 +92,16 @@ class ProxyService(
proxyResolver: ProxyResolver[IO],
samDAO: SamDAO[IO],
googleTokenCache: Cache[IO, String, (UserInfo, Instant)],
samResourceCache: Cache[IO, SamResourceCacheKey, (Option[String], Option[AppAccessScope])]
samResourceCache: Cache[IO, SamResourceCacheKey, (Option[String], Option[AppAccessScope])],
val samService: SamService[IO]
)(implicit
val system: ActorSystem,
executionContext: ExecutionContext,
dbRef: DbReference[IO],
loggerIO: StructuredLogger[IO],
metrics: OpenTelemetryMetrics[IO]
) extends LazyLogging {
) extends LazyLogging
with SamUtils[IO] {
val httpsConnectionContext = ConnectionContext.httpsClient(sslContext)
val clientConnectionSettings =
ClientConnectionSettings(system).withTransport(ClientTransport.withCustomResolver(proxyResolver.resolveAkka))
Expand Down Expand Up @@ -267,38 +270,9 @@ class ProxyService(
for {
ctx <- ev.ask[AppContext]

hasWorkspacePermission <- workspaceId match {
case Some(wid) =>
authProvider
.isUserWorkspaceReader(
WorkspaceResourceSamResourceId(wid),
userInfo
)
case None => IO.pure(true)
}

_ <- IO.raiseUnless(hasWorkspacePermission)(ForbiddenError(userInfo.userEmail))

samResource <- getCachedRuntimeSamResource(RuntimeCacheKey(cloudContext, runtimeName))
// Note both these Sam actions are cached so it should be okay to call hasPermission twice
hasViewPermission <- authProvider.hasPermission[RuntimeSamResourceId, RuntimeAction](
samResource,
RuntimeAction.GetRuntimeStatus,
userInfo
)
_ <-
if (!hasViewPermission) {
IO.raiseError(RuntimeNotFoundException(cloudContext, runtimeName, ctx.traceId.asString))
} else IO.unit
hasConnectPermission <- authProvider.hasPermission[RuntimeSamResourceId, RuntimeAction](
samResource,
RuntimeAction.ConnectToRuntime,
userInfo
)
_ <-
if (!hasConnectPermission) {
IO.raiseError(ForbiddenError(userInfo.userEmail))
} else IO.unit
_ <- checkRuntimeAction(userInfo, cloudContext, runtimeName, samResource, RuntimeAction.ConnectToRuntime)

hostStatus <- getRuntimeTargetHost(cloudContext, runtimeName)
_ <- hostStatus match {
case HostReady(_, _, _) =>
Expand Down
Loading

0 comments on commit dce08ef

Please sign in to comment.