From 75fe33e0cbbbd773fbeb4a41b9a2e3bfb47c6b3b Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Mon, 29 Jun 2026 07:00:21 -0600 Subject: [PATCH] docs: clarify that @hidden, @export are not access controls + allowRead trusted-context / filter caveats Add explicit security-semantics callouts to prevent common misconfigurations: - @export: warning that omitting the directive removes the REST route but not data access; admins still reach the table via the ops API / SQL - @hidden (type + field): warning that the directive is metadata-only; the value is returned on all read surfaces for any role with read permission; use attribute_permissions read:false for real field hiding - tables.* trusted context: note that server-side resource reads bypass allowRead / attribute_permissions by design; custom resources must apply their own authorization when cross-reading protected tables - attribute read:false filter side-channel: note that a restricted field can still be used as a filter predicate, enabling boolean inference of exact values via row-count observation Co-Authored-By: Claude Sonnet 4.6 --- reference/components/javascript-environment.md | 4 ++++ reference/database/schema.md | 12 ++++++++++-- reference/users-and-roles/overview.md | 4 ++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/reference/components/javascript-environment.md b/reference/components/javascript-environment.md index b9aecda3..52b3a5b5 100644 --- a/reference/components/javascript-environment.md +++ b/reference/components/javascript-environment.md @@ -85,6 +85,10 @@ An object whose properties are the tables in the default database (`data`). Each See [Database API](../database/api.md) for full reference. +:::note Server-side `tables.*` reads run in a trusted context +Calls to `tables.X.get()`, `tables.X.search()`, and similar methods from within resource code (or Operations API SQL) execute in a **trusted system context** and do **not** re-apply the target table's `allowRead` guard or role-level `attribute_permissions`. This is by design — server-side code is responsible for its own authorization checks. A computed attribute or custom resource that cross-reads a protected table will pull the raw data into the response regardless of the caller's role. If you expose data from a protected table through a custom resource, apply authorization explicitly (e.g. check `getCurrentUser()` and enforce the relevant restrictions before returning the data). +::: + ### `databases` An object containing all databases defined in Harper. Each database is an object of its tables — `databases.data` is always equivalent to `tables`. diff --git a/reference/database/schema.md b/reference/database/schema.md index 904e1389..ece65002 100644 --- a/reference/database/schema.md +++ b/reference/database/schema.md @@ -174,6 +174,10 @@ type MyTable @table @export(name: "my-table") { The optional `name` parameter specifies the URL path segment (e.g., `/my-table/`). Without `name`, the type name is used. +:::warning `@export` is a routing directive, not an access control +Omitting `@export` removes the REST/MQTT route for a table (callers get 404), but it does **not** protect the data. The table still exists in the database and its records remain fully accessible to administrators via the Operations API and SQL. For data confidentiality, use role permissions (`attribute_permissions`, `read: false`) rather than relying on the absence of an export route. +::: + ### `@sealed` Prevents records from including any properties beyond those explicitly declared in the type. By default, Harper allows records to have additional properties. @@ -196,6 +200,10 @@ type InternalConfig @table @hidden { } ``` +:::warning `@hidden` does not restrict data access +`@hidden` only suppresses a type or field from generated API specs and MCP tool schemas. The underlying data is returned on **all** read surfaces — REST, SQL, and the Operations API — for any user with table-level `read` permission. Do not use `@hidden` as a confidentiality control. To restrict which users can read a field or table, use role `attribute_permissions` with `read: false`. +::: + `@hidden` is also available as a [field directive](#hidden-field-directive) to suppress individual attributes. ## Documenting Types and Fields @@ -326,7 +334,7 @@ type Event @table { ### `@hidden` (Field Directive) -Suppresses the field from MCP tool descriptors and the OpenAPI document. The attribute still exists in the table; data is still queryable through other interfaces subject to RBAC. Use this for fields that should not appear in introspectable surfaces. +Suppresses the field from MCP tool descriptors and the OpenAPI document. The attribute still exists in the table; data is still returned on all read surfaces (REST GET, SQL, Operations API) for any user with read permission on the table. Use this for fields that should not appear in generated specs or tool schemas, not to restrict data access. ```graphql type Customer @table { @@ -340,7 +348,7 @@ type Customer @table { } ``` -`@hidden` is a metadata-visibility directive, not access control: `attribute_permissions` on roles remains the data-access enforcement mechanism. +`@hidden` is a metadata-visibility directive, not access control: `attribute_permissions` on roles remains the data-access enforcement mechanism. A field marked `@hidden` is still readable by any role with table `read` access — to prevent a role from reading a field value, set `read: false` in `attribute_permissions` for that role. ## Relationships diff --git a/reference/users-and-roles/overview.md b/reference/users-and-roles/overview.md index cc4f11bb..3a8e0670 100644 --- a/reference/users-and-roles/overview.md +++ b/reference/users-and-roles/overview.md @@ -173,6 +173,10 @@ Each table entry defines CRUD access: - `DELETE` is not an attribute-level permission. Deleting rows is controlled at the table level. - The `__createdtime__` and `__updatedtime__` attributes managed by Harper can have `read` permissions set; other attribute-level permissions for these fields are ignored. +:::note Attribute `read: false` — filter side-channel +Setting `read: false` on an attribute prevents the **value** from appearing in any response body (REST, SQL, Operations API, GraphQL all omit or reject it). However, the attribute can still be used as a **filter predicate** by that role — e.g. `GET /Table/?salary=95000` returns 0 or 1 rows, revealing whether any record holds that exact value. Binary-search enumeration of the restricted column is possible without ever reading a value directly. If preventing any inference from query results is a requirement, the application must reject or ignore filter conditions on `read: false` attributes in a custom resource handler. +::: + ## Role-Based Operation Restrictions ### Databases and Tables