From 7a95fbe079f241be4b8c3d5293906d7d556c149d Mon Sep 17 00:00:00 2001 From: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Date: Sun, 3 May 2026 23:37:50 -0700 Subject: [PATCH 1/4] fix(auth): JwtAuthFilter eager-401 with @PermitAll opt-out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Today JwtAuthFilter silently passes through any request that lacks a valid Bearer token; the 401 only surfaces later when Dropwizard's @Auth injection fails. amber's toastshaman path returns 401 directly from its filter with a WWW-Authenticate challenge — strictly more correct. Align the microservice filter: - No `Authorization: Bearer …` header → throw 401 with bare `WWW-Authenticate: Bearer realm="texera"` (RFC 6750 §3 challenge). - Header present but token verification / claim extraction fails → throw 401 with `error="invalid_token"` so a well-behaved client can discard the bad token instead of retrying. - Header present and valid → install SecurityContext as before. @PermitAll opt-out: a resource method (or class) annotated with `jakarta.annotation.security.PermitAll` skips the eager 401 only on the "no header" path. The `@Auth Optional[SessionUser]` parameter is then injected as empty. An invalid token still 401s on @PermitAll endpoints — a tampered or stale token is never silently treated as anonymous. The single in-tree consumer of the optional pattern is `file-service/.../DatasetResource.getDatasetCover` (anonymous read of public dataset covers); annotate it with @PermitAll. Failure is signaled by throwing WebApplicationException rather than abortWith — the JAX-RS-idiomatic shape, plus it composes with Dropwizard's WebApplicationExceptionCatchingFilter when reused elsewhere. Tests: 9-case JwtAuthFilterSpec covering required-auth (no header / non-Bearer / unverifiable / valid), method-level @PermitAll (unauthenticated → pass / invalid token → 401 / valid → SecurityContext), class-level @PermitAll, and resourceInfo-absent fallback to required-auth. Common/auth gains two test-scope deps (jakarta.annotation-api for @PermitAll inspection; jersey-common to provide a RuntimeDelegate so Response.build() works in unit tests without a Jersey runtime). Closes #4901 --- common/auth/build.sbt | 4 +- .../apache/texera/auth/JwtAuthFilter.scala | 82 +++++-- .../texera/auth/JwtAuthFilterSpec.scala | 211 ++++++++++++++++++ .../service/resource/DatasetResource.scala | 7 +- 4 files changed, 284 insertions(+), 20 deletions(-) create mode 100644 common/auth/src/test/scala/org/apache/texera/auth/JwtAuthFilterSpec.scala diff --git a/common/auth/build.sbt b/common/auth/build.sbt index a33da64fea5..92676f6f9af 100644 --- a/common/auth/build.sbt +++ b/common/auth/build.sbt @@ -57,7 +57,9 @@ 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 @PermitAll opt-out in JwtAuthFilter "jakarta.servlet" % "jakarta.servlet-api" % "5.0.0" % "provided", // for RequestLoggingFilter "org.eclipse.jetty" % "jetty-servlet" % "11.0.24" % "provided", // for FilterHolder - "org.scalatest" %% "scalatest" % "3.2.17" % Test + "org.scalatest" %% "scalatest" % "3.2.17" % Test, + "org.glassfish.jersey.core" % "jersey-common" % "3.0.12" % Test // for RuntimeDelegate in JwtAuthFilterSpec ) \ No newline at end of file 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..cb54951aa55 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,35 +20,81 @@ package org.apache.texera.auth import com.typesafe.scalalogging.LazyLogging -import jakarta.ws.rs.container.{ContainerRequestContext, ContainerRequestFilter} -import jakarta.ws.rs.core.{HttpHeaders, SecurityContext} +import jakarta.annotation.security.PermitAll +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.container.{ContainerRequestContext, ContainerRequestFilter, ResourceInfo} +import jakarta.ws.rs.core.{Context, HttpHeaders, Response, SecurityContext} import jakarta.ws.rs.ext.Provider import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum import java.security.Principal +/** JAX-RS request filter that authenticates a Bearer JWT and installs a + * [[SessionUser]] security context. + * + * Failure semantics (RFC 6750): + * - No `Authorization: Bearer …` header: throw `401` with a bare + * `WWW-Authenticate: Bearer realm="texera"` challenge — unless the + * resource method or class is annotated with `@PermitAll`, in which + * case the request continues with no security context. This supports + * the `@Auth Optional[SessionUser]` pattern for endpoints that need + * to serve anonymous users. + * - Header present but token verification / claim extraction fails: + * throw `401` with `error="invalid_token"` always, even on `@PermitAll` + * endpoints — a tampered or stale token is never silently treated as + * anonymous. + * - Header present and valid: install a `SecurityContext` whose + * principal is the parsed [[SessionUser]]. + */ @Provider class JwtAuthFilter extends ContainerRequestFilter with LazyLogging { + @Context + private var resourceInfo: ResourceInfo = _ + override def filter(requestContext: ContainerRequestContext): Unit = { val authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION) - if (authHeader != null && authHeader.startsWith("Bearer ")) { - val token = authHeader.substring(7) // Remove "Bearer " prefix - val userOpt = JwtParser.parseToken(token) - - if (userOpt.isPresent) { - val user = userOpt.get() - requestContext.setSecurityContext(new SecurityContext { - override def getUserPrincipal: Principal = user - override def isUserInRole(role: String): Boolean = - user.isRoleOf(UserRoleEnum.valueOf(role)) - override def isSecure: Boolean = false - override def getAuthenticationScheme: String = "Bearer" - }) - } else { - logger.warn("Invalid JWT: Unable to parse token") - } + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + if (isPermitAll) return + throw new WebApplicationException(unauthorized(BearerChallenge)) + } + + val token = authHeader.substring(7) // Remove "Bearer " prefix + val userOpt = JwtParser.parseToken(token) + if (!userOpt.isPresent) { + logger.warn("Invalid JWT: Unable to parse token") + throw new WebApplicationException(unauthorized(InvalidTokenChallenge)) } + + val user = userOpt.get() + requestContext.setSecurityContext(new SecurityContext { + override def getUserPrincipal: Principal = user + override def isUserInRole(role: String): Boolean = + user.isRoleOf(UserRoleEnum.valueOf(role)) + override def isSecure: Boolean = false + override def getAuthenticationScheme: String = "Bearer" + }) } + + private def isPermitAll: Boolean = { + if (resourceInfo == null) return false + val m = resourceInfo.getResourceMethod + val c = resourceInfo.getResourceClass + (m != null && m.isAnnotationPresent(classOf[PermitAll])) || + (c != null && c.isAnnotationPresent(classOf[PermitAll])) + } + + // RFC 6750 §3: the bare challenge means "please authenticate"; the + // `error="invalid_token"` parameter signals "the token you sent is + // malformed / expired / signature failed" so a well-behaved client can + // discard it instead of retrying. + private val BearerChallenge = "Bearer realm=\"texera\"" + private val InvalidTokenChallenge = "Bearer realm=\"texera\", error=\"invalid_token\"" + + private def unauthorized(challenge: String): Response = + Response + .status(Response.Status.UNAUTHORIZED) + .header(HttpHeaders.WWW_AUTHENTICATE, challenge) + .build() } 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..ab03c5058cf --- /dev/null +++ b/common/auth/src/test/scala/org/apache/texera/auth/JwtAuthFilterSpec.scala @@ -0,0 +1,211 @@ +/* + * 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.security.PermitAll +import jakarta.ws.rs.WebApplicationException +import jakarta.ws.rs.container.{ContainerRequestContext, ResourceInfo} +import jakarta.ws.rs.core.{HttpHeaders, Response, SecurityContext} +import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum +import org.jose4j.jwt.JwtClaims +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import java.lang.reflect.{Field, Method} +import java.util.concurrent.atomic.AtomicReference + +class JwtAuthFilterSpec extends AnyFlatSpec with Matchers { + + // Minimal stand-in for a request context. Only the methods the filter + // actually touches are wired up; the rest are unimplemented. + private class StubRequestContext(authHeader: String) extends ContainerRequestContext { + val securityContext = new AtomicReference[SecurityContext](null) + + override def getHeaderString(name: String): String = + if (name == HttpHeaders.AUTHORIZATION) authHeader else null + override def setSecurityContext(context: SecurityContext): Unit = securityContext.set(context) + override def getSecurityContext: SecurityContext = securityContext.get() + + // unused + override def abortWith(response: Response): Unit = () + override def getProperty(x$1: String): Object = null + override def getPropertyNames: java.util.Collection[String] = + java.util.Collections.emptyList() + override def setProperty(x$1: String, x$2: Object): Unit = () + override def removeProperty(x$1: String): Unit = () + override def getRequest: jakarta.ws.rs.core.Request = null + override def getMethod: String = null + override def setMethod(x$1: String): Unit = () + override def getUriInfo: jakarta.ws.rs.core.UriInfo = null + override def setRequestUri(x$1: java.net.URI): Unit = () + override def setRequestUri(x$1: java.net.URI, x$2: java.net.URI): Unit = () + override def getHeaders: jakarta.ws.rs.core.MultivaluedMap[String, String] = null + override def getCookies: java.util.Map[String, jakarta.ws.rs.core.Cookie] = null + override def getDate: java.util.Date = null + override def getLanguage: java.util.Locale = null + override def getLength: Int = 0 + override def getMediaType: jakarta.ws.rs.core.MediaType = null + override def getAcceptableMediaTypes: java.util.List[jakarta.ws.rs.core.MediaType] = null + override def getAcceptableLanguages: java.util.List[java.util.Locale] = null + override def hasEntity: Boolean = false + override def getEntityStream: java.io.InputStream = null + override def setEntityStream(x$1: java.io.InputStream): Unit = () + } + + private def buildClaims(): JwtClaims = { + val c = new JwtClaims + c.setSubject("alice") + c.setClaim("userId", 42) + c.setClaim("googleId", "g-123") + c.setClaim("email", "alice@example.com") + c.setClaim("role", UserRoleEnum.ADMIN.name) + c.setClaim("googleAvatar", "avatar") + c.setExpirationTimeMinutesInTheFuture(10f) + c + } + + private def challenge(thrown: WebApplicationException): String = { + thrown.getResponse.getStatus shouldBe 401 + thrown.getResponse.getHeaderString(HttpHeaders.WWW_AUTHENTICATE) + } + + // -------------------- tests -------------------- + + "JwtAuthFilter" should "challenge with bare Bearer realm when no Authorization header is present" in { + val filter = new JwtAuthFilter + val ctx = new StubRequestContext(null) + val thrown = the[WebApplicationException] thrownBy filter.filter(ctx) + challenge(thrown) shouldBe "Bearer realm=\"texera\"" + ctx.getSecurityContext shouldBe null + } + + it should "challenge with bare Bearer realm when the header is not a Bearer token" in { + val filter = new JwtAuthFilter + val ctx = new StubRequestContext("Basic abc") + val thrown = the[WebApplicationException] thrownBy filter.filter(ctx) + challenge(thrown) shouldBe "Bearer realm=\"texera\"" + } + + it should "challenge with error=invalid_token when the Bearer token cannot be verified" in { + val filter = new JwtAuthFilter + val ctx = new StubRequestContext("Bearer not-a-real-jwt") + val thrown = the[WebApplicationException] thrownBy filter.filter(ctx) + challenge(thrown) shouldBe "Bearer realm=\"texera\", error=\"invalid_token\"" + } + + it should "install a SecurityContext with the parsed SessionUser when the token is valid" in { + val filter = new JwtAuthFilter + val ctx = new StubRequestContext(s"Bearer ${JwtAuth.jwtToken(buildClaims())}") + + filter.filter(ctx) + + val sc = ctx.getSecurityContext + sc should not be null + sc.getUserPrincipal.asInstanceOf[SessionUser].getUid shouldBe 42 + sc.getAuthenticationScheme shouldBe "Bearer" + sc.isUserInRole(UserRoleEnum.ADMIN.name) shouldBe true + sc.isUserInRole(UserRoleEnum.REGULAR.name) shouldBe false + } + + // -------------------- @PermitAll opt-out -------------------- + + private class RequiredAuthResource { def secured(): Unit = () } + private class OptionalAuthResource { @PermitAll def cover(): Unit = () } + @PermitAll private class OpenResource { def anything(): Unit = () } + + private def methodOf(cls: Class[_], name: String): Method = + cls.getDeclaredMethods.find(_.getName == name).get + + private def withResourceInfo(filter: JwtAuthFilter, info: ResourceInfo): Unit = { + val f: Field = classOf[JwtAuthFilter].getDeclaredField("resourceInfo") + f.setAccessible(true) + f.set(filter, info) + } + + private class StubResourceInfo(method: Method, cls: Class[_]) extends ResourceInfo { + override def getResourceMethod: Method = method + override def getResourceClass: Class[_] = cls + } + + "JwtAuthFilter on a @PermitAll method" should "let an unauthenticated request pass through with no SecurityContext" in { + val filter = new JwtAuthFilter + withResourceInfo( + filter, + new StubResourceInfo( + methodOf(classOf[OptionalAuthResource], "cover"), + classOf[OptionalAuthResource] + ) + ) + val ctx = new StubRequestContext(null) + + filter.filter(ctx) // must NOT throw + ctx.getSecurityContext shouldBe null + } + + it should "still 401 when a token is supplied but invalid (tampered or stale)" in { + val filter = new JwtAuthFilter + withResourceInfo( + filter, + new StubResourceInfo( + methodOf(classOf[OptionalAuthResource], "cover"), + classOf[OptionalAuthResource] + ) + ) + val ctx = new StubRequestContext("Bearer not-a-real-jwt") + + val thrown = the[WebApplicationException] thrownBy filter.filter(ctx) + challenge(thrown) shouldBe "Bearer realm=\"texera\", error=\"invalid_token\"" + } + + it should "install a SecurityContext when a valid token is supplied" in { + val filter = new JwtAuthFilter + withResourceInfo( + filter, + new StubResourceInfo( + methodOf(classOf[OptionalAuthResource], "cover"), + classOf[OptionalAuthResource] + ) + ) + val ctx = new StubRequestContext(s"Bearer ${JwtAuth.jwtToken(buildClaims())}") + + filter.filter(ctx) + ctx.getSecurityContext.getUserPrincipal.asInstanceOf[SessionUser].getUid shouldBe 42 + } + + "JwtAuthFilter on a class-level @PermitAll" should "honor the class annotation when the method has none" in { + val filter = new JwtAuthFilter + withResourceInfo( + filter, + new StubResourceInfo(methodOf(classOf[OpenResource], "anything"), classOf[OpenResource]) + ) + val ctx = new StubRequestContext(null) + + filter.filter(ctx) // must NOT throw + ctx.getSecurityContext shouldBe null + } + + "JwtAuthFilter without resource info" should "default to required-auth (eager 401)" in { + val filter = new JwtAuthFilter + // resourceInfo left as null — pre-matching path or test scenario + val ctx = new StubRequestContext(null) + val thrown = the[WebApplicationException] thrownBy filter.filter(ctx) + challenge(thrown) shouldBe "Bearer realm=\"texera\"" + } +} 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 7bcd3bb77c3..afabf946913 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 @@ -2148,6 +2148,11 @@ class DatasetResource { */ @GET @Path("/{did}/cover") + // Anonymous callers may read covers of public datasets; access checks + // below still gate everything else. JwtAuthFilter inspects @PermitAll + // to skip its eager 401 when no Bearer header is present, so the + // @Auth Optional[SessionUser] parameter is injected as empty. + @PermitAll def getDatasetCover( @PathParam("did") did: Integer, @Auth sessionUser: Optional[SessionUser] From 5048f371e38efb058d8e2fa4867216db9ce10561 Mon Sep 17 00:00:00 2001 From: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Date: Sun, 3 May 2026 23:58:34 -0700 Subject: [PATCH 2/4] fixup: declare jakarta.annotation-api in amber LICENSE-binary-java --- amber/LICENSE-binary-java | 1 + 1 file changed, 1 insertion(+) diff --git a/amber/LICENSE-binary-java b/amber/LICENSE-binary-java index d82f69bd837..56ed1ff916d 100644 --- a/amber/LICENSE-binary-java +++ b/amber/LICENSE-binary-java @@ -630,6 +630,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 From 6ae2c4489f19b4a0bc5ce78cdfb2e2b2a3aad29e Mon Sep 17 00:00:00 2001 From: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Date: Mon, 4 May 2026 00:39:08 -0700 Subject: [PATCH 3/4] fixup: throw UnauthorizedException + ExceptionMapper instead of WAE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Building a JAX-RS Response from inside the filter requires a RuntimeDelegate on the classpath. The earlier attempt added jersey-common 3.0.12 to common/auth's test classpath to satisfy that; amber declares `Auth % "test->test"` in build.sbt, so this jar bled into amber's test classpath and triggered a LinkageError on javax.ws.rs.ext.RuntimeDelegate (mismatched ClassLoader on the same class) — breaking 4 WorkflowAccessResourceSpec tests. Avoid the dependency entirely: throw a custom `UnauthorizedException` (extends RuntimeException) carrying the RFC 6750 §3 challenge string as a field. A new `UnauthorizedExceptionMapper` translates it to a `401` with `WWW-Authenticate` at the JAX-RS edge. Tests inspect the field directly — no Response build, no RuntimeDelegate needed, no test-classpath cross-contamination. Side benefits: `BearerChallenge` and `InvalidTokenChallenge` move to the JwtAuthFilter companion as exposed `val`s, so the contract is asserted via plain string equality. Each of the 4 microservices that registers `JwtAuthFilter` now also registers `UnauthorizedExceptionMapper`. amber's existing tests pass unchanged. --- .../texera/service/AccessControlService.scala | 3 +- common/auth/build.sbt | 3 +- .../apache/texera/auth/JwtAuthFilter.scala | 37 +++--- .../texera/auth/UnauthorizedException.scala | 46 +++++++ .../texera/auth/JwtAuthFilterSpec.scala | 114 +++++++++++------- .../ComputingUnitManagingService.scala | 3 +- .../apache/texera/service/ConfigService.scala | 3 +- .../apache/texera/service/FileService.scala | 3 +- 8 files changed, 144 insertions(+), 68 deletions(-) create mode 100644 common/auth/src/main/scala/org/apache/texera/auth/UnauthorizedException.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..a629e803893 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 @@ -24,7 +24,7 @@ import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, Substituting import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig -import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser} +import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser, UnauthorizedExceptionMapper} import org.apache.texera.dao.SqlServer import org.apache.texera.service.activity.UserActivityEventListener import org.apache.texera.service.resource.{ @@ -72,6 +72,7 @@ class AccessControlService extends Application[AccessControlServiceConfiguration // Register JWT authentication filter environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) + environment.jersey.register(classOf[UnauthorizedExceptionMapper]) // Enable @Auth annotation for injecting SessionUser environment.jersey.register( diff --git a/common/auth/build.sbt b/common/auth/build.sbt index 92676f6f9af..24feb1984f8 100644 --- a/common/auth/build.sbt +++ b/common/auth/build.sbt @@ -60,6 +60,5 @@ libraryDependencies ++= Seq( "jakarta.annotation" % "jakarta.annotation-api" % "2.1.1", // for @PermitAll opt-out in JwtAuthFilter "jakarta.servlet" % "jakarta.servlet-api" % "5.0.0" % "provided", // for RequestLoggingFilter "org.eclipse.jetty" % "jetty-servlet" % "11.0.24" % "provided", // for FilterHolder - "org.scalatest" %% "scalatest" % "3.2.17" % Test, - "org.glassfish.jersey.core" % "jersey-common" % "3.0.12" % Test // for RuntimeDelegate in JwtAuthFilterSpec + "org.scalatest" %% "scalatest" % "3.2.17" % Test ) \ No newline at end of file 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 cb54951aa55..76274297479 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 @@ -21,9 +21,8 @@ package org.apache.texera.auth import com.typesafe.scalalogging.LazyLogging import jakarta.annotation.security.PermitAll -import jakarta.ws.rs.WebApplicationException import jakarta.ws.rs.container.{ContainerRequestContext, ContainerRequestFilter, ResourceInfo} -import jakarta.ws.rs.core.{Context, HttpHeaders, Response, SecurityContext} +import jakarta.ws.rs.core.{Context, HttpHeaders, SecurityContext} import jakarta.ws.rs.ext.Provider import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum @@ -32,19 +31,23 @@ import java.security.Principal /** JAX-RS request filter that authenticates a Bearer JWT and installs a * [[SessionUser]] security context. * - * Failure semantics (RFC 6750): - * - No `Authorization: Bearer …` header: throw `401` with a bare - * `WWW-Authenticate: Bearer realm="texera"` challenge — unless the + * Failure semantics (RFC 6750 §3): + * - No `Authorization: Bearer …` header: throw [[UnauthorizedException]] + * carrying a bare `Bearer realm="texera"` challenge — unless the * resource method or class is annotated with `@PermitAll`, in which * case the request continues with no security context. This supports * the `@Auth Optional[SessionUser]` pattern for endpoints that need * to serve anonymous users. * - Header present but token verification / claim extraction fails: - * throw `401` with `error="invalid_token"` always, even on `@PermitAll` - * endpoints — a tampered or stale token is never silently treated as - * anonymous. + * throw [[UnauthorizedException]] with `error="invalid_token"` + * always, even on `@PermitAll` endpoints — a tampered or stale token + * is never silently treated as anonymous. * - Header present and valid: install a `SecurityContext` whose * principal is the parsed [[SessionUser]]. + * + * HTTP translation (status 401, `WWW-Authenticate` header) is done by + * [[UnauthorizedExceptionMapper]], registered alongside this filter in + * each service. */ @Provider class JwtAuthFilter extends ContainerRequestFilter with LazyLogging { @@ -57,14 +60,14 @@ class JwtAuthFilter extends ContainerRequestFilter with LazyLogging { if (authHeader == null || !authHeader.startsWith("Bearer ")) { if (isPermitAll) return - throw new WebApplicationException(unauthorized(BearerChallenge)) + throw new UnauthorizedException(JwtAuthFilter.BearerChallenge) } val token = authHeader.substring(7) // Remove "Bearer " prefix val userOpt = JwtParser.parseToken(token) if (!userOpt.isPresent) { logger.warn("Invalid JWT: Unable to parse token") - throw new WebApplicationException(unauthorized(InvalidTokenChallenge)) + throw new UnauthorizedException(JwtAuthFilter.InvalidTokenChallenge) } val user = userOpt.get() @@ -84,17 +87,13 @@ class JwtAuthFilter extends ContainerRequestFilter with LazyLogging { (m != null && m.isAnnotationPresent(classOf[PermitAll])) || (c != null && c.isAnnotationPresent(classOf[PermitAll])) } +} - // RFC 6750 §3: the bare challenge means "please authenticate"; the +object JwtAuthFilter { + // RFC 6750 §3: bare challenge = "please authenticate". The // `error="invalid_token"` parameter signals "the token you sent is // malformed / expired / signature failed" so a well-behaved client can // discard it instead of retrying. - private val BearerChallenge = "Bearer realm=\"texera\"" - private val InvalidTokenChallenge = "Bearer realm=\"texera\", error=\"invalid_token\"" - - private def unauthorized(challenge: String): Response = - Response - .status(Response.Status.UNAUTHORIZED) - .header(HttpHeaders.WWW_AUTHENTICATE, challenge) - .build() + val BearerChallenge: String = "Bearer realm=\"texera\"" + val InvalidTokenChallenge: String = "Bearer realm=\"texera\", error=\"invalid_token\"" } diff --git a/common/auth/src/main/scala/org/apache/texera/auth/UnauthorizedException.scala b/common/auth/src/main/scala/org/apache/texera/auth/UnauthorizedException.scala new file mode 100644 index 00000000000..74375e4e362 --- /dev/null +++ b/common/auth/src/main/scala/org/apache/texera/auth/UnauthorizedException.scala @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.texera.auth + +import jakarta.ws.rs.core.{HttpHeaders, Response} +import jakarta.ws.rs.ext.{ExceptionMapper, Provider} + +/** Carries an RFC 6750 §3 `WWW-Authenticate: Bearer …` challenge to be + * returned alongside a `401 Unauthorized` response. + * + * Extends `RuntimeException` (not `WebApplicationException`) so it can be + * constructed without a JAX-RS `RuntimeDelegate` on the classpath, which + * keeps unit tests for [[JwtAuthFilter]] independent of any Jersey + * implementation. The companion [[UnauthorizedExceptionMapper]] converts + * the exception to the actual HTTP response at the JAX-RS edge. + */ +class UnauthorizedException(val challenge: String) extends RuntimeException(challenge) + +/** Maps [[UnauthorizedException]] to a `401` response with the carried + * `WWW-Authenticate` challenge header. + */ +@Provider +class UnauthorizedExceptionMapper extends ExceptionMapper[UnauthorizedException] { + override def toResponse(e: UnauthorizedException): Response = + Response + .status(Response.Status.UNAUTHORIZED) + .header(HttpHeaders.WWW_AUTHENTICATE, e.challenge) + .build() +} 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 index ab03c5058cf..6944e0d0f63 100644 --- a/common/auth/src/test/scala/org/apache/texera/auth/JwtAuthFilterSpec.scala +++ b/common/auth/src/test/scala/org/apache/texera/auth/JwtAuthFilterSpec.scala @@ -20,7 +20,6 @@ package org.apache.texera.auth import jakarta.annotation.security.PermitAll -import jakarta.ws.rs.WebApplicationException import jakarta.ws.rs.container.{ContainerRequestContext, ResourceInfo} import jakarta.ws.rs.core.{HttpHeaders, Response, SecurityContext} import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum @@ -33,8 +32,9 @@ import java.util.concurrent.atomic.AtomicReference class JwtAuthFilterSpec extends AnyFlatSpec with Matchers { - // Minimal stand-in for a request context. Only the methods the filter - // actually touches are wired up; the rest are unimplemented. + // Minimal stand-in for ContainerRequestContext. The filter only reads the + // Authorization header and writes a SecurityContext; everything else is + // unimplemented. private class StubRequestContext(authHeader: String) extends ContainerRequestContext { val securityContext = new AtomicReference[SecurityContext](null) @@ -69,6 +69,26 @@ class JwtAuthFilterSpec extends AnyFlatSpec with Matchers { override def setEntityStream(x$1: java.io.InputStream): Unit = () } + // Inject @Context ResourceInfo via reflection so tests can flip annotation + // states per-case without spinning up Jersey. + private def withResourceInfo(filter: JwtAuthFilter, info: ResourceInfo): Unit = { + val f: Field = classOf[JwtAuthFilter].getDeclaredField("resourceInfo") + f.setAccessible(true) + f.set(filter, info) + } + + private class StubResourceInfo(method: Method, cls: Class[_]) extends ResourceInfo { + override def getResourceMethod: Method = method + override def getResourceClass: Class[_] = cls + } + + private def methodOf(cls: Class[_], name: String): Method = + cls.getDeclaredMethods.find(_.getName == name).get + + private class RequiredAuthResource { def secured(): Unit = () } + private class OptionalAuthResource { @PermitAll def cover(): Unit = () } + @PermitAll private class OpenResource { def anything(): Unit = () } + private def buildClaims(): JwtClaims = { val c = new JwtClaims c.setSubject("alice") @@ -81,37 +101,67 @@ class JwtAuthFilterSpec extends AnyFlatSpec with Matchers { c } - private def challenge(thrown: WebApplicationException): String = { - thrown.getResponse.getStatus shouldBe 401 - thrown.getResponse.getHeaderString(HttpHeaders.WWW_AUTHENTICATE) + // -------------------- challenge constants -------------------- + + "JwtAuthFilter constants" should "match RFC 6750 §3 challenge syntax" in { + JwtAuthFilter.BearerChallenge shouldBe "Bearer realm=\"texera\"" + JwtAuthFilter.InvalidTokenChallenge shouldBe "Bearer realm=\"texera\", error=\"invalid_token\"" } - // -------------------- tests -------------------- + // -------------------- required-auth method -------------------- - "JwtAuthFilter" should "challenge with bare Bearer realm when no Authorization header is present" in { + "JwtAuthFilter on a required-auth method" should "throw UnauthorizedException(BearerChallenge) when no Authorization header is present" in { val filter = new JwtAuthFilter + withResourceInfo( + filter, + new StubResourceInfo( + methodOf(classOf[RequiredAuthResource], "secured"), + classOf[RequiredAuthResource] + ) + ) val ctx = new StubRequestContext(null) - val thrown = the[WebApplicationException] thrownBy filter.filter(ctx) - challenge(thrown) shouldBe "Bearer realm=\"texera\"" + val thrown = the[UnauthorizedException] thrownBy filter.filter(ctx) + thrown.challenge shouldBe JwtAuthFilter.BearerChallenge ctx.getSecurityContext shouldBe null } - it should "challenge with bare Bearer realm when the header is not a Bearer token" in { + it should "throw UnauthorizedException(BearerChallenge) when the header is not a Bearer token" in { val filter = new JwtAuthFilter + withResourceInfo( + filter, + new StubResourceInfo( + methodOf(classOf[RequiredAuthResource], "secured"), + classOf[RequiredAuthResource] + ) + ) val ctx = new StubRequestContext("Basic abc") - val thrown = the[WebApplicationException] thrownBy filter.filter(ctx) - challenge(thrown) shouldBe "Bearer realm=\"texera\"" + val thrown = the[UnauthorizedException] thrownBy filter.filter(ctx) + thrown.challenge shouldBe JwtAuthFilter.BearerChallenge } - it should "challenge with error=invalid_token when the Bearer token cannot be verified" in { + it should "throw UnauthorizedException(InvalidTokenChallenge) when the Bearer token cannot be verified" in { val filter = new JwtAuthFilter + withResourceInfo( + filter, + new StubResourceInfo( + methodOf(classOf[RequiredAuthResource], "secured"), + classOf[RequiredAuthResource] + ) + ) val ctx = new StubRequestContext("Bearer not-a-real-jwt") - val thrown = the[WebApplicationException] thrownBy filter.filter(ctx) - challenge(thrown) shouldBe "Bearer realm=\"texera\", error=\"invalid_token\"" + val thrown = the[UnauthorizedException] thrownBy filter.filter(ctx) + thrown.challenge shouldBe JwtAuthFilter.InvalidTokenChallenge } it should "install a SecurityContext with the parsed SessionUser when the token is valid" in { val filter = new JwtAuthFilter + withResourceInfo( + filter, + new StubResourceInfo( + methodOf(classOf[RequiredAuthResource], "secured"), + classOf[RequiredAuthResource] + ) + ) val ctx = new StubRequestContext(s"Bearer ${JwtAuth.jwtToken(buildClaims())}") filter.filter(ctx) @@ -126,24 +176,6 @@ class JwtAuthFilterSpec extends AnyFlatSpec with Matchers { // -------------------- @PermitAll opt-out -------------------- - private class RequiredAuthResource { def secured(): Unit = () } - private class OptionalAuthResource { @PermitAll def cover(): Unit = () } - @PermitAll private class OpenResource { def anything(): Unit = () } - - private def methodOf(cls: Class[_], name: String): Method = - cls.getDeclaredMethods.find(_.getName == name).get - - private def withResourceInfo(filter: JwtAuthFilter, info: ResourceInfo): Unit = { - val f: Field = classOf[JwtAuthFilter].getDeclaredField("resourceInfo") - f.setAccessible(true) - f.set(filter, info) - } - - private class StubResourceInfo(method: Method, cls: Class[_]) extends ResourceInfo { - override def getResourceMethod: Method = method - override def getResourceClass: Class[_] = cls - } - "JwtAuthFilter on a @PermitAll method" should "let an unauthenticated request pass through with no SecurityContext" in { val filter = new JwtAuthFilter withResourceInfo( @@ -154,12 +186,11 @@ class JwtAuthFilterSpec extends AnyFlatSpec with Matchers { ) ) val ctx = new StubRequestContext(null) - filter.filter(ctx) // must NOT throw ctx.getSecurityContext shouldBe null } - it should "still 401 when a token is supplied but invalid (tampered or stale)" in { + it should "still throw UnauthorizedException(InvalidTokenChallenge) when a token is supplied but invalid" in { val filter = new JwtAuthFilter withResourceInfo( filter, @@ -169,9 +200,8 @@ class JwtAuthFilterSpec extends AnyFlatSpec with Matchers { ) ) val ctx = new StubRequestContext("Bearer not-a-real-jwt") - - val thrown = the[WebApplicationException] thrownBy filter.filter(ctx) - challenge(thrown) shouldBe "Bearer realm=\"texera\", error=\"invalid_token\"" + val thrown = the[UnauthorizedException] thrownBy filter.filter(ctx) + thrown.challenge shouldBe JwtAuthFilter.InvalidTokenChallenge } it should "install a SecurityContext when a valid token is supplied" in { @@ -184,7 +214,6 @@ class JwtAuthFilterSpec extends AnyFlatSpec with Matchers { ) ) val ctx = new StubRequestContext(s"Bearer ${JwtAuth.jwtToken(buildClaims())}") - filter.filter(ctx) ctx.getSecurityContext.getUserPrincipal.asInstanceOf[SessionUser].getUid shouldBe 42 } @@ -196,7 +225,6 @@ class JwtAuthFilterSpec extends AnyFlatSpec with Matchers { new StubResourceInfo(methodOf(classOf[OpenResource], "anything"), classOf[OpenResource]) ) val ctx = new StubRequestContext(null) - filter.filter(ctx) // must NOT throw ctx.getSecurityContext shouldBe null } @@ -205,7 +233,7 @@ class JwtAuthFilterSpec extends AnyFlatSpec with Matchers { val filter = new JwtAuthFilter // resourceInfo left as null — pre-matching path or test scenario val ctx = new StubRequestContext(null) - val thrown = the[WebApplicationException] thrownBy filter.filter(ctx) - challenge(thrown) shouldBe "Bearer realm=\"texera\"" + val thrown = the[UnauthorizedException] thrownBy filter.filter(ctx) + thrown.challenge shouldBe JwtAuthFilter.BearerChallenge } } 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..d852468c913 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 @@ -25,7 +25,7 @@ import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, Substituting import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig -import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser} +import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser, UnauthorizedExceptionMapper} import org.apache.texera.dao.SqlServer import org.apache.texera.service.resource.{ ComputingUnitAccessResource, @@ -64,6 +64,7 @@ class ComputingUnitManagingService extends Application[ComputingUnitManagingServ // Register JWT authentication filter environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) + environment.jersey.register(classOf[UnauthorizedExceptionMapper]) // Enable @Auth annotation for injecting SessionUser environment.jersey.register( 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..fda956e195b 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 @@ -26,7 +26,7 @@ import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, Substituting import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig -import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser} +import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser, UnauthorizedExceptionMapper} import org.apache.texera.config.DefaultsConfig import org.apache.texera.dao.SqlServer import org.apache.texera.service.resource.{ConfigResource, HealthCheckResource} @@ -65,6 +65,7 @@ class ConfigService extends Application[ConfigServiceConfiguration] with LazyLog // Register JWT authentication filter environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) + environment.jersey.register(classOf[UnauthorizedExceptionMapper]) // Enable @Auth annotation for injecting SessionUser environment.jersey.register( 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..ece4b225c2e 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 @@ -28,7 +28,7 @@ import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig import org.apache.texera.amber.core.storage.util.LakeFSStorageClient -import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser} +import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser, UnauthorizedExceptionMapper} import org.apache.texera.dao.SqlServer import org.apache.texera.service.`type`.DatasetFileNode import org.apache.texera.service.`type`.serde.DatasetFileNodeSerializer @@ -83,6 +83,7 @@ class FileService extends Application[FileServiceConfiguration] with LazyLogging // Register JWT authentication filter environment.jersey.register(new AuthDynamicFeature(classOf[JwtAuthFilter])) + environment.jersey.register(classOf[UnauthorizedExceptionMapper]) // Enable @Auth annotation for injecting SessionUser environment.jersey.register( From 655db3b46b370366c8e8830c39e0870207b06ffc Mon Sep 17 00:00:00 2001 From: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Date: Wed, 6 May 2026 00:19:25 -0700 Subject: [PATCH 4/4] fixup: scalafmt + UnauthorizedExceptionMapper unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scalafmt: rewrap the 4-symbol auth import line in each microservice's Application class — the previous one-liner blew the column limit once `UnauthorizedExceptionMapper` joined the import list. Tests: add `UnauthorizedExceptionMapperSpec` covering the three things that matter at the JAX-RS edge — status is 401, the exception's challenge string is forwarded verbatim into `WWW-Authenticate`, and the response has no entity body (so clients see the auth challenge instead of a JSON error). Test classpath needs a Jersey RuntimeDelegate to call `Response.build()`, so add `dropwizard-jersey` as a Test-only dep on common/auth. --- .../texera/service/AccessControlService.scala | 7 ++- .../UnauthorizedExceptionMapperSpec.scala | 59 +++++++++++++++++++ .../ComputingUnitManagingService.scala | 7 ++- .../apache/texera/service/ConfigService.scala | 7 ++- .../apache/texera/service/FileService.scala | 7 ++- 5 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 access-control-service/src/test/scala/org/apache/texera/auth/UnauthorizedExceptionMapperSpec.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 a629e803893..0cb5738419e 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 @@ -24,7 +24,12 @@ import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, Substituting import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig -import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser, UnauthorizedExceptionMapper} +import org.apache.texera.auth.{ + JwtAuthFilter, + RequestLoggingFilter, + SessionUser, + UnauthorizedExceptionMapper +} import org.apache.texera.dao.SqlServer import org.apache.texera.service.activity.UserActivityEventListener import org.apache.texera.service.resource.{ diff --git a/access-control-service/src/test/scala/org/apache/texera/auth/UnauthorizedExceptionMapperSpec.scala b/access-control-service/src/test/scala/org/apache/texera/auth/UnauthorizedExceptionMapperSpec.scala new file mode 100644 index 00000000000..cd897c896ba --- /dev/null +++ b/access-control-service/src/test/scala/org/apache/texera/auth/UnauthorizedExceptionMapperSpec.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.auth + +import jakarta.ws.rs.core.HttpHeaders +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class UnauthorizedExceptionMapperSpec extends AnyFlatSpec with Matchers { + + // The mapper sits behind every microservice's `environment.jersey.register( + // classOf[UnauthorizedExceptionMapper])` wiring. JwtAuthFilter throws + // `UnauthorizedException(challenge)` (covered by JwtAuthFilterSpec); this + // spec pins what the mapper turns that exception into when JAX-RS calls + // `toResponse` at the edge. + + private val mapper = new UnauthorizedExceptionMapper + + "UnauthorizedExceptionMapper" should "map any UnauthorizedException to HTTP 401" in { + val response = mapper.toResponse(new UnauthorizedException("Bearer realm=\"texera\"")) + response.getStatus shouldBe 401 + } + + it should "carry the exception's challenge string verbatim in the WWW-Authenticate header" in { + // The challenge is RFC 6750 §3 syntax. The mapper must not rewrite it — + // JwtAuthFilter is the single source of truth for which challenge fires + // (Bearer vs. Bearer + invalid_token), and any rewrite here would mask + // a regression in the filter. + val challenge = + """Bearer realm="texera", error="invalid_token", error_description="JWT verification failed"""" + val response = mapper.toResponse(new UnauthorizedException(challenge)) + response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE) shouldBe challenge + } + + it should "produce no entity body — only status + challenge header" in { + // Browsers and curl expect `WWW-Authenticate` on a body-less 401; an + // accidental JSON entity (e.g. via Dropwizard's default error mapper) + // would suppress the auth challenge prompt in some clients. + val response = mapper.toResponse(new UnauthorizedException("Bearer realm=\"texera\"")) + response.hasEntity shouldBe false + } +} 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 d852468c913..ec5169eee39 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 @@ -25,7 +25,12 @@ import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, Substituting import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig -import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser, UnauthorizedExceptionMapper} +import org.apache.texera.auth.{ + JwtAuthFilter, + RequestLoggingFilter, + SessionUser, + UnauthorizedExceptionMapper +} import org.apache.texera.dao.SqlServer import org.apache.texera.service.resource.{ 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 fda956e195b..ae695607818 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 @@ -26,7 +26,12 @@ import io.dropwizard.configuration.{EnvironmentVariableSubstitutor, Substituting import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig -import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser, UnauthorizedExceptionMapper} +import org.apache.texera.auth.{ + JwtAuthFilter, + RequestLoggingFilter, + SessionUser, + UnauthorizedExceptionMapper +} import org.apache.texera.config.DefaultsConfig import org.apache.texera.dao.SqlServer import org.apache.texera.service.resource.{ConfigResource, HealthCheckResource} 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 ece4b225c2e..64a2f64eba5 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 @@ -28,7 +28,12 @@ import io.dropwizard.core.Application import io.dropwizard.core.setup.{Bootstrap, Environment} import org.apache.texera.amber.config.StorageConfig import org.apache.texera.amber.core.storage.util.LakeFSStorageClient -import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser, UnauthorizedExceptionMapper} +import org.apache.texera.auth.{ + JwtAuthFilter, + RequestLoggingFilter, + SessionUser, + UnauthorizedExceptionMapper +} import org.apache.texera.dao.SqlServer import org.apache.texera.service.`type`.DatasetFileNode import org.apache.texera.service.`type`.serde.DatasetFileNodeSerializer