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

Support sudoers in the "usr merge" location #5161

Merged
merged 6 commits into from
May 30, 2024

Conversation

rjschwei
Copy link
Contributor

@rjschwei rjschwei commented Apr 8, 2024

Proposed Commit Message

When sudo is compiled with the secure-path option and the configuration file exists in /usr/etc and not in /etc take the configuration in /usr/etc into account. If the desired include directive exists in the system (/usr/etc ) configuration there is nothing to do if there is no file in etc.

Additional Context

Test Steps

Checklist

Merge type

  • Squash merge using "Proposed Commit Message"
  • Rebase and merge unique commits. Requires commit messages per-commit each referencing the pull request number (#<PR_NUM>)

@rjschwei
Copy link
Contributor Author

rjschwei commented Apr 8, 2024

@blackboxsw need some help with the test please, I do not understand why I am triggering a permission error in the mock for the file write. Thanks

@blackboxsw blackboxsw self-assigned this Apr 16, 2024
Copy link
Collaborator

@blackboxsw blackboxsw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good for handling the case of /usr/etc/sudoers vs an absent /etc/sudoers file. Some general questions I have are:

  • Is the a changeset represented in upstream sudo project that points to this behavior and/or /usr/etc/sudoers overrides?
  • what documentation tells us we should ignore /usr/etc/sudoers when /etc/sudoers exits?
  • What should be the behavior on a system with both /etc/sudoers.d and /usr/etc/sudoers.d? Do all the sudo config parts.d directory get aggregated?

if not isinstance(sudo_base, (list, tuple)):
sudo_base = [sudo_base]
for sudoers_base in sudo_base:
if os.path.exists(sudoers_base):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will there be a case where /usr/etc/sudoers exists bit /etc/sudoers does not? It seems this is possible given this read-only FS note in SUSE docs which talk about visudo writing to /etc/sudoers if /usr/etc/sudoers exists but /etc/sudoers does not.

Do we want to completely ignore /usr/etc/sudoers in any environment which already has an /etc/sudoers file? There is no active merge of /usr/etc/sudoers content with primary /etc/sudoers anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will there be a case where /usr/etc/sudoers exists bit /etc/sudoers does not? It seems this is possible given this read-only FS note in SUSE docs which talk about visudo writing to /etc/sudoers if /usr/etc/sudoers exists but /etc/sudoers does not.

Yes, in newer SUSE distributions /etc/sudoers will not exist by default.

Do we want to completely ignore /usr/etc/sudoers in any environment which already has an /etc/sudoers file? There is no active merge of /usr/etc/sudoers content with primary /etc/sudoers anywhere.

If /etc/sudoers exists the underlying premise is that the user (sysadmin) has made the modifications they want to have in place. Should the include directive be missing we add it to this file to match the current behavior. If /etc/usr/sudoers exists and contains the include directive and we would not add the include directive to an existing /etc/sudoers we would change the behavior compared to the current implementation because the include files would be processed at a different point in time.

Comment on lines 1050 to 1052
if sudoers_base != admin_sudo_base:
util.write_file(
admin_sudo_base, sudoers_contents, 0o440)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case where /etc/sudoers doesn't exist and /usr/etc/sudoers does exist . We fall into this conditional where we now create a root read-only /etc/sudoers file with a copy of /usr/etc/soduers (presumably the sudoers path of the read-only filesystem). At this point, permissions of 0o440 will disallow our calling of append_file below as a non-root user that is executing the unit tests. The reason other unit tests pass is because those unit tests actually performed the util.write_file("/etc/sudeors") with default mode 0o644 leaving the unittest able to append_file.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a unit-test workaround that avoids a non-root test-user getting hit by the read-only permission check on the test-created /etc/sudoers

diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index ab656ab5a..63b50b2fc 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -1048,6 +1048,11 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta):
                     util.write_file(admin_sudo_base, sudoers_contents, 0o440)
                 else:
                     if sudoers_base != admin_sudo_base:
+                        LOG.debug(
+                            "Copying content from %s to %s",
+                            sudoers_base,
+                            admin_sudo_base,
+                        )
                         util.write_file(
                             admin_sudo_base, sudoers_contents, 0o440)
                     lines = [
diff --git a/tests/unittests/distros/test__init__.py b/tests/unittests/distros/test__init__.py
index e762f79f6..aa682acc5 100644
--- a/tests/unittests/distros/test__init__.py
+++ b/tests/unittests/distros/test__init__.py
@@ -251,9 +251,15 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase):
         self.patchOS(self.tmp)
         self.patchUtils(self.tmp)
         util.write_file("/usr/etc/sudoers", "josh, josh\n")
-        d.ensure_sudo_dir("/b")
+        # This test creates missing /etc/sudoers with mode=0o440.
+        # Avoid append_file which raises a permissions error on 0o440.
+        with mock.patch("cloudinit.distros.util.append_file") as append:
+            d.ensure_sudo_dir("/b")
+        path, appended_content = append.call_args_list[0][0]
+        self.assertEqual("/etc/sudoers",  path)
+        self.assertRegex(appended_content, r"#includedir \/b")
+
         contents = util.load_text_file("/etc/sudoers")
-        self.assertIn("includedir /b", contents)
         self.assertTrue(os.path.isdir("/b"))
         self.assertIn("josh", contents)
         self.assertEqual(2, contents.count("josh"))

else:
if sudoers_base != admin_sudo_base:
util.write_file(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's log what we've done here, otherwise our write_files logging won't really tell us where we got the contents. "Copying content from /usr/etc/sudoers to /etc/sudoers"

With the usr merge effort many configuration files are moving from /etc to
/usr/etc clearing the way for the /etc directory to be a space managed
by the system administrator and the /usr/etc space used by the distribution
vendor. Support the /usr/etc/sudoers location when managing the sudoers file.
There is no need to copy the content of the file. If we do not find the
include directive in either the /etc/sudoers file or the /usr/etc/sudoers
file we append the include directive to the /etc/sudoers file if it exists,
if it does not exist we simply write the include directive into /etc/sudoers.
This preserves the current behavior w.r.t. oredering of the read operation.
@rjschwei
Copy link
Contributor Author

rjschwei commented Apr 25, 2024

OK, thanks for the questions, made me realize that I had a logic error. There is no need to copy the content from /usr/etc/sudoers if it exists. If the "secure path" file does not have the desired include directive and the "sysadmin location", i.e. /etc/sudoers does not exist we simply write the include directive to /etc/sudoers . If /etc/sudoers exists but does not have the include directive we append there to preserve the current behavior.

@rjschwei
Copy link
Contributor Author

Looks pretty good for handling the case of /usr/etc/sudoers vs an absent /etc/sudoers file. Some general questions I have are:

  • Is the a changeset represented in upstream sudo project that points to this behavior and/or /usr/etc/sudoers overrides?

Not that I can find. The existence of /usr/etc appears to be quite old and can be when building the upstream sources with --enable-secure-path. For SUSE distributions we set the configuration explicitly with

%if %{defined _distconfdir}
    --prefix=/usr \
    --sysconfdir=%{_distconfdir} \
    --enable-adminconf=%{_sysconfdir} \
%endif

The condition on defined _distconfdir exists to differentiate the build between distributions that have /usr/etc and distributions that do not.

https://build.opensuse.org/projects/openSUSE:Factory/packages/sudo/files/sudo.spec?expand=1

  • what documentation tells us we should ignore /usr/etc/sudoers when /etc/sudoers exits?

I changed that in the current update. I twisted myself into a knot with the first go around. The logic is more straight forwrad now, IMHO.

  • What should be the behavior on a system with both /etc/sudoers.d and /usr/etc/sudoers.d? Do all the sudo config parts.d directory get aggregated?

The default SUSE configuration is to include /usr/etc/sudoers.d and then /etc/sudoers.d. So the answer is yes they do. The updated logic makes sure that /etc/sudoers.d is included last if no include directive exists in either /etc/sudoers or /usr/etc/sudoers preserving the current behavior.

If the include directive we expect is found in the /usr/etc/sudoers file
we shold not create a file in /etc.
When /etc/sudoers exists the content of /usr/etc/sudoers is ignored. Therefore,
if it does not already exist we copy the content of /usr/etc/sudores to make
sure we preserve the default settings and then add the include directive,
Log info when we use the content form /usr/etc/sudoers as the base to seed
/etc/sudoers
Copy link

Hello! Thank you for this proposed change to cloud-init. This pull request is now marked as stale as it has not seen any activity in 14 days. If no activity occurs within the next 7 days, this pull request will automatically close.

If you are waiting for code review and you are seeing this message, apologies! Please reply, tagging TheRealFalcon, and he will ensure that someone takes a look soon.

(If the pull request is closed and you would like to continue working on it, please do tag TheRealFalcon to reopen it.)

@github-actions github-actions bot added the stale-pr Pull request is stale; will be auto-closed soon label May 15, 2024
@rjschwei
Copy link
Contributor Author

@blackboxsw I think I addressed the requested changes. Anything else?

@TheRealFalcon TheRealFalcon removed the stale-pr Pull request is stale; will be auto-closed soon label May 15, 2024
@blackboxsw blackboxsw added this to the cloud-init-24.2 milestone May 27, 2024
Copy link
Collaborator

@blackboxsw blackboxsw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @rjschwei this is much simpler and straight-forward.

I've added a couple of supplemental log checks to unittests here. but looks good to me.

commit 43d29f1350057ab60c992c340eda8cf2e0f227a2 (HEAD -> usrMerge)
Author: Chad Smith <[email protected]>
Date:   Wed May 29 21:27:20 2024 -0600

    test log additions

diff --git a/tests/unittests/distros/test__init__.py b/tests/unittests/distros/test__init__.py
index 6ce3d25bd..39583b137 100644
--- a/tests/unittests/distros/test__init__.py
+++ b/tests/unittests/distros/test__init__.py
@@ -55,6 +55,8 @@ gapmi = distros._get_arch_package_mirror_info
 
 
 class TestGenericDistro(helpers.FilesystemMockingTestCase):
+    with_logs = True
+
     def setUp(self):
         super(TestGenericDistro, self).setUp()
         # Make a temp directoy for tests to use.
@@ -231,6 +233,9 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase):
         self.assertTrue(os.path.isdir("/b"))
         self.assertIn("josh", contents)
         self.assertEqual(2, contents.count("josh"))
+        self.assertIn(
+            "Added '#includedir /b' to /etc/sudoers", self.logs.getvalue()
+        )
 
     def test_sudoers_ensure_append_sudoer_file(self):
         cls = distros.fetch("ubuntu")
@@ -257,8 +262,11 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase):
         self.assertEqual(2, contents.count("josh"))
         self.assertIn("includedir /b", contents)
         self.assertTrue(os.path.isdir("/b"))
+        self.assertIn(
+            "Using content from '/usr/etc/sudoers", self.logs.getvalue()
+        )
 
-    def test_usr_sudoers_ensure_no_etc_creat(self):
+    def test_usr_sudoers_ensure_no_etc_create_when_include_in_usr_etc(self):
         cls = distros.fetch("ubuntu")
         d = cls("ubuntu", {}, None)
         self.patchOS(self.tmp)

In a followup PR this cycle we'll be converting all this FilesystemMocking tests to pytest so look for that in the future.

@blackboxsw blackboxsw merged commit c76ad9c into canonical:main May 30, 2024
28 of 29 checks passed
@rjschwei rjschwei deleted the usrMerge branch May 30, 2024 09:53
holmanb pushed a commit that referenced this pull request Jun 28, 2024
When sudo is compiled with the secure-path option and the configuration
file sudoers exists in /usr/etc and not in /etc take the configuration in
/usr/etc into account. If the desired include directive exists in the system
(/usr/etc) configuration there is nothing to do if there is no file in /etc.

When /etc/sudoers exists, the content of /usr/etc/sudoers is ignored.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants