mirror of
https://github.com/saltstack/salt.git
synced 2025-04-16 09:40:20 +00:00
Allow policy parameters from pillar
This commit is contained in:
parent
d56d6f7025
commit
477c2c5ad1
9 changed files with 582 additions and 31 deletions
1
changelog/43287.added
Normal file
1
changelog/43287.added
Normal file
|
@ -0,0 +1 @@
|
|||
Added pillar templating to vault policies
|
|
@ -130,6 +130,12 @@ Functions to interact with Hashicorp Vault.
|
|||
values, eg ``my-policies/{grains[os]}``. ``{minion}`` is shorthand for
|
||||
``grains[id]``, eg ``saltstack/minion/{minion}``.
|
||||
|
||||
.. versionadded:: 3006
|
||||
|
||||
Policies can be templated with pillar values as well: ``salt_role_{pillar[roles]}``
|
||||
Make sure to only reference pillars that are not sourced from Vault since the latter
|
||||
ones might be unavailable during policy rendering.
|
||||
|
||||
.. important::
|
||||
|
||||
See :ref:`Is Targeting using Grain Data Secure?
|
||||
|
@ -160,6 +166,29 @@ Functions to interact with Hashicorp Vault.
|
|||
Optional. If policies is not configured, ``saltstack/minions`` and
|
||||
``saltstack/{minion}`` are used as defaults.
|
||||
|
||||
policies_refresh_pillar
|
||||
Whether to refresh the pillar data when rendering templated policies.
|
||||
When unset (=null/None), will only refresh when the cached data
|
||||
is unavailable, boolean values force one behavior always.
|
||||
|
||||
.. note::
|
||||
|
||||
Using cached pillar data only (policies_refresh_pillar=False)
|
||||
might cause the policies to be out of sync. If there is no cached pillar
|
||||
data available for the minion, pillar templates will fail to render at all.
|
||||
|
||||
If you use pillar values for templating policies and do not disable
|
||||
refreshing pillar data, make sure the relevant values are not sourced
|
||||
from Vault (ext_pillar, sdb) or from a pillar sls file that uses the vault
|
||||
execution module. Although this will often work when cached pillar data is
|
||||
available, if the master needs to compile the pillar data during policy rendering,
|
||||
all Vault modules will be broken to prevent an infinite loop.
|
||||
|
||||
policies_cache_time
|
||||
Policy computation can be heavy in case pillar data is used in templated policies and
|
||||
it has not been cached. Therefore, a short-lived cache specifically for rendered policies
|
||||
is used. This specifies the expiration timeout in seconds. Defaults to 60.
|
||||
|
||||
keys
|
||||
List of keys to use to unseal vault server with the vault.unseal runner.
|
||||
|
||||
|
|
|
@ -170,10 +170,16 @@ def ext_pillar(
|
|||
nesting_key=None,
|
||||
merge_strategy=None,
|
||||
merge_lists=None,
|
||||
extra_minion_data=None,
|
||||
):
|
||||
"""
|
||||
Get pillar data from Vault for the configuration ``conf``.
|
||||
"""
|
||||
extra_minion_data = extra_minion_data or {}
|
||||
if extra_minion_data.get("_vault_runner_is_compiling_pillar_templates"):
|
||||
# Disable vault ext_pillar while compiling pillar for vault policy templates
|
||||
return {}
|
||||
|
||||
comps = conf.split()
|
||||
|
||||
paths = [comp for comp in comps if comp.startswith("path=")]
|
||||
|
|
|
@ -8,14 +8,20 @@ documented in the execution module docs.
|
|||
"""
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
|
||||
import requests
|
||||
|
||||
import salt.cache
|
||||
import salt.crypt
|
||||
import salt.exceptions
|
||||
import salt.pillar
|
||||
from salt.defaults import NOT_SET
|
||||
from salt.exceptions import SaltRunnerError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
@ -62,6 +68,8 @@ def generate_token(
|
|||
if not allow_minion_override or ttl is None:
|
||||
ttl = config["auth"].get("ttl", None)
|
||||
storage_type = config["auth"].get("token_backend", "session")
|
||||
policies_refresh_pillar = config.get("policies_refresh_pillar", None)
|
||||
policies_cache_time = config.get("policies_cache_time", 60)
|
||||
|
||||
if config["auth"]["method"] == "approle":
|
||||
if _selftoken_expired():
|
||||
|
@ -92,7 +100,12 @@ def generate_token(
|
|||
"saltstack-user": globals().get("__user__", "<no user set>"),
|
||||
}
|
||||
payload = {
|
||||
"policies": _get_policies(minion_id, config),
|
||||
"policies": _get_policies_cached(
|
||||
minion_id,
|
||||
config,
|
||||
refresh_pillar=policies_refresh_pillar,
|
||||
expire=policies_cache_time,
|
||||
),
|
||||
"num_uses": uses,
|
||||
"meta": audit_data,
|
||||
}
|
||||
|
@ -160,12 +173,24 @@ def unseal():
|
|||
return False
|
||||
|
||||
|
||||
def show_policies(minion_id):
|
||||
def show_policies(minion_id, refresh_pillar=NOT_SET, expire=None):
|
||||
"""
|
||||
Show the Vault policies that are applied to tokens for the given minion
|
||||
Show the Vault policies that are applied to tokens for the given minion.
|
||||
|
||||
minion_id
|
||||
The minions id
|
||||
The minion's id.
|
||||
|
||||
refresh_pillar
|
||||
Whether to refresh the pillar data when rendering templated policies.
|
||||
None will only refresh when the cached data is unavailable, boolean values
|
||||
force one behavior always.
|
||||
Defaults to config value ``policies_refresh_pillar`` or None.
|
||||
|
||||
expire
|
||||
Policy computation can be heavy in case pillar data is used in templated policies and
|
||||
it has not been cached. Therefore, a short-lived cache specifically for rendered policies
|
||||
is used. This specifies the expiration timeout in seconds.
|
||||
Defaults to config value ``policies_cache_time`` or 60.
|
||||
|
||||
CLI Example:
|
||||
|
||||
|
@ -173,8 +198,13 @@ def show_policies(minion_id):
|
|||
|
||||
salt-run vault.show_policies myminion
|
||||
"""
|
||||
config = __opts__["vault"]
|
||||
return _get_policies(minion_id, config)
|
||||
config = __opts__.get("vault", {})
|
||||
if refresh_pillar == NOT_SET:
|
||||
refresh_pillar = config.get("policies_refresh_pillar")
|
||||
expire = expire if expire is not None else config.get("policies_cache_time", 60)
|
||||
return _get_policies_cached(
|
||||
minion_id, config, refresh_pillar=refresh_pillar, expire=expire
|
||||
)
|
||||
|
||||
|
||||
def _validate_signature(minion_id, signature, impersonated_by_master):
|
||||
|
@ -197,15 +227,18 @@ def _validate_signature(minion_id, signature, impersonated_by_master):
|
|||
log.trace("Signature ok")
|
||||
|
||||
|
||||
def _get_policies(minion_id, config):
|
||||
# **kwargs because salt.cache.Cache does not pop "expire" from kwargs
|
||||
def _get_policies(
|
||||
minion_id, config, refresh_pillar=None, **kwargs
|
||||
): # pylint: disable=unused-argument
|
||||
"""
|
||||
Get the policies that should be applied to a token for minion_id
|
||||
"""
|
||||
_, grains, _ = salt.utils.minions.get_minion_data(minion_id, __opts__)
|
||||
grains, pillar = _get_minion_data(minion_id, refresh_pillar)
|
||||
policy_patterns = config.get(
|
||||
"policies", ["saltstack/minion/{minion}", "saltstack/minions"]
|
||||
)
|
||||
mappings = {"minion": minion_id, "grains": grains or {}}
|
||||
mappings = {"minion": minion_id, "grains": grains, "pillar": pillar}
|
||||
|
||||
policies = []
|
||||
for pattern in policy_patterns:
|
||||
|
@ -217,12 +250,84 @@ def _get_policies(minion_id, config):
|
|||
expanded_pattern.format(**mappings).lower() # Vault requirement
|
||||
)
|
||||
except KeyError:
|
||||
log.warning("Could not resolve policy pattern %s", pattern)
|
||||
log.warning(
|
||||
"Could not resolve policy pattern %s for minion %s", pattern, minion_id
|
||||
)
|
||||
|
||||
log.debug("%s policies: %s", minion_id, policies)
|
||||
return policies
|
||||
|
||||
|
||||
def _get_policies_cached(minion_id, config, refresh_pillar=None, expire=60):
|
||||
# expiration of 0 disables cache
|
||||
if not expire:
|
||||
return _get_policies(minion_id, config, refresh_pillar=refresh_pillar)
|
||||
cbank = f"minions/{minion_id}/vault"
|
||||
ckey = "policies"
|
||||
cache = salt.cache.factory(__opts__)
|
||||
policies = cache.cache(
|
||||
cbank,
|
||||
ckey,
|
||||
_get_policies,
|
||||
expire=expire,
|
||||
minion_id=minion_id,
|
||||
config=config,
|
||||
refresh_pillar=refresh_pillar,
|
||||
)
|
||||
if not isinstance(policies, list):
|
||||
log.warning("Cached vault policies were not formed as a list. Refreshing.")
|
||||
cache.flush(cbank, ckey)
|
||||
policies = cache.cache(
|
||||
cbank,
|
||||
ckey,
|
||||
_get_policies,
|
||||
expire=expire,
|
||||
minion_id=minion_id,
|
||||
config=config,
|
||||
refresh_pillar=refresh_pillar,
|
||||
)
|
||||
return policies
|
||||
|
||||
|
||||
def _get_minion_data(minion_id, refresh_pillar=None):
|
||||
_, grains, pillar = salt.utils.minions.get_minion_data(minion_id, __opts__)
|
||||
|
||||
if grains is None:
|
||||
grains = {}
|
||||
# To properly refresh minion grains, something like this could be used:
|
||||
# __salt__["salt.execute"](minion_id, "saltutil.refresh_grains", refresh_pillar=False)
|
||||
# This is deliberately not done since grains should not be used to target
|
||||
# secrets anyways.
|
||||
|
||||
# salt.utils.minions.get_minion_data only returns data from cache or None.
|
||||
# To make sure the correct policies are available, the pillar needs to be
|
||||
# refreshed. This can cause an infinite loop if the pillar data itself
|
||||
# depends on the vault execution module, which relies on this function.
|
||||
# By default, only refresh when necessary. Boolean values force one way.
|
||||
if refresh_pillar is True or (refresh_pillar is None and pillar is None):
|
||||
if __opts__.get("_vault_runner_is_compiling_pillar_templates"):
|
||||
raise SaltRunnerError(
|
||||
"Cyclic dependency detected while refreshing pillar for vault policy templating. "
|
||||
"This is caused by some pillar value relying on the vault execution module. "
|
||||
"Either remove the dependency from your pillar, disable refreshing pillar data "
|
||||
"for policy templating or do not use pillar values in policy templates."
|
||||
)
|
||||
local_opts = copy.deepcopy(__opts__)
|
||||
# Relying on opts for ext_pillars does not work properly (only the first one runs
|
||||
# correctly).
|
||||
extra_minion_data = {"_vault_runner_is_compiling_pillar_templates": True}
|
||||
local_opts.update(extra_minion_data)
|
||||
pillar = LazyPillar(
|
||||
local_opts, grains, minion_id, extra_minion_data=extra_minion_data
|
||||
)
|
||||
elif pillar is None:
|
||||
# Make sure pillar is a dict. Necessary because a check on LazyPillar would
|
||||
# refresh it unconditionally (even when no pillar values are used)
|
||||
pillar = {}
|
||||
|
||||
return grains, pillar
|
||||
|
||||
|
||||
def _selftoken_expired():
|
||||
"""
|
||||
Validate the current token exists and is still valid
|
||||
|
@ -256,3 +361,41 @@ def _get_token_create_url(config):
|
|||
auth_path = "/v1/auth/token/create"
|
||||
base_url = config["url"]
|
||||
return "/".join(x.strip("/") for x in (base_url, auth_path, role_name) if x)
|
||||
|
||||
|
||||
class LazyPillar(Mapping):
|
||||
"""
|
||||
Simulates a pillar dictionary. Only compiles the pillar
|
||||
once an item is requested.
|
||||
"""
|
||||
|
||||
def __init__(self, opts, grains, minion_id, extra_minion_data=None):
|
||||
self.opts = opts
|
||||
self.grains = grains
|
||||
self.minion_id = minion_id
|
||||
self.extra_minion_data = extra_minion_data or {}
|
||||
self._pillar = None
|
||||
|
||||
def _load(self):
|
||||
log.info("Refreshing pillar for vault templating.")
|
||||
self._pillar = salt.pillar.get_pillar(
|
||||
self.opts,
|
||||
self.grains,
|
||||
self.minion_id,
|
||||
extra_minion_data=self.extra_minion_data,
|
||||
).compile_pillar()
|
||||
|
||||
def __getitem__(self, key):
|
||||
if self._pillar is None:
|
||||
self._load()
|
||||
return self._pillar[key]
|
||||
|
||||
def __iter__(self):
|
||||
if self._pillar is None:
|
||||
self._load()
|
||||
yield from self._pillar
|
||||
|
||||
def __len__(self):
|
||||
if self._pillar is None:
|
||||
self._load()
|
||||
return len(self._pillar)
|
||||
|
|
|
@ -80,6 +80,8 @@ def get(key, profile=None):
|
|||
"""
|
||||
Get a value from the vault service
|
||||
"""
|
||||
if __opts__.get("_vault_runner_is_compiling_pillar_templates"):
|
||||
return None
|
||||
if "?" in key:
|
||||
path, key = key.split("?")
|
||||
else:
|
||||
|
|
285
tests/pytests/integration/runners/test_vault.py
Normal file
285
tests/pytests/integration/runners/test_vault.py
Normal file
|
@ -0,0 +1,285 @@
|
|||
"""
|
||||
Tests for the Vault runner
|
||||
"""
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.slow_test,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def pillar_state_tree(tmp_path_factory):
|
||||
_pillar_state_tree = tmp_path_factory.mktemp("pillar")
|
||||
try:
|
||||
yield _pillar_state_tree
|
||||
finally:
|
||||
shutil.rmtree(str(_pillar_state_tree), ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def pillar_salt_master(salt_factories, pillar_state_tree):
|
||||
config_defaults = {
|
||||
"pillar_roots": {"base": [str(pillar_state_tree)]},
|
||||
"open_mode": True,
|
||||
"ext_pillar": [{"vault": "path=does/not/matter"}],
|
||||
"sdbvault": {
|
||||
"driver": "vault",
|
||||
},
|
||||
"vault": {
|
||||
"auth": {"token": "testsecret"},
|
||||
"policies": [
|
||||
"salt_minion",
|
||||
"salt_minion_{minion}",
|
||||
"salt_role_{pillar[roles]}",
|
||||
"salt_unsafe_{grains[foo]}",
|
||||
],
|
||||
},
|
||||
}
|
||||
factory = salt_factories.salt_master_daemon(
|
||||
"vault-pillarpolicy-functional-master", defaults=config_defaults
|
||||
)
|
||||
with factory.started():
|
||||
yield factory
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def pillar_salt_minion(pillar_salt_master):
|
||||
assert pillar_salt_master.is_running()
|
||||
factory = pillar_salt_master.salt_minion_daemon(
|
||||
"vault-pillarpolicy-functional-minion-1",
|
||||
defaults={"open_mode": True, "grains": {"foo": "bar"}},
|
||||
)
|
||||
with factory.started():
|
||||
# Sync All
|
||||
salt_call_cli = factory.salt_call_cli()
|
||||
ret = salt_call_cli.run("saltutil.sync_all", _timeout=120)
|
||||
assert ret.returncode == 0, ret
|
||||
yield factory
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def pillar_salt_run_cli(pillar_salt_master):
|
||||
return pillar_salt_master.salt_run_cli()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def pillar_salt_call_cli(pillar_salt_minion):
|
||||
return pillar_salt_minion.salt_call_cli()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def pillar_policy_tree(pillar_state_tree, pillar_salt_minion, pillar_salt_call_cli):
|
||||
top_file = """
|
||||
base:
|
||||
'{}':
|
||||
- roles
|
||||
- sdb_loop
|
||||
""".format(
|
||||
pillar_salt_minion.id
|
||||
)
|
||||
roles_pillar = """
|
||||
roles:
|
||||
- minion
|
||||
- web
|
||||
"""
|
||||
sdb_loop_pillar = """
|
||||
foo: {{ salt["sdb.get"]("sdb://sdbvault/does/not/matter/val") }}
|
||||
"""
|
||||
top_tempfile = pytest.helpers.temp_file("top.sls", top_file, pillar_state_tree)
|
||||
roles_tempfile = pytest.helpers.temp_file(
|
||||
"roles.sls", roles_pillar, pillar_state_tree
|
||||
)
|
||||
sdb_loop_tempfile = pytest.helpers.temp_file(
|
||||
"sdb_loop.sls", sdb_loop_pillar, pillar_state_tree
|
||||
)
|
||||
|
||||
with top_tempfile, roles_tempfile, sdb_loop_tempfile:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def pillar_loop(
|
||||
pillar_state_tree, pillar_policy_tree, pillar_salt_minion, pillar_salt_call_cli
|
||||
):
|
||||
top_file = """
|
||||
base:
|
||||
'{}':
|
||||
- roles
|
||||
- sdb_loop
|
||||
- exe_loop
|
||||
""".format(
|
||||
pillar_salt_minion.id
|
||||
)
|
||||
exe_loop_pillar = r"""
|
||||
bar: {{ salt["vault.read_secret"]("does/not/matter") }}
|
||||
"""
|
||||
top_tempfile = pytest.helpers.temp_file("top.sls", top_file, pillar_state_tree)
|
||||
exe_loop_tempfile = pytest.helpers.temp_file(
|
||||
"exe_loop.sls", exe_loop_pillar, pillar_state_tree
|
||||
)
|
||||
|
||||
with top_tempfile, exe_loop_tempfile:
|
||||
yield
|
||||
|
||||
|
||||
class TestVaultPillarPolicyTemplatesWithoutCache:
|
||||
@pytest.fixture()
|
||||
def minion_data_cache_absent(
|
||||
self, pillar_salt_run_cli, pillar_salt_minion, pillar_policy_tree
|
||||
):
|
||||
ret = pillar_salt_run_cli.run(
|
||||
"cache.flush", f"minions/{pillar_salt_minion.id}", "data"
|
||||
)
|
||||
assert ret.returncode == 0
|
||||
cached = pillar_salt_run_cli.run(
|
||||
"cache.fetch", f"minions/{pillar_salt_minion.id}", "data"
|
||||
)
|
||||
assert cached.returncode == 0
|
||||
assert not cached.data
|
||||
yield
|
||||
|
||||
def test_show_policies(
|
||||
self, pillar_salt_run_cli, pillar_salt_minion, minion_data_cache_absent
|
||||
):
|
||||
"""
|
||||
Test that pillar data is refreshed correctly before rendering policies when necessary.
|
||||
This test includes the prevention of loop exceptions by sdb/ext_pillar modules
|
||||
This refresh does not include grains and pillar data targeted by these grains (unsafe anyways!).
|
||||
"""
|
||||
ret = pillar_salt_run_cli.run(
|
||||
"vault.show_policies", pillar_salt_minion.id, expire=0
|
||||
)
|
||||
assert ret.data == [
|
||||
"salt_minion",
|
||||
f"salt_minion_{pillar_salt_minion.id}",
|
||||
"salt_role_minion",
|
||||
"salt_role_web",
|
||||
]
|
||||
assert "Pillar render error: Failed to load ext_pillar vault" not in ret.stderr
|
||||
assert "Pillar render error: Rendering SLS 'sdb_loop' failed" not in ret.stderr
|
||||
|
||||
def test_show_policies_uncached_data_no_pillar_refresh(
|
||||
self, pillar_salt_run_cli, pillar_salt_minion, minion_data_cache_absent
|
||||
):
|
||||
"""
|
||||
Test that pillar is not refreshed when explicitly disabled
|
||||
"""
|
||||
ret = pillar_salt_run_cli.run(
|
||||
"vault.show_policies", pillar_salt_minion.id, refresh_pillar=False, expire=0
|
||||
)
|
||||
assert ret.data == ["salt_minion", f"salt_minion_{pillar_salt_minion.id}"]
|
||||
|
||||
def test_policy_compilation_prevents_loop(
|
||||
self,
|
||||
pillar_salt_run_cli,
|
||||
pillar_salt_minion,
|
||||
pillar_loop,
|
||||
minion_data_cache_absent,
|
||||
):
|
||||
"""
|
||||
Test that the runner prevents a recursive cycle from happening
|
||||
"""
|
||||
ret = pillar_salt_run_cli.run(
|
||||
"vault.show_policies", pillar_salt_minion.id, refresh_pillar=True, expire=0
|
||||
)
|
||||
assert ret.data == [
|
||||
"salt_minion",
|
||||
f"salt_minion_{pillar_salt_minion.id}",
|
||||
"salt_role_minion",
|
||||
"salt_role_web",
|
||||
]
|
||||
assert "Pillar render error: Rendering SLS 'exe_loop' failed" in ret.stderr
|
||||
|
||||
|
||||
class TestVaultPillarPolicyTemplatesWithCache:
|
||||
@pytest.fixture()
|
||||
def minion_data_cache_present(
|
||||
self,
|
||||
pillar_salt_call_cli,
|
||||
pillar_salt_run_cli,
|
||||
pillar_salt_minion,
|
||||
pillar_policy_tree,
|
||||
):
|
||||
ret = pillar_salt_call_cli.run("saltutil.refresh_pillar", wait=True)
|
||||
assert ret.returncode == 0
|
||||
assert ret.data is True
|
||||
yield
|
||||
|
||||
@pytest.fixture()
|
||||
def minion_data_cache_outdated(
|
||||
self,
|
||||
minion_data_cache_present,
|
||||
pillar_salt_run_cli,
|
||||
pillar_salt_call_cli,
|
||||
pillar_policy_tree,
|
||||
pillar_state_tree,
|
||||
pillar_salt_minion,
|
||||
):
|
||||
roles_pillar = """
|
||||
roles:
|
||||
- minion
|
||||
- web
|
||||
- fresh
|
||||
"""
|
||||
roles_tempfile = pytest.helpers.temp_file(
|
||||
"roles.sls", roles_pillar, pillar_state_tree
|
||||
)
|
||||
cached = pillar_salt_run_cli.run(
|
||||
"cache.fetch", f"minions/{pillar_salt_minion.id}", "data"
|
||||
)
|
||||
assert cached.returncode == 0
|
||||
assert cached.data
|
||||
assert "pillar" in cached.data
|
||||
assert "grains" in cached.data
|
||||
assert "foo" in cached.data["grains"]
|
||||
assert "roles" in cached.data["pillar"]
|
||||
assert ["minion", "web"] == cached.data["pillar"]["roles"]
|
||||
with roles_tempfile:
|
||||
yield
|
||||
|
||||
def test_show_policies_cached_data_no_pillar_refresh(
|
||||
self, pillar_salt_run_cli, pillar_salt_minion, minion_data_cache_outdated
|
||||
):
|
||||
"""
|
||||
Test that pillar data from cache is used when it is available
|
||||
"""
|
||||
ret = pillar_salt_run_cli.run(
|
||||
"vault.show_policies", pillar_salt_minion.id, expire=0
|
||||
)
|
||||
assert ret.data == [
|
||||
"salt_minion",
|
||||
f"salt_minion_{pillar_salt_minion.id}",
|
||||
"salt_role_minion",
|
||||
"salt_role_web",
|
||||
"salt_unsafe_bar",
|
||||
]
|
||||
|
||||
def test_show_policies_refresh_pillar(
|
||||
self, pillar_salt_run_cli, pillar_salt_minion, minion_data_cache_outdated
|
||||
):
|
||||
"""
|
||||
Test that pillar data is always refreshed when requested.
|
||||
This test includes the prevention of loops by sdb/ext_pillar modules
|
||||
This refresh does not include grains and pillar data targeted by these grains (unsafe anyways!).
|
||||
"""
|
||||
ret = pillar_salt_run_cli.run(
|
||||
"vault.show_policies", pillar_salt_minion.id, refresh_pillar=True, expire=0
|
||||
)
|
||||
assert ret.data == [
|
||||
"salt_minion",
|
||||
f"salt_minion_{pillar_salt_minion.id}",
|
||||
"salt_role_minion",
|
||||
"salt_role_web",
|
||||
"salt_role_fresh",
|
||||
"salt_unsafe_bar",
|
||||
]
|
||||
assert "Pillar render error: Failed to load ext_pillar vault" not in ret.stderr
|
||||
assert "Pillar render error: Rendering SLS 'sdb_loop' failed" not in ret.stderr
|
|
@ -182,3 +182,18 @@ def test_ext_pillar_merging(is_v2_false):
|
|||
merge_lists=False,
|
||||
)
|
||||
assert ext_pillar == expected
|
||||
|
||||
|
||||
def text_ext_pillar_disabled_during_policy_pillar_rendering():
|
||||
mock_version = Mock()
|
||||
mock_vault = Mock()
|
||||
extra = {"_vault_runner_is_compiling_pillar_templates": True}
|
||||
|
||||
with patch.dict(
|
||||
vault.__utils__, {"vault.make_request": mock_vault, "vault.is_v2": mock_version}
|
||||
):
|
||||
assert {} == vault.ext_pillar(
|
||||
"test-minion", {}, conf="path=secret/path", extra_minion_data=extra
|
||||
)
|
||||
assert mock_version.call_count == 0
|
||||
assert mock_vault.call_count == 0
|
||||
|
|
|
@ -30,8 +30,13 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
|||
"mixedcase": "UP-low-UP",
|
||||
}
|
||||
|
||||
self.pillar = {
|
||||
"role": "test",
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
del self.grains
|
||||
del self.pillar
|
||||
|
||||
def test_get_policies_for_nonexisting_minions(self):
|
||||
minion_id = "salt_master"
|
||||
|
@ -45,7 +50,6 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
|||
"salt.utils.minions.get_minion_data",
|
||||
MagicMock(return_value=(None, None, None)),
|
||||
):
|
||||
for case, correct_output in cases.items():
|
||||
with patch.dict(
|
||||
vault.__utils__,
|
||||
{
|
||||
|
@ -54,6 +58,7 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
|||
)
|
||||
},
|
||||
):
|
||||
for case, correct_output in cases.items():
|
||||
test_config = {"policies": [case]}
|
||||
output = vault._get_policies(
|
||||
minion_id, test_config
|
||||
|
@ -77,13 +82,13 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
|||
"Case-Should-Be-Lowered:{grains[mixedcase]}": [
|
||||
"case-should-be-lowered:up-low-up"
|
||||
],
|
||||
"pillar-rendering:{pillar[role]}": ["pillar-rendering:test"],
|
||||
}
|
||||
|
||||
with patch(
|
||||
"salt.utils.minions.get_minion_data",
|
||||
MagicMock(return_value=(None, self.grains, None)),
|
||||
MagicMock(return_value=(None, self.grains, self.pillar)),
|
||||
):
|
||||
for case, correct_output in cases.items():
|
||||
with patch.dict(
|
||||
vault.__utils__,
|
||||
{
|
||||
|
@ -92,6 +97,7 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
|||
)
|
||||
},
|
||||
):
|
||||
for case, correct_output in cases.items():
|
||||
test_config = {"policies": [case]}
|
||||
output = vault._get_policies(
|
||||
"test-minion", test_config
|
||||
|
@ -103,6 +109,39 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
|||
log.debug("Difference:\n\t%s", diff)
|
||||
self.assertEqual(output, correct_output)
|
||||
|
||||
def test_get_policies_does_not_render_pillar_unnecessarily(self):
|
||||
"""
|
||||
The pillar data should only be refreshed in case items are accessed.
|
||||
"""
|
||||
get_pillar = Mock(name="g")
|
||||
get_pillar.return_value.compile_pillar.return_value = self.pillar
|
||||
|
||||
cases = [
|
||||
("salt_minion_{minion}", 0),
|
||||
("salt_grain_{grains[id]}", 0),
|
||||
("unset_{foo}", 0),
|
||||
("salt_pillar_{pillar[role]}", 1),
|
||||
]
|
||||
|
||||
with patch(
|
||||
"salt.utils.minions.get_minion_data",
|
||||
Mock(return_value=(None, self.grains, None)),
|
||||
), patch("salt.pillar.get_pillar", get_pillar):
|
||||
with patch.dict(
|
||||
vault.__utils__,
|
||||
{
|
||||
"vault.expand_pattern_lists": Mock(
|
||||
side_effect=lambda x, *args, **kwargs: [x]
|
||||
)
|
||||
},
|
||||
):
|
||||
for case, expected in cases:
|
||||
test_config = {"policies": [case]}
|
||||
vault._get_policies(
|
||||
"test-minion", test_config, refresh_pillar=True
|
||||
) # pylint: disable=protected-access
|
||||
assert get_pillar.call_count == expected
|
||||
|
||||
def test_get_token_create_url(self):
|
||||
"""
|
||||
Ensure _get_token_create_url parses config correctly
|
||||
|
@ -170,6 +209,10 @@ class VaultTokenAuthTest(TestCase, LoaderModuleMockMixin):
|
|||
"salt.runners.vault._get_token_create_url",
|
||||
MagicMock(return_value="http://fake_url"),
|
||||
)
|
||||
@patch(
|
||||
"salt.runners.vault._get_policies_cached",
|
||||
Mock(return_value=["saltstack/minion/test-minion", "saltstack/minions"]),
|
||||
)
|
||||
def test_generate_token(self):
|
||||
"""
|
||||
Basic tests for test_generate_token: all exits
|
||||
|
@ -231,7 +274,7 @@ class VaultTokenAuthTest(TestCase, LoaderModuleMockMixin):
|
|||
self.assertTrue("error" in result)
|
||||
self.assertEqual(result["error"], "no reason")
|
||||
|
||||
with patch("salt.runners.vault._get_policies", MagicMock(return_value=[])):
|
||||
with patch("salt.runners.vault._get_policies_cached", Mock(return_value=[])):
|
||||
result = vault.generate_token("test-minion", "signature")
|
||||
self.assertTrue(isinstance(result, dict))
|
||||
self.assertTrue("error" in result)
|
||||
|
@ -250,6 +293,10 @@ class VaultTokenAuthTest(TestCase, LoaderModuleMockMixin):
|
|||
"salt.runners.vault._get_token_create_url",
|
||||
MagicMock(return_value="http://fake_url"),
|
||||
)
|
||||
@patch(
|
||||
"salt.runners.vault._get_policies_cached",
|
||||
Mock(return_value=["saltstack/minion/test-minion", "saltstack/minions"]),
|
||||
)
|
||||
def test_generate_token_with_namespace(self):
|
||||
"""
|
||||
Basic tests for test_generate_token: all exits
|
||||
|
@ -303,6 +350,10 @@ class VaultAppRoleAuthTest(TestCase, LoaderModuleMockMixin):
|
|||
"salt.runners.vault._get_token_create_url",
|
||||
MagicMock(return_value="http://fake_url"),
|
||||
)
|
||||
@patch(
|
||||
"salt.runners.vault._get_policies_cached",
|
||||
Mock(return_value=["saltstack/minion/test-minion", "saltstack/minions"]),
|
||||
)
|
||||
def test_generate_token(self):
|
||||
"""
|
||||
Basic test for test_generate_token with approle (two vault calls)
|
||||
|
|
|
@ -5,7 +5,7 @@ Test case for the vault SDB module
|
|||
|
||||
import salt.sdb.vault as vault
|
||||
from tests.support.mixins import LoaderModuleMockMixin
|
||||
from tests.support.mock import MagicMock, call, patch
|
||||
from tests.support.mock import MagicMock, Mock, call, patch
|
||||
from tests.support.unit import TestCase
|
||||
|
||||
|
||||
|
@ -201,3 +201,22 @@ class TestVaultSDB(LoaderModuleMockMixin, TestCase):
|
|||
assert mock_vault.call_args_list == [
|
||||
call("GET", "v1/sdb://myvault/path/to/foo")
|
||||
]
|
||||
|
||||
def test_get_disabled_during_policy_pillar_rendering(self):
|
||||
"""
|
||||
Ensure that during pillar rendering for templated policies,
|
||||
salt.sdb.vault.get function does not query vault to prevent
|
||||
a cyclic dependency.
|
||||
"""
|
||||
mock_version = Mock()
|
||||
mock_vault = Mock()
|
||||
with patch.dict(
|
||||
vault.__utils__,
|
||||
{"vault.make_request": mock_vault, "vault.is_v2": mock_version},
|
||||
):
|
||||
with patch.dict(
|
||||
vault.__opts__, {"_vault_runner_is_compiling_pillar_templates": True}
|
||||
):
|
||||
self.assertIsNone(vault.get("sdb://myvault/path/to/foo/foo"))
|
||||
assert mock_version.call_count == 0
|
||||
assert mock_vault.call_count == 0
|
||||
|
|
Loading…
Add table
Reference in a new issue