fix(roles): realign the admin password on every deploy, not only at install

3c72c2002a and 5366788ab1 moved application roles from the host account
administrator onto platform_administrator. administrator never rotates: it
declares ansible_become_password in roles/user-administrator/meta/users.yml, so
the generator pins nothing into host_vars, and rotate.py:12 exempts it anyway.
platform_administrator rotates with every --users pass, between PASS 1 and
PASS 2 of a matrix-deploy round. The roles below wrote the password only when
they created the account, so from the second pass on the instance kept the
previous value while the deploy rendered the new one, and the sign-in failed.

Each one now re-applies the declared password through its own handle. baserow
drops the 'if created' guard and compares with check_password. gitea and
mattermost gain a change-password call beside the create they already had.
gitlab realigns its native root break-glass through the Rails runner and clears
password_automatically_set, which would otherwise make valid_password? return
false whatever the password is. magento and snipe-it get a script because
neither ships a CLI for an existing account; magento's runs under AREA_GLOBAL so
the adminhtml-only admin_user_save_after observer never loads, at the price of
the admin_passwords history row. matomo moves its realign out from behind the
token gate a rotation never opens. The matrix ansible flavor gets the realign
its compose flavor already had. bluesky resets the native PDS account through
the admin API when the create reports the handle taken.

Every new path follows the contract web-app-nextcloud and sys-svc-rdbms already
use: [CHANGED] / [UNCHANGED] / [FAIL] on the wire, failed_when on rc or [FAIL],
and an absent account is a failure rather than a silent zero-row success. The
password reaches the container through container exec -e NAME plus a task
environment, never the host-side argv.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-10-12 21:21:57 +02:00
1 parent 9204f053dd
commit 23e642a3f3
18 files changed
+430 -15

No files matched your search

@@ -14,7 +14,8 @@ u, created = User.objects.get_or_create(
defaults={"email": email, "is_staff": True, "is_superuser": True},
)
if created:
password_written = created or not u.check_password(password)
if password_written:
u.set_password(password)
u.save()
@@ -29,4 +30,4 @@ if profile.completed_onboarding is not True:
if profile_updates:
profile.save(update_fields=profile_updates)
print("created" if created or profile_created else "exists")
print("created" if password_written or profile_created else "exists")
@@ -31,3 +31,31 @@
- bluesky_admin_create.status != 200
- "'Handle already taken' not in (bluesky_admin_create.content | default(''))"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔑 Realign the password an existing native PDS administrator kept"
when: bluesky_admin_create.status != 200
block:
- name: "🔎 Resolve the DID of the native PDS administrator handle"
uri_retry:
url: "{{ BLUESKY_PDS_INTERNAL_URL }}/xrpc/com.atproto.identity.resolveHandle?handle={{ BLUESKY_ADMIN_HANDLE | urlencode }}"
method: GET
status_code: 200
validate_certs: false
register: bluesky_admin_did
changed_when: false
- name: "🔑 Set the native administrator password through the admin API"
uri_retry:
url: "{{ BLUESKY_PDS_INTERNAL_URL }}/xrpc/com.atproto.admin.updateAccountPassword"
method: POST
headers:
Content-Type: application/json
Authorization: "Basic {{ ('admin:' ~ BLUESKY_ADMIN_PASSWORD) | b64encode }}"
body_format: json
body:
did: "{{ bluesky_admin_did.json.did }}"
password: "{{ BLUESKY_ADMIN_PASSWORD }}"
status_code: 200
validate_certs: false
changed_when: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
@@ -47,6 +47,20 @@
changed_when: "'has been successfully created' in create_admin.stdout"
failed_when: create_admin.rc != 0 and 'user already exists' not in create_admin.stderr
- name: "🔑 Realign the administrator password Gitea persisted at install"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
shell: | # nocheck: container-exec-resolver runtime-resolved CID, delegated to the hosting node
container exec -i -e GITEA_ADMIN_PASSWORD --user {{ GITEA_USER }} {{ GITEA_LOCAL_CID | quote }} \
/app/gitea/gitea admin user change-password \
--username "{{ lookup('users', 'platform_administrator').username }}" \
--password "$GITEA_ADMIN_PASSWORD" \
--must-change-password=false \
-c {{ GITEA_CONFIG }}
environment:
GITEA_ADMIN_PASSWORD: "{{ lookup('users', 'platform_administrator').password }}"
delegate_to: "{{ GITEA_HOST_NODE }}"
changed_when: true
- name: "⏳ Wait until Gitea setup and migrations are ready"
uri_retry:
url: "http://{{ DOCKER_REACH_HOST }}:{{ lookup('config', application_id, 'services.gitea.ports.local.http') }}/api/v1/version"
@@ -0,0 +1,28 @@
# nocheck: mirrored-unit-test - runs in `gitlab-rails runner` against the User model,
# which comes from the GitLab image and is not loadable anywhere else
#
# Env (from the container):
# GITLAB_ROOT_USERNAME login of the built-in break-glass account
# GITLAB_ROOT_PASSWORD password it must accept afterwards
#
# Exit: 0 with [CHANGED] or [UNCHANGED] on stdout, 1 with [FAIL] on stderr.
#
# password_automatically_set makes User#valid_password? return false whatever the
# password is (app/models/user.rb), so a realign that leaves it set locks the
# account out instead of recovering it.
username = ENV.fetch('GITLAB_ROOT_USERNAME')
password = ENV.fetch('GITLAB_ROOT_PASSWORD')
user = User.find_by_username(username)
abort("[FAIL] no GitLab user named '#{username}'") if user.nil?
if user.valid_password?(password) && !user.password_automatically_set?
puts "[UNCHANGED] #{username} already accepts the declared password"
exit(0)
end
user.password = user.password_confirmation = password
user.password_automatically_set = false
user.save!
puts "[CHANGED] #{username} was realigned with the declared password"
+3
View File
@@ -30,6 +30,9 @@
- name: "⏳ Wait for GitLab services"
include_tasks: 02_wait.yml
- name: "🔑 Realign the native root account with the declared password"
include_tasks: utils/root_password.yml
- name: "🔌 Provision the MCP server surface"
include_tasks: utils/mcp.yml
when: GITLAB_MCP_ENABLED | bool
@@ -0,0 +1,34 @@
---
- name: "🔍 Resolve the node and container id running the GitLab Rails app"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}"
vars:
resolve_service_key: "{{ GITLAB_WEBSERVICE_SERVICE_NAME }}"
resolve_output_prefix: GITLAB_RAILS
resolve_compose_cid: "{{ lookup('container_address', application_id, GITLAB_WEBSERVICE_SERVICE_NAME) }}"
- name: "🔑 Realign the root password GitLab seeded on the empty database"
ansible.builtin.command:
argv:
- container
- exec
- -i
- -e
- GITLAB_ROOT_USERNAME
- -e
- GITLAB_ROOT_PASSWORD
- "{{ GITLAB_RAILS_LOCAL_CID }}" # nocheck: container-exec-resolver
- "{{ GITLAB_RAILS_BIN }}"
- runner
- "-"
stdin: "{{ lookup('file', 'ruby/realign_root_password.rb') }}"
environment:
GITLAB_ROOT_USERNAME: "{{ GITLAB_ROOT_USERNAME }}"
GITLAB_ROOT_PASSWORD: "{{ GITLAB_INIT_ROOT_PASSWORD }}"
register: gitlab_root_realign
changed_when: gitlab_root_realign.stdout is search('\[CHANGED\]')
failed_when: >-
gitlab_root_realign.rc != 0
or gitlab_root_realign.stdout is search('\[FAIL\]')
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
delegate_to: "{{ GITLAB_RAILS_HOST_NODE }}"
run_once: true
+1
View File
@@ -29,6 +29,7 @@ GITLAB_OIDC_CLIENT_SECRET: "{{ OIDC.CLIENT.SECRET }}"
GITLAB_OIDC_ISSUER: "{{ OIDC.CLIENT.ISSUER_URL }}"
GITLAB_OIDC_REDIRECT_URI: "{{ (GITLAB_URL | regex_replace('/$', '')) ~ '/users/auth/openid_connect/callback' }}"
GITLAB_ROOT_USERNAME: "root"
GITLAB_INIT_ROOT_PASSWORD: "{{ lookup('users', 'platform_administrator').password }}"
GITLAB_MCP_ENABLED: "{{ lookup('config', application_id, 'services.gitlabmcp.enabled') | bool }}"
@@ -0,0 +1,79 @@
<?php
// nocheck: mirrored-unit-test - boots Magento through app/bootstrap.php and rewrites the
// admin_user row through Magento's own encryptor; running it against a live install is the
// whole behaviour
/**
* Realign the Magento administrator password with the value the deployment renders.
*
* `password` takes the plaintext: `User::beforeSave()` replaces it with
* `EncryptorInterface::getHash($password, true)`, so pre-hashing it here would store a hash
* of a hash and lock the administrator out.
*
* Every failure is caught and reduced to its message: PHP's uncaught-exception trace carries
* the call arguments unless `zend.exception_ignore_args` is on, and the official PHP images
* activate no php.ini, so an uncaught throw would put the password on stderr.
*
* Env, passed with `container exec -e <name>` so it never reaches the host-side argv:
* MAGENTO_REALIGN_ROOT Magento base directory, the one holding app/bootstrap.php
* MAGENTO_REALIGN_USERNAME administrator login to realign
* MAGENTO_REALIGN_PASSWORD password that login must accept afterwards
*
* Exit: 0 with [CHANGED] or [UNCHANGED] on stdout, 1 with [FAIL] on stderr.
*/
declare(strict_types=1);
use Magento\Framework\App\Area;
use Magento\Framework\App\Bootstrap;
use Magento\Framework\App\State;
use Magento\Framework\Encryption\EncryptorInterface;
use Magento\User\Model\UserFactory;
function required(string $name): string
{
$value = getenv($name);
if ($value === false || $value === '') {
fwrite(STDERR, "[FAIL] $name is empty\n");
exit(1);
}
return $value;
}
$root = required('MAGENTO_REALIGN_ROOT');
$username = required('MAGENTO_REALIGN_USERNAME');
$password = required('MAGENTO_REALIGN_PASSWORD');
require_once $root . '/app/bootstrap.php';
try {
$objects = Bootstrap::create(BP, $_SERVER)->getObjectManager();
// Exception: AREA_ADMINHTML would register admin_user_save_after, whose observer
// constructs Backend\Model\Auth\Session and starts a PHP session the CLI has no
// request for. The cost of AREA_GLOBAL is the admin_passwords history row.
$objects->get(State::class)->setAreaCode(Area::AREA_GLOBAL);
$user = $objects->get(UserFactory::class)->create()->loadByUsername($username);
if (!$user->getId()) {
fwrite(STDERR, "[FAIL] no Magento administrator named '$username'\n");
exit(1);
}
if ($objects->get(EncryptorInterface::class)->isValidHash($password, (string) $user->getPassword())) {
echo "[UNCHANGED] the Magento administrator already accepts the declared password\n";
exit(0);
}
$user->setPassword($password)->save();
$written = $objects->get(UserFactory::class)->create()->loadByUsername($username);
if (!$objects->get(EncryptorInterface::class)->isValidHash($password, (string) $written->getPassword())) {
fwrite(STDERR, "[FAIL] the realigned password is not the one '$username' now accepts\n");
exit(1);
}
} catch (Throwable $error) {
fwrite(STDERR, '[FAIL] ' . $error->getMessage() . "\n");
exit(1);
}
echo "[CHANGED] the Magento administrator was realigned with the declared password\n";
+18
View File
@@ -81,6 +81,24 @@
register: magento_db_password_realign
changed_when: "'You saved the new configuration' in magento_db_password_realign.stdout"
- name: "🔑 Realign the Magento administrator password with the declared one"
ansible.builtin.shell:
cmd: >-
container exec -i --user {{ MAGENTO_USER }}
-e MAGENTO_REALIGN_ROOT -e MAGENTO_REALIGN_USERNAME -e MAGENTO_REALIGN_PASSWORD
{{ MAGENTO_PHP_CONTAINER_ADDRESS }} php
stdin: "{{ lookup('file', 'php/realign_admin_password.php') }}"
environment:
MAGENTO_REALIGN_ROOT: "{{ MAGENTO_ROOT }}"
MAGENTO_REALIGN_USERNAME: "{{ MAGENTO_ADMIN_USERNAME }}"
MAGENTO_REALIGN_PASSWORD: "{{ MAGENTO_ADMIN_PASSWORD }}"
register: magento_admin_password_realign
changed_when: magento_admin_password_realign.stdout is search('\[CHANGED\]')
failed_when: >-
magento_admin_password_realign.rc != 0
or magento_admin_password_realign.stdout is search('\[FAIL\]')
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🪣 Wire Magento to its S3 remote storage"
when: MAGENTO_OBJSTORE_ENABLED | bool
block:
+2 -2
View File
@@ -6,8 +6,8 @@ MAGENTO_USE_SECURE={{ lookup('tls', application_id, 'enabled') | bool | ternary(
MAGENTO_BASE_URL_SECURE={{ lookup('tls', application_id, 'enabled') | bool | ternary('1','0') }}
MAGENTO_USE_SECURE_ADMIN={{ lookup('tls', application_id, 'enabled') | bool | ternary('1','0') }}
MAGENTO_ADMIN_USERNAME={{ lookup('users', 'platform_administrator').username }}
MAGENTO_ADMIN_PASSWORD={{ lookup('users', 'platform_administrator').password | dotenv_quote }}
MAGENTO_ADMIN_USERNAME={{ MAGENTO_ADMIN_USERNAME }}
MAGENTO_ADMIN_PASSWORD={{ MAGENTO_ADMIN_PASSWORD | dotenv_quote }}
MAGENTO_ADMIN_EMAIL={{ lookup('users', 'platform_administrator').email }}
MAGENTO_ADMIN_FIRSTNAME={{ lookup('users', 'platform_administrator').firstname }}
MAGENTO_ADMIN_LASTNAME={{ lookup('users', 'platform_administrator').lastname }}
+3
View File
@@ -9,6 +9,9 @@ MAGENTO_INSTALL_MARKER: "{{ [ magento_volume_mountpoint.stdout | trim, 'html/var
MAGENTO_DOMAIN: "{{ lookup('domain',application_id) }}"
MAGENTO_URL: "{{ lookup('tls', application_id, 'url.base') }}"
MAGENTO_ADMIN_USERNAME: "{{ lookup('users', 'platform_administrator').username }}"
MAGENTO_ADMIN_PASSWORD: "{{ lookup('users', 'platform_administrator').password }}"
MAGENTO_PHP_HOST: "php"
MAGENTO_PHP_PORT: "{{ lookup('config', application_id, 'services.php.ports.internal.fpm') }}"
MAGENTO_NGINX_PORT: "{{ lookup('config', application_id, 'services.magento.ports.internal.http') }}"
@@ -0,0 +1,70 @@
<?php
// nocheck: mirrored-unit-test - rewrites the live Matomo user table over a
// connection it opens itself; running it is the whole behaviour
/**
* Realign the Matomo superuser password with the value the deployment renders.
*
* Matomo stores `password_hash(md5($plain))`, not a hash of the plaintext
* (plugins/UsersManager/API.php via core/Auth/Password.php), and PASSWORD_DEFAULT
* is safe against a differing `password_hash_algorithm` because
* `password_verify` accepts every algorithm Matomo can be configured with.
*
* Env (from the container):
* MATOMO_CONFIG path of config.ini.php, for the table prefix
* MATOMO_DATABASE_HOST host or host:port
* MATOMO_DATABASE_DBNAME database holding the user table
* MATOMO_DATABASE_USERNAME app user
* MATOMO_DATABASE_PASSWORD app password
* MATOMO_SUPERUSER_LOGIN login to realign
* MATOMO_SUPERUSER_PASSWORD password it must accept afterwards
*
* Exit: 0 with [CHANGED] or [UNCHANGED] on stdout, 1 with [FAIL] on stderr.
*/
function required(string $name): string
{
$value = getenv($name);
if ($value === false || $value === '') {
fwrite(STDERR, "[FAIL] $name is empty\n");
exit(1);
}
return $value;
}
$configPath = required('MATOMO_CONFIG');
$login = required('MATOMO_SUPERUSER_LOGIN');
$password = required('MATOMO_SUPERUSER_PASSWORD');
$config = parse_ini_file($configPath, true);
if ($config === false || !isset($config['database']['tables_prefix'])) {
fwrite(STDERR, "[FAIL] $configPath carries no database.tables_prefix\n");
exit(1);
}
$prefix = $config['database']['tables_prefix'];
[$host, $port] = array_pad(explode(':', required('MATOMO_DATABASE_HOST')), 2, 3306);
$pdo = new PDO(
"mysql:host=$host;port=$port;dbname=" . required('MATOMO_DATABASE_DBNAME'),
required('MATOMO_DATABASE_USERNAME'),
required('MATOMO_DATABASE_PASSWORD'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$read = $pdo->prepare("SELECT password FROM `{$prefix}user` WHERE login = ?");
$read->execute([$login]);
$stored = $read->fetchColumn();
if ($stored === false) {
fwrite(STDERR, "[FAIL] no Matomo user named '$login'\n");
exit(1);
}
if (password_verify(md5($password), $stored)) {
echo "[UNCHANGED] the Matomo superuser already accepts the declared password\n";
exit(0);
}
$write = $pdo->prepare(
"UPDATE `{$prefix}user` SET password = ?, ts_password_modified = UTC_TIMESTAMP() WHERE login = ?"
);
$write->execute([password_hash(md5($password), PASSWORD_DEFAULT), $login]);
echo "[CHANGED] the Matomo superuser was realigned with the declared password\n";
@@ -7,6 +7,13 @@
changed_when: false
delegate_to: "{{ MATOMO_HOST_NODE }}"
- name: "🔑 Matomo install | Realign the superuser of an existing install"
include_tasks:
file: utils/converge_superuser.yml
apply:
delegate_to: "{{ MATOMO_HOST_NODE }}"
when: (matomo_install_state.stdout | trim) == 'installed'
- name: "🥾 Bootstrap Matomo (Initialisation and setting of API token if not defined)"
include_tasks: 04_bootstrap.yml
when: >
@@ -0,0 +1,16 @@
---
- name: "🔑 Realign the administrator password Matomo persisted at install"
ansible.builtin.shell: # nocheck: container-exec-resolver MATOMO_LOCAL_CID resolved at delegate_to runtime; container_address would return a manager-only subshell
cmd: >
container exec -i --user root
-e MATOMO_CONFIG -e MATOMO_SUPERUSER_LOGIN -e MATOMO_SUPERUSER_PASSWORD
{{ MATOMO_LOCAL_CID | quote }} php
stdin: "{{ lookup('file', 'php/converge_superuser_password.php') }}"
environment:
MATOMO_CONFIG: "{{ MATOMO_CONFIG }}"
MATOMO_SUPERUSER_LOGIN: "{{ MATOMO_ADMIN_USER_NAME }}"
MATOMO_SUPERUSER_PASSWORD: "{{ MATOMO_ADMIN_PASSWORD }}"
register: matomo_superuser_realign
changed_when: matomo_superuser_realign.stdout is search('\[CHANGED\]')
failed_when: matomo_superuser_realign.rc != 0 or matomo_superuser_realign.stdout is search('\[FAIL\]')
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
@@ -84,18 +84,51 @@
delay: 2
until: matrix_element_loopback_probe.status == 200
- name: "📁 ansible-flavor: create native Synapse admin (DiD, idempotent shared-secret)"
- name: "🔑 ansible-flavor: converge the native Synapse admin account"
when: IS_STACK_HOST | bool
ansible.builtin.shell:
cmd: >- # nocheck: raw-docker, container-exec-resolver, container-address-quote inner docker CLI talks to the DiD daemon inside matrix-mdad; no host docker involved; ansible flavor forces compose mode
container exec {{ MATRIX_MDAD_CONTAINER_ADDRESS | quote }} docker exec matrix-synapse register_new_matrix_user
-u {{ MATRIX_ADMINISTRATOR_USERNAME | quote }}
-p {{ MATRIX_ADMINISTRATOR_PASSWORD | quote }}
-a -c {{ lookup('volume', 'web-app-matrix', 'synapse_config').target }} http://localhost:8008
register: matrix_admin_create
changed_when: matrix_admin_create.rc == 0 and 'User ID already taken' not in (matrix_admin_create.stdout + matrix_admin_create.stderr)
failed_when: matrix_admin_create.rc != 0 and 'User ID already taken' not in (matrix_admin_create.stdout + matrix_admin_create.stderr)
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
block:
- name: "📁 ansible-flavor: create native Synapse admin (DiD, idempotent shared-secret)"
ansible.builtin.shell:
cmd: >- # nocheck: raw-docker, container-exec-resolver, container-address-quote inner docker CLI talks to the DiD daemon inside matrix-mdad; no host docker involved; ansible flavor forces compose mode
container exec {{ MATRIX_MDAD_CONTAINER_ADDRESS | quote }} docker exec matrix-synapse register_new_matrix_user
-u {{ MATRIX_ADMINISTRATOR_USERNAME | quote }}
-p {{ MATRIX_ADMINISTRATOR_PASSWORD | quote }}
-a -c {{ lookup('volume', 'web-app-matrix', 'synapse_config').target }} http://localhost:8008
register: matrix_admin_create
changed_when: matrix_admin_create.rc == 0 and 'User ID already taken' not in (matrix_admin_create.stdout + matrix_admin_create.stderr)
failed_when: matrix_admin_create.rc != 0 and 'User ID already taken' not in (matrix_admin_create.stdout + matrix_admin_create.stderr)
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔑 ansible-flavor: realign the admin password Synapse stored"
when: not matrix_admin_create.changed
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
block:
- name: "🔑 ansible-flavor: hash the declared admin password"
ansible.builtin.shell:
cmd: >- # nocheck: raw-docker, container-exec-resolver inner docker CLI talks to the DiD daemon inside matrix-mdad; no host docker involved
container exec -i {{ MATRIX_MDAD_CONTAINER_ADDRESS }} docker exec -i matrix-synapse
hash_password -c {{ MATRIX_SYNAPSE_CONFIG_PATH_CONTAINER | quote }}
stdin: "{{ MATRIX_ADMINISTRATOR_PASSWORD }}"
executable: /bin/bash
register: matrix_mdad_password_hash
changed_when: false
- name: "🔑 ansible-flavor: point the admin at the declared password"
ansible.builtin.shell:
cmd: >-
container exec -i -e PGPASSWORD
{{ lookup('database', application_id, 'address') }}
psql -v ON_ERROR_STOP=1 -U {{ MATRIX_POSTGRES_USER | quote }}
-d {{ MATRIX_POSTGRES_DATABASE | quote }}
-v hash={{ (matrix_mdad_password_hash.stdout_lines | last | trim) | quote }}
-v name={{ lookup('mxid', MATRIX_ADMINISTRATOR_USERNAME) | quote }}
-f -
stdin: "{{ lookup('file', 'sql/realign_user_password.sql') }}"
executable: /bin/bash
environment:
PGPASSWORD: "{{ MATRIX_POSTGRES_PASSWORD }}"
register: matrix_mdad_password_update
changed_when: "'UPDATE 1' in matrix_mdad_password_update.stdout"
- name: "🔀 front-proxy: Element web client"
ansible.builtin.include_role:
@@ -34,6 +34,19 @@
failed_when: mm_create_admin.rc != 0 and 'already exists' not in (mm_create_admin.stdout + mm_create_admin.stderr)
delegate_to: "{{ MATTERMOST_HOST_NODE }}"
- name: "🔑 Realign the administrator password Mattermost persisted at install"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
ansible.builtin.shell: | # nocheck: container-exec-resolver MATTERMOST_LOCAL_CID resolved at delegate_to runtime; container_address would return a manager-only subshell
container exec -i -e MATTERMOST_ADMIN_PASSWORD {{ MATTERMOST_LOCAL_CID | quote }} \
{{ MATTERMOST_MMCTL }} --local user change-password \
"{{ lookup('users', 'platform_administrator').username }}" \
--password "$MATTERMOST_ADMIN_PASSWORD"
environment:
MATTERMOST_ADMIN_PASSWORD: "{{ lookup('users', 'platform_administrator').password }}"
register: mm_realign_admin
changed_when: mm_realign_admin.rc == 0
delegate_to: "{{ MATTERMOST_HOST_NODE }}"
- name: "📁 Mattermost | Create default team"
ansible.builtin.shell: | # nocheck: container-exec-resolver MATTERMOST_LOCAL_CID resolved at delegate_to runtime; container_address would return a manager-only subshell
container exec -i {{ MATTERMOST_LOCAL_CID | quote }} \
@@ -0,0 +1,50 @@
<?php
// nocheck: mirrored-unit-test - rewrites the live Snipe-IT user row through the
// application's own bootstrap; running it is the whole behaviour
/**
* Realign the Snipe-IT administrator password with the value the deployment renders.
*
* Env (from the container):
* SNIPE_IT_REALIGN_USERNAME login to realign
* SNIPE_IT_REALIGN_PASSWORD password it must accept afterwards
*
* Exit: 0 with [CHANGED] or [UNCHANGED] on stdout, 1 with [FAIL] on stderr.
*/
function required(string $name): string
{
$value = getenv($name);
if ($value === false || $value === '') {
fwrite(STDERR, "[FAIL] $name is empty\n");
exit(1);
}
return $value;
}
$username = required('SNIPE_IT_REALIGN_USERNAME');
$password = required('SNIPE_IT_REALIGN_PASSWORD');
try {
require 'vendor/autoload.php';
$app = require 'bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$user = \App\Models\User::where('username', $username)->whereNull('deleted_at')->first();
if (!$user) {
fwrite(STDERR, "[FAIL] no Snipe-IT user named '$username'\n");
exit(1);
}
if (\Illuminate\Support\Facades\Hash::check($password, $user->password)) {
echo "[UNCHANGED] the Snipe-IT administrator already accepts the declared password\n";
exit(0);
}
$user->password = \Illuminate\Support\Facades\Hash::make($password);
$user->save();
echo "[CHANGED] the Snipe-IT administrator was realigned with the declared password\n";
} catch (Throwable $error) {
// Exception: an uncaught throw prints call arguments unless
// zend.exception_ignore_args is set, which would put the password on stderr.
fwrite(STDERR, '[FAIL] ' . $error->getMessage() . "\n");
exit(1);
}
+17
View File
@@ -39,3 +39,20 @@
and 'users_groups' in (snipe_it_admin_create.stderr | default(''))
)
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🔑 Realign the administrator password Snipe-IT persisted at install"
ansible.builtin.shell:
cmd: >
container exec -i
-e APP_KEY -e SNIPE_IT_REALIGN_USERNAME -e SNIPE_IT_REALIGN_PASSWORD
{{ SNIPE_IT_CONTAINER_ADDRESS }} php
stdin: "{{ lookup('file', 'php/realign_admin_password.php') }}"
environment:
APP_KEY: "{{ SNIPE_IT_APP_KEY }}"
SNIPE_IT_REALIGN_USERNAME: "{{ lookup('users', 'platform_administrator').username }}"
SNIPE_IT_REALIGN_PASSWORD: "{{ lookup('users', 'platform_administrator').password }}"
when: snipe_it_admin_exists.rc == 0
register: snipe_it_admin_realign
changed_when: snipe_it_admin_realign.stdout is search('\[CHANGED\]')
failed_when: snipe_it_admin_realign.rc != 0 or snipe_it_admin_realign.stderr is search('\[FAIL\]')
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"