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
2 changes: 1 addition & 1 deletion packages/catalog/test-subset.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"repository": "cardstack/boxel-catalog",
"revision": "e9266fee70d06ad639e7481473fdeeeb5f88c1cf",
"revision": "2845e727868e364cb5197bd78d5e657131cce18a",
"files": [
{
"path": "realm-policy/realm-policy.gts",
Expand Down
31 changes: 31 additions & 0 deletions packages/host/tests/integration/realm-policy-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,37 @@ module('Integration | realm policy', function (hooks) {
assert.dom('[data-test-policy-predicate-snapshot]').exists({ count: 1 });
});

test('a grant opened to callers who are not signed in says so, and a write names the setting it acts as', async function (assert) {
await setupPolicyRealm({
'policies/public.json': policyDocument([
{
targetType: { module: '../classroom', name: 'Classroom' },
grants: [
{ operation: 'read', anonymous: true },
{
operation: 'update',
anonymous: true,
actingUser: 'feedbackWriter',
},
{ operation: 'delete' },
],
},
]),
});
let policy = await loadPolicy('policies/public');
await renderCard(loader, policy, 'embedded');

assert.dom('[data-test-operation-grant]').exists({ count: 3 });
assert
.dom('[data-test-operation-grant-anonymous]')
.exists({ count: 2 }, 'only the grants that opt in are marked')
.hasText('anyone');
assert
.dom('[data-test-operation-grant-acting-user]')
.exists({ count: 1 }, 'only the write names who it acts as')
.hasText('config.feedbackWriter');
});

test('a policy with no rules says it grants nothing', async function (assert) {
await setupPolicyRealm({ 'policies/empty.json': policyDocument([]) });
let policy = await loadPolicy('policies/empty');
Expand Down
11 changes: 9 additions & 2 deletions packages/realm-server/tests/catalog-test-subset-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,22 @@ module(basename(import.meta.filename), function (hooks) {
)}`,
);
let rules = (doc?.type === 'doc' ? doc.doc.data.attributes?.rules : []) as {
grants: { operation: string; where: unknown }[];
grants: {
operation: string;
where: unknown;
anonymous: unknown;
actingUser: unknown;
}[];
}[];
assert.deepEqual(
rules[0]?.grants,
[
{ operation: 'read', where: null },
{ operation: 'read', where: null, anonymous: false, actingUser: null },
{
operation: 'appendActivity',
where: { bxl: 'actor() in .teacherIds', snapshot: true },
anonymous: false,
actingUser: null,
},
],
"the catalog definition's fields, including its serializer-backed predicate, round-trip",
Expand Down
183 changes: 182 additions & 1 deletion packages/realm-server/tests/realm-policy-compile-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ function classroomModule(rosterField: string) {
set: { status: 'locked' },
nonGrantable: true,
};
@operation static listOpen = {
base: 'query',
query: { filter: { type: () => Classroom } },
};
}
export class OpenClassroom extends Classroom {
@operation static lock = {
Expand All @@ -86,7 +90,12 @@ function classroomModule(rosterField: string) {
`;
}

type Grant = { operation: string; where?: unknown };
type Grant = {
operation: string;
where?: unknown;
anonymous?: boolean;
actingUser?: string;
};
type Rule = { targetType: { module: string; name: string }; grants: Grant[] };

const CLASSROOM = { module: `${EDUCATION}classroom`, name: 'Classroom' };
Expand Down Expand Up @@ -309,6 +318,7 @@ module(basename(import.meta.filename), function (hooks) {
source: '.teacherIds | any(. == actor())',
canonical: '.teacherIds | any(. == actor())',
snapshot: false,
readsActor: true,
},
},
{
Expand All @@ -318,6 +328,7 @@ module(basename(import.meta.filename), function (hooks) {
source: '.headTeacher == actor()',
canonical: '.headTeacher == actor()',
snapshot: true,
readsActor: true,
},
},
{
Expand Down Expand Up @@ -822,4 +833,174 @@ module(basename(import.meta.filename), function (hooks) {
);
assert.notOk(policy?.uncompilable, 'the policy as a whole compiled');
});

test('a grant opens a base operation to callers who are not signed in, and a write names who it is made as', async function (assert) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Three branches this change adds have no test that would fail if they broke:

  1. A named query with anonymous. The issue message names it, but the fixture has no saved search, so only the custom-transform half of anonymous-not-base-operation is pinned.
  2. A query grant left with no filter (policy-not-filterable) stays out of anonymous.operations. The only anonymous query here has a filter, so deleting || grant.filter from anonymousSummary keeps this test green.
  3. An uncompilable policy gives an empty getAnonymousAdmission(). This is the fail-closed path the doc comment promises.

Also, the actor-reading read grant sits beside an unconditional anonymous read, so the summary assertion can't tell how a caller-reading grant is treated. Whatever is decided in the anonymousSummary thread, give that grant a rule of its own here. Non-blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in f87ad86. The module's Classroom declares a named query, listOpen, which is refused with anonymous-not-base-operation. A query grant that compiles no filter (realmConfig(...)) gives no anonymous summary and an empty admission. A policy card that isn't a RealmPolicy is uncompilable and gives an empty admission. The grant that reads the caller is the only grant of its own rule.

const OPEN_CLASSROOM = {
module: `${EDUCATION}classroom`,
name: 'OpenClassroom',
};
await writeTo(
org,
'policies/education.json',
policyOf([
{
targetType: CLASSROOM,
grants: [
{ operation: 'read', anonymous: true },
// A custom operation, built on a base one.
{ operation: 'appendActivity', anonymous: true },
// A named query.
{ operation: 'listOpen', anonymous: true },
// A write that names nobody to make it as, and one whose key is
// blank.
{ operation: 'transform', anonymous: true },
{ operation: 'update', anonymous: true, actingUser: ' ' },
{
operation: 'transform',
anonymous: true,
actingUser: 'feedbackWriter',
},
// A read names no acting user, whatever it is given.
{ operation: 'readSource', anonymous: true, actingUser: 'reader' },
{ operation: 'query', anonymous: true, where: '.status == "open"' },
// Opened to signed-in callers only.
{ operation: 'delete' },
],
},
{
// The only anonymous grant of its operation reads the caller.
targetType: OPEN_CLASSROOM,
grants: [
{
operation: 'update',
anonymous: true,
actingUser: 'feedbackWriter',
where: '.status == "public" or (.teacherIds | any(. == actor()))',
},
],
},
]),
);
let policy = await compiled();
assert.deepEqual(
policy?.issues.map(({ code, path, severity }) => ({
code,
path,
severity,
})),
[
{
code: 'anonymous-not-base-operation',
path: 'rules[0].grants[1].anonymous',
severity: 'inactive',
},
{
code: 'anonymous-not-base-operation',
path: 'rules[0].grants[2].anonymous',
severity: 'inactive',
},
{
code: 'anonymous-write-without-acting-user',
path: 'rules[0].grants[3].actingUser',
severity: 'inactive',
},
{
code: 'anonymous-write-without-acting-user',
path: 'rules[0].grants[4].actingUser',
severity: 'inactive',
},
{
code: 'anonymous-grant-reads-actor',
path: 'rules[1].grants[0].where',
severity: 'warning',
},
],
'a custom operation, a named query and a write with no acting user are refused; a grant that reads the caller is warned about and kept',
);
let grants = policy?.rules[0]?.grants ?? [];
assert.deepEqual(
grants.map(({ path, anonymous }) => ({ path, anonymous })),
[
{ path: 'rules[0].grants[0]', anonymous: {} },
{
path: 'rules[0].grants[5]',
anonymous: { actingUserKey: 'feedbackWriter' },
},
{ path: 'rules[0].grants[6]', anonymous: {} },
{ path: 'rules[0].grants[7]', anonymous: {} },
{ path: 'rules[0].grants[8]', anonymous: undefined },
],
'the refused grants are left out, only the grants that opt in carry the opt-in, and only a write names who it is made as',
);
assert.ok(grants[3]?.filter, 'the anonymous query grant has a filter');
let readsCaller = policy?.rules[1]?.grants[0];
assert.true(
readsCaller?.where?.readsActor,
'a predicate that reads the caller says so',
);
assert.notOk(
grants[3]?.where?.readsActor,
'and one that reads only the card does not',
);
assert.deepEqual(
policy?.anonymous,
{ operations: ['query', 'read', 'readSource', 'transform'] },
'the operations opened to callers who are not signed in, leaving out the one whose only grant reads the caller',
);
assert.deepEqual(
[...(await education.getAnonymousAdmission())].sort(),
['query', 'read', 'readSource', 'transform'],
'and the realm reads them from the compiled policy',
);
});

test('a query grant with no search filter opens nothing to callers who are not signed in', async function (assert) {
await writeTo(
org,
'policies/education.json',
policyCard([
// Reads a realm setting, which a search does not resolve.
{
operation: 'query',
anonymous: true,
where: '.status == realmConfig("approver")',
},
]),
);
let policy = await compiled();
assert.deepEqual(
policy?.issues.map(({ code }) => code),
['policy-not-filterable'],
);
assert.strictEqual(policy?.anonymous, undefined);
assert.strictEqual((await education.getAnonymousAdmission()).size, 0);
});

test('a policy that opens nothing, or that does not compile, opens nothing to callers who are not signed in', async function (assert) {
await writeTo(org, 'policies/education.json', policyCard(GRANTS));
assert.strictEqual(
(await compiled())?.anonymous,
undefined,
'a policy that opens nothing records nothing',
);
assert.strictEqual((await education.getAnonymousAdmission()).size, 0);

await writeTo(
org,
'policies/education.json',
policyCard([{ operation: 'read', anonymous: true }], {
module: rri('@cardstack/base/card-api'),
name: 'CardDef',
}),
);
assert.true(
(await compiled())?.uncompilable,
'the policy does not compile',
);
assert.strictEqual(
(await education.getAnonymousAdmission()).size,
0,
'and opens nothing',
);
});
});
7 changes: 6 additions & 1 deletion packages/realm-server/tests/realm-policy-tiers-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,12 @@ module(basename(import.meta.filename), function () {
assert.deepEqual(issues, [], `${source}: no issue`);
assert.deepEqual(
grant?.where,
{ source, canonical: grant?.where?.canonical, snapshot: true },
{
source,
canonical: grant?.where?.canonical as string,
snapshot: true,
...(source.includes('actor()') ? { readsActor: true as const } : {}),
},
`${source}: judged against the snapshot`,
);
}
Expand Down
Loading
Loading