mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 12:47:04 +00:00
Application roles used the host administrator account.
user-administrator/tasks/00_core.yml:17 hashes its password into the OS
user, files/administrator grants that name ALL=(ALL) ALL without
NOPASSWD, and rotate.py lists it in ROTATION_EXEMPT, so the value is
password-authenticated sudo that cannot be rotated to recover. Every role
that typed it into a web form was typing the host sudo credential, and the
staged Playwright env carrying it is written mode 0644.
roles/user-administrator/meta/users.yml now declares platform_administrator
beside administrator: no host account, no rotation exemption, mailbox and
identity so it stays SSO-capable and can receive the mail applications send
to their admin. Every use moves to it, all fields and not only the
password: leaving .email behind built accounts from both identities at
once, with the host operator's address under the platform account's login
name, and an IdP matching on e-mail resolves such an account back to the
host operator.
The account is reachable by two spellings, and both are migrated:
lookup('users', 'administrator') in its two- and three-argument forms, and
the dotted lookup('config', application_id, 'users.administrator.email')
that five roles used. administrator is now referenced nowhere outside
roles/user-administrator.
Four lints hold the boundary:
- no role outside user-administrator may use the host administrator, in
either spelling or as a *user_key literal
- a role that looks a user up must declare it in its own meta/users.yml,
because required_user_policies only pins passwords for users declared
by a role resolved into the inventory, and those are the invokable
roles; a user declared only by a non-invokable role keeps the
{{ 42 | strong_password }} fallback, which re-renders per evaluation
- a role that iterates a user or passes it as a *user_key must declare it
- the biber and mapache test personas may be read only from the
Playwright and CLI test surfaces
Two credentials that were derived by hashing the admin password get their
own declaration, because deriving them tied an unrelated rotation to a
password change: web-app-fider jwt_secret, which signs session and e-mail
sign-in tokens, and web-app-nextcloud integration_suitecrm_client_secret,
whose two ends must be re-registered in lockstep.
web-app-keycloak separates the two admins it has: a new meta/users.yml
declares keycloak_bootstrap for the temporary account the container creates
on first start, while the permanent account is platform_administrator. Both
resolve in vars/main.yml so the lookup runs once per role.
web-app-semaphore's break-glass admin no longer shares ansible_become_password
and no longer forwards its mail to the host administrator; tasks/02_breakglass.yml
realigns the password on every deploy, so it converges on its own.
The six roles whose meta/secrets.yml declared administrator_password as its
only credential lose the file.
tests/integration/ansible/lookups/test_declared_users_resolve.py runs the
lookup through the real plugin loader and fails on a declared user that
cannot be resolved or whose username, e-mail or password renders empty. The
two declarations reading ansible_become_password resolve to an empty string
when that variable is absent, and nothing raises on it.
Existing deployments keep the admin account their application database
already holds, under the old name. Roles that re-apply the credential every
run converge; 30 roles feed it from config with no provisioning task and do
not, so those accounts need a reset. web-app-friendica additionally guards
on "'administrator' not in stdout", which the substring platform-administrator
satisfies, and web-app-n8n initialises an owner that already exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
25 lines
670 B
Python
25 lines
670 B
Python
"""Test-only persona accounts and the surfaces allowed to read them."""
|
|
|
|
from __future__ import annotations
|
|
|
|
TEST_PERSONA_USERS = ("biber", "mapache")
|
|
|
|
TEST_SURFACE_BASENAMES = ("playwright.env.j2", "test.env.j2")
|
|
|
|
|
|
def is_test_surface(rel_path: str) -> bool:
|
|
"""Whether a path may read a test persona.
|
|
|
|
Args:
|
|
rel_path: repository-relative POSIX path.
|
|
|
|
Returns:
|
|
True for the Playwright and CLI test surfaces.
|
|
"""
|
|
if rel_path.startswith(("tests/", "roles/test-")):
|
|
return True
|
|
parts = rel_path.split("/")
|
|
if "files" in parts and "playwright" in parts:
|
|
return True
|
|
return parts[-1] in TEST_SURFACE_BASENAMES
|