From 8e6a1b4ed68a71de1f98ccdd92a745035d12b673 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 8 Jun 2026 15:46:51 -0700 Subject: [PATCH 1/2] fix(config-service): make /config/user-system anonymous so INACTIVE users can read inviteOnly A freshly-registered user is INACTIVE until an admin approves them and therefore cannot reach the @RolesAllowed("REGULAR", "ADMIN") config endpoints. The frontend reads the `inviteOnly` flag at exactly that point to decide whether to show the registration-request form (and notify admins). Since #5305 moved /config/user-system behind a role check, the flag became unreachable for the very users it targets, so the form never appeared and no admin notification was sent. Restore @PermitAll on /config/user-system. It only exposes the boolean inviteOnly flag, which is non-sensitive and already needed pre-activation. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../org/apache/texera/service/resource/ConfigResource.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala b/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala index 2cb951d01e4..4f0c468abd1 100644 --- a/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala +++ b/config-service/src/main/scala/org/apache/texera/service/resource/ConfigResource.scala @@ -76,7 +76,7 @@ class ConfigResource { ) @GET - @RolesAllowed(Array("REGULAR", "ADMIN")) + @PermitAll @Path("/user-system") def getUserSystemConfig: Map[String, Any] = Map( From 8723d87fe49cd632dc884e08fcb4f920603ceba4 Mon Sep 17 00:00:00 2001 From: ali Date: Mon, 8 Jun 2026 16:03:45 -0700 Subject: [PATCH 2/2] test(config-service): update ConfigResourceAuthSpec for anonymous /config/user-system The spec pinned /config/user-system as @RolesAllowed (401 without a token). Now that the endpoint is @PermitAll, assert it answers anonymous callers with 200 and exposes exactly the inviteOnly flag, while still serving authenticated callers. Mirrors the existing /config/pre-login guard. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../resource/ConfigResourceAuthSpec.scala | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceAuthSpec.scala b/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceAuthSpec.scala index da912843346..2fd36c272d4 100644 --- a/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceAuthSpec.scala +++ b/config-service/src/test/scala/org/apache/texera/service/resource/ConfigResourceAuthSpec.scala @@ -36,13 +36,15 @@ import org.scalatest.matchers.should.Matchers // Wires ConfigResource through the same Jersey auth pipeline production uses // (JwtAuthFilter + RolesAllowedDynamicFeature) and fires HTTP requests with and -// without an Authorization header. /config/pre-login is the only @PermitAll -// endpoint and must answer unauthenticated callers (bootstrap regression guard, +// without an Authorization header. /config/pre-login and /config/user-system are +// @PermitAll and must answer unauthenticated callers (bootstrap regression guard, // same shape as the break that caused PR #5049 to be reverted in #5173). -// /config/gui and /config/user-system are @RolesAllowed; they must reject -// anonymous traffic with a 401 (now from JwtAuthFilter's eager check, not -// from a downstream RolesAllowedRequestFilter 403) and accept callers with a -// valid Bearer token. +// /config/user-system stays anonymous because a freshly-registered user is INACTIVE +// (so cannot reach @RolesAllowed endpoints) yet the frontend needs its inviteOnly +// flag to decide whether to show the registration-request form. +// /config/gui is @RolesAllowed; it must reject anonymous traffic with a 401 (now +// from JwtAuthFilter's eager check, not from a downstream RolesAllowedRequestFilter +// 403) and accept callers with a valid Bearer token. class ConfigResourceAuthSpec extends AnyFlatSpec with Matchers with BeforeAndAfterAll { // Mirror production's mapper: ConfigService bootstraps Dropwizard's default mapper @@ -136,14 +138,24 @@ class ConfigResourceAuthSpec extends AnyFlatSpec with Matchers with BeforeAndAft ) } - "GET /config/user-system" should "return 401 with a Bearer challenge without an Authorization header" in { + "GET /config/user-system" should "return 200 without an Authorization header" in { + // @PermitAll: a freshly-registered user is INACTIVE and cannot reach the + // @RolesAllowed endpoints, yet the frontend needs inviteOnly at exactly that + // point to decide whether to show the registration-request form. val response = resources.target("/config/user-system").request(MediaType.APPLICATION_JSON).get() - response.getStatus shouldBe 401 - response.getHeaderString("WWW-Authenticate") shouldBe JwtAuthFilter.BearerChallenge + response.getStatus shouldBe 200 } - it should "return 200 with a valid Bearer token whose role matches @RolesAllowed" in { + it should "expose exactly the inviteOnly flag and nothing else" in { + val payload = resources + .target("/config/user-system") + .request(MediaType.APPLICATION_JSON) + .get(classOf[Map[String, Any]]) + payload.keySet shouldBe Set("inviteOnly") + } + + it should "also return 200 with a valid Bearer token" in { val response = resources .target("/config/user-system") .request(MediaType.APPLICATION_JSON)