Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions reference/components/javascript-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

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

:::

### `databases`

An object containing all databases defined in Harper. Each database is an object of its tables — `databases.data` is always equivalent to `tables`.
Expand Down
12 changes: 10 additions & 2 deletions reference/database/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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

:::

### `@sealed`

Prevents records from including any properties beyond those explicitly declared in the type. By default, Harper allows records to have additional properties.
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions reference/users-and-roles/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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

:::

## Role-Based Operation Restrictions

### Databases and Tables
Expand Down