From 84d4de577057f66e1ad1c8e91631c441c0294532 Mon Sep 17 00:00:00 2001 From: Nikita Dubrovskii Date: Thu, 17 Oct 2024 12:57:00 +0200 Subject: [PATCH] org.osbuild.selinux: support operating on mounts This adds support for specifying paths to operate on, rather than just the root of the target: ``` - type: org.osbuild.selinux options: file_contexts: etc/selinux/targeted/contexts/files/file_contexts target: mount://root/path/to/dir mounts: - name: root source: disk target: / ``` or ``` - type: org.osbuild.selinux options: labels: mount://root/path/to/file: system_u:object_r:boot_t:s0 mount://root/path/to/other/file: system_u:object_r:var_t:s0 mounts: - name: root source: disk target: / ``` --- stages/org.osbuild.selinux | 21 +++--- stages/org.osbuild.selinux.meta.json | 8 ++- stages/test/test_selinux.py | 97 +++++++++++++++++++++++++--- 3 files changed, 106 insertions(+), 20 deletions(-) diff --git a/stages/org.osbuild.selinux b/stages/org.osbuild.selinux index 563d827b..40487599 100755 --- a/stages/org.osbuild.selinux +++ b/stages/org.osbuild.selinux @@ -4,26 +4,30 @@ import pathlib import sys import osbuild.api -from osbuild.util import selinux +from osbuild.util import parsing, selinux -def main(tree, options): +def main(args): + # Get the path where the tree is + options = args["options"] file_contexts = options.get("file_contexts") exclude_paths = options.get("exclude_paths") + target = options.get("target", "tree:///") + root, target = parsing.parse_location_into_parts(target, args) if file_contexts: - file_contexts = os.path.join(f"{tree}", options["file_contexts"]) + file_contexts = os.path.join(args["tree"], options["file_contexts"]) if exclude_paths: - exclude_paths = [os.path.join(tree, p.lstrip("/")) for p in exclude_paths] - selinux.setfiles(file_contexts, os.fspath(tree), "", exclude_paths=exclude_paths) + exclude_paths = [os.path.normpath(f"{root}/{target}/{p}") for p in exclude_paths] + selinux.setfiles(file_contexts, os.path.normpath(root), target, exclude_paths=exclude_paths) labels = options.get("labels", {}) for path, label in labels.items(): - fullpath = os.path.join(tree, path.lstrip("/")) + fullpath = parsing.parse_location(path, args) selinux.setfilecon(fullpath, label) if options.get("force_autorelabel", False): - stamp = pathlib.Path(tree, ".autorelabel") + stamp = pathlib.Path(root, ".autorelabel") # Creating just empty /.autorelabel resets only the type of files. # To ensure that the full context is reset, we write "-F" into the file. # This mimics the behavior of `fixfiles -F boot`. The "-F" option is @@ -34,6 +38,5 @@ def main(tree, options): if __name__ == '__main__': - args = osbuild.api.arguments() - r = main(args["tree"], args["options"]) + r = main(osbuild.api.arguments()) sys.exit(r) diff --git a/stages/org.osbuild.selinux.meta.json b/stages/org.osbuild.selinux.meta.json index 30dbddae..e536cead 100644 --- a/stages/org.osbuild.selinux.meta.json +++ b/stages/org.osbuild.selinux.meta.json @@ -33,6 +33,12 @@ } ], "properties": { + "target": { + "type": "string", + "description": "Target path in the tree or on a mount", + "pattern": "^mount://[^/]+/|^tree:///", + "default": "tree:///" + }, "file_contexts": { "type": "string", "description": "Path to the active SELinux policy's `file_contexts`" @@ -53,7 +59,7 @@ }, "force_autorelabel": { "type": "boolean", - "description": "Do not use. Forces auto-relabelling on first boot.", + "description": "Do not use. Forces auto-relabelling on first boot. Affects target's root or tree:/// by default", "default": false } } diff --git a/stages/test/test_selinux.py b/stages/test/test_selinux.py index 7cb51bcd..cca72f87 100644 --- a/stages/test/test_selinux.py +++ b/stages/test/test_selinux.py @@ -3,7 +3,7 @@ import os.path from unittest.mock import call, patch -import pytest +import pytest # type: ignore from osbuild import testutil @@ -29,7 +29,10 @@ def get_test_input(test_data, file_contexts=False, labels=False): ({"labels": {"/usr/bin/cp": "system_u:object_r:install_exec_t:s0"}}, ""), ({"force_autorelabel": True}, ""), ({"exclude_paths": ["/sysroot"]}, ""), + ({"target": "mount://disk/boot/efi"}, ""), + ({"target": "tree:///boot/efi"}, ""), # bad + ({"target": "/boot/efi"}, "'/boot/efi' does not match '^mount://[^/]+/|^tree:///'"), ({"file_contexts": 1234}, "1234 is not of type 'string'"), ({"labels": "xxx"}, "'xxx' is not of type 'object'"), ({"force_autorelabel": "foo"}, "'foo' is not of type 'boolean'"), @@ -59,11 +62,41 @@ def test_selinux_file_contexts(mocked_setfiles, tmp_path, stage_module): options = { "file_contexts": "etc/selinux/thing", } - stage_module.main(tmp_path, options) + args = { + "tree": f"{tmp_path}", + "options": options + } + stage_module.main(args) assert len(mocked_setfiles.call_args_list) == 1 args, kwargs = mocked_setfiles.call_args_list[0] - assert args == (f"{tmp_path}/etc/selinux/thing", os.fspath(tmp_path), "") + assert args == (f"{tmp_path}/etc/selinux/thing", os.fspath(tmp_path), "/") + assert kwargs == {"exclude_paths": None} + + +@patch("osbuild.util.selinux.setfiles") +def test_selinux_file_contexts_mounts(mocked_setfiles, tmp_path, stage_module): + tree = tmp_path / "tree" + mounts = tmp_path / "mounts" + + args = { + "tree": f"{tree}", + "options": { + "file_contexts": "etc/selinux/thing", + "target": "mount://root/" + }, + "paths": { + "mounts": mounts, + }, + "mounts": { + "root": {"path": mounts} + } + } + stage_module.main(args) + + assert len(mocked_setfiles.call_args_list) == 1 + args, kwargs = mocked_setfiles.call_args_list[0] + assert args == (f"{tree}/etc/selinux/thing", f"{mounts}", "/") assert kwargs == {"exclude_paths": None} @@ -73,33 +106,73 @@ def test_selinux_file_contexts_exclude(mocked_setfiles, tmp_path, stage_module): "file_contexts": "etc/selinux/thing", "exclude_paths": ["/sysroot"], } - stage_module.main(tmp_path, options) + args = { + "tree": f"{tmp_path}", + "options": options + } + stage_module.main(args) assert len(mocked_setfiles.call_args_list) == 1 args, kwargs = mocked_setfiles.call_args_list[0] - assert args == (f"{tmp_path}/etc/selinux/thing", os.fspath(tmp_path), "") + assert args == (f"{tmp_path}/etc/selinux/thing", os.fspath(tmp_path), "/") assert kwargs == {"exclude_paths": [f"{tmp_path}/sysroot"]} @patch("osbuild.util.selinux.setfilecon") @patch("osbuild.util.selinux.setfiles") def test_selinux_labels(mocked_setfiles, mocked_setfilecon, tmp_path, stage_module): + tree = tmp_path / "tree" testutil.make_fake_input_tree(tmp_path, { - "/usr/bin/bootc": "I'm only an imposter", + "/usr/bin/echo": "I'm only an imposter", + "/sbin/sulogin": "I'm only an imposter", }) options = { "file_contexts": "etc/selinux/thing", "labels": { - "/tree/usr/bin/bootc": "system_u:object_r:install_exec_t:s0", + "tree:///usr/bin/echo": "system_u:object_r:bin_t:s0", + "/sbin/sulogin": "system_u:object_r:sulogin_exec_t:s0", } } - stage_module.main(tmp_path, options) + args = { + "tree": tree, + "options": options + } + stage_module.main(args) assert len(mocked_setfiles.call_args_list) == 1 + assert len(mocked_setfilecon.call_args_list) == 2 + assert mocked_setfilecon.call_args_list == [ + call(f"{tree}/usr/bin/echo", "system_u:object_r:bin_t:s0"), + call(f"{tree}/sbin/sulogin", "system_u:object_r:sulogin_exec_t:s0"), + ] + + +@patch("osbuild.util.selinux.setfilecon") +def test_selinux_labels_mount(mocked_setfilecon, tmp_path, stage_module): + tree = tmp_path / "tree" + mounts = tmp_path / "mounts" + + testutil.make_fake_tree(mounts, {"/sbin/su": "I'm only an imposter"}) + args = { + "tree": tree, + "options": { + "labels": { + "mount://root/sbin/su": "system_u:object_r:su_exec_t:s0", + } + }, + "paths": { + "mounts": mounts, + }, + "mounts": { + "root": {"path": mounts} + } + } + stage_module.main(args) + assert len(mocked_setfilecon.call_args_list) == 1 assert mocked_setfilecon.call_args_list == [ - call(f"{tmp_path}/tree/usr/bin/bootc", "system_u:object_r:install_exec_t:s0"), + call(f"{mounts}/sbin/su", "system_u:object_r:su_exec_t:s0"), ] @@ -110,7 +183,11 @@ def test_selinux_force_autorelabel(mocked_setfiles, tmp_path, stage_module): # "file_contexts": "etc/selinux/thing", "force_autorelabel": enable_autorelabel, } - stage_module.main(tmp_path, options) + args = { + "tree": f"{tmp_path}", + "options": options + } + stage_module.main(args) assert (tmp_path / ".autorelabel").exists() == enable_autorelabel if enable_autorelabel: