mirror of
https://github.com/valitydev/salt.git
synced 2024-11-07 17:09:03 +00:00
Merge pull request #48586 from astorath/add-vault-token-role
added named roles feature for module.vault
This commit is contained in:
commit
103d6dcec6
@ -25,9 +25,11 @@ Functions to interact with Hashicorp Vault.
|
||||
vault:
|
||||
url: https://vault.service.domain:8200
|
||||
verify: /etc/ssl/certs/ca-certificates.crt
|
||||
role_name: minion_role
|
||||
auth:
|
||||
method: token
|
||||
token: 11111111-2222-3333-4444-555555555555
|
||||
method: approle
|
||||
role_id: 11111111-2222-3333-4444-1111111111111
|
||||
secret_id: 11111111-1111-1111-1111-1111111111111
|
||||
policies:
|
||||
- saltstack/minions
|
||||
- saltstack/minion/{minion}
|
||||
@ -48,11 +50,28 @@ Functions to interact with Hashicorp Vault.
|
||||
|
||||
.. versionadded:: 2018.3.0
|
||||
|
||||
auth
|
||||
Currently only token auth is supported. The token must be able to create
|
||||
tokens with the policies that should be assigned to minions. Required.
|
||||
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
|
||||
policy (including token creation capabilities). Optional.
|
||||
|
||||
You can still use the token via a OS environment variable via this
|
||||
For details please see:
|
||||
https://www.vaultproject.io/api/auth/token/index.html#create-token
|
||||
Example configuration:
|
||||
https://www.nomadproject.io/docs/vault-integration/index.html#vault-token-role-configuration
|
||||
|
||||
auth
|
||||
Currently only token and approle auth types are supported. Required.
|
||||
|
||||
Approle is the preferred way to authenticate with Vault as it provide
|
||||
some advanced options to control authentication process.
|
||||
Please visit Vault documentation for more info:
|
||||
https://www.vaultproject.io/docs/auth/approle.html
|
||||
|
||||
The token must be able to create tokens with the policies that should be
|
||||
assigned to minions.
|
||||
|
||||
You can still use the token auth via a OS environment variable via this
|
||||
config example:
|
||||
|
||||
.. code-block: yaml
|
||||
@ -65,7 +84,6 @@ Functions to interact with Hashicorp Vault.
|
||||
osenv:
|
||||
driver: env
|
||||
|
||||
|
||||
And then export the VAULT_TOKEN variable in your OS:
|
||||
|
||||
.. code-block: bash
|
||||
|
@ -55,6 +55,7 @@ def generate_token(minion_id, signature, impersonated_by_master=False):
|
||||
log.debug('Vault token expired. Recreating one')
|
||||
# Requesting a short ttl token
|
||||
url = '{0}/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']
|
||||
@ -63,7 +64,7 @@ def generate_token(minion_id, signature, impersonated_by_master=False):
|
||||
return {'error': response.reason}
|
||||
config['auth']['token'] = response.json()['auth']['client_token']
|
||||
|
||||
url = '{0}/v1/auth/token/create'.format(config['url'])
|
||||
url = _get_token_create_url(config)
|
||||
headers = {'X-Vault-Token': config['auth']['token']}
|
||||
audit_data = {
|
||||
'saltstack-jid': globals().get('__jid__', '<no jid set>'),
|
||||
@ -73,7 +74,7 @@ def generate_token(minion_id, signature, impersonated_by_master=False):
|
||||
payload = {
|
||||
'policies': _get_policies(minion_id, config),
|
||||
'num_uses': 1,
|
||||
'metadata': audit_data
|
||||
'meta': audit_data
|
||||
}
|
||||
|
||||
if payload['policies'] == []:
|
||||
@ -85,9 +86,9 @@ def generate_token(minion_id, signature, impersonated_by_master=False):
|
||||
if response.status_code != 200:
|
||||
return {'error': response.reason}
|
||||
|
||||
authData = response.json()['auth']
|
||||
auth_data = response.json()['auth']
|
||||
return {
|
||||
'token': authData['client_token'],
|
||||
'token': auth_data['client_token'],
|
||||
'url': config['url'],
|
||||
'verify': verify,
|
||||
}
|
||||
@ -256,3 +257,13 @@ def _selftoken_expired():
|
||||
raise salt.exceptions.CommandExecutionError(
|
||||
'Error while looking up self token : {0}'.format(six.text_type(e))
|
||||
)
|
||||
|
||||
|
||||
def _get_token_create_url(config):
|
||||
'''
|
||||
Create Vault url for token creation
|
||||
'''
|
||||
role_name = config.get('role_name', None)
|
||||
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)
|
||||
|
@ -12,9 +12,12 @@ from tests.support.mixins import LoaderModuleMockMixin
|
||||
from tests.support.unit import skipIf, TestCase
|
||||
from tests.support.mock import (
|
||||
MagicMock,
|
||||
Mock,
|
||||
patch,
|
||||
NO_MOCK,
|
||||
NO_MOCK_REASON
|
||||
NO_MOCK_REASON,
|
||||
ANY,
|
||||
call
|
||||
)
|
||||
|
||||
# Import salt libs
|
||||
@ -84,7 +87,7 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
||||
for case, correct_output in six.iteritems(cases):
|
||||
output = vault._expand_pattern_lists(case, **mappings) # pylint: disable=protected-access
|
||||
diff = set(output).symmetric_difference(set(correct_output))
|
||||
if len(diff) != 0:
|
||||
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)
|
||||
@ -104,7 +107,7 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
||||
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 len(diff) != 0:
|
||||
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)
|
||||
@ -145,8 +148,135 @@ class VaultTest(TestCase, LoaderModuleMockMixin):
|
||||
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 len(diff) != 0:
|
||||
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):
|
||||
'''
|
||||
Ensure _get_token_create_url parses config correctly
|
||||
'''
|
||||
self.assertEqual(vault._get_token_create_url( # pylint: disable=protected-access
|
||||
{"url": "http://127.0.0.1"}),
|
||||
"http://127.0.0.1/v1/auth/token/create")
|
||||
self.assertEqual(vault._get_token_create_url( # pylint: disable=protected-access
|
||||
{"url": "https://127.0.0.1/"}),
|
||||
"https://127.0.0.1/v1/auth/token/create")
|
||||
self.assertEqual(vault._get_token_create_url( # pylint: disable=protected-access
|
||||
{"url": "http://127.0.0.1:8200", "role_name": "therole"}),
|
||||
"http://127.0.0.1:8200/v1/auth/token/create/therole")
|
||||
self.assertEqual(vault._get_token_create_url( # pylint: disable=protected-access
|
||||
{"url": "https://127.0.0.1/test", "role_name": "therole"}),
|
||||
"https://127.0.0.1/test/v1/auth/token/create/therole")
|
||||
|
||||
|
||||
def _mock_json_response(data, status_code=200, reason=""):
|
||||
'''
|
||||
Mock helper for http response
|
||||
'''
|
||||
response = MagicMock()
|
||||
response.json = MagicMock(return_value=data)
|
||||
response.status_code = status_code
|
||||
response.reason = reason
|
||||
return Mock(return_value=response)
|
||||
|
||||
|
||||
class VaultTokenAuthTest(TestCase, LoaderModuleMockMixin):
|
||||
'''
|
||||
Tests for the runner module of the Vault with token setup
|
||||
'''
|
||||
|
||||
def setup_loader_modules(self):
|
||||
return {
|
||||
vault: {
|
||||
'__opts__': {
|
||||
'vault': {
|
||||
'url': "http://127.0.0.1",
|
||||
"auth": {
|
||||
'token': 'test',
|
||||
'method': 'token'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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(self):
|
||||
'''
|
||||
Basic tests for test_generate_token: all exits
|
||||
'''
|
||||
mock = _mock_json_response({'auth': {'client_token': 'test'}})
|
||||
with patch('requests.post', mock):
|
||||
result = vault.generate_token('test-minion', 'signature')
|
||||
log.debug('generate_token result: %s', result)
|
||||
self.assertTrue(isinstance(result, dict))
|
||||
self.assertFalse('error' in result)
|
||||
self.assertTrue('token' in result)
|
||||
self.assertEqual(result['token'], 'test')
|
||||
mock.assert_called_with("http://fake_url", headers=ANY, json=ANY, verify=ANY)
|
||||
|
||||
mock = _mock_json_response({}, status_code=403, reason="no reason")
|
||||
with patch('requests.post', mock):
|
||||
result = vault.generate_token('test-minion', 'signature')
|
||||
self.assertTrue(isinstance(result, dict))
|
||||
self.assertTrue('error' in result)
|
||||
self.assertEqual(result['error'], "no reason")
|
||||
|
||||
with patch('salt.runners.vault._get_policies', MagicMock(return_value=[])):
|
||||
result = vault.generate_token('test-minion', 'signature')
|
||||
self.assertTrue(isinstance(result, dict))
|
||||
self.assertTrue('error' in result)
|
||||
self.assertEqual(result['error'], 'No policies matched minion')
|
||||
|
||||
with patch('requests.post',
|
||||
MagicMock(side_effect=Exception('Test Exception Reason'))):
|
||||
result = vault.generate_token('test-minion', 'signature')
|
||||
self.assertTrue(isinstance(result, dict))
|
||||
self.assertTrue('error' in result)
|
||||
self.assertEqual(result['error'], 'Test Exception Reason')
|
||||
|
||||
|
||||
class VaultAppRoleAuthTest(TestCase, LoaderModuleMockMixin):
|
||||
'''
|
||||
Tests for the runner module of the Vault with approle setup
|
||||
'''
|
||||
|
||||
def setup_loader_modules(self):
|
||||
return {
|
||||
vault: {
|
||||
'__opts__': {
|
||||
'vault': {
|
||||
'url': "http://127.0.0.1",
|
||||
"auth": {
|
||||
'method': 'approle',
|
||||
'role_id': 'role',
|
||||
'secret_id': 'secret'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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(self):
|
||||
'''
|
||||
Basic test for test_generate_token with approle (two vault calls)
|
||||
'''
|
||||
mock = _mock_json_response({'auth': {'client_token': 'test'}})
|
||||
with patch('requests.post', mock):
|
||||
result = vault.generate_token('test-minion', 'signature')
|
||||
log.debug('generate_token result: %s', result)
|
||||
self.assertTrue(isinstance(result, dict))
|
||||
self.assertFalse('error' in result)
|
||||
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://fake_url", headers=ANY, json=ANY, verify=ANY)
|
||||
]
|
||||
mock.assert_has_calls(calls)
|
||||
|
Loading…
Reference in New Issue
Block a user