Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e6ffc27
enforce @RolesAllowed on microservice resources
Ma77Ball May 13, 2026
9c49941
Add dropwizard-auth as a dependency for workflow-compiling-service
Ma77Ball May 13, 2026
f029770
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 13, 2026
6f6da20
Added appropriate licenses to License-binary
Ma77Ball May 13, 2026
e513a47
Merge remote-tracking branch 'origin/fix/RolesAllowedUnenforced' into…
Ma77Ball May 13, 2026
76b1eff
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 13, 2026
950bf39
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 13, 2026
3dcfef8
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 13, 2026
a7963fd
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 13, 2026
0a86a8f
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 13, 2026
93f40f3
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 15, 2026
5fda988
Added test
Ma77Ball May 15, 2026
993e28b
Merge remote-tracking branch 'origin/fix/RolesAllowedUnenforced' into…
Ma77Ball May 15, 2026
e1bdf9e
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 19, 2026
46812ef
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 20, 2026
a5ba0e9
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 22, 2026
ec42428
extract registerAuthFeatures to decouple Jersey setup from DB init
Ma77Ball May 22, 2026
df10f86
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 22, 2026
877960f
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 22, 2026
47a21a3
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 23, 2026
70b1760
Merge remote-tracking branch 'upstream/main' into fix/RolesAllowedUne…
Ma77Ball May 24, 2026
6923952
order JwtAuthFilter before authz so @RolesAllowed stops 403ing valid …
Ma77Ball May 25, 2026
ca257df
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 25, 2026
3ea0aaf
reverted fileservice test
Ma77Ball May 25, 2026
84b59b0
Merge remote-tracking branch 'origin/fix/RolesAllowedUnenforced' into…
Ma77Ball May 25, 2026
38ff523
added jakarta.annotation-api to EPL 2.0 license binary jar
Ma77Ball May 25, 2026
8e03082
Merge branch 'main' into fix/RolesAllowedUnenforced
Ma77Ball May 25, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -191,6 +192,7 @@ object AccessControlResource extends LazyLogging {
}
}
@Produces(Array(MediaType.APPLICATION_JSON))
@PermitAll
@Path("/auth")
class AccessControlResource extends LazyLogging {

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ 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
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])
Expand All @@ -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])
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
1 change: 1 addition & 0 deletions amber/LICENSE-binary-java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions common/auth/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
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 @@ -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(
Expand Down Expand Up @@ -64,7 +64,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,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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -89,6 +90,9 @@ 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])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -653,6 +653,7 @@ class DatasetResource {
}

@GET
@PermitAll
@Path("/public-presign-download")
def getPublicPresignedUrl(
@QueryParam("filePath") encodedUrl: String,
Expand All @@ -663,6 +664,7 @@ class DatasetResource {
}

@GET
@PermitAll
@Path("/public-presign-download-s3")
def getPublicPresignedUrlWithS3(
@QueryParam("filePath") encodedUrl: String,
Expand Down Expand Up @@ -1151,6 +1153,7 @@ class DatasetResource {
}

@GET
@PermitAll
@Path("/{name}/publicVersion/list")
def getPublicDatasetVersionList(
@PathParam("name") did: Integer
Expand Down Expand Up @@ -1297,6 +1300,7 @@ class DatasetResource {
}

@GET
@PermitAll
@Path("/{did}/publicVersion/{dvid}/rootFileNodes")
def retrievePublicDatasetVersionRootFileNodes(
@PathParam("did") did: Integer,
Expand All @@ -1317,6 +1321,7 @@ class DatasetResource {
}

@GET
@PermitAll
@Path("/public/{did}")
def getPublicDataset(
@PathParam("did") did: Integer
Expand Down Expand Up @@ -2141,6 +2146,7 @@ class DatasetResource {
* @return 307 Temporary Redirect to cover image
*/
@GET
@PermitAll
@Path("/{did}/cover")
def getDatasetCover(
@PathParam("did") did: Integer,
Expand Down
Loading
Loading