diff --git a/changelog/68323.added.md b/changelog/68323.added.md new file mode 100644 index 00000000000..260a12b692c --- /dev/null +++ b/changelog/68323.added.md @@ -0,0 +1,3 @@ +Added booleans argument to selinux.booleans +Added mod_aggregate to selinux to combine boolean +Added some type hints to selinux module and made some minor changes to improve readability and performance slightly diff --git a/salt/modules/selinux.py b/salt/modules/selinux.py index 0f78d1be9c6..4658527b8b0 100644 --- a/salt/modules/selinux.py +++ b/salt/modules/selinux.py @@ -10,26 +10,71 @@ proper packages are installed. """ +from __future__ import annotations + import os import re +from enum import Enum +from typing import Any, Literal, TypedDict import salt.utils.decorators as decorators import salt.utils.files import salt.utils.path import salt.utils.stringutils -import salt.utils.versions from salt.exceptions import CommandExecutionError, SaltInvocationError -_SELINUX_FILETYPES = { - "a": "all files", - "f": "regular file", - "d": "directory", - "c": "character device", - "b": "block device", - "s": "socket", - "l": "symbolic link", - "p": "named pipe", -} + +class SeFileType(Enum): + _description: str + + ALL = ("a", "all files") + REGULAR = ("f", "regular file") + DIRECTORY = ("d", "directory") + CHAR_DEV = ("c", "character device") + BLOCK_DEV = ("b", "block device") + SOCKET = ("s", "socket") + SYMLINK = ("l", "symbolic link") + NAMED_PIPE = ("p", "named pipe") + + def __new__(cls, code: str, description: str): + obj = object.__new__(cls) + obj._value_ = code + obj._description = description + return obj + + @property + def description(self) -> str: + return self._description + + @classmethod + def from_code(cls, code: str) -> SeFileType: + """ + Validate a raw string and return the matching FileType. + Raises SaltInvocationError on invalid codes. + """ + try: + return cls(code) # pylint: disable=no-value-for-parameter + except ValueError: + valid = ", ".join(e.value for e in cls) + raise SaltInvocationError( + f"Invalid file type code {code!r}; expected one of: {valid}" + ) + + +EmptyDict = TypedDict("EmptyDict", {}) + +SeBool = Literal[True, "true", "on", "1", 1, False, "false", "off", "0", 0] + + +class SeBoolDict(TypedDict): + State: str + Default: str + Description: str + + +class SeModDict(TypedDict): + Enabled: bool + Version: str | None def __virtual__(): @@ -99,7 +144,7 @@ def getenforce(): return "Disabled" -def getconfig(): +def getconfig() -> str | None: """ Return the selinux mode from the config file @@ -113,15 +158,19 @@ def getconfig(): config = "/etc/selinux/config" with salt.utils.files.fopen(config, "r") as _fp: for line in _fp: - line = salt.utils.stringutils.to_unicode(line) - if line.strip().startswith("SELINUX="): - return line.split("=")[1].capitalize().strip() + stripped_line = salt.utils.stringutils.to_unicode(line).strip() + if stripped_line.startswith("SELINUX="): + return stripped_line.split("=", maxsplit=1)[1].capitalize().lstrip() except (OSError, AttributeError): return None return None -def setenforce(mode): +def setenforce( + mode: Literal[ + "enforcing", "Enforcing", "Permissive", "permissive", "Disabled", "disabled" + ] +): """ Set the SELinux enforcing mode @@ -132,34 +181,30 @@ def setenforce(mode): salt '*' selinux.setenforce enforcing """ if isinstance(mode, str): - if mode.lower() == "enforcing": - mode = "1" + mode_lower = mode.lower() + if mode_lower == "enforcing": + enforce_mode = "1" modestring = "enforcing" - elif mode.lower() == "permissive": - mode = "0" + elif mode_lower == "permissive": + enforce_mode = "0" modestring = "permissive" - elif mode.lower() == "disabled": - mode = "0" + elif mode_lower == "disabled": + enforce_mode = "0" modestring = "disabled" else: - return f"Invalid mode {mode}" - elif isinstance(mode, int): - if mode: - mode = "1" - else: - mode = "0" + raise SaltInvocationError(f"Invalid mode {mode}") else: - return f"Invalid mode {mode}" + raise SaltInvocationError(f"Invalid mode {mode}") # enforce file does not exist if currently disabled. Only for toggling enforcing/permissive - if getenforce() != "Disabled": - enforce = os.path.join(selinux_fs_path(), "enforce") + if getenforce() != "Disabled" and (_selinux_fs_path := selinux_fs_path()): + enforce = os.path.join(_selinux_fs_path, "enforce") try: with salt.utils.files.fopen(enforce, "w") as _fp: - _fp.write(salt.utils.stringutils.to_str(mode)) + _fp.write(salt.utils.stringutils.to_str(enforce_mode)) except OSError as exc: - msg = "Could not write SELinux enforce file: {0}" - raise CommandExecutionError(msg.format(exc)) + msg = f"Could not write SELinux enforce file: {exc}" + raise CommandExecutionError(msg) config = "/etc/selinux/config" try: @@ -170,16 +215,16 @@ def setenforce(mode): conf = re.sub(r"\nSELINUX=.*\n", "\nSELINUX=" + modestring + "\n", conf) _cf.write(salt.utils.stringutils.to_str(conf)) except OSError as exc: - msg = "Could not write SELinux config file: {0}" - raise CommandExecutionError(msg.format(exc)) + msg = f"Could not write SELinux config file: {exc}" + raise CommandExecutionError(msg) except OSError as exc: - msg = "Could not read SELinux config file: {0}" - raise CommandExecutionError(msg.format(exc)) + msg = f"Could not read SELinux config file: {exc}" + raise CommandExecutionError(msg) return getenforce() -def getsebool(boolean): +def getsebool(boolean: str) -> SeBoolDict | EmptyDict: """ Return the information on a specific selinux boolean @@ -192,7 +237,7 @@ def getsebool(boolean): return list_sebool().get(boolean, {}) -def setsebool(boolean, value, persist=False): +def setsebool(boolean: Any, value: SeBool, persist=False): """ Set the value for a boolean @@ -203,13 +248,13 @@ def setsebool(boolean, value, persist=False): salt '*' selinux.setsebool virt_use_usb off """ if persist: - cmd = f"setsebool -P {boolean} {value}" + cmd = f"setsebool -P '{boolean}' '{value}'" else: - cmd = f"setsebool {boolean} {value}" + cmd = f"setsebool '{boolean}' '{value}'" return not __salt__["cmd.retcode"](cmd, python_shell=False) -def setsebools(pairs, persist=False): +def setsebools(pairs: dict[Any, SeBool], persist=False): """ Set the value of multiple booleans @@ -219,18 +264,15 @@ def setsebools(pairs, persist=False): salt '*' selinux.setsebools '{virt_use_usb: on, squid_use_tproxy: off}' """ - if not isinstance(pairs, dict): - return {} + cmd = ["setsebool"] if persist: - cmd = "setsebool -P " - else: - cmd = "setsebool " + cmd.append("-P") for boolean, value in pairs.items(): - cmd = f"{cmd} {boolean}={value}" + cmd.append(f"{boolean}={value}") return not __salt__["cmd.retcode"](cmd, python_shell=False) -def list_sebool(): +def list_sebool() -> dict[str, SeBoolDict]: """ Return a structure listing all of the selinux booleans on the system and what state they are in @@ -241,21 +283,21 @@ def list_sebool(): salt '*' selinux.list_sebool """ - bdata = __salt__["cmd.run"]("semanage boolean -l").splitlines() + bdata = __salt__["cmd.run"]("semanage boolean --list").splitlines() ret = {} for line in bdata[1:]: - if not line.strip(): + if not line or line.isspace(): continue comps = line.split() - ret[comps[0]] = { - "State": comps[1][1:], - "Default": comps[3][:-1], - "Description": " ".join(comps[4:]), - } + ret[comps[0]] = SeBoolDict( + State=comps[1][1:], + Default=comps[3][:-1], + Description=" ".join(comps[4:]), + ) return ret -def getsemod(module): +def getsemod(module) -> SeModDict | EmptyDict: """ Return the information on a specific selinux module @@ -270,7 +312,7 @@ def getsemod(module): return list_semod().get(module, {}) -def setsemod(module, state): +def setsemod(module: Any, state: Literal["enabled", "Enabled", "disabled", "Disabled"]): """ Enable or disable an SELinux module. @@ -282,14 +324,17 @@ def setsemod(module, state): .. versionadded:: 2016.3.0 """ - if state.lower() == "enabled": - cmd = f"semodule -e {module}" - elif state.lower() == "disabled": - cmd = f"semodule -d {module}" + state_lower = state.lower() + if state_lower == "enabled": + cmd = f"semodule --enable '{module}'" + elif state_lower == "disabled": + cmd = f"semodule --disable '{module}'" + else: + raise SaltInvocationError(f"Invalid state {state}") return not __salt__["cmd.retcode"](cmd) -def install_semod(module_path): +def install_semod(module_path: str): """ Install custom SELinux module from file @@ -301,9 +346,9 @@ def install_semod(module_path): .. versionadded:: 2016.11.6 """ - if module_path.find("salt://") == 0: + if module_path.startswith("salt://"): module_path = __salt__["cp.cache_file"](module_path) - cmd = f"semodule -i {module_path}" + cmd = f"semodule --install '{module_path}'" return not __salt__["cmd.retcode"](cmd) @@ -319,11 +364,11 @@ def remove_semod(module): .. versionadded:: 2016.11.6 """ - cmd = f"semodule -r {module}" + cmd = f"semodule --remove '{module}'" return not __salt__["cmd.retcode"](cmd) -def list_semod(): +def list_semod() -> dict[str, SeModDict]: """ Return a structure listing all of the selinux modules on the system and what state they are in @@ -336,49 +381,19 @@ def list_semod(): .. versionadded:: 2016.3.0 """ - helptext = __salt__["cmd.run"]("semodule -h").splitlines() - semodule_version = "" - for line in helptext: - if line.strip().startswith("full"): - semodule_version = "new" - - if semodule_version == "new": - mdata = __salt__["cmd.run"]("semodule -lfull").splitlines() - ret = {} - for line in mdata: - if not line.strip(): - continue - comps = line.split() - if len(comps) == 4: - ret[comps[1]] = {"Enabled": False, "Version": None} - else: - ret[comps[1]] = {"Enabled": True, "Version": None} - else: - mdata = __salt__["cmd.run"]("semodule -l").splitlines() - ret = {} - for line in mdata: - if not line.strip(): - continue - comps = line.split() - if len(comps) == 3: - ret[comps[0]] = {"Enabled": False, "Version": comps[1]} - else: - ret[comps[0]] = {"Enabled": True, "Version": comps[1]} + mdata = __salt__["cmd.run"]("semodule --list-modules=full").splitlines() + ret = {} + for line in mdata: + if not line or line.isspace(): + continue + comps = line.split() + if len(comps) == 4: + ret[comps[1]] = SeModDict(Enabled=False, Version=None) + else: + ret[comps[1]] = SeModDict(Enabled=True, Version=None) return ret -def _validate_filetype(filetype): - """ - .. versionadded:: 2017.7.0 - - Checks if the given filetype is a valid SELinux filetype - specification. Throws an SaltInvocationError if it isn't. - """ - if filetype not in _SELINUX_FILETYPES: - raise SaltInvocationError(f"Invalid filetype given: {filetype}") - return True - - def _parse_protocol_port(name, protocol, port): """ .. versionadded:: 2019.2.0 @@ -396,8 +411,8 @@ def _parse_protocol_port(name, protocol, port): name_parts = re.match(protocol_port_pattern, f"{protocol}/{port}") if not name_parts: raise SaltInvocationError( - 'Invalid name "{}" format and protocol and port not provided or invalid:' - ' "{}" "{}".'.format(name, protocol, port) + f'Invalid name "{name}" format and protocol and port not provided or invalid:' + f' "{protocol}" "{port}".' ) return name_parts.group(1), name_parts.group(2) @@ -411,7 +426,7 @@ def _context_dict_to_string(context): return "{sel_user}:{sel_role}:{sel_type}:{sel_level}".format(**context) -def _context_string_to_dict(context): +def _context_string_to_dict(context: str): """ .. versionadded:: 2017.7.0 @@ -419,10 +434,10 @@ def _context_string_to_dict(context): """ if not re.match("[^:]+:[^:]+:[^:]+:[^:]+$", context): raise SaltInvocationError( - "Invalid SELinux context string: {0}. " + f"Invalid SELinux context string: {context}. " + 'Expected "sel_user:sel_role:sel_type:sel_level"' ) - context_list = context.split(":", 3) + context_list = context.split(":", maxsplit=3) ret = {} for index, value in enumerate(["sel_user", "sel_role", "sel_type", "sel_level"]): ret[value] = context_list[index] @@ -437,8 +452,7 @@ def filetype_id_to_string(filetype="a"): human-readable version (which is also used in `semanage fcontext -l`). """ - _validate_filetype(filetype) - return _SELINUX_FILETYPES.get(filetype, "error") + return SeFileType.from_code(filetype).description def fcontext_get_policy( @@ -473,8 +487,7 @@ def fcontext_get_policy( salt '*' selinux.fcontext_get_policy my-policy """ - if filetype: - _validate_filetype(filetype) + se_file_type = SeFileType.from_code(filetype) if filetype else None re_spacer = "[ ]+" re_optional_spacer = "[ |\t]*" cmd_kwargs = { @@ -485,31 +498,33 @@ def fcontext_get_policy( "sel_role": "[^:]+", # se_role for file context is always object_r "sel_type": sel_type or "[^:]+", "sel_level": sel_level or "[^:]+", + "filetype": ( + "[[:alpha:] ]+" if se_file_type is None else se_file_type.description + ), } - cmd_kwargs["filetype"] = ( - "[[:alpha:] ]+" if filetype is None else filetype_id_to_string(filetype) - ) cmd = ( - "semanage fcontext -l | grep -E " + "semanage fcontext --list | grep -E " + "'^{filespec}{spacer}{filetype}{spacer}{sel_user}:{sel_role}:{sel_type}:{sel_level}{ospacer}$'".format( **cmd_kwargs ) ) current_entry_text = __salt__["cmd.shell"](cmd, ignore_retcode=True) - if current_entry_text == "": + if not current_entry_text: return None - parts = re.match( + if parts := re.match( r"^({filespec}) +([a-z ]+) (.*)$".format(**{"filespec": re.escape(name)}), current_entry_text, + ): + ret = { + "filespec": parts.group(1).strip(), + "filetype": parts.group(2).strip(), + } + ret.update(_context_string_to_dict(parts.group(3).strip())) + return ret + raise CommandExecutionError( + "Output from the fcontext command did not match the expected format" ) - ret = { - "filespec": parts.group(1).strip(), - "filetype": parts.group(2).strip(), - } - ret.update(_context_string_to_dict(parts.group(3).strip())) - - return ret def fcontext_add_policy( @@ -616,7 +631,7 @@ def _fcontext_add_or_delete_policy( if "add" == action: # need to use --modify if context for name file exists, otherwise ValueError filespec = re.escape(name) - cmd = f"semanage fcontext -l | grep -E '{filespec} '" + cmd = f"semanage fcontext --list | grep -E '{filespec} '" current_entry_text = __salt__["cmd.shell"](cmd, ignore_retcode=True) if current_entry_text != "": action = "modify" @@ -625,7 +640,7 @@ def _fcontext_add_or_delete_policy( # "semanage --ftype a" isn't valid on Centos 6, # don't pass --ftype since "a" is the default filetype. if filetype is not None and filetype != "a": - _validate_filetype(filetype) + SeFileType.from_code(filetype) cmd += f" --ftype {filetype}" if sel_type is not None: cmd += f" --type {sel_type}" @@ -762,7 +777,7 @@ def port_get_policy(name, sel_type=None, protocol=None, port=None): "port": port, } cmd = ( - "semanage port -l | grep -E " + "semanage port --list | grep -E " + "'^{sel_type}{spacer}{protocol}{spacer}((.*)*)[ ]{port}($|,)'".format( **cmd_kwargs ) @@ -771,12 +786,15 @@ def port_get_policy(name, sel_type=None, protocol=None, port=None): if port_policy == "": return None - parts = re.match(r"^(\w+)[ ]+(\w+)[ ]+([\d\-, ]+)", port_policy) - return { - "sel_type": parts.group(1).strip(), - "protocol": parts.group(2).strip(), - "port": parts.group(3).strip(), - } + if parts := re.match(r"^(\w+)[ ]+(\w+)[ ]+([\d\-, ]+)", port_policy): + return { + "sel_type": parts.group(1), + "protocol": parts.group(2), + "port": parts.group(3).strip(), + } + raise CommandExecutionError( + f"Port policy {port_policy} did not match expected format" + ) def port_add_policy(name, sel_type=None, protocol=None, port=None, sel_range=None): diff --git a/salt/states/pkg.py b/salt/states/pkg.py index 117e8797767..49b348c5706 100644 --- a/salt/states/pkg.py +++ b/salt/states/pkg.py @@ -3652,7 +3652,7 @@ def mod_aggregate(low, chunks, running): # the URI for sources low_pkgs_list = [ name if value is None else {name: value} - for name, values in pkgs.items() + for name, values in low_pkgs.items() for value in values ] low[pkg_type] = low_pkgs_list diff --git a/salt/states/selinux.py b/salt/states/selinux.py index 1c2aa4cd61a..d49b89713e8 100644 --- a/salt/states/selinux.py +++ b/salt/states/selinux.py @@ -24,6 +24,17 @@ execution module is available. """ +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any + +import salt.utils.data +import salt.utils.state +from salt.modules.selinux import SeBool + +LowChunk = dict[str, Any] + def __virtual__(): """ @@ -121,56 +132,152 @@ def mode(name): return ret -def boolean(name, value, persist=False): +def boolean( + name: str, + value: SeBool | None = None, + booleans: Iterable[str | dict[str, SeBool]] | dict[str, SeBool] | None = None, + persist=False, +): """ Set up an SELinux boolean name - The name of the boolean to set + The name of the boolean to set. + Note that this parameter is ignored if either "booleans" is passed. value The value to set on the boolean + You can install a different value for each boolean with the ``booleans`` + argument by including the version after the boolean name: + + .. code-block:: yaml + + "my selinux booleans": + selinux.boolean: + - value: true + - booleans: + - foo + - bar + - baz: false + + booleans + A list of boolean to set. All booleans + listed under ``booleans`` will be installed via a single command. + + .. code-block:: yaml + + mybooleans: + selinux.boolean: + - value: true + - booleans: + - foo + - bar + - baz + + value can be specified + in the ``booleans`` argument. For example: + + .. code-block:: yaml + + mybooleans: + selinux.boolean: + - value: true + - booleans: + - foo + - bar: false + - baz + + .. code-block:: yaml + + mybooleans: + selinux.boolean: + - booleans: + foo: true + bar: true + baz: false persist Defaults to False, set persist to true to make the boolean apply on a reboot """ - ret = {"name": name, "result": True, "comment": "", "changes": {}} - bools = __salt__["selinux.list_sebool"]() - if name not in bools: - ret["comment"] = f"Boolean {name} is not available" - ret["result"] = False - return ret - rvalue = _refine_value(value) - if rvalue is None: - ret["comment"] = f"{value} is not a valid value for the boolean" - ret["result"] = False + desired_bools = {} + if not booleans: + desired_bools[name] = value + elif isinstance(booleans, dict): + desired_bools.update(booleans) + else: + for sebool in booleans: + if isinstance(sebool, dict): + desired_bools.update(sebool) + else: + desired_bools[sebool] = value + ret = {"name": name, "result": False, "comment": "", "changes": {}} + existing_bools = __salt__["selinux.list_sebool"]() + to_change = {} + correct = [] + old = {} + for sebool_name, sebool_value in desired_bools.items(): + if sebool_name not in existing_bools: + ret["comment"] = f"Boolean {sebool_name} is not available" + return ret + rvalue = _refine_value(sebool_value) + if rvalue is None: + ret["comment"] = ( + f"{sebool_value} is not a valid value for the boolean {sebool_name}" + ) + return ret + state_same = existing_bools[sebool_name]["State"] == rvalue + default_same = existing_bools[sebool_name]["Default"] == rvalue + if state_same and (default_same or not persist): + correct.append(sebool_name) + else: + old[sebool_name] = ( + ( + existing_bools[sebool_name]["State"], + existing_bools[sebool_name]["Default"], + ) + if persist + else existing_bools[sebool_name]["State"] + ) + to_change[sebool_name] = rvalue + + comments = [] + if correct: + comments.append( + "The following booleans were in the correct state: " + + ", ".join(sorted(correct)) + ) + if not to_change: + ret["comment"] = "\n".join(comments) + ret["result"] = True return ret - state = bools[name]["State"] == rvalue - default = bools[name]["Default"] == rvalue + if persist: - if state and default: - ret["comment"] = "Boolean is in the correct state" - return ret + new = {key: (value, value) for key, value in to_change.items()} else: - if state: - ret["comment"] = "Boolean is in the correct state" - return ret - if __opts__["test"]: + new = to_change + diffs = salt.utils.data.compare_dicts(old, new) + if __opts__.get("test"): + comments.append( + "The following booleans would be changed: " + + ", ".join(sorted(to_change.keys())) + ) + ret["comment"] = "\n".join(comments) + ret["changes"] = diffs ret["result"] = None - ret["comment"] = f"Boolean {name} is set to be changed to {rvalue}" return ret - ret["result"] = __salt__["selinux.setsebool"](name, rvalue, persist) - if ret["result"]: - ret["comment"] = f"Boolean {name} has been set to {rvalue}" - ret["changes"].update({"State": {"old": bools[name]["State"], "new": rvalue}}) - if persist and not default: - ret["changes"].update( - {"Default": {"old": bools[name]["Default"], "new": rvalue}} - ) + mod_ret = __salt__["selinux.setsebools"](to_change, persist) + if mod_ret: + comments.append( + "The following booleans have been changed: " + + ", ".join(sorted(to_change.keys())) + ) + ret["comment"] = "\n".join(comments) + ret["changes"] = diffs + ret["result"] = True return ret - ret["comment"] = f"Failed to set the boolean {name} to {rvalue}" + ret["comment"] = f"Failed to set the following booleans: {desired_bools}" return ret @@ -227,9 +334,9 @@ def module(name, module_state="Enabled", version="any", **opts): installed_version = modules[name]["Version"] if not installed_version == version: ret["comment"] = ( - "Module version is {} and does not match " - "the desired version of {} or you are " - "using semodule >= 2.4".format(installed_version, version) + f"Module version is {installed_version} and does not match " + f"the desired version of {version} or you are " + "using semodule >= 2.4" ) ret["result"] = False return ret @@ -607,3 +714,61 @@ def port_policy_absent(name, sel_type=None, protocol=None, port=None): ) ret["changes"].update({"old": old_state, "new": new_state}) return ret + + +def mod_aggregate(low: LowChunk, chunks: Iterable[LowChunk], running: dict): + """ + The mod_aggregate function which looks up all selinux boolean in the available + low chunks and merges them into a single boolean ref in the present low data + """ + agg_enabled = [ + "boolean", + ] + if low.get("fun") not in agg_enabled: + return low + + booleans = {} + for chunk in chunks: + tag = salt.utils.state.gen_tag(chunk) + if tag in running: + # Already ran the boolean state, skip aggregation + continue + if chunk.get("state") == "selinux": + if "__agg__" in chunk: + continue + # Check for the same function + if chunk.get("fun") != low.get("fun"): + continue + # Check for the persist value + if chunk.get("persist") != low.get("persist"): + continue + if "booleans" in chunk: + _combine_booleans(chunk.get("value"), booleans, chunk["booleans"]) + chunk["__agg__"] = True + elif "name" in chunk: + booleans[chunk["name"]] = chunk.get("value") + chunk["__agg__"] = True + if booleans: + low_booleans = {} + if "booleans" in low: + _combine_booleans(low.get("value"), low_booleans, low["booleans"]) + else: + low_booleans[low["name"]] = low.get("value") + low_booleans.update(booleans) + low["booleans"] = low_booleans + return low + + +def _combine_booleans( + value: SeBool | None, + bools_dict: dict[str, SeBool], + additional_bools: dict[str, SeBool] | list[str | dict[str, SeBool]], +): + if isinstance(additional_bools, dict): + bools_dict.update(additional_bools) + return + for item in additional_bools: + if isinstance(item, dict): + bools_dict.update(item) + elif value is not None: + bools_dict[item] = value diff --git a/tests/pytests/unit/modules/test_selinux.py b/tests/pytests/unit/modules/test_selinux.py index 0ceba06a134..680d2837dba 100644 --- a/tests/pytests/unit/modules/test_selinux.py +++ b/tests/pytests/unit/modules/test_selinux.py @@ -401,7 +401,7 @@ def test_selinux_add_policy_regex(name, sel_type): ): selinux.fcontext_add_policy(name, sel_type=sel_type) filespec = re.escape(name) - expected_cmd_shell = f"semanage fcontext -l | grep -E '{filespec} '" + expected_cmd_shell = f"semanage fcontext --list | grep -E '{filespec} '" mock_cmd_shell.assert_called_once_with( expected_cmd_shell, ignore_retcode=True, @@ -433,7 +433,7 @@ def test_selinux_add_policy_shorter_path(name, sel_type): ): selinux.fcontext_add_policy(name, sel_type=sel_type) filespec = re.escape(name) - expected_cmd_shell = f"semanage fcontext -l | grep -E '{filespec} '" + expected_cmd_shell = f"semanage fcontext --list | grep -E '{filespec} '" mock_cmd_shell.assert_called_once_with( expected_cmd_shell, ignore_retcode=True, diff --git a/tests/pytests/unit/states/test_selinux.py b/tests/pytests/unit/states/test_selinux.py index 55fdbcd6027..54c380dd39a 100644 --- a/tests/pytests/unit/states/test_selinux.py +++ b/tests/pytests/unit/states/test_selinux.py @@ -3,12 +3,16 @@ :codeauthor: Jason Woods """ +import copy + import pytest import salt.states.selinux as selinux from tests.support.mock import MagicMock, patch pytestmark = [pytest.mark.skip_unless_on_linux] +_SEBOOL_ON_VALUES = [True, "true", "on", "1", 1] +_SEBOOL_OFF_VALUES = [False, "false", "off", "0", 0] @pytest.fixture @@ -70,55 +74,1194 @@ def test_mode(): assert selinux.mode("Permissive") == ret -def test_boolean(): - """ - Test to set up an SELinux boolean. - """ +def test_boolean_not_available(): name = "samba_create_home_dirs" - value = True ret = {"name": name, "changes": {}, "result": False, "comment": ""} mock_en = MagicMock(return_value=[]) with patch.dict(selinux.__salt__, {"selinux.list_sebool": mock_en}): comt = f"Boolean {name} is not available" ret.update({"comment": comt}) - assert selinux.boolean(name, value) == ret + assert selinux.boolean(name, True) == ret + + +def test_boolean_on_on(): + """ + Test setting an SELinux boolean with existing state on and default on. + """ + name = "samba_create_home_dirs" + ret = {"name": name, "changes": {}, "result": False, "comment": ""} - mock_bools = MagicMock(return_value={name: {"State": "on", "Default": "on"}}) - with patch.dict(selinux.__salt__, {"selinux.list_sebool": mock_bools}): - comt = "None is not a valid value for the boolean" + old = {"State": "on", "Default": "on"} + mock_bools = MagicMock(return_value={name: old}) + mock = MagicMock(return_value=True) + with patch.dict( + selinux.__salt__, + {"selinux.list_sebool": mock_bools, "selinux.setsebools": mock}, + ): + comt = f"None is not a valid value for the boolean {name}" ret.update({"comment": comt}) assert selinux.boolean(name, None) == ret - comt = "Boolean is in the correct state" + comt = f"The following booleans were in the correct state: {name}" ret.update({"comment": comt, "result": True}) - assert selinux.boolean(name, value, True) == ret + for value in _SEBOOL_ON_VALUES: + assert selinux.boolean(name, value) == ret + assert selinux.boolean(name, value, persist=True) == ret + + comt = f"The following booleans have been changed: {name}" + ret.update( + {"comment": comt, "changes": {name: {"old": old["State"], "new": "off"}}} + ) + for value in _SEBOOL_OFF_VALUES: + assert selinux.boolean(name, value) == ret + ret.update( + { + "changes": { + name: {"old": (old["State"], old["Default"]), "new": ("off", "off")} + } + } + ) + for value in _SEBOOL_OFF_VALUES: + assert selinux.boolean(name, value, persist=True) == ret + + with patch.dict(selinux.__opts__, {"test": True}): + comt = f"The following booleans would be changed: {name}" + ret.update( + { + "comment": comt, + "result": None, + "changes": {name: {"old": old["State"], "new": "off"}}, + } + ) + for value in _SEBOOL_OFF_VALUES: + assert selinux.boolean(name, value) == ret + ret.update( + { + "changes": { + name: { + "old": (old["State"], old["Default"]), + "new": ("off", "off"), + } + } + } + ) + for value in _SEBOOL_OFF_VALUES: + assert selinux.boolean(name, value, persist=True) == ret - comt = "Boolean is in the correct state" - ret.update({"comment": comt, "result": True}) - assert selinux.boolean(name, value) == ret - mock_bools = MagicMock(return_value={name: {"State": "off", "Default": "on"}}) - mock = MagicMock(side_effect=[True, False]) +def test_boolean_on_off(): + """ + Test setting an SELinux boolean with existing state on and default off. + """ + name = "samba_create_home_dirs" + ret = {"name": name, "changes": {}, "result": True, "comment": ""} + + old = {"State": "on", "Default": "off"} + mock_bools = MagicMock(return_value={name: old}) + mock = MagicMock(return_value=True) with patch.dict( selinux.__salt__, - {"selinux.list_sebool": mock_bools, "selinux.setsebool": mock}, + {"selinux.list_sebool": mock_bools, "selinux.setsebools": mock}, ): + comt = f"The following booleans have been changed: {name}" + ret.update( + { + "comment": comt, + "changes": { + name: {"old": (old["State"], old["Default"]), "new": ("on", "on")} + }, + } + ) + for value in _SEBOOL_ON_VALUES: + assert selinux.boolean(name, value, persist=True) == ret + + comt = f"The following booleans were in the correct state: {name}" + ret.update({"comment": comt, "changes": {}}) + for value in _SEBOOL_ON_VALUES: + assert selinux.boolean(name, value) == ret + + comt = f"The following booleans have been changed: {name}" + ret.update( + {"comment": comt, "changes": {name: {"old": old["State"], "new": "off"}}} + ) + for value in _SEBOOL_OFF_VALUES: + assert selinux.boolean(name, value) == ret + + comt = f"The following booleans have been changed: {name}" + ret.update( + { + "comment": comt, + "changes": { + name: {"old": (old["State"], old["Default"]), "new": ("off", "off")} + }, + } + ) + for value in _SEBOOL_OFF_VALUES: + assert selinux.boolean(name, value, persist=True) == ret + with patch.dict(selinux.__opts__, {"test": True}): - comt = "Boolean samba_create_home_dirs is set to be changed to on" - ret.update({"comment": comt, "result": None}) + comt = f"The following booleans would be changed: {name}" + ret.update( + { + "comment": comt, + "result": None, + "changes": {name: {"old": old["State"], "new": "off"}}, + } + ) + for value in _SEBOOL_OFF_VALUES: + assert selinux.boolean(name, value) == ret + + ret.update( + { + "changes": { + name: { + "old": (old["State"], old["Default"]), + "new": ("off", "off"), + } + } + } + ) + for value in _SEBOOL_OFF_VALUES: + assert selinux.boolean(name, value, persist=True) == ret + + +def test_boolean_off_off(): + """ + Test setting an SELinux boolean with existing state off and default off. + """ + name = "samba_create_home_dirs" + ret = {"name": name, "changes": {}, "result": True, "comment": ""} + old = {"State": "off", "Default": "off"} + mock_bools = MagicMock(return_value={name: {"State": "off", "Default": "off"}}) + mock = MagicMock(return_value=True) + with patch.dict( + selinux.__salt__, + {"selinux.list_sebool": mock_bools, "selinux.setsebools": mock}, + ): + comt = f"The following booleans have been changed: {name}" + ret.update( + {"comment": comt, "changes": {name: {"old": old["State"], "new": "on"}}} + ) + for value in _SEBOOL_ON_VALUES: assert selinux.boolean(name, value) == ret - with patch.dict(selinux.__opts__, {"test": False}): - comt = "Boolean samba_create_home_dirs has been set to on" - ret.update({"comment": comt, "result": True}) - ret.update({"changes": {"State": {"old": "off", "new": "on"}}}) + ret.update({"changes": {name: {"old": ("off", "off"), "new": ("on", "on")}}}) + for value in _SEBOOL_ON_VALUES: + assert selinux.boolean(name, value, persist=True) == ret + + comt = f"The following booleans were in the correct state: {name}" + ret.update({"comment": comt, "changes": {}}) + for value in _SEBOOL_OFF_VALUES: + assert selinux.boolean(name, value) == ret + assert selinux.boolean(name, value, persist=True) == ret + + with patch.dict(selinux.__opts__, {"test": True}): + comt = f"The following booleans would be changed: {name}" + ret.update( + { + "comment": comt, + "result": None, + "changes": {name: {"old": old["State"], "new": "on"}}, + } + ) + for value in _SEBOOL_ON_VALUES: + assert selinux.boolean(name, value) == ret + + ret.update( + {"changes": {name: {"old": ("off", "off"), "new": ("on", "on")}}} + ) + for value in _SEBOOL_ON_VALUES: + assert selinux.boolean(name, value, persist=True) == ret + + comt = f"The following booleans were in the correct state: {name}" + ret.update({"comment": comt, "result": True, "changes": {}}) + for value in _SEBOOL_OFF_VALUES: + assert selinux.boolean(name, value) == ret + assert selinux.boolean(name, value, persist=True) == ret + + +def test_boolean_off_on(): + """ + Test setting an SELinux boolean with existing state off and default on. + """ + name = "samba_create_home_dirs" + ret = {"name": name, "changes": {}, "result": True, "comment": ""} + old = {"State": "off", "Default": "on"} + mock_bools = MagicMock(return_value={name: old}) + mock = MagicMock(return_value=True) + with patch.dict( + selinux.__salt__, + {"selinux.list_sebool": mock_bools, "selinux.setsebools": mock}, + ): + comt = f"The following booleans have been changed: {name}" + ret.update( + { + "comment": comt, + "result": True, + "changes": {name: {"old": old["State"], "new": "on"}}, + } + ) + for value in _SEBOOL_ON_VALUES: + assert selinux.boolean(name, value) == ret + + comt = f"The following booleans have been changed: {name}" + ret.update( + { + "comment": comt, + "changes": { + name: {"old": (old["State"], old["Default"]), "new": ("on", "on")} + }, + } + ) + for value in _SEBOOL_ON_VALUES: + assert selinux.boolean(name, value, persist=True) == ret + + comt = f"The following booleans were in the correct state: {name}" + ret.update({"comment": comt, "changes": {}}) + for value in _SEBOOL_OFF_VALUES: + assert selinux.boolean(name, value) == ret + + comt = f"The following booleans have been changed: {name}" + ret.update( + { + "comment": comt, + "changes": { + name: {"old": (old["State"], old["Default"]), "new": ("off", "off")} + }, + } + ) + for value in _SEBOOL_OFF_VALUES: + assert selinux.boolean(name, value, persist=True) == ret + + with patch.dict(selinux.__opts__, {"test": True}): + comt = f"The following booleans would be changed: {name}" + ret.update( + { + "comment": comt, + "result": None, + "changes": {name: {"old": old["State"], "new": "on"}}, + } + ) + for value in _SEBOOL_ON_VALUES: + assert selinux.boolean(name, value) == ret + + ret.update( + { + "changes": { + name: { + "old": (old["State"], old["Default"]), + "new": ("on", "on"), + } + } + } + ) + for value in _SEBOOL_ON_VALUES: + assert selinux.boolean(name, value, persist=True) == ret + + +def test_boolean_failure(): + """ + Test setting an SELinux boolean with failure. + """ + name = "samba_create_home_dirs" + ret = {"name": name, "changes": {}, "result": True, "comment": ""} + old = {"State": "off", "Default": "off"} + mock_bools = MagicMock(return_value={name: old}) + mock = MagicMock(return_value=False) + with patch.dict( + selinux.__salt__, + {"selinux.list_sebool": mock_bools, "selinux.setsebools": mock}, + ): + ret.update({"result": False, "changes": {}}) + for value in _SEBOOL_ON_VALUES: + comt = f"Failed to set the following booleans: { {name: value} }" + ret.update({"comment": comt}) assert selinux.boolean(name, value) == ret + assert selinux.boolean(name, value, persist=True) == ret - comt = "Failed to set the boolean samba_create_home_dirs to on" - ret.update({"comment": comt, "result": False}) - ret.update({"changes": {}}) + old = {"State": "on", "Default": "on"} + mock_bools = MagicMock(return_value={name: old}) + mock = MagicMock(return_value=False) + with patch.dict( + selinux.__salt__, + {"selinux.list_sebool": mock_bools, "selinux.setsebools": mock}, + ): + ret.update({"result": False, "changes": {}}) + for value in _SEBOOL_OFF_VALUES: + comt = f"Failed to set the following booleans: { {name: value} }" + ret.update({"comment": comt}) assert selinux.boolean(name, value) == ret + assert selinux.boolean(name, value, persist=True) == ret + + +def test_boolean_booleans(): + """ + Test to set up multiple SELinux booleans with a single state. + """ + name = "mybooleans" + se_bool_names = [ + "abrt_anon_write", + "abrt_handle_event", + "domain_can_ptrace", + "ftpd_anon_write", + "ftpd_home_dir", + "ftpd_use_cifs", + "nis_enabled", + "samba_create_home_dirs", + "tftp_anon_write", + "tftp_home_dir", + ] + on_values_dict = { + "abrt_anon_write": True, + "abrt_handle_event": True, + "domain_can_ptrace": "True", + "ftpd_anon_write": "true", + "ftpd_home_dir": "on", + "ftpd_use_cifs": "1", + "nis_enabled": "1", + "samba_create_home_dirs": 1, + "tftp_home_dir": 1, + "tftp_anon_write": 1, + } + on_values_list_of_dict = [ + {"abrt_anon_write": True}, + {"abrt_handle_event": True}, + {"domain_can_ptrace": "True"}, + {"ftpd_anon_write": "true"}, + {"ftpd_home_dir": "on"}, + {"ftpd_use_cifs": "1"}, + {"nis_enabled": "1"}, + {"samba_create_home_dirs": 1}, + {"tftp_home_dir": 1}, + {"tftp_anon_write": 1}, + ] + on_values_list_of_str_and_dict = [ + "abrt_anon_write", + {"abrt_handle_event": True}, + {"domain_can_ptrace": "true"}, + {"ftpd_anon_write": "on"}, + "ftpd_home_dir", + {"ftpd_use_cifs": "1"}, + {"nis_enabled": 1}, + "samba_create_home_dirs", + "tftp_home_dir", + "tftp_anon_write", + ] + off_values_dict = { + "abrt_anon_write": False, + "abrt_handle_event": False, + "domain_can_ptrace": "False", + "ftpd_anon_write": "false", + "ftpd_home_dir": "off", + "ftpd_use_cifs": "0", + "nis_enabled": "0", + "samba_create_home_dirs": 0, + "tftp_home_dir": 0, + "tftp_anon_write": 0, + } + off_values_list_of_dict = [ + {"abrt_anon_write": False}, + {"abrt_handle_event": False}, + {"domain_can_ptrace": "False"}, + {"ftpd_anon_write": "false"}, + {"ftpd_home_dir": "off"}, + {"ftpd_use_cifs": "0"}, + {"nis_enabled": "0"}, + {"samba_create_home_dirs": 0}, + {"tftp_home_dir": 0}, + {"tftp_anon_write": 0}, + ] + off_values_list_of_str_and_dict = [ + "abrt_anon_write", + {"abrt_handle_event": False}, + {"domain_can_ptrace": "false"}, + {"ftpd_anon_write": "off"}, + "ftpd_home_dir", + {"ftpd_use_cifs": "0"}, + {"nis_enabled": 0}, + "samba_create_home_dirs", + "tftp_home_dir", + "tftp_anon_write", + ] + on_values_for_mixed = { + "abrt_anon_write": True, + "abrt_handle_event": "true", + "domain_can_ptrace": "on", + "ftpd_anon_write": "1", + "ftpd_home_dir": 1, + } + off_values_for_mixed = { + "ftpd_use_cifs": False, + "nis_enabled": "false", + "samba_create_home_dirs": "off", + "tftp_anon_write": "0", + "tftp_home_dir": 0, + } + mixed_values_dict = on_values_for_mixed | off_values_for_mixed + mixed_values_list = [{key: value} for key, value in mixed_values_dict.items()] + ret = {"name": name, "changes": {}, "result": False, "comment": ""} + + mock_en = MagicMock(return_value=[]) + with patch.dict(selinux.__salt__, {"selinux.list_sebool": mock_en}): + comt = f"Boolean {se_bool_names[0]} is not available" + ret.update({"comment": comt}) + assert selinux.boolean(name, True, booleans=se_bool_names) == ret + + old = {"State": "on", "Default": "on"} + mock_bools = MagicMock( + return_value={se_bool_name: old for se_bool_name in se_bool_names} + ) + mock = MagicMock(return_value=True) + with patch.dict( + selinux.__salt__, + {"selinux.list_sebool": mock_bools, "selinux.setsebools": mock}, + ): + # value not set; booleans as a list of str + comt = "None is not a valid value for the boolean abrt_anon_write" + ret.update({"comment": comt, "result": False, "changes": {}}) + assert selinux.boolean(name, booleans=se_bool_names) == ret + assert selinux.boolean(name, None, booleans=se_bool_names) == ret + # value not set; booleans as a list of mixed str and dict + assert selinux.boolean(name, booleans=on_values_list_of_str_and_dict) == ret + + comt = f"The following booleans were in the correct state: {', '.join(se_bool_names)}" + ret.update({"comment": comt, "result": True}) + # value set; booleans as a list of str + assert selinux.boolean(name, True, booleans=se_bool_names) == ret + # value set; booleans as a list of mixed str and dict + assert ( + selinux.boolean(name, True, booleans=on_values_list_of_str_and_dict) == ret + ) + # value not set; booleans as a list of dict + assert selinux.boolean(name, booleans=on_values_list_of_dict) == ret + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=on_values_dict) == ret + # value set differently than booleans as a dict + assert selinux.boolean(name, False, booleans=on_values_dict) == ret + # persist; value set; booleans as a list of mixed str and dict + assert ( + selinux.boolean( + name, True, booleans=on_values_list_of_str_and_dict, persist=True + ) + == ret + ) + # persist; value not set; booleans as dict + assert selinux.boolean(name, booleans=on_values_dict, persist=True) == ret + + comt = f"The following booleans have been changed: {', '.join(se_bool_names)}" + ret.update( + { + "comment": comt, + "result": True, + "changes": { + se_bool_name: {"old": old["State"], "new": "off"} + for se_bool_name in se_bool_names + }, + } + ) + # value set; booleans as a list of str + assert selinux.boolean(name, False, booleans=se_bool_names) == ret + # value set; booleans as a list of mixed str and dict + assert ( + selinux.boolean(name, False, booleans=off_values_list_of_str_and_dict) + == ret + ) + # value not set; booleans as a list of dict + assert selinux.boolean(name, booleans=off_values_list_of_dict) == ret + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=off_values_dict) == ret + # value set differently than booleans as a dict + assert selinux.boolean(name, True, booleans=off_values_dict) == ret + + # persist; value set; booleans as a list of mixed str and dict + ret.update( + { + "changes": { + se_bool_name: { + "old": (old["State"], old["Default"]), + "new": ("off", "off"), + } + for se_bool_name in se_bool_names + } + } + ) + assert ( + selinux.boolean( + name, False, booleans=off_values_list_of_str_and_dict, persist=True + ) + == ret + ) + # persist; value not set; booleans as dict + assert selinux.boolean(name, persist=True, booleans=off_values_dict) == ret + + comt = ( + f"The following booleans were in the correct state: {', '.join(on_values_for_mixed)}\n" + + f"The following booleans have been changed: {', '.join(off_values_for_mixed)}" + ) + ret.update( + { + "comment": comt, + "result": True, + "changes": { + se_bool_name: {"old": old["State"], "new": "off"} + for se_bool_name in off_values_for_mixed + }, + } + ) + + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=mixed_values_dict) == ret + # value set on with dict of mixed values + assert selinux.boolean(name, True, booleans=mixed_values_dict) == ret + # value set off with dict of mixed values + assert selinux.boolean(name, False, booleans=mixed_values_dict) == ret + # value not set; booleans as a list of mixed values + assert selinux.boolean(name, booleans=mixed_values_list) == ret + # value set on with list of mixed values + assert selinux.boolean(name, True, booleans=mixed_values_list) == ret + # value set off with list of mixed values + assert selinux.boolean(name, False, booleans=mixed_values_list) == ret + + ret.update( + { + "changes": { + se_bool_name: { + "old": (old["State"], old["Default"]), + "new": ("off", "off"), + } + for se_bool_name in off_values_for_mixed + } + } + ) + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=mixed_values_dict, persist=True) == ret + # value set on with dict of mixed values + assert ( + selinux.boolean(name, True, booleans=mixed_values_dict, persist=True) == ret + ) + # value set off with dict of mixed values + assert ( + selinux.boolean(name, False, booleans=mixed_values_dict, persist=True) + == ret + ) + # value not set; booleans as a list of mixed values + assert selinux.boolean(name, booleans=mixed_values_list, persist=True) == ret + # value set on with list of mixed values + assert ( + selinux.boolean(name, True, booleans=mixed_values_list, persist=True) == ret + ) + # value set off with list of mixed values + assert ( + selinux.boolean(name, False, booleans=mixed_values_list, persist=True) + == ret + ) + + old = {"State": "off", "Default": "on"} + mock_bools = MagicMock( + return_value={se_bool_name: old for se_bool_name in se_bool_names} + ) + mock = MagicMock(return_value=True) + with patch.dict( + selinux.__salt__, + {"selinux.list_sebool": mock_bools, "selinux.setsebools": mock}, + ): + comt = f"The following booleans have been changed: {', '.join(se_bool_names)}" + ret.update( + { + "comment": comt, + "result": True, + "changes": { + se_bool_name: {"old": old["State"], "new": "on"} + for se_bool_name in se_bool_names + }, + } + ) + # value set; booleans as a list of str + assert selinux.boolean(name, True, booleans=se_bool_names) == ret + # value set; booleans as a list of mixed str and dict + assert ( + selinux.boolean(name, True, booleans=on_values_list_of_str_and_dict) == ret + ) + # value not set; booleans as a list of dict + assert selinux.boolean(name, booleans=on_values_list_of_dict) == ret + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=on_values_dict) == ret + # value set differently than booleans as a dict + assert selinux.boolean(name, False, booleans=on_values_dict) == ret + + # persist; value set; booleans as a list of mixed str and dict + ret.update( + { + "changes": { + se_bool_name: { + "old": (old["State"], old["Default"]), + "new": ("on", "on"), + } + for se_bool_name in se_bool_names + } + } + ) + assert ( + selinux.boolean( + name, True, booleans=on_values_list_of_str_and_dict, persist=True + ) + == ret + ) + # persist; value not set; booleans as dict + assert selinux.boolean(name, persist=True, booleans=on_values_dict) == ret + + with patch.dict(selinux.__opts__, {"test": True}): + comt = ( + f"The following booleans would be changed: {', '.join(se_bool_names)}" + ) + ret.update( + { + "comment": comt, + "result": None, + "changes": { + se_bool_name: {"old": old["State"], "new": "on"} + for se_bool_name in se_bool_names + }, + } + ) + + assert selinux.boolean(name, True, booleans=se_bool_names) == ret + # value set; booleans as a list of mixed str and dict + assert ( + selinux.boolean(name, True, booleans=on_values_list_of_str_and_dict) + == ret + ) + # value not set; booleans as a list of dict + assert selinux.boolean(name, booleans=on_values_list_of_dict) == ret + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=on_values_dict) == ret + # value set differently than booleans as a dict + assert selinux.boolean(name, False, booleans=on_values_dict) == ret + + # persist; value set; booleans as a list of mixed str and dict + ret.update( + { + "changes": { + se_bool_name: { + "old": (old["State"], old["Default"]), + "new": ("on", "on"), + } + for se_bool_name in se_bool_names + } + } + ) + assert ( + selinux.boolean( + name, + True, + persist=True, + booleans=on_values_list_of_str_and_dict, + ) + == ret + ) + # persist; value not set; booleans as dict + assert selinux.boolean(name, persist=True, booleans=on_values_dict) == ret + + comt = ( + f"The following booleans were in the correct state: {', '.join(off_values_for_mixed)}\n" + + f"The following booleans have been changed: {', '.join(on_values_for_mixed)}" + ) + ret.update( + { + "comment": comt, + "result": True, + "changes": { + se_bool_name: {"old": old["State"], "new": "on"} + for se_bool_name in on_values_for_mixed + }, + } + ) + + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=mixed_values_dict) == ret + # value set on with dict of mixed values + assert selinux.boolean(name, True, booleans=mixed_values_dict) == ret + # value set off with dict of mixed values + assert selinux.boolean(name, False, booleans=mixed_values_dict) == ret + # value not set; booleans as a list of mixed values + assert selinux.boolean(name, booleans=mixed_values_list) == ret + # value set on with list of mixed values + assert selinux.boolean(name, True, booleans=mixed_values_list) == ret + # value set off with list of mixed values + assert selinux.boolean(name, False, booleans=mixed_values_list) == ret + + comt = ( + f"The following booleans have been changed: {', '.join(mixed_values_dict)}" + ) + ret.update( + { + "comment": comt, + "changes": { + se_bool_name: { + "old": (old["State"], old["Default"]), + "new": ("off", "off"), + } + for se_bool_name in off_values_for_mixed + } + | { + se_bool_name: { + "old": (old["State"], old["Default"]), + "new": ("on", "on"), + } + for se_bool_name in on_values_for_mixed + }, + } + ) + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=mixed_values_dict, persist=True) == ret + # value set on with dict of mixed values + assert ( + selinux.boolean(name, True, booleans=mixed_values_dict, persist=True) == ret + ) + # value set off with dict of mixed values + assert ( + selinux.boolean(name, False, booleans=mixed_values_dict, persist=True) + == ret + ) + # value not set; booleans as a list of mixed values + assert selinux.boolean(name, booleans=mixed_values_list, persist=True) == ret + # value set on with list of mixed values + assert ( + selinux.boolean(name, True, booleans=mixed_values_list, persist=True) == ret + ) + # value set off with list of mixed values + assert ( + selinux.boolean(name, False, booleans=mixed_values_list, persist=True) + == ret + ) + + old = {"State": "off", "Default": "off"} + mock_bools = MagicMock( + return_value={se_bool_name: old for se_bool_name in se_bool_names} + ) + mock = MagicMock(return_value=True) + with patch.dict( + selinux.__salt__, + {"selinux.list_sebool": mock_bools, "selinux.setsebools": mock}, + ): + # value not set; booleans as a list of str + comt = "None is not a valid value for the boolean abrt_anon_write" + ret.update({"comment": comt, "result": False, "changes": {}}) + assert selinux.boolean(name, booleans=se_bool_names) == ret + assert selinux.boolean(name, None, booleans=se_bool_names) == ret + # value not set; booleans as a list of mixed str and dict + assert selinux.boolean(name, booleans=on_values_list_of_str_and_dict) == ret + + comt = f"The following booleans were in the correct state: {', '.join(se_bool_names)}" + ret.update({"comment": comt, "result": True}) + # value set; booleans as a list of str + assert selinux.boolean(name, False, booleans=se_bool_names) == ret + # value set; booleans as a list of mixed str and dict + assert ( + selinux.boolean(name, False, booleans=off_values_list_of_str_and_dict) + == ret + ) + # value not set; booleans as a list of dict + assert selinux.boolean(name, booleans=off_values_list_of_dict) == ret + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=off_values_dict) == ret + # value set differently than booleans as a dict + assert selinux.boolean(name, True, booleans=off_values_dict) == ret + + # persist; value set; booleans as a list of mixed str and dict + assert ( + selinux.boolean( + name, + False, + persist=True, + booleans=off_values_list_of_str_and_dict, + ) + == ret + ) + # persist; value not set; booleans as dict + assert selinux.boolean(name, persist=True, booleans=off_values_dict) == ret + + comt = ( + f"The following booleans were in the correct state: {', '.join(off_values_for_mixed)}\n" + + f"The following booleans have been changed: {', '.join(on_values_for_mixed)}" + ) + ret.update( + { + "comment": comt, + "result": True, + "changes": { + se_bool_name: {"old": old["State"], "new": "on"} + for se_bool_name in on_values_for_mixed + }, + } + ) + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=mixed_values_dict) == ret + # value set on with dict of mixed values + assert selinux.boolean(name, True, booleans=mixed_values_dict) == ret + # value set off with dict of mixed values + assert selinux.boolean(name, False, booleans=mixed_values_dict) == ret + # value not set; booleans as a list of mixed values + assert selinux.boolean(name, booleans=mixed_values_list) == ret + # value set on with list of mixed values + assert selinux.boolean(name, True, booleans=mixed_values_list) == ret + # value set off with list of mixed values + assert selinux.boolean(name, False, booleans=mixed_values_list) == ret + + ret.update( + { + "changes": { + se_bool_name: { + "old": (old["State"], old["Default"]), + "new": ("on", "on"), + } + for se_bool_name in on_values_for_mixed + } + } + ) + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=mixed_values_dict, persist=True) == ret + # value set on with dict of mixed values + assert ( + selinux.boolean(name, True, booleans=mixed_values_dict, persist=True) == ret + ) + # value set off with dict of mixed values + assert ( + selinux.boolean(name, False, booleans=mixed_values_dict, persist=True) + == ret + ) + # value not set; booleans as a list of mixed values + assert selinux.boolean(name, booleans=mixed_values_list, persist=True) == ret + # value set on with list of mixed values + assert ( + selinux.boolean(name, True, booleans=mixed_values_list, persist=True) == ret + ) + # value set off with list of mixed values + assert ( + selinux.boolean(name, False, booleans=mixed_values_list, persist=True) + == ret + ) + + old = {"State": "off", "Default": "on"} + mock_bools = MagicMock( + return_value={se_bool_name: old for se_bool_name in se_bool_names} + ) + mock = MagicMock(return_value=True) + with patch.dict( + selinux.__salt__, + {"selinux.list_sebool": mock_bools, "selinux.setsebools": mock}, + ): + comt = f"The following booleans have been changed: {', '.join(se_bool_names)}" + ret.update( + { + "comment": comt, + "result": True, + "changes": { + se_bool_name: {"old": old["State"], "new": "on"} + for se_bool_name in se_bool_names + }, + } + ) + # value set; booleans as a list of str + assert selinux.boolean(name, True, booleans=se_bool_names) == ret + # value set; booleans as a list of mixed str and dict + assert ( + selinux.boolean(name, True, booleans=on_values_list_of_str_and_dict) == ret + ) + # value not set; booleans as a list + assert selinux.boolean(name, booleans=on_values_list_of_dict) == ret + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=on_values_dict) == ret + # value set differently than booleans as a dict + assert selinux.boolean(name, False, booleans=on_values_dict) == ret + + # persist; value set; booleans as a list of mixed str and dict + ret.update( + { + "changes": { + se_bool_name: { + "old": (old["State"], old["Default"]), + "new": ("on", "on"), + } + for se_bool_name in se_bool_names + } + } + ) + assert ( + selinux.boolean( + name, + True, + persist=True, + booleans=on_values_list_of_str_and_dict, + ) + == ret + ) + # persist; value not set; booleans as dict + assert selinux.boolean(name, persist=True, booleans=on_values_dict) == ret + + with patch.dict(selinux.__opts__, {"test": True}): + comt = ( + f"The following booleans would be changed: {', '.join(se_bool_names)}" + ) + ret.update( + { + "comment": comt, + "result": None, + "changes": { + name: {"old": old["State"], "new": "on"} + for name in se_bool_names + }, + } + ) + + assert selinux.boolean(name, True, booleans=se_bool_names) == ret + # value set; booleans as a list of mixed str and dict + assert ( + selinux.boolean(name, True, booleans=on_values_list_of_str_and_dict) + == ret + ) + # value not set; booleans as a list of dict + assert selinux.boolean(name, booleans=on_values_list_of_dict) == ret + # value not set; booleans as a dict + assert selinux.boolean(name, booleans=on_values_dict) == ret + # value set differently than booleans as a dict + assert selinux.boolean(name, False, booleans=on_values_dict) == ret + + # persist; value set; booleans as a list of mixed str and dict + ret.update( + { + "changes": { + se_bool_name: { + "old": (old["State"], old["Default"]), + "new": ("on", "on"), + } + for se_bool_name in se_bool_names + } + } + ) + assert ( + selinux.boolean( + name, + True, + booleans=on_values_list_of_str_and_dict, + persist=True, + ) + == ret + ) + # persist; value not set; booleans as dict + assert selinux.boolean(name, persist=True, booleans=on_values_dict) == ret + + +def test_mod_aggregate(): + """ + Test mod_aggregate function + """ + chunks = [ + { + "state": "file", + "name": "/tmp/install-vim", + "__sls__": "47628", + "__env__": "base", + "__id__": "/tmp/install-vim", + "order": 10000, + "fun": "managed", + }, + { + "state": "file", + "name": "/tmp/install-tmux", + "__sls__": "47628", + "__env__": "base", + "__id__": "/tmp/install-tmux", + "order": 10001, + "fun": "managed", + }, + { + "state": "selinux", + "name": "other_bools_list", + "__sls__": "47628", + "__env __": "base", + "__id__": "other_bools_list", + "booleans": [ + "domain_can_ptrace", + "ftpd_anon_write", + {"samba_create_home_dirs": "on"}, + ], + "value": "off", + "aggregate": True, + "order": 10002, + "fun": "boolean", + }, + { + "state": "selinux", + "name": "other_bools_dict", + "__sls__": "47628", + "__env __": "base", + "__id__": "other_bools_dict", + "booleans": { + "ftpd_home_dir": False, + "ftpd_use_cifs": True, + }, + "value": "off", + "aggregate": True, + "order": 10003, + "fun": "boolean", + }, + { + "state": "selinux", + # override previous value + "name": "ftpd_home_dir", + "value": "on", + "__sls__": "47628", + "__env__": "base", + "__id__": "ftpd_home_dir", + "aggregate": True, + "order": 10004, + "fun": "boolean", + }, + { + "state": "selinux", + "name": "ftpd_connect_all_unreserved", + "value": "False", + "__sls__": "47628", + "__env__": "base", + "__id__": "ftpd_connect_all_unreserved", + "require": ["/tmp/install-vim"], + "order": 10004, + "fun": "boolean", + }, + { + "state": "selinux", + "name": "nis_enabled", + "__sls__": "47628", + "__env__": "base", + "__id__": "nis_enabled", + "require": ["/tmp/install-tmux"], + "value": "on", + "order": 10005, + "fun": "boolean", + }, + ] + + running = { + "file_|-/tmp/install-vim_| -/tmp/install-vim_|-managed": { + "changes": {}, + "comment": "File /tmp/install-vim exists with proper permissions. No changes made.", + "name": "/tmp/install-vim", + "result": True, + "__sls__": "47628", + "__run_num__": 0, + "start_time": "18:41:20.987275", + "duration": 5.833, + "__id__": "/tmp/install-vim", + }, + "file_|-/tmp/install-tmux_|-/tmp/install-tmux_|-managed": { + "changes": {}, + "comment": "File /tmp/install-tmux exists with proper permissions. No changes made.", + "name": "/tmp/install-tmux", + "result": True, + "__sls__": "47628", + "__run_num__": 1, + "start_time": "18:41:20.993258", + "duration": 1.263, + "__id__": "/tmp/install-tmux", + }, + } + + expected = { + "__id__": "agg_root_selinux_bool", + "state": "selinux", + "name": "abrt_anon_write", + "value": "on", + "aggregate": True, + "fun": "boolean", + "booleans": { + "abrt_anon_write": "on", + "domain_can_ptrace": "off", + "ftpd_anon_write": "off", + "samba_create_home_dirs": "on", + "ftpd_home_dir": "on", + "ftpd_use_cifs": True, + "ftpd_connect_all_unreserved": "False", + "nis_enabled": "on", + }, + } + low_single = { + "__id__": "agg_root_selinux_bool", + "state": "selinux", + "name": "abrt_anon_write", + "value": "on", + "aggregate": True, + "fun": "boolean", + } + res = selinux.mod_aggregate(low_single, copy.deepcopy(chunks), running) + assert res == expected + + expected = { + "__id__": "agg_root_selinux_bool", + "booleans": { + "abrt_anon_write": False, + "abrt_handle_event": False, + "domain_can_ptrace": "off", + "ftpd_anon_write": "off", + "samba_create_home_dirs": "on", + "ftpd_home_dir": "on", + "ftpd_use_cifs": True, + "ftpd_connect_all_unreserved": "False", + "nis_enabled": "on", + }, + "name": "agg_root_selinux_bool", + "value": False, + "fun": "boolean", + "aggregate": True, + "state": "selinux", + } + low_bool_list = { + "__id__": "agg_root_selinux_bool", + "state": "selinux", + "name": "agg_root_selinux_bool", + "value": False, + "booleans": [ + "abrt_anon_write", + "abrt_handle_event", + ], + "aggregate": True, + "fun": "boolean", + } + res = selinux.mod_aggregate(low_bool_list, copy.deepcopy(chunks), running) + assert res == expected + + expected = { + "__id__": "agg_root_selinux_bool", + "booleans": { + "abrt_anon_write": True, + "abrt_handle_event": False, + "domain_can_ptrace": "off", + "ftpd_anon_write": "off", + "samba_create_home_dirs": "on", + "ftpd_home_dir": "on", + "ftpd_use_cifs": True, + "ftpd_connect_all_unreserved": "False", + "nis_enabled": "on", + }, + "name": "agg_root_selinux_bool", + "fun": "boolean", + "aggregate": True, + "state": "selinux", + } + low_bool_dict = { + "__id__": "agg_root_selinux_bool", + "state": "selinux", + "name": "agg_root_selinux_bool", + "booleans": { + "abrt_anon_write": True, + "abrt_handle_event": False, + }, + "aggregate": True, + "fun": "boolean", + } + res = selinux.mod_aggregate(low_bool_dict, copy.deepcopy(chunks), running) + assert res == expected def test_port_policy_present():