Move vault parameter list expansion to utils module

This commit is contained in:
jeanluc 2022-09-12 15:42:19 +02:00
parent a1a07d1c8c
commit 7f9d276c2a
No known key found for this signature in database
GPG key ID: 3EB52D4C754CD898
4 changed files with 132 additions and 128 deletions

View file

@ -10,7 +10,6 @@ documented in the execution module docs.
import base64
import json
import logging
import string
import time
import requests
@ -211,7 +210,9 @@ def _get_policies(minion_id, config):
policies = []
for pattern in policy_patterns:
try:
for expanded_pattern in _expand_pattern_lists(pattern, **mappings):
for expanded_pattern in __utils__["vault.expand_pattern_lists"](
pattern, **mappings
):
policies.append(
expanded_pattern.format(**mappings).lower() # Vault requirement
)
@ -222,56 +223,6 @@ def _get_policies(minion_id, config):
return policies
def _expand_pattern_lists(pattern, **mappings):
"""
Expands the pattern for any list-valued mappings, such that for any list of
length N in the mappings present in the pattern, N copies of the pattern are
returned, each with an element of the list substituted.
pattern:
A pattern to expand, for example ``by-role/{grains[roles]}``
mappings:
A dictionary of variables that can be expanded into the pattern.
Example: Given the pattern `` by-role/{grains[roles]}`` and the below grains
.. code-block:: yaml
grains:
roles:
- web
- database
This function will expand into two patterns,
``[by-role/web, by-role/database]``.
Note that this method does not expand any non-list patterns.
"""
expanded_patterns = []
f = string.Formatter()
# This function uses a string.Formatter to get all the formatting tokens from
# the pattern, then recursively replaces tokens whose expanded value is a
# list. For a list with N items, it will create N new pattern strings and
# then continue with the next token. In practice this is expected to not be
# very expensive, since patterns will typically involve a handful of lists at
# most.
for (_, field_name, _, _) in f.parse(pattern):
if field_name is None:
continue
(value, _) = f.get_field(field_name, None, mappings)
if isinstance(value, list):
token = "{{{0}}}".format(field_name)
expanded = [pattern.replace(token, str(elem)) for elem in value]
for expanded_item in expanded:
result = _expand_pattern_lists(expanded_item, **mappings)
expanded_patterns += result
return expanded_patterns
return [pattern]
def _selftoken_expired():
"""
Validate the current token exists and is still valid

View file

@ -10,6 +10,7 @@ documented in the execution module docs.
import base64
import logging
import os
import string
import tempfile
import time
@ -545,3 +546,53 @@ def _get_secret_path_metadata(path):
except Exception as err: # pylint: disable=broad-except
log.error("Failed to get secret metadata %s: %s", type(err).__name__, err)
return ret
def expand_pattern_lists(pattern, **mappings):
"""
Expands the pattern for any list-valued mappings, such that for any list of
length N in the mappings present in the pattern, N copies of the pattern are
returned, each with an element of the list substituted.
pattern:
A pattern to expand, for example ``by-role/{grains[roles]}``
mappings:
A dictionary of variables that can be expanded into the pattern.
Example: Given the pattern `` by-role/{grains[roles]}`` and the below grains
.. code-block:: yaml
grains:
roles:
- web
- database
This function will expand into two patterns,
``[by-role/web, by-role/database]``.
Note that this method does not expand any non-list patterns.
"""
expanded_patterns = []
f = string.Formatter()
# This function uses a string.Formatter to get all the formatting tokens from
# the pattern, then recursively replaces tokens whose expanded value is a
# list. For a list with N items, it will create N new pattern strings and
# then continue with the next token. In practice this is expected to not be
# very expensive, since patterns will typically involve a handful of lists at
# most.
for (_, field_name, _, _) in f.parse(pattern):
if field_name is None:
continue
(value, _) = f.get_field(field_name, None, mappings)
if isinstance(value, list):
token = "{{{0}}}".format(field_name)
expanded = [pattern.replace(token, str(elem)) for elem in value]
for expanded_item in expanded:
result = expand_pattern_lists(expanded_item, **mappings)
expanded_patterns += result
return expanded_patterns
return [pattern]

View file

@ -544,3 +544,42 @@ def test_get_secret_path_metadata_no_cache(metadata_v2, cache_uses, cache_secret
assert function_result == metadata_v2
mock_write_cache.assert_called_with(cache_object)
assert cache_object == expected_cache_object
def test_expand_pattern_lists():
"""
Ensure expand_pattern_lists works as intended:
- Expand list-valued patterns
- Do not change non-list-valued tokens
"""
cases = {
"no-tokens-to-replace": ["no-tokens-to-replace"],
"single-dict:{minion}": ["single-dict:{minion}"],
"single-list:{grains[roles]}": ["single-list:web", "single-list:database"],
"multiple-lists:{grains[roles]}+{grains[aux]}": [
"multiple-lists:web+foo",
"multiple-lists:web+bar",
"multiple-lists:database+foo",
"multiple-lists:database+bar",
],
"single-list-with-dicts:{grains[id]}+{grains[roles]}+{grains[id]}": [
"single-list-with-dicts:{grains[id]}+web+{grains[id]}",
"single-list-with-dicts:{grains[id]}+database+{grains[id]}",
],
"deeply-nested-list:{grains[deep][foo][bar][baz]}": [
"deeply-nested-list:hello",
"deeply-nested-list:world",
],
}
pattern_vars = {
"id": "test-minion",
"roles": ["web", "database"],
"aux": ["foo", "bar"],
"deep": {"foo": {"bar": {"baz": ["hello", "world"]}}},
}
mappings = {"minion": "test-minion", "grains": pattern_vars}
for case, correct_output in cases.items():
output = vault.expand_pattern_lists(case, **mappings)
assert output == correct_output

View file

@ -33,91 +33,46 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
def tearDown(self):
del self.grains
def test_pattern_list_expander(self):
"""
Ensure _expand_pattern_lists works as intended:
- Expand list-valued patterns
- Do not change non-list-valued tokens
"""
cases = {
"no-tokens-to-replace": ["no-tokens-to-replace"],
"single-dict:{minion}": ["single-dict:{minion}"],
"single-list:{grains[roles]}": ["single-list:web", "single-list:database"],
"multiple-lists:{grains[roles]}+{grains[aux]}": [
"multiple-lists:web+foo",
"multiple-lists:web+bar",
"multiple-lists:database+foo",
"multiple-lists:database+bar",
],
"single-list-with-dicts:{grains[id]}+{grains[roles]}+{grains[id]}": [
"single-list-with-dicts:{grains[id]}+web+{grains[id]}",
"single-list-with-dicts:{grains[id]}+database+{grains[id]}",
],
"deeply-nested-list:{grains[deep][foo][bar][baz]}": [
"deeply-nested-list:hello",
"deeply-nested-list:world",
],
}
# The mappings dict is assembled in _get_policies, so emulate here
mappings = {"minion": self.grains["id"], "grains": self.grains}
for case, correct_output in cases.items():
output = vault._expand_pattern_lists(
case, **mappings
) # pylint: disable=protected-access
diff = set(output).symmetric_difference(set(correct_output))
if diff:
log.debug("Test %s failed", case)
log.debug("Expected:\n\t%s\nGot\n\t%s", output, correct_output)
log.debug("Difference:\n\t%s", diff)
self.assertEqual(output, correct_output)
def test_get_policies_for_nonexisting_minions(self):
minion_id = "salt_master"
# For non-existing minions, or the master-minion, grains will be None
cases = {
"no-tokens-to-replace": ["no-tokens-to-replace"],
"single-dict:{minion}": ["single-dict:{}".format(minion_id)],
"single-list:{grains[roles]}": [],
"single-grain:{grains[id]}": [],
}
with patch(
"salt.utils.minions.get_minion_data",
MagicMock(return_value=(None, None, None)),
):
for case, correct_output in cases.items():
test_config = {"policies": [case]}
output = vault._get_policies(
minion_id, test_config
) # pylint: disable=protected-access
diff = set(output).symmetric_difference(set(correct_output))
if diff:
log.debug("Test %s failed", case)
log.debug("Expected:\n\t%s\nGot\n\t%s", output, correct_output)
log.debug("Difference:\n\t%s", diff)
self.assertEqual(output, correct_output)
with patch.dict(
vault.__utils__,
{
"vault.expand_pattern_lists": Mock(
side_effect=lambda x, *args, **kwargs: [x]
)
},
):
test_config = {"policies": [case]}
output = vault._get_policies(
minion_id, test_config
) # pylint: disable=protected-access
diff = set(output).symmetric_difference(set(correct_output))
if diff:
log.debug("Test %s failed", case)
log.debug("Expected:\n\t%s\nGot\n\t%s", output, correct_output)
log.debug("Difference:\n\t%s", diff)
self.assertEqual(output, correct_output)
def test_get_policies(self):
"""
Ensure _get_policies works as intended, including expansion of lists
Ensure _get_policies works as intended.
The expansion of lists is tested in the vault utility module unit tests.
"""
cases = {
"no-tokens-to-replace": ["no-tokens-to-replace"],
"single-dict:{minion}": ["single-dict:test-minion"],
"single-list:{grains[roles]}": ["single-list:web", "single-list:database"],
"multiple-lists:{grains[roles]}+{grains[aux]}": [
"multiple-lists:web+foo",
"multiple-lists:web+bar",
"multiple-lists:database+foo",
"multiple-lists:database+bar",
],
"single-list-with-dicts:{grains[id]}+{grains[roles]}+{grains[id]}": [
"single-list-with-dicts:test-minion+web+test-minion",
"single-list-with-dicts:test-minion+database+test-minion",
],
"deeply-nested-list:{grains[deep][foo][bar][baz]}": [
"deeply-nested-list:hello",
"deeply-nested-list:world",
],
"should-not-cause-an-exception,but-result-empty:{foo}": [],
"Case-Should-Be-Lowered:{grains[mixedcase]}": [
"case-should-be-lowered:up-low-up"
@ -129,16 +84,24 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
MagicMock(return_value=(None, self.grains, None)),
):
for case, correct_output in cases.items():
test_config = {"policies": [case]}
output = vault._get_policies(
"test-minion", test_config
) # pylint: disable=protected-access
diff = set(output).symmetric_difference(set(correct_output))
if diff:
log.debug("Test %s failed", case)
log.debug("Expected:\n\t%s\nGot\n\t%s", output, correct_output)
log.debug("Difference:\n\t%s", diff)
self.assertEqual(output, correct_output)
with patch.dict(
vault.__utils__,
{
"vault.expand_pattern_lists": Mock(
side_effect=lambda x, *args, **kwargs: [x]
)
},
):
test_config = {"policies": [case]}
output = vault._get_policies(
"test-minion", test_config
) # pylint: disable=protected-access
diff = set(output).symmetric_difference(set(correct_output))
if diff:
log.debug("Test %s failed", case)
log.debug("Expected:\n\t%s\nGot\n\t%s", output, correct_output)
log.debug("Difference:\n\t%s", diff)
self.assertEqual(output, correct_output)
def test_get_token_create_url(self):
"""