Skip to content

1.7 Step | CreateDbRole

Omisen edited this page Aug 14, 2026 · 2 revisions

Creates the PostgreSQL role for Odoo (db_user), reversibly. It lives in src/steps/create_db_role.rs. A port of create_db_user from lib/postgres.sh.


Life cycle

Phase Behaviour
snapshot does the role already exist? (SELECT 1 FROM pg_roles WHERE rolname = ...) → Preexisting/Untracked
run Preexisting → skip. Absent → CREATE ROLE "<user>" WITH LOGIN CREATEDB [PASSWORD '…'], then CreatedByUs. dry_run → log only
undo CreatedByUs only: DROP ROLE IF EXISTS "<user>". Idempotent, best-effort

With a password (from the .env DB_PASSWORD) → ... PASSWORD '<escaped>'; without one → peer authentication, the safe local default where the OS user and the PG role share a name.


Coordinating with the database (reverse order)

DROP ROLE fails if the role owns objects — and the role owns the database created by 1.8 CreateDatabase. It is the same pattern as the home coordination in 1.2 CreateOdooUser: every step owns the removal of what it created, and the reverse order guarantees the right sequence.

production:  CreateDbRole → CreateDatabase
rollback:    undo CreateDatabase (drop DB) → undo CreateDbRole (drop role)

The database goes before the role that owns it. A test verifies this by running the engine with a shared log and asserting that DROP DATABASE precedes DROP ROLE.


Password safety (never in the logs)

The password arrives as a Secret (redacted Debug). The plaintext value is extracted only at the call site of the SystemOps boundary (pg_create_role), and it is never logged: the log carries at most with_password = true/false.

Inside the boundary, where all the risk is:

  • the identifier (role name) is validated as an identifier during resolution and double-quoted anyway;
  • the password is an SQL literal with single quotes doubled (escape_sql_literal);
  • the SQL travels through stdin, not in argv, so it does not show up in the process command line;
  • on error the stderr is suppressed: psql echoes the failing line, which would contain the password. We give up diagnostic detail rather than risk a leak.

The mock in the tests records only has_password: bool, never the value.


Design notes

  • No aggression towards pre-existing things: a role that was already there is neither created nor dropped.
  • Best-effort, idempotent undo (DROP ROLE IF EXISTS).
  • Tests: role absent (create/drop with the expected arguments), password containing a single quote (correct escaping, value not recorded), Preexisting role (never touched), pure escaping '''.

Clone this wiki locally