Security fix: role access rules are now enforced
Severity: high. Before this release, the per-role access rules (service_name, component, verb_mask) were never consulted for API-key requests. Any valid API key could read and write every table of every service, regardless of the role it was bound to. Only the permissions of the underlying database user limited what it could do. The same gap existed for MCP tool calls.
What changed
- REST requests to
/api/v1/{service}/...made with an API key are now checked against the key's role. Rules are evaluated per service, per component (_table/customers,_schema,_proc/name, ...) and per HTTP verb. A request that no rule permits is refused with403and an error envelope naming the role, service, component and verb. - Enforcement is fail-closed: a role with no rules, a rule with
verb_mask = 0, or an inactive role grants nothing. Config-store failures during the check produce500, never a pass-through. - Admin sessions (JWT) are not subject to role rules.
- Services flagged
read_onlynow reject every non-GET request for everyone, including admins and MCP raw SQL. Previously only DDL calls honoured the flag. - MCP tools honour the same rules:
faucet_queryneeds GET,faucet_insertPOST,faucet_updatePATCH,faucet_deleteDELETE,faucet_list_tablesGET on_table,faucet_describe_tableGET on_schema/{table}.faucet_raw_sqlrequires all five verbs on a rule whose component matches_sql(for example*).faucet_list_servicesand tool error hints only name services the role can reach. faucet mcp --transport httpnow requires the same API key or JWT credentials as the main server. It previously served without authentication. stdio mode runs with local admin privileges.- JWT secret:
FAUCET_AUTH_JWT_SECRET(aliasFAUCET_JWT_SECRET) was silently ignored, so deployments without a config file signed admin tokens with a hard-coded development secret. The env vars now work, and when no secret is configured one is generated once and stored in the data directory. Existing admin sessions must log in again after upgrading unlessauth.jwt_secretwas set in a config file. - Rule patterns match service and table names exactly as they appear in the URL (case-sensitive), because service names and quoted table identifiers are case-sensitive.
PUT /api/v1/system/role/{id}no longer deactivates a role whenis_activeis omitted, and role responses return the stored, normalized rules.faucet servelogs a warning at startup for every role that has active keys but no usable rules.- New CLI:
faucet role create --verbs GET [--service NAME] [--component PATTERN]andfaucet role grant --role NAME --verbs ... [--service ...] [--component ...]. - The embedded SQLite config store now really runs in WAL mode with a busy timeout (the previous pragma syntax was ignored by the driver), and the per-request
last_usedwrite on API keys is throttled to once a minute. - The OpenAPI spec documents the
403response.
Upgrade notes
Roles created with faucet role create before this release have no access rules, so keys bound to them will start receiving 403. Roles created in the admin UI default to GET-only on every service, so keys that used to write through them now need POST, PUT, PATCH or DELETE granted. Grant what is needed, for example:
faucet role grant --role readonly --verbs GET
faucet role grant --role writer --verbs GET,POST,PUT,PATCH,DELETE
or edit the role in the admin UI, or PUT /api/v1/system/role/{id} with an access array. The server logs a warning at startup for each affected role.
Component patterns: * (everything), _table/customers (one table), _table/* (all tables and the table listing), or a bare table name. Verb bits: GET=1, POST=2, PUT=4, PATCH=8, DELETE=16, all=31.
Row-level filters on a rule are stored and returned by the API but are still not applied to queries. That remains a documented gap.
npm
npx @faucetdb/faucet@0.1.13 — published to npm as @faucetdb/faucet 0.1.13 with its six platform packages.
Changelog
- 73d4aa0 Merge branch 'wt/cli' into fix/rbac-verb-mask-enforcement
- 3e30939 Merge branch 'wt/core2' into fix/rbac-verb-mask-enforcement
- 9cc18cb Merge branch 'wt/mcp' into fix/rbac-verb-mask-enforcement
- 50a66a8 Merge branch 'wt/ops2' into fix/rbac-verb-mask-enforcement
- d633128 Merge pull request #4 from jonas136/feat/aggregate-group-by
- 85b521c Update README
- bf46912 feat(cli): add role grant, --verbs on role create, RBAC docs and 403 in OpenAPI
- c90ee8e feat(query): add aggregate functions and GROUP BY to the table query API
- 96e0ae0 fix(cli): generate and persist JWT secret, honor FAUCET_AUTH_JWT_SECRET, warn on unusable roles
- db3ca39 fix(mcp): enforce read_only on raw SQL, fail closed on store errors, stop leaking service names
- bc27684 fix(mcp): enforce role access rules for MCP tools and resources
- 53d1a78 fix(rbac): address review findings — raw SQL honours read_only, exact-case matching, JWT secret handling, startup diagnostics
- 3e3497e fix(rbac): case-sensitive rule matching, stored-rule responses, sqlite pragmas, last_used throttle
- 4157d9c fix(rbac): close remaining review findings
- 527b159 fix(rbac): enforce role verb_mask for API-key requests (REST + MCP) (#6)
- f8bf572 fix(rbac): enforce role verb_mask on API-key requests
- 337e9b0 fix(rbac): fail closed on store errors, reject empty verb specs, normalize empty rule patterns
- c97b615 test(rbac): lock in verb_mask enforcement semantics