This is an automated email from the ASF dual-hosted git repository.

aicam pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new 8b28c196f6 feat(access-control-service): add `access-control-service` 
to authorize the requests to `/wsapi` and `Computing Unit` endpoints (#3598)
8b28c196f6 is described below

commit 8b28c196f6ecf96858bd4e3ebd660cb6c8ccc2ff
Author: ali risheh <[email protected]>
AuthorDate: Tue Oct 7 21:26:44 2025 -0700

    feat(access-control-service): add `access-control-service` to authorize the 
requests to `/wsapi` and `Computing Unit` endpoints (#3598)
    
    ## Access Control Service
    This service is currently used only by envoy as authorization service.
    It act as a third party service to authorize any request sent to the
    computing unit to get socket connection through `/wsapi`. It parses the
    `user-token` from URL parameters and then check user access to the
    computing unit by checking the database and add the corresponding
    information to the following headers:
    - x-user-cu-access
    - x-user-id
    - x-user-name
    - x-user-email
    If the service can not parse the token or fail for any reason, the
    access to computing unit is denied by envoy. If the authorization
    succeed, the user is directly connected to computing unit using
    `Upgrade` on the first `HTTP` handshake request so the latency will not
    change.
    
    ## The new connection flow
    
    <img width="1282" height="577"
    alt="489656839-e09b06ee-3915-4c18-9584-e880bc06011d"
    
src="https://github.com/user-attachments/assets/f7b0d29e-f30b-4e7f-9a0d-966f52d8d48a";
    />
    
    
    1. A user initiates an `HTTP` request to connect to a specific Computing
    Unit.
    2.  The request is first routed through the **Gateway** to **Envoy**.
    3. Envoy pauses the request and sends a query to the **Access Control
    Service** to get an authorization decision.
    4. The Access Control Service verifies the user's token and checks a
    PostgreSQL database to see if the user has the necessary permissions for
    the target Computing Unit.
    5. **If authorized**, the service injects specific HTTP headers
    (`x-user-cu-access`, `x-user-id`, `x-user-name`) into the request and
    sends an approval back to Envoy.
    6.  Envoy then forwards the approved request to the Computing Unit.
    7. The connection is then upgraded to a WebSocket, establishing a
    secure, interactive session.
    
    If authorization fails at any point, Envoy immediately denies the
    connection request, and the user is prevented from accessing the
    Computing Unit. This new process provides **enhanced security**, a
    **centralized authorization logic**, and is designed to have **no
    performance impact** on the established WebSocket connection since the
    check is performed only on the initial handshake.
    
    ## Summary of file changes
    
    | Component/Flow | File | Description |
    | :--- | :--- | :--- |
    | **Database Access Logic** |
    
`core/auth/src/main/scala/edu/uci/ics/texera/auth/util/ComputingUnitAccess.scala`
    | Implements the logic to query the PostgreSQL database and determine a
    user's access privilege (`READ`, `WRITE`, `NONE`) for a given Computing
    Unit. |
    | |
    `core/auth/src/main/scala/edu/uci/ics/texera/auth/util/HeaderField.scala`
    | Defines constants for the custom HTTP headers (`x-user-cu-access`,
    `x-user-id`, etc.) that are injected by the Access Control Service. |
    | **WebSocket Connection Handling** |
    
`core/amber/src/main/scala/edu/uci/ics/texera/web/ServletAwareConfigurator.scala`
    | Modified to read the new authorization headers during the WebSocket
    handshake. If headers are present, it creates the `User` object from
    them; otherwise, it falls back to the old method of parsing the JWT from
    URL parameters for single-node mode. |
    | |
    `core/amber/src/main/scala/edu/uci/ics/texera/web/SessionState.scala` |
    Updated to store the user's access privilege level for the current
    computing unit within the session. |
    | |
    
`core/amber/src/main/scala/edu/uci/ics/texera/web/resource/WorkflowWebsocketResource.scala`
    | Enforces the access control by checking if the user has `WRITE`
    privilege before allowing a `WorkflowExecuteRequest`. |
    | **Deployment & Routing** |
    `deployment/access-control-service.dockerfile` | New Dockerfile for
    building and containerizing the Access Control Service. |
    | |
    
`deployment/k8s/texera-helmchart/templates/access-control-service-deployment.yaml`
    | New Kubernetes manifest to deploy the Access Control Service. |
    | |
    
`deployment/k8s/texera-helmchart/templates/access-control-service-service.yaml`
    | New Kubernetes service manifest to expose the Access Control Service
    within the cluster. |
    | | `deployment/k8s/texera-helmchart/templates/envoy-config.yaml` |
    **Key change:** Configures Envoy to use the new service as an external
    authorization filter (`ext_authz`). It intercepts relevant requests,
    forwards them for an authorization check, and then passes the injected
    headers to the upstream service (AmberMaster). |
    | | `deployment/k8s/texera-helmchart/values.yaml` | Adds the
    configuration parameters for the new Access Control Service to the Helm
    chart. |
    | **Frontend UI** |
    `core/gui/src/app/workspace/component/menu/menu.component.ts` & `.html`|
    The frontend is updated to disable the "Run" button if the connected
    user does not have `WRITE` access to the selected Computing Unit,
    providing immediate visual feedback. |
    | **Build & Configuration** | `core/build.sbt` | The root SBT build file
    is updated to include the new `AccessControlService` module. |
    | | `core/config/src/main/scala/edu/uci/ics/amber/util/PathUtils.scala`
    | Adds a path helper for the new service's directory structure. |
    
    ---------
    
    Co-authored-by: Ali Risheh <[email protected]>
---
 .../ics/texera/web/ServletAwareConfigurator.scala  | 90 ++++++++++++++++------
 .../edu/uci/ics/texera/web/SessionState.scala      |  7 ++
 .../web/resource/WorkflowWebsocketResource.scala   | 16 +++-
 core/build.sbt                                     |  1 +
 .../user/share-access/share-access.component.html  |  4 +-
 .../workspace/component/menu/menu.component.html   |  2 +-
 .../app/workspace/component/menu/menu.component.ts | 17 +++-
 deployment/access-control-service.dockerfile       | 54 +++++++++++++
 .../access-control-service-deployment.yaml         | 60 +++++++++++++++
 .../templates/access-control-service-service.yaml  | 30 ++++++++
 .../texera-helmchart/templates/envoy-config.yaml   | 38 +++++++++
 .../templates/envoy-deployment.yaml                |  2 +-
 deployment/k8s/texera-helmchart/values.yaml        |  8 ++
 13 files changed, 299 insertions(+), 30 deletions(-)

diff --git 
a/core/amber/src/main/scala/edu/uci/ics/texera/web/ServletAwareConfigurator.scala
 
b/core/amber/src/main/scala/edu/uci/ics/texera/web/ServletAwareConfigurator.scala
index 0df84812aa..034698939f 100644
--- 
a/core/amber/src/main/scala/edu/uci/ics/texera/web/ServletAwareConfigurator.scala
+++ 
b/core/amber/src/main/scala/edu/uci/ics/texera/web/ServletAwareConfigurator.scala
@@ -21,6 +21,9 @@ package edu.uci.ics.texera.web
 
 import com.typesafe.scalalogging.LazyLogging
 import edu.uci.ics.texera.auth.JwtAuth.jwtConsumer
+import edu.uci.ics.texera.auth.util.HeaderField
+import edu.uci.ics.texera.config.KubernetesConfig
+import edu.uci.ics.texera.dao.jooq.generated.enums.PrivilegeEnum
 import edu.uci.ics.texera.dao.jooq.generated.tables.pojos.User
 import org.apache.http.client.utils.URLEncodedUtils
 
@@ -29,6 +32,7 @@ import java.nio.charset.Charset
 import javax.websocket.HandshakeResponse
 import javax.websocket.server.{HandshakeRequest, ServerEndpointConfig}
 import scala.jdk.CollectionConverters.ListHasAsScala
+import scala.jdk.CollectionConverters._
 
 /**
   * This configurator extracts HTTPSession and associates it to 
ServerEndpointConfig,
@@ -46,30 +50,70 @@ class ServletAwareConfigurator extends 
ServerEndpointConfig.Configurator with La
       response: HandshakeResponse
   ): Unit = {
     try {
-      val params =
-        URLEncodedUtils.parse(new URI("?" + request.getQueryString), 
Charset.defaultCharset())
-      params.asScala
-        .map(pair => pair.getName -> pair.getValue)
-        .toMap
-        .get("access-token")
-        .map(token => {
-          val claims = jwtConsumer.process(token).getJwtClaims
-          config.getUserProperties.put(
-            classOf[User].getName,
-            new User(
-              claims.getClaimValue("userId").asInstanceOf[Long].toInt,
-              claims.getSubject,
-              
String.valueOf(claims.getClaimValue("email").asInstanceOf[String]),
-              null,
-              null,
-              null,
-              null,
-              null,
-              null
-            )
-          )
-        })
+      val headers = 
request.getHeaders.asScala.view.mapValues(_.asScala.headOption).toMap
+      if (
+        headers.contains(HeaderField.UserComputingUnitAccess) &&
+        headers.contains(HeaderField.UserId) &&
+        headers.contains(HeaderField.UserName) &&
+        headers.contains(HeaderField.UserEmail)
+      ) {
+        // KUBERNETES MODE: Construct the User object from trusted headers
+        // coming from envoy and generated by access control service.
+
+        val userId = headers.get(HeaderField.UserId).flatten.map(_.toInt).get
+        val userName = headers.get(HeaderField.UserName).flatten.get
+        val userEmail = headers.get(HeaderField.UserEmail).flatten.get
+        val cuAccess = 
headers.get(HeaderField.UserComputingUnitAccess).flatten.getOrElse("")
+        config.getUserProperties.put(HeaderField.UserComputingUnitAccess, 
cuAccess)
+        logger.info(
+          s"User ID: $userId, User Name: $userName, User Email: $userEmail 
with CU Access: $cuAccess"
+        )
 
+        config.getUserProperties.put(
+          classOf[User].getName,
+          new User(
+            userId,
+            userName,
+            userEmail,
+            null,
+            null,
+            null,
+            null,
+            null,
+            null
+          )
+        )
+        logger.debug(s"User created from headers: ID=$userId, Name=$userName")
+      } else {
+        // SINGLE NODE MODE: Construct the User object from JWT in query 
parameters.
+        val params =
+          URLEncodedUtils.parse(new URI("?" + request.getQueryString), 
Charset.defaultCharset())
+        config.getUserProperties.put(
+          HeaderField.UserComputingUnitAccess,
+          PrivilegeEnum.WRITE.name()
+        )
+        params.asScala
+          .map(pair => pair.getName -> pair.getValue)
+          .toMap
+          .get("access-token")
+          .map(token => {
+            val claims = jwtConsumer.process(token).getJwtClaims
+            config.getUserProperties.put(
+              classOf[User].getName,
+              new User(
+                claims.getClaimValue("userId").asInstanceOf[Long].toInt,
+                claims.getSubject,
+                
String.valueOf(claims.getClaimValue("email").asInstanceOf[String]),
+                null,
+                null,
+                null,
+                null,
+                null,
+                null
+              )
+            )
+          })
+      }
     } catch {
       case e: Exception =>
         logger.error("Failed to retrieve the User during websocket handshake", 
e)
diff --git 
a/core/amber/src/main/scala/edu/uci/ics/texera/web/SessionState.scala 
b/core/amber/src/main/scala/edu/uci/ics/texera/web/SessionState.scala
index 3d296349a6..642b5bbad8 100644
--- a/core/amber/src/main/scala/edu/uci/ics/texera/web/SessionState.scala
+++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/SessionState.scala
@@ -20,6 +20,7 @@
 package edu.uci.ics.texera.web
 
 import edu.uci.ics.amber.util.JSONUtils.objectMapper
+import edu.uci.ics.texera.dao.jooq.generated.enums.PrivilegeEnum
 import edu.uci.ics.texera.web.model.websocket.event.TexeraWebSocketEvent
 import edu.uci.ics.texera.web.service.WorkflowService
 import io.reactivex.rxjava3.disposables.Disposable
@@ -53,6 +54,7 @@ class SessionState(session: Session) {
   private var currentWorkflowState: Option[WorkflowService] = None
   private var workflowSubscription = Disposable.empty()
   private var executionSubscription = Disposable.empty()
+  private var userComputingUnitAccess: PrivilegeEnum = PrivilegeEnum.NONE
 
   def send(msg: TexeraWebSocketEvent): Unit = {
     session.getAsyncRemote.sendText(objectMapper.writeValueAsString(msg))
@@ -80,4 +82,9 @@ class SessionState(session: Session) {
     )
 
   }
+
+  def setUserComputingUnitAccess(cuAccess: PrivilegeEnum): Unit = {
+    this.userComputingUnitAccess = cuAccess
+  }
+  def getUserComputingUnitAccess: PrivilegeEnum = this.userComputingUnitAccess
 }
diff --git 
a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/WorkflowWebsocketResource.scala
 
b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/WorkflowWebsocketResource.scala
index 1cf90241e9..01b3154580 100644
--- 
a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/WorkflowWebsocketResource.scala
+++ 
b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/WorkflowWebsocketResource.scala
@@ -27,6 +27,8 @@ import 
edu.uci.ics.amber.error.ErrorUtils.getStackTraceWithAllCauses
 import edu.uci.ics.amber.core.virtualidentity.WorkflowIdentity
 import 
edu.uci.ics.amber.core.workflowruntimestate.FatalErrorType.COMPILATION_ERROR
 import edu.uci.ics.amber.core.workflowruntimestate.WorkflowFatalError
+import edu.uci.ics.texera.auth.util.HeaderField
+import edu.uci.ics.texera.dao.jooq.generated.enums.PrivilegeEnum
 import edu.uci.ics.texera.dao.jooq.generated.tables.pojos.User
 import edu.uci.ics.texera.web.model.websocket.event.{WorkflowErrorEvent, 
WorkflowStateEvent}
 import edu.uci.ics.texera.web.model.websocket.request._
@@ -51,13 +53,22 @@ class WorkflowWebsocketResource extends LazyLogging {
     SessionState.setState(session.getId, sessionState)
     val wid = session.getRequestParameterMap.get("wid").get(0).toLong
     val cuid = session.getRequestParameterMap.get("cuid").get(0).toInt
+    val cuAccessEnum: PrivilegeEnum = PrivilegeEnum.valueOf(
+      session.getUserProperties
+        .get(HeaderField.UserComputingUnitAccess)
+        .asInstanceOf[String]
+    )
+
+    sessionState.setUserComputingUnitAccess(cuAccessEnum)
+    logger.info(
+      s"Websocket connection opened for workflow $wid with computing unit 
$cuid and access $cuAccessEnum"
+    )
     // hack to refresh frontend run button state
     sessionState.send(WorkflowStateEvent("Uninitialized"))
     val workflowState =
       WorkflowService.getOrCreate(WorkflowIdentity(wid), cuid)
     sessionState.subscribe(workflowState)
     
sessionState.send(ClusterStatusUpdateEvent(ClusterListener.numWorkerNodesInCluster))
-    logger.info("connection open")
   }
 
   @OnClose
@@ -94,6 +105,9 @@ class WorkflowWebsocketResource extends LazyLogging {
             sessionState.send(modifyLogicResponse)
           }
         case workflowExecuteRequest: WorkflowExecuteRequest =>
+          if (sessionState.getUserComputingUnitAccess != PrivilegeEnum.WRITE) {
+            throw new IllegalStateException("User does not have write access 
to the computing unit")
+          }
           workflowStateOpt match {
             case Some(workflow) =>
               sessionState.send(WorkflowStateEvent("Initializing"))
diff --git a/core/build.sbt b/core/build.sbt
index 3e2e150ac9..11982fa6e8 100644
--- a/core/build.sbt
+++ b/core/build.sbt
@@ -99,6 +99,7 @@ lazy val CoreProject = (project in file("."))
     DAO,
     Config,
     ConfigService,
+    AccessControlService,
     Auth,
     WorkflowCore,
     ComputingUnitManagingService,
diff --git 
a/core/gui/src/app/dashboard/component/user/share-access/share-access.component.html
 
b/core/gui/src/app/dashboard/component/user/share-access/share-access.component.html
index 64c7eb9d47..f181ef839d 100644
--- 
a/core/gui/src/app/dashboard/component/user/share-access/share-access.component.html
+++ 
b/core/gui/src/app/dashboard/component/user/share-access/share-access.component.html
@@ -115,9 +115,7 @@
     <br />
 
     <nz-card nzTitle="Share">
-      <div
-        style="height: 50px"
-        *ngIf="type !== 'computing-unit'">
+      <div style="height: 50px">
         Access Level:
         <select formControlName="accessLevel">
           <option value="READ">read</option>
diff --git a/core/gui/src/app/workspace/component/menu/menu.component.html 
b/core/gui/src/app/workspace/component/menu/menu.component.html
index e212169321..7abddf5df9 100644
--- a/core/gui/src/app/workspace/component/menu/menu.component.html
+++ b/core/gui/src/app/workspace/component/menu/menu.component.html
@@ -364,7 +364,7 @@
           [nzPopoverTrigger]="this.config.env.userSystemEnabled?'hover':null"
           [nzPopoverContent]="executionSettings"
           nzPopoverPlacement="bottom"
-          [disabled]="runDisable || (!workflowWebsocketService.isConnected && 
computingUnitStatus !== ComputingUnitState.NoComputingUnit) || 
displayParticularWorkflowVersion"
+          [disabled]="runDisable || (!workflowWebsocketService.isConnected && 
computingUnitStatus !== ComputingUnitState.NoComputingUnit) || 
displayParticularWorkflowVersion || selectedComputingUnit?.accessPrivilege !== 
Privilege.WRITE"
           id="run-button"
           nz-button
           nzType="primary">
diff --git a/core/gui/src/app/workspace/component/menu/menu.component.ts 
b/core/gui/src/app/workspace/component/menu/menu.component.ts
index f2dec0bfc0..bdeea578b3 100644
--- a/core/gui/src/app/workspace/component/menu/menu.component.ts
+++ b/core/gui/src/app/workspace/component/menu/menu.component.ts
@@ -54,6 +54,8 @@ import { ComputingUnitStatusService } from 
"../../service/computing-unit-status/
 import { ComputingUnitState } from 
"../../types/computing-unit-connection.interface";
 import { ComputingUnitSelectionComponent } from 
"../power-button/computing-unit-selection.component";
 import { GuiConfigService } from "../../../common/service/gui-config.service";
+import { DashboardWorkflowComputingUnit } from 
"../../types/workflow-computing-unit";
+import { Privilege } from "../../../dashboard/type/share-access.interface";
 
 /**
  * MenuComponent is the top level menu bar that shows
@@ -110,6 +112,7 @@ export class MenuComponent implements OnInit, OnDestroy {
 
   // Computing unit status variables
   private computingUnitStatusSubscription: Subscription = new Subscription();
+  public selectedComputingUnit: DashboardWorkflowComputingUnit | null = null;
   public computingUnitStatus: ComputingUnitState = 
ComputingUnitState.NoComputingUnit;
 
   @ViewChild(ComputingUnitSelectionComponent) 
computingUnitSelectionComponent!: ComputingUnitSelectionComponent;
@@ -162,7 +165,8 @@ export class MenuComponent implements OnInit, OnDestroy {
     this.registerWorkflowModifiableChangedHandler();
     this.registerWorkflowIdUpdateHandler();
 
-    // Subscribe to computing unit status changes
+    // Subscribe to computing unit
+    this.subscribeToComputingUnitSelection();
     this.subscribeToComputingUnitStatus();
   }
 
@@ -202,6 +206,15 @@ export class MenuComponent implements OnInit, OnDestroy {
     this.computingUnitStatusSubscription.unsubscribe();
   }
 
+  private subscribeToComputingUnitSelection(): void {
+    this.computingUnitStatusService
+      .getSelectedComputingUnit()
+      .pipe(untilDestroyed(this))
+      .subscribe(unit => {
+        this.selectedComputingUnit = unit;
+      });
+  }
+
   /**
    * Subscribe to computing unit status changes from the 
ComputingUnitStatusService
    */
@@ -720,4 +733,6 @@ export class MenuComponent implements OnInit, OnDestroy {
       this.config.env.workflowEmailNotificationEnabled && 
this.config.env.userSystemEnabled
     );
   }
+
+  protected readonly Privilege = Privilege;
 }
diff --git a/deployment/access-control-service.dockerfile 
b/deployment/access-control-service.dockerfile
new file mode 100644
index 0000000000..6121a2709d
--- /dev/null
+++ b/deployment/access-control-service.dockerfile
@@ -0,0 +1,54 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+FROM sbtscala/scala-sbt:eclipse-temurin-jammy-11.0.17_8_1.9.3_2.13.11 AS build
+
+# Set working directory
+WORKDIR /core
+
+# Copy all projects under core to /core
+COPY core/ .
+
+# Update system and install dependencies
+RUN apt-get update && apt-get install -y \
+    netcat \
+    unzip \
+    libpq-dev \
+    && apt-get clean
+
+WORKDIR /core
+# Add .git for runtime calls to jgit from OPversion
+COPY .git ../.git
+
+RUN sbt clean AccessControlService/dist
+
+# Unzip the texera binary
+RUN unzip  
access-control-service/target/universal/access-control-service-*.zip -d target/
+
+FROM eclipse-temurin:11-jre-jammy AS runtime
+
+WORKDIR /core
+
+COPY --from=build /.git /.git
+# Copy the built texera binary from the build phase
+COPY --from=build /core/target/access-control-service* /core/
+# Copy resources directories under /core from build phase
+COPY --from=build /core/access-control-service/src/main/resources 
/core/access-control-service/src/main/resources
+
+CMD ["bin/access-control-service"]
+
+EXPOSE 9096
\ No newline at end of file
diff --git 
a/deployment/k8s/texera-helmchart/templates/access-control-service-deployment.yaml
 
b/deployment/k8s/texera-helmchart/templates/access-control-service-deployment.yaml
new file mode 100644
index 0000000000..a332c65bd3
--- /dev/null
+++ 
b/deployment/k8s/texera-helmchart/templates/access-control-service-deployment.yaml
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name:  {{.Release.Name}}-{{ .Values.accessControlService.name }}
+  namespace: {{ .Release.Namespace }}
+  labels:
+    app: {{ .Release.Name }}-{{ .Values.accessControlService.name }}
+spec:
+  replicas: {{ .Values.accessControlService.numOfPods | default 1 }}
+  selector:
+    matchLabels:
+      app: {{ .Release.Name }}-{{ .Values.accessControlService.name }}
+  template:
+    metadata:
+      labels:
+        app: {{ .Release.Name }}-{{ .Values.accessControlService.name }}
+    spec:
+      containers:
+        - name: {{ .Values.accessControlService.name }}
+          image: {{ .Values.accessControlService.imageName }}
+          imagePullPolicy: {{ .Values.texeraImages.pullPolicy }}
+          ports:
+            - containerPort: {{ .Values.accessControlService.service.port }}
+          env:
+            - name: STORAGE_JDBC_URL
+              value: jdbc:postgresql://{{ .Release.Name 
}}-postgresql:5432/texera_db?currentSchema=texera_db,public
+            - name: STORAGE_JDBC_PASSWORD
+              valueFrom:
+                secretKeyRef:
+                  name: {{ .Release.Name }}-postgresql
+                  key: postgres-password
+          livenessProbe:
+            httpGet:
+              path: /api/healthcheck
+              port: {{ .Values.accessControlService.service.port }}
+            initialDelaySeconds: 30
+            periodSeconds: 10
+          readinessProbe:
+            httpGet:
+              path: /api/healthcheck
+              port: {{ .Values.accessControlService.service.port }}
+            initialDelaySeconds: 5
+            periodSeconds: 5
\ No newline at end of file
diff --git 
a/deployment/k8s/texera-helmchart/templates/access-control-service-service.yaml 
b/deployment/k8s/texera-helmchart/templates/access-control-service-service.yaml
new file mode 100644
index 0000000000..25f0390566
--- /dev/null
+++ 
b/deployment/k8s/texera-helmchart/templates/access-control-service-service.yaml
@@ -0,0 +1,30 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ .Release.Name }}-{{ .Values.accessControlService.name }}-svc
+  namespace: {{ .Release.Namespace }}
+spec:
+  type: {{ .Values.accessControlService.service.type }}
+  selector:
+    app: {{ .Release.Name }}-{{ .Values.accessControlService.name }}
+  ports:
+    - protocol: TCP
+      port: {{ .Values.accessControlService.service.port }}
+      targetPort: {{ .Values.accessControlService.service.port }}
\ No newline at end of file
diff --git a/deployment/k8s/texera-helmchart/templates/envoy-config.yaml 
b/deployment/k8s/texera-helmchart/templates/envoy-config.yaml
index 1de4411c09..cdce931176 100644
--- a/deployment/k8s/texera-helmchart/templates/envoy-config.yaml
+++ b/deployment/k8s/texera-helmchart/templates/envoy-config.yaml
@@ -63,6 +63,28 @@ data:
                                 prefix_rewrite: "/api/executions/result/export"
                                 timeout: "0s"
                     http_filters:
+                      - name: envoy.filters.http.ext_authz
+                        typed_config:
+                          "@type": 
type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
+                          http_service:
+                            server_uri:
+                              uri: http://{{ .Release.Name }}-{{ 
.Values.accessControlService.name }}-svc.{{ .Release.Namespace 
}}.svc.cluster.local
+                              cluster: access_control_service_cluster
+                              timeout: 5s
+                            path_prefix: /api/auth
+                            authorization_request:
+                              allowed_headers:
+                                patterns:
+                                  - exact: "authorization"
+                                  - exact: "x-envoy-original-path"
+                            authorization_response:
+                              allowed_upstream_headers:
+                                patterns:
+                                  - exact: "x-user-cu-access"
+                                  - exact: "x-user-id"
+                                  - exact: "x-user-name"
+                                  - exact: "x-user-email"
+                          failure_mode_allow: false
                       - name: envoy.filters.http.lua
                         typed_config:
                           "@type": 
type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
@@ -90,6 +112,9 @@ data:
                       - name: envoy.access_loggers.stdout
                         typed_config:
                           "@type": 
type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
+                          log_format:
+                            text_format_source:
+                              inline_string: "[%START_TIME%] \"%REQ(:METHOD)% 
%REQ(X-ENVOY-ORIGINAL-PATH?PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% 
%BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% 
\"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" 
\"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\" 
AuthHeaders[user-id:\"%REQ(X-USER-ID)%\" email:\"%REQ(X-USER-EMAIL)%\" 
cu-access:\"%REQ(X-USER-CU-ACCESS)%\"]\n"
 
       clusters:
         - name: dynamic_service
@@ -103,3 +128,16 @@ data:
                 name: dynamic_dns_cache
                 dns_lookup_family: V4_ONLY
                 dns_refresh_rate: 1s
+        - name: access_control_service_cluster # The Access Control Service 
authorize requests and add user info headers
+          connect_timeout: 0.25s
+          type: LOGICAL_DNS
+          lb_policy: round_robin
+          load_assignment:
+            cluster_name: access_control_service_cluster
+            endpoints:
+              - lb_endpoints:
+                  - endpoint:
+                      address:
+                        socket_address:
+                          address: {{ .Release.Name }}-{{ 
.Values.accessControlService.name }}-svc.{{ .Release.Namespace 
}}.svc.cluster.local.
+                          port_value: {{ 
.Values.accessControlService.service.port }}
diff --git a/deployment/k8s/texera-helmchart/templates/envoy-deployment.yaml 
b/deployment/k8s/texera-helmchart/templates/envoy-deployment.yaml
index ee1894c2e2..0e0522ee49 100644
--- a/deployment/k8s/texera-helmchart/templates/envoy-deployment.yaml
+++ b/deployment/k8s/texera-helmchart/templates/envoy-deployment.yaml
@@ -42,7 +42,7 @@ spec:
           args:
             - "-c"
             - "/etc/envoy/envoy.yaml"  # Specify the path to the configuration 
file
-            # - "--log-level debug"  # Set level of logging
+            - "--log-level debug"  # Set level of logging
       volumes:
         - name: envoy-config
           configMap:
diff --git a/deployment/k8s/texera-helmchart/values.yaml 
b/deployment/k8s/texera-helmchart/values.yaml
index 1e36c20f6a..7a10c08642 100644
--- a/deployment/k8s/texera-helmchart/values.yaml
+++ b/deployment/k8s/texera-helmchart/values.yaml
@@ -157,6 +157,14 @@ configService:
     type: ClusterIP
     port: 9094
 
+accessControlService:
+  name: access-control-service
+  numOfPods: 1
+  imageName: texera/access-control-service:cluster
+  service:
+    type: ClusterIP
+    port: 9096
+
 # Configs of the envoy proxy, used to routerequests to the computing units
 envoy:
   replicas: 1

Reply via email to