HarperFast / HarperFast/harper
role operations allowlist: grants are gate-inert for ops registered without api_name (deploy_component, get_status, …); sql bypasses the allowlist
- Dominant language
- JavaScript
- Stars
- 89
- Forks
- 10
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 205
Description
While building the Studio UI for `permission.operations` (HarperFast/studio#1627, shipped in HarperFast/studio#1628), I traced the allowlist gate through the source and believe a set of grants that `add_role`/`alter_role` **accept** can never **match** at authorization time. Static reading of `main` @ 21043f994 and `v5.2.2` — not yet reproduced against a live instance, so please correct me if I've misread the dispatch.
## The mechanism
The allowlist gate resolves the operation's name like this (`utility/operation_authorization.ts:635`):
```ts
const opApiName = requiredPermissions.get(op)?.api_name ?? op;
if (!allowedOps.has(opApiName)) { /* deny */ }
```
`op` is the **handler function's camelCase name**: `serverUtilities.ts` passes the handler function itself to `verifyPerms` (`const verifyPermsResult = opAuth.verifyPerms(operation_json, functionToCheck)`), and `verifyPerms` does `op = operation.name` for functions. `allowedOps` holds the **snake_case** names that `validateOperations` accepted at role-write time.
So for any `requiredPermissions` entry registered **without** the `api_name` third argument, `opApiName` falls back to the camelCase handler name — which can never equal a granted snake_case name. The grant validates, saves, and is silently gate-inert: gate 1 denies the operation even though it is explicitly listed, and the gate-2 SU delegation never fires.
## Affected registrations (no `api_name`, SU-only, wire name ≠ handler name)
From `utility/operation_authorization.ts` on `main`:
- **Components**: `deployComponent` (`deploy_component` — the headline example in studio#1627), `addComponent`, `dropComponent`, `packageComponent`, `setComponentFile`, `setCustomFunction`, `dropCustomFunction`, `dropCustomFunctionProject`
- **Status**: `status.get`/`set`/`clear` (`get_status`, `set_status`, `clear_status`)
- **System**: `configUtils.setConfiguration` (`set_configuration`), `restart.restartService` (`restart_service`), `npmUtilities.installModules` (`install_node_modules`)
- **Data/logs**: `getBackup` (`get_backup`), `schema.cleanupOrphanBlobs` (`cleanup_orphan_blobs`), `transactionLog.readTransactionLog` (`read_transaction_log`), `deleteTransactionLogsBefore`, `delete_.deleteFilesBefore`, `delete_.deleteAuditLogsBefore`
- **Jobs**: `handleGetJobsByStartDate` (`search_jobs_by_start_date`)
(`catchup` also lacks `api_name` but its handler name equals the wire name, so the fallback happens to work. `login`/`logout` bypass `verifyPerms` entirely and are unaffected.)
Notably, the gate's own TODO comment says "get_backup remains the one that relies solely on this gate" — i.e. the *intent* is that these are delegable; the registration just never wired the name.
Ops with **no `requiredPermissions` entry at all** (e.g. `audit_node_modules`, `add_node`, `update_node`, `set_node_replication`, `purge_stream`, `delete_job`, `update_job`, `delete_records_before`) hit the same `?? op` fallback and are equally ungrantable — worth sweeping in the same pass if any are meant to be user-addressable.
## Related gap: `sql` bypasses the allowlist entirely
`serverUtilities.ts` routes `operation === 'sql'` to `checkASTPermissions`/`verifyPermsAST` *instead of* `verifyPerms`, and `verifyPermsAST` has no allowlist check at all. Two consequences:
1. A role restricted to e.g. `operations: ["insert"]` can still run arbitrary `sql` (subject to table CRUD perms) — the "deny anything unlisted" contract doesn't hold for `sql`.
2. Granting `sql` (or `read_only`, which includes it) "works" only because `sql` is never gate-checked in the first place.
## Related nit: legacy alias spellings
`validateOperations` accepts both spellings of aliased ops (`describe_database`/`describe_schema`, `create_schema`/`create_database`, `search_by_id`/`search_by_hash`, `*_custom_function_project`/`*_component`), but the gate resolves to the single `api_name` on the registration — so granting the *other* spelling is gate-inert too. The predefined groups defensively include both spellings, which papers over this for group users but not for individual grants.
## Suggested fix
- Pass `terms.OPERATIONS_ENUM.*` as `api_name` on every user-addressable registration (mechanical), or normalize camelCase→snake_case in the gate fallback.
- Decide whether `sql` should flow through gate 1 (I'd argue yes — the allowlist reads as a hard contract) and add it to the AST path if so.
- An integration test in `operation-user-rbac.test.ts` that grants one of the previously-unwired names (e.g. `operations: ["deploy_component"]`) and asserts the op passes gate 1 — today's tests only exercise ops that happen to have `api_name` set.
- For aliases: either expand alias pairs in `expandOperationsPerms` or document that grants must use the canonical spelling.
## Why it matters downstream
Studio's role editor (HarperFast/studio#1628) offers every name `validateOperations` accepts, so today it can produce roles whose grants look right, save cleanly, and silently don't work — including the deploy-only CI role that motivated studio#1627.
---
🤖 Filed by Claude on behalf of @dawsontoth
Contributor guide
Research direction
Start in utility/operation_authorization.ts and serverUtilities.ts, then inspect the AST permission path and registrations listed in the issue. Run the relevant operation-user-rbac.test.ts coverage, beginning with a grant for deploy_component. Done means the intended user-addressable operations and SQL allowlist behavior are covered by clear regression tests, with alias handling decided.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- authorization, backend-api-design, security, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100