Skip to content

Commit

Permalink
feat: Ensure random passwords contain multiple character types (canon…
Browse files Browse the repository at this point in the history
…ical#5815)

The complexity of the random password generated by the
rand_user_password() method may not meet the security configuration
requirements of the system authentication module. This can cause
chpasswd to fail.

This commit ensures we generate a password using 4 different character
classes.

Fixes canonicalGH-5814

Co-authored-by: James Falcon <james.falcon@canonical.com>
  • Loading branch information
xiaoge1001 and TheRealFalcon authored Oct 17, 2024
1 parent 5819c94 commit 879945f
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 5 deletions.
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

0 comments on commit 879945f

Please sign in to comment.