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 71fce70c3d feat(auth): enforce role annotation coverage at service 
startup (#5743)
71fce70c3d is described below

commit 71fce70c3d8e58e8247a85e2927260fa0d5e2dcd
Author: Matthew B. <[email protected]>
AuthorDate: Wed Jun 24 14:40:14 2026 -0700

    feat(auth): enforce role annotation coverage at service startup (#5743)
    
    ### What changes were proposed in this PR?
    - Add `RoleAnnotationEnforcer` in `common/auth`: a pure,
    reflection-based check whose `findUnannotatedEndpoints` flags every
    HTTP-mapped resource method (detected generically via the JAX-RS
    `@HttpMethod` meta-annotation, covering
    GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS and custom verbs) that lacks
    `@RolesAllowed`/`@PermitAll`/`@DenyAll` at either the method or its
    resource class.
    - Wire all five #5199 microservices (config, access-control,
    computing-unit-managing, workflow-compiling, file) to call
    `enforceRoleAnnotations(environment)` once every resource has been
    registered in `run()`, reading the live Jersey `ResourceConfig`; an
    unannotated endpoint now logs and throws `IllegalStateException` so the
    service fails to boot instead of exposing a silent public endpoint.
    - Add `RoleAnnotationEnforcerSpec` for the enforcer logic plus a
    per-service guard test (over each service's real registered resources)
    so a forgotten annotation fails in CI without needing a full boot.
    - Scope is the five microservices only; amber is intentionally left out
    because its pre-existing endpoints are not all annotated yet and would
    fail the check (a separate cleanup).
    ### Any related issues, documentation, discussions?
    Closes: #5742
    ### How was this PR tested?
    - Run `sbt "Auth/testOnly *RoleAnnotationEnforcerSpec"`, expect 16 cases
    green. Coverage of `findUnannotatedEndpoints`: all-annotated passes; an
    unannotated `@GET` is flagged; class-level annotation covers methods;
    `@PermitAll`/`@DenyAll` accepted; non-HTTP methods ignored; no resources
    returns empty; holes across multiple resources reported as
    fully-qualified `Class#method`; verbs beyond GET/POST/DELETE
    (`@PUT`/`@PATCH`/`@HEAD`/`@OPTIONS`) detected via the `@HttpMethod`
    meta-annotation; a security annotation inherited from a superclass
    method counts as covered; a subclass class-level annotation covers an
    inherited endpoint; an inherited unannotated endpoint is flagged against
    the scanned subclass; duplicate resources are de-duplicated. Coverage of
    `enforce`: throws (message names the service and offending method, and
    lists every offender) and does not throw on clean input or empty input.
    Verified locally.
    - Run `sbt "ConfigService/testOnly *ConfigServiceRunSpec"` (and the
    equivalent
    
`AccessControlService`/`ComputingUnitManagingService`/`WorkflowCompilingService`/`FileService`
    RunSpecs); expect the new "registered resources should all declare
    access control" guard to pass, confirming each service's real endpoints
    are fully annotated.
    - Reviewer check for the regression: drop a new `@GET` with no role
    annotation onto any wired resource and start the service (or run that
    service's RunSpec); expect an `IllegalStateException` naming
    `Class#method` and a failed boot.
    - Local environment limitation: this machine runs JDK 25, where the
    repo's existing Mockito `*RunSpec` cases cannot mock `JerseyEnvironment`
    and file-service tests hit a JaCoCo 0.8.11 instrumentation crash on an
    unrelated class; these are pre-existing toolchain issues (baseline fails
    identically) and run on CI's supported JDK. The new non-mock guard tests
    were verified locally for
    config/access-control/computing-unit-managing/workflow-compiling.
    ### Was this PR authored or co-authored using generative AI tooling?
    Co-authored with Claude Opus 4.8 in compliance with ASF
    
    ---------
    
    Signed-off-by: Matthew B. <[email protected]>
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../texera/service/AccessControlService.scala      |  23 +-
 .../service/AccessControlServiceRunSpec.scala      |  22 +-
 common/auth/build.sbt                              |   6 +-
 .../org/apache/texera/auth/AuthFeatures.scala      |  46 ++++
 .../texera/auth/RoleAnnotationEnforcer.scala       |  87 +++++++
 .../org/apache/texera/auth/AuthFeaturesSpec.scala  |  16 +-
 .../texera/auth/RoleAnnotationEnforcerSpec.scala   | 251 +++++++++++++++++++++
 .../service/ComputingUnitManagingService.scala     |  31 +--
 .../ComputingUnitManagingServiceRunSpec.scala      |  37 ++-
 .../org/apache/texera/service/ConfigService.scala  |  31 +--
 .../texera/service/ConfigServiceRunSpec.scala      |  29 +--
 .../org/apache/texera/service/FileService.scala    |  23 +-
 .../apache/texera/service/FileServiceRunSpec.scala |  43 ++++
 .../texera/service/WorkflowCompilingService.scala  |  26 +--
 .../service/WorkflowCompilingServiceRunSpec.scala  |  29 +--
 15 files changed, 516 insertions(+), 184 deletions(-)

diff --git 
a/access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala
 
b/access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala
index f01d06f941..1f50c86c9f 100644
--- 
a/access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala
+++ 
b/access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala
@@ -19,17 +19,11 @@ package org.apache.texera.service
 
 import com.fasterxml.jackson.module.scala.DefaultScalaModule
 import com.typesafe.scalalogging.LazyLogging
-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.common.config.StorageConfig
-import org.apache.texera.auth.{
-  JwtAuthFilter,
-  RequestLoggingFilter,
-  SessionUser,
-  UnauthorizedExceptionMapper
-}
+import org.apache.texera.auth.{AuthFeatures, RequestLoggingFilter, 
RoleAnnotationEnforcer}
 import org.apache.texera.dao.SqlServer
 import org.apache.texera.service.activity.UserActivityEventListener
 import org.apache.texera.service.resource.{
@@ -39,7 +33,6 @@ import org.apache.texera.service.resource.{
   LiteLLMProxyResource
 }
 import org.eclipse.jetty.server.session.SessionHandler
-import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
 import java.nio.file.Path
 
 class AccessControlService extends 
Application[AccessControlServiceConfiguration] with LazyLogging {
@@ -76,17 +69,7 @@ class AccessControlService extends 
Application[AccessControlServiceConfiguration
     environment.jersey.register(classOf[LiteLLMProxyResource])
     environment.jersey.register(classOf[LiteLLMModelsResource])
 
-    // Register JWT authentication filter
-    environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))
-    environment.jersey.register(classOf[UnauthorizedExceptionMapper])
-
-    // Enable @Auth annotation for injecting SessionUser
-    environment.jersey.register(
-      new 
io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])
-    )
-
-    // Required for @RolesAllowed on resources to be enforced.
-    environment.jersey.register(classOf[RolesAllowedDynamicFeature])
+    AuthFeatures.register(environment)
 
     // Record USER_LAST_ACTIVE_TIME on every matched, completed request.
     // Lives only in this service because authenticated client sessions
@@ -94,6 +77,8 @@ class AccessControlService extends 
Application[AccessControlServiceConfiguration
     // with high recall.
     environment.jersey.register(new UserActivityEventListener())
 
+    RoleAnnotationEnforcer.enforce(environment.jersey.getResourceConfig, 
"AccessControlService")
+
     // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL
     RequestLoggingFilter.register(environment.getApplicationContext)
   }
diff --git 
a/access-control-service/src/test/scala/org/apache/texera/service/AccessControlServiceRunSpec.scala
 
b/access-control-service/src/test/scala/org/apache/texera/service/AccessControlServiceRunSpec.scala
index 2460d18b45..04443ab9c6 100644
--- 
a/access-control-service/src/test/scala/org/apache/texera/service/AccessControlServiceRunSpec.scala
+++ 
b/access-control-service/src/test/scala/org/apache/texera/service/AccessControlServiceRunSpec.scala
@@ -20,11 +20,18 @@
 package org.apache.texera.service
 
 import io.dropwizard.core.setup.Environment
+import io.dropwizard.jersey.DropwizardResourceConfig
 import io.dropwizard.jersey.setup.JerseyEnvironment
 import io.dropwizard.jetty.MutableServletContextHandler
 import io.dropwizard.jetty.setup.ServletEnvironment
-import org.apache.texera.auth.UnauthorizedExceptionMapper
+import org.apache.texera.auth.{RoleAnnotationEnforcer, 
UnauthorizedExceptionMapper}
 import org.apache.texera.service.activity.UserActivityEventListener
+import org.apache.texera.service.resource.{
+  AccessControlResource,
+  HealthCheckResource,
+  LiteLLMModelsResource,
+  LiteLLMProxyResource
+}
 import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
 import org.mockito.ArgumentMatchers.isA
 import org.mockito.Mockito.{mock, verify, when}
@@ -41,6 +48,7 @@ class AccessControlServiceRunSpec extends AnyFlatSpec with 
Matchers {
     when(env.jersey).thenReturn(jersey)
     when(env.servlets).thenReturn(servlets)
     when(env.getApplicationContext).thenReturn(context)
+    
when(jersey.getResourceConfig).thenReturn(DropwizardResourceConfig.forTesting())
 
     val service = new AccessControlService
     service.run(mock(classOf[AccessControlServiceConfiguration]), env)
@@ -51,4 +59,16 @@ class AccessControlServiceRunSpec extends AnyFlatSpec with 
Matchers {
     verify(jersey).register(classOf[RolesAllowedDynamicFeature])
     verify(jersey).setUrlPattern("/api/*")
   }
+
+  // Every endpoint this service registers declares 
@RolesAllowed/@PermitAll/@DenyAll.
+  "AccessControlService's registered resources" should "all declare access 
control" in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(
+        classOf[AccessControlResource],
+        classOf[LiteLLMProxyResource],
+        classOf[LiteLLMModelsResource],
+        classOf[HealthCheckResource]
+      )
+    ) shouldBe empty
+  }
 }
diff --git a/common/auth/build.sbt b/common/auth/build.sbt
index 742cf95eea..4f325bf77e 100644
--- a/common/auth/build.sbt
+++ b/common/auth/build.sbt
@@ -60,5 +60,9 @@ libraryDependencies ++= Seq(
   "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
+  "org.glassfish.jersey.core" % "jersey-server" % "3.0.12" % "provided", // 
for RoleAnnotationEnforcer's ResourceConfig overload and AuthFeatures' 
RolesAllowedDynamicFeature
+  "io.dropwizard" % "dropwizard-core" % "4.0.7" % "provided",          // for 
AuthFeatures' Environment
+  "io.dropwizard" % "dropwizard-auth" % "4.0.7" % "provided",          // for 
AuthFeatures' AuthDynamicFeature/AuthValueFactoryProvider
+  "org.scalatest" %% "scalatest" % "3.2.17" % Test,
+  "org.mockito" % "mockito-core" % "5.4.0" % Test                      // for 
mocking the Jersey environment in AuthFeaturesSpec
 )
\ No newline at end of file
diff --git 
a/common/auth/src/main/scala/org/apache/texera/auth/AuthFeatures.scala 
b/common/auth/src/main/scala/org/apache/texera/auth/AuthFeatures.scala
new file mode 100644
index 0000000000..ab5d8b7cde
--- /dev/null
+++ b/common/auth/src/main/scala/org/apache/texera/auth/AuthFeatures.scala
@@ -0,0 +1,46 @@
+/*
+ * 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.auth
+
+import io.dropwizard.auth.{AuthDynamicFeature, AuthValueFactoryProvider}
+import io.dropwizard.core.setup.Environment
+import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
+
+/** Registers the standard Texera auth stack on a Dropwizard service: JWT
+  * authentication, `@Auth` SessionUser injection, and `@RolesAllowed`
+  * enforcement. Shared by every service so the registrations don't drift 
apart.
+  */
+object AuthFeatures {
+
+  /** Register JWT auth, the `@Auth` value factory, and the `@RolesAllowed`
+    * dynamic feature on `environment`'s Jersey config.
+    */
+  def register(environment: Environment): Unit = {
+    // Register JWT authentication filter
+    environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))
+    environment.jersey.register(classOf[UnauthorizedExceptionMapper])
+
+    // Enable @Auth annotation for injecting SessionUser
+    environment.jersey.register(new 
AuthValueFactoryProvider.Binder(classOf[SessionUser]))
+
+    // Enforce @RolesAllowed annotations on resource methods
+    environment.jersey.register(classOf[RolesAllowedDynamicFeature])
+  }
+}
diff --git 
a/common/auth/src/main/scala/org/apache/texera/auth/RoleAnnotationEnforcer.scala
 
b/common/auth/src/main/scala/org/apache/texera/auth/RoleAnnotationEnforcer.scala
new file mode 100644
index 0000000000..d99a03ee12
--- /dev/null
+++ 
b/common/auth/src/main/scala/org/apache/texera/auth/RoleAnnotationEnforcer.scala
@@ -0,0 +1,87 @@
+/*
+ * 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.auth
+
+import com.typesafe.scalalogging.LazyLogging
+import jakarta.annotation.security.{DenyAll, PermitAll, RolesAllowed}
+import jakarta.ws.rs.HttpMethod
+import org.glassfish.jersey.server.ResourceConfig
+
+import java.lang.reflect.Method
+import scala.jdk.CollectionConverters._
+
+/** Scans Jersey resource classes and fails if any HTTP-mapped method lacks an
+  * @RolesAllowed/@PermitAll/@DenyAll annotation at the method or class level.
+  */
+object RoleAnnotationEnforcer extends LazyLogging {
+
+  private val securityAnnotations: Seq[Class[_ <: 
java.lang.annotation.Annotation]] =
+    Seq(classOf[RolesAllowed], classOf[PermitAll], classOf[DenyAll])
+
+  /** Enforce over every resource registered on `resourceConfig`, both
+    * `getClasses` and singleton `getInstances`.
+    */
+  def enforce(resourceConfig: ResourceConfig, serviceName: String): Unit =
+    enforce(
+      resourceConfig.getClasses.asScala.toSet ++
+        resourceConfig.getInstances.asScala.map(_.getClass),
+      serviceName
+    )
+
+  /** Scans `resourceClasses` and throws if any HTTP-mapped method is missing 
an
+    * access-control annotation, after logging the offending methods.
+    */
+  def enforce(resourceClasses: Iterable[Class[_]], serviceName: String): Unit 
= {
+    val violations = findUnannotatedEndpoints(resourceClasses)
+    if (violations.nonEmpty) {
+      val message =
+        s"$serviceName has HTTP endpoint(s) without an 
@RolesAllowed/@PermitAll/@DenyAll " +
+          s"annotation; every endpoint must declare its access control 
explicitly:\n  " +
+          violations.mkString("\n  ")
+      logger.error(message)
+      throw new IllegalStateException(message)
+    }
+  }
+
+  /** Returns `Class#method` identifiers for every HTTP-mapped method that 
lacks
+    * a security annotation at either the method or its declaring resource 
class.
+    */
+  def findUnannotatedEndpoints(resourceClasses: Iterable[Class[_]]): 
Seq[String] =
+    resourceClasses.toSeq.flatMap { resourceClass =>
+      val classSecured = hasSecurityAnnotation(resourceClass)
+      resourceClass.getMethods.toSeq
+        .filter(isHttpMethod)
+        .filterNot(method => classSecured || hasSecurityAnnotation(method))
+        .map(method => s"${resourceClass.getName}#${method.getName}")
+    }.distinct
+
+  /** A method is HTTP-mapped if one of its annotations is itself 
meta-annotated
+    * with `@HttpMethod` (covers `@GET`/`@POST`/`@PUT`/`@DELETE`/`@PATCH`/
+    * `@HEAD`/`@OPTIONS` and any custom verb).
+    */
+  private def isHttpMethod(method: Method): Boolean =
+    
method.getAnnotations.exists(_.annotationType.isAnnotationPresent(classOf[HttpMethod]))
+
+  private def hasSecurityAnnotation(method: Method): Boolean =
+    securityAnnotations.exists(method.isAnnotationPresent)
+
+  private def hasSecurityAnnotation(clazz: Class[_]): Boolean =
+    securityAnnotations.exists(clazz.isAnnotationPresent)
+}
diff --git 
a/config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala
 b/common/auth/src/test/scala/org/apache/texera/auth/AuthFeaturesSpec.scala
similarity index 74%
copy from 
config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala
copy to common/auth/src/test/scala/org/apache/texera/auth/AuthFeaturesSpec.scala
index 388a48136a..a5c3c11a44 100644
--- 
a/config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala
+++ b/common/auth/src/test/scala/org/apache/texera/auth/AuthFeaturesSpec.scala
@@ -17,28 +17,28 @@
  * under the License.
  */
 
-package org.apache.texera.service
+package org.apache.texera.auth
 
 import io.dropwizard.auth.{AuthDynamicFeature, AuthValueFactoryProvider}
 import io.dropwizard.core.setup.Environment
 import io.dropwizard.jersey.setup.JerseyEnvironment
-import org.apache.texera.auth.UnauthorizedExceptionMapper
 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 {
+class AuthFeaturesSpec 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 {
+  // Enforcing @RolesAllowed on resource methods requires 
RolesAllowedDynamicFeature,
+  // AuthDynamicFeature, the @Auth value factory, and the 
UnauthorizedExceptionMapper
+  // to be registered on the Jersey environment. Every service shares this 
helper, so
+  // the registrations are verified once here rather than per service.
+  "AuthFeatures.register" 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)
+    AuthFeatures.register(env)
 
     verify(jersey).register(classOf[RolesAllowedDynamicFeature])
     verify(jersey).register(classOf[UnauthorizedExceptionMapper])
diff --git 
a/common/auth/src/test/scala/org/apache/texera/auth/RoleAnnotationEnforcerSpec.scala
 
b/common/auth/src/test/scala/org/apache/texera/auth/RoleAnnotationEnforcerSpec.scala
new file mode 100644
index 0000000000..7513646fe3
--- /dev/null
+++ 
b/common/auth/src/test/scala/org/apache/texera/auth/RoleAnnotationEnforcerSpec.scala
@@ -0,0 +1,251 @@
+/*
+ * 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.auth
+
+import ch.qos.logback.classic.{Level, Logger => LogbackLogger}
+import jakarta.annotation.security.{DenyAll, PermitAll, RolesAllowed}
+import jakarta.ws.rs.{DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT}
+import org.glassfish.jersey.server.ResourceConfig
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+import org.slf4j.LoggerFactory
+
+class RoleAnnotationEnforcerSpec extends AnyFlatSpec with Matchers {
+
+  "findUnannotatedEndpoints" should "return nothing when every HTTP method is 
annotated" in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(classOf[RoleAnnotationEnforcerSpec.FullyAnnotatedResource])
+    ) shouldBe empty
+  }
+
+  it should "flag an HTTP method with no security annotation" in {
+    val violations =
+      RoleAnnotationEnforcer.findUnannotatedEndpoints(
+        Seq(classOf[RoleAnnotationEnforcerSpec.PartiallyAnnotatedResource])
+      )
+    violations should have size 1
+    violations.head should endWith("#openEndpoint")
+  }
+
+  it should "treat a class-level annotation as covering every method" in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(classOf[RoleAnnotationEnforcerSpec.ClassLevelResource])
+    ) shouldBe empty
+  }
+
+  it should "accept @PermitAll and @DenyAll, not only @RolesAllowed" in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(classOf[RoleAnnotationEnforcerSpec.PermitAndDenyResource])
+    ) shouldBe empty
+  }
+
+  it should "ignore methods that are not HTTP-mapped" in {
+    // helper has no @RolesAllowed but is not a JAX-RS endpoint, so it is not 
a hole
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(classOf[RoleAnnotationEnforcerSpec.NonEndpointMethodResource])
+    ) shouldBe empty
+  }
+
+  it should "return nothing when given no resources" in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(Seq.empty) shouldBe empty
+  }
+
+  it should "report every hole across multiple resources as fully-qualified 
Class#method" in {
+    val violations = RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(
+        classOf[RoleAnnotationEnforcerSpec.PartiallyAnnotatedResource],
+        classOf[RoleAnnotationEnforcerSpec.MultiHoleResource]
+      )
+    )
+    violations should contain allOf (
+      
s"${classOf[RoleAnnotationEnforcerSpec.PartiallyAnnotatedResource].getName}#openEndpoint",
+      s"${classOf[RoleAnnotationEnforcerSpec.MultiHoleResource].getName}#put",
+      s"${classOf[RoleAnnotationEnforcerSpec.MultiHoleResource].getName}#patch"
+    )
+  }
+
+  it should "detect verbs beyond GET/POST/DELETE via the @HttpMethod 
meta-annotation" in {
+    val violations = RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(classOf[RoleAnnotationEnforcerSpec.AllVerbsUnannotatedResource])
+    )
+    violations.map(_.split("#").last) should contain theSameElementsAs
+      Seq("put", "patch", "head", "options")
+  }
+
+  it should "treat a security annotation inherited from a superclass method as 
covering it" in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(classOf[RoleAnnotationEnforcerSpec.InheritsAnnotatedEndpoint])
+    ) shouldBe empty
+  }
+
+  it should "let a subclass class-level annotation cover an inherited 
unannotated endpoint" in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(classOf[RoleAnnotationEnforcerSpec.SecuredSubclass])
+    ) shouldBe empty
+  }
+
+  it should "flag an inherited unannotated endpoint against the scanned 
subclass" in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(classOf[RoleAnnotationEnforcerSpec.UnsecuredSubclass])
+    ) should contain(
+      
s"${classOf[RoleAnnotationEnforcerSpec.UnsecuredSubclass].getName}#inheritedWrite"
+    )
+  }
+
+  it should "deduplicate when the same resource is scanned more than once" in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      
Seq.fill(3)(classOf[RoleAnnotationEnforcerSpec.PartiallyAnnotatedResource])
+    ) should have size 1
+  }
+
+  "enforce" should "throw when an endpoint is unannotated" in {
+    val ex = intercept[IllegalStateException] {
+      RoleAnnotationEnforcer.enforce(
+        Seq(classOf[RoleAnnotationEnforcerSpec.PartiallyAnnotatedResource]),
+        "TestService"
+      )
+    }
+    ex.getMessage should include("TestService")
+    ex.getMessage should include("openEndpoint")
+  }
+
+  "enforce" should "not throw when every endpoint is annotated" in {
+    noException should be thrownBy RoleAnnotationEnforcer.enforce(
+      Seq(classOf[RoleAnnotationEnforcerSpec.FullyAnnotatedResource]),
+      "TestService"
+    )
+  }
+
+  it should "list every offending endpoint in the thrown message" in {
+    val ex = intercept[IllegalStateException] {
+      RoleAnnotationEnforcer.enforce(
+        Seq(classOf[RoleAnnotationEnforcerSpec.MultiHoleResource]),
+        "TestService"
+      )
+    }
+    ex.getMessage should include("#put")
+    ex.getMessage should include("#patch")
+  }
+
+  it should "not throw when given no resources" in {
+    noException should be thrownBy RoleAnnotationEnforcer.enforce(Seq.empty, 
"TestService")
+  }
+
+  "enforce(ResourceConfig)" should "pass when every registered resource is 
annotated" in {
+    val resourceConfig = new ResourceConfig()
+    
resourceConfig.register(classOf[RoleAnnotationEnforcerSpec.FullyAnnotatedResource])
+    noException should be thrownBy 
RoleAnnotationEnforcer.enforce(resourceConfig, "TestService")
+  }
+
+  it should "throw when a registered resource class has an unannotated 
endpoint" in {
+    val resourceConfig = new ResourceConfig()
+    
resourceConfig.register(classOf[RoleAnnotationEnforcerSpec.PartiallyAnnotatedResource])
+    val ex = intercept[IllegalStateException] {
+      RoleAnnotationEnforcer.enforce(resourceConfig, "TestService")
+    }
+    ex.getMessage should include("TestService")
+    ex.getMessage should include("openEndpoint")
+  }
+
+  it should "throw when a resource registered as an instance has an 
unannotated endpoint" in {
+    val resourceConfig = new ResourceConfig()
+    resourceConfig.register(new 
RoleAnnotationEnforcerSpec.PartiallyAnnotatedResource)
+    an[IllegalStateException] should be thrownBy
+      RoleAnnotationEnforcer.enforce(resourceConfig, "TestService")
+  }
+
+  it should "still fail closed when error logging is disabled" in {
+    // enforce logs the violation at error level before throwing. Even with 
error
+    // logging suppressed, enforcement must still throw rather than silently 
pass.
+    val backendLogger = LoggerFactory
+      .getLogger(RoleAnnotationEnforcer.getClass.getName)
+      .asInstanceOf[LogbackLogger]
+    val previousLevel = backendLogger.getLevel
+    backendLogger.setLevel(Level.OFF)
+    try {
+      an[IllegalStateException] should be thrownBy
+        RoleAnnotationEnforcer.enforce(
+          Seq(classOf[RoleAnnotationEnforcerSpec.PartiallyAnnotatedResource]),
+          "TestService"
+        )
+    } finally {
+      backendLogger.setLevel(previousLevel)
+    }
+  }
+}
+
+object RoleAnnotationEnforcerSpec {
+
+  class FullyAnnotatedResource {
+    @GET @RolesAllowed(Array("REGULAR")) def read: String = ""
+    @POST @PermitAll def create: String = ""
+  }
+
+  class PartiallyAnnotatedResource {
+    @GET @RolesAllowed(Array("ADMIN")) def securedEndpoint: String = ""
+    @POST def openEndpoint: String = ""
+  }
+
+  @RolesAllowed(Array("ADMIN"))
+  class ClassLevelResource {
+    @GET def read: String = ""
+    @DELETE def remove: String = ""
+  }
+
+  class PermitAndDenyResource {
+    @PermitAll @GET def open: String = ""
+    @DenyAll @POST def closed: String = ""
+  }
+
+  class NonEndpointMethodResource {
+    @GET @RolesAllowed(Array("REGULAR")) def read: String = ""
+    def helper: String = ""
+  }
+
+  // One secured endpoint plus two holes on distinct verbs.
+  class MultiHoleResource {
+    @GET @RolesAllowed(Array("ADMIN")) def get: String = ""
+    @PUT def put: String = ""
+    @PATCH def patch: String = ""
+  }
+
+  // Every method maps to a verb that is not GET/POST/DELETE; all are holes.
+  class AllVerbsUnannotatedResource {
+    @PUT def put: String = ""
+    @PATCH def patch: String = ""
+    @HEAD def head: String = ""
+    @OPTIONS def options: String = ""
+  }
+
+  class AnnotatedBaseResource {
+    @GET @PermitAll def inheritedOpen: String = ""
+  }
+  // Inherits an endpoint whose annotation lives on the superclass method.
+  class InheritsAnnotatedEndpoint extends AnnotatedBaseResource
+
+  class UnannotatedBaseResource {
+    @PUT def inheritedWrite: String = ""
+  }
+  // Class-level annotation on the subclass covers the inherited unannotated 
endpoint.
+  @RolesAllowed(Array("ADMIN"))
+  class SecuredSubclass extends UnannotatedBaseResource
+  // No annotation anywhere: the inherited endpoint is a hole.
+  class UnsecuredSubclass extends UnannotatedBaseResource
+}
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 0650990264..db63bbf2eb 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
@@ -20,24 +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.common.config.StorageConfig
-import org.apache.texera.auth.{
-  JwtAuthFilter,
-  RequestLoggingFilter,
-  SessionUser,
-  UnauthorizedExceptionMapper
-}
+import org.apache.texera.auth.{AuthFeatures, RequestLoggingFilter, 
RoleAnnotationEnforcer}
 import org.apache.texera.dao.SqlServer
 import org.apache.texera.service.resource.{
   ComputingUnitAccessResource,
   ComputingUnitManagingResource,
   HealthCheckResource
 }
-import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
 import java.nio.file.Path
 
 class ComputingUnitManagingService extends 
Application[ComputingUnitManagingServiceConfiguration] {
@@ -63,7 +56,7 @@ class ComputingUnitManagingService extends 
Application[ComputingUnitManagingServ
     environment.jersey.setUrlPattern("/api/*")
     environment.jersey.register(classOf[HealthCheckResource])
 
-    ComputingUnitManagingService.registerAuthFeatures(environment)
+    AuthFeatures.register(environment)
 
     SqlServer.initConnection(
       StorageConfig.jdbcUrl,
@@ -74,27 +67,17 @@ class ComputingUnitManagingService extends 
Application[ComputingUnitManagingServ
     environment.jersey().register(new ComputingUnitManagingResource)
     environment.jersey().register(new ComputingUnitAccessResource)
 
+    RoleAnnotationEnforcer.enforce(
+      environment.jersey.getResourceConfig,
+      "ComputingUnitManagingService"
+    )
+
     // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL
     RequestLoggingFilter.register(environment.getApplicationContext)
   }
 }
 
 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]))
-    environment.jersey.register(classOf[UnauthorizedExceptionMapper])
-
-    // 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/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
index b189cf3480..d2162d48c7 100644
--- 
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
@@ -19,32 +19,25 @@
 
 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.apache.texera.auth.UnauthorizedExceptionMapper
-import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
-import org.mockito.Mockito.{mock, verify, when}
+import org.apache.texera.auth.RoleAnnotationEnforcer
+import org.apache.texera.service.resource.{
+  ComputingUnitAccessResource,
+  ComputingUnitManagingResource,
+  HealthCheckResource
+}
 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(classOf[UnauthorizedExceptionMapper])
-    
verify(jersey).register(org.mockito.ArgumentMatchers.any(classOf[AuthDynamicFeature]))
-    verify(jersey).register(
-      
org.mockito.ArgumentMatchers.any(classOf[AuthValueFactoryProvider.Binder[_]])
-    )
+  // Every endpoint this service registers declares 
@RolesAllowed/@PermitAll/@DenyAll.
+  "ComputingUnitManagingService's registered resources" should "all declare 
access control" in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(
+        classOf[ComputingUnitManagingResource],
+        classOf[ComputingUnitAccessResource],
+        classOf[HealthCheckResource]
+      )
+    ) shouldBe empty
   }
 }
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 e4736cf251..b45c6ce62b 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
@@ -21,21 +21,14 @@ package org.apache.texera.service
 
 import com.fasterxml.jackson.module.scala.DefaultScalaModule
 import com.typesafe.scalalogging.LazyLogging
-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.auth.{
-  JwtAuthFilter,
-  RequestLoggingFilter,
-  SessionUser,
-  UnauthorizedExceptionMapper
-}
+import org.apache.texera.auth.{AuthFeatures, RequestLoggingFilter, 
RoleAnnotationEnforcer}
 import org.apache.texera.common.config.{DefaultsConfig, StorageConfig}
 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
@@ -68,10 +61,12 @@ class ConfigService extends 
Application[ConfigServiceConfiguration] with LazyLog
 
     environment.jersey.register(classOf[HealthCheckResource])
 
-    ConfigService.registerAuthFeatures(environment)
+    AuthFeatures.register(environment)
 
     environment.jersey.register(new ConfigResource)
 
+    RoleAnnotationEnforcer.enforce(environment.jersey.getResourceConfig, 
"ConfigService")
+
     // Preload default.conf into site_setting tables
     try {
       val ctx = SqlServer.getInstance().createDSLContext()
@@ -108,24 +103,6 @@ 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]))
-    environment.jersey.register(classOf[UnauthorizedExceptionMapper])
-
-    // 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/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala
 
b/config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala
index 388a48136a..1481b311e6 100644
--- 
a/config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala
+++ 
b/config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala
@@ -19,32 +19,17 @@
 
 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.apache.texera.auth.UnauthorizedExceptionMapper
-import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
-import org.mockito.Mockito.{mock, verify, when}
+import org.apache.texera.auth.RoleAnnotationEnforcer
+import org.apache.texera.service.resource.{ConfigResource, HealthCheckResource}
 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(classOf[UnauthorizedExceptionMapper])
-    
verify(jersey).register(org.mockito.ArgumentMatchers.any(classOf[AuthDynamicFeature]))
-    verify(jersey).register(
-      
org.mockito.ArgumentMatchers.any(classOf[AuthValueFactoryProvider.Binder[_]])
-    )
+  // Every endpoint this service registers declares 
@RolesAllowed/@PermitAll/@DenyAll.
+  "ConfigService's registered resources" should "all declare access control" 
in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(classOf[ConfigResource], classOf[HealthCheckResource])
+    ) shouldBe empty
   }
 }
diff --git 
a/file-service/src/main/scala/org/apache/texera/service/FileService.scala 
b/file-service/src/main/scala/org/apache/texera/service/FileService.scala
index 9a2688212d..fe3c3b8741 100644
--- a/file-service/src/main/scala/org/apache/texera/service/FileService.scala
+++ b/file-service/src/main/scala/org/apache/texera/service/FileService.scala
@@ -22,18 +22,12 @@ package org.apache.texera.service
 import com.fasterxml.jackson.databind.module.SimpleModule
 import com.fasterxml.jackson.module.scala.DefaultScalaModule
 import com.typesafe.scalalogging.LazyLogging
-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.common.config.StorageConfig
 import org.apache.texera.amber.core.storage.util.LakeFSStorageClient
-import org.apache.texera.auth.{
-  JwtAuthFilter,
-  RequestLoggingFilter,
-  SessionUser,
-  UnauthorizedExceptionMapper
-}
+import org.apache.texera.auth.{AuthFeatures, RequestLoggingFilter, 
RoleAnnotationEnforcer}
 import org.apache.texera.dao.SqlServer
 import org.apache.texera.service.`type`.DatasetFileNode
 import org.apache.texera.service.`type`.serde.DatasetFileNodeSerializer
@@ -45,7 +39,6 @@ import org.apache.texera.service.resource.{
 import org.apache.texera.service.util.S3StorageClient
 import org.apache.texera.service.util.LargeBinaryManager
 import org.eclipse.jetty.server.session.SessionHandler
-import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
 import java.nio.file.Path
 
 class FileService extends Application[FileServiceConfiguration] with 
LazyLogging {
@@ -91,21 +84,13 @@ class FileService extends 
Application[FileServiceConfiguration] with LazyLogging
 
     environment.jersey.register(classOf[HealthCheckResource])
 
-    // Register JWT authentication filter
-    environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))
-    environment.jersey.register(classOf[UnauthorizedExceptionMapper])
-
-    // 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])
+    AuthFeatures.register(environment)
 
     environment.jersey.register(classOf[DatasetResource])
     environment.jersey.register(classOf[DatasetAccessResource])
 
+    RoleAnnotationEnforcer.enforce(environment.jersey.getResourceConfig, 
"FileService")
+
     // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL
     RequestLoggingFilter.register(environment.getApplicationContext)
   }
diff --git 
a/file-service/src/test/scala/org/apache/texera/service/FileServiceRunSpec.scala
 
b/file-service/src/test/scala/org/apache/texera/service/FileServiceRunSpec.scala
new file mode 100644
index 0000000000..82b5169bd7
--- /dev/null
+++ 
b/file-service/src/test/scala/org/apache/texera/service/FileServiceRunSpec.scala
@@ -0,0 +1,43 @@
+/*
+ * 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 org.apache.texera.auth.RoleAnnotationEnforcer
+import org.apache.texera.service.resource.{
+  DatasetAccessResource,
+  DatasetResource,
+  HealthCheckResource
+}
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class FileServiceRunSpec extends AnyFlatSpec with Matchers {
+
+  // Every endpoint this service registers declares 
@RolesAllowed/@PermitAll/@DenyAll.
+  "FileService's registered resources" should "all declare access control" in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(
+        classOf[DatasetResource],
+        classOf[DatasetAccessResource],
+        classOf[HealthCheckResource]
+      )
+    ) shouldBe empty
+  }
+}
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 c278b21b4d..a69ef54524 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,17 +20,15 @@
 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.common.config.StorageConfig
 import org.apache.texera.amber.util.ObjectMapperUtils
-import org.apache.texera.auth.{JwtAuthFilter, SessionUser, 
UnauthorizedExceptionMapper}
+import org.apache.texera.auth.{AuthFeatures, RoleAnnotationEnforcer}
 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
 
@@ -58,7 +56,7 @@ class WorkflowCompilingService extends 
Application[WorkflowCompilingServiceConfi
 
     environment.jersey.register(classOf[HealthCheckResource])
 
-    WorkflowCompilingService.registerAuthFeatures(environment)
+    AuthFeatures.register(environment)
 
     SqlServer.initConnection(
       StorageConfig.jdbcUrl,
@@ -69,6 +67,11 @@ class WorkflowCompilingService extends 
Application[WorkflowCompilingServiceConfi
     // register the compilation endpoint
     environment.jersey.register(classOf[WorkflowCompilationResource])
 
+    RoleAnnotationEnforcer.enforce(
+      environment.jersey.getResourceConfig,
+      "WorkflowCompilingService"
+    )
+
     // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL
     val requestLogger = 
org.slf4j.LoggerFactory.getLogger("org.eclipse.jetty.server.RequestLog")
     environment.getApplicationContext.addFilter(
@@ -95,21 +98,6 @@ 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]))
-    environment.jersey.register(classOf[UnauthorizedExceptionMapper])
-
-    // 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
index cecc09a4d3..898d32a9b4 100644
--- 
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
@@ -19,32 +19,17 @@
 
 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.apache.texera.auth.UnauthorizedExceptionMapper
-import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
-import org.mockito.Mockito.{mock, verify, when}
+import org.apache.texera.auth.RoleAnnotationEnforcer
+import org.apache.texera.service.resource.{HealthCheckResource, 
WorkflowCompilationResource}
 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(classOf[UnauthorizedExceptionMapper])
-    
verify(jersey).register(org.mockito.ArgumentMatchers.any(classOf[AuthDynamicFeature]))
-    verify(jersey).register(
-      
org.mockito.ArgumentMatchers.any(classOf[AuthValueFactoryProvider.Binder[_]])
-    )
+  // Every endpoint this service registers declares 
@RolesAllowed/@PermitAll/@DenyAll.
+  "WorkflowCompilingService's registered resources" should "all declare access 
control" in {
+    RoleAnnotationEnforcer.findUnannotatedEndpoints(
+      Seq(classOf[WorkflowCompilationResource], classOf[HealthCheckResource])
+    ) shouldBe empty
   }
 }

Reply via email to