Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.typesafe.scalalogging.LazyLogging
import jakarta.ws.rs.client.{Client, ClientBuilder, Entity}
import jakarta.ws.rs.core._
import jakarta.ws.rs.{Consumes, GET, POST, Path, Produces}
import jakarta.ws.rs.{Consumes, DELETE, GET, POST, Path, Produces}
import org.apache.texera.auth.JwtParser.parseToken
import org.apache.texera.auth.SessionUser
import org.apache.texera.auth.util.{ComputingUnitAccess, HeaderField}
Expand All @@ -43,6 +43,11 @@ object AccessControlResource extends LazyLogging {
private val wsapiWorkflowWebsocket: Regex = """.*/wsapi/workflow-websocket.*""".r
private val apiExecutionsStats: Regex = """.*/api/executions/[0-9]+/stats/[0-9]+.*""".r
private val apiExecutionsResultExport: Regex = """.*/api/executions/result/export.*""".r
private val pveRoute: Regex = """^/?(?:auth/)?(?:api/|wsapi/)?pve(?:/.*)?$""".r
// Path patterns whose cuid lives in the URL path rather than the query string.
private val pvePvesCuidPath: Regex = """^/?(?:auth/)?(?:api/|wsapi/)?pve/pves/([0-9]+)$""".r
private val pvePackagesCuidPath: Regex =
"""^/?(?:auth/)?(?:api/|wsapi/)?pve/([0-9]+)/[^/]+/packages/.+$""".r

/**
* Authorize the request based on the path and headers.
Expand All @@ -60,7 +65,8 @@ object AccessControlResource extends LazyLogging {
logger.info(s"Authorizing request for path: $path")

path match {
case wsapiWorkflowWebsocket() | apiExecutionsStats() | apiExecutionsResultExport() =>
case wsapiWorkflowWebsocket() | apiExecutionsStats() | apiExecutionsResultExport() |
pveRoute() =>
Comment thread
SarahAsad23 marked this conversation as resolved.
checkComputingUnitAccess(uriInfo, headers, bodyOpt)
case _ =>
logger.warn(s"No authorization logic for path: $path. Denying access.")
Expand Down Expand Up @@ -95,7 +101,14 @@ object AccessControlResource extends LazyLogging {
qToken.orElse(hToken).orElse(bToken).getOrElse("")
}
logger.info(s"token extracted from request $token")
val cuid = queryParams.getOrElse("cuid", "")

val cuid = queryParams.get("cuid").filter(_.nonEmpty).getOrElse {
uriInfo.getPath match {
case pvePvesCuidPath(c) => c
case pvePackagesCuidPath(c) => c
case _ => ""
}
}
val cuidInt =
try {
cuid.toInt
Expand Down Expand Up @@ -213,6 +226,15 @@ class AccessControlResource extends LazyLogging {
logger.info("Request body: " + body)
AccessControlResource.authorize(uriInfo, headers, Option(body).map(_.trim).filter(_.nonEmpty))
}

@DELETE
@Path("/{path:.*}")
def authorizeDelete(
@Context uriInfo: UriInfo,
@Context headers: HttpHeaders
): Response = {
AccessControlResource.authorize(uriInfo, headers)
}
}

@Path("/chat")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,62 @@ class AccessControlResourceSpec
response.getHeaderString(HeaderField.UserName) shouldBe testUser1.getName
response.getHeaderString(HeaderField.UserEmail) shouldBe testUser1.getEmail
}

private def mockRequest(
path: String,
cuidQueryParam: Option[String]
): (UriInfo, HttpHeaders) = {
val mockUriInfo = mock(classOf[UriInfo])
val mockHttpHeaders = mock(classOf[HttpHeaders])

val queryParams = new MultivaluedHashMap[String, String]()
cuidQueryParam.foreach(queryParams.add("cuid", _))

val requestHeaders = new MultivaluedHashMap[String, String]()
requestHeaders.add("Authorization", "Bearer " + token)

when(mockUriInfo.getQueryParameters).thenReturn(queryParams)
when(mockUriInfo.getRequestUri).thenReturn(new URI(testURI))
when(mockUriInfo.getPath).thenReturn(path)
when(mockHttpHeaders.getRequestHeaders).thenReturn(requestHeaders)
when(mockHttpHeaders.getRequestHeader("Authorization"))
.thenReturn(util.Arrays.asList("Bearer " + token))

(mockUriInfo, mockHttpHeaders)
}

it should "return OK for /pve/system with cuid as query parameter" in {
val (uri, headers) = mockRequest("/pve/system", Some(testCU.getCuid.toString))
val response = new AccessControlResource().authorizeGet(uri, headers)

response.getStatus shouldBe Response.Status.OK.getStatusCode
}

it should "return OK for /pve/pves/{cuid} (cuid extracted from path)" in {
val (uri, headers) = mockRequest(s"/pve/pves/${testCU.getCuid}", None)
val response = new AccessControlResource().authorizeDelete(uri, headers)

response.getStatus shouldBe Response.Status.OK.getStatusCode
}

it should "return OK for /pve/{cuid}/{pveName}/packages/{packageName} (cuid extracted from path)" in {
val (uri, headers) = mockRequest(s"/pve/${testCU.getCuid}/myenv/packages/numpy", None)
val response = new AccessControlResource().authorizeDelete(uri, headers)

response.getStatus shouldBe Response.Status.OK.getStatusCode
}

it should "return FORBIDDEN for a PVE path with no cuid in query or path" in {
val (uri, headers) = mockRequest("/pve/no-cuid-anywhere", None)
val response = new AccessControlResource().authorizeGet(uri, headers)

response.getStatus shouldBe Response.Status.FORBIDDEN.getStatusCode
}

it should "return FORBIDDEN for a non-PVE / non-whitelisted path" in {
val (uri, headers) = mockRequest("/random/garbage", Some(testCU.getCuid.toString))
val response = new AccessControlResource().authorizeGet(uri, headers)

response.getStatus shouldBe Response.Status.FORBIDDEN.getStatusCode
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

package org.apache.texera.web.resource.pythonvirtualenvironment

import org.apache.texera.config.KubernetesConfig

import javax.ws.rs._
import javax.ws.rs.core.MediaType
import scala.jdk.CollectionConverters._
Expand All @@ -37,11 +39,8 @@ class PveResource {
@Path("/system")
@Produces(Array(MediaType.APPLICATION_JSON))
def getSystemPackages: util.Map[String, util.List[String]] = {
val isLocal = !KubernetesConfig.kubernetesComputingUnitEnabled
try {

// TODO: Support Kubernetes environment handling
val isLocal = true

val systemPkgs =
PveManager.getSystemPackages(isLocal).toList.asJava

Expand Down Expand Up @@ -103,9 +102,9 @@ class PveResource {
def deletePackage(
@PathParam("cuid") cuid: Int,
@PathParam("pveName") pveName: String,
@PathParam("packageName") packageName: String,
@QueryParam("isLocal") isLocal: Boolean
@PathParam("packageName") packageName: String
): Response = {
val isLocal = !KubernetesConfig.kubernetesComputingUnitEnabled
val messages = PveManager.deletePackages(
cuid,
packageName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

package org.apache.texera.web.resource.pythonvirtualenvironment

import org.apache.texera.config.KubernetesConfig

import javax.websocket._
import javax.websocket.server.ServerEndpoint
import java.util.concurrent.LinkedBlockingQueue
Expand All @@ -41,7 +43,7 @@ class PveWebsocketResource {

val cuid = params.get("cuid").get(0).toInt
val pveName = params.get("pveName").get(0)
val isLocal = params.get("isLocal").get(0).toBoolean
val isLocal = !KubernetesConfig.kubernetesComputingUnitEnabled
val action = params.getOrDefault("action", java.util.List.of("create")).get(0)

val queue = new LinkedBlockingQueue[String]()
Expand Down
2 changes: 2 additions & 0 deletions bin/computing-unit-master.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,13 @@ WORKDIR /texera/amber

COPY --from=build /texera/amber/requirements.txt /tmp/requirements.txt
COPY --from=build /texera/amber/operator-requirements.txt /tmp/operator-requirements.txt
COPY --from=build /texera/amber/system-requirements-lock.txt /tmp/system-requirements-lock.txt

# Install Python runtime dependencies
RUN apt-get update && apt-get install -y \
python3-pip \
python3-dev \
python3-venv \
libpq-dev \
&& apt-get clean

Expand Down
14 changes: 14 additions & 0 deletions bin/k8s/templates/gateway-routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ spec:
- group: gateway.envoyproxy.io
kind: Backend
name: texera-dynamic-backend
- matches:
- path:
type: PathPrefix
value: /pve
filters:
- type: URLRewrite
urlRewrite:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /api/pve
backendRefs:
- group: gateway.envoyproxy.io
kind: Backend
name: texera-dynamic-backend
---
# MinIO Route
{{- if .Values.minio.gateway.enabled }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,6 @@ export class ComputingUnitSelectionComponent implements OnInit {

getPVEs(): void {
const cuId = this.selectedComputingUnit!.computingUnit.cuid;
const isLocal = this.selectedComputingUnit?.computingUnit.type === "local";

this.workflowPveService
.fetchPVEs(cuId)
Expand All @@ -802,7 +801,7 @@ export class ComputingUnitSelectionComponent implements OnInit {
}));

this.workflowPveService
.getSystemPackages(isLocal)
.getSystemPackages(cuId)
.pipe(untilDestroyed(this))
.subscribe({
next: installedResp => {
Expand Down Expand Up @@ -880,11 +879,10 @@ export class ComputingUnitSelectionComponent implements OnInit {
const cuId = this.selectedComputingUnit!.computingUnit.cuid;
const env = this.pves[index];
const trimmedName = env.name.trim();
const isLocal = this.selectedComputingUnit?.computingUnit.type === "local";

env.socket?.close();

const websocketUrl = this.workflowPveService.getPveWebSocketUrl(cuId, trimmedName, isLocal, action, packages);
const websocketUrl = this.workflowPveService.getPveWebSocketUrl(cuId, trimmedName, action, packages);

const socket = new WebSocket(websocketUrl);

Expand Down Expand Up @@ -967,7 +965,6 @@ export class ComputingUnitSelectionComponent implements OnInit {
createVirtualEnvironment(index: number): void {
const env = this.pves[index];
const trimmedName = env.name.trim();
const isLocal = this.selectedComputingUnit?.computingUnit.type === "local";

if (!/^[a-zA-Z0-9]+$/.test(trimmedName)) {
this.notificationService.error("Environment name must contain only letters and numbers.");
Expand Down Expand Up @@ -1066,7 +1063,6 @@ export class ComputingUnitSelectionComponent implements OnInit {

private deleteUserPackages(index: number, onDone?: () => void): void {
const cuId = this.selectedComputingUnit!.computingUnit.cuid;
const isLocal = this.selectedComputingUnit?.computingUnit.type === "local";
const pveName = this.pves[index].name.trim();
const packagesToDelete = [...this.pves[index].deletingPackages];

Expand Down Expand Up @@ -1094,7 +1090,7 @@ export class ComputingUnitSelectionComponent implements OnInit {
const pkg = packagesToDelete[deleteIndex];

this.workflowPveService
.deletePackage(cuId, pveName, pkg.name, isLocal)
.deletePackage(cuId, pveName, pkg.name)
.pipe(untilDestroyed(this))
.subscribe({
next: messages => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ export class WorkflowPveService {
return params;
}

getSystemPackages(isLocal: boolean): Observable<PackageResponse> {
const params = this.buildBaseParams();
getSystemPackages(cuid: number): Observable<PackageResponse> {
const params = this.buildBaseParams().set("cuid", cuid.toString());
return this.http.get<PackageResponse>("/pve/system", { params });
}

Expand All @@ -67,16 +67,16 @@ export class WorkflowPveService {
return this.http.delete(`/pve/pves/${cuid}`);
}

deletePackage(cuid: number, pveName: string, packageName: string, isLocal: boolean) {
const params = this.buildBaseParams().set("isLocal", isLocal.toString());
deletePackage(cuid: number, pveName: string, packageName: string) {
const params = this.buildBaseParams();

return this.http.delete<string[]>(
`/pve/${cuid}/${encodeURIComponent(pveName)}/packages/${encodeURIComponent(packageName)}`,
{ params }
);
}

getPveWebSocketUrl(cuid: number, pveName: string, isLocal: boolean, action: string, packages: string[] = []): string {
getPveWebSocketUrl(cuid: number, pveName: string, action: string, packages: string[] = []): string {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const query = encodeURIComponent(JSON.stringify(packages));

Expand All @@ -88,7 +88,6 @@ export class WorkflowPveService {
`?packages=${query}` +
`&cuid=${cuid}` +
`&pveName=${encodeURIComponent(pveName)}` +
`&isLocal=${isLocal}` +
`&action=${action}` +
tokenParam
);
Expand Down
Loading