Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

generate random passwords using minclass #5815

Merged
merged 1 commit into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions cloudinit/config/cc_set_passwords.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
"""Set Passwords: Set user passwords and enable/disable SSH password auth"""

import logging
import random
import re
from string import ascii_letters, digits
import string
from typing import List

from cloudinit import features, lifecycle, subp, util
Expand All @@ -30,9 +31,6 @@

LOG = logging.getLogger(__name__)

# We are removing certain 'painful' letters/numbers
PW_SET = "".join([x for x in ascii_letters + digits if x not in "loLOI01"])


def get_users_by_type(users_list: list, pw_type: str) -> list:
"""either password or type: RANDOM is required, user is always required"""
Expand Down Expand Up @@ -248,4 +246,29 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None:


def rand_user_password(pwlen=20):
return util.rand_str(pwlen, select_from=PW_SET)
if pwlen < 4:
raise ValueError("Password length must be at least 4 characters.")

# There are often restrictions on the minimum number of character
# classes required in a password, so ensure we at least one character
# from each class.
res_rand_list = [
random.choice(string.digits),
random.choice(string.ascii_lowercase),
random.choice(string.ascii_uppercase),
random.choice(string.punctuation),
]

res_rand_list.extend(
list(
util.rand_str(
pwlen - len(res_rand_list),
select_from=string.digits
+ string.ascii_lowercase
+ string.ascii_uppercase
+ string.punctuation,
)
)
)
random.shuffle(res_rand_list)
return "".join(res_rand_list)
38 changes: 38 additions & 0 deletions tests/unittests/config/test_cc_set_passwords.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import copy
import logging
import string
from unittest import mock

import pytest
Expand Down Expand Up @@ -559,6 +560,43 @@ def test_expire_old_behavior(self, cfg, mocker, caplog):
assert "Expired passwords" not in caplog.text


class TestRandUserPassword:
def _get_str_class_num(self, str):
return sum(
[
any(c.islower() for c in str),
any(c.isupper() for c in str),
any(c.isupper() for c in str),
any(c in string.punctuation for c in str),
]
)

@pytest.mark.parametrize(
"strlen, expected_result",
[
(1, ValueError),
(2, ValueError),
(3, ValueError),
(4, 4),
(5, 4),
(5, 4),
(6, 4),
(20, 4),
],
)
def test_rand_user_password(self, strlen, expected_result):
if expected_result is ValueError:
with pytest.raises(
expected_result,
match="Password length must be at least 4 characters.",
):
setpass.rand_user_password(strlen)
else:
rand_password = setpass.rand_user_password(strlen)
assert len(rand_password) == strlen
assert self._get_str_class_num(rand_password) == expected_result


class TestSetPasswordsSchema:
@pytest.mark.parametrize(
"config, expectation",
Expand Down