Repository navigation
Open anonymous access through a grant's where clause and its own BXL expressions - #825
Conversation
…sions on the grant Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m info Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ymous-grant-flag-acting-user-key-and-realmjson
Staging Submissions PreviewThis PR's content is pushed to the staging submissions realm: https://realms-staging.stack.cards/submissions/ Changed folders:
Updated at 2026-10-09 14:57:21 UTC for commit |
richardhjtan
left a comment
There was a problem hiding this comment.
[Claude Code 🤖] One blocking finding, inline.
richardhjtan
left a comment
There was a problem hiding this comment.
[Claude Code 🤖] One blocking finding, inline.
| // reads it in. `reads-actor` is named here as well as through the | ||
| // explanation type, so the map covers it whichever platform version this | ||
| // realm runs on. | ||
| const REASONS: Record<PolicyExplanation['reason'] | 'reads-actor', string> = { |
There was a problem hiding this comment.
[Claude Code 🤖] Fixed in abeacc61 and retained in 5c2ff14b. The map and its key type include blocklist-invalid for compatibility with both platform reason unions.
| let requests = grant?.rateLimitRequests ?? platform?.requests; | ||
| let windowSeconds = grant?.rateLimitWindowSeconds ?? platform?.windowSeconds; |
There was a problem hiding this comment.
[Claude Code 🤖] Fixed in 5c2ff14b. Empty rate-limit fields fall back to the platform values before the inline limit is rendered.
| "summary": "An operation, an optional BXL condition, and whether callers who aren't signed in are admitted." | ||
| }, | ||
| "readMe": "## What it is\n\nOne grant in a `PolicyRule`. It admits a caller to one operation on cards of the rule's type when its condition holds.\n\n```ts\nimport { OperationGrant } from '@cardstack/catalog/realm-policy/realm-policy';\n\n@field grants = containsMany(OperationGrant);\n```\n\n## Fields\n\n* **`operation`**: the operation name as a caller invokes it. That is a base operation such as `read`, `create`, `update`, `delete` or `query`, or the name of an operation a card declares. A grant on a named operation does not grant the base operation it is built on. A search runs as `query`, so a type callers should find in a search needs a `query` grant: a `read` grant opens a card by its URL only. A `query` grant's condition becomes a search filter, which can't express everything a condition can (it can't test a yes/no field, for one), and a `query` grant whose condition it can't express is inactive.\n* **`where`**: a BXL boolean expression over the target card and the caller (`actor()`), stored as written. Absent means the grant is unconditional. In a policy document it is a bare string of BXL.\n\n A condition checks the saved card, so it should read fields the card stores. A computed field, or a field of a linked card, exists only in the search index's copy of the card, so a condition that reads one compiles as inactive and grants nothing. To read the index's copy on purpose, accepting that it lags the card until the card is indexed again, write the condition as `{ \"bxl\": \"...\", \"snapshot\": true }`. A `create` can't use one, since the card it would mint isn't indexed yet.\n\n A write's condition is checked against the card as it was before the write. So it can't stop the write from changing the field it checks: a grant to `update` drafts lets its caller publish one in the same write.\n* **`anonymous`**: whether the grant also admits a caller who isn't signed in. Off, which is the default, it admits only signed-in callers.\n* **`actingUser`**: for an anonymous grant on a write, the key in the governed realm's `realm.json` `config` settings whose value is the Matrix user id the write is made as. That user must be able to write the realm. An anonymous grant on a write without it is left out of the policy, for signed-in callers too.\n\n## Callers who aren't signed in\n\n`anonymous` may be set on a grant for a base operation other than `explain` or `validate`, or for an operation a card declares on such a base. It may not be set on a grant for a named query: setting it there leaves the whole grant out, for signed-in callers too.\n\nA caller who isn't signed in has no actor. A grant whose `where` reads `actor()`, or whose operation does, never admits one, though it still applies to signed-in callers. So an anonymous grant is scoped by what the target holds, not by who is asking.\n\nThe realm, not the policy, names the user an anonymous write is made as. The policy can live in another realm, and its writers must not decide whose identity this realm's writes carry, so `actingUser` holds a key and the governed realm maps it to a user:\n\n```json\n\"attributes\": {\n \"policy\": \"https://app.example/org/policies/newsroom\",\n \"config\": { \"submitter\": \"@submitter:example.com\" }\n}\n```\n\n## The examples\n\n1. **Unconditional**: `read` with no condition, for every signed-in caller, including one the realm's own permissions don't let read it.\n2. **Conditioned on the card**: `update` while the card isn't `published`.\n3. **Conditioned on the caller**: `delete` when the caller is one of the card's `authorIds`.\n4. **Open to visitors**: `read` of `published` cards for callers who aren't signed in.\n5. **An anonymous write**: `create` by callers who aren't signed in, made as the user the realm's `submitter` setting names.\n\n## Non-goals\n\nA grant can't refuse anything, can't open any operation on a policy card or the realm's settings card, reads included, and can't open `explain` or `validate`, which no policy may grant.\n", | ||
| "readMe": "## What it is\n\nOne grant in a `PolicyRule`. It admits a caller to one operation on cards of the rule's type when its condition holds.\n\n```ts\nimport { OperationGrant } from '@cardstack/catalog/realm-policy/realm-policy';\n\n@field grants = containsMany(OperationGrant);\n```\n\n## Fields\n\n* **`operation`**: the operation name as a caller invokes it. That is a base operation such as `read`, `create`, `update`, `delete` or `query`, or the name of an operation a card declares. A grant on a named operation does not grant the base operation it is built on. A search runs as `query`, so a type callers should find in a search needs a `query` grant: a `read` grant opens a card by its URL only. A `query` grant's condition becomes a search filter, which can't express everything a condition can (it can't test a yes/no field, for one), and a `query` grant whose condition it can't express is inactive.\n* **`where`**: a BXL boolean expression over the target card and the caller (`actor()`), stored as written. It can also read the governed realm's settings (`realmConfig()`) and this policy card's fields (`policy()`). Absent means the grant is unconditional. In a policy document it is a bare string of BXL.\n\n A condition checks the saved card, so it should read fields the card stores. A computed field, or a field of a linked card, exists only in the search index's copy of the card, so a condition that reads one compiles as inactive and grants nothing. To read the index's copy on purpose, accepting that it lags the card until the card is indexed again, write the condition as `{ \"bxl\": \"...\", \"snapshot\": true }`. A `create` can't use one, since the card it would mint isn't indexed yet.\n\n A write's condition is checked against the card as it was before the write. So it can't stop the write from changing the field it checks: a grant to `update` drafts lets its caller publish one in the same write.\n* **`actingUser`**, **`blocklist`**, **`rateLimitRequests`**, **`rateLimitWindowSeconds`**: BXL expressions that say how the grant treats callers who aren't signed in (below). Each is used only for such a caller.\n\n## Callers who aren't signed in\n\nFor a caller who isn't signed in, `actor()` is `\"anonymous\"`. A grant admits one only when its `where` names that text, such as `actor() == \"anonymous\"`, or `actor() == \"anonymous\" and .published == true`. A grant whose `where` never names it admits only signed-in callers, even one whose condition would hold for anybody. So a grant written for signed-in callers never starts admitting everyone, and one meant for everyone is two grants: one naming `\"anonymous\"`, and one for signed-in callers.\n\nOnly a grant for a base operation other than `explain` or `validate`, or for an operation a card declares on such a base, can name `\"anonymous\"`. A grant for a named query that names it is left out, for signed-in callers too. An operation whose program reads `actor()` can't be run by a caller who isn't signed in, so such a grant never admits one.\n\nEach expression can read the governed realm's settings (`realmConfig(\"key\")`), this policy card's fields (`policy(\"field\")`), or be written out:\n\n* **`actingUser`**: for a grant on a write, the Matrix user a write by a caller who isn't signed in is made as, such as `realmConfig(\"submitter\")`, `policy(\"writer\")`, `\"@writer:example.com\"`, or a user the card names, `instance().owner`. That user must be able to write the realm. Without one, the grant admits no write by such a caller. A signed-in caller's writes are made as themselves, and reads never use it.\n* **`blocklist`**: the addresses the grant refuses, as a comma-separated string of IP addresses and CIDR ranges, such as `\"192.0.2.1, 10.0.0.0/8\"`, or a list of them. One that can't be read refuses every caller the grant would admit.\n* **`rateLimitRequests`** and **`rateLimitWindowSeconds`**: how many requests one address may make through the grant in a window, as whole numbers. Each one left out, or that produces no whole number, is the platform's default, which the grant's views show. Each grant counts its own requests.\n\nThe blocklist and rate limit can't read the card a request names (`instance()`). They are settled before the card is read, so a refused address gets the same answer whatever card it asks for, and costs the realm nothing more.\n\n## The examples\n\n1. **Unconditional**: `read` with no condition, for every signed-in caller, including one the realm's own permissions don't let read it.\n2. **Conditioned on the card**: `update` while the card isn't `published`.\n3. **Conditioned on the caller**: `delete` when the caller is one of the card's `authorIds`.\n4. **Open to visitors**: `read` of `published` cards for callers who aren't signed in, limited to the requests the realm's `visitorRequests` setting allows, and refusing the addresses its `blockedIps` setting lists.\n5. **An anonymous write**: `create` by callers who aren't signed in, made as the user the realm's `submitter` setting names.\n\n## Non-goals\n\nA grant can't refuse anything, can't open any operation on a policy card or the realm's settings card, reads included, and can't open `explain` or `validate`, which no policy may grant.\n", |
There was a problem hiding this comment.
[Claude Code 🤖] Fixed in 5c2ff14b. The OperationGrant documentation distinguishes anonymous address blocking from refusing access the realm already permits.
| @field blocklist = contains(StringField); | ||
| // How many requests one address may make through this grant in a window of | ||
| // this many seconds. Either one left out is the platform's. | ||
| @field rateLimitRequests = contains(StringField); | ||
| @field rateLimitWindowSeconds = contains(StringField); |
There was a problem hiding this comment.
[Claude Code 🤖] Fixed in 5c2ff14b. The RealmPolicy documentation describes grant-level anonymous traffic rules and preserves the policy’s access-widening constraint.
habdelra
left a comment
There was a problem hiding this comment.
[Claude Code 🤖] Independent cross-check complete. No catalog-specific change request: the grant fields, edit/view presentation, and platform-default display are covered by the paired implementation and tests.



Important
Merge order: this PR, then cardstack/boxel#6622.
Merge this first. The boxel PR pins this PR's head and runs its tests against these fields; once this merges, re-run its
Catalog Test Subsetcheck and merge it.Merges before: cardstack/boxel#6622
OperationGrantused to open itself to callers who aren't signed in with ananonymousboolean, and to name the user their writes were made as with a key into the governed realm'srealm.jsonconfig. How hard such callers could hit the realm, and which addresses it refused, weren't on the grant at all — they were separaterealm.jsonsettings.The grant now says all of it, in the same BXL the condition is written in. A caller who isn't signed in has
actor() == "anonymous", so the condition itself is the opt-in, and the boolean is gone. Four expression fields sit beside it:actingUser— who a visitor's write is made as. A full expression now, sorealmConfig("submitter")keeps the old behaviour while"@writer:example.com",policy("writer")andinstance().ownerbecome possible.blocklist— the addresses this grant refuses, as a comma-separated string or a list.rateLimitRequestsandrateLimitWindowSeconds— how many requests one address may make through this grant in a window. Either left empty is the platform's, which the realm reports in its info, so the edit form can name the actual number instead of saying "the default".The one-line grant view gains the limit and the blocklist beside the acting user, and the edit form gains a field and a hint for each. The explanation panel reads the new per-grant detail: who a write resolves to or why it resolves to nobody, what the blocklist produced or why it closes the grant, and the limit with where each half came from. The realm-wide limit and blocklist lines are gone from the explanation header, replaced by the platform default a grant falls back to.
Every view of a policy card records the platform limit for its realm before its grants render, because a grant is a field and sees no realm info of its own.
🤖 Generated with Claude Code
Preview panels
Policy edit view:
Realm Config edit view: