diff --git a/control/provision.py b/control/provision.py index 9339dbf..21d7e4c 100644 --- a/control/provision.py +++ b/control/provision.py @@ -34,6 +34,7 @@ import argparse import os import re import secrets +import string import sys from sqlalchemy import create_engine, text @@ -63,6 +64,30 @@ def _sanitize_ident(slug): return re.sub(r'[^a-z0-9]', '_', slug.lower()) +# Special chars chosen to be safe in SQL parameters, shell, and URLs +# (no quotes, backslash, %, &, ;, space, /, :). +_PW_SPECIAL = '!#^*+=-_@' + + +def _gen_password(length=28): + """Random password guaranteed to satisfy MySQL validate_password MEDIUM: + at least one lowercase, uppercase, digit, and special character.""" + alphabet = string.ascii_letters + string.digits + _PW_SPECIAL + while True: + chars = [ + secrets.choice(string.ascii_lowercase), + secrets.choice(string.ascii_uppercase), + secrets.choice(string.digits), + secrets.choice(_PW_SPECIAL), + ] + chars += [secrets.choice(alphabet) for _ in range(length - 4)] + secrets.SystemRandom().shuffle(chars) + pw = ''.join(chars) + # guard against a leading '-' (awkward in some CLIs/URLs) + if pw[0] != '-': + return pw + + def db_name_for(slug): return f'jqc_{_sanitize_ident(slug)}'[:64] @@ -191,7 +216,7 @@ def create_tenant(slug, name, plan_code, admin_email, admin_username=None, host = db_host or _default_db_host() dbname = db_name_for(slug) dbuser = db_user_for(slug) - password = secrets.token_urlsafe(24) + password = _gen_password() with control_session() as s: if s.query(Tenant).filter_by(slug=slug).first(): @@ -442,4 +467,4 @@ def main(argv=None): if __name__ == '__main__': - main() + main() \ No newline at end of file