Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for azure auth method #293

Merged
merged 15 commits into from
Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelogs/fragments/293-support-azure-auth-method.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
minor_changes:
- community.hashi_vault collection - add support for ``azure`` auth method, for Azure service pricinpal, managed identity, or plain JWT access token (https://github.com/ansible-collections/community.hashi_vault/issues/293).
4 changes: 4 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ flags:
paths:
- plugins/module_utils/_auth_method_aws_iam.py

target_auth_azure:
paths:
- plugins/module_utils/_auth_method_azure.py

target_auth_cert:
paths:
- plugins/module_utils/_auth_method_cert.py
Expand Down
3 changes: 2 additions & 1 deletion docs/docsite/rst/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The content in ``community.hashi_vault`` requires the `hvac <https://hvac.readth

In general, we recommend using the latest version of ``hvac`` that is supported for your given Python version because that is what we test against. Where possible we will try to list version-specific restrictions here, but this list may not be exhaustive.

* ``hvac`` 0.7.0+ (for namespace support)
* ``hvac`` 0.7.0+ (for Azure auth and namespace support)
* ``hvac`` 0.9.6+ (to avoid most deprecation warnings)
* ``hvac`` 0.10.5+ (for JWT auth)
* ``hvac`` 0.10.6+ (to avoid deprecation warning for AppRole)
Expand All @@ -41,6 +41,7 @@ Other requirements
------------------

* ``boto3`` (only if loading credentials from a boto session, for example using an AWS profile or IAM role credentials)
* ``azure-identity`` (only if using a service principal or managed identity)

Retrying failed requests
========================
Expand Down
2 changes: 2 additions & 0 deletions meta/ee-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ urllib3 >= 1.15

boto3 # these are only needed if inferring AWS credentials or
botocore # using a boto profile; including for completeness

azure-identity # only needed when using a servide principal or managed identity
62 changes: 61 additions & 1 deletion plugins/doc_fragments/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ class ModuleDocFragment(object):
- C(none) auth method was added in collection version C(1.2.0).
- C(cert) auth method was added in collection version C(1.4.0).
- C(aws_iam_login) was renamed C(aws_iam) in collection version C(2.1.0) and was removed in C(3.0.0).
- C(azure) auth method was added in collection version C(3.2.0).
choices:
- token
- userpass
- ldap
- approle
- aws_iam
- azure
- jwt
- cert
- none
Expand Down Expand Up @@ -64,8 +66,9 @@ class ModuleDocFragment(object):
type: str
role_id:
description:
- Vault Role ID or name. Used in C(approle), C(aws_iam), and C(cert) auth methods.
- Vault Role ID or name. Used in C(approle), C(aws_iam), C(azure) and C(cert) auth methods.
- For C(cert) auth, if no I(role_id) is supplied, the default behavior is to try all certificate roles and return any one that matches.
- For C(azure) auth, I(role_id) is required.
type: str
secret_id:
description: Secret ID to be used for Vault AppRole authentication.
Expand Down Expand Up @@ -96,6 +99,34 @@ class ModuleDocFragment(object):
required: False
type: str
version_added: '0.2.0'
azure_tenant_id:
description:
- The Azure Active Directory Tenant ID (also known as the Directory ID) of the service principal. Should be a UUID.
- >-
Required when using a service principal to authenticate to Vault,
e.g. required when both I(azure_client_id) and I(azure_client_secret) are specified.
- Optional when using managed identity to authenticate to Vault.
required: False
type: str
version_added: '3.2.0'
azure_client_id:
description:
- The client ID (also known as application ID) of the Azure AD service principal or managed identity. Should be a UUID.
- If not specified, will use the system assigned managed identity.
required: False
type: str
version_added: '3.2.0'
azure_client_secret:
description: The client secret of the Azure AD service principal.
required: False
type: str
version_added: '3.2.0'
azure_resource:
description: The resource URL for the application registered in Azure Active Directory. Usually should not be changed from the default.
required: False
type: str
default: https://management.azure.com/
version_added: '3.2.0'
cert_auth_public_key:
description: For C(cert) auth, path to the certificate file to authenticate with, in PEM format.
type: path
Expand Down Expand Up @@ -234,6 +265,35 @@ class ModuleDocFragment(object):
- section: hashi_vault_collection
key: aws_iam_server_id
version_added: 1.4.0
azure_tenant_id:
env:
- name: ANSIBLE_HASHI_VAULT_AZURE_TENANT_ID
ini:
- section: hashi_vault_collection
key: azure_tenant_id
vars:
- name: ansible_hashi_vault_azure_tenant_id
azure_client_id:
env:
- name: ANSIBLE_HASHI_VAULT_AZURE_CLIENT_ID
ini:
- section: hashi_vault_collection
key: azure_client_id
vars:
- name: ansible_hashi_vault_azure_client_id
azure_client_secret:
env:
- name: ANSIBLE_HASHI_VAULT_AZURE_CLIENT_SECRET
vars:
- name: ansible_hashi_vault_azure_client_secret
azure_resource:
env:
- name: ANSIBLE_HASHI_VAULT_AZURE_RESOURCE
ini:
- section: hashi_vault_collection
key: azure_resource
vars:
- name: ansible_hashi_vault_azure_resource
cert_auth_public_key:
env:
- name: ANSIBLE_HASHI_VAULT_CERT_AUTH_PUBLIC_KEY
Expand Down
105 changes: 105 additions & 0 deletions plugins/module_utils/_auth_method_azure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022 Junrui Chen (@jchenship)
# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause)
# SPDX-License-Identifier: BSD-2-Clause

'''Python versions supported: >=3.6'''

# FOR INTERNAL COLLECTION USE ONLY
# The interfaces in this file are meant for use within the community.hashi_vault collection
# and may not remain stable to outside uses. Changes may be made in ANY release, even a bugfix release.
# See also: https://github.com/ansible/community/issues/539#issuecomment-780839686
# Please open an issue if you have questions about this.

from __future__ import absolute_import, division, print_function
__metaclass__ = type

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


class HashiVaultAuthMethodAzure(HashiVaultAuthMethodBase):
'''HashiVault auth method for Azure'''

NAME = 'azure'
OPTIONS = [
'role_id',
'jwt',
'mount_point',
'azure_tenant_id',
'azure_client_id',
'azure_client_secret',
'azure_resource',
]

def __init__(self, option_adapter, warning_callback, deprecate_callback):
super(HashiVaultAuthMethodAzure, self).__init__(
option_adapter, warning_callback, deprecate_callback
)

def validate(self):
params = {
'role': self._options.get_option_default('role_id'),
'jwt': self._options.get_option_default('jwt'),
}
if not params['role']:
raise HashiVaultValueError(
'role_id is required for azure authentication.'
)

# if mount_point is not provided, it will use the default value defined
# in hvac library (e.g. `azure`)
mount_point = self._options.get_option_default('mount_point')
if mount_point:
params['mount_point'] = mount_point

# if jwt exists, use provided jwt directly, otherwise trying to get jwt
# from azure service principal or managed identity
if not params['jwt']:
azure_tenant_id = self._options.get_option_default('azure_tenant_id')
azure_client_id = self._options.get_option_default('azure_client_id')
azure_client_secret = self._options.get_option_default('azure_client_secret')

# the logic of getting azure scope is from this function
# https://github.com/Azure/azure-cli/blob/azure-cli-2.39.0/src/azure-cli-core/azure/cli/core/auth/util.py#L72
# the reason we expose resource instead of scope is resource is
# more aligned with the vault azure auth config here
# https://www.vaultproject.io/api-docs/auth/azure#resource
azure_resource = self._options.get_option('azure_resource')
azure_scope = azure_resource + "/.default"

try:
import azure.identity
except ImportError:
raise HashiVaultValueError(
"azure-identity is required for getting access token from azure service principal or managed identity."
)

if azure_client_id and azure_client_secret:
# service principal
if not azure_tenant_id:
raise HashiVaultValueError(
'azure_tenant_id is required when using azure service principal.'
)
azure_credentials = azure.identity.ClientSecretCredential(
azure_tenant_id, azure_client_id, azure_client_secret
)
elif azure_client_id:
# user assigned managed identity
azure_credentials = azure.identity.ManagedIdentityCredential(
client_id=azure_client_id
)
else:
# system assigned managed identity
azure_credentials = azure.identity.ManagedIdentityCredential()

params['jwt'] = azure_credentials.get_token(azure_scope).token

self._auth_azure_login_params = params

def authenticate(self, client, use_token=True):
params = self._auth_azure_login_params
response = client.auth.azure.login(use_token=use_token, **params)
return response
7 changes: 7 additions & 0 deletions plugins/module_utils/_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# please keep this list in alphabetical order of auth method name
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_approle import HashiVaultAuthMethodApprole
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_aws_iam import HashiVaultAuthMethodAwsIam
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_azure import HashiVaultAuthMethodAzure
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_cert import HashiVaultAuthMethodCert
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_jwt import HashiVaultAuthMethodJwt
from ansible_collections.community.hashi_vault.plugins.module_utils._auth_method_ldap import HashiVaultAuthMethodLdap
Expand All @@ -33,6 +34,7 @@ class HashiVaultAuthenticator():
'ldap',
'approle',
'aws_iam',
'azure',
'jwt',
'cert',
'none',
Expand All @@ -54,6 +56,10 @@ class HashiVaultAuthenticator():
aws_security_token=dict(type='str', no_log=False),
region=dict(type='str'),
aws_iam_server_id=dict(type='str'),
azure_tenant_id=dict(type='str'),
azure_client_id=dict(type='str'),
azure_client_secret=dict(type='str', no_log=True),
azure_resource=dict(type='str', default='https://management.azure.com/'),
cert_auth_private_key=dict(type='path', no_log=False),
cert_auth_public_key=dict(type='path'),
)
Expand All @@ -65,6 +71,7 @@ def __init__(self, option_adapter, warning_callback, deprecate_callback):
# so that it's easier to scan and see at a glance that a given auth method is present or absent
'approle': HashiVaultAuthMethodApprole(option_adapter, warning_callback, deprecate_callback),
'aws_iam': HashiVaultAuthMethodAwsIam(option_adapter, warning_callback, deprecate_callback),
'azure': HashiVaultAuthMethodAzure(option_adapter, warning_callback, deprecate_callback),
'cert': HashiVaultAuthMethodCert(option_adapter, warning_callback, deprecate_callback),
'jwt': HashiVaultAuthMethodJwt(option_adapter, warning_callback, deprecate_callback),
'ldap': HashiVaultAuthMethodLdap(option_adapter, warning_callback, deprecate_callback),
Expand Down
4 changes: 4 additions & 0 deletions tests/integration/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ hvac >= 0.10.6 ; python_version >= '3.6'
# these should be satisfied naturally by the requests versions required by hvac anyway
urllib3 >= 1.15 ; python_version >= '3.6' # we need raise_on_status for retry support to raise the correct exceptions https://github.com/urllib3/urllib3/blob/main/CHANGES.rst#115-2016-04-06
urllib3 >= 1.15, <2.0.0 ; python_version < '3.6' # https://urllib3.readthedocs.io/en/latest/v2-roadmap.html#optimized-for-python-3-6

# azure-identity 1.7.0 depends on cryptography 2.5 which drops python 2.6 support
azure-identity < 1.7.0; python_version < '2.7'
azure-identity; python_version >= '2.7'
2 changes: 2 additions & 0 deletions tests/integration/targets/auth_azure/aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
vault/auth/azure
context/target
10 changes: 10 additions & 0 deletions tests/integration/targets/auth_azure/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copyright (c) 2022 Junrui Chen (@jchenship)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
---
ansible_hashi_vault_url: '{{ vault_mmock_server_http }}'
ansible_hashi_vault_auth_method: azure

auth_paths:
- azure
- azure-alt
7 changes: 7 additions & 0 deletions tests/integration/targets/auth_azure/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copyright (c) 2022 Junrui Chen (@jchenship)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
---
dependencies:
- setup_vault_test_plugins
- setup_vault_configure
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright (c) 2022 Junrui Chen (@jchenship)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
---
- name: "Test block"
vars:
is_default_path: "{{ this_path == default_path }}"
kwargs_mount: "{{ {} if is_default_path else {'mount_point': this_path} }}"
kwargs_common:
jwt: azure-jwt
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', role_id='not-important', **kwargs) }}"

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

- name: Failure expected when something goes wrong (simulated)
set_fact:
response: "{{ lookup('vault_test_auth', role_id='fail-me-role', want_exception=true, **kwargs) }}"

- assert:
fail_msg: "An invalid request somehow did not cause a failure."
that:
- response is failed
- response.msg is search('expected audience .+ got .+')

- name: Failure expected when role_id is not given
set_fact:
response: "{{ lookup('vault_test_auth', want_exception=true, **kwargs) }}"

- assert:
fail_msg: |
Missing role_id did not cause an expected failure.
{{ response }}
that:
- response is failed
- response.msg is search('^role_id is required for azure authentication\.$')
Loading