docs: clarify that @hidden, @export are not access controls + allowRead trusted-context / filter caveats#553
docs: clarify that @hidden, @export are not access controls + allowRead trusted-context / filter caveats#553kriszyp wants to merge 1 commit into
Conversation
…ad 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 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request updates the documentation to clarify security boundaries and access controls, specifically highlighting that server-side tables.* reads run in a trusted context, @export and @hidden are not access control mechanisms, and read: false attributes can still be exploited via filter side-channels. The review feedback suggests correcting the reference to getCurrentUser() to this.getCurrentUser() or getUser(), clarifying that non-exported tables are accessible to any role with SQL/Operations API permissions (not just administrators), and correcting the side-channel example to account for non-unique attributes returning multiple rows.
| 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). |
There was a problem hiding this comment.
In the Harper JavaScript environment, getCurrentUser() is not a global or standalone function; the global function is getUser(). getCurrentUser() is a method on the Resource class/instance. Therefore, calling getCurrentUser() directly will result in a ReferenceError.
Inside a custom resource handler, you should call this.getCurrentUser() or import and use the global getUser() function.
Suggested change:
... (e.g. check `this.getCurrentUser()` or use `getUser()` and enforce the relevant restrictions before returning the data).| 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. |
There was a problem hiding this comment.
While administrators certainly have full access, the table and its records also remain accessible to any non-admin user/role that has permissions to execute SQL or NoSQL operations via the Operations API.
To prevent a common security misconception that only administrators can bypass the missing @export route, consider clarifying that the data is accessible to any role with sufficient Operations API or SQL permissions.
Suggested change:
... and its records remain fully accessible to administrators (and any roles with SQL or Operations API permissions) via the Operations API and SQL. ...| - 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. |
There was a problem hiding this comment.
The statement that GET /Table/?salary=95000 "returns 0 or 1 rows" is only accurate if the attribute is unique. For non-unique attributes like salary, it can return multiple matching rows. Furthermore, performing a binary-search enumeration requires using inequality operators (such as greater_than or less_than), which naturally return multiple rows.
Consider clarifying this to refer to matching rows or the count of matching rows.
Suggested change:
... — e.g. `GET /Table/?salary=95000` returns matching rows (or a count of matching rows), revealing whether any record holds that exact value. ...
🚀 Preview DeploymentYour preview deployment is ready! 🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-553 This preview will update automatically when you push new commits. |
These documentation additions address four behaviors that are easy to mistake for access controls but are not, helping developers avoid building insecure schemas.
reference/database/schema.md— Added:::warningcallout under@exportclarifying it is a REST routing directive only (omitting it returns 404 but the data remains ops-API/SQL accessible to admins). Added:::warningcallout under@hidden(type directive) that the value is returned on all read surfaces for any role with tablereadpermission. Strengthened the@hiddenfield directive description with the same boundary.reference/users-and-roles/overview.md— Added:::noteunder attribute-level rules clarifying thatread: falseblocks the value from response bodies but the attribute can still be used as a filter predicate, enabling boolean inference of exact values via row-count observation.reference/components/javascript-environment.md— Added:::noteunder thetablesglobal clarifying that server-sidetables.X.get()/.search()calls execute in a trusted system context and do not re-apply the target table'sallowReador role-levelattribute_permissions; resource code must apply its own authorization when cross-reading protected tables.🤖 Generated with Claude Code