File: test_users_groups.py

package info (click to toggle)
cloud-init 25.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 12,412 kB
  • sloc: python: 135,894; sh: 3,883; makefile: 141; javascript: 30; xml: 22
file content (194 lines) | stat: -rw-r--r-- 6,711 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
"""Integration tests for the user_groups module.

TODO:
* This module assumes that the "ubuntu" user will be created when "default" is
  specified; this will need modification to run on other OSes.
"""

import re

import pytest

from tests.integration_tests.instances import IntegrationInstance
from tests.integration_tests.releases import (
    CURRENT_RELEASE,
    IS_UBUNTU,
    JAMMY,
    NOBLE,
)
from tests.integration_tests.util import verify_clean_boot

USER_DATA = """\
#cloud-config
# Add groups to the system
groups:
  - secret: [root]
  - cloud-users

# Add users to the system. Users are added after groups are added.
users:
  - default
  - name: foobar
    gecos: Foo B. Bar
    primary_group: foobar
    groups: users
    expiredate: '2038-01-19'
    lock_passwd: false
    passwd: $6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYe\
AHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/
  - name: barfoo
    gecos: Bar B. Foo
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    groups: [cloud-users, secret]
    lock_passwd: true
  - name: nopassworduser
    gecos: I do not like passwords
    lock_passwd: false
  - name: cloudy
    gecos: Magic Cloud App Daemon User
    inactive: '0'
    system: true
  - name: eric
    sudo: null
    uid: 1742
  - name: archivist
    uid: 1743
"""

NEW_USER_EMPTY_PASSWD_WARNING = "Not unlocking password for user {username}. 'lock_passwd: false' present in user-data but no 'passwd'/'plain_text_passwd'/'hashed_passwd' provided in user-data"  # noqa: E501

EXISTING_USER_EMPTY_PASSWD_WARNING = "Not unlocking blank password for existing user {username}. 'lock_passwd: false' present in user-data but no existing password set and no 'plain_text_passwd'/'hashed_passwd' provided in user-data"  # noqa E501


@pytest.mark.ci
@pytest.mark.user_data(USER_DATA)
class TestUsersGroups:
    """Test users and groups.

    This test specifies a number of users and groups via user-data, and
    confirms that they have been configured correctly in the system under test.
    """

    @pytest.mark.skipif(not IS_UBUNTU, reason="Test assumes 'ubuntu' user")
    @pytest.mark.parametrize(
        "getent_args,regex",
        [
            # Test the ubuntu group
            (["group", "ubuntu"], r"ubuntu:x:[0-9]{4}:"),
            # Test the cloud-users group
            (["group", "cloud-users"], r"cloud-users:x:[0-9]{4}:barfoo"),
            # Test the ubuntu user
            (
                ["passwd", "ubuntu"],
                r"ubuntu:x:[0-9]{4}:[0-9]{4}:Ubuntu:/home/ubuntu:/bin/bash",
            ),
            # Test the foobar user
            (
                ["passwd", "foobar"],
                r"foobar:x:[0-9]{4}:[0-9]{4}:Foo B. Bar:/home/foobar:",
            ),
            # Test the barfoo user
            (
                ["passwd", "barfoo"],
                r"barfoo:x:[0-9]{4}:[0-9]{4}:Bar B. Foo:/home/barfoo:",
            ),
            # Test the cloudy user
            (["passwd", "cloudy"], r"cloudy:x:[0-9]{3,4}:"),
            # Test str uid
            (["passwd", "eric"], r"eric:x:1742:"),
            # Test int uid
            (["passwd", "archivist"], r"archivist:x:1743:"),
            # Test int uid
            (
                ["passwd", "nopassworduser"],
                r"nopassworduser:x:[0-9]{4}:[0-9]{4}:I do not like passwords",
            ),
        ],
    )
    def test_users_groups(self, regex, getent_args, class_client):
        """Use getent to interrogate the various expected outcomes"""
        result = class_client.execute(["getent"] + getent_args)
        assert re.search(regex, result.stdout) is not None, (
            "'getent {}' resulted in '{}', "
            "but expected to match regex {}".format(
                " ".join(getent_args), result.stdout, regex
            )
        )

    def test_initial_warnings(self, class_client):
        """Check for initial warnings."""
        warnings = (
            [NEW_USER_EMPTY_PASSWD_WARNING.format(username="nopassworduser")]
            if CURRENT_RELEASE > NOBLE
            else []
        )
        verify_clean_boot(
            class_client,
            require_warnings=warnings,
        )

    def test_user_root_in_secret(self, class_client):
        """Test root user is in 'secret' group."""
        output = class_client.execute("groups root").stdout
        _, groups_str = output.split(":", maxsplit=1)
        groups = groups_str.split()
        assert "secret" in groups

    def test_nopassword_unlock_warnings(self, class_client):
        """Verify warnings for empty passwords for new and existing users."""
        # Fake admin clearing and unlocking and empty unlocked password foobar
        # This will generate additional warnings about not unlocking passwords
        # for pre-existing users which have an existing empty password
        class_client.execute("passwd -d foobar")
        class_client.instance.clean()
        class_client.restart()
        warnings = (
            [
                EXISTING_USER_EMPTY_PASSWD_WARNING.format(
                    username="nopassworduser"
                ),
                EXISTING_USER_EMPTY_PASSWD_WARNING.format(username="foobar"),
            ]
            if CURRENT_RELEASE > NOBLE
            else []
        )
        verify_clean_boot(
            class_client,
            ignore_warnings=True,  # ignore warnings about existing groups
            require_warnings=warnings,
        )


@pytest.mark.user_data(USER_DATA)
@pytest.mark.skipif(
    CURRENT_RELEASE < JAMMY,
    reason="Requires version of sudo not available in older releases",
)
def test_sudoers_includedir(client: IntegrationInstance):
    """Ensure we don't add additional #includedir to sudoers.

    Newer versions of /etc/sudoers will use @includedir rather than
    #includedir. Ensure we handle that properly and don't include an
    additional #includedir when one isn't warranted.

    https://github.com/canonical/cloud-init/pull/783
    """
    client.execute("sed -i 's/#include/@include/g' /etc/sudoers")

    sudoers_content_before = client.read_from_file(
        "/etc/sudoers.d/90-cloud-init-users"
    ).splitlines()[1:]
    sudoers = client.read_from_file("/etc/sudoers")
    if "@includedir /etc/sudoers.d" not in sudoers:
        client.execute("echo '@includedir /etc/sudoers.d' >> /etc/sudoers")
    client.instance.clean()
    client.restart()
    sudoers = client.read_from_file("/etc/sudoers")

    assert "#includedir" not in sudoers
    assert sudoers.count("includedir /etc/sudoers.d") == 1

    sudoers_content_after = client.read_from_file(
        "/etc/sudoers.d/90-cloud-init-users"
    ).splitlines()[1:]
    assert sudoers_content_before == sudoers_content_after