You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .claude/CLAUDE.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -71,6 +71,8 @@ Every feature plan MUST include a comprehensive test case inventory before imple
71
71
72
72
**Test naming convention:** Group tests by category with descriptive names. Map security-relevant tests to vector numbers in `docs/security-vectors.md`.
73
73
74
+
**Adding a new security vector:** When a new feature or bug fix touches access control, policy resolution, or data visibility, add (or update) an entry in `docs/security-vectors.md` using the standard schema defined at the top of that file. Section order is fixed: `**Vector**` → `**Attacks**` → `**Defense**` → `**Previously**`*(only if strengthening an earlier defense)* → `**Status**`*(only for unmitigated threats or accepted trade-offs)* → `**Tests**`. Every attack variant must either have a test back-reference (`— attack N`) in the `**Tests**` section or be explicitly marked under `**Status**`. Never use `**Bug**` as a section label — historical fix context goes in `**Previously**` in past tense. See the top of `docs/security-vectors.md` for the full schema description and `### 13` for a canonical example with multiple attack variants plus a `Previously` section.
75
+
74
76
### Security-First Thinking
75
77
This is a data security product. Every feature that touches access control, policy resolution, or data visibility must be evaluated through a security lens:
76
78
-**What can an attacker do?** — enumerate bypass vectors before building defenses
Copy file name to clipboardExpand all lines: CHANGELOG.md
+16Lines changed: 16 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
8
8
## [Unreleased]
9
9
10
+
### Changed
11
+
12
+
-**[Proxy] BREAKING: `ctx.query.tables` is now an array of objects, not strings** — decision functions with `evaluate_context = "query"` previously received `ctx.query.tables` as `string[]` (e.g. `["public.orders"]`). It is now `Array<{datasource, schema, table}>`, so decision function JS must access the fields explicitly. Bare references like `SELECT * FROM orders` now also resolve to the session's default schema (e.g. `public`) rather than an empty schema segment, so qualified and unqualified references produce identical entries.
13
+
14
+
Migration for any decision function that inspected `ctx.query.tables`:
Copy file name to clipboardExpand all lines: docs/permission-system.md
+50Lines changed: 50 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -488,6 +488,33 @@ function evaluate(ctx, config) {
488
488
489
489
`time.now` is an ISO 8601 / RFC 3339 timestamp representing the **evaluation time** — the moment the context is built. For visibility-level functions this is when the connection context is computed; for query-level functions it is when the query is processed. This enables time-windowed decision functions (e.g., break-glass temporary access).
Each entry in `ctx.query.tables` is an object with three string fields:
494
+
495
+
```js
496
+
ctx.query.tables; // Array of { datasource, schema, table }
497
+
```
498
+
499
+
| Field | What it contains |
500
+
|-------|------------------|
501
+
|`datasource`| The BetweenRows datasource name the session is connected to (e.g. `"prod"`). Same value as `ctx.session.datasource.name`. |
502
+
|`schema`| The schema alias the user referenced (or the upstream schema name if no alias is configured). For bare references like `FROM orders`, this is the session's default schema (e.g. `"public"`) — **never** an empty string. |
503
+
|`table`| The table name as referenced in the SQL. |
504
+
505
+
A bare reference `SELECT * FROM orders JOIN users` and a qualified reference `SELECT * FROM public.orders JOIN public.users` produce **identical**`ctx.query.tables` arrays, so decision function logic doesn't need to handle both forms:
506
+
507
+
```js
508
+
// Matches regardless of whether the user wrote bare `orders` or `public.orders`
509
+
consttouchesOrders=ctx.query.tables.some(
510
+
t=>t.schema==="public"&&t.table==="orders"
511
+
);
512
+
```
513
+
514
+
System tables (`pg_catalog.*`, `information_schema.*`) are filtered out of `ctx.query.tables` entirely — decision functions never see them.
515
+
516
+
**Identifier stability**: `datasource` and `schema` are **user-facing labels** that admins can rename (see "Rename fragility and label-based identifiers" below). If an admin renames the `prod` datasource to `production`, decision functions that hardcoded `t.datasource === "prod"` will silently stop matching. Prefer matching on `table` (which is stable with the upstream name) plus `schema` (which is generally stable unless an alias is reconfigured) for rules you want to survive renames. A future rename-warning UX will surface the impact of renames before admins commit them.
517
+
491
518
Custom user attributes are flattened as first-class fields on the `user` object with correctly typed values (string/number/boolean/array) — e.g., `ctx.session.user.region`, not `ctx.session.user.attributes.region`. This matches the flat `{user.KEY}` namespace used in template variables. In the API, the same attributes are nested under `attributes` (see [Namespace design](#namespace-design-flat-in-expressions-nested-in-api)). List attributes appear as JSON arrays of strings. Built-in fields (`id`, `username`, `roles`) always take priority. See [User Attributes (ABAC)](#user-attributes-abac) for details.
492
519
493
520
**Visibility-level enforcement**: `column_deny`, `table_deny`, and `column_allow` policies are enforced at connect time (visibility level) by removing columns/tables from the per-user schema. Decision functions on these policy types are evaluated at visibility time when `evaluate_context = "session"`. If the decision function returns `fire: false`, the policy is skipped and the column/table remains visible. For `evaluate_context = "query"`, the policy's visibility effect is skipped entirely (deferred to query time), since query metadata is not available at connect time — the column/table stays visible in the schema and the decision function runs at query time as normal.
@@ -1307,4 +1334,27 @@ Attach this to a `table_deny` policy targeting `executive_comp` — executives s
1307
1334
| Tiered masking by clearance | Nested `CASE WHEN` in `column_mask` | `CASE WHEN {user.clearance} >= 5 THEN val WHEN ... END` |
1308
1335
| Mask using row data + user attributes | `CASE WHEN` referencing both | `CASE WHEN region = {user.region} THEN val ELSE ... END` |
BetweenRows uses **user-facing labels** for identifiers everywhere users touch the system: datasource names, schema aliases, policy names, and decision function contexts. This is intentional — stable IDs (UUIDs) would be unusable as SQL identifiers and would force admins to hand-write UUIDs in policy targets.
1341
+
1342
+
The tradeoff is that **renames are breaking changes** for anything that references the old label. Three rename operations matter:
1343
+
1344
+
| Rename | What breaks | What keeps working |
1345
+
|--------|-------------|--------------------|
1346
+
| **Datasource name** (e.g. `prod` → `production`) | Connection strings (`dbname=prod`); 3-part SQL references (`prod.public.orders`); `ctx.session.datasource.name` checks; `ctx.query.tables[*].datasource` checks in decision function JS; audit log rows tagged with the old name | Policy enforcement for in-flight queries: policies are assigned by `datasource.id` (UUID), not name, so they continue to match after the rename |
1347
+
| **Schema alias** (e.g. `public` → `main`) | SQL queries using the old alias; `ctx.query.tables[*].schema` checks; `{user.KEY}` expressions that embed the alias; dashboards and stored queries | Policy target matching: `TargetEntry::matches_table`resolves the new alias to the upstream schema name via `df_to_upstream` at session build time, and policy targets store upstream names — so enforcement is stable across alias renames |
1348
+
| **Upstream schema or database rename** (performed in the actual Postgres, not BR) | Everything that references it — catalog discovery must be re-run, and `df_to_upstream` entries rebuilt | Policies need to be retargeted manually |
1349
+
1350
+
**Guidance for decision function authors**: prefer matching on `t.table` (stable with upstream) plus `t.schema` (usually stable unless an alias is reconfigured) over `t.datasource`. If a rule must survive datasource renames, gate it on `ctx.session.user.roles` or attributes, not on datasource identity.
1351
+
1352
+
**Guidance for admins**: when renaming a datasource or schema alias, audit existing:
1353
+
1354
+
1. Decision function JS for references to the old name
1355
+
2. Stored queries, dashboards, and SQL snippets in external tools (BR cannot see these)
1356
+
3. Connection strings in deployed applications
1357
+
4. Audit logs and any downstream log aggregators keyed on the old name
1358
+
1359
+
A future rename-warning feature will surface a list of affected entities before the rename commits. Until then, renames should be treated as a coordinated operation, not a casual UI edit. Policy *enforcement* (the security layer) is the one thing that stays correct across renames automatically.
1310
1360
| Combine multiple filters | Separate policies (AND-combined) | Two `row_filter` policies on same table |
0 commit comments