Files
boltage/plans/04-users-transactions-sysinfo.md
T

8.5 KiB

User and role management, SHOW TRANSACTIONS, :sysinfo

Problem

  • Neo4j's user and role commands on the system database (SHOW USERS, CREATE USER, GRANT ROLE, ...) are rejected by _run_system (NotSystemDatabaseCommand). dbms.showCurrentUser always reports the role admin (boltage/backend.py:247).
  • SHOW TRANSACTIONS / TERMINATE TRANSACTIONS are not emulated.
  • neo4j-browser's :sysinfo frame shows nothing useful, because the queries behind it are not emulated.

Authentication already maps Neo4j users to PostgreSQL roles one to one, so the natural model is that Neo4j users and roles are PostgreSQL roles.

Part 1: users and roles

Mapping

Neo4j PostgreSQL
user role with LOGIN
role role without LOGIN
user has role role membership (GRANT role TO user)
suspended user NOLOGIN on a role that has a password (or a marker, see below)
admin role superuser, or membership in a configurable role
home database role setting boltage.home_database (ALTER ROLE ... SET boltage.home_database = 'g', a custom GUC)

Commands

Cypher (on system) SQL
SHOW USERS pg_roles where rolcanlogin (or suspended), with memberships from pg_auth_members
SHOW CURRENT USER same, for current_user
CREATE USER u [IF NOT EXISTS] SET [PLAINTEXT] PASSWORD 'p' [CHANGE [NOT] REQUIRED] [SET STATUS ...] [SET HOME DATABASE g] CREATE ROLE u LOGIN PASSWORD 'p', plus ALTER ROLE ... SET
ALTER USER u SET PASSWORD 'p', SET STATUS SUSPENDED/ACTIVE, SET HOME DATABASE ALTER ROLE
ALTER CURRENT USER SET PASSWORD FROM 'old' TO 'new' check old by opening a short connection with it, then ALTER ROLE current_user PASSWORD 'new'
DROP USER u [IF EXISTS] DROP ROLE u
SHOW ROLES [WITH USERS], CREATE ROLE r, DROP ROLE r, RENAME ROLE/USER pg_roles, CREATE/DROP ROLE, ALTER ROLE ... RENAME TO
GRANT ROLE r TO u, REVOKE ROLE r FROM u GRANT r TO u, REVOKE r FROM u

Steps:

  1. Parse the commands with regexes in _run_system, like CREATE DATABASE; the grammar is small and fixed. Run them only on the system database.
  2. Build SQL with psycopg.sql.Identifier for names. Passwords cannot be bind parameters in CREATE ROLE; use sql.Literal, and make sure the debug log at boltage/backend.py (log.debug("SQL: ...")) and _sql never log statements containing passwords (add a secret=True flag to _sql that redacts).
  3. CHANGE REQUIRED: PostgreSQL has no equivalent. Accept CHANGE NOT REQUIRED and reject CHANGE REQUIRED, or store a marker in a role setting (boltage.password_change_required) and enforce it at login by allowing only ALTER CURRENT USER SET PASSWORD (Neo4j's behaviour, signalled with Neo.ClientError.Security.CredentialsExpired). Start with rejecting.
  4. Suspension: NOLOGIN makes a suspended user indistinguishable from a role. Store boltage.suspended = 'true' as a role setting next to NOLOGIN so SHOW USERS still lists the user.
  5. Use the home database at login: after connect(), read current_setting('boltage.home_database', true) and use it instead of BOLTAGE_DEFAULT_GRAPH in resolve_db and SHOW HOME DATABASE.
  6. dbms.showCurrentUser and SHOW CURRENT USER report the real roles; report admin only for superusers / members of the admin role, so neo4j-browser does not offer admin actions that will fail.
  7. Errors: map PostgreSQL's 42501 (insufficient privilege) on these commands to Neo.ClientError.Security.Forbidden, and duplicate/missing roles to Neo4j's Neo.ClientError.General.InvalidArguments messages.

Privileges (GRANT ... ON GRAPH)

Neo4j's fine-grained privileges (TRAVERSE, READ {prop}, per label) do not map onto PostgreSQL. A coarse subset does, per graph schema:

Cypher SQL
GRANT ACCESS ON DATABASE g TO r GRANT USAGE ON SCHEMA g TO r
GRANT MATCH {*} ON GRAPH g TO r GRANT SELECT ON ALL TABLES IN SCHEMA g TO r (+ ALTER DEFAULT PRIVILEGES for new labels)
GRANT WRITE ON GRAPH g TO r INSERT, UPDATE, DELETE on all tables and USAGE on sequences in the schema
GRANT MATCH {*} ON GRAPH g NODES L TO r SELECT on g."L" only

Anything finer (property-level, TRAVERSE without READ, DENY) is rejected with FeatureNotSupported. Do this after the user commands, and only if wanted; the README's trigger section already explains granting SQL privileges directly.

neo4j-browser

The browser's :server user list / :server user add frames send these commands. Capture the exact queries (run boltage with BOLTAGE_LOG_LEVEL=DEBUG, or read the failure log lines) and make sure the shapes they use are covered.

Part 2: SHOW TRANSACTIONS and TERMINATE TRANSACTIONS

Approach

  1. Make PostgreSQL backends identifiable: set application_name to boltage <connection id> (boltage/backend.py:126), and remember each session's backend pid (SELECT pg_backend_pid() at connect).
  2. Keep a process-wide registry in boltage of live sessions: connection id, user, client address (peername), driver user_agent from HELLO, current graph, transaction start time, and the current Cypher text (not the generated SQL).
  3. SHOW TRANSACTIONS [ids] [YIELD ...] joins the registry with pg_stat_activity (by pid) for state, xact_start, query_start, wait events, and lists: database, transactionId, currentQueryId, connectionId, clientAddress, username, currentQuery, startTime, status, elapsedTime. Transaction ids look like Neo4j's: <database>-transaction-<n>, with n from a per-process counter incremented at each BEGIN / auto-commit query.
  4. Visibility: show only the user's own transactions unless they are a superuser or member of pg_read_all_stats, matching what pg_stat_activity allows.
  5. TERMINATE TRANSACTIONS 'id': pg_cancel_backend(pid) on the session's backend, which aborts the running statement and its transaction. Mark the Bolt session so its next message fails with Neo.TransientError.Transaction.Terminated. Only allowed for one's own transactions or with pg_signal_backend.
  6. Several boltage processes against one database (for example behind a load balancer) each know only their own registry. Fall back to pg_stat_activity alone for backends with application_name starting with boltage that are not in the local registry, extracting the Cypher text from between the dollar quotes of the cypher() call.

Also emulate the older CALL dbms.listTransactions() and CALL dbms.listQueries() with the same data if neo4j-browser or cypher-shell still send them.

Part 3: :sysinfo

  1. Find out what the frame queries. neo4j-browser's :sysinfo frame uses different queries depending on the server version it detects (boltage reports 5.26.0): typically SHOW DATABASES plus CALL dbms.queryJmx(...) for store sizes, id usage, page cache and transaction counts. Capture them from the failure log, or read SysInfoFrame in the neo4j-browser version pinned by NEO4J_BROWSER_REF.

  2. Emulate dbms.queryJmx for the beans the frame reads, from PostgreSQL:

    Sysinfo field Source
    store sizes pg_total_relation_size summed over the graph schema's tables; pg_database_size for the total
    node / relationship id counts row counts or reltuples of _ag_label_vertex / _ag_label_edge (already used by _counts)
    page cache hits / faults / hit ratio pg_statio_user_tables (heap_blks_hit, heap_blks_read) for the schema
    transactions (open, committed, rolled back, peak) pg_stat_database (xact_commit, xact_rollback) and pg_stat_activity
    version / edition the existing dbms.components values, plus PostgreSQL and AGE versions
  3. Fields that have no PostgreSQL equivalent return empty values rather than errors, so the frame still renders.

Tests

Smoke test, as the superuser of the test instance: create a user with a password, log in as that user, grant and revoke a role, SHOW USERS / SHOW ROLES, change the user's own password, drop the user. SHOW TRANSACTIONS from one session sees a transaction open in another, and TERMINATE TRANSACTIONS makes the other fail. Manual check of :sysinfo and :server user list in neo4j-browser.

README changes

Replace the bullet with a "Users and roles" section explaining that they are PostgreSQL roles, the supported subset of privileges, and what :sysinfo shows.