mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-12-08 18:35:11 +00:00
Implement reserved username handling for users, LDAP and Keycloak
Add end-to-end support for reserved usernames and tighten CAPTCHA / Keycloak logic.
Changes:
- Makefile: rename EXTRA_USERS → RESERVED_USERNAMES and pass it as --reserved-usernames to the users defaults generator.
- cli/build/defaults/users.py: propagate flag into generated users, add --reserved-usernames CLI option and mark listed accounts as reserved.
- Add reserved_users filter plugin with and helpers for Ansible templates and tasks.
- Add unit tests for reserved_users filters and the new reserved-usernames behaviour in the users defaults generator.
- group_vars/all/00_general.yml: harden RECAPTCHA_ENABLED / HCAPTCHA_ENABLED checks with default('') and explicit > 0 length checks.
- svc-db-openldap: introduce OPENLDAP_PROVISION_* flags, add OPENLDAP_PROVISION_RESERVED and OPERNLDAP_USERS to optionally exclude reserved users from provisioning.
- svc-db-openldap templates/tasks: switch role/group LDIF and user import loops to use OPERNLDAP_USERS instead of the full users dict.
- networks: assign dedicated subnet for web-app-roulette-wheel.
- web-app-keycloak vars: compute KEYCLOAK_RESERVED_USERNAMES_LIST and KEYCLOAK_RESERVED_USERNAMES_REGEX from users | reserved_usernames.
- web-app-keycloak user profile template: inject reserved-username regex into username validation pattern and improve error message, fix SSH public key attribute usage and add component name field.
- web-app-keycloak update/_update.yml: strip subComponents from component payloads before update and disable async/poll for easier debugging.
- web-app-keycloak tasks/main.yml: guard cleanup include with MODE_CLEANUP and keep reCAPTCHA update behind KEYCLOAK_RECAPTCHA_ENABLED.
- user/users defaults: mark system/service accounts (root, daemon, mail, admin, webmaster, etc.) as reserved so they cannot be chosen as login names.
- svc-prx-openresty vars: simplify OPENRESTY_CONTAINER lookup by dropping unused default parameter.
- sys-ctl-rpr-btrfs-balancer: simplify main.yml by removing the extra block wrapper.
- sys-daemon handlers: quote handler name for consistency.
Context: change set discussed and refined in ChatGPT on 2025-11-29 (Infinito.Nexus reserved usernames & Keycloak user profile flow). See conversation: https://chatgpt.com/share/692b21f5-5d98-800f-8e15-1ded49deddc9
This commit is contained in:
@@ -248,6 +248,97 @@ class TestGenerateUsers(unittest.TestCase):
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
def test_build_users_reserved_flag_propagated(self):
|
||||
"""
|
||||
Ensure that the 'reserved' flag from the definitions is copied
|
||||
into the final user entries, and is not added for non-reserved users.
|
||||
"""
|
||||
defs = {
|
||||
"admin": {"reserved": True},
|
||||
"bob": {},
|
||||
}
|
||||
|
||||
build = users.build_users(
|
||||
defs=defs,
|
||||
primary_domain="example.com",
|
||||
start_id=1001,
|
||||
become_pwd="pw",
|
||||
)
|
||||
|
||||
# Reserved user should carry the flag
|
||||
self.assertIn("reserved", build["admin"])
|
||||
self.assertTrue(build["admin"]["reserved"])
|
||||
|
||||
# Non-reserved user should not have the flag at all
|
||||
self.assertNotIn("reserved", build["bob"])
|
||||
|
||||
def test_cli_reserved_usernames_flag_sets_reserved_field(self):
|
||||
"""
|
||||
Verify that --reserved-usernames marks given usernames as reserved
|
||||
in the generated YAML, and that existing definitions are preserved
|
||||
(only 'reserved' is added).
|
||||
"""
|
||||
import tempfile
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
tmpdir = Path(tempfile.mkdtemp())
|
||||
try:
|
||||
roles_dir = tmpdir / "roles"
|
||||
roles_dir.mkdir()
|
||||
|
||||
# Role with an existing user definition "admin"
|
||||
(roles_dir / "role-base" / "users").mkdir(parents=True, exist_ok=True)
|
||||
with open(roles_dir / "role-base" / "users" / "main.yml", "w") as f:
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"users": {
|
||||
"admin": {
|
||||
"email": "admin@ex",
|
||||
"description": "Admin from role",
|
||||
}
|
||||
}
|
||||
},
|
||||
f,
|
||||
)
|
||||
|
||||
out_file = tmpdir / "users.yml"
|
||||
script_path = Path(__file__).resolve().parents[5] / "cli" / "build" / "defaults" / "users.py"
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"python3",
|
||||
str(script_path),
|
||||
"--roles-dir",
|
||||
str(roles_dir),
|
||||
"--output",
|
||||
str(out_file),
|
||||
"--reserved-usernames",
|
||||
"admin,service",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
||||
self.assertTrue(out_file.exists(), "Output file was not created.")
|
||||
|
||||
data = yaml.safe_load(out_file.read_text())
|
||||
self.assertIn("default_users", data)
|
||||
users_map = data["default_users"]
|
||||
|
||||
# "service" was created from the reserved list and must be reserved
|
||||
self.assertIn("service", users_map)
|
||||
self.assertTrue(users_map["service"].get("reserved", False))
|
||||
|
||||
# "admin" existed before; its fields must remain unchanged,
|
||||
# but it must now be marked as reserved
|
||||
self.assertIn("admin", users_map)
|
||||
self.assertEqual(users_map["admin"]["email"], "admin@ex")
|
||||
self.assertEqual(users_map["admin"]["description"], "Admin from role")
|
||||
self.assertTrue(users_map["admin"].get("reserved", False))
|
||||
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user