Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 58 additions & 27 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,10 +450,11 @@ Four things worth knowing before changing the record editor:
save that 403s. Check both `attribute_permissions` (v5) and `attribute_restrictions` (translated
v4). Only `super_user` short-circuits; `structure_user` covers DDL, not DML.
- **Version-gated, but an unreadable version does NOT block the save.** Harper denies
`registration_info` to any role carrying an `operations` allowlist — it is in the gate-inert list in
`config/roles/operations/operationsCatalog.ts` — which is exactly the shape of role
`checkTablePutPermission` exists to accept. Blocking on an unread version therefore made the
feature permanently unavailable for those roles, behind a "reload and try again" that never helps.
`registration_info` to any role whose `operations` allowlist does not list it (and before 5.3 to one
that does — `gate: 'from-5.3'` in `config/roles/operations/operationsCatalog.ts`), which is exactly
the shape of role `checkTablePutPermission` exists to accept. Blocking on an unread version therefore
made the feature permanently unavailable for those roles, behind a "reload and try again" that never
helps.
Allowing it is safe because the fallback isn't silent: `put` is a distinct operation name, so a
pre-5.3 instance rejects the request outright rather than merging and keeping the attribute. A
version that IS readable and predates the operation still blocks, for the clearer message, and
Expand Down Expand Up @@ -583,21 +584,39 @@ execute relationship joins).

Harper 5.0+ roles accept an `operations` allowlist. Almost every intuitive reading of it is wrong,
and each of these cost a review round on #1628 — verify against the Harper source before changing
`src/features/instance/config/roles/operations/**` or `src/integrations/api/localRolePermission.ts`.

- **It does not restrict super users.** `verifyPerms` returns early for `super_user`
("admins can do (almost) anything") _before_ the allowlist gate. The allowlist is a way to scope
a NON-super role (gate 2 then lets it hold otherwise-SU operations), not a way to narrow an admin.
`src/features/instance/config/roles/operations/**` or `src/integrations/api/localRolePermission.ts`,
**at the tag you mean**. Harper changed gate 1 twice in 5.3, and `resolveGrantBand` turns a version
into one of three bands (`pre-5.3`, `5.3-early`, `5.3`) so callers don't parse SemVer per operation:
from v5.3.0-alpha.1 (harper `f8a5aa90a`) `verifyPerms` runs the gate ahead of every privilege
early-return and `chooseOperation` runs it on `sql`; from v5.3.0-beta.3 every entry names its grant.
`allowlistGatesEveryPath(band)` is the first change.

- **The list never narrows a stored super user, for a different reason on each side.** Through 5.2.x
`verifyPerms` returns early for `super_user` ("admins can do (almost) anything") _before_ the gate.
From v5.3.0-alpha.1 the gate comes first ("must run before EVERY privilege early-return"), and a
list beside `super_user` would be enforced, but `validateNoSUPerms` (next bullet) refuses to store
one. Either way the allowlist scopes a NON-super role (gate 2 then lets it hold otherwise-SU
operations).
- **`super_user`/`cluster_user` + `operations` cannot even be saved.** `validateNoSUPerms` rejects
any permission with more than one key that sets either flag, so the combination 400s. Strip the
allowlist for those roles rather than sending it.
- **`structure_user` is different and IS gated** — only `create_table`/`create_attribute`/
`drop_table`/`drop_attribute` (plus create/drop database for the boolean form) short-circuit. The
array form scopes that carve-out to its listed databases and never reaches create/drop database.
- **~23 operations are inert when granted.** Their `requiredPermissions` entry omits `api_name`, so
the gate compares a camelCase handler name against a snake_case grant and never matches
(`deploy_component`, `get_status`, `registration_info`, …). Filed as HarperFast/harper#2175.
`sql` is worse: it routes to `verifyPermsAST`, which never consults the allowlist at all.
- **`structure_user` DDL skips the list only before 5.3.** Through 5.2.x `create_table`/
`create_attribute`/`drop_table`/`drop_attribute` (plus create/drop database for the boolean form)
return before the gate. From v5.3.0-alpha.1 the gate runs first, so an allowlisted structure user
runs that DDL only when it is listed. On both, the array form keeps the DDL to its listed databases
(listing `drop_table` cannot reach another one), and its flag never covers create/drop database.
`structureUserDdlScope` reads the shape only; the editor's copy takes the band.
- **Which names the gate matches depends on the version.** Before v5.3.0-beta.3, 20 entries omit `api_name`,
so the gate compares a camelCase handler name against a snake_case grant and never matches
(`deploy_component`, `get_status`, `registration_info`, …; HarperFast/harper#2175). From
v5.3.0-beta.3 every entry names its grant, and `get_backup`, `read_transaction_log` and `catchup`
name `null` on purpose, so no allowlist reaches them. The catalog records this per entry (`gate`), and
callers resolve the instance version into a `GrantBand` once.
- **`sql` ignores the list before 5.3 and needs it from v5.3.0-alpha.1.** Through 5.2.x
`chooseOperation` sends it straight to `checkASTPermissions`, which never consults the allowlist,
so listing or omitting it changes nothing. From alpha.1 it calls `verifyOperationsAllowlist` on
`sql` first, so a list without `sql` (or a group carrying it, like `read_only`) denies every SQL
statement. Table permissions apply on both. The catalog marks it `outOfGate: 'pre-5.3'`.
- **Alias spellings are inert too** (`describe_database`, `search_by_id`, `create_schema`, …): both
spellings dispatch to one handler whose entry carries only the canonical `api_name`.
- **A non-iterable value takes the instance down.** `listUsers` expands every assigned role's
Expand All @@ -613,22 +632,28 @@ and each of these cost a review round on #1628 — verify against the Harper sou
Practical consequence for the UI: never describe a restriction the server does not enforce, and
never advise "fixing" an `operations` value in place — both mistakes shipped and were caught in
review. `classifyOperationsValue` needs the instance version because the same shape means different
things on either side of the 5.0.0-alpha.8 floor (the feature's first tagged build, not 5.0.0).
things on either side of the 5.0.0-alpha.8 floor (the feature's first tagged build, not 5.0.0). The
editor's structure-user and SQL sentences need the band for the same reason: each is true on one side
of v5.3.0-alpha.1 only.

### Which of those the UI gating turns on

The two facts that decide Studio's permission hooks (`src/hooks/checkOperationPermission.ts`):

- **Gate admin chrome on the allowlist and you hide UI that works.** Because super users never reach
the gate, `useInstanceManagePermission` and `useInstanceBrowseManagePermission` are deliberately NOT
gated. Note browse-manage is only half short-circuited: its DDL half is, but the application-editor
operations behind it (`set_component_file`, `drop_component`, `deploy_component`, `set_env_value`,
`restart_service`) are not in `STRUCTURE_USER_OPS`, so for a non-super role they do reach the gate —
where an allowlist listing them _grants_ them via gate 2. Denying that half on the allowlist would
therefore hide UI a scoped role can legitimately use.
- **`isElevatedRole` is the wrong question for a specific operation.** It answers "is the list
unenforceable anywhere for this role", which is what the editor warns about. For DML,
`structure_user` and `cluster_user` are still gated.
- **Gate admin chrome on the allowlist and you hide UI that works.** No stored super user fails the
gate on any version (see above), so `useInstanceManagePermission` is deliberately NOT gated.
`useInstanceBrowseManagePermission` isn't either, but only pre-5.3 makes that exact: there its DDL
half returns before the gate for a structure user. From v5.3.0-alpha.1 nothing does, so an
allowlisted structure user is offered DDL the server refuses unless the list names it. The hooks
can't see the version, and gating them on the list would hide DDL that works on 5.2, so they stay
open and the server's 403 names the operation (HarperFast/studio#1894 tracks per-operation checks).
The application-editor operations behind browse-manage (`set_component_file`, `drop_component`,
`deploy_component`, `set_env_value`, `restart_service`) are not in `STRUCTURE_USER_OPS`, so for a
non-super role they reach the gate on every version — where an allowlist listing them _grants_ them
via gate 2. Denying that half on the allowlist would hide UI a scoped role can legitimately use.
- **Only `super_user` short-circuits a specific operation.** For DML, `structure_user` and
`cluster_user` are gated on every version, so `checkTableOperationsAllowed` exempts the first flag
alone. Asking whether a role carries an elevating flag is the editor's question, not this one.

Also two different expansions are correct for two different questions, and sharing one silently broke
the second once: `expandEffectiveOperations` folds alias spellings together to describe a role's
Expand All @@ -646,6 +671,12 @@ denied. `testUtils.preTestPrep()` + `testUtils.setGlobalSchema(...)` provide the
`verifyPermsAST` (to show SQL skipping the gate). Run with `npx mocha unitTests/utility/<file>`, read
the verdicts, delete the file. Measured this way: `super_user` + `operations: []` still ALLOWS insert,
search, restart and get_configuration, while `super_user: false` + `operations: []` DENIES insert.
Those are 5.2 verdicts. A built `dist` answers too, without touching a checkout — a checkout's, or an
installed `harper` package's, which is how to compare versions: `require` its
`utility/operation_authorization.js`, set `global.hdb_schema`, and call `verifyPerms` (or
`verifyOperationsAllowlist(requestJson, 'sql')` for the SQL gate). Run that way on 5.2.3, 5.3.0-beta.2
and 5.3.1, `structure_user: true` + `operations: ['read_only']` ALLOWS `create_table` on the first and
DENIES it on the other two, and so does `super_user: true` + `operations: []`.

## Datadog RUM — `beforeSend` fails open, so nothing in it may throw

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ export const dataTableColumns: Array<ColumnDef<LocalRole>> = [
function OperationsCell({ permission }: { permission: LocalRole['permission'] }) {
// One observer for the whole table: the query is shared, but a hook per row would still
// allocate an observer and query options for every one of them.
const allowlistSupported = useContext(AllowlistSupportedContext);
if (allowlistSupported === undefined) {
const allowlistVersion = useContext(AllowlistSupportedContext);
if (allowlistVersion === undefined) {
// The verdict depends on the version; render nothing rather than guess it for a frame.
return null;
}
const kind = classifyOperationsValue(permission, allowlistSupported);
const kind = classifyOperationsValue(permission, allowlistVersion.supported);
// Below the floor the key is either a real database grant or inert; neither is a restriction.
if (kind === 'absent' || kind === 'database' || kind === 'inert') {
// aria-label is not exposed on a roleless span, so the text itself has to carry it.
Expand Down Expand Up @@ -115,7 +115,7 @@ function OperationsCell({ permission }: { permission: LocalRole['permission'] })
</span>
);
}
const effective = expandEffectiveOperations(getOperationsAllowlist(permission) ?? []);
const effective = expandEffectiveOperations(getOperationsAllowlist(permission) ?? [], allowlistVersion.band);
const summary = summarizeOperations(effective);
return (
<span title={summary}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { useOperationsAllowlistSupported } from '@/features/instance/config/roles/operations/useOperationsAllowlistSupported';
import {
AllowlistVersion,
useAllowlistVersion,
} from '@/features/instance/config/roles/operations/useOperationsAllowlistSupported';
import { createContext, PropsWithChildren } from 'react';

/**
* The instance's allowlist support, resolved once for a list rather than per row. The underlying
* The instance's allowlist verdicts, resolved once for a list rather than per row. The underlying
* query is shared, but a hook call per row still allocates an observer and query options for every
* row — including the ordinary case where no role carries an `operations` key.
*/
export const AllowlistSupportedContext = createContext<boolean | undefined>(undefined);
export const AllowlistSupportedContext = createContext<AllowlistVersion | undefined>(undefined);

export function AllowlistSupportedProvider({ children }: PropsWithChildren) {
return <AllowlistSupportedContext value={useOperationsAllowlistSupported()}>{children}</AllowlistSupportedContext>;
return <AllowlistSupportedContext value={useAllowlistVersion()}>{children}</AllowlistSupportedContext>;
}
Loading
Loading