From 894b79a9ee519d0f840d85aa47ad3944f400264d Mon Sep 17 00:00:00 2001 From: ylm Date: Mon, 29 Jun 2026 13:14:04 -0400 Subject: [PATCH 1/2] Pin federated-search archived-realm contract via regression test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original CS-11666 plan filtered archived realms inside handle-search. That filter is subsumed by the enumeration filter added in CS-11665 (#5358): once a realm is archived, the requester has no permission for it per fetchUserPermissions, so multi-realm-authorization 403s the request before handle-search runs. Drop the redundant handler- level filter and the bulk fetchArchivedRealmURLs helper that fed it. Keep one integration test on the federated-search endpoint to pin the user-facing contract: a federated search payload that names an archived realm gets 403, and the same request restricted to active realms still searches normally. The test exercises the full stack (handle-search → multi-realm-auth → fetchUserPermissions → realm_metadata.archived_at) so a future refactor of the enumeration layer can't silently weaken it. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tests/server-endpoints/search-test.ts | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/realm-server/tests/server-endpoints/search-test.ts b/packages/realm-server/tests/server-endpoints/search-test.ts index 3772996b65..7c2664da1a 100644 --- a/packages/realm-server/tests/server-endpoints/search-test.ts +++ b/packages/realm-server/tests/server-endpoints/search-test.ts @@ -10,7 +10,7 @@ import type { QueueRunner, Realm, } from '@cardstack/runtime-common'; -import { baseCardRef, rri } from '@cardstack/runtime-common'; +import { archiveRealm, baseCardRef, rri } from '@cardstack/runtime-common'; import type { PgAdapter } from '@cardstack/postgres'; import { resetCatalogRealms } from '../../handlers/handle-fetch-catalog-realms.ts'; import { @@ -241,6 +241,49 @@ module(`server-endpoints/${basename(import.meta.filename)}`, function (_hooks) { } }); + // A federated search payload that names an archived realm must not + // return hits. The mechanism is the enumeration filter in + // fetchUserPermissions: once a realm is archived, the requester has + // no permission for it per the filtered enumeration, so the + // multi-realm-authorization middleware short-circuits the request + // with 403 before handle-search runs. The handler itself does no + // extra work; this test pins the contract end-to-end so a refactor + // of the enumeration layer can't silently weaken it. + test('archived realms in a federated search payload are refused at the auth boundary', async function (assert) { + await archiveRealm(dbAdapter, new URL(secondaryRealm.url)); + + let response = await postSearch({ + filter: personFilter(), + realms: [testRealm.url, secondaryRealm.url], + }); + assert.strictEqual( + response.status, + 403, + 'a request including an archived realm is forbidden', + ); + assert.ok( + String(response.body?.errors?.[0] ?? response.text).includes( + secondaryRealm.url, + ), + 'the forbidden response names the archived realm', + ); + + let activeOnly = await postSearch({ + filter: personFilter(), + realms: [testRealm.url], + }); + assert.strictEqual( + activeOnly.status, + 200, + 'the same request restricted to the active realm succeeds', + ); + assert.strictEqual( + activeOnly.body.meta.page.total, + 2, + 'active realms continue to search normally', + ); + }); + test('cardUrls narrows results across the federation', async function (assert) { let response = await postSearch({ filter: personFilter(), From 2919405c0e0b2f9ed3b712e694125dbac7f4a709 Mon Sep 17 00:00:00 2001 From: ylm Date: Tue, 30 Jun 2026 12:29:42 -0400 Subject: [PATCH 2/2] Filter to test-seeded realms in fetchUserPermissions archived test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The exact-key-set assertion picked up '*: read' system-realm permissions (boxel-homepage, catalog, openrouter, …) seeded by migrations into the template DB, since the public-read arm of fetchUserPermissions surfaces those for every user. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/realm-server/tests/queries-test.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/realm-server/tests/queries-test.ts b/packages/realm-server/tests/queries-test.ts index 73542c9cc7..7fc77e07bf 100644 --- a/packages/realm-server/tests/queries-test.ts +++ b/packages/realm-server/tests/queries-test.ts @@ -167,11 +167,18 @@ module(basename(import.meta.filename), function () { await archiveRealm(dbAdapter, new URL(archivedOwned)); await archiveRealm(dbAdapter, new URL(archivedPublic)); + // Seed migrations grant '*: read' to system realms (boxel-homepage, + // catalog, openrouter, …), which the public-read arm of + // fetchUserPermissions surfaces for every user. Filter to the URLs + // this test seeded so the assertion isn't coupled to that fixture. + const testRealms = (urls: string[]) => + urls.filter((u) => u.startsWith('http://example.com/')).sort(); + let active = await fetchUserPermissions(dbAdapter, { userId: ownerUserId, }); assert.deepEqual( - Object.keys(active).sort(), + testRealms(Object.keys(active)), [activeOwned, activePublic].sort(), 'default enumeration excludes archived realms in both UNION arms', ); @@ -181,7 +188,7 @@ module(basename(import.meta.filename), function () { includeArchived: true, }); assert.deepEqual( - Object.keys(withArchived).sort(), + testRealms(Object.keys(withArchived)), [activeOwned, activePublic, archivedOwned, archivedPublic].sort(), 'includeArchived re-includes archived realms in both arms', ); @@ -191,7 +198,7 @@ module(basename(import.meta.filename), function () { onlyOwnRealms: true, }); assert.deepEqual( - Object.keys(activeOwnersOnly).sort(), + testRealms(Object.keys(activeOwnersOnly)), [activeOwned].sort(), 'onlyOwnRealms also excludes archived realms by default', ); @@ -202,7 +209,7 @@ module(basename(import.meta.filename), function () { includeArchived: true, }); assert.deepEqual( - Object.keys(ownersWithArchived).sort(), + testRealms(Object.keys(ownersWithArchived)), [activeOwned, archivedOwned].sort(), 'onlyOwnRealms + includeArchived returns the owner archived realm too', );