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

github-merge-queue[bot] 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 d1cf4ebde7 fix: enforce @RolesAllowed on microservice resources (#5049)
d1cf4ebde7 is described below

commit d1cf4ebde7f49346533e3c9506013f97c7fef88f
Author: Matthew B. <[email protected]>
AuthorDate: Sat May 23 15:13:10 2026 -0700

    fix: enforce @RolesAllowed on microservice resources (#5049)
    
    ### What changes were proposed in this PR?
    `@RolesAllowed` annotations on `config-service`,
    `computing-unit-managing-service`, and `workflow-compiling-service`
    resources were decorative because none of these services registered
    Jersey's `RolesAllowedDynamicFeature`. This PR registers that feature in
    each service's `run(...)`. For `workflow-compiling-service`, which was
    not registering JWT auth at all, this PR also registers
    `AuthDynamicFeature(JwtAuthFilter)` and the `SessionUser`
    `AuthValueFactoryProvider.Binder`, and adds `Auth` as an sbt dependency
    for the module. `access-control-service` and `file-service` use no
    `@RolesAllowed` today and were intentionally left alone to keep the
    change minimal.
      ### Any related issues, documentation, or discussions?
      Closes: #4904
      ### How was this PR tested?
    Added `ConfigServiceRunSpec` (mirrors `AccessControlServiceRunSpec`)
    that mocks the Jersey environment and verifies
    `RolesAllowedDynamicFeature` is registered when `ConfigService.run`
    runs. The same one-line registration applies to the other two services;
    tests there would require either refactoring `SqlServer.initConnection`
    out of `run` or static-mocking the Scala `SqlServer` object, both of
    which are larger than the fix itself, so they are out of scope. Manual
    verification via the reproduction in the issue (low-role JWT against an
    annotated endpoint should now return 403; unauthenticated request to
    `WorkflowCompilationResource` should now return 401).
      ### Was this PR authored or co-authored using generative AI tooling?
      Co-authored with Claude Opus 4.7 in compliance with ASF
---
 build.sbt                                          |  2 +-
 computing-unit-managing-service/build.sbt          |  7 +++
 .../service/ComputingUnitManagingService.scala     | 29 ++++++++----
 .../ComputingUnitManagingServiceRunSpec.scala      | 48 +++++++++++++++++++
 .../org/apache/texera/service/ConfigService.scala  |  4 ++
 .../texera/service/ConfigServiceRunSpec.scala      | 55 ++++++++++++++++++++++
 workflow-compiling-service/LICENSE-binary          |  3 ++
 workflow-compiling-service/build.sbt               |  1 +
 .../texera/service/WorkflowCompilingService.scala  | 23 ++++++++-
 .../service/WorkflowCompilingServiceRunSpec.scala  | 48 +++++++++++++++++++
 10 files changed, 207 insertions(+), 13 deletions(-)

diff --git a/build.sbt b/build.sbt
index b7b6b3cfb2..8b38e2e009 100644
--- a/build.sbt
+++ b/build.sbt
@@ -113,7 +113,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/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..545aac494b 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
@@ -71,6 +72,9 @@ class ConfigService extends 
Application[ConfigServiceConfiguration] with LazyLog
       new 
io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])
     )
 
+    // Enforce @RolesAllowed annotations on resource methods
+    environment.jersey.register(classOf[RolesAllowedDynamicFeature])
+
     environment.jersey.register(new ConfigResource)
 
     // Preload default.conf into site_setting tables
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..e3982e3775
--- /dev/null
+++ 
b/config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala
@@ -0,0 +1,55 @@
+/*
+ * 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.core.setup.Environment
+import io.dropwizard.jersey.setup.JerseyEnvironment
+import io.dropwizard.jetty.MutableServletContextHandler
+import io.dropwizard.jetty.setup.ServletEnvironment
+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 {
+
+  // Verifies that the @RolesAllowed annotations on ConfigResource are actually
+  // enforced by Jersey, which requires RolesAllowedDynamicFeature to be
+  // registered on the Jersey environment.
+  "ConfigService.run" should "register RolesAllowedDynamicFeature on the 
Jersey environment" in {
+    val jersey = mock(classOf[JerseyEnvironment])
+    val servlets = mock(classOf[ServletEnvironment])
+    val context = mock(classOf[MutableServletContextHandler])
+    val env = mock(classOf[Environment])
+    when(env.jersey).thenReturn(jersey)
+    when(env.servlets).thenReturn(servlets)
+    when(env.getApplicationContext).thenReturn(context)
+
+    val service = new ConfigService
+    // run() reaches into SqlServer near the end to preload defaults; that 
throws
+    // here because no real DB is wired up. By that point all Jersey 
registrations
+    // have already executed, so the verification below is still valid.
+    intercept[Exception] {
+      service.run(mock(classOf[ConfigServiceConfiguration]), env)
+    }
+
+    verify(jersey).register(classOf[RolesAllowedDynamicFeature])
+  }
+}
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