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
|
values, eg ``my-policies/{grains[os]}``. ``{minion}`` is shorthand for
|
||||||
``grains[id]``, eg ``saltstack/minion/{minion}``.
|
``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::
|
.. important::
|
||||||
|
|
||||||
See :ref:`Is Targeting using Grain Data Secure?
|
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
|
Optional. If policies is not configured, ``saltstack/minions`` and
|
||||||
``saltstack/{minion}`` are used as defaults.
|
``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
|
keys
|
||||||
List of keys to use to unseal vault server with the vault.unseal runner.
|
List of keys to use to unseal vault server with the vault.unseal runner.
|
||||||
|
|
||||||
|
|
|
@ -170,10 +170,16 @@ def ext_pillar(
|
||||||
nesting_key=None,
|
nesting_key=None,
|
||||||
merge_strategy=None,
|
merge_strategy=None,
|
||||||
merge_lists=None,
|
merge_lists=None,
|
||||||
|
extra_minion_data=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get pillar data from Vault for the configuration ``conf``.
|
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()
|
comps = conf.split()
|
||||||
|
|
||||||
paths = [comp for comp in comps if comp.startswith("path=")]
|
paths = [comp for comp in comps if comp.startswith("path=")]
|
||||||
|
|
|
@ -8,14 +8,20 @@ documented in the execution module docs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
import salt.cache
|
||||||
import salt.crypt
|
import salt.crypt
|
||||||
import salt.exceptions
|
import salt.exceptions
|
||||||
|
import salt.pillar
|
||||||
|
from salt.defaults import NOT_SET
|
||||||
|
from salt.exceptions import SaltRunnerError
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -62,6 +68,8 @@ def generate_token(
|
||||||
if not allow_minion_override or ttl is None:
|
if not allow_minion_override or ttl is None:
|
||||||
ttl = config["auth"].get("ttl", None)
|
ttl = config["auth"].get("ttl", None)
|
||||||
storage_type = config["auth"].get("token_backend", "session")
|
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 config["auth"]["method"] == "approle":
|
||||||
if _selftoken_expired():
|
if _selftoken_expired():
|
||||||
|
@ -92,7 +100,12 @@ def generate_token(
|
||||||
"saltstack-user": globals().get("__user__", "<no user set>"),
|
"saltstack-user": globals().get("__user__", "<no user set>"),
|
||||||
}
|
}
|
||||||
payload = {
|
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,
|
"num_uses": uses,
|
||||||
"meta": audit_data,
|
"meta": audit_data,
|
||||||
}
|
}
|
||||||
|
@ -160,12 +173,24 @@ def unseal():
|
||||||
return False
|
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
|
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:
|
CLI Example:
|
||||||
|
|
||||||
|
@ -173,8 +198,13 @@ def show_policies(minion_id):
|
||||||
|
|
||||||
salt-run vault.show_policies myminion
|
salt-run vault.show_policies myminion
|
||||||
"""
|
"""
|
||||||
config = __opts__["vault"]
|
config = __opts__.get("vault", {})
|
||||||
return _get_policies(minion_id, config)
|
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):
|
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")
|
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
|
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(
|
policy_patterns = config.get(
|
||||||
"policies", ["saltstack/minion/{minion}", "saltstack/minions"]
|
"policies", ["saltstack/minion/{minion}", "saltstack/minions"]
|
||||||
)
|
)
|
||||||
mappings = {"minion": minion_id, "grains": grains or {}}
|
mappings = {"minion": minion_id, "grains": grains, "pillar": pillar}
|
||||||
|
|
||||||
policies = []
|
policies = []
|
||||||
for pattern in policy_patterns:
|
for pattern in policy_patterns:
|
||||||
|
@ -217,12 +250,84 @@ def _get_policies(minion_id, config):
|
||||||
expanded_pattern.format(**mappings).lower() # Vault requirement
|
expanded_pattern.format(**mappings).lower() # Vault requirement
|
||||||
)
|
)
|
||||||
except KeyError:
|
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)
|
log.debug("%s policies: %s", minion_id, policies)
|
||||||
return 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():
|
def _selftoken_expired():
|
||||||
"""
|
"""
|
||||||
Validate the current token exists and is still valid
|
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"
|
auth_path = "/v1/auth/token/create"
|
||||||
base_url = config["url"]
|
base_url = config["url"]
|
||||||
return "/".join(x.strip("/") for x in (base_url, auth_path, role_name) if x)
|
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
|
Get a value from the vault service
|
||||||
"""
|
"""
|
||||||
|
if __opts__.get("_vault_runner_is_compiling_pillar_templates"):
|
||||||
|
return None
|
||||||
if "?" in key:
|
if "?" in key:
|
||||||
path, key = key.split("?")
|
path, key = key.split("?")
|
||||||
else:
|
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,
|
merge_lists=False,
|
||||||
)
|
)
|
||||||
assert ext_pillar == expected
|
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",
|
"mixedcase": "UP-low-UP",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.pillar = {
|
||||||
|
"role": "test",
|
||||||
|
}
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
del self.grains
|
del self.grains
|
||||||
|
del self.pillar
|
||||||
|
|
||||||
def test_get_policies_for_nonexisting_minions(self):
|
def test_get_policies_for_nonexisting_minions(self):
|
||||||
minion_id = "salt_master"
|
minion_id = "salt_master"
|
||||||
|
@ -45,15 +50,15 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
||||||
"salt.utils.minions.get_minion_data",
|
"salt.utils.minions.get_minion_data",
|
||||||
MagicMock(return_value=(None, None, None)),
|
MagicMock(return_value=(None, None, None)),
|
||||||
):
|
):
|
||||||
for case, correct_output in cases.items():
|
with patch.dict(
|
||||||
with patch.dict(
|
vault.__utils__,
|
||||||
vault.__utils__,
|
{
|
||||||
{
|
"vault.expand_pattern_lists": Mock(
|
||||||
"vault.expand_pattern_lists": Mock(
|
side_effect=lambda x, *args, **kwargs: [x]
|
||||||
side_effect=lambda x, *args, **kwargs: [x]
|
)
|
||||||
)
|
},
|
||||||
},
|
):
|
||||||
):
|
for case, correct_output in cases.items():
|
||||||
test_config = {"policies": [case]}
|
test_config = {"policies": [case]}
|
||||||
output = vault._get_policies(
|
output = vault._get_policies(
|
||||||
minion_id, test_config
|
minion_id, test_config
|
||||||
|
@ -77,21 +82,22 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
||||||
"Case-Should-Be-Lowered:{grains[mixedcase]}": [
|
"Case-Should-Be-Lowered:{grains[mixedcase]}": [
|
||||||
"case-should-be-lowered:up-low-up"
|
"case-should-be-lowered:up-low-up"
|
||||||
],
|
],
|
||||||
|
"pillar-rendering:{pillar[role]}": ["pillar-rendering:test"],
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"salt.utils.minions.get_minion_data",
|
"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(
|
||||||
with patch.dict(
|
vault.__utils__,
|
||||||
vault.__utils__,
|
{
|
||||||
{
|
"vault.expand_pattern_lists": Mock(
|
||||||
"vault.expand_pattern_lists": Mock(
|
side_effect=lambda x, *args, **kwargs: [x]
|
||||||
side_effect=lambda x, *args, **kwargs: [x]
|
)
|
||||||
)
|
},
|
||||||
},
|
):
|
||||||
):
|
for case, correct_output in cases.items():
|
||||||
test_config = {"policies": [case]}
|
test_config = {"policies": [case]}
|
||||||
output = vault._get_policies(
|
output = vault._get_policies(
|
||||||
"test-minion", test_config
|
"test-minion", test_config
|
||||||
|
@ -103,6 +109,39 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
||||||
log.debug("Difference:\n\t%s", diff)
|
log.debug("Difference:\n\t%s", diff)
|
||||||
self.assertEqual(output, correct_output)
|
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):
|
def test_get_token_create_url(self):
|
||||||
"""
|
"""
|
||||||
Ensure _get_token_create_url parses config correctly
|
Ensure _get_token_create_url parses config correctly
|
||||||
|
@ -170,6 +209,10 @@ class VaultTokenAuthTest(TestCase, LoaderModuleMockMixin):
|
||||||
"salt.runners.vault._get_token_create_url",
|
"salt.runners.vault._get_token_create_url",
|
||||||
MagicMock(return_value="http://fake_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):
|
def test_generate_token(self):
|
||||||
"""
|
"""
|
||||||
Basic tests for test_generate_token: all exits
|
Basic tests for test_generate_token: all exits
|
||||||
|
@ -231,7 +274,7 @@ class VaultTokenAuthTest(TestCase, LoaderModuleMockMixin):
|
||||||
self.assertTrue("error" in result)
|
self.assertTrue("error" in result)
|
||||||
self.assertEqual(result["error"], "no reason")
|
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")
|
result = vault.generate_token("test-minion", "signature")
|
||||||
self.assertTrue(isinstance(result, dict))
|
self.assertTrue(isinstance(result, dict))
|
||||||
self.assertTrue("error" in result)
|
self.assertTrue("error" in result)
|
||||||
|
@ -250,6 +293,10 @@ class VaultTokenAuthTest(TestCase, LoaderModuleMockMixin):
|
||||||
"salt.runners.vault._get_token_create_url",
|
"salt.runners.vault._get_token_create_url",
|
||||||
MagicMock(return_value="http://fake_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):
|
def test_generate_token_with_namespace(self):
|
||||||
"""
|
"""
|
||||||
Basic tests for test_generate_token: all exits
|
Basic tests for test_generate_token: all exits
|
||||||
|
@ -303,6 +350,10 @@ class VaultAppRoleAuthTest(TestCase, LoaderModuleMockMixin):
|
||||||
"salt.runners.vault._get_token_create_url",
|
"salt.runners.vault._get_token_create_url",
|
||||||
MagicMock(return_value="http://fake_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):
|
def test_generate_token(self):
|
||||||
"""
|
"""
|
||||||
Basic test for test_generate_token with approle (two vault calls)
|
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
|
import salt.sdb.vault as vault
|
||||||
from tests.support.mixins import LoaderModuleMockMixin
|
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
|
from tests.support.unit import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
@ -201,3 +201,22 @@ class TestVaultSDB(LoaderModuleMockMixin, TestCase):
|
||||||
assert mock_vault.call_args_list == [
|
assert mock_vault.call_args_list == [
|
||||||
call("GET", "v1/sdb://myvault/path/to/foo")
|
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