Merge pull request #48586 from astorath/add-vault-token-role

added named roles feature for module.vault
This commit is contained in:
Nicole Thomas 2018-07-20 10:38:59 -04:00 committed by GitHub
commit 103d6dcec6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 174 additions and 15 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)