8.5 KiB
User and role management, SHOW TRANSACTIONS, :sysinfo
Problem
- Neo4j's user and role commands on the
systemdatabase (SHOW USERS,CREATE USER,GRANT ROLE, ...) are rejected by_run_system(NotSystemDatabaseCommand).dbms.showCurrentUseralways reports the roleadmin(boltage/backend.py:247). SHOW TRANSACTIONS/TERMINATE TRANSACTIONSare not emulated.- neo4j-browser's
:sysinfoframe 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:
- Parse the commands with regexes in
_run_system, likeCREATE DATABASE; the grammar is small and fixed. Run them only on thesystemdatabase. - Build SQL with
psycopg.sql.Identifierfor names. Passwords cannot be bind parameters inCREATE ROLE; usesql.Literal, and make sure the debug log atboltage/backend.py(log.debug("SQL: ...")) and_sqlnever log statements containing passwords (add asecret=Trueflag to_sqlthat redacts). CHANGE REQUIRED: PostgreSQL has no equivalent. AcceptCHANGE NOT REQUIREDand rejectCHANGE REQUIRED, or store a marker in a role setting (boltage.password_change_required) and enforce it at login by allowing onlyALTER CURRENT USER SET PASSWORD(Neo4j's behaviour, signalled withNeo.ClientError.Security.CredentialsExpired). Start with rejecting.- Suspension:
NOLOGINmakes a suspended user indistinguishable from a role. Storeboltage.suspended = 'true'as a role setting next toNOLOGINsoSHOW USERSstill lists the user. - Use the home database at login: after
connect(), readcurrent_setting('boltage.home_database', true)and use it instead ofBOLTAGE_DEFAULT_GRAPHinresolve_dbandSHOW HOME DATABASE. dbms.showCurrentUserandSHOW CURRENT USERreport the real roles; reportadminonly for superusers / members of the admin role, so neo4j-browser does not offer admin actions that will fail.- Errors: map PostgreSQL's
42501(insufficient privilege) on these commands toNeo.ClientError.Security.Forbidden, and duplicate/missing roles to Neo4j'sNeo.ClientError.General.InvalidArgumentsmessages.
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
- Make PostgreSQL backends identifiable: set
application_nametoboltage <connection id>(boltage/backend.py:126), and remember each session's backend pid (SELECT pg_backend_pid()at connect). - Keep a process-wide registry in boltage of live sessions: connection id, user,
client address (
peername), driveruser_agentfromHELLO, current graph, transaction start time, and the current Cypher text (not the generated SQL). SHOW TRANSACTIONS [ids] [YIELD ...]joins the registry withpg_stat_activity(by pid) forstate,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>, withnfrom a per-process counter incremented at eachBEGIN/ auto-commit query.- Visibility: show only the user's own transactions unless they are a superuser or
member of
pg_read_all_stats, matching whatpg_stat_activityallows. 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 withNeo.TransientError.Transaction.Terminated. Only allowed for one's own transactions or withpg_signal_backend.- Several boltage processes against one database (for example behind a load
balancer) each know only their own registry. Fall back to
pg_stat_activityalone for backends withapplication_namestarting withboltagethat are not in the local registry, extracting the Cypher text from between the dollar quotes of thecypher()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
-
Find out what the frame queries. neo4j-browser's
:sysinfoframe uses different queries depending on the server version it detects (boltage reports5.26.0): typicallySHOW DATABASESplusCALL dbms.queryJmx(...)for store sizes, id usage, page cache and transaction counts. Capture them from the failure log, or readSysInfoFramein the neo4j-browser version pinned byNEO4J_BROWSER_REF. -
Emulate
dbms.queryJmxfor the beans the frame reads, from PostgreSQL:Sysinfo field Source store sizes pg_total_relation_sizesummed over the graph schema's tables;pg_database_sizefor the totalnode / relationship id counts row counts or reltuplesof_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 schematransactions (open, committed, rolled back, peak) pg_stat_database(xact_commit,xact_rollback) andpg_stat_activityversion / edition the existing dbms.componentsvalues, plus PostgreSQL and AGE versions -
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.