mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
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>
70 lines
2.6 KiB
Python
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"
|
|
)
|