Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions packages/realm-server/tests/queries-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,149 @@ module(basename(import.meta.filename), function () {
'filters out published realms when fetching all permissions',
);
});

test('excludes archived realms by default; includeArchived re-includes them', async function (assert) {
const ownerUserId = '@owner:localhost';
const activeOwned = 'http://example.com/active-owned/';
const archivedOwned = 'http://example.com/archived-owned/';
const activePublic = 'http://example.com/active-public/';
const archivedPublic = 'http://example.com/archived-public/';

await insertPermissions(dbAdapter, new URL(activeOwned), {
[ownerUserId]: ['read', 'write', 'realm-owner'],
});
await insertPermissions(dbAdapter, new URL(archivedOwned), {
[ownerUserId]: ['read', 'write', 'realm-owner'],
});
// Archiving via the endpoint is forbidden for public realms, but the
// helper itself doesn't enforce that — directly seed an archived
// public realm to exercise the public-read arm of the UNION too.
await insertPermissions(dbAdapter, new URL(activePublic), {
'*': ['read'],
});
await insertPermissions(dbAdapter, new URL(archivedPublic), {
'*': ['read'],
});
await archiveRealm(dbAdapter, new URL(archivedOwned));
await archiveRealm(dbAdapter, new URL(archivedPublic));

// fetchUserPermissions' public arm returns every public realm in the
// database, which includes the bootstrap realms carried by the seed
// template. Assert on the four realms this test seeds rather than the
// full key set so the result stays stable regardless of what else the
// seed template carries.
let active = await fetchUserPermissions(dbAdapter, {
userId: ownerUserId,
});
assert.true(
activeOwned in active,
'default: active owned realm is enumerated',
);
assert.true(
activePublic in active,
'default: active public realm is enumerated',
);
assert.false(
archivedOwned in active,
'default: archived owned realm is excluded (owner arm)',
);
assert.false(
archivedPublic in active,
'default: archived public realm is excluded (public arm)',
);

let withArchived = await fetchUserPermissions(dbAdapter, {
userId: ownerUserId,
includeArchived: true,
});
assert.true(
activeOwned in withArchived,
'includeArchived: active owned realm is enumerated',
);
assert.true(
activePublic in withArchived,
'includeArchived: active public realm is enumerated',
);
assert.true(
archivedOwned in withArchived,
'includeArchived: archived owned realm is re-included (owner arm)',
);
assert.true(
archivedPublic in withArchived,
'includeArchived: archived public realm is re-included (public arm)',
);

let activeOwnersOnly = await fetchUserPermissions(dbAdapter, {
userId: ownerUserId,
onlyOwnRealms: true,
});
assert.true(
activeOwned in activeOwnersOnly,
'onlyOwnRealms: active owned realm is enumerated',
);
assert.false(
archivedOwned in activeOwnersOnly,
'onlyOwnRealms: archived owned realm is excluded by default',
);
assert.false(
activePublic in activeOwnersOnly,
'onlyOwnRealms: public realm is not an owned realm',
);
assert.false(
archivedPublic in activeOwnersOnly,
'onlyOwnRealms: archived public realm is excluded',
);

let ownersWithArchived = await fetchUserPermissions(dbAdapter, {
userId: ownerUserId,
onlyOwnRealms: true,
includeArchived: true,
});
assert.true(
activeOwned in ownersWithArchived,
'onlyOwnRealms + includeArchived: active owned realm is enumerated',
);
assert.true(
archivedOwned in ownersWithArchived,
'onlyOwnRealms + includeArchived: archived owned realm is included',
);
assert.false(
activePublic in ownersWithArchived,
'onlyOwnRealms + includeArchived: public realm is not an owned realm',
);
assert.false(
archivedPublic in ownersWithArchived,
'onlyOwnRealms + includeArchived: archived public realm is excluded',
);
});

test('an unarchived realm is enumerated again', async function (assert) {
const ownerUserId = '@owner:localhost';
const realmURL = 'http://example.com/restored/';

await insertPermissions(dbAdapter, new URL(realmURL), {
[ownerUserId]: ['read', 'write', 'realm-owner'],
});
await archiveRealm(dbAdapter, new URL(realmURL));

let archivedView = await fetchUserPermissions(dbAdapter, {
userId: ownerUserId,
});
assert.false(
realmURL in archivedView,
'archived realm is not enumerated',
);

await unarchiveRealm(dbAdapter, new URL(realmURL));
let restoredView = await fetchUserPermissions(dbAdapter, {
userId: ownerUserId,
});
assert.deepEqual(
restoredView[realmURL],
['read', 'write', 'realm-owner'],
'unarchived realm is enumerated again',
);
});
});

module('fetchAllRealmsWithOwners', function (hooks) {
Expand Down
49 changes: 49 additions & 0 deletions packages/realm-server/tests/realm-auth-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import sinon from 'sinon';
import { basename } from 'path';

import type { PgAdapter } from '@cardstack/postgres';
import { archiveRealm, unarchiveRealm } from '@cardstack/runtime-common';
import { MatrixClient } from '@cardstack/runtime-common/matrix-client';
import { fetchSessionRoom } from '@cardstack/runtime-common/db-queries/session-room-queries';

Expand Down Expand Up @@ -179,5 +180,53 @@ module(basename(import.meta.filename), function () {
'the handler did NOT cold-mount the realm — mount remains deferred to the next per-realm request',
);
});

test('POST /_realm-auth omits archived realms; unarchive restores them', async function (assert) {
sinon
.stub(MatrixClient.prototype, 'createDM')
.resolves('!archived-test-session-room:localhost');
sinon.stub(MatrixClient.prototype, 'sendEvent').resolves();
sinon.stub(MatrixClient.prototype, 'getJoinedRooms').resolves({
joined_rooms: [],
});
sinon.stub(MatrixClient.prototype, 'joinRoom').resolves();

let realmAuthRequest = () =>
request
.post('/_realm-auth')
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.set(
'Authorization',
`Bearer ${createRealmServerJWT(
{ user: matrixUserId, sessionRoom: 'server-session-room' },
realmSecretSeed,
)}`,
)
.send('{}');

let before = await realmAuthRequest();
assert.strictEqual(before.status, 200);
assert.ok(
before.body[testRealmHref],
'active realm appears in the response',
);

await archiveRealm(dbAdapter, new URL(testRealmHref));
let archived = await realmAuthRequest();
assert.strictEqual(archived.status, 200);
assert.notOk(
archived.body[testRealmHref],
'archived realm is omitted from the response',
);

await unarchiveRealm(dbAdapter, new URL(testRealmHref));
let restored = await realmAuthRequest();
assert.strictEqual(restored.status, 200);
assert.ok(
restored.body[testRealmHref],
'unarchived realm reappears in the response',
);
});
});
});
22 changes: 19 additions & 3 deletions packages/runtime-common/db-queries/realm-permission-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,24 @@ export async function fetchUserPermissions(
args: {
userId: string;
onlyOwnRealms?: boolean;
// Archived realms are sealed: by default they're omitted from this
// enumeration so _realm-auth, multi-realm authorization, the indexer
// sweep, etc. see only active realms. Set true to include them.
// The archive-management endpoints reach archived realms by URL via
// fetchRealmPermissions / fetchArchivedRealmsForOwner instead, so
// they bypass this filter and don't need the opt-in.
includeArchived?: boolean;
},
): Promise<{
[realm: string]: RealmAction[];
}> {
const { userId, onlyOwnRealms = false } = args;
const { userId, onlyOwnRealms = false, includeArchived = false } = args;
// Render the archive predicate inline rather than appending a fragment:
// the `false` branch's UNION applies the filter to both arms, so a
// single composable expression keeps the two arms in sync.
const excludeArchivedSql = includeArchived
? ''
: `AND realm_url NOT IN (SELECT url FROM realm_metadata WHERE archived_at IS NOT NULL)`;
Comment thread
lukemelia marked this conversation as resolved.
let permissions: {
realm_url: string;
read: boolean;
Expand All @@ -156,7 +169,8 @@ export async function fetchUserPermissions(
`SELECT realm_url, read, write, realm_owner FROM realm_user_permissions WHERE username =`,
param(userId),
`AND realm_owner = true
AND realm_url NOT IN (SELECT url FROM realm_registry WHERE kind = 'published')`,
AND realm_url NOT IN (SELECT url FROM realm_registry WHERE kind = 'published')
${excludeArchivedSql}`,
])) as {
realm_url: string;
read: boolean;
Expand All @@ -170,12 +184,14 @@ export async function fetchUserPermissions(
`SELECT realm_url, read, write, realm_owner FROM realm_user_permissions WHERE username =`,
param(userId),
`AND realm_url NOT IN (SELECT url FROM realm_registry WHERE kind = 'published')
${excludeArchivedSql}
UNION
SELECT realm_url, true as read, false as write, false as realm_owner FROM realm_user_permissions WHERE username = '*' AND read = true
AND realm_url NOT IN (SELECT realm_url FROM realm_user_permissions WHERE username =`,
param(userId),
`)
AND realm_url NOT IN (SELECT url FROM realm_registry WHERE kind = 'published')`,
AND realm_url NOT IN (SELECT url FROM realm_registry WHERE kind = 'published')
${excludeArchivedSql}`,
])) as {
realm_url: string;
read: boolean;
Expand Down
Loading