mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
fix(roles): realign the admin password on every deploy, not only at install
3c72c2002aand5366788ab1moved 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:
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"
|
||||
@@ -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
|
||||
@@ -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";
|
||||
@@ -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:
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 }}"
|
||||
Reference in new issue
Block a user