cloud-init/backport-feat-Ensure-random-passwords-contain-multiple-charac.patch

141 lines
4.4 KiB
Diff

From 879945f56103d937a7fee84bfe7662dc2a5be708 Mon Sep 17 00:00:00 2001
From: sxt1001 <shixuantong1@huawei.com>
Date: Thu, 17 Oct 2024 20:45:07 +0800
Subject: [PATCH] feat: Ensure random passwords contain multiple character
types (#5815)
Reference:https://github.com/canonical/cloud-init/commit/879945f56103d937a7fee84bfe7662dc2a5be708
Conflict:NA
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 GH-5814
Co-authored-by: James Falcon <james.falcon@canonical.com>
---
cloudinit/config/cc_set_passwords.py | 33 +++++++++++++---
.../unittests/config/test_cc_set_passwords.py | 38 +++++++++++++++++++
2 files changed, 66 insertions(+), 5 deletions(-)
diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py
index 24d8267..d46c7f2 100644
--- a/cloudinit/config/cc_set_passwords.py
+++ b/cloudinit/config/cc_set_passwords.py
@@ -9,7 +9,8 @@
import logging
import re
-from string import ascii_letters, digits
+import random
+import string
from textwrap import dedent
from typing import List
@@ -89,9 +90,6 @@ __doc__ = get_meta_doc(meta)
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"""
@@ -307,4 +305,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)
diff --git a/tests/unittests/config/test_cc_set_passwords.py b/tests/unittests/config/test_cc_set_passwords.py
index ef34a8c..b5d561c 100644
--- a/tests/unittests/config/test_cc_set_passwords.py
+++ b/tests/unittests/config/test_cc_set_passwords.py
@@ -1,6 +1,7 @@
# This file is part of cloud-init. See LICENSE file for license information.
import logging
+import string
from unittest import mock
import pytest
@@ -555,6 +556,43 @@ class TestExpire:
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",
--
2.33.0