HarperFast / HarperFast/harper

roles.yaml cannot grant an operation the same component registers: roles plugin loads before jsResource

Open
#2,203 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
89
Forks
10
Avg merge
2d 6h
Merged PRs (30d)
200

Description

A component's own `roles.yaml` cannot grant an operation that the same component's `resources.js` registers, because the `roles` plugin runs before `jsResource` in the same worker. Static trace against `main` @ ddb27164b; not reproduced against a live instance, so please correct me if I've misread the load order.

This is adjacent to but distinct from the cross-thread grantability gap being fixed separately (companion PR linked below) — that one was main-thread `validateOperations` never learning about worker registrations at all. This one is a same-thread ordering problem and survives that fix.

## The mechanism

`server.registerOperation({ requiresSuperUser })` calls `registerOperationPermission`, which marks the operation grantable in the registering thread's module-local set (`utility/operationPermissions.ts` → `dynamicallyRegisteredOps`). A component's `roles.yaml` is applied by `resources/roles.ts → handleApplication`, which calls `addRole`/`alterRole` (`security/role.ts`) → `addRoleValidation` → `validateOperations`.

Both run in the **same worker** (`componentLoader.ts:669` gates `handleApplication` on `resources.isWorker`), so grantability is visible in principle. The problem is ordering:

- `components/DEFAULT_CONFIG.ts` declares plugins in the order `rest`, `graphqlSchema`, `roles`, `jsResource`, `fastifyRoutes`, `static`.
- `componentLoader.ts:542` iterates with `for (const componentName in config)` — insertion order.

So `roles` is processed before `jsResource`. When `roles.yaml` is validated, the `registerOperation` calls in `resources.js` have not run yet, the name is not in `dynamicallyRegisteredOps`, and the role is rejected with `INVALID_OPERATIONS_OP`:

```
Invalid operations value ''. Must be a valid operation name or group (e.g. 'read_only').
```

## Why it matters

Declaring an operation and the role that may call it in the same component is the natural shape — it's the whole point of shipping `roles.yaml` alongside `resources.js`. Today an author has to either grant the operation out-of-band via the `add_role` API after deploy, or split the operation and its role across two components ordered so the registering one loads first.

It fails closed (a rejected role, never a widened one), so this is a DX/correctness bug, not a security hole.

## Repro sketch

A single component with:

```js
// resources.js
server.registerOperation({
name: 'my_component_op',
requiresSuperUser: true,
execute: async function myComponentOp() { return { ok: true }; },
});
```

```yaml
# roles.yaml
my_op_role:
operations:
- my_component_op
```

Expected: the role is created with the grant. Actual (expected from the trace): component load reports the role as invalid.

## Possible directions

- Order `jsResource` before `roles` in `DEFAULT_CONFIG` — smallest change, but relies on object key order as a load-order contract, and only helps the default config (an explicit component config with its own key order would still be able to get this wrong).
- Defer `roles.yaml` application until after the component's other plugins have loaded (e.g. apply on the existing `deploy:end`/ready hook rather than inline at `handleEntry`) — `resources/roles.ts` already has a `deploy:end` reconcile path, so this may be mostly a matter of which pass performs the *first* application.
- Make load order explicit rather than incidental, so a plugin can declare that it must run after `jsResource`.

I'd lean toward the second — the reconcile pass already exists and ordering-by-key-insertion is a fragile contract to lean on.

## Tests worth adding

An integration test with a single fixture component that registers a grantable operation in `resources.js` and grants it in its own `roles.yaml`, asserting the role exists with the grant after load. `integrationTests/components/registered-operation.test.ts` now covers the API-side (`add_role`) path across the worker/main boundary but nothing exercises `roles.yaml`.

---
🤖 Filed by Claude on behalf of @dawsontoth

Contributor guide

Open the contributing guide

Research direction

Start with components/DEFAULT_CONFIG.ts and componentLoader.ts to trace plugin ordering, then read resources/roles.ts, security/role.ts, and utility/operationPermissions.ts. Use integrationTests/components/registered-operation.test.ts as the testing entry point and add a fixture covering resources.js with its own roles.yaml. Done means the role loads with the registered operation granted.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js, typescript
Domain
authorization, backend, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
56/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.