HarperFast / HarperFast/harper

role operations allowlist: grants are gate-inert for ops registered without api_name (deploy_component, get_status, …); sql bypasses the allowlist

Open
#2,175 4 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.