Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
7 changes: 7 additions & 0 deletions computing-unit-managing-service/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand All @@ -53,21 +54,16 @@ class ComputingUnitManagingService extends Application[ComputingUnitManagingServ
configuration: ComputingUnitManagingServiceConfiguration,
environment: Environment
): Unit = {
SqlServer.initConnection(
StorageConfig.jdbcUrl,
StorageConfig.jdbcUsername,
StorageConfig.jdbcPassword
)
// Register http resources
environment.jersey.setUrlPattern("/api/*")
environment.jersey.register(classOf[HealthCheckResource])

// Register JWT authentication filter
environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))
ComputingUnitManagingService.registerAuthFeatures(environment)

// Enable @Auth annotation for injecting SessionUser
environment.jersey.register(
new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])
SqlServer.initConnection(
StorageConfig.jdbcUrl,
StorageConfig.jdbcUsername,
StorageConfig.jdbcPassword
)

environment.jersey().register(new ComputingUnitManagingResource)
Expand All @@ -79,6 +75,19 @@ class ComputingUnitManagingService extends Application[ComputingUnitManagingServ
}

object ComputingUnitManagingService {
// Registers JWT auth, @Auth injection, and @RolesAllowed enforcement.
def registerAuthFeatures(environment: Environment): Unit = {
// Register JWT authentication filter
environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter]))

// Enable @Auth annotation for injecting SessionUser
environment.jersey.register(
new io.dropwizard.auth.AuthValueFactoryProvider.Binder(classOf[SessionUser])
)

// Enforce @RolesAllowed annotations on resource methods
environment.jersey.register(classOf[RolesAllowedDynamicFeature])
}

def main(args: Array[String]): Unit = {
val configFilePath = Path
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.texera.service

import io.dropwizard.auth.{AuthDynamicFeature, AuthValueFactoryProvider}
import io.dropwizard.core.setup.Environment
import io.dropwizard.jersey.setup.JerseyEnvironment
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
import org.mockito.Mockito.{mock, verify, when}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class ComputingUnitManagingServiceRunSpec extends AnyFlatSpec with Matchers {

// Verifies that the @RolesAllowed annotations on resource methods are actually
// enforced by Jersey, which requires RolesAllowedDynamicFeature, AuthDynamicFeature,
// and AuthValueFactoryProvider.Binder to be registered on the Jersey environment.
"ComputingUnitManagingService.registerAuthFeatures" should "register auth + RolesAllowedDynamicFeature on the Jersey environment" in {
val jersey = mock(classOf[JerseyEnvironment])
val env = mock(classOf[Environment])
when(env.jersey).thenReturn(jersey)

ComputingUnitManagingService.registerAuthFeatures(env)

verify(jersey).register(classOf[RolesAllowedDynamicFeature])
verify(jersey).register(org.mockito.ArgumentMatchers.any(classOf[AuthDynamicFeature]))
verify(jersey).register(
org.mockito.ArgumentMatchers.any(classOf[AuthValueFactoryProvider.Binder[_]])
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -28,8 +28,15 @@ import org.apache.texera.config.{AuthConfig, ComputingUnitConfig, GuiConfig, Use
@Produces(Array(MediaType.APPLICATION_JSON))
class ConfigResource {

// The frontend loads /config/gui and /config/user-system as an APP_INITIALIZER
// (GuiConfigService.load() in gui-config.service.ts) — i.e. before any login. They
// must answer unauthenticated callers so the login page can render. PR #5049 left
// @RolesAllowed on both endpoints, which once RolesAllowedDynamicFeature was
// registered started returning 403 during bootstrap and broke the whole app; that
// PR was reverted in #5173. @PermitAll keeps enforcement on for the rest of the
// service while explicitly whitelisting these two pre-login endpoints.
@GET
@RolesAllowed(Array("REGULAR", "ADMIN"))
@PermitAll
@Path("/gui")
def getGuiConfig: Map[String, Any] =
Map(
Expand Down Expand Up @@ -64,7 +71,7 @@ class ConfigResource {
)

@GET
@RolesAllowed(Array("REGULAR", "ADMIN"))
@PermitAll
@Path("/user-system")
def getUserSystemConfig: Map[String, Any] =
Map(
Expand Down
Original file line number Diff line number Diff line change
@@ -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])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.texera.service.resource

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import io.dropwizard.jackson.Jackson
import io.dropwizard.testing.junit5.ResourceExtension
import jakarta.annotation.security.RolesAllowed
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.{GET, Path, Produces}
import org.apache.texera.auth.JwtAuthFilter
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature
import org.scalatest.BeforeAndAfterAll
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

// Wires ConfigResource through the same Jersey auth pipeline production uses
// (JwtAuthFilter + RolesAllowedDynamicFeature) and fires HTTP requests with no
// Authorization header. Regression guard for the bootstrap break that caused
// PR #5049 to be reverted in #5173: /config/gui and /config/user-system are
// loaded by the frontend's APP_INITIALIZER before any login, so they must
// return 200 to unauthenticated callers even with role enforcement enabled.
class ConfigResourceAuthSpec extends AnyFlatSpec with Matchers with BeforeAndAfterAll {

// Companion to ConfigResource that's deliberately @RolesAllowed, so the same
// setup also proves the feature actually rejects when it should — a 200 on
// the @PermitAll endpoints would otherwise be consistent with the feature
// being silently no-op'd.
// Mirror production's mapper: ConfigService bootstraps Dropwizard's default mapper
// (Jackson.newObjectMapper) and registers DefaultScalaModule on top. Same call here.
private val testMapper: ObjectMapper =
Jackson.newObjectMapper().registerModule(DefaultScalaModule)

private val resources: ResourceExtension = ResourceExtension
.builder()
.setMapper(testMapper)
.addProvider(classOf[JwtAuthFilter])
.addProvider(classOf[RolesAllowedDynamicFeature])
.addResource(new ConfigResource)
.addResource(new ConfigResourceAuthSpec.ProtectedProbe)
.build()

override protected def beforeAll(): Unit = resources.before()
override protected def afterAll(): Unit = resources.after()

"GET /config/gui" should "return 200 without an Authorization header" in {
val response = resources.target("/config/gui").request(MediaType.APPLICATION_JSON).get()
response.getStatus shouldBe 200
}

"GET /config/user-system" should "return 200 without an Authorization header" in {
val response =
resources.target("/config/user-system").request(MediaType.APPLICATION_JSON).get()
response.getStatus shouldBe 200
}

"GET an @RolesAllowed endpoint" should "return 403 without an Authorization header" in {
// Sanity: with no SecurityContext set by JwtAuthFilter, RolesAllowedDynamicFeature
// must reject. Catches the case where the feature is registered but somehow
// disabled (e.g. swallowed exception during setup).
val response =
resources.target("/auth-probe").request(MediaType.APPLICATION_JSON).get()
response.getStatus shouldBe 403
}
}

object ConfigResourceAuthSpec {
@Path("/auth-probe")
@Produces(Array(MediaType.APPLICATION_JSON))
class ProtectedProbe {
@GET
@RolesAllowed(Array("REGULAR", "ADMIN"))
def probe: String = "should never reach this"
}
}
3 changes: 3 additions & 0 deletions workflow-compiling-service/LICENSE-binary
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions workflow-compiling-service/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Loading
Loading