Skip to content

Commit

Permalink
add userpass auth tests (#192)
Browse files Browse the repository at this point in the history
* update comment in JWT auth

* add unit tests for userpass auth

* add integration tests for userpass auth

* fix localenv coverage regression
  • Loading branch information
briantist authored Nov 13, 2021
1 parent 5117d42 commit 8852550
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 3 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ansible-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -380,15 +380,15 @@ jobs:

# ansible-test support producing code coverage data
- name: Generate coverage report
if: ${{ matrix.docker && github.event_name != 'schedule' }}
if: ${{ github.event_name != 'schedule' }}
run: ansible-test coverage xml -v --requirements --group-by command --group-by environment --group-by target
working-directory: ${{ env.COLLECTION_PATH }}

- name: Upload ${{ github.job }} coverage reports
if: ${{ matrix.docker && github.event_name != 'schedule' }}
if: ${{ github.event_name != 'schedule' }}
uses: actions/upload-artifact@v2
with:
name: coverage=${{ github.job }}=${{ matrix.runner }}=${{ matrix.docker }}=ansible_${{ matrix.ansible }}=${{ matrix.python }}=data
name: coverage=${{ github.job }}=${{ matrix.runner }}=ansible_${{ matrix.ansible }}=${{ matrix.python }}=data
path: ${{ env.COLLECTION_PATH }}/tests/output/reports/
if-no-files-found: error
retention-days: 1
Expand Down
2 changes: 2 additions & 0 deletions plugins/module_utils/_auth_method_jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ def authenticate(self, client, use_token=True):

# must manually set the client token with JWT login
# see https://github.com/hvac/hvac/issues/644
# fixed in https://github.com/hvac/hvac/pull/746
# but we do it manually to maintain compatibilty with older hvac versions.
if use_token:
client.token = response['auth']['client_token']

Expand Down
2 changes: 2 additions & 0 deletions tests/integration/targets/auth_userpass/aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
vault/auth/userpass
context/target
14 changes: 14 additions & 0 deletions tests/integration/targets/auth_userpass/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
ansible_hashi_vault_url: '{{ vault_test_server_http }}'
ansible_hashi_vault_auth_method: userpass

auth_paths:
- userpass
- userpass-alt

userpass_username: testuser
userpass_password: testpass

vault_userpass_canary:
path: cubbyhole/configure_userpass
value: complete # value does not matter
4 changes: 4 additions & 0 deletions tests/integration/targets/auth_userpass/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
dependencies:
- setup_vault_test_plugins
- setup_vault_configure
50 changes: 50 additions & 0 deletions tests/integration/targets/auth_userpass/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
# task vars are not templated when used as vars, so we'll need to set_fact this evaluate the template
# see: https://github.com/ansible/ansible/issues/73268
- name: Persist defaults
set_fact:
'{{ item.key }}': "{{ lookup('vars', item.key) }}"
loop: "{{ lookup('file', role_path ~ '/defaults/main.yml') | from_yaml | dict2items }}"
loop_control:
label: '{{ item.key }}'

- name: Configuration tasks
module_defaults:
vault_ci_enable_auth: '{{ vault_plugins_module_defaults_common }}'
vault_ci_policy_put: '{{ vault_plugins_module_defaults_common }}'
vault_ci_write: '{{ vault_plugins_module_defaults_common }}'
vault_ci_read: '{{ vault_plugins_module_defaults_common }}'
block:
- name: Canary for userpass auth
vault_ci_read:
path: '{{ vault_userpass_canary.path }}'
register: canary

- name: Configure userpass
when: canary.result is none
loop: '{{ auth_paths }}'
include_tasks:
file: userpass_setup.yml
apply:
vars:
default_path: '{{ ansible_hashi_vault_auth_method }}'
this_path: '{{ item }}'

- name: Write Canary
when: canary.result is none
vault_ci_write:
path: '{{ vault_userpass_canary.path }}'
data:
value: '{{ vault_userpass_canary.value }}'

- name: Run userpass tests
loop: '{{ auth_paths | product(["target", "controller"]) | list }}'
include_tasks:
file: userpass_test_{{ item[1] }}.yml
apply:
vars:
default_path: '{{ ansible_hashi_vault_auth_method }}'
this_path: '{{ item[0] }}'
module_defaults:
assert:
quiet: yes
27 changes: 27 additions & 0 deletions tests/integration/targets/auth_userpass/tasks/userpass_setup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
- name: "Setup block"
vars:
is_default_path: "{{ this_path == default_path }}"
block:
- name: 'Enable the userpass auth method'
vault_ci_enable_auth:
method_type: userpass
path: '{{ omit if is_default_path else this_path }}'
config:
default_lease_ttl: 60m

- name: 'Create a userpass policy'
vault_ci_policy_put:
name: userpass-policy
policy: |
path "auth/{{ this_path }}/login" {
capabilities = [ "create", "read" ]
}
- name: 'Create a named role'
vault_ci_write:
path: 'auth/{{ this_path }}/users/{{ userpass_username }}'
data:
# in docs, this is token_policies (changed in Vault 1.2)
# use 'policies' to support older versions
policies: "{{ 'test-policy' if is_default_path else 'alt-policy' }},userpass-policy"
password: '{{ userpass_password }}'
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
- name: "Test block"
vars:
is_default_path: "{{ this_path == default_path }}"
kwargs_mount: "{{ {} if is_default_path else {'mount_point': this_path} }}"
kwargs_common:
username: '{{ userpass_username }}'
kwargs: "{{ kwargs_common | combine(kwargs_mount) }}"
block:
# the purpose of this test is to catch when the plugin accepts mount_point but does not pass it into hvac
# we set the policy of the default mount to deny access to this secret and so we expect failure when the mount
# is default, and success when the mount is alternate
- name: Check auth mount differing result
set_fact:
response: "{{ lookup('vault_test_auth', '', password=userpass_password, **kwargs) }}"

- assert:
fail_msg: "A token from mount path '{{ this_path }}' had the wrong policy: {{ response.login.auth.policies }}"
that:
- ('test-policy' in response.login.auth.policies) | bool == is_default_path
- ('test-policy' not in response.login.auth.policies) | bool != is_default_path
- ('alt-policy' in response.login.auth.policies) | bool != is_default_path
- ('alt-policy' not in response.login.auth.policies) | bool == is_default_path

- name: Failure expected when erroneous credentials are used
set_fact:
response: "{{ lookup('vault_test_auth', '', password='fake', want_exception=true, **kwargs) }}"

- assert:
fail_msg: "An invalid password somehow did not cause a failure."
that:
- response is failed
- response.msg is search('invalid username or password')
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
- name: "Test block"
vars:
is_default_path: "{{ this_path == default_path }}"
module_defaults:
vault_test_auth:
url: '{{ ansible_hashi_vault_url }}'
auth_method: '{{ ansible_hashi_vault_auth_method }}'
mount_point: '{{ omit if is_default_path else this_path }}'
username: '{{ userpass_username }}'
password: '{{ userpass_password }}'
block:
# the purpose of this test is to catch when the plugin accepts mount_point but does not pass it into hvac
# we set the policy of the default mount to deny access to this secret and so we expect failure when the mount
# is default, and success when the mount is alternate
- name: Check auth mount differing result
register: response
vault_test_auth:

- assert:
fail_msg: "A token from mount path '{{ this_path }}' had the wrong policy: {{ response.login.auth.policies }}"
that:
- ('test-policy' in response.login.auth.policies) | bool == is_default_path
- ('test-policy' not in response.login.auth.policies) | bool != is_default_path
- ('alt-policy' in response.login.auth.policies) | bool != is_default_path
- ('alt-policy' not in response.login.auth.policies) | bool == is_default_path

- name: Failure expected when erroneous credentials are used
register: response
vault_test_auth:
password: fake
want_exception: yes

- assert:
fail_msg: "An invalid password somehow did not cause a failure."
that:
- response.inner is failed
- response.msg is search('invalid username or password')
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"auth": {
"accessor": "mQewzgKRx5Yui1h1eMemJlMu",
"client_token": "s.drgLxu6ZtttSVn5Zkoy0huMR",
"entity_id": "8a74ffd3-f71b-8ebe-7942-610428051ea9",
"lease_duration": 3600,
"metadata": {
"username": "testuser"
},
"orphan": true,
"policies": [
"alt-policy",
"default",
"userpass-policy"
],
"renewable": true,
"token_policies": [
"alt-policy",
"default",
"userpass-policy"
],
"token_type": "service"
},
"data": null,
"lease_duration": 0,
"lease_id": "",
"renewable": false,
"request_id": "511e8fba-83f0-4b7e-95ea-770aa19c1957",
"warnings": null,
"wrap_info": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021 Brian Scholer (@briantist)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import pytest

from ansible_collections.community.hashi_vault.tests.unit.compat import mock

from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_userpass import (
HashiVaultAuthMethodUserpass,
)

from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import (
HashiVaultAuthMethodBase,
HashiVaultValueError,
)


@pytest.fixture
def option_dict():
return {
'auth_method': 'userpass',
'username': None,
'password': None,
'mount_point': None,
}


@pytest.fixture
def userpass_password():
return 'opaque'


@pytest.fixture
def userpass_username():
return 'fake-user'


@pytest.fixture
def auth_userpass(adapter, warner):
return HashiVaultAuthMethodUserpass(adapter, warner)


@pytest.fixture
def userpass_login_response(fixture_loader):
return fixture_loader('userpass_login_response.json')


class TestAuthUserpass(object):

def test_auth_userpass_is_auth_method_base(self, auth_userpass):
assert isinstance(auth_userpass, HashiVaultAuthMethodUserpass)
assert issubclass(HashiVaultAuthMethodUserpass, HashiVaultAuthMethodBase)

def test_auth_userpass_validate_direct(self, auth_userpass, adapter, userpass_username, userpass_password):
adapter.set_option('username', userpass_username)
adapter.set_option('password', userpass_password)

auth_userpass.validate()

@pytest.mark.parametrize('opt_patch', [
{'username': 'user-only'},
{'password': 'password-only'},
])
def test_auth_userpass_validate_xfailures(self, auth_userpass, adapter, opt_patch):
adapter.set_options(**opt_patch)

with pytest.raises(HashiVaultValueError, match=r'Authentication method userpass requires options .*? to be set, but these are missing:'):
auth_userpass.validate()

@pytest.mark.parametrize('use_token', [True, False], ids=lambda x: 'use_token=%s' % x)
@pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x)
def test_auth_userpass_authenticate(
self, auth_userpass, client, adapter, userpass_password, userpass_username, mount_point, use_token, userpass_login_response
):
adapter.set_option('username', userpass_username)
adapter.set_option('password', userpass_password)
adapter.set_option('mount_point', mount_point)

expected_login_params = {
'username': userpass_username,
'password': userpass_password,
}
if mount_point:
expected_login_params['mount_point'] = mount_point

def _set_client_token(*args, **kwargs):
return userpass_login_response

with mock.patch.object(client.auth.userpass, 'login', side_effect=_set_client_token) as userpass_login:
response = auth_userpass.authenticate(client, use_token=use_token)
userpass_login.assert_called_once_with(**expected_login_params)

assert response['auth']['client_token'] == userpass_login_response['auth']['client_token']
assert (client.token == userpass_login_response['auth']['client_token']) is use_token

0 comments on commit 8852550

Please sign in to comment.