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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-5199-ab79ea44d0e6d12097fcb11674b092341af01761
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 2ebef323605b156bcaaeb9eb02454797059758a2
Author: Yicong Huang <[email protected]>
AuthorDate: Sun May 31 16:47:20 2026 -0700

    fix: enforce @RolesAllowed on microservice resources (#5199)
    
    ### What changes were proposed in this PR?
    
    Re-applies #5049 (Jersey `@RolesAllowed` enforcement on
    `config-service`, `computing-unit-managing-service`, and
    `workflow-compiling-service`) and additionally marks the two pre-login
    `ConfigResource` endpoints — `/api/config/gui` and
    `/api/config/user-system` — as `@PermitAll`. Those endpoints are loaded
    by `GuiConfigService.load()` in the Angular `APP_INITIALIZER` before any
    login, so once role enforcement is on they must keep returning `200` to
    unauthenticated callers; missing this was what broke bootstrap and got
    #5049 reverted in #5173. Everything outside `config-service` matches
    #5049 byte-for-byte.
    
    ### Any related issues, documentation, or discussions?
    
    Closes: #4904
    Prior attempt: #5049, reverted by #5173. The bootstrap root cause was
    diagnosed inline at
    https://github.com/apache/texera/pull/5049#issuecomment-4527214062.
    
    ### How was this PR tested?
    
    Added `ConfigResourceAuthSpec`: wires `ConfigResource` through the same
    `JwtAuthFilter` + `RolesAllowedDynamicFeature` pipeline production uses
    (via Dropwizard's `ResourceExtension`) and fires HTTP requests with no
    `Authorization` header.
    
    - `GET /config/gui` → expects `200`
    - `GET /config/user-system` → expects `200`
    - `GET /auth-probe` (an in-test `@RolesAllowed` resource) → expects
    `403`
    
    Manually tested. The `403` sanity guard ensures the feature is actually
    enforcing, so a future "200 everywhere" regression cannot silently slip
    through. Kept the three `*ServiceRunSpec` structural tests from #5049
    verifying that `RolesAllowedDynamicFeature` is registered. Manual
    reproduction with `curl` against a local dev server confirmed the
    unauthenticated bootstrap path returns `200` while a low-role JWT
    against an annotated endpoint returns `403`.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Co-authored with Claude Opus 4.7 in compliance with ASF.
    
    ---------
    
    Signed-off-by: Yicong Huang <[email protected]>
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
    Co-authored-by: Matthew B. <[email protected]>
---
 amber/LICENSE-binary-java                          |   1 +
 amber/NOTICE-binary                                |  43 ++++++++
 build.sbt                                          |   2 +-
 common/auth/build.sbt                              |   1 +
 .../org/apache/texera/auth/JwtAuthFilter.scala     |   9 ++
 computing-unit-managing-service/build.sbt          |   7 ++
 .../service/ComputingUnitManagingService.scala     |  29 +++--
 .../ComputingUnitManagingServiceRunSpec.scala      |  48 +++++++++
 .../org/apache/texera/service/ConfigService.scala  |  26 +++--
 .../texera/service/resource/ConfigResource.scala   |  10 +-
 .../texera/service/ConfigServiceRunSpec.scala      |  48 +++++++++
 .../service/resource/ConfigResourceAuthSpec.scala  | 117 +++++++++++++++++++++
 workflow-compiling-service/LICENSE-binary          |   3 +
 workflow-compiling-service/build.sbt               |   1 +
 .../texera/service/WorkflowCompilingService.scala  |  23 +++-
 .../service/WorkflowCompilingServiceRunSpec.scala  |  48 +++++++++
 16 files changed, 393 insertions(+), 23 deletions(-)

diff --git a/amber/LICENSE-binary-java b/amber/LICENSE-binary-java
index 86cdb8c8f4..fba8dd9cd2 100644
--- a/amber/LICENSE-binary-java
+++ b/amber/LICENSE-binary-java
@@ -631,6 +631,7 @@ licensed with GPL-2.0 with Classpath Exception)
 
--------------------------------------------------------------------------------
 
 Scala/Java jars:
+  - jakarta.annotation.jakarta.annotation-api-2.1.1.jar
   - jakarta.ws.rs.jakarta.ws.rs-api-3.0.0.jar
   - javax.ws.rs.javax.ws.rs-api-2.1.1.jar
   - org.jgrapht.jgrapht-core-1.4.0.jar
diff --git a/amber/NOTICE-binary b/amber/NOTICE-binary
index d4408e53c3..5c8c0b0ae8 100644
--- a/amber/NOTICE-binary
+++ b/amber/NOTICE-binary
@@ -1483,6 +1483,49 @@ please check the country's laws, regulations and 
policies concerning the import,
 possession, or use, and re-export of encryption software, to see if this is
 permitted.
 
+--------------------------------------------------------------------------------
+Jakarta Annotations API (jakarta.annotation-api 2.1.1)
+--------------------------------------------------------------------------------
+
+# Notices for Jakarta Annotations
+
+This content is produced and maintained by the Jakarta Annotations project.
+
+ * Project home: https://projects.eclipse.org/projects/ee4j.ca
+
+## Trademarks
+
+Jakarta Annotations is a trademark of the Eclipse Foundation.
+
+## Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License v. 2.0 which is available at
+http://www.eclipse.org/legal/epl-2.0. This Source Code may also be made
+available under the following Secondary Licenses when the conditions for such
+availability set forth in the Eclipse Public License v. 2.0 are satisfied: GNU
+General Public License, version 2 with the GNU Classpath Exception which is
+available at https://www.gnu.org/software/classpath/license.html.
+
+SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+
+## Source Code
+
+The project maintains the following source code repositories:
+
+ * https://github.com/eclipse-ee4j/common-annotations-api
+
+## Third-party Content
+
+## Cryptography
+
+Content may contain encryption software. The country in which you are currently
+may have restrictions on the import, possession, and use, and/or re-export to
+another country, of encryption software. BEFORE using any encryption software,
+please check the country's laws, regulations and policies concerning the 
import,
+possession, or use, and re-export of encryption software, to see if this is
+permitted.
+
 
--------------------------------------------------------------------------------
 Jakarta RESTful Web Services API (jakarta.ws.rs-api 3.0.x / 3.1.0)
 
--------------------------------------------------------------------------------
diff --git a/build.sbt b/build.sbt
index 3767e7a6c2..cfdd06864b 100644
--- a/build.sbt
+++ b/build.sbt
@@ -125,7 +125,7 @@ lazy val FileService = (project in file("file-service"))
 
 lazy val WorkflowOperator = (project in 
file("common/workflow-operator")).settings(asfLicensingSettingsWithVendored).dependsOn(WorkflowCore)
 lazy val WorkflowCompilingService = (project in 
file("workflow-compiling-service"))
-  .dependsOn(WorkflowOperator, Config)
+  .dependsOn(WorkflowOperator, Auth, Config)
   .settings(asfLicensingSettings)
   .settings(
     dependencyOverrides ++= Seq(
diff --git a/common/auth/build.sbt b/common/auth/build.sbt
index a33da64fea..742cf95eea 100644
--- a/common/auth/build.sbt
+++ b/common/auth/build.sbt
@@ -57,6 +57,7 @@ libraryDependencies ++= Seq(
   "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5",            // for 
LazyLogging
   "org.bitbucket.b_c" % "jose4j" % "0.9.6",                             // for 
jwt parser
   "jakarta.ws.rs" % "jakarta.ws.rs-api" % "3.0.0",                      // for 
JwtAuthFilter
+  "jakarta.annotation" % "jakarta.annotation-api" % "2.1.1",            // for 
@Priority on JwtAuthFilter
   "jakarta.servlet" % "jakarta.servlet-api" % "5.0.0" % "provided",    // for 
RequestLoggingFilter
   "org.eclipse.jetty" % "jetty-servlet" % "11.0.24" % "provided",      // for 
FilterHolder
   "org.scalatest" %% "scalatest" % "3.2.17" % Test
diff --git 
a/common/auth/src/main/scala/org/apache/texera/auth/JwtAuthFilter.scala 
b/common/auth/src/main/scala/org/apache/texera/auth/JwtAuthFilter.scala
index 5698515630..cedc86573d 100644
--- a/common/auth/src/main/scala/org/apache/texera/auth/JwtAuthFilter.scala
+++ b/common/auth/src/main/scala/org/apache/texera/auth/JwtAuthFilter.scala
@@ -20,6 +20,8 @@
 package org.apache.texera.auth
 
 import com.typesafe.scalalogging.LazyLogging
+import jakarta.annotation.Priority
+import jakarta.ws.rs.Priorities
 import jakarta.ws.rs.container.{ContainerRequestContext, 
ContainerRequestFilter}
 import jakarta.ws.rs.core.{HttpHeaders, SecurityContext}
 import jakarta.ws.rs.ext.Provider
@@ -27,7 +29,14 @@ import 
org.apache.texera.dao.jooq.generated.enums.UserRoleEnum
 
 import java.security.Principal
 
+// Must run before Jersey's RolesAllowedRequestFilter (which sits at
+// Priorities.AUTHORIZATION = 2000). Without an explicit @Priority, this
+// filter defaults to Priorities.USER (5000) and would run *after* the
+// role check, so a request bearing a valid JWT would still be rejected
+// because the SecurityContext hasn't been populated yet. Pinning to
+// AUTHENTICATION (1000) restores the standard auth → authz ordering.
 @Provider
+@Priority(Priorities.AUTHENTICATION)
 class JwtAuthFilter extends ContainerRequestFilter with LazyLogging {
 
   override def filter(requestContext: ContainerRequestContext): Unit = {
diff --git a/computing-unit-managing-service/build.sbt 
b/computing-unit-managing-service/build.sbt
index 3d385d33d3..1c39a6b03d 100644
--- a/computing-unit-managing-service/build.sbt
+++ b/computing-unit-managing-service/build.sbt
@@ -34,6 +34,13 @@ Universal / mappings := AddMetaInfLicenseFiles.distMappings(
 
 // Dependency Versions
 val dropwizardVersion = "4.0.7"
+val mockitoVersion = "5.4.0"
+
+// Test Dependencies
+libraryDependencies ++= Seq(
+  "org.scalatest" %% "scalatest" % "3.2.17" % Test,
+  "org.mockito" % "mockito-core" % mockitoVersion % Test
+)
 
 // Dependencies
 libraryDependencies ++= Seq(
diff --git 
a/computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingService.scala
 
b/computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingService.scala
index a15ced30a2..6184cf545a 100644
--- 
a/computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingService.scala
+++ 
b/computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingService.scala
@@ -32,6 +32,7 @@ import org.apache.texera.service.resource.{
   ComputingUnitManagingResource,
   HealthCheckResource
 }
+import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
 import java.nio.file.Path
 
 class ComputingUnitManagingService extends 
Application[ComputingUnitManagingServiceConfiguration] {
@@ -53,21 +54,16 @@ class ComputingUnitManagingService extends 
Application[ComputingUnitManagingServ
       configuration: ComputingUnitManagingServiceConfiguration,
       environment: Environment
   ): Unit = {
-    SqlServer.initConnection(
-      StorageConfig.jdbcUrl,
-      StorageConfig.jdbcUsername,
-      StorageConfig.jdbcPassword
-    )
     // Register http resources
     environment.jersey.setUrlPattern("/api/*")
     environment.jersey.register(classOf[HealthCheckResource])
 
-    // Register JWT authentication filter
-    environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))
+    ComputingUnitManagingService.registerAuthFeatures(environment)
 
-    // Enable @Auth annotation for injecting SessionUser
-    environment.jersey.register(
-      new 
io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])
+    SqlServer.initConnection(
+      StorageConfig.jdbcUrl,
+      StorageConfig.jdbcUsername,
+      StorageConfig.jdbcPassword
     )
 
     environment.jersey().register(new ComputingUnitManagingResource)
@@ -79,6 +75,19 @@ class ComputingUnitManagingService extends 
Application[ComputingUnitManagingServ
 }
 
 object ComputingUnitManagingService {
+  // Registers JWT auth, @Auth injection, and @RolesAllowed enforcement.
+  def registerAuthFeatures(environment: Environment): Unit = {
+    // Register JWT authentication filter
+    environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))
+
+    // Enable @Auth annotation for injecting SessionUser
+    environment.jersey.register(
+      new 
io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])
+    )
+
+    // Enforce @RolesAllowed annotations on resource methods
+    environment.jersey.register(classOf[RolesAllowedDynamicFeature])
+  }
 
   def main(args: Array[String]): Unit = {
     val configFilePath = Path
diff --git 
a/computing-unit-managing-service/src/test/scala/org/apache/texera/service/ComputingUnitManagingServiceRunSpec.scala
 
b/computing-unit-managing-service/src/test/scala/org/apache/texera/service/ComputingUnitManagingServiceRunSpec.scala
new file mode 100644
index 0000000000..d27f5725ac
--- /dev/null
+++ 
b/computing-unit-managing-service/src/test/scala/org/apache/texera/service/ComputingUnitManagingServiceRunSpec.scala
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+package org.apache.texera.service
+
+import io.dropwizard.auth.{AuthDynamicFeature, AuthValueFactoryProvider}
+import io.dropwizard.core.setup.Environment
+import io.dropwizard.jersey.setup.JerseyEnvironment
+import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
+import org.mockito.Mockito.{mock, verify, when}
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class ComputingUnitManagingServiceRunSpec extends AnyFlatSpec with Matchers {
+
+  // Verifies that the @RolesAllowed annotations on resource methods are 
actually
+  // enforced by Jersey, which requires RolesAllowedDynamicFeature, 
AuthDynamicFeature,
+  // and AuthValueFactoryProvider.Binder to be registered on the Jersey 
environment.
+  "ComputingUnitManagingService.registerAuthFeatures" should "register auth + 
RolesAllowedDynamicFeature on the Jersey environment" in {
+    val jersey = mock(classOf[JerseyEnvironment])
+    val env = mock(classOf[Environment])
+    when(env.jersey).thenReturn(jersey)
+
+    ComputingUnitManagingService.registerAuthFeatures(env)
+
+    verify(jersey).register(classOf[RolesAllowedDynamicFeature])
+    
verify(jersey).register(org.mockito.ArgumentMatchers.any(classOf[AuthDynamicFeature]))
+    verify(jersey).register(
+      
org.mockito.ArgumentMatchers.any(classOf[AuthValueFactoryProvider.Binder[_]])
+    )
+  }
+}
diff --git 
a/config-service/src/main/scala/org/apache/texera/service/ConfigService.scala 
b/config-service/src/main/scala/org/apache/texera/service/ConfigService.scala
index c787016c27..5b2712f26e 100644
--- 
a/config-service/src/main/scala/org/apache/texera/service/ConfigService.scala
+++ 
b/config-service/src/main/scala/org/apache/texera/service/ConfigService.scala
@@ -31,6 +31,7 @@ import org.apache.texera.config.DefaultsConfig
 import org.apache.texera.dao.SqlServer
 import org.apache.texera.service.resource.{ConfigResource, HealthCheckResource}
 import org.eclipse.jetty.server.session.SessionHandler
+import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
 import org.jooq.impl.DSL
 
 import java.nio.file.Path
@@ -63,13 +64,7 @@ class ConfigService extends 
Application[ConfigServiceConfiguration] with LazyLog
 
     environment.jersey.register(classOf[HealthCheckResource])
 
-    // Register JWT authentication filter
-    environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))
-
-    // Enable @Auth annotation for injecting SessionUser
-    environment.jersey.register(
-      new 
io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])
-    )
+    ConfigService.registerAuthFeatures(environment)
 
     environment.jersey.register(new ConfigResource)
 
@@ -109,6 +104,23 @@ class ConfigService extends 
Application[ConfigServiceConfiguration] with LazyLog
 }
 
 object ConfigService {
+  // Registers JWT auth, @Auth injection, and @RolesAllowed enforcement.
+  // Mirrors ComputingUnitManagingService.registerAuthFeatures and
+  // WorkflowCompilingService.registerAuthFeatures so the three services
+  // don't drift apart.
+  def registerAuthFeatures(environment: Environment): Unit = {
+    // Register JWT authentication filter
+    environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))
+
+    // Enable @Auth annotation for injecting SessionUser
+    environment.jersey.register(
+      new 
io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])
+    )
+
+    // Enforce @RolesAllowed annotations on resource methods
+    environment.jersey.register(classOf[RolesAllowedDynamicFeature])
+  }
+
   def main(args: Array[String]): Unit = {
     val configFilePath = Path
       .of(sys.env.getOrElse("TEXERA_HOME", "."))
diff --git 
a/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala
 
b/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala
index 2614719040..ace8e8618d 100644
--- 
a/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala
+++ 
b/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala
@@ -19,7 +19,7 @@
 
 package org.apache.texera.service.resource
 
-import jakarta.annotation.security.RolesAllowed
+import jakarta.annotation.security.PermitAll
 import jakarta.ws.rs.core.MediaType
 import jakarta.ws.rs.{GET, Path, Produces}
 import org.apache.texera.config.{AuthConfig, ComputingUnitConfig, GuiConfig, 
UserSystemConfig}
@@ -28,8 +28,12 @@ import org.apache.texera.config.{AuthConfig, 
ComputingUnitConfig, GuiConfig, Use
 @Produces(Array(MediaType.APPLICATION_JSON))
 class ConfigResource {
 
+  // These two endpoints are fetched by the frontend during app initialization,
+  // before any login, so they must answer unauthenticated callers — hence 
@PermitAll.
+  // They are the only endpoints in this resource, so role enforcement gates 
nothing
+  // here; @PermitAll is what keeps them reachable when role enforcement is 
enabled.
   @GET
-  @RolesAllowed(Array("REGULAR", "ADMIN"))
+  @PermitAll
   @Path("/gui")
   def getGuiConfig: Map[String, Any] =
     Map(
@@ -64,7 +68,7 @@ class ConfigResource {
     )
 
   @GET
-  @RolesAllowed(Array("REGULAR", "ADMIN"))
+  @PermitAll
   @Path("/user-system")
   def getUserSystemConfig: Map[String, Any] =
     Map(
diff --git 
a/config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala
 
b/config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala
new file mode 100644
index 0000000000..5bbf1ff007
--- /dev/null
+++ 
b/config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+package org.apache.texera.service
+
+import io.dropwizard.auth.{AuthDynamicFeature, AuthValueFactoryProvider}
+import io.dropwizard.core.setup.Environment
+import io.dropwizard.jersey.setup.JerseyEnvironment
+import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
+import org.mockito.Mockito.{mock, verify, when}
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class ConfigServiceRunSpec extends AnyFlatSpec with Matchers {
+
+  // ConfigResource's own endpoints are @PermitAll, but the service still 
registers
+  // RolesAllowedDynamicFeature so that any @RolesAllowed endpoint is enforced 
by
+  // Jersey. This verifies the helper actually runs the three registrations.
+  "ConfigService.registerAuthFeatures" should "register auth + 
RolesAllowedDynamicFeature on the Jersey environment" in {
+    val jersey = mock(classOf[JerseyEnvironment])
+    val env = mock(classOf[Environment])
+    when(env.jersey).thenReturn(jersey)
+
+    ConfigService.registerAuthFeatures(env)
+
+    verify(jersey).register(classOf[RolesAllowedDynamicFeature])
+    
verify(jersey).register(org.mockito.ArgumentMatchers.any(classOf[AuthDynamicFeature]))
+    verify(jersey).register(
+      
org.mockito.ArgumentMatchers.any(classOf[AuthValueFactoryProvider.Binder[_]])
+    )
+  }
+}
diff --git 
a/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceAuthSpec.scala
 
b/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceAuthSpec.scala
new file mode 100644
index 0000000000..56cde8c263
--- /dev/null
+++ 
b/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceAuthSpec.scala
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+package org.apache.texera.service.resource
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.scala.DefaultScalaModule
+import io.dropwizard.jackson.Jackson
+import io.dropwizard.testing.junit5.ResourceExtension
+import jakarta.annotation.security.RolesAllowed
+import jakarta.ws.rs.core.MediaType
+import jakarta.ws.rs.{GET, Path, Produces}
+import org.apache.texera.auth.{JwtAuth, JwtAuthFilter}
+import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum
+import org.apache.texera.dao.jooq.generated.tables.pojos.User
+import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+// Wires ConfigResource through the same Jersey auth pipeline production uses
+// (JwtAuthFilter + RolesAllowedDynamicFeature) and fires HTTP requests with no
+// Authorization header. Regression guard for the bootstrap break that caused
+// PR #5049 to be reverted in #5173: /config/gui and /config/user-system are
+// loaded by the frontend's APP_INITIALIZER before any login, so they must
+// return 200 to unauthenticated callers even with role enforcement enabled.
+class ConfigResourceAuthSpec extends AnyFlatSpec with Matchers with 
BeforeAndAfterAll {
+
+  // Mirror production's mapper: ConfigService bootstraps Dropwizard's default 
mapper
+  // (Jackson.newObjectMapper) and registers DefaultScalaModule on top. Same 
call here.
+  private val testMapper: ObjectMapper =
+    Jackson.newObjectMapper().registerModule(DefaultScalaModule)
+
+  private val resources: ResourceExtension = ResourceExtension
+    .builder()
+    .setMapper(testMapper)
+    .addProvider(classOf[JwtAuthFilter])
+    .addProvider(classOf[RolesAllowedDynamicFeature])
+    .addResource(new ConfigResource)
+    .addResource(new ConfigResourceAuthSpec.ProtectedProbe)
+    .build()
+
+  override protected def beforeAll(): Unit = resources.before()
+  override protected def afterAll(): Unit = resources.after()
+
+  "GET /config/gui" should "return 200 without an Authorization header" in {
+    val response = 
resources.target("/config/gui").request(MediaType.APPLICATION_JSON).get()
+    response.getStatus shouldBe 200
+  }
+
+  "GET /config/user-system" should "return 200 without an Authorization 
header" in {
+    val response =
+      
resources.target("/config/user-system").request(MediaType.APPLICATION_JSON).get()
+    response.getStatus shouldBe 200
+  }
+
+  "GET an @RolesAllowed endpoint" should "return 403 without an Authorization 
header" in {
+    // Sanity: with no SecurityContext set by JwtAuthFilter, 
RolesAllowedDynamicFeature
+    // must reject. Catches the case where the feature is registered but 
somehow
+    // disabled (e.g. swallowed exception during setup).
+    val response =
+      resources.target("/auth-probe").request(MediaType.APPLICATION_JSON).get()
+    response.getStatus shouldBe 403
+  }
+
+  it should "return 200 with a valid Bearer token whose role matches 
@RolesAllowed" in {
+    // Positive-direction sibling to the previous test. Without this, a filter-
+    // priority bug that lets RolesAllowedRequestFilter run *before* 
JwtAuthFilter
+    // is invisible to the spec: the no-auth case still 403s, the @PermitAll 
cases
+    // still 200, and the only path that actually exercises auth → authz 
ordering
+    // is "valid JWT → 200". Manual integration testing of PR #5199 found this:
+    // a real admin JWT was getting 403 on every @RolesAllowed endpoint until
+    // JwtAuthFilter was pinned to Priorities.AUTHENTICATION.
+    val u = new User()
+    u.setUid(1)
+    u.setName("test-admin")
+    u.setEmail("[email protected]")
+    u.setGoogleId(null)
+    u.setRole(UserRoleEnum.ADMIN)
+    val token = JwtAuth.jwtToken(JwtAuth.jwtClaims(u, expireInDays = 1))
+    val response = resources
+      .target("/auth-probe")
+      .request(MediaType.APPLICATION_JSON)
+      .header("Authorization", s"Bearer $token")
+      .get()
+    response.getStatus shouldBe 200
+  }
+}
+
+object ConfigResourceAuthSpec {
+  // A deliberately @RolesAllowed companion to ConfigResource, so the same 
setup also
+  // proves the feature actually rejects when it should — a 200 on the 
@PermitAll
+  // endpoints would otherwise be consistent with the feature being silently 
no-op'd.
+  @Path("/auth-probe")
+  @Produces(Array(MediaType.APPLICATION_JSON))
+  class ProtectedProbe {
+    @GET
+    @RolesAllowed(Array("REGULAR", "ADMIN"))
+    def probe: String = "should never reach this"
+  }
+}
diff --git a/workflow-compiling-service/LICENSE-binary 
b/workflow-compiling-service/LICENSE-binary
index 5b7548a4ed..ed6a9e1d26 100644
--- a/workflow-compiling-service/LICENSE-binary
+++ b/workflow-compiling-service/LICENSE-binary
@@ -281,6 +281,7 @@ Scala/Java jars:
   - commons-pool.commons-pool-1.6.jar
   - dev.failsafe.failsafe-3.3.2.jar
   - io.airlift.aircompressor-0.27.jar
+  - io.dropwizard.dropwizard-auth-4.0.7.jar
   - io.dropwizard.dropwizard-configuration-4.0.7.jar
   - io.dropwizard.dropwizard-core-4.0.7.jar
   - io.dropwizard.dropwizard-health-4.0.7.jar
@@ -296,6 +297,7 @@ Scala/Java jars:
   - io.dropwizard.dropwizard-validation-4.0.7.jar
   - io.dropwizard.logback.logback-throttling-appender-1.4.2.jar
   - io.dropwizard.metrics.metrics-annotation-4.2.25.jar
+  - io.dropwizard.metrics.metrics-caffeine-4.2.25.jar
   - io.dropwizard.metrics.metrics-core-4.2.25.jar
   - io.dropwizard.metrics.metrics-healthchecks-4.2.25.jar
   - io.dropwizard.metrics.metrics-jakarta-servlets-4.2.25.jar
@@ -419,6 +421,7 @@ Scala/Java jars:
   - org.apache.yetus.audience-annotations-0.13.0.jar
   - org.apache.zookeeper.zookeeper-3.5.6.jar
   - org.apache.zookeeper.zookeeper-jute-3.5.6.jar
+  - org.bitbucket.b_c.jose4j-0.9.6.jar
   - org.eclipse.jetty.jetty-http-11.0.20.jar
   - org.eclipse.jetty.jetty-io-11.0.20.jar
   - org.eclipse.jetty.jetty-security-11.0.20.jar
diff --git a/workflow-compiling-service/build.sbt 
b/workflow-compiling-service/build.sbt
index 9560751d00..95a6926992 100644
--- a/workflow-compiling-service/build.sbt
+++ b/workflow-compiling-service/build.sbt
@@ -84,5 +84,6 @@ libraryDependencies ++= Seq(
 // Core Dependencies
 libraryDependencies ++= Seq(
   "io.dropwizard" % "dropwizard-core" % dropwizardVersion,
+  "io.dropwizard" % "dropwizard-auth" % dropwizardVersion, // Dropwizard 
Authentication module
   "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.18.6"
 )
diff --git 
a/workflow-compiling-service/src/main/scala/org/apache/texera/service/WorkflowCompilingService.scala
 
b/workflow-compiling-service/src/main/scala/org/apache/texera/service/WorkflowCompilingService.scala
index 40fb3a2dd8..8dc573aaf8 100644
--- 
a/workflow-compiling-service/src/main/scala/org/apache/texera/service/WorkflowCompilingService.scala
+++ 
b/workflow-compiling-service/src/main/scala/org/apache/texera/service/WorkflowCompilingService.scala
@@ -20,14 +20,17 @@
 package org.apache.texera.service
 
 import com.fasterxml.jackson.module.scala.DefaultScalaModule
+import io.dropwizard.auth.AuthDynamicFeature
 import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, 
SubstitutingSourceProvider}
 import io.dropwizard.core.Application
 import io.dropwizard.core.setup.{Bootstrap, Environment}
 import org.apache.texera.amber.config.StorageConfig
 import org.apache.texera.amber.util.ObjectMapperUtils
+import org.apache.texera.auth.{JwtAuthFilter, SessionUser}
 import org.apache.texera.dao.SqlServer
 import org.apache.texera.service.resource.{HealthCheckResource, 
WorkflowCompilationResource}
 import org.eclipse.jetty.servlet.FilterHolder
+import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
 
 import java.nio.file.Path
 
@@ -53,14 +56,16 @@ class WorkflowCompilingService extends 
Application[WorkflowCompilingServiceConfi
     // serve backend at /api
     environment.jersey.setUrlPattern("/api/*")
 
+    environment.jersey.register(classOf[HealthCheckResource])
+
+    WorkflowCompilingService.registerAuthFeatures(environment)
+
     SqlServer.initConnection(
       StorageConfig.jdbcUrl,
       StorageConfig.jdbcUsername,
       StorageConfig.jdbcPassword
     )
 
-    environment.jersey.register(classOf[HealthCheckResource])
-
     // register the compilation endpoint
     environment.jersey.register(classOf[WorkflowCompilationResource])
 
@@ -90,6 +95,20 @@ class WorkflowCompilingService extends 
Application[WorkflowCompilingServiceConfi
 }
 
 object WorkflowCompilingService {
+  // Registers JWT auth, @Auth injection, and @RolesAllowed enforcement.
+  def registerAuthFeatures(environment: Environment): Unit = {
+    // Register JWT authentication filter
+    environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))
+
+    // Enable @Auth annotation for injecting SessionUser
+    environment.jersey.register(
+      new 
io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])
+    )
+
+    // Enforce @RolesAllowed annotations on resource methods
+    environment.jersey.register(classOf[RolesAllowedDynamicFeature])
+  }
+
   def main(args: Array[String]): Unit = {
     // set the configuration file's path
     val configFilePath = Path
diff --git 
a/workflow-compiling-service/src/test/scala/org/apache/texera/service/WorkflowCompilingServiceRunSpec.scala
 
b/workflow-compiling-service/src/test/scala/org/apache/texera/service/WorkflowCompilingServiceRunSpec.scala
new file mode 100644
index 0000000000..ff5da1b561
--- /dev/null
+++ 
b/workflow-compiling-service/src/test/scala/org/apache/texera/service/WorkflowCompilingServiceRunSpec.scala
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+package org.apache.texera.service
+
+import io.dropwizard.auth.{AuthDynamicFeature, AuthValueFactoryProvider}
+import io.dropwizard.core.setup.Environment
+import io.dropwizard.jersey.setup.JerseyEnvironment
+import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
+import org.mockito.Mockito.{mock, verify, when}
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class WorkflowCompilingServiceRunSpec extends AnyFlatSpec with Matchers {
+
+  // Verifies that the @RolesAllowed annotations on resource methods are 
actually
+  // enforced by Jersey, which requires RolesAllowedDynamicFeature, 
AuthDynamicFeature,
+  // and AuthValueFactoryProvider.Binder to be registered on the Jersey 
environment.
+  "WorkflowCompilingService.registerAuthFeatures" should "register auth + 
RolesAllowedDynamicFeature on the Jersey environment" in {
+    val jersey = mock(classOf[JerseyEnvironment])
+    val env = mock(classOf[Environment])
+    when(env.jersey).thenReturn(jersey)
+
+    WorkflowCompilingService.registerAuthFeatures(env)
+
+    verify(jersey).register(classOf[RolesAllowedDynamicFeature])
+    
verify(jersey).register(org.mockito.ArgumentMatchers.any(classOf[AuthDynamicFeature]))
+    verify(jersey).register(
+      
org.mockito.ArgumentMatchers.any(classOf[AuthValueFactoryProvider.Binder[_]])
+    )
+  }
+}


Reply via email to