Skip to content

docs: clarify that @hidden, @export are not access controls + allowRead trusted-context / filter caveats#553

Draft
kriszyp wants to merge 1 commit into
mainfrom
kris/docs-security-semantics
Draft

docs: clarify that @hidden, @export are not access controls + allowRead trusted-context / filter caveats#553
kriszyp wants to merge 1 commit into
mainfrom
kris/docs-security-semantics

Conversation

@kriszyp

@kriszyp kriszyp commented Jun 29, 2026

Copy link
Copy Markdown
Member

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 :::warning callout under @export clarifying it is a REST routing directive only (omitting it returns 404 but the data remains ops-API/SQL accessible to admins). Added :::warning callout under @hidden (type directive) that the value is returned on all read surfaces for any role with table read permission. Strengthened the @hidden field directive description with the same boundary.
  • reference/users-and-roles/overview.md — Added :::note under attribute-level rules clarifying that read: false blocks 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 :::note under the tables global clarifying that server-side tables.X.get()/.search() calls execute in a trusted system context and do not re-apply the target table's allowRead or role-level attribute_permissions; resource code must apply its own authorization when cross-reading protected tables.

🤖 Generated with Claude Code

…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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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. ...

@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-553

This preview will update automatically when you push new commits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants