Files
computer-playbook/cli/administration/inventory/provision/passwords.py
kevinveenbirkenbachandClaude Fable 5.1 eab8f5fc90 fix(inventory): give pinned user passwords the mark the realm policy demands
The shell-safe default drew from letters and digits only, and every user without declared accounts is an identity account, so kcadm set-password rejected hermes and openclaw with invalidPasswordMinSpecialCharsMessage as soon as keycloak entered the GitLab#1 round without LDAP (4164f37981) and the extras file stopped replacing the users map (03e42bae6d; run 33689701895). The generator now draws from letters, digits and -_. and guarantees upper case, lower case, a digit and one mark, never in the leading position.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 02:15:53 +02:00

70 lines
2.6 KiB
Python

from __future__ import annotations
import re
import secrets
import string
from utils.manager.value_generator import ValueGenerator
USER_PASSWORD_LENGTH = 64
DECLARED_PASSWORD_ATTEMPTS = 64
SHELL_SAFE_MARKS = "-_."
_USER_PASSWORD_ALPHABET = string.ascii_letters + string.digits + SHELL_SAFE_MARKS
_USER_PASSWORD_RE = re.compile(
r"^[A-Za-z0-9](?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[-_.])[A-Za-z0-9._-]{15,}$"
)
def generate_random_password(length: int = 64) -> str:
return ValueGenerator().generate_strong_password(length)
def generate_user_password(length: int = USER_PASSWORD_LENGTH) -> str:
"""Return a shell-safe password for a declared user.
Args:
length: characters to generate, at least 16.
Returns:
letters, digits and the marks ``-_.``, never a leading ``-``, with one
of each class the Keycloak realm policy ``specialChars(1)`` counts.
"""
if length < 16:
raise ValueError("A user password must be at least 16 characters")
for _ in range(DECLARED_PASSWORD_ATTEMPTS):
body = [secrets.choice(_USER_PASSWORD_ALPHABET) for _ in range(length)]
body[0] = secrets.choice(string.ascii_letters + string.digits)
body[secrets.randbelow(length - 1) + 1] = secrets.choice(SHELL_SAFE_MARKS)
password = "".join(body)
if _USER_PASSWORD_RE.match(password):
return password
raise RuntimeError("Failed to generate a user password after many attempts")
def generate_declared_user_password(username, algorithm, validation):
"""Return a password satisfying what the user's own declaration demands.
Args:
username: the user the password belongs to, for the failure message.
algorithm: a ValueGenerator algorithm name, or None for the shell-safe
default every user gets when its declaration asks for nothing.
validation: a regular expression the value must match, or None.
An application whose registration rejects the shell-safe alphabet states
that in its own meta/users.yml rather than carrying a second credential
beside the account it already owns.
"""
for _ in range(DECLARED_PASSWORD_ATTEMPTS):
value = (
ValueGenerator().generate_value(algorithm)
if algorithm
else generate_user_password()
)
if not validation or re.search(validation, value):
return value
raise SystemExit(
f"user {username!r}: {DECLARED_PASSWORD_ATTEMPTS} passwords from "
f"algorithm {algorithm or 'the shell-safe default'!r} all failed "
f"password_validation {validation!r}; the two cannot both hold"
)