module

Krikri::PluginHelpers::UserState

UserState - pure logic for parsing getent passwd output and deciding what (if anything) needs to change to reconcile a user account with its desired state. No I/O here: the plugin itself calls getent/useradd/etc and hands the results in as plain strings.

Class methods

chage_flags(current : ShadowAgeing, min : String | Nil, max : String | Nil, warn : String | Nil) : Array(String)

chage -m <min> -M <max> -W <warn> <name> flags needed to reconcile the account's current password-ageing fields with the desired ones - only ever emitted for a param that was actually given (non-nil) and differs from the current value, same changed_flag convention every other *_flags helper here uses.

Source
expires_changed?(timestamp : Int64, current_days : Int32 | Nil) : Bool

Real Ansible's own usermod-path idempotency check compares /etc/shadow's own expire field (whole days since epoch) against the requested timestamp's own day-since-epoch, NOT a full- precision timestamp comparison - a expires: value that maps to the SAME calendar day as what's already set is treated as unchanged, matching int(math.floor(expires)) // 86400 against current_expires (itself already whole days from /etc/shadow).

Source
expires_date(timestamp : Int64) : String

expires:'s own Unix-timestamp-to-useradd/usermod--e-value conversion - verified against real ansible/modules/user.py's own source: time.gmtime(timestamp) then strftime('%Y-%m-%d', ...) (UTC, matching time.gmtime's own UTC-not-local semantics) for a non-negative timestamp; a NEGATIVE timestamp (real Ansible's own documented "-1 to remove" convention) maps to the empty string, useradd/usermod -e '' being how those commands themselves clear an existing expiration date.

Source
local_expiry_days(timestamp : Int64) : Int64

local: true's expires conversion - unlike the normal path's useradd/usermod -e YYYY-MM-DD, libuser's lchage takes whole DAYS since epoch (-E), real Ansible's own int(floor(expires)) // 86400 (or -1, lchage's clear-value, for a negative timestamp). Live-verified: the real module emits lchage -E 21915 for expires: 1893456000.

Source
normalize_groups_value(raw : String) : String

groups: "{{ list_var }}" (a full-value bare-variable substitution of a real multi-item list, as opposed to a literal YAML groups: list - already comma-joined by the parser before this ever runs) arrives as the double-quoted JSON the wire serialized it to (see substitute_task_params's whole-single-span comment) - parse it back into real elements to comma-join for useradd -G. Previously the whole bracketed string went into useradd -G as ONE malformed group-list argument and useradd failed "group ... does not exist" for the bracket-stuck pieces. Found via kostiantyn-nemchenko.mongodb_exporter's own groups: "{{ mongodb_exporter_system_groups }}" on user:'s create (useradd) path.

ONLY valid JSON, though - never a Python-repr repair pass: a value that merely LOOKS like a container (a literal groups: "['a']" string, or a {% if %}...{% else %}['a']{% endif %} block's rendered output) is a plain STRING in real ansible-core - native typing requires the template's whole AST to be one output node wrapping one expression, so block-tag output is never re-parsed (live-verified vs ansible-playbook 2.19.11, see apt.cr's parse_package_names). The repr-looking string passes through unchanged and useradd -G fails on it exactly like real Ansible's comma-split garbage does.

Source
parse(line : String) : User | Nil

Parses a single getent passwd <name> line: "name:password:uid:gid:comment:home:shell"

Source
password_matches?(current_hash : String, desired_password : String) : Bool

Real Ansible compares password hashes with a leading ! (its own lock-marker prefix) stripped from both sides before comparing - verified against its own info[1].lstrip('!') != self.password.lstrip('!')

  • so locking/unlocking alone never looks like a password change.
Source
password_update_flags(current_hash : String | Nil, desired_password : String | Nil, update_password : String, locked : Bool | Nil) : Array(String)

usermod flags to reconcile an existing account's password/lock state - verified against real Ansible's own modify_user_usermod logic (including the real, easy-to-miss detail that a queued -L/ -U gets folded into -p '!hash'/-p hash instead, rather than coexisting with it, whenever both a password update and a lock state are requested together - -p and -L/-U are mutually exclusive usermod flags).

  • update_password: "on_create" (real Ansible's other allowed value, vs. the default "always") means an existing account's password is never touched here at all, only at creation time - this codebase's own mysql_user.cr already documents the same simplification for MySQL: unlike real Ansible, this can't compare a candidate cleartext password to a stored hash (the caller is always expected to pass an already-hashed value, same as real Ansible itself requires), so "unchanged" here means "the given hash already matches what's stored," not "the account's password is already this."
Source
shadow_ageing(shadow_content : String, name : String) : ShadowAgeing
Source
shadow_expire_days(shadow_content : String, name : String) : Int32 | Nil

/etc/shadow's own account-expiration field (the 8th colon-separated field, days since epoch) - nil if the account has no expiration set or no shadow entry at all.

Source
shadow_password(shadow_content : String, name : String) : String | Nil

Extracts the /etc/shadow password-hash field (the 2nd colon-separated field) for name from a real /etc/shadow's full content - verified against real Ansible's own parse_shadow_file (the fallback it uses when Python's spwd module isn't available, which is the only shadow-reading strategy this codebase replicates - no getspnam-equivalent libc binding is needed just to read a field this codebase can already get by shelling cat /etc/shadow, the same remote_exec local/remote split every other plugin already uses). nil if the account has no shadow entry at all (matches real Ansible's own empty-string default becoming an empty comparison target).

Source
useradd_args(name : String, uid : String | Nil, gid : String | Nil, groups : String | Nil, shell : String | Nil, home : String | Nil, comment : String | Nil, system : Bool, create_home : Bool, non_unique : Bool = false, skeleton : String | Nil = nil, umask : String | Nil = nil, inactive : String | Nil = nil, local : Bool = false) : Array(String)

useradd argument list for a brand new account. Desired values that are nil are simply omitted, letting useradd apply its own defaults.

Source
useradd_password_args(password : String | Nil, locked : Bool | Nil) : Array(String)

useradd -p <hash> (or -p '!<hash>' when password_lock: true - real Ansible's own convention for "set this password, but start the account locked").

Source
userdel_args(name : String, remove_home : Bool) : Array(String)
Source
usermod_flags(current : User, uid : String | Nil, gid : String | Nil, shell : String | Nil, home : String | Nil, comment : String | Nil, non_unique : Bool = false, move_home : Bool = false, inactive : String | Nil = nil) : Array(String)

usermod flags needed to reconcile an existing account with the desired attributes. Only attributes that were actually requested (non-nil) and differ from the current value produce a flag.

Source

Nested types