Skip to content

feat(access): list roles as roles, show allow-all, one membership source - #435

Merged
huyplb merged 2 commits into
mainfrom
feat/access-roles-and-allow-all
Sep 26, 2026
Merged

huyplb merged 2 commits into
mainfrom
feat/access-roles-and-allow-all

Conversation

@huyplb

@huyplb huyplb commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

This implements the Database Access Review. There were three complaints:

  • The UI doesn't show existing roles or groups.
  • A user's roles contradict each other between screens.
  • There's no allow-all / * permission.

The root cause was mostly the catalog read, so that's fixed first, then the UI.

1. The catalog read (packages/sql/src/modules/access/db-access.ts)

Checked live against the local containers by running probeDbAccess with throwaway accounts on each engine:

Engine Before After
MySQL, TiDB Every role listed as a user. GRANT ALL ON *.* showed as "No object privileges". Locked, passwordless row = role. USER_PRIVILEGES read as GLOBAL rows. role_edges read as ROLE rows.
MariaDB Roles listed as users, named r@. is_role, bare role names, members from roles_mapping. *.* read.
Postgres, Cockroach, Yugabyte Superuser, ALL ON DATABASE and ALL ON SCHEMA all read as nothing. The schema branch (usage_privileges WHERE object_type='SCHEMA') could never match. rolsuper read. Schema and database ACLs read via aclexplode. The old forms are kept as fallbacks.
ClickHouse No memberships. *.* shown as SYSTEM. system.role_grants read. *.* shown as GLOBAL.
Db2 OS groups dropped. GRANTEETYPE 'G' listed as groups.
SQL Server sysadmin and db_owner showed as nothing. IS_SRVROLEMEMBER('sysadmin') read as superuser. Fixed roles' implied permissions added (source: 'implied').

reconcileDbAccess (called in db-access.service.ts) fills memberOf/members and the ROLE rows from each other. Every screen now gets one answer about membership. Before, the list said "member of X" while the detail pane said "Belongs to no roles".

Each MySQL-family query has a fallback ladder. I checked a low-privilege MySQL login live: it still gets its own grants, plus a warning naming what it couldn't read.

2. UI

  • Database Access (Utilities, also embedded in Access):
    • an allow-all / superuser tag and a banner, followed through role chains ("Inherited through ops → admin");
    • typing allow-all in the filter lists only those accounts;
    • a full privilege set collapses into one ALL PRIVILEGES row, which expands and has Revoke all;
    • grantable replaces the * marker, which clashed with the SQL wildcard;
    • "Membership of a role" is now a picker of the listed roles, with Other… for a typed name.
  • User Management: the allow-all tag. Add can put the account in existing roles through a new UserRequest.roles. It also emits SET DEFAULT ROLE ALL on MySQL/TiDB, or SET DEFAULT ROLE r FOR on MariaDB, because a granted role is otherwise inactive at login. All three engines ran these statements live. MySQL/TiDB roles now parse as name@host, so Drop emits DROP ROLE 'r'@'%'.
  • Permissions tab: the allow-all banner on the account stage.

3. Allow-all grants

A new "All privileges (allow-all)" grant kind uses allPrivilegeTargets. Each engine gets its own widest form, with a note on what it really confers:

Engine What is granted
MySQL family, ClickHouse *.*, or db.*
Postgres Database or schema ALL. The note says it reads no table, and that server-wide access is superuser.
SQL Server CONTROL
Oracle ALL PRIVILEGES
Db2 DBADM

It's marked critical, and Run stays disabled until the principal's name is typed. I checked grant and Revoke all end to end in the browser on MySQL.

This differs from the review doc. The doc proposed a new all permission and an instance scope inside the intent builder. A new AccessScope type would touch about ten files of scope-switching logic (emitters, diff, matrix, sections). I made allow-all its own deliberate grant kind instead, on the catalog grant path that already emitted ALL / *.* correctly.

Also fixed along the way

  • Role grants now pass WITH ADMIN OPTION. The form's checkbox was being dropped silently.
  • ClickHouse database grants render as db.*. A bare db names a table.
  • effective.ts used literal NUL bytes as a key separator, which made grep treat the file as binary.

Tests: each one fails without its fix

  • Live e2e. A new step in database-access-dialects.test.ts creates a role, a member and an allow-all grant through /api/sql/execute, asserts what the Users screen shows, then cleans up.
    • It passes on postgres, mysql, mariadb and clickhouse. TiDB is skipped, with a warning, because its e2e login lacks ROLE_ADMIN.
    • A/B, run 1: reverting role-kind detection fails MySQL and MariaDB ("expected 'user' to be 'role'"). Reverting rolsuper fails Postgres (no superuser tag).
    • A/B, run 2: reverting ClickHouse role_grants fails the roles column. Reverting MySQL *.* fails the allow-all tag.
  • Unit.
    • The new privilege-groups.test.ts and reconcileDbAccess / query-shape tests in db-access.test.ts.
    • Create-with-roles tests in user-sql.test.ts.
    • Mutation checks: removing either direction of reconciliation fails "reads a membership from either side on its own". Stopping findAllowAll from following roles fails 4 tests.
  • Components. DatabaseAccessModal.test.tsx (ALL row and Revoke all, inherited superuser plus filter, grantable, role picker, typed allow-all confirm), AccessView.test.tsx (member-of picker, superuser tag, MySQL role drop) and AccessPermissionPanel.test.tsx (banner).
  • Gates. cd apps/web && npx tsc --noEmit is clean. The e2e tsc is clean. The unit tests for packages/sql, access and naming pass 1,663, and web-ui passes 243.

Not done, or unverified

  • TiDB leaves information_schema.SCHEMA_PRIVILEGES empty even for root (the grant is only in mysql.db), so TiDB db.* grants still don't show. This predates this PR; it needs a mysql.db read.
  • Redshift still runs the Postgres principal query. Real Redshift keeps groups in pg_group, and I have no way to verify against it locally.
  • SQL Server was checked through unit tests only. Its container wasn't running.
  • Full npx vitest run on macOS has 41 failures in workflow-engine / workflow-server file-pipe tests. They reproduce on unmodified origin/main. The cause is c20efd3's path confinement meeting the /var → /private/var symlink, and it's unrelated to this PR. I've flagged it as a separate task.

🤖 Generated with Claude Code


Note

Medium Risk
Changes cross-engine GRANT catalog queries, reconciliation, and grant/revoke SQL generation—security-sensitive database access behavior with wide dialect surface area.

Overview
Fixes database access catalog reads and UI so roles list as roles, membership is consistent everywhere, and superuser / ON *.* / db-wide control show up instead of looking like empty privilege sets.

Catalog (@foxschema/sql + server probe): Dialect probes now pull MySQL-family roles, global USER_PRIVILEGES, Postgres rolsuper and schema/database ACLs, ClickHouse role_grants / GLOBAL, SQL Server sysadmin, and more. reconcileDbAccess merges memberOf / members with ROLE privilege rows (and adds implied SQL Server fixed-role privileges). New privilege-groups helpers collapse expanded ALL grants, detect allow-all (including via roles), and drive dialect-specific “grant everything” targets.

UI: Database Access and User Management show allow-all / superuser badges and banners, filter by those terms, group privileges as collapsible ALL PRIVILEGES with Revoke all, and add an All privileges grant flow with name-to-confirm. Add user can pick Member of roles (SQL includes MySQL/MariaDB SET DEFAULT ROLE). MySQL/TiDB roles use name@host for drop/grant. Permissions account stage shows the same allow-all banner.

Tests & docs: E2E step for roles/tags per dialect, broad unit/component coverage, and USER_GUIDE updates.

Reviewed by Cursor Bugbot for commit a0e4c35. Bugbot is set up for automated code reviews on this repo. Configure here.

The Database Access screens read the catalog incompletely, so the most
powerful accounts looked like they held nothing and roles looked like users.

Catalog (packages/sql db-access):
- MySQL/TiDB: a locked, passwordless mysql.user row is a role. MariaDB:
  is_role, named bare (no host), members from roles_mapping.
- MySQL family: read USER_PRIVILEGES as GLOBAL (*.*) rows and role
  memberships as ROLE rows, with fallbacks for logins that cannot read mysql.*.
- Postgres family: read rolsuper; read schema and database ACLs with
  aclexplode (the usage_privileges WHERE object_type = 'SCHEMA' branch could
  never match). Old forms kept as fallbacks.
- ClickHouse: memberships from system.role_grants; *.* is GLOBAL, not SYSTEM.
- Db2: list OS groups (GRANTEETYPE 'G'). SQL Server: sysadmin as superuser,
  and what db_owner / db_datareader / db_datawriter imply.
- reconcileDbAccess fills memberOf/members and ROLE rows from each other, so
  the list and the detail pane can no longer disagree.
- Role grants honour WITH ADMIN OPTION; MySQL roles are named 'r'@'%'.

UI:
- Database Access: allow-all/superuser tag and banner (followed through
  roles), filter by "allow-all", a full privilege set collapsed to one
  ALL PRIVILEGES row with Revoke all, "grantable" instead of "*", a role
  picker for membership, and an "All privileges" grant kind per engine with
  typed-name confirmation.
- User Management: allow-all tag, and Add can put the account in existing
  roles (UserRequest.roles), with SET DEFAULT ROLE on the MySQL family.
- Permissions tab: allow-all banner on the account.

Also: effective.ts used literal NUL bytes as a key separator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_e0b6f285-10f9-4b59-8360-bec4e485810f)

The modal tests clicked db-access-principal-alice as soon as fetchDbAccess
had been called. The mock resolves a tick later, so on a slow runner the
list was still empty and CI failed 'lists the membership under Role
memberships' with no element to click. Wait for the row itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_b7585e4b-91a2-47ee-a2f2-0b92ee5d47ae)

@huyplb
huyplb merged commit a20cd61 into main Sep 26, 2026
12 checks passed
@huyplb
huyplb deleted the feat/access-roles-and-allow-all branch September 26, 2026 04:59
huyplb added a commit that referenced this pull request Sep 27, 2026
refactor(access): simplify the roles and allow-all code from #435
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant