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: /

```
This commit is contained in:
Nikita Dubrovskii 2024-10-17 12:57:00 +02:00 committed by Michael Vogt
parent 6a59e740e4
commit 84d4de5770
3 changed files with 106 additions and 20 deletions

View file

@ -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)

View file

@ -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
}
}

View file

@ -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: