org.osbuild.selinux: support for specifying where file_contexts comes from

file_context now can come from
- tree (current default)
- mount
- input

Example:
```
- type: org.osbuild.selinux
  inputs:
    tree:
      type: org.osbuild.tree
      origin: org.osbuild.pipeline
      references:
        - name:tree
  options:
    file_contexts: input://tree/etc/selinux/targeted/contexts/files/file_contexts
```
This commit is contained in:
Nikita Dubrovskii 2024-10-18 17:04:07 +02:00 committed by Michael Vogt
parent 84d4de5770
commit a8e8ebde44
3 changed files with 56 additions and 24 deletions

View file

@ -9,6 +9,7 @@ from osbuild.util import parsing, selinux
def main(args):
# Get the path where the tree is
tree = args["tree"]
options = args["options"]
file_contexts = options.get("file_contexts")
exclude_paths = options.get("exclude_paths")
@ -16,7 +17,10 @@ def main(args):
root, target = parsing.parse_location_into_parts(target, args)
if file_contexts:
file_contexts = os.path.join(args["tree"], options["file_contexts"])
if "://" not in file_contexts:
file_contexts = os.path.normpath(f"{tree}/{file_contexts}")
else:
file_contexts = parsing.parse_location(file_contexts, args)
if 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)

View file

@ -1,8 +1,8 @@
{
"summary": "Set SELinux file contexts",
"description": [
"Sets correct SELinux labels for every file in the tree, according to the",
"SELinux policy installed inside the tree.",
"Sets correct SELinux labels for every file in the tree or on mount, according to",
"the SELinux policy.",
"Uses the host's `setfiles` program and the tree's `file_contexts`, usually",
" /etc/selinux/<SELINUXTYPE>/contexts/files/file_contexts",
"where <SELINUXTYPE> is the value set in /etc/selinux/config (usually \"targeted\"",
@ -40,8 +40,8 @@
"default": "tree:///"
},
"file_contexts": {
"type": "string",
"description": "Path to the active SELinux policy's `file_contexts`"
"description": "Path to the active SELinux policy's `file_contexts`. Supports `tree://`, `mount://`, and `input://` schemes. Plain paths imply `tree://`.",
"type": "string"
},
"exclude_paths": {
"type": "array",
@ -70,6 +70,10 @@
},
"mounts": {
"type": "array"
},
"inputs": {
"type": "object",
"additionalProperties": true
}
}
}

View file

@ -31,6 +31,7 @@ def get_test_input(test_data, file_contexts=False, labels=False):
({"exclude_paths": ["/sysroot"]}, ""),
({"target": "mount://disk/boot/efi"}, ""),
({"target": "tree:///boot/efi"}, ""),
({"file_contexts": "path/to/file_contexts"}, ""),
# bad
({"target": "/boot/efi"}, "'/boot/efi' does not match '^mount://[^/]+/|^tree:///'"),
({"file_contexts": 1234}, "1234 is not of type 'string'"),
@ -58,19 +59,38 @@ def test_schema_validation_selinux_required_options(stage_schema):
@patch("osbuild.util.selinux.setfiles")
def test_selinux_file_contexts(mocked_setfiles, tmp_path, stage_module):
options = {
"file_contexts": "etc/selinux/thing",
}
def test_selinux_file_contexts_schemeless_relpath(mocked_setfiles, tmp_path, stage_module):
tree = tmp_path / "tree"
args = {
"tree": f"{tmp_path}",
"options": options
"tree": tree,
"options": {
"file_contexts": "etc/selinux/thing",
}
}
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"{tree}/etc/selinux/thing", os.fspath(tree), "/")
assert kwargs == {"exclude_paths": None}
@patch("osbuild.util.selinux.setfiles")
def test_selinux_file_contexts_schemeless_abspath(mocked_setfiles, tmp_path, stage_module):
tree = tmp_path / "tree"
args = {
"tree": tree,
"options": {
"file_contexts": "/etc/selinux/thing",
}
}
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", os.fspath(tree), "/")
assert kwargs == {"exclude_paths": None}
@ -78,44 +98,48 @@ def test_selinux_file_contexts(mocked_setfiles, tmp_path, stage_module):
def test_selinux_file_contexts_mounts(mocked_setfiles, tmp_path, stage_module):
tree = tmp_path / "tree"
mounts = tmp_path / "mounts"
root = mounts / "."
args = {
"tree": f"{tree}",
"tree": tree,
"options": {
"file_contexts": "etc/selinux/thing",
"file_contexts": "mount://root/etc/selinux/file_contexts",
"target": "mount://root/"
},
"paths": {
"mounts": mounts,
},
"mounts": {
"root": {"path": mounts}
"root": {
"path": root
}
}
}
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 args == (f"{mounts}/etc/selinux/file_contexts", f"{mounts}", "/")
assert kwargs == {"exclude_paths": None}
@patch("osbuild.util.selinux.setfiles")
def test_selinux_file_contexts_exclude(mocked_setfiles, tmp_path, stage_module):
options = {
"file_contexts": "etc/selinux/thing",
"exclude_paths": ["/sysroot"],
}
tree = tmp_path / "tree"
args = {
"tree": f"{tmp_path}",
"options": options
"tree": tree,
"options": {
"file_contexts": "etc/selinux/thing",
"exclude_paths": ["/sysroot"],
}
}
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 kwargs == {"exclude_paths": [f"{tmp_path}/sysroot"]}
assert args == (f"{tree}/etc/selinux/thing", os.fspath(tree), "/")
assert kwargs == {"exclude_paths": [f"{tree}/sysroot"]}
@patch("osbuild.util.selinux.setfilecon")