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
docs: standardize NULL terminology across UI, docs, and code comments
Replace inconsistent phrases like "no default (null)", "no default",
and "with no default" with explicit "NULL" or "default is NULL"
throughout UI text, doc comments, inline comments, and documentation.
Also improve default value UX in AttributeDefinitionForm (NULL badge,
type-specific placeholders, icon clear button) and reorder search/filter
controls on AttributeDefinitionsPage.
? `Users without this attribute will be treated as having the value "${defaultValue}" when policies are evaluated. This value is applied by the proxy at query time — it is not stored on the user.`
221
-
: 'When null: users without this attribute will have NULL substituted in policy expressions. In SQL, comparisons with NULL (e.g., tenant = NULL) evaluate to NULL, which is treated as false — so equality filters return zero rows. This is applied by the proxy at query time, not stored on the user.'}
230
+
: 'The default is NULL. Users without this attribute will have NULL substituted in policy expressions. In SQL, comparisons with NULL (e.g., tenant = NULL) evaluate to NULL, which is treated as false — so equality filters return zero rows. This is applied by the proxy at query time, not stored on the user.'}
@@ -727,7 +727,7 @@ export function DecisionFunctionModal({
727
727
728
728
<pclassName="text-xs text-gray-400 -mt-3">
729
729
Custom user attributes (e.g. <codeclassName="bg-gray-100 px-1 rounded">ctx.session.user.clearance</code>) are <codeclassName="bg-gray-100 px-1 rounded">null</code> when
730
-
not set on a user or when their default is null. Use defensive checks
730
+
not set on a user and the attribute's default is NULL. Use defensive checks
731
731
like <codeclassName="bg-gray-100 px-1 rounded">{'if (ctx.session.user.clearance == null)'}</code> before
732
732
numeric comparisons, since <codeclassName="bg-gray-100 px-1 rounded">null >= 0</code> is <codeclassName="bg-gray-100 px-1 rounded">true</code> in JavaScript.
**SQL NULL semantics**: In SQL, comparisons with NULL (e.g., `column = NULL`) evaluate to NULL (three-valued logic), which is treated as false in WHERE clauses — so the user sees zero rows. This is standard SQL behavior consistent across DataFusion (which evaluates the filter in-process) and upstream databases like PostgreSQL (if the filter is pushed down). This applies to equality (`=`, `!=`), `IN`, and comparison operators (`>`, `<`). NULL is also handled naturally by `COALESCE` expressions. Note: `IS NULL` would match, so avoid writing filter expressions that use `IS NULL` with user attributes unless that behavior is intentional.
330
330
331
-
**Decision function null semantics**: Missing attributes with no default appear as `null` (not `undefined`). Equality checks work correctly (`null === "acme"` → `false`). However, numeric comparisons have a JS quirk: `null >= 0` is `true` because `null` coerces to `0`. Always guard numeric comparisons with a null check: `if (ctx.session.user.clearance == null) return { fire: true, reason: "missing clearance" }`.
331
+
**Decision function null semantics**: Missing attributes whose default is NULL appear as `null` (not `undefined`). Equality checks work correctly (`null === "acme"` → `false`). However, numeric comparisons have a JS quirk: `null >= 0` is `true` because `null` coerces to `0`. Always guard numeric comparisons with a null check: `if (ctx.session.user.clearance == null) return { fire: true, reason: "missing clearance" }`.
332
332
333
333
**Undefined attributes**: If a policy references `{user.foo}` but no attribute definition named `foo` exists at all, the query fails with an error. This catches typos and stale policies referencing deleted attributes.
334
334
@@ -400,7 +400,7 @@ curl -X POST ... \
400
400
401
401
See [Template variables](#template-variables) above. `{user.KEY}` references produce typed literals based on the attribute definition's `value_type`.
402
402
403
-
**Missing attributes**: if a user does not have an attribute set, the proxy resolves the value from the attribute definition's `default_value`. If a default is set, it is substituted as a typed literal. If no default is set (null), SQL `NULL` is substituted — comparisons with `NULL` evaluate to `NULL` (three-valued logic), which is treated as false in WHERE clauses, so the user sees zero rows. If the attribute has no definition at all, the query fails with an error. See [Missing attribute behavior](#missing-attribute-behavior) for the full resolution table.
403
+
**Missing attributes**: if a user does not have an attribute set, the proxy resolves the value from the attribute definition's `default_value`. If a default is set, it is substituted as a typed literal. If the default is NULL, SQL `NULL` is substituted — comparisons with `NULL` evaluate to `NULL` (three-valued logic), which is treated as false in WHERE clauses, so the user sees zero rows. If the attribute has no definition at all, the query fails with an error. See [Missing attribute behavior](#missing-attribute-behavior) for the full resolution table.
404
404
405
405
### Using attributes in decision functions
406
406
@@ -434,7 +434,7 @@ User attributes are available as first-class fields on `ctx.session.user` with c
434
434
435
435
Note: integer and boolean attributes appear as native JSON types in the decision function context (not strings). List attributes appear as JSON arrays of strings.
436
436
437
-
**Missing attributes in decision context**: attributes the user lacks are resolved via `default_value` from the attribute definition. If a default is set, the typed value appears on `ctx.session.user` as if the user had that attribute. If no default is set, the field is `null` (not `undefined`). Use `== null` checks before numeric comparisons, since `null >= 0` is `true` in JavaScript. See [Missing attribute behavior](#missing-attribute-behavior) for the full resolution table.
437
+
**Missing attributes in decision context**: attributes the user lacks are resolved via `default_value` from the attribute definition. If a default is set, the typed value appears on `ctx.session.user` as if the user had that attribute. If the default is NULL, the field is `null` (not `undefined`). Use `== null` checks before numeric comparisons, since `null >= 0` is `true` in JavaScript. See [Missing attribute behavior](#missing-attribute-behavior) for the full resolution table.
Copy file name to clipboardExpand all lines: docs/security-vectors.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -864,7 +864,7 @@ Note: `tenant` is no longer a reserved key — it is a regular custom attribute.
864
864
865
865
**Bug**: `mangle_vars()` used `unwrap_or("")` for missing attributes with no error or warning. Decision function context omitted missing attributes entirely (`undefined` in JS), causing `undefined >= 0` to silently evaluate to `false` in numeric comparisons.
866
866
867
-
**Defense**: `resolve_user_attribute_defaults()` merges user attributes with definition defaults. Missing attributes with a `default_value` get that value substituted (typed literal in SQL, typed JSON in decision context). Missing attributes with no default get SQL `NULL` (zero rows for equality) and JSON `null` (distinguishable from `undefined`). References to completely undefined attributes (no definition) return an error. The centralized helper is used in all three paths: `mangle_vars` (row filters + masks), query-level decision context (`handle_query`), and visibility-level decision context (`build_typed_json_attributes`).
867
+
**Defense**: `resolve_user_attribute_defaults()` merges user attributes with definition defaults. Missing attributes with a `default_value` get that value substituted (typed literal in SQL, typed JSON in decision context). Missing attributes whose default is NULL get SQL `NULL` (zero rows for equality) and JSON `null` (distinguishable from `undefined`). References to completely undefined attributes (no definition) return an error. The centralized helper is used in all three paths: `mangle_vars` (row filters + masks), query-level decision context (`handle_query`), and visibility-level decision context (`build_typed_json_attributes`).
Copy file name to clipboardExpand all lines: proxy/CLAUDE.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -138,9 +138,9 @@ Custom key-value attributes on users, governed by a schema-first attribute defin
138
138
139
139
**Template variables**: `{user.KEY}` in filter/mask expressions. Built-in fields (`{user.username}`, `{user.id}`) take priority via `match` arms in `UserVars::get()`, preventing attribute override attacks. Custom attributes (including `tenant`) fall through to the resolved attribute map (user values + definition defaults via `resolve_user_attribute_defaults()`).
140
140
141
-
**Missing attribute resolution**: When a user lacks an attribute referenced by a policy, the proxy resolves it from the attribute definition's `default_value`. If set, the default is used as a typed literal. If null, SQL `NULL` is substituted (comparisons with NULL evaluate to NULL → treated as false in WHERE → zero rows). If no definition exists, the query errors. This is handled centrally by `resolve_user_attribute_defaults()` in `hooks/policy.rs`, used in all three paths: template variables, query-level decision context, and visibility-level decision context (`build_typed_json_attributes` in `engine/mod.rs`).
141
+
**Missing attribute resolution**: When a user lacks an attribute referenced by a policy, the proxy resolves it from the attribute definition's `default_value`. If a non-NULL default is set, it is used as a typed literal. If the default is NULL (the default), SQL `NULL` is substituted (comparisons with NULL evaluate to NULL → treated as false in WHERE → zero rows). If no definition exists, the query errors. This is handled centrally by `resolve_user_attribute_defaults()` in `hooks/policy.rs`, used in all three paths: template variables, query-level decision context, and visibility-level decision context (`build_typed_json_attributes` in `engine/mod.rs`).
142
142
143
-
**Decision function context**: Custom attributes are flattened as first-class fields on `ctx.session.user` (e.g., `ctx.session.user.region`, `ctx.session.user.tenant`) with typed JSON values. Missing attributes with a `default_value` appear as their typed default; missing with no default appear as `null` (not `undefined`). Built-in fields (`id`, `username`, `roles`) always take priority. `ctx.session.time.now` is an ISO 8601 / RFC 3339 timestamp of the evaluation time (not session start).
143
+
**Decision function context**: Custom attributes are flattened as first-class fields on `ctx.session.user` (e.g., `ctx.session.user.region`, `ctx.session.user.tenant`) with typed JSON values. Missing attributes with a `default_value` appear as their typed default; missing attributes whose default is NULL appear as `null` (not `undefined`). Built-in fields (`id`, `username`, `roles`) always take priority. `ctx.session.time.now` is an ISO 8601 / RFC 3339 timestamp of the evaluation time (not session start).
144
144
145
145
**Save-time expression validation**: `validate_expression()` in `hooks/policy.rs` dry-run parses filter/mask expressions at policy create/update time and returns 422 if the syntax is unsupported. Called from `validate_definition()` in `dto.rs`.
0 commit comments