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
}
}