Skip to content

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

Description

@dawsontoth

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 @ 21043f9 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):

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

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions