diff --git a/salt/pillar/__init__.py b/salt/pillar/__init__.py index d986c4393baf..bd05b8985978 100644 --- a/salt/pillar/__init__.py +++ b/salt/pillar/__init__.py @@ -956,54 +956,58 @@ def render_pillar(self, matches, errors=None): Extract the sls pillar files from the matches and render them into the pillar """ - pillar = copy.copy(self.pillar_override) - if errors is None: - errors = [] - for saltenv, pstates in matches.items(): - pstatefiles = [] - mods = {} - for sls_match in pstates: - matched_pstates = [] - try: - matched_pstates = fnmatch.filter(self.avail[saltenv], sls_match) - except KeyError: - errors.extend( - [ - "No matching pillar environment for environment " - "'{}' found".format(saltenv) - ] - ) - if matched_pstates: - pstatefiles.extend(matched_pstates) - else: - pstatefiles.append(sls_match) - - for sls in pstatefiles: - pstate, mods, err = self.render_pstate(sls, saltenv, mods) - - if err: - errors += err - - if pstate is not None: - if not isinstance(pstate, dict): - log.error( - "The rendered pillar sls file, '%s' state did " - "not return the expected data format. This is " - "a sign of a malformed pillar sls file. Returned " - "errors: %s", - sls, - ", ".join([f"'{e}'" for e in errors]), + _token = salt.utils.secret.mask_pillar.set(False) + try: + pillar = copy.copy(self.pillar_override) + if errors is None: + errors = [] + for saltenv, pstates in matches.items(): + pstatefiles = [] + mods = {} + for sls_match in pstates: + matched_pstates = [] + try: + matched_pstates = fnmatch.filter(self.avail[saltenv], sls_match) + except KeyError: + errors.extend( + [ + "No matching pillar environment for environment " + "'{}' found".format(saltenv) + ] + ) + if matched_pstates: + pstatefiles.extend(matched_pstates) + else: + pstatefiles.append(sls_match) + + for sls in pstatefiles: + pstate, mods, err = self.render_pstate(sls, saltenv, mods) + + if err: + errors += err + + if pstate is not None: + if not isinstance(pstate, dict): + log.error( + "The rendered pillar sls file, '%s' state did " + "not return the expected data format. This is " + "a sign of a malformed pillar sls file. Returned " + "errors: %s", + sls, + ", ".join([f"'{e}'" for e in errors]), + ) + continue + pillar = salt.utils.dictupdate.merge( + pillar, + pstate, + self.merge_strategy, + self.opts.get("renderer", "yaml"), + self.opts.get("pillar_merge_lists", False), ) - continue - pillar = salt.utils.dictupdate.merge( - pillar, - pstate, - self.merge_strategy, - self.opts.get("renderer", "yaml"), - self.opts.get("pillar_merge_lists", False), - ) - return pillar, errors + return pillar, errors + finally: + salt.utils.secret.mask_pillar.reset(_token) def _external_pillar_data(self, pillar, val, key): """ diff --git a/tests/pytests/functional/pillar/test_pillar_masking.py b/tests/pytests/functional/pillar/test_pillar_masking.py new file mode 100644 index 000000000000..8eac36baf003 --- /dev/null +++ b/tests/pytests/functional/pillar/test_pillar_masking.py @@ -0,0 +1,52 @@ +""" +Functional tests for pillar masking behaviour: render_pillar() must set +mask_pillar=False so that pillar.get() calls inside pillar SLS renderers +return plain values instead of **********-redacted ones. +""" + +import salt.loader +import salt.pillar +import salt.utils.secret + + +def test_render_pillar_py_renderer_sees_unmasked_values( + temp_salt_master, temp_salt_minion +): + """Pillar SLS files using the #!py renderer must receive plain pillar + values from pillar.get(), not **********-redacted ones. + + Without the fix, render_pillar() never sets mask_pillar=False. The + Python renderer calls mod.run() directly with no render_tmpl() wrapper, + so mask_pillar stays True and pillar.get() calls serial(), replacing all + string values (even in plain Python lists) with **********. + """ + py_pillar_sls = """\ +#!py +def run(): + # Without render_pillar() setting mask_pillar=False, pillar.get() + # calls serial() and returns ['**********', ...] for list values. + return {"derived_list": __salt__["pillar.get"]("base_list")} +""" + top_sls = """ +base: + '*': + - py_pillar +""" + opts = temp_salt_master.config.copy() + # plain Python list — serial() redacts string elements when mask_pillar=True + # even without any MaskedDict/MaskedList wrapping. + opts["pillar"] = {"base_list": ["a", "b", "c"]} + + with temp_salt_master.pillar_tree.base.temp_file( + "top.sls", top_sls + ), temp_salt_master.pillar_tree.base.temp_file("py_pillar.sls", py_pillar_sls): + grains = salt.loader.grains(opts) + pillar_obj = salt.pillar.Pillar(opts, grains, temp_salt_minion.id, "base") + result = pillar_obj.compile_pillar() + + assert result.get("derived_list") == ["a", "b", "c"], ( + f"Expected plain list values but got: {result.get('derived_list')!r}. " + "render_pillar() must set mask_pillar=False so that pillar.get() " + "inside #!py SLS files returns expose()d values instead of " + "serial()-redacted ones." + )