This is an automated email from the ASF dual-hosted git repository. kunwp1 pushed a commit to branch revert-4903-refactor/jwt-filter-eager-401 in repository https://gitbox.apache.org/repos/asf/texera.git
commit a25e9dc12f051dc95182b4eae8def0ab6bf34ebf Author: Kunwoo (Chris) <[email protected]> AuthorDate: Mon May 11 13:20:15 2026 -0700 Revert "fix(auth): JwtAuthFilter eager-401 with @PermitAll opt-out (#4903)" This reverts commit 6f9f0e355dfba403b5584d0a8703ff58f43b26e0. --- .../texera/service/AccessControlService.scala | 8 +- .../auth/UnauthorizedExceptionMapperSpec.scala | 59 ---- amber/LICENSE-binary-java | 1 - common/auth/build.sbt | 1 - .../org/apache/texera/auth/JwtAuthFilter.scala | 96 ++----- .../apache/texera/auth/UnauthorizedException.scala | 57 ---- .../org/apache/texera/auth/JwtAuthFilterSpec.scala | 313 --------------------- .../service/ComputingUnitManagingService.scala | 8 +- .../org/apache/texera/service/ConfigService.scala | 8 +- .../org/apache/texera/service/FileService.scala | 8 +- .../texera/service/resource/DatasetResource.scala | 7 +- 11 files changed, 25 insertions(+), 541 deletions(-) diff --git a/access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala b/access-control-service/src/main/scala/org/apache/texera/service/AccessControlService.scala index 0cb5738419..21d367e2bb 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,12 +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, - UnauthorizedExceptionMapper -} +import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser} import org.apache.texera.dao.SqlServer import org.apache.texera.service.activity.UserActivityEventListener import org.apache.texera.service.resource.{ @@ -77,7 +72,6 @@ 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/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 deleted file mode 100644 index cd897c896b..0000000000 --- a/access-control-service/src/test/scala/org/apache/texera/auth/UnauthorizedExceptionMapperSpec.scala +++ /dev/null @@ -1,59 +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.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/amber/LICENSE-binary-java b/amber/LICENSE-binary-java index d413b9f8d2..8fbd626ca4 100644 --- a/amber/LICENSE-binary-java +++ b/amber/LICENSE-binary-java @@ -630,7 +630,6 @@ 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 diff --git a/common/auth/build.sbt b/common/auth/build.sbt index 24feb1984f..a33da64fea 100644 --- a/common/auth/build.sbt +++ b/common/auth/build.sbt @@ -57,7 +57,6 @@ 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 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 779618726c..5698515630 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,91 +20,35 @@ package org.apache.texera.auth import com.typesafe.scalalogging.LazyLogging -import jakarta.annotation.security.PermitAll -import jakarta.ws.rs.container.{ContainerRequestContext, ContainerRequestFilter, ResourceInfo} -import jakarta.ws.rs.core.{Context, HttpHeaders, SecurityContext} +import jakarta.ws.rs.container.{ContainerRequestContext, ContainerRequestFilter} +import jakarta.ws.rs.core.{HttpHeaders, 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 §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 [[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 { - @Context - private var resourceInfo: ResourceInfo = _ - override def filter(requestContext: ContainerRequestContext): Unit = { - val tokenOpt = extractBearerToken(requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)) - - if (tokenOpt.isEmpty) { - if (isPermitAll) return - throw new UnauthorizedException(JwtAuthFilter.BearerChallenge) - } - - val userOpt = JwtParser.parseToken(tokenOpt.get) - if (!userOpt.isPresent) { - logger.warn("Invalid JWT: Unable to parse token") - throw new UnauthorizedException(JwtAuthFilter.InvalidTokenChallenge) + 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") + } } - - 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 7235 §2.1: auth-scheme is case-insensitive and the credentials - // follow after 1*SP. Tolerate surrounding whitespace and any - // capitalization of "Bearer" so that e.g. `authorization: bearer <jwt>` - // is accepted instead of being rejected as a malformed header. - private def extractBearerToken(authHeader: String): Option[String] = { - if (authHeader == null) return None - val parts = authHeader.trim.split("\\s+", 2) - if (parts.length != 2 || !parts(0).equalsIgnoreCase("Bearer")) return None - val token = parts(1).trim - if (token.isEmpty) None else Some(token) - } -} - -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. - 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 deleted file mode 100644 index 646f8a1101..0000000000 --- a/common/auth/src/main/scala/org/apache/texera/auth/UnauthorizedException.scala +++ /dev/null @@ -1,57 +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.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. - * - * Constructed with `writableStackTrace = false` because this exception is - * thrown on every unauthenticated request and the stack trace is never - * inspected — skipping `fillInStackTrace` avoids a per-request CPU hit on - * the auth hot path. - */ -class UnauthorizedException(val challenge: String) - extends RuntimeException( - challenge, - /* cause = */ null, - /* enableSuppression = */ false, - /* writableStackTrace = */ false - ) - -/** 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 deleted file mode 100644 index dfab579d48..0000000000 --- a/common/auth/src/test/scala/org/apache/texera/auth/JwtAuthFilterSpec.scala +++ /dev/null @@ -1,313 +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.auth - -import jakarta.annotation.security.PermitAll -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 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) - - 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 = () - } - - // 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") - c.setClaim("userId", 42) - c.setClaim("googleId", "g-123") - c.setClaim("email", "[email protected]") - c.setClaim("role", UserRoleEnum.ADMIN.name) - c.setClaim("googleAvatar", "avatar") - c.setExpirationTimeMinutesInTheFuture(10f) - c - } - - // -------------------- 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\"" - } - - // -------------------- required-auth method -------------------- - - "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[UnauthorizedException] thrownBy filter.filter(ctx) - thrown.challenge shouldBe JwtAuthFilter.BearerChallenge - ctx.getSecurityContext shouldBe null - } - - 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[UnauthorizedException] thrownBy filter.filter(ctx) - thrown.challenge shouldBe JwtAuthFilter.BearerChallenge - } - - 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[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) - - 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 -------------------- - - "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 throw UnauthorizedException(InvalidTokenChallenge) when a token is supplied but invalid" 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[UnauthorizedException] thrownBy filter.filter(ctx) - thrown.challenge shouldBe JwtAuthFilter.InvalidTokenChallenge - } - - 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[UnauthorizedException] thrownBy filter.filter(ctx) - thrown.challenge shouldBe JwtAuthFilter.BearerChallenge - } - - // -------------------- case-insensitive Bearer scheme -------------------- - - // RFC 7235 §2.1: auth-scheme is case-insensitive. The header parser must - // accept any capitalization of "Bearer" and tolerate surrounding / - // intra-header whitespace. - private def filterFor(authHeader: String): StubRequestContext = { - val filter = new JwtAuthFilter - withResourceInfo( - filter, - new StubResourceInfo( - methodOf(classOf[RequiredAuthResource], "secured"), - classOf[RequiredAuthResource] - ) - ) - val ctx = new StubRequestContext(authHeader) - filter.filter(ctx) - ctx - } - - "JwtAuthFilter Bearer scheme parsing" should "accept lowercase 'bearer'" in { - val ctx = filterFor(s"bearer ${JwtAuth.jwtToken(buildClaims())}") - ctx.getSecurityContext.getUserPrincipal.asInstanceOf[SessionUser].getUid shouldBe 42 - } - - it should "accept uppercase 'BEARER'" in { - val ctx = filterFor(s"BEARER ${JwtAuth.jwtToken(buildClaims())}") - ctx.getSecurityContext.getUserPrincipal.asInstanceOf[SessionUser].getUid shouldBe 42 - } - - it should "accept mixed-case 'BeArEr'" in { - val ctx = filterFor(s"BeArEr ${JwtAuth.jwtToken(buildClaims())}") - ctx.getSecurityContext.getUserPrincipal.asInstanceOf[SessionUser].getUid shouldBe 42 - } - - it should "tolerate leading whitespace before the scheme" in { - val ctx = filterFor(s" Bearer ${JwtAuth.jwtToken(buildClaims())}") - ctx.getSecurityContext.getUserPrincipal.asInstanceOf[SessionUser].getUid shouldBe 42 - } - - it should "tolerate multiple spaces between scheme and token" in { - val ctx = filterFor(s"Bearer ${JwtAuth.jwtToken(buildClaims())}") - ctx.getSecurityContext.getUserPrincipal.asInstanceOf[SessionUser].getUid shouldBe 42 - } - - it should "tolerate trailing whitespace after the token" in { - val ctx = filterFor(s"Bearer ${JwtAuth.jwtToken(buildClaims())} ") - ctx.getSecurityContext.getUserPrincipal.asInstanceOf[SessionUser].getUid shouldBe 42 - } - - it should "reject a Bearer header with no token" in { - val filter = new JwtAuthFilter - withResourceInfo( - filter, - new StubResourceInfo( - methodOf(classOf[RequiredAuthResource], "secured"), - classOf[RequiredAuthResource] - ) - ) - val ctx = new StubRequestContext("Bearer ") - val thrown = the[UnauthorizedException] thrownBy filter.filter(ctx) - thrown.challenge shouldBe JwtAuthFilter.BearerChallenge - } - - // -------------------- exception is stack-trace-less -------------------- - - // UnauthorizedException is thrown on every unauthenticated request and the - // stack is never inspected. Ensure fillInStackTrace was suppressed so the - // auth hot path does not pay for stack capture. - "UnauthorizedException" should "carry no stack trace" in { - val e = new UnauthorizedException(JwtAuthFilter.BearerChallenge) - e.getStackTrace.length shouldBe 0 - e.getMessage 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 ec5169eee3..a15ced30a2 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,12 +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, - UnauthorizedExceptionMapper -} +import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser} import org.apache.texera.dao.SqlServer import org.apache.texera.service.resource.{ ComputingUnitAccessResource, @@ -69,7 +64,6 @@ 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 ae69560781..c787016c27 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,12 +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, - UnauthorizedExceptionMapper -} +import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser} import org.apache.texera.config.DefaultsConfig import org.apache.texera.dao.SqlServer import org.apache.texera.service.resource.{ConfigResource, HealthCheckResource} @@ -70,7 +65,6 @@ 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 64a2f64eba..cc4174682f 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,12 +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, - UnauthorizedExceptionMapper -} +import org.apache.texera.auth.{JwtAuthFilter, RequestLoggingFilter, SessionUser} import org.apache.texera.dao.SqlServer import org.apache.texera.service.`type`.DatasetFileNode import org.apache.texera.service.`type`.serde.DatasetFileNodeSerializer @@ -88,7 +83,6 @@ 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( 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 d9bb85cf4b..46457c9454 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.{PermitAll, RolesAllowed} +import jakarta.annotation.security.RolesAllowed import jakarta.ws.rs._ import jakarta.ws.rs.core._ import org.apache.texera.amber.config.StorageConfig @@ -2142,11 +2142,6 @@ 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]
