Skip to content

1.2 Step | CreateOdooUser

Omisen edited this page Aug 15, 2026 · 5 revisions

Creates the odoo system user (useradd --system), its dedicated group, and makes odoo:odoo the owner of the /opt/odoo home. Reversible. It lives in src/steps/create_odoo_user.rs. It follows the model of 1.1 PrepareOptRoot on a richer resource (user + group + ownership).

A port of create_odoo_user / _verify_odoo_user_homedir from lib/system.sh.


Life cycle

Phase Behaviour
snapshot detects three independent things: (1) does the user already exist? → Preexisting/Untracked; (2) the current owner of the home before our chown; (3) its mode, likewise. Then it applies the precondition described below
run Preexisting → skip useradd, no aggressive chown; absent → useradd, then CreatedByUs immediately, then chown odoo:odoo + chmod 0750. dry_run → log only
undo CreatedByUs only: userdel without -r + groupdel (best-effort) + restore the home's original owner and mode. Preexisting/Untracked → no-op

The order inside run is not cosmetic: the verdict is claimed the moment the user exists, not once the home is tidy. A chown or chmod that fails after useradd would otherwise leave a system user nobody will ever remove — the step is not in the completed list, so its undo would only run because the engine now undoes the failing step too, and it would find Untracked and do nothing.

Owner and mode, likewise: run sets 0750 on a home that may have been somebody else's with permissions of their choosing, and handing a directory back to its owner with permissions we picked is not handing it back. The mode used to be missing, and it was the model that found it — once the test double started remembering chmods, the end-to-end assertion “and that state is the virgin system” stopped holding.

useradd arguments (least privilege, as in Bash):

useradd --system --create-home --home-dir /opt/odoo --user-group --shell /bin/false odoo

--shell /bin/false → no interactive shell; --system → UID < 1000 with no password; --user-group → a dedicated odoo group.


Coordinating with PrepareOptRoot (the delicate point)

This is the first case of two steps both touching the same artifact (/opt/odoo). One creates it (PrepareOptRoot), the other becomes its owner (CreateOdooUser). The rule that unties the knot:

Every step owns the removal of what it created.

Concretely:

  • CreateOdooUser.undo runs userdel without -r: it does NOT remove the home. With -r it would delete /opt/odoo, which is PrepareOptRoot's business.
  • the home is removed by PrepareOptRoot.undo, which runs later in the reverse order (SetupLogDir → CreateOdooUser → PrepareOptRoot).
  • if the home was Preexisting (not ours) and our chown/chmod changed it, undo restores the original owner and mode saved at snapshot — so it does not stay owned by a user we are deleting, nor with permissions we chose.

Invariant from CLAUDE.md: never userdel -r on a Preexisting user. Here the undo acts only on CreatedByUs users, and even then without -r. A pre-existing user is never touched.

This shape — ownership of removal plus reverse order — is the model for future cases where two steps share an artifact (for instance the role that owns the database in PostgreSQL).


The testable SystemOps boundary

The privileged commands (useradd/userdel/groupdel/chown/chmod) are not called directly: they go through a SystemOps trait held inside the step. That way the Step trait and the engine do not change.

Impl Use
RealSystemOps production: useradd via Command, chown/mkdir via nix + std::fs
MockSystemOps (tests) records which operation would run and with which arguments, without root

The tests check the decision logic (the PreState branch, the exact useradd arguments, userdel without -r, owner restoration) without executing anything and without touching the system.


Persisted snapshot

struct CreateUserSnapshot {
    user_prestate: PreState,               // Preexisting | Untracked | CreatedByUs
    home_original_owner: Option<OwnerId>,  // the home's owner before our chown
}

Serialised (snapshot_value) and persisted, and read back by rehydrate: the rollback knows whether the user is ours and whom to restore the home to — even when it runs from disk months later.


Design notes

  • No aggression towards pre-existing things: if the user was already there, no useradd and no chown — it is not ours to reconfigure.
  • useradd does not re-chown a pre-existing home: the chown odoo:odoo is explicit, after creation (like _verify_odoo_user_homedir in Bash).
  • Every undo step is best-effort: a failure logs a warn and carries on, without blocking the other steps' cleanup.
  • Tests: CreatedByUs (useradd+chown / userdel without -r), Preexisting (never touched), owner restoration on a pre-existing home, dry_run (no operation).

Precondition: a pre-existing user with a home it cannot use

If the user already exists and /opt/odoo belongs to root without having been created by this installation, the installer stops here, before mutating.

The reason: this step does not chown (the directory is not ours), and three steps later SetupCacheDir would run sudo -u odoo mkdir -p /opt/odoo/.cache on a root-owned directory. The error used to be a Permission denied on a mkdir, naming neither the cause (the home belongs to root) nor the condition that makes it a problem (the user already exists, so nobody hands it over).

It is a precondition, not an undo: it is not a mutation to reverse, it is a mutation not to begin. The message names the home, its owner and the two ways out — chown if that directory is meant for Odoo, or remove it if it is a leftover.

The case where we created the home never reaches here: the handover already happened in PrepareOptRoot, the only place that knows who created it.

Clone this wiki locally