From e6ffc27e1a0943954c463c5d6656058300cd88b1 Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Tue, 12 May 2026 23:08:39 -0700 Subject: [PATCH 1/8] enforce @RolesAllowed on microservice resources --- build.sbt | 2 +- .../ComputingUnitManagingService.scala | 4 ++ .../apache/texera/service/ConfigService.scala | 4 ++ .../texera/service/ConfigServiceRunSpec.scala | 55 +++++++++++++++++++ .../service/WorkflowCompilingService.scala | 14 +++++ 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala diff --git a/build.sbt b/build.sbt index b7b6b3cfb20..8b38e2e009f 100644 --- a/build.sbt +++ b/build.sbt @@ -113,7 +113,7 @@ lazy val FileService = (project in file("file-service")) lazy val WorkflowOperator = (project in file("common/workflow-operator")).settings(asfLicensingSettingsWithVendored).dependsOn(WorkflowCore) lazy val WorkflowCompilingService = (project in file("workflow-compiling-service")) - .dependsOn(WorkflowOperator, Config) + .dependsOn(WorkflowOperator, Auth, Config) .settings(asfLicensingSettings) .settings( dependencyOverrides ++= Seq( diff --git a/computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingService.scala b/computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingService.scala index a15ced30a29..ce6a4cae2ba 100644 --- a/computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingService.scala +++ b/computing-unit-managing-service/src/main/scala/org/apache/texera/service/ComputingUnitManagingService.scala @@ -32,6 +32,7 @@ import org.apache.texera.service.resource.{ ComputingUnitManagingResource, HealthCheckResource } +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature import java.nio.file.Path class ComputingUnitManagingService extends Application[ComputingUnitManagingServiceConfiguration] { @@ -70,6 +71,9 @@ class ComputingUnitManagingService extends Application[ComputingUnitManagingServ new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser]) ) + // Enforce @RolesAllowed annotations on resource methods + environment.jersey.register(classOf[RolesAllowedDynamicFeature]) + environment.jersey().register(new ComputingUnitManagingResource) environment.jersey().register(new ComputingUnitAccessResource) 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 c787016c270..545aac494b3 100644 --- a/config-service/src/main/scala/org/apache/texera/service/ConfigService.scala +++ b/config-service/src/main/scala/org/apache/texera/service/ConfigService.scala @@ -31,6 +31,7 @@ import org.apache.texera.config.DefaultsConfig import org.apache.texera.dao.SqlServer import org.apache.texera.service.resource.{ConfigResource, HealthCheckResource} import org.eclipse.jetty.server.session.SessionHandler +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature import org.jooq.impl.DSL import java.nio.file.Path @@ -71,6 +72,9 @@ class ConfigService extends Application[ConfigServiceConfiguration] with LazyLog new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser]) ) + // Enforce @RolesAllowed annotations on resource methods + environment.jersey.register(classOf[RolesAllowedDynamicFeature]) + environment.jersey.register(new ConfigResource) // Preload default.conf into site_setting tables diff --git a/config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala b/config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala new file mode 100644 index 00000000000..e3982e37750 --- /dev/null +++ b/config-service/src/test/scala/org/apache/texera/service/ConfigServiceRunSpec.scala @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.texera.service + +import io.dropwizard.core.setup.Environment +import io.dropwizard.jersey.setup.JerseyEnvironment +import io.dropwizard.jetty.MutableServletContextHandler +import io.dropwizard.jetty.setup.ServletEnvironment +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature +import org.mockito.Mockito.{mock, verify, when} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class ConfigServiceRunSpec extends AnyFlatSpec with Matchers { + + // Verifies that the @RolesAllowed annotations on ConfigResource are actually + // enforced by Jersey, which requires RolesAllowedDynamicFeature to be + // registered on the Jersey environment. + "ConfigService.run" should "register RolesAllowedDynamicFeature on the Jersey environment" in { + val jersey = mock(classOf[JerseyEnvironment]) + val servlets = mock(classOf[ServletEnvironment]) + val context = mock(classOf[MutableServletContextHandler]) + val env = mock(classOf[Environment]) + when(env.jersey).thenReturn(jersey) + when(env.servlets).thenReturn(servlets) + when(env.getApplicationContext).thenReturn(context) + + val service = new ConfigService + // run() reaches into SqlServer near the end to preload defaults; that throws + // here because no real DB is wired up. By that point all Jersey registrations + // have already executed, so the verification below is still valid. + intercept[Exception] { + service.run(mock(classOf[ConfigServiceConfiguration]), env) + } + + verify(jersey).register(classOf[RolesAllowedDynamicFeature]) + } +} diff --git a/workflow-compiling-service/src/main/scala/org/apache/texera/service/WorkflowCompilingService.scala b/workflow-compiling-service/src/main/scala/org/apache/texera/service/WorkflowCompilingService.scala index 40fb3a2dd8f..dbfff379f89 100644 --- a/workflow-compiling-service/src/main/scala/org/apache/texera/service/WorkflowCompilingService.scala +++ b/workflow-compiling-service/src/main/scala/org/apache/texera/service/WorkflowCompilingService.scala @@ -20,14 +20,17 @@ package org.apache.texera.service import com.fasterxml.jackson.module.scala.DefaultScalaModule +import io.dropwizard.auth.AuthDynamicFeature import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, SubstitutingSourceProvider} import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig import org.apache.texera.amber.util.ObjectMapperUtils +import org.apache.texera.auth.{JwtAuthFilter, SessionUser} import org.apache.texera.dao.SqlServer import org.apache.texera.service.resource.{HealthCheckResource, WorkflowCompilationResource} import org.eclipse.jetty.servlet.FilterHolder +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature import java.nio.file.Path @@ -61,6 +64,17 @@ class WorkflowCompilingService extends Application[WorkflowCompilingServiceConfi environment.jersey.register(classOf[HealthCheckResource]) + // Register JWT authentication filter + environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) + + // Enable @Auth annotation for injecting SessionUser + environment.jersey.register( + new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser]) + ) + + // Enforce @RolesAllowed annotations on resource methods + environment.jersey.register(classOf[RolesAllowedDynamicFeature]) + // register the compilation endpoint environment.jersey.register(classOf[WorkflowCompilationResource]) From 9c499413aac4383a713b4f3f708963f31e94bb72 Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Tue, 12 May 2026 23:17:03 -0700 Subject: [PATCH 2/8] Add dropwizard-auth as a dependency for workflow-compiling-service --- workflow-compiling-service/build.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/workflow-compiling-service/build.sbt b/workflow-compiling-service/build.sbt index 9560751d00b..95a69269927 100644 --- a/workflow-compiling-service/build.sbt +++ b/workflow-compiling-service/build.sbt @@ -84,5 +84,6 @@ libraryDependencies ++= Seq( // Core Dependencies libraryDependencies ++= Seq( "io.dropwizard" % "dropwizard-core" % dropwizardVersion, + "io.dropwizard" % "dropwizard-auth" % dropwizardVersion, // Dropwizard Authentication module "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.18.6" ) From 6f6da209c09802d06cbfa12037ac6ff54363cc87 Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Tue, 12 May 2026 23:39:38 -0700 Subject: [PATCH 3/8] Added appropriate licenses to License-binary --- workflow-compiling-service/LICENSE-binary | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workflow-compiling-service/LICENSE-binary b/workflow-compiling-service/LICENSE-binary index 5b7548a4edc..ed6a9e1d266 100644 --- a/workflow-compiling-service/LICENSE-binary +++ b/workflow-compiling-service/LICENSE-binary @@ -281,6 +281,7 @@ Scala/Java jars: - commons-pool.commons-pool-1.6.jar - dev.failsafe.failsafe-3.3.2.jar - io.airlift.aircompressor-0.27.jar + - io.dropwizard.dropwizard-auth-4.0.7.jar - io.dropwizard.dropwizard-configuration-4.0.7.jar - io.dropwizard.dropwizard-core-4.0.7.jar - io.dropwizard.dropwizard-health-4.0.7.jar @@ -296,6 +297,7 @@ Scala/Java jars: - io.dropwizard.dropwizard-validation-4.0.7.jar - io.dropwizard.logback.logback-throttling-appender-1.4.2.jar - io.dropwizard.metrics.metrics-annotation-4.2.25.jar + - io.dropwizard.metrics.metrics-caffeine-4.2.25.jar - io.dropwizard.metrics.metrics-core-4.2.25.jar - io.dropwizard.metrics.metrics-healthchecks-4.2.25.jar - io.dropwizard.metrics.metrics-jakarta-servlets-4.2.25.jar @@ -419,6 +421,7 @@ Scala/Java jars: - org.apache.yetus.audience-annotations-0.13.0.jar - org.apache.zookeeper.zookeeper-3.5.6.jar - org.apache.zookeeper.zookeeper-jute-3.5.6.jar + - org.bitbucket.b_c.jose4j-0.9.6.jar - org.eclipse.jetty.jetty-http-11.0.20.jar - org.eclipse.jetty.jetty-io-11.0.20.jar - org.eclipse.jetty.jetty-security-11.0.20.jar From 5fda9880bbfbe7a0332ad2e98ac5bb1e73b95e44 Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Fri, 15 May 2026 16:02:16 -0700 Subject: [PATCH 4/8] Added test --- computing-unit-managing-service/build.sbt | 7 +++ .../ComputingUnitManagingServiceRunSpec.scala | 59 +++++++++++++++++++ .../WorkflowCompilingServiceRunSpec.scala | 59 +++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 computing-unit-managing-service/src/test/scala/org/apache/texera/service/ComputingUnitManagingServiceRunSpec.scala create mode 100644 workflow-compiling-service/src/test/scala/org/apache/texera/service/WorkflowCompilingServiceRunSpec.scala diff --git a/computing-unit-managing-service/build.sbt b/computing-unit-managing-service/build.sbt index 3d385d33d30..1c39a6b03d9 100644 --- a/computing-unit-managing-service/build.sbt +++ b/computing-unit-managing-service/build.sbt @@ -34,6 +34,13 @@ Universal / mappings := AddMetaInfLicenseFiles.distMappings( // Dependency Versions val dropwizardVersion = "4.0.7" +val mockitoVersion = "5.4.0" + +// Test Dependencies +libraryDependencies ++= Seq( + "org.scalatest" %% "scalatest" % "3.2.17" % Test, + "org.mockito" % "mockito-core" % mockitoVersion % Test +) // Dependencies libraryDependencies ++= Seq( diff --git a/computing-unit-managing-service/src/test/scala/org/apache/texera/service/ComputingUnitManagingServiceRunSpec.scala b/computing-unit-managing-service/src/test/scala/org/apache/texera/service/ComputingUnitManagingServiceRunSpec.scala new file mode 100644 index 00000000000..38e28e9253e --- /dev/null +++ b/computing-unit-managing-service/src/test/scala/org/apache/texera/service/ComputingUnitManagingServiceRunSpec.scala @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.texera.service + +import io.dropwizard.auth.{AuthDynamicFeature, AuthValueFactoryProvider} +import io.dropwizard.core.setup.Environment +import io.dropwizard.jersey.setup.JerseyEnvironment +import io.dropwizard.jetty.MutableServletContextHandler +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature +import org.mockito.Mockito.{mock, verify, when} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class ComputingUnitManagingServiceRunSpec extends AnyFlatSpec with Matchers { + + // Verifies that the @RolesAllowed annotations on resource methods are actually + // enforced by Jersey, which requires RolesAllowedDynamicFeature, AuthDynamicFeature, + // and AuthValueFactoryProvider.Binder to be registered on the Jersey environment. + "ComputingUnitManagingService.run" should "register auth + RolesAllowedDynamicFeature on the Jersey environment" in { + val jersey = mock(classOf[JerseyEnvironment]) + val context = mock(classOf[MutableServletContextHandler]) + val env = mock(classOf[Environment]) + when(env.jersey).thenReturn(jersey) + when(env.getApplicationContext).thenReturn(context) + + val service = new ComputingUnitManagingService + // run() may throw if SqlServer/StorageConfig or other side effects fail + // under mocks. Auth + RolesAllowedDynamicFeature registrations happen + // before any such failure, so the verifications below remain valid. + try { + service.run(mock(classOf[ComputingUnitManagingServiceConfiguration]), env) + } catch { + case _: Throwable => // expected under mocks + } + + verify(jersey).register(classOf[RolesAllowedDynamicFeature]) + verify(jersey).register(org.mockito.ArgumentMatchers.any(classOf[AuthDynamicFeature])) + verify(jersey).register( + org.mockito.ArgumentMatchers.any(classOf[AuthValueFactoryProvider.Binder[_]]) + ) + } +} diff --git a/workflow-compiling-service/src/test/scala/org/apache/texera/service/WorkflowCompilingServiceRunSpec.scala b/workflow-compiling-service/src/test/scala/org/apache/texera/service/WorkflowCompilingServiceRunSpec.scala new file mode 100644 index 00000000000..e3049e8bcd9 --- /dev/null +++ b/workflow-compiling-service/src/test/scala/org/apache/texera/service/WorkflowCompilingServiceRunSpec.scala @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.texera.service + +import io.dropwizard.auth.{AuthDynamicFeature, AuthValueFactoryProvider} +import io.dropwizard.core.setup.Environment +import io.dropwizard.jersey.setup.JerseyEnvironment +import io.dropwizard.jetty.MutableServletContextHandler +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature +import org.mockito.Mockito.{mock, verify, when} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class WorkflowCompilingServiceRunSpec extends AnyFlatSpec with Matchers { + + // Verifies that the @RolesAllowed annotations on resource methods are actually + // enforced by Jersey, which requires RolesAllowedDynamicFeature, AuthDynamicFeature, + // and AuthValueFactoryProvider.Binder to be registered on the Jersey environment. + "WorkflowCompilingService.run" should "register auth + RolesAllowedDynamicFeature on the Jersey environment" in { + val jersey = mock(classOf[JerseyEnvironment]) + val context = mock(classOf[MutableServletContextHandler]) + val env = mock(classOf[Environment]) + when(env.jersey).thenReturn(jersey) + when(env.getApplicationContext).thenReturn(context) + + val service = new WorkflowCompilingService + // run() may throw if SqlServer/StorageConfig or other side effects fail + // under mocks. Auth + RolesAllowedDynamicFeature registrations happen + // before any such failure, so the verifications below remain valid. + try { + service.run(mock(classOf[WorkflowCompilingServiceConfiguration]), env) + } catch { + case _: Throwable => // expected under mocks + } + + verify(jersey).register(classOf[RolesAllowedDynamicFeature]) + verify(jersey).register(org.mockito.ArgumentMatchers.any(classOf[AuthDynamicFeature])) + verify(jersey).register( + org.mockito.ArgumentMatchers.any(classOf[AuthValueFactoryProvider.Binder[_]]) + ) + } +} From ec42428fd9c3f3e36f8ef69cc91b6f636671edef Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Fri, 22 May 2026 01:10:31 -0700 Subject: [PATCH 5/8] extract registerAuthFeatures to decouple Jersey setup from DB init --- .../ComputingUnitManagingService.scala | 29 ++++++++++------- .../ComputingUnitManagingServiceRunSpec.scala | 15 ++------- .../service/WorkflowCompilingService.scala | 31 +++++++++++-------- .../WorkflowCompilingServiceRunSpec.scala | 15 ++------- 4 files changed, 39 insertions(+), 51 deletions(-) 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 ce6a4cae2ba..6184cf545a2 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 @@ -54,15 +54,29 @@ class ComputingUnitManagingService extends Application[ComputingUnitManagingServ configuration: ComputingUnitManagingServiceConfiguration, environment: Environment ): Unit = { + // Register http resources + environment.jersey.setUrlPattern("/api/*") + environment.jersey.register(classOf[HealthCheckResource]) + + ComputingUnitManagingService.registerAuthFeatures(environment) + SqlServer.initConnection( StorageConfig.jdbcUrl, StorageConfig.jdbcUsername, StorageConfig.jdbcPassword ) - // Register http resources - environment.jersey.setUrlPattern("/api/*") - environment.jersey.register(classOf[HealthCheckResource]) + environment.jersey().register(new ComputingUnitManagingResource) + environment.jersey().register(new ComputingUnitAccessResource) + + // 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])) @@ -73,16 +87,7 @@ class ComputingUnitManagingService extends Application[ComputingUnitManagingServ // Enforce @RolesAllowed annotations on resource methods environment.jersey.register(classOf[RolesAllowedDynamicFeature]) - - environment.jersey().register(new ComputingUnitManagingResource) - environment.jersey().register(new ComputingUnitAccessResource) - - // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL - RequestLoggingFilter.register(environment.getApplicationContext) } -} - -object ComputingUnitManagingService { def main(args: Array[String]): Unit = { val configFilePath = Path diff --git a/computing-unit-managing-service/src/test/scala/org/apache/texera/service/ComputingUnitManagingServiceRunSpec.scala b/computing-unit-managing-service/src/test/scala/org/apache/texera/service/ComputingUnitManagingServiceRunSpec.scala index 38e28e9253e..d27f5725ac9 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 @@ -22,7 +22,6 @@ package org.apache.texera.service import io.dropwizard.auth.{AuthDynamicFeature, AuthValueFactoryProvider} import io.dropwizard.core.setup.Environment import io.dropwizard.jersey.setup.JerseyEnvironment -import io.dropwizard.jetty.MutableServletContextHandler import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature import org.mockito.Mockito.{mock, verify, when} import org.scalatest.flatspec.AnyFlatSpec @@ -33,22 +32,12 @@ 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.run" should "register auth + RolesAllowedDynamicFeature on the Jersey environment" in { + "ComputingUnitManagingService.registerAuthFeatures" should "register auth + RolesAllowedDynamicFeature on the Jersey environment" in { val jersey = mock(classOf[JerseyEnvironment]) - val context = mock(classOf[MutableServletContextHandler]) val env = mock(classOf[Environment]) when(env.jersey).thenReturn(jersey) - when(env.getApplicationContext).thenReturn(context) - val service = new ComputingUnitManagingService - // run() may throw if SqlServer/StorageConfig or other side effects fail - // under mocks. Auth + RolesAllowedDynamicFeature registrations happen - // before any such failure, so the verifications below remain valid. - try { - service.run(mock(classOf[ComputingUnitManagingServiceConfiguration]), env) - } catch { - case _: Throwable => // expected under mocks - } + ComputingUnitManagingService.registerAuthFeatures(env) verify(jersey).register(classOf[RolesAllowedDynamicFeature]) verify(jersey).register(org.mockito.ArgumentMatchers.any(classOf[AuthDynamicFeature])) 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 dbfff379f89..8dc573aaf8b 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 @@ -56,25 +56,16 @@ class WorkflowCompilingService extends Application[WorkflowCompilingServiceConfi // serve backend at /api environment.jersey.setUrlPattern("/api/*") + environment.jersey.register(classOf[HealthCheckResource]) + + WorkflowCompilingService.registerAuthFeatures(environment) + SqlServer.initConnection( StorageConfig.jdbcUrl, StorageConfig.jdbcUsername, StorageConfig.jdbcPassword ) - environment.jersey.register(classOf[HealthCheckResource]) - - // Register JWT authentication filter - environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) - - // Enable @Auth annotation for injecting SessionUser - environment.jersey.register( - new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser]) - ) - - // Enforce @RolesAllowed annotations on resource methods - environment.jersey.register(classOf[RolesAllowedDynamicFeature]) - // register the compilation endpoint environment.jersey.register(classOf[WorkflowCompilationResource]) @@ -104,6 +95,20 @@ class WorkflowCompilingService extends Application[WorkflowCompilingServiceConfi } object WorkflowCompilingService { + // Registers JWT auth, @Auth injection, and @RolesAllowed enforcement. + def registerAuthFeatures(environment: Environment): Unit = { + // Register JWT authentication filter + environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) + + // Enable @Auth annotation for injecting SessionUser + environment.jersey.register( + new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser]) + ) + + // Enforce @RolesAllowed annotations on resource methods + environment.jersey.register(classOf[RolesAllowedDynamicFeature]) + } + def main(args: Array[String]): Unit = { // set the configuration file's path val configFilePath = Path diff --git a/workflow-compiling-service/src/test/scala/org/apache/texera/service/WorkflowCompilingServiceRunSpec.scala b/workflow-compiling-service/src/test/scala/org/apache/texera/service/WorkflowCompilingServiceRunSpec.scala index e3049e8bcd9..ff5da1b5613 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 @@ -22,7 +22,6 @@ package org.apache.texera.service import io.dropwizard.auth.{AuthDynamicFeature, AuthValueFactoryProvider} import io.dropwizard.core.setup.Environment import io.dropwizard.jersey.setup.JerseyEnvironment -import io.dropwizard.jetty.MutableServletContextHandler import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature import org.mockito.Mockito.{mock, verify, when} import org.scalatest.flatspec.AnyFlatSpec @@ -33,22 +32,12 @@ 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.run" should "register auth + RolesAllowedDynamicFeature on the Jersey environment" in { + "WorkflowCompilingService.registerAuthFeatures" should "register auth + RolesAllowedDynamicFeature on the Jersey environment" in { val jersey = mock(classOf[JerseyEnvironment]) - val context = mock(classOf[MutableServletContextHandler]) val env = mock(classOf[Environment]) when(env.jersey).thenReturn(jersey) - when(env.getApplicationContext).thenReturn(context) - val service = new WorkflowCompilingService - // run() may throw if SqlServer/StorageConfig or other side effects fail - // under mocks. Auth + RolesAllowedDynamicFeature registrations happen - // before any such failure, so the verifications below remain valid. - try { - service.run(mock(classOf[WorkflowCompilingServiceConfiguration]), env) - } catch { - case _: Throwable => // expected under mocks - } + WorkflowCompilingService.registerAuthFeatures(env) verify(jersey).register(classOf[RolesAllowedDynamicFeature]) verify(jersey).register(org.mockito.ArgumentMatchers.any(classOf[AuthDynamicFeature])) From 69239527344dee4ac190889abff8bf514d3b6edc Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Sun, 24 May 2026 17:00:23 -0700 Subject: [PATCH 6/8] order JwtAuthFilter before authz so @RolesAllowed stops 403ing valid tokens --- .../texera/service/AccessControlService.scala | 4 ++ .../resource/AccessControlResource.scala | 4 ++ .../service/AccessControlServiceRunSpec.scala | 17 +++++- ...AccessControlResourcePermissionsSpec.scala | 50 +++++++++++++++++ .../web/resource/auth/AuthResource.scala | 2 +- common/auth/build.sbt | 1 + .../apache/texera/auth/JwtAuthFilter.scala | 3 + .../texera/auth/JwtAuthFilterSpec.scala | 44 +++++++++++++++ .../service/resource/ConfigResource.scala | 6 +- .../service/resource/ConfigResourceSpec.scala | 43 +++++++++++++++ .../apache/texera/service/FileService.scala | 31 +++++++---- .../service/resource/DatasetResource.scala | 8 ++- .../texera/service/FileServiceRunSpec.scala | 55 +++++++++++++++++++ .../DatasetResourcePermissionsSpec.scala | 51 +++++++++++++++++ 14 files changed, 299 insertions(+), 20 deletions(-) create mode 100644 access-control-service/src/test/scala/org/apache/texera/service/resource/AccessControlResourcePermissionsSpec.scala create mode 100644 common/auth/src/test/scala/org/apache/texera/auth/JwtAuthFilterSpec.scala create mode 100644 config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceSpec.scala create mode 100644 file-service/src/test/scala/org/apache/texera/service/FileServiceRunSpec.scala create mode 100644 file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourcePermissionsSpec.scala 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 21d367e2bb1..e501a012339 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 @@ -34,6 +34,7 @@ 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 { @@ -78,6 +79,9 @@ class AccessControlService extends Application[AccessControlServiceConfiguration new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser]) ) + // Enforce @RolesAllowed annotations on resource methods + environment.jersey.register(classOf[RolesAllowedDynamicFeature]) + // Record USER_LAST_ACTIVE_TIME on every matched, completed request. // Lives only in this service because authenticated client sessions // contact access-control-service often enough to capture activity diff --git a/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala b/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala index 0cd52f49194..b8542662b81 100644 --- a/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala +++ b/access-control-service/src/main/scala/org/apache/texera/service/resource/AccessControlResource.scala @@ -20,6 +20,7 @@ package org.apache.texera.service.resource import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.typesafe.scalalogging.LazyLogging +import jakarta.annotation.security.{PermitAll, RolesAllowed} import jakarta.ws.rs.client.{Client, ClientBuilder, Entity} import jakarta.ws.rs.core._ import jakarta.ws.rs.{Consumes, GET, POST, Path, Produces} @@ -191,6 +192,7 @@ object AccessControlResource extends LazyLogging { } } @Produces(Array(MediaType.APPLICATION_JSON)) +@PermitAll @Path("/auth") class AccessControlResource extends LazyLogging { @@ -218,6 +220,7 @@ class AccessControlResource extends LazyLogging { @Path("/chat") @Produces(Array(MediaType.APPLICATION_JSON)) @Consumes(Array(MediaType.APPLICATION_JSON)) +@RolesAllowed(Array("REGULAR", "ADMIN")) class LiteLLMProxyResource extends LazyLogging { private val client: Client = ClientBuilder.newClient() @@ -289,6 +292,7 @@ class LiteLLMProxyResource extends LazyLogging { @Path("/models") @Produces(Array(MediaType.APPLICATION_JSON)) +@RolesAllowed(Array("REGULAR", "ADMIN")) class LiteLLMModelsResource extends LazyLogging { private val client: Client = ClientBuilder.newClient() 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 89979ee816f..4b4d82f171f 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 @@ -24,6 +24,7 @@ import io.dropwizard.jersey.setup.JerseyEnvironment import io.dropwizard.jetty.MutableServletContextHandler import io.dropwizard.jetty.setup.ServletEnvironment import org.apache.texera.service.activity.UserActivityEventListener +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature import org.mockito.ArgumentMatchers.isA import org.mockito.Mockito.{mock, verify, when} import org.scalatest.flatspec.AnyFlatSpec @@ -31,7 +32,7 @@ import org.scalatest.matchers.should.Matchers class AccessControlServiceRunSpec extends AnyFlatSpec with Matchers { - "AccessControlService.run" should "register UserActivityEventListener on the Jersey environment" in { + private def runWithMockEnv(): JerseyEnvironment = { val jersey = mock(classOf[JerseyEnvironment]) val servlets = mock(classOf[ServletEnvironment]) val context = mock(classOf[MutableServletContextHandler]) @@ -40,10 +41,20 @@ class AccessControlServiceRunSpec extends AnyFlatSpec with Matchers { when(env.servlets).thenReturn(servlets) when(env.getApplicationContext).thenReturn(context) - val service = new AccessControlService - service.run(mock(classOf[AccessControlServiceConfiguration]), env) + new AccessControlService().run(mock(classOf[AccessControlServiceConfiguration]), env) + jersey + } + "AccessControlService.run" should "register UserActivityEventListener on the Jersey environment" in { + val jersey = runWithMockEnv() verify(jersey).register(isA(classOf[UserActivityEventListener])) verify(jersey).setUrlPattern("/api/*") } + + // Without RolesAllowedDynamicFeature registered, @RolesAllowed on LiteLLM + // resources and @PermitAll on AccessControlResource have no effect. + it should "register RolesAllowedDynamicFeature so role annotations are enforced" in { + val jersey = runWithMockEnv() + verify(jersey).register(classOf[RolesAllowedDynamicFeature]) + } } diff --git a/access-control-service/src/test/scala/org/apache/texera/service/resource/AccessControlResourcePermissionsSpec.scala b/access-control-service/src/test/scala/org/apache/texera/service/resource/AccessControlResourcePermissionsSpec.scala new file mode 100644 index 00000000000..e2f01cb6afb --- /dev/null +++ b/access-control-service/src/test/scala/org/apache/texera/service/resource/AccessControlResourcePermissionsSpec.scala @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.texera.service.resource + +import jakarta.annotation.security.{PermitAll, RolesAllowed} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class AccessControlResourcePermissionsSpec extends AnyFlatSpec with Matchers { + + // /auth/* is the ExtAuth endpoint Envoy calls — it validates JWTs itself. + // Requiring a JWT-bearing role here would be circular and lock the gateway out. + "AccessControlResource" should "be @PermitAll because it IS the auth check" in { + val permit = classOf[AccessControlResource].getAnnotation(classOf[PermitAll]) + val roles = classOf[AccessControlResource].getAnnotation(classOf[RolesAllowed]) + permit should not be null + roles shouldBe null + } + + // /chat/* proxies copilot requests to LiteLLM; only logged-in users should burn LLM credits. + "LiteLLMProxyResource" should "require REGULAR or ADMIN role" in { + val roles = classOf[LiteLLMProxyResource].getAnnotation(classOf[RolesAllowed]) + roles should not be null + roles.value() should contain theSameElementsAs Array("REGULAR", "ADMIN") + } + + // /models is used by the copilot UI; same reasoning as the proxy. + "LiteLLMModelsResource" should "require REGULAR or ADMIN role" in { + val roles = classOf[LiteLLMModelsResource].getAnnotation(classOf[RolesAllowed]) + roles should not be null + roles.value() should contain theSameElementsAs Array("REGULAR", "ADMIN") + } +} diff --git a/amber/src/main/scala/org/apache/texera/web/resource/auth/AuthResource.scala b/amber/src/main/scala/org/apache/texera/web/resource/auth/AuthResource.scala index 0f99da681d5..ad2e253185a 100644 --- a/amber/src/main/scala/org/apache/texera/web/resource/auth/AuthResource.scala +++ b/amber/src/main/scala/org/apache/texera/web/resource/auth/AuthResource.scala @@ -110,7 +110,7 @@ class AuthResource { val user = new User user.setName(username) user.setEmail(username) - user.setRole(UserRoleEnum.RESTRICTED) + user.setRole(UserRoleEnum.REGULAR) // hash the plain text password user.setPassword(new StrongPasswordEncryptor().encryptPassword(request.password)) userDao.insert(user) diff --git a/common/auth/build.sbt b/common/auth/build.sbt index a33da64fea5..742cf95eea2 100644 --- a/common/auth/build.sbt +++ b/common/auth/build.sbt @@ -57,6 +57,7 @@ libraryDependencies ++= Seq( "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5", // for LazyLogging "org.bitbucket.b_c" % "jose4j" % "0.9.6", // for jwt parser "jakarta.ws.rs" % "jakarta.ws.rs-api" % "3.0.0", // for JwtAuthFilter + "jakarta.annotation" % "jakarta.annotation-api" % "2.1.1", // for @Priority on JwtAuthFilter "jakarta.servlet" % "jakarta.servlet-api" % "5.0.0" % "provided", // for RequestLoggingFilter "org.eclipse.jetty" % "jetty-servlet" % "11.0.24" % "provided", // for FilterHolder "org.scalatest" %% "scalatest" % "3.2.17" % Test diff --git a/common/auth/src/main/scala/org/apache/texera/auth/JwtAuthFilter.scala b/common/auth/src/main/scala/org/apache/texera/auth/JwtAuthFilter.scala index 56985156302..893fa3fa22f 100644 --- a/common/auth/src/main/scala/org/apache/texera/auth/JwtAuthFilter.scala +++ b/common/auth/src/main/scala/org/apache/texera/auth/JwtAuthFilter.scala @@ -20,6 +20,8 @@ package org.apache.texera.auth import com.typesafe.scalalogging.LazyLogging +import jakarta.annotation.Priority +import jakarta.ws.rs.Priorities import jakarta.ws.rs.container.{ContainerRequestContext, ContainerRequestFilter} import jakarta.ws.rs.core.{HttpHeaders, SecurityContext} import jakarta.ws.rs.ext.Provider @@ -28,6 +30,7 @@ import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum import java.security.Principal @Provider +@Priority(Priorities.AUTHENTICATION) class JwtAuthFilter extends ContainerRequestFilter with LazyLogging { override def filter(requestContext: ContainerRequestContext): Unit = { diff --git a/common/auth/src/test/scala/org/apache/texera/auth/JwtAuthFilterSpec.scala b/common/auth/src/test/scala/org/apache/texera/auth/JwtAuthFilterSpec.scala new file mode 100644 index 00000000000..f78c1451aad --- /dev/null +++ b/common/auth/src/test/scala/org/apache/texera/auth/JwtAuthFilterSpec.scala @@ -0,0 +1,44 @@ +/* + * 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 jakarta.annotation.Priority +import jakarta.ws.rs.Priorities +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class JwtAuthFilterSpec extends AnyFlatSpec with Matchers { + + // Regression guard: without an explicit @Priority(AUTHENTICATION) on this + // filter, Jersey defaults to Priorities.USER (5000), which runs AFTER + // RolesAllowedRequestFilter (AUTHORIZATION = 2000). The authz filter would + // then see no principal and 403 every authenticated request with + // "User not authorized." — even for ADMIN tokens. + "JwtAuthFilter" should "carry @Priority(Priorities.AUTHENTICATION) so it runs before authorization" in { + val priority = classOf[JwtAuthFilter].getAnnotation(classOf[Priority]) + priority should not be null + priority.value() shouldBe Priorities.AUTHENTICATION + } + + it should "run before Jersey's RolesAllowedRequestFilter priority" in { + val priority = classOf[JwtAuthFilter].getAnnotation(classOf[Priority]).value() + priority should be < Priorities.AUTHORIZATION + } +} diff --git a/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala b/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala index 2614719040c..31e32104974 100644 --- a/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala +++ b/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala @@ -19,7 +19,7 @@ package org.apache.texera.service.resource -import jakarta.annotation.security.RolesAllowed +import jakarta.annotation.security.PermitAll import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.{GET, Path, Produces} import org.apache.texera.config.{AuthConfig, ComputingUnitConfig, GuiConfig, UserSystemConfig} @@ -29,7 +29,7 @@ import org.apache.texera.config.{AuthConfig, ComputingUnitConfig, GuiConfig, Use class ConfigResource { @GET - @RolesAllowed(Array("REGULAR", "ADMIN")) + @PermitAll @Path("/gui") def getGuiConfig: Map[String, Any] = Map( @@ -64,7 +64,7 @@ class ConfigResource { ) @GET - @RolesAllowed(Array("REGULAR", "ADMIN")) + @PermitAll @Path("/user-system") def getUserSystemConfig: Map[String, Any] = Map( diff --git a/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceSpec.scala b/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceSpec.scala new file mode 100644 index 00000000000..c0857199fc2 --- /dev/null +++ b/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceSpec.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.resource + +import jakarta.annotation.security.{PermitAll, RolesAllowed} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class ConfigResourceSpec extends AnyFlatSpec with Matchers { + + // Both endpoints are called by Angular's APP_INITIALIZER before the user has + // a JWT (the GUI uses /config/gui to decide whether to show local-login or + // Google-login). They MUST be @PermitAll — tagging them @RolesAllowed makes + // the SPA's bootstrap fail with 403 and the whole site appears dead. + "ConfigResource.getGuiConfig" should "be @PermitAll so it loads before login" in { + val method = classOf[ConfigResource].getMethod("getGuiConfig") + method.getAnnotation(classOf[PermitAll]) should not be null + method.getAnnotation(classOf[RolesAllowed]) shouldBe null + } + + "ConfigResource.getUserSystemConfig" should "be @PermitAll so it loads before login" in { + val method = classOf[ConfigResource].getMethod("getUserSystemConfig") + method.getAnnotation(classOf[PermitAll]) should not be null + method.getAnnotation(classOf[RolesAllowed]) shouldBe null + } +} 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 cc4174682fe..1d68f1721fc 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 @@ -40,6 +40,7 @@ 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 { @@ -63,19 +64,9 @@ class FileService extends Application[FileServiceConfiguration] with LazyLogging override def run(configuration: FileServiceConfiguration, environment: Environment): Unit = { // Serve backend at /api environment.jersey.setUrlPattern("/api/*") - SqlServer.initConnection( - StorageConfig.jdbcUrl, - StorageConfig.jdbcUsername, - StorageConfig.jdbcPassword - ) - - // check if the texera dataset bucket exists, if not create it - S3StorageClient.createBucketIfNotExist(StorageConfig.lakefsBucketName) - // ensure the large-binary S3 bucket exists before any workflow execution attempts to use it - S3StorageClient.createBucketIfNotExist(LargeBinaryManager.DEFAULT_BUCKET) - // check if we can connect to the lakeFS service - LakeFSStorageClient.healthCheck() + // Wire Jersey first (registrations don't depend on DB/S3 state); infra init + // happens after so unit tests can drive run() with a mocked environment. environment.jersey.register(classOf[SessionHandler]) environment.servlets.setSessionHandler(new SessionHandler) @@ -89,11 +80,27 @@ class FileService extends Application[FileServiceConfiguration] with LazyLogging new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser]) ) + // Enforce @RolesAllowed annotations on resource methods + environment.jersey.register(classOf[RolesAllowedDynamicFeature]) + environment.jersey.register(classOf[DatasetResource]) environment.jersey.register(classOf[DatasetAccessResource]) // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL RequestLoggingFilter.register(environment.getApplicationContext) + + SqlServer.initConnection( + StorageConfig.jdbcUrl, + StorageConfig.jdbcUsername, + StorageConfig.jdbcPassword + ) + + // check if the texera dataset bucket exists, if not create it + S3StorageClient.createBucketIfNotExist(StorageConfig.lakefsBucketName) + // ensure the large-binary S3 bucket exists before any workflow execution attempts to use it + S3StorageClient.createBucketIfNotExist(LargeBinaryManager.DEFAULT_BUCKET) + // check if we can connect to the lakeFS service + LakeFSStorageClient.healthCheck() } } diff --git a/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala b/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala index 46457c9454e..4ac9b81773c 100644 --- a/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala +++ b/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala @@ -20,7 +20,7 @@ package org.apache.texera.service.resource import io.dropwizard.auth.Auth -import jakarta.annotation.security.RolesAllowed +import jakarta.annotation.security.{PermitAll, RolesAllowed} import jakarta.ws.rs._ import jakarta.ws.rs.core._ import org.apache.texera.amber.config.StorageConfig @@ -653,6 +653,7 @@ class DatasetResource { } @GET + @PermitAll @Path("/public-presign-download") def getPublicPresignedUrl( @QueryParam("filePath") encodedUrl: String, @@ -663,6 +664,7 @@ class DatasetResource { } @GET + @PermitAll @Path("/public-presign-download-s3") def getPublicPresignedUrlWithS3( @QueryParam("filePath") encodedUrl: String, @@ -1151,6 +1153,7 @@ class DatasetResource { } @GET + @PermitAll @Path("/{name}/publicVersion/list") def getPublicDatasetVersionList( @PathParam("name") did: Integer @@ -1297,6 +1300,7 @@ class DatasetResource { } @GET + @PermitAll @Path("/{did}/publicVersion/{dvid}/rootFileNodes") def retrievePublicDatasetVersionRootFileNodes( @PathParam("did") did: Integer, @@ -1317,6 +1321,7 @@ class DatasetResource { } @GET + @PermitAll @Path("/public/{did}") def getPublicDataset( @PathParam("did") did: Integer @@ -2141,6 +2146,7 @@ class DatasetResource { * @return 307 Temporary Redirect to cover image */ @GET + @PermitAll @Path("/{did}/cover") def getDatasetCover( @PathParam("did") did: Integer, 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 00000000000..04988583587 --- /dev/null +++ b/file-service/src/test/scala/org/apache/texera/service/FileServiceRunSpec.scala @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.texera.service + +import io.dropwizard.core.setup.Environment +import io.dropwizard.jersey.setup.JerseyEnvironment +import io.dropwizard.jetty.MutableServletContextHandler +import io.dropwizard.jetty.setup.ServletEnvironment +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature +import org.mockito.Mockito.{mock, verify, when} +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import scala.util.Try + +class FileServiceRunSpec extends AnyFlatSpec with Matchers { + + // Verifies @RolesAllowed annotations on DatasetResource / DatasetAccessResource + // are actually enforced, which requires RolesAllowedDynamicFeature to be + // registered on the Jersey environment. + "FileService.run" should "register RolesAllowedDynamicFeature on the Jersey environment" in { + val jersey = mock(classOf[JerseyEnvironment]) + val servlets = mock(classOf[ServletEnvironment]) + val context = mock(classOf[MutableServletContextHandler]) + val env = mock(classOf[Environment]) + when(env.jersey).thenReturn(jersey) + when(env.servlets).thenReturn(servlets) + when(env.getApplicationContext).thenReturn(context) + + val service = new FileService + // Jersey registrations happen before the S3/LakeFS health checks. Those + // probes may throw or pass depending on whether a real infra is reachable + // from the test runner — either way the verify below holds. + Try(service.run(mock(classOf[FileServiceConfiguration]), env)) + + verify(jersey).register(classOf[RolesAllowedDynamicFeature]) + } +} diff --git a/file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourcePermissionsSpec.scala b/file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourcePermissionsSpec.scala new file mode 100644 index 00000000000..4b16d391b12 --- /dev/null +++ b/file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourcePermissionsSpec.scala @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.texera.service.resource + +import jakarta.annotation.security.PermitAll +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class DatasetResourcePermissionsSpec extends AnyFlatSpec with Matchers { + + // Once RolesAllowedDynamicFeature is registered in FileService, every method + // on DatasetResource is enforced. These endpoints serve unauthenticated hub + // visitors browsing public datasets — they must remain reachable without a JWT. + private val publicEndpointMethods = Seq( + "getPublicPresignedUrl", + "getPublicPresignedUrlWithS3", + "getPublicDatasetVersionList", + "retrievePublicDatasetVersionRootFileNodes", + "getPublicDataset", + "getDatasetCover" + ) + + publicEndpointMethods.foreach { methodName => + s"DatasetResource.$methodName" should "be @PermitAll so unauthenticated visitors can hit it" in { + val methods = classOf[DatasetResource].getMethods.filter(_.getName == methodName) + methods should not be empty + methods.foreach { m => + withClue(s"method $methodName missing @PermitAll: ") { + m.getAnnotation(classOf[PermitAll]) should not be null + } + } + } + } +} From 3ea0aaf7a31f4b0722c5845be14f3b675db3347c Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Sun, 24 May 2026 17:19:30 -0700 Subject: [PATCH 7/8] reverted fileservice test --- .../apache/texera/service/FileService.scala | 27 ++++----- .../texera/service/FileServiceRunSpec.scala | 55 ------------------- 2 files changed, 12 insertions(+), 70 deletions(-) delete mode 100644 file-service/src/test/scala/org/apache/texera/service/FileServiceRunSpec.scala 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 1d68f1721fc..8542a9383d6 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 @@ -64,9 +64,19 @@ class FileService extends Application[FileServiceConfiguration] with LazyLogging override def run(configuration: FileServiceConfiguration, environment: Environment): Unit = { // Serve backend at /api environment.jersey.setUrlPattern("/api/*") + SqlServer.initConnection( + StorageConfig.jdbcUrl, + StorageConfig.jdbcUsername, + StorageConfig.jdbcPassword + ) + + // check if the texera dataset bucket exists, if not create it + S3StorageClient.createBucketIfNotExist(StorageConfig.lakefsBucketName) + // ensure the large-binary S3 bucket exists before any workflow execution attempts to use it + S3StorageClient.createBucketIfNotExist(LargeBinaryManager.DEFAULT_BUCKET) + // check if we can connect to the lakeFS service + LakeFSStorageClient.healthCheck() - // Wire Jersey first (registrations don't depend on DB/S3 state); infra init - // happens after so unit tests can drive run() with a mocked environment. environment.jersey.register(classOf[SessionHandler]) environment.servlets.setSessionHandler(new SessionHandler) @@ -88,19 +98,6 @@ class FileService extends Application[FileServiceConfiguration] with LazyLogging // Route request logs through SLF4J, controlled by TEXERA_SERVICE_LOG_LEVEL RequestLoggingFilter.register(environment.getApplicationContext) - - SqlServer.initConnection( - StorageConfig.jdbcUrl, - StorageConfig.jdbcUsername, - StorageConfig.jdbcPassword - ) - - // check if the texera dataset bucket exists, if not create it - S3StorageClient.createBucketIfNotExist(StorageConfig.lakefsBucketName) - // ensure the large-binary S3 bucket exists before any workflow execution attempts to use it - S3StorageClient.createBucketIfNotExist(LargeBinaryManager.DEFAULT_BUCKET) - // check if we can connect to the lakeFS service - LakeFSStorageClient.healthCheck() } } 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 deleted file mode 100644 index 04988583587..00000000000 --- a/file-service/src/test/scala/org/apache/texera/service/FileServiceRunSpec.scala +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.texera.service - -import io.dropwizard.core.setup.Environment -import io.dropwizard.jersey.setup.JerseyEnvironment -import io.dropwizard.jetty.MutableServletContextHandler -import io.dropwizard.jetty.setup.ServletEnvironment -import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature -import org.mockito.Mockito.{mock, verify, when} -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers - -import scala.util.Try - -class FileServiceRunSpec extends AnyFlatSpec with Matchers { - - // Verifies @RolesAllowed annotations on DatasetResource / DatasetAccessResource - // are actually enforced, which requires RolesAllowedDynamicFeature to be - // registered on the Jersey environment. - "FileService.run" should "register RolesAllowedDynamicFeature on the Jersey environment" in { - val jersey = mock(classOf[JerseyEnvironment]) - val servlets = mock(classOf[ServletEnvironment]) - val context = mock(classOf[MutableServletContextHandler]) - val env = mock(classOf[Environment]) - when(env.jersey).thenReturn(jersey) - when(env.servlets).thenReturn(servlets) - when(env.getApplicationContext).thenReturn(context) - - val service = new FileService - // Jersey registrations happen before the S3/LakeFS health checks. Those - // probes may throw or pass depending on whether a real infra is reachable - // from the test runner — either way the verify below holds. - Try(service.run(mock(classOf[FileServiceConfiguration]), env)) - - verify(jersey).register(classOf[RolesAllowedDynamicFeature]) - } -} From 38ff523dda23d6d35bf981ad51560c3ea29c1bda Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Sun, 24 May 2026 17:28:51 -0700 Subject: [PATCH 8/8] added jakarta.annotation-api to EPL 2.0 license binary jar --- amber/LICENSE-binary-java | 1 + 1 file changed, 1 insertion(+) diff --git a/amber/LICENSE-binary-java b/amber/LICENSE-binary-java index 86cdb8c8f4d..fba8dd9cd29 100644 --- a/amber/LICENSE-binary-java +++ b/amber/LICENSE-binary-java @@ -631,6 +631,7 @@ licensed with GPL-2.0 with Classpath Exception) -------------------------------------------------------------------------------- Scala/Java jars: + - jakarta.annotation.jakarta.annotation-api-2.1.1.jar - jakarta.ws.rs.jakarta.ws.rs-api-3.0.0.jar - javax.ws.rs.javax.ws.rs-api-2.1.1.jar - org.jgrapht.jgrapht-core-1.4.0.jar