mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Features/vault enterprise (#58586)
* runner, utils and docs * revert that * changelog update * revert to existing structure to unblock tests * pulling in Teds fixes * resolve uninitialized namespace * Add namespace key to test inputs * remove headers creation without Namespace * Revert "remove headers creation without Namespace" This reverts commit f72cb25ff5297cacc6121570fdb3a99967f34087. * headers=ANY to account for namespace logic change * Add unit tests * lint update * utils test fix * runner, utils and docs * revert to existing structure to unblock tests * pulling in Teds fixes * remove headers creation without Namespace * Revert "remove headers creation without Namespace" This reverts commit f72cb25ff5297cacc6121570fdb3a99967f34087. * Add unit tests * lint update * runner, utils and docs * revert that * revert to existing structure to unblock tests * pulling in Teds fixes * Add namespace key to test inputs * remove headers creation without Namespace * Revert "remove headers creation without Namespace" This reverts commit f72cb25ff5297cacc6121570fdb3a99967f34087. * Add unit tests * utils test fix * revert doc chnages * revert doc changes * update docs * make lint happy * remove py2 dep * layout fixes * Update salt/runners/vault.py Co-authored-by: Pedro Algarvio <pedro@algarvio.me> * Update salt/utils/vault.py Co-authored-by: Pedro Algarvio <pedro@algarvio.me> * Update salt/utils/vault.py Co-authored-by: Pedro Algarvio <pedro@algarvio.me> * Update tests/unit/runners/test_vault.py Co-authored-by: Pedro Algarvio <pedro@algarvio.me> * Update tests/unit/runners/test_vault.py Co-authored-by: Pedro Algarvio <pedro@algarvio.me> * Update tests/unit/runners/test_vault.py Co-authored-by: Pedro Algarvio <pedro@algarvio.me> * Delete Issues_55775.txt.bak * Update salt/utils/vault.py Co-authored-by: Pedro Algarvio <pedro@algarvio.me> * Update salt/utils/vault.py Co-authored-by: Pedro Algarvio <pedro@algarvio.me> * Update salt/utils/vault.py Co-authored-by: Pedro Algarvio <pedro@algarvio.me> * Update salt/runners/vault.py Co-authored-by: Pedro Algarvio <pedro@algarvio.me> * Update salt/modules/vault.py Co-authored-by: Megan Wilhite <megan.wilhite@gmail.com> * Update vault.py Linting Co-authored-by: Patrick McConnell <pmcconnell@tucows.com> Co-authored-by: vveliev <vveliev@tucows.com> Co-authored-by: Pedro Algarvio <pedro@algarvio.me> Co-authored-by: Megan Wilhite <megan.wilhite@gmail.com>
This commit is contained in:
parent
72d64e30e5
commit
56911b683d
6 changed files with 134 additions and 23 deletions
1
changelog/58585.added
Normal file
1
changelog/58585.added
Normal file
|
@ -0,0 +1 @@
|
|||
Added namespace headers to allow use of namespace from config to communicate with Vault Enterprise namespaces
|
|
@ -25,6 +25,7 @@ Functions to interact with Hashicorp Vault.
|
|||
url: https://vault.service.domain:8200
|
||||
verify: /etc/ssl/certs/ca-certificates.crt
|
||||
role_name: minion_role
|
||||
namespace: vault_enterprice_namespace
|
||||
auth:
|
||||
method: approle
|
||||
role_id: 11111111-2222-3333-4444-1111111111111
|
||||
|
@ -49,6 +50,14 @@ Functions to interact with Hashicorp Vault.
|
|||
|
||||
.. versionadded:: 2018.3.0
|
||||
|
||||
namespaces
|
||||
Optional Vault Namespace. Used with Vault enterprice
|
||||
|
||||
For detail please see:
|
||||
https://www.vaultproject.io/docs/enterprise/namespaces
|
||||
|
||||
.. versionadded:: 3004
|
||||
|
||||
role_name
|
||||
Role name for minion tokens created. If omitted, minion tokens will be
|
||||
created without any role, thus being able to inherit any master token
|
||||
|
|
|
@ -52,6 +52,8 @@ def generate_token(
|
|||
try:
|
||||
config = __opts__.get("vault", {})
|
||||
verify = config.get("verify", None)
|
||||
# Vault Enterprise requires a namespace
|
||||
namespace = config.get("namespace")
|
||||
# Allow disabling of minion provided values via the master
|
||||
allow_minion_override = config["auth"].get("allow_minion_override", False)
|
||||
# This preserves the previous behavior of default TTL and 1 use
|
||||
|
@ -66,17 +68,24 @@ def generate_token(
|
|||
log.debug("Vault token expired. Recreating one")
|
||||
# Requesting a short ttl token
|
||||
url = "{}/v1/auth/approle/login".format(config["url"])
|
||||
|
||||
payload = {"role_id": config["auth"]["role_id"]}
|
||||
if "secret_id" in config["auth"]:
|
||||
payload["secret_id"] = config["auth"]["secret_id"]
|
||||
response = requests.post(url, json=payload, verify=verify)
|
||||
# Vault Enterprise call requires headers
|
||||
headers = None
|
||||
if namespace is not None:
|
||||
headers = {"X-Vault-Namespace": namespace}
|
||||
response = requests.post(
|
||||
url, headers=headers, json=payload, verify=verify
|
||||
)
|
||||
if response.status_code != 200:
|
||||
return {"error": response.reason}
|
||||
config["auth"]["token"] = response.json()["auth"]["client_token"]
|
||||
|
||||
url = _get_token_create_url(config)
|
||||
headers = {"X-Vault-Token": config["auth"]["token"]}
|
||||
if namespace is not None:
|
||||
headers["X-Vault-Namespace"] = namespace
|
||||
audit_data = {
|
||||
"saltstack-jid": globals().get("__jid__", "<no jid set>"),
|
||||
"saltstack-minion": minion_id,
|
||||
|
@ -109,6 +118,7 @@ def generate_token(
|
|||
"url": config["url"],
|
||||
"verify": verify,
|
||||
"token_backend": storage_type,
|
||||
"namespace": namespace,
|
||||
}
|
||||
if uses >= 0:
|
||||
ret["uses"] = uses
|
||||
|
@ -267,10 +277,15 @@ def _selftoken_expired():
|
|||
"""
|
||||
try:
|
||||
verify = __opts__["vault"].get("verify", None)
|
||||
# Vault Enterprise requires a namespace
|
||||
namespace = __opts__["vault"].get("namespace")
|
||||
url = "{}/v1/auth/token/lookup-self".format(__opts__["vault"]["url"])
|
||||
if "token" not in __opts__["vault"]["auth"]:
|
||||
return True
|
||||
headers = {"X-Vault-Token": __opts__["vault"]["auth"]["token"]}
|
||||
# Add Vault namespace to headers if Vault Enterprise enabled
|
||||
if namespace is not None:
|
||||
headers["X-Vault-Namespace"] = namespace
|
||||
response = requests.get(url, headers=headers, verify=verify)
|
||||
if response.status_code != 200:
|
||||
return True
|
||||
|
|
|
@ -103,6 +103,7 @@ def _get_token_and_url_from_master():
|
|||
"url": result["url"],
|
||||
"token": result["token"],
|
||||
"verify": result.get("verify", None),
|
||||
"namespace": result.get("namespace"),
|
||||
"uses": result.get("uses", 1),
|
||||
"lease_duration": result["lease_duration"],
|
||||
"issued": result["issued"],
|
||||
|
@ -117,6 +118,8 @@ def get_vault_connection():
|
|||
|
||||
def _use_local_config():
|
||||
log.debug("Using Vault connection details from local config")
|
||||
# Vault Enterprise requires a namespace
|
||||
namespace = __opts__["vault"].get("namespace")
|
||||
try:
|
||||
if __opts__["vault"]["auth"]["method"] == "approle":
|
||||
verify = __opts__["vault"].get("verify", None)
|
||||
|
@ -127,7 +130,13 @@ def get_vault_connection():
|
|||
payload = {"role_id": __opts__["vault"]["auth"]["role_id"]}
|
||||
if "secret_id" in __opts__["vault"]["auth"]:
|
||||
payload["secret_id"] = __opts__["vault"]["auth"]["secret_id"]
|
||||
response = requests.post(url, json=payload, verify=verify)
|
||||
if namespace is not None:
|
||||
headers = {"X-Vault-Namespace": namespace}
|
||||
response = requests.post(
|
||||
url, headers=headers, json=payload, verify=verify
|
||||
)
|
||||
else:
|
||||
response = requests.post(url, json=payload, verify=verify)
|
||||
if response.status_code != 200:
|
||||
errmsg = "An error occurred while getting a token from approle"
|
||||
raise salt.exceptions.CommandExecutionError(errmsg)
|
||||
|
@ -139,6 +148,8 @@ def get_vault_connection():
|
|||
if _wrapped_token_valid():
|
||||
url = "{}/v1/sys/wrapping/unwrap".format(__opts__["vault"]["url"])
|
||||
headers = {"X-Vault-Token": __opts__["vault"]["auth"]["token"]}
|
||||
if namespace is not None:
|
||||
headers["X-Vault-Namespace"] = namespace
|
||||
response = requests.post(url, headers=headers, verify=verify)
|
||||
if response.status_code != 200:
|
||||
errmsg = "An error occured while unwrapping vault token"
|
||||
|
@ -148,6 +159,7 @@ def get_vault_connection():
|
|||
]
|
||||
return {
|
||||
"url": __opts__["vault"]["url"],
|
||||
"namespace": namespace,
|
||||
"token": __opts__["vault"]["auth"]["token"],
|
||||
"verify": __opts__["vault"].get("verify", None),
|
||||
"issued": int(round(time.time())),
|
||||
|
@ -287,6 +299,7 @@ def make_request(
|
|||
resource,
|
||||
token=None,
|
||||
vault_url=None,
|
||||
namespace=None,
|
||||
get_token_url=False,
|
||||
retry=False,
|
||||
**args
|
||||
|
@ -300,6 +313,7 @@ def make_request(
|
|||
connection = get_cache()
|
||||
token = connection["token"] if not token else token
|
||||
vault_url = connection["url"] if not vault_url else vault_url
|
||||
namespace = namespace or connection["namespace"]
|
||||
if "verify" in args:
|
||||
args["verify"] = args["verify"]
|
||||
else:
|
||||
|
@ -310,6 +324,8 @@ def make_request(
|
|||
pass
|
||||
url = "{}/{}".format(vault_url, resource)
|
||||
headers = {"X-Vault-Token": str(token), "Content-Type": "application/json"}
|
||||
if namespace is not None:
|
||||
headers["X-Vault-Namespace"] = namespace
|
||||
response = requests.request(method, url, headers=headers, **args)
|
||||
if not response.ok and response.json().get("errors", None) == ["permission denied"]:
|
||||
log.info("Permission denied from vault")
|
||||
|
@ -363,10 +379,14 @@ def _selftoken_expired():
|
|||
"""
|
||||
try:
|
||||
verify = __opts__["vault"].get("verify", None)
|
||||
# Vault Enterprise requires a namespace
|
||||
namespace = __opts__["vault"].get("namespace")
|
||||
url = "{}/v1/auth/token/lookup-self".format(__opts__["vault"]["url"])
|
||||
if "token" not in __opts__["vault"]["auth"]:
|
||||
return True
|
||||
headers = {"X-Vault-Token": __opts__["vault"]["auth"]["token"]}
|
||||
if namespace is not None:
|
||||
headers["X-Vault-Namespace"] = namespace
|
||||
response = requests.get(url, headers=headers, verify=verify)
|
||||
if response.status_code != 200:
|
||||
return True
|
||||
|
@ -383,10 +403,14 @@ def _wrapped_token_valid():
|
|||
"""
|
||||
try:
|
||||
verify = __opts__["vault"].get("verify", None)
|
||||
# Vault Enterprise requires a namespace
|
||||
namespace = __opts__["vault"].get("namespace")
|
||||
url = "{}/v1/sys/wrapping/lookup".format(__opts__["vault"]["url"])
|
||||
if "token" not in __opts__["vault"]["auth"]:
|
||||
return False
|
||||
headers = {"X-Vault-Token": __opts__["vault"]["auth"]["token"]}
|
||||
if namespace is not None:
|
||||
headers["X-Vault-Namespace"] = namespace
|
||||
response = requests.post(url, headers=headers, verify=verify)
|
||||
if response.status_code != 200:
|
||||
return False
|
||||
|
|
|
@ -1,19 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Unit tests for the Vault runner
|
||||
"""
|
||||
|
||||
# Import Python Libs
|
||||
from __future__ import absolute_import, print_function, unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
import salt.runners.vault as vault
|
||||
|
||||
# Import salt libs
|
||||
from salt.ext import six
|
||||
|
||||
# Import Salt Testing Libs
|
||||
from tests.support.mixins import LoaderModuleMockMixin
|
||||
from tests.support.mock import ANY, MagicMock, Mock, call, patch
|
||||
from tests.support.unit import TestCase
|
||||
|
@ -69,7 +61,7 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
|||
|
||||
# The mappings dict is assembled in _get_policies, so emulate here
|
||||
mappings = {"minion": self.grains["id"], "grains": self.grains}
|
||||
for case, correct_output in six.iteritems(cases):
|
||||
for case, correct_output in cases.items():
|
||||
output = vault._expand_pattern_lists(
|
||||
case, **mappings
|
||||
) # pylint: disable=protected-access
|
||||
|
@ -85,14 +77,14 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
|||
# 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:{0}".format(minion_id)],
|
||||
"single-dict:{minion}": ["single-dict:{}".format(minion_id)],
|
||||
"single-list:{grains[roles]}": [],
|
||||
}
|
||||
with patch(
|
||||
"salt.utils.minions.get_minion_data",
|
||||
MagicMock(return_value=(None, None, None)),
|
||||
):
|
||||
for case, correct_output in six.iteritems(cases):
|
||||
for case, correct_output in cases.items():
|
||||
test_config = {"policies": [case]}
|
||||
output = vault._get_policies(
|
||||
minion_id, test_config
|
||||
|
@ -136,7 +128,7 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
|||
"salt.utils.minions.get_minion_data",
|
||||
MagicMock(return_value=(None, self.grains, None)),
|
||||
):
|
||||
for case, correct_output in six.iteritems(cases):
|
||||
for case, correct_output in cases.items():
|
||||
test_config = {"policies": [case]}
|
||||
output = vault._get_policies(
|
||||
"test-minion", test_config
|
||||
|
@ -290,6 +282,37 @@ class VaultTokenAuthTest(TestCase, LoaderModuleMockMixin):
|
|||
self.assertTrue("error" in result)
|
||||
self.assertEqual(result["error"], "Test Exception Reason")
|
||||
|
||||
@patch("salt.runners.vault._validate_signature", MagicMock(return_value=None))
|
||||
@patch(
|
||||
"salt.runners.vault._get_token_create_url",
|
||||
MagicMock(return_value="http://fake_url"),
|
||||
)
|
||||
def test_generate_token_with_namespace(self):
|
||||
"""
|
||||
Basic tests for test_generate_token: all exits
|
||||
"""
|
||||
mock = _mock_json_response(
|
||||
{"auth": {"client_token": "test", "renewable": False, "lease_duration": 0}}
|
||||
)
|
||||
supplied_config = {"namespace": "test_namespace"}
|
||||
with patch("requests.post", mock):
|
||||
with patch.dict(vault.__opts__["vault"], supplied_config):
|
||||
result = vault.generate_token("test-minion", "signature")
|
||||
log.debug("generate_token result: %s", result)
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertNotIn("error", result)
|
||||
self.assertIn("token", result)
|
||||
self.assertEqual(result["token"], "test")
|
||||
mock.assert_called_with(
|
||||
"http://fake_url",
|
||||
headers={
|
||||
"X-Vault-Token": "test",
|
||||
"X-Vault-Namespace": "test_namespace",
|
||||
},
|
||||
json=ANY,
|
||||
verify=ANY,
|
||||
)
|
||||
|
||||
|
||||
class VaultAppRoleAuthTest(TestCase, LoaderModuleMockMixin):
|
||||
"""
|
||||
|
@ -332,7 +355,12 @@ class VaultAppRoleAuthTest(TestCase, LoaderModuleMockMixin):
|
|||
self.assertTrue("token" in result)
|
||||
self.assertEqual(result["token"], "test")
|
||||
calls = [
|
||||
call("http://127.0.0.1/v1/auth/approle/login", json=ANY, verify=ANY),
|
||||
call(
|
||||
"http://127.0.0.1/v1/auth/approle/login",
|
||||
headers=ANY,
|
||||
json=ANY,
|
||||
verify=ANY,
|
||||
),
|
||||
call("http://fake_url", headers=ANY, json=ANY, verify=ANY),
|
||||
]
|
||||
mock.assert_has_calls(calls)
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test case for the vault utils module
|
||||
"""
|
||||
|
||||
# Import python libs
|
||||
from __future__ import absolute_import, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from copy import copy
|
||||
|
||||
# Import Salt libs
|
||||
import salt.utils.vault as vault
|
||||
from tests.support.mixins import LoaderModuleMockMixin
|
||||
from tests.support.mock import ANY, MagicMock, Mock, mock_open, patch
|
||||
|
||||
# Import Salt Testing libs
|
||||
from tests.support.unit import TestCase
|
||||
|
||||
|
||||
|
@ -57,6 +50,16 @@ class TestVaultUtils(LoaderModuleMockMixin, TestCase):
|
|||
"url": "http://127.0.0.1:8200",
|
||||
"token": "test",
|
||||
"verify": None,
|
||||
"namespace": None,
|
||||
"uses": 1,
|
||||
"lease_duration": 100,
|
||||
"issued": 3000,
|
||||
}
|
||||
cache_single_namespace = {
|
||||
"url": "http://127.0.0.1:8200",
|
||||
"token": "test",
|
||||
"verify": None,
|
||||
"namespace": "test_namespace",
|
||||
"uses": 1,
|
||||
"lease_duration": 100,
|
||||
"issued": 3000,
|
||||
|
@ -65,6 +68,7 @@ class TestVaultUtils(LoaderModuleMockMixin, TestCase):
|
|||
"url": "http://127.0.0.1:8200",
|
||||
"token": "test",
|
||||
"verify": None,
|
||||
"namespace": None,
|
||||
"uses": 10,
|
||||
"lease_duration": 100,
|
||||
"issued": 3000,
|
||||
|
@ -74,6 +78,7 @@ class TestVaultUtils(LoaderModuleMockMixin, TestCase):
|
|||
"url": "http://127.0.0.1:8200",
|
||||
"token": "test",
|
||||
"verify": None,
|
||||
"namespace": None,
|
||||
"uses": 1,
|
||||
"lease_duration": 100,
|
||||
"issued": 3000,
|
||||
|
@ -83,6 +88,7 @@ class TestVaultUtils(LoaderModuleMockMixin, TestCase):
|
|||
"url": "http://127.0.0.1:8200",
|
||||
"token": "test",
|
||||
"verify": None,
|
||||
"namespace": None,
|
||||
"uses": 0,
|
||||
"lease_duration": 100,
|
||||
"issued": 3000,
|
||||
|
@ -193,6 +199,7 @@ class TestVaultUtils(LoaderModuleMockMixin, TestCase):
|
|||
"url": "http://127.0.0.1:8200",
|
||||
"token": "test",
|
||||
"verify": None,
|
||||
"namespace": None,
|
||||
"uses": 9,
|
||||
"lease_duration": 100,
|
||||
"issued": 3000,
|
||||
|
@ -327,6 +334,7 @@ class TestVaultUtils(LoaderModuleMockMixin, TestCase):
|
|||
"url": "http://127.0.0.1:8200",
|
||||
"token": "test",
|
||||
"verify": None,
|
||||
"namespace": None,
|
||||
"uses": 10,
|
||||
"lease_duration": 100,
|
||||
"issued": 3000,
|
||||
|
@ -354,6 +362,7 @@ class TestVaultUtils(LoaderModuleMockMixin, TestCase):
|
|||
"url": "http://127.0.0.1:8200",
|
||||
"token": "test",
|
||||
"verify": None,
|
||||
"namespace": None,
|
||||
"uses": 0,
|
||||
"lease_duration": 100,
|
||||
"issued": 3000,
|
||||
|
@ -362,6 +371,7 @@ class TestVaultUtils(LoaderModuleMockMixin, TestCase):
|
|||
"url": "http://127.0.0.1:8200",
|
||||
"token": "test",
|
||||
"verify": None,
|
||||
"namespace": None,
|
||||
"uses": 0,
|
||||
"lease_duration": 100,
|
||||
"issued": 3000,
|
||||
|
@ -399,6 +409,30 @@ class TestVaultUtils(LoaderModuleMockMixin, TestCase):
|
|||
function_return = vault.is_v2("secret/mything")
|
||||
self.assertEqual(function_return, expected_return)
|
||||
|
||||
def test_request_with_namespace(self):
|
||||
"""
|
||||
Test request with namespace configured
|
||||
"""
|
||||
mock = self._mock_json_response(self.json_success)
|
||||
expected_headers = {
|
||||
"X-Vault-Token": "test",
|
||||
"X-Vault-Namespace": "test_namespace",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
supplied_config = {"namespace": "test_namespace"}
|
||||
supplied_context = {"vault_token": copy(self.cache_single_namespace)}
|
||||
with patch.dict(vault.__context__, supplied_context):
|
||||
with patch.dict(vault.__opts__["vault"], supplied_config):
|
||||
with patch("requests.request", mock):
|
||||
vault_return = vault.make_request("/secret/my/secret", "key")
|
||||
mock.assert_called_with(
|
||||
"/secret/my/secret",
|
||||
"http://127.0.0.1:8200/key",
|
||||
headers=expected_headers,
|
||||
verify=ANY,
|
||||
)
|
||||
self.assertEqual(vault_return.json(), self.json_success)
|
||||
|
||||
def test_get_secret_path_metadata_no_cache(self):
|
||||
"""
|
||||
test with no cache file
|
||||
|
|
Loading…
Add table
Reference in a new issue