Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CORE-182] Convert remaining runtime operations to new Sam permissions model #4820

Merged
merged 12 commits into from
Jan 10, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@
final case object Delete extends WorkspaceAction {
val asString = "delete"
}
final case object Compute extends WorkspaceAction {
val asString = "compute"

Check warning on line 243 in core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/samModels.scala

View check run for this annotation

Codecov / codecov/patch

core/src/main/scala/org/broadinstitute/dsde/workbench/leonardo/samModels.scala#L243

Added line #L243 was not covered by tests
}

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
Loading