From b4f3f9e406232a0d227150a6125d00ae859a33c9 Mon Sep 17 00:00:00 2001 From: Junrui Chen Date: Sun, 14 Aug 2022 16:38:04 +1000 Subject: [PATCH 01/15] Add support for azure auth method --- codecov.yml | 4 + meta/ee-requirements.txt | 2 + plugins/doc_fragments/auth.py | 25 ++- plugins/module_utils/_auth_method_azure.py | 83 ++++++++++ plugins/module_utils/_authenticator.py | 7 + tests/unit/fixtures/azure_login_response.json | 33 ++++ .../authentication/test_auth_azure.py | 156 ++++++++++++++++++ 7 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 plugins/module_utils/_auth_method_azure.py create mode 100644 tests/unit/fixtures/azure_login_response.json create mode 100644 tests/unit/plugins/module_utils/authentication/test_auth_azure.py diff --git a/codecov.yml b/codecov.yml index bb14293b7..8dafd8ba1 100644 --- a/codecov.yml +++ b/codecov.yml @@ -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 diff --git a/meta/ee-requirements.txt b/meta/ee-requirements.txt index a85197761..f818e202a 100644 --- a/meta/ee-requirements.txt +++ b/meta/ee-requirements.txt @@ -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 diff --git a/plugins/doc_fragments/auth.py b/plugins/doc_fragments/auth.py index b02436d35..1bcd58350 100644 --- a/plugins/doc_fragments/auth.py +++ b/plugins/doc_fragments/auth.py @@ -24,6 +24,7 @@ class ModuleDocFragment(object): - ldap - approle - aws_iam + - azure - jwt - cert - none @@ -64,7 +65,7 @@ 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. type: str secret_id: @@ -96,6 +97,28 @@ class ModuleDocFragment(object): required: False type: str version_added: '0.2.0' + azure_tenant_id: + description: + - The azure tenant id of the service principal, also known as directory id of the active directory, should be a uuid. + - Required when using service principal to authenticate to vault, e.g. required when both C(azure_client_id) and C(azure_client_secret) are specified. + - Optional when using managed identity to authenticate to vault. + required: False + type: str + azure_client_id: + description: + - The azure client id of the service principal or the managed identity, also known as application id, should be a uuid. + - If not specified, will use the system assigned managed identity. + required: False + type: str + azure_client_secret: + description: The azure client secret of the service principal. + required: False + type: str + azure_resource: + description: The resource URL for the application registered in Azure Active Directory, usually should be https://management.azure.com/. + required: False + type: str + default: https://management.azure.com/ cert_auth_public_key: description: For C(cert) auth, path to the certificate file to authenticate with, in PEM format. type: path diff --git a/plugins/module_utils/_auth_method_azure.py b/plugins/module_utils/_auth_method_azure.py new file mode 100644 index 000000000..642979f85 --- /dev/null +++ b/plugins/module_utils/_auth_method_azure.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# 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 emthod 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 = {} + if self._options.get_option_default('role_id'): + params['role'] = self._options.get_option_default('role_id') + if self._options.get_option_default('jwt'): + params['jwt'] = self._options.get_option_default('jwt') + # if mount_point is not provided, it will use the default value defined in hvac library (e.g. `azure`) + if self._options.get_option_default('mount_point'): + params['mount_point'] = self._options.get_option_default('mount_point') + + # if jwt exists, use provided jwt directly, otherwise trying to get jwt from azure service principal or managed identity + if not params.get('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') + # this logic 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_scope = self._options.get_option_default('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_params = params + + def authenticate(self, client, use_token=True): + params = self._auth_azure_params + response = client.auth.azure.login(use_token=use_token, **params) + return response diff --git a/plugins/module_utils/_authenticator.py b/plugins/module_utils/_authenticator.py index 85d03d12d..2292d5b6b 100644 --- a/plugins/module_utils/_authenticator.py +++ b/plugins/module_utils/_authenticator.py @@ -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 @@ -33,6 +34,7 @@ class HashiVaultAuthenticator(): 'ldap', 'approle', 'aws_iam', + 'azure', 'jwt', 'cert', 'none', @@ -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'), ) @@ -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), diff --git a/tests/unit/fixtures/azure_login_response.json b/tests/unit/fixtures/azure_login_response.json new file mode 100644 index 000000000..f1d1302e5 --- /dev/null +++ b/tests/unit/fixtures/azure_login_response.json @@ -0,0 +1,33 @@ +{ + "request_id": "cbfb16b9-4cf6-917d-182b-170801fc5a4e", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": null, + "wrap_info": null, + "warnings": null, + "auth": { + "client_token": "hvs.CAESIH6iy4yyvKMpk-vcaaVvU8nGfZFRCcH92hVa24lGNxHNGh4KHGh2cy5qU29Ua1FscTJIQ3BBY1AwTDM4dzNpR0E", + "accessor": "60U0DvUOIMOIGI7kzAneeD2x", + "policies": [ + "default", + "azure-sample-policy" + ], + "token_policies": [ + "default", + "azure-sample-policy" + ], + "metadata": { + "resource_group_name": "", + "role": "msi-vault", + "subscription_id": "" + }, + "lease_duration": 2764800, + "renewable": true, + "entity_id": "ff6a9d66-c2eb-6b78-e463-b3192243b5c1", + "token_type": "service", + "orphan": true, + "mfa_requirement": null, + "num_uses": 0 + } +} diff --git a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py new file mode 100644 index 000000000..4ac92bb03 --- /dev/null +++ b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021 Brian Scholer (@briantist) +# 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 + +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_azure import ( + HashiVaultAuthMethodAzure, +) + +from ansible_collections.community.hashi_vault.plugins.module_utils._hashi_vault_common import ( + HashiVaultAuthMethodBase, + HashiVaultValueError, +) + + +@pytest.fixture +def azure_tenant_id(): + return 'tenant-id' + + +@pytest.fixture +def azure_client_id(): + return 'client-id' + + +@pytest.fixture +def azure_client_secret(): + return 'client-secret' + + +@pytest.fixture +def jwt(): + return 'jwt-token' + + +@pytest.fixture +def auth_azure(adapter, warner, deprecator): + return HashiVaultAuthMethodAzure(adapter, warner, deprecator) + + +@pytest.fixture +def azure_login_response(fixture_loader): + return fixture_loader('azure_login_response.json') + + +class TestAuthAzure(object): + + def test_auth_azure_is_auth_method_base(self, auth_azure): + assert isinstance(auth_azure, HashiVaultAuthMethodAzure) + assert issubclass(HashiVaultAuthMethodAzure, HashiVaultAuthMethodBase) + + @pytest.mark.parametrize('role_id', [None, 'vault-role'], ids=lambda x: 'role_id=%s' % x) + @pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x) + def test_auth_azure_validate_use_jwt( + self, auth_azure, adapter, role_id, mount_point, jwt + ): + adapter.set_options( + role_id=role_id, mount_point=mount_point, jwt=jwt, + ) + + auth_azure.validate() + + params = auth_azure._auth_azure_login_params + + assert (role_id is None and 'role' not in params) or params['role'] == role_id + assert (mount_point is None and 'mount_point' not in params) or params['mount_point'] == mount_point + assert params['jwt'] == jwt + + @pytest.mark.parametrize('role_id', [None, 'vault-role'], ids=lambda x: 'role_id=%s' % x) + @pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x) + @pytest.mark.parametrize('use_token', [True, False], ids=lambda x: 'use_token=%s' % x) + def test_auth_azure_authenticate_use_jwt( + self, auth_azure, client, adapter, role_id, mount_point, jwt, use_token, azure_login_response + ): + adapter.set_options( + role_id=role_id, mount_point=mount_point, jwt=jwt, + ) + + auth_azure.validate() + + params = auth_azure._auth_azure_login_params.copy() + + with mock.patch.object(client.auth.azure, 'login', return_value=azure_login_response) as azure_login: + response = auth_azure.authenticate(client, use_token=use_token) + azure_login.assert_called_once_with(use_token=use_token, **params) + + assert response['auth']['client_token'] == azure_login_response['auth']['client_token'] + + def test_auth_azure_validate_use_identity_no_azure_identity_lib(self, auth_azure, mock_import_error): + with mock_import_error('azure.identity'): + with pytest.raises(HashiVaultValueError, match=r'azure-identity is required'): + auth_azure.validate() + + def test_auth_azure_validate_use_service_principal(self, auth_azure, adapter, jwt, azure_tenant_id, azure_client_id, azure_client_secret): + adapter.set_options( + azure_tenant_id=azure_tenant_id, + azure_client_id=azure_client_id, + azure_client_secret=azure_client_secret, + ) + + with mock.patch('azure.identity.ClientSecretCredential') as mocked_credential_class: + credential = mocked_credential_class.return_value + credential.get_token.return_value = jwt + auth_azure.validate() + + assert mocked_credential_class.called_once_with(azure_tenant_id, azure_client_id, azure_client_secret) + assert credential.get_token.called_once_with('https://management.azure.com/') + + params = auth_azure._auth_azure_login_params + assert params['jwt'] == jwt + + def test_auth_azure_validate_use_service_principal_no_tenant_id(self, auth_azure, adapter, azure_client_id, azure_client_secret): + adapter.set_options( + azure_client_id=azure_client_id, + azure_client_secret=azure_client_secret, + ) + + with pytest.raises(HashiVaultValueError, match='azure_tenant_id is required'): + auth_azure.validate() + + def test_auth_azure_validate_use_user_managed_identity(self, auth_azure, adapter, jwt, azure_client_id): + adapter.set_options( + azure_client_id=azure_client_id, + ) + + with mock.patch('azure.identity.ManagedIdentityCredential') as mocked_credential_class: + credential = mocked_credential_class.return_value + credential.get_token.return_value = jwt + auth_azure.validate() + + assert mocked_credential_class.called_once_with(azure_client_id) + assert credential.get_token.called_once_with('https://management.azure.com/') + + params = auth_azure._auth_azure_login_params + assert params['jwt'] == jwt + + def test_auth_azure_validate_use_system_managed_identity(self, auth_azure, adapter, jwt): + adapter.set_options() + + with mock.patch('azure.identity.ManagedIdentityCredential') as mocked_credential_class: + credential = mocked_credential_class.return_value + credential.get_token.return_value = jwt + auth_azure.validate() + + assert mocked_credential_class.called_once_with() + assert credential.get_token.called_once_with('https://management.azure.com/') + + params = auth_azure._auth_azure_login_params + assert params['jwt'] == jwt From ba691a1904171a30eab4574012ea9904b172404e Mon Sep 17 00:00:00 2001 From: Junrui Chen Date: Sat, 20 Aug 2022 21:25:32 +1000 Subject: [PATCH 02/15] fix unit tests --- plugins/module_utils/_auth_method_azure.py | 8 ++++---- .../module_utils/authentication/test_auth_azure.py | 12 ++++++------ tests/unit/requirements.txt | 5 ++++- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/plugins/module_utils/_auth_method_azure.py b/plugins/module_utils/_auth_method_azure.py index 642979f85..1122afc2d 100644 --- a/plugins/module_utils/_auth_method_azure.py +++ b/plugins/module_utils/_auth_method_azure.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2021 Brian Scholer (@briantist) +# 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 @@ -54,7 +54,7 @@ def validate(self): azure_client_secret = self._options.get_option_default('azure_client_secret') # this logic 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_scope = self._options.get_option_default('azure_resource') + "/.default" + azure_scope = self._options.get_option_default('azure_resource', 'https://management.azure.com/') + "/.default" try: import azure.identity @@ -75,9 +75,9 @@ def validate(self): params['jwt'] = azure_credentials.get_token(azure_scope).token - self._auth_azure_params = params + self._auth_azure_login_params = params def authenticate(self, client, use_token=True): - params = self._auth_azure_params + params = self._auth_azure_login_params response = client.auth.azure.login(use_token=use_token, **params) return response diff --git a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py index 4ac92bb03..a95baf747 100644 --- a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py +++ b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py @@ -107,11 +107,11 @@ def test_auth_azure_validate_use_service_principal(self, auth_azure, adapter, jw with mock.patch('azure.identity.ClientSecretCredential') as mocked_credential_class: credential = mocked_credential_class.return_value - credential.get_token.return_value = jwt + credential.get_token.return_value.token = jwt auth_azure.validate() assert mocked_credential_class.called_once_with(azure_tenant_id, azure_client_id, azure_client_secret) - assert credential.get_token.called_once_with('https://management.azure.com/') + assert credential.get_token.called_once_with('https://management.azure.com//.default') params = auth_azure._auth_azure_login_params assert params['jwt'] == jwt @@ -132,11 +132,11 @@ def test_auth_azure_validate_use_user_managed_identity(self, auth_azure, adapter with mock.patch('azure.identity.ManagedIdentityCredential') as mocked_credential_class: credential = mocked_credential_class.return_value - credential.get_token.return_value = jwt + credential.get_token.return_value.token = jwt auth_azure.validate() assert mocked_credential_class.called_once_with(azure_client_id) - assert credential.get_token.called_once_with('https://management.azure.com/') + assert credential.get_token.called_once_with('https://management.azure.com//.default') params = auth_azure._auth_azure_login_params assert params['jwt'] == jwt @@ -146,11 +146,11 @@ def test_auth_azure_validate_use_system_managed_identity(self, auth_azure, adapt with mock.patch('azure.identity.ManagedIdentityCredential') as mocked_credential_class: credential = mocked_credential_class.return_value - credential.get_token.return_value = jwt + credential.get_token.return_value.token = jwt auth_azure.validate() assert mocked_credential_class.called_once_with() - assert credential.get_token.called_once_with('https://management.azure.com/') + assert credential.get_token.called_once_with('https://management.azure.com//.default') params = auth_azure._auth_azure_login_params assert params['jwt'] == jwt diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt index 7a9aaae0b..d8844e35b 100644 --- a/tests/unit/requirements.txt +++ b/tests/unit/requirements.txt @@ -2,7 +2,6 @@ # earlier python versions are still needed for Ansible < 2.12 which doesn't # support tests/config.yml, so that unit tests (which will be skipped) won't # choke on installing requirements. - hvac >= 0.10.6, != 0.10.12, != 0.10.13, < 1.0.0 ; python_version == '2.7' # bugs in 0.10.12 and 0.10.13 prevent it from working in Python 2 hvac >= 0.10.6, < 1.0.0 ; python_version == '3.5' # py3.5 support will be dropped in 1.0.0 hvac >= 0.10.6 ; python_version >= '3.6' @@ -10,3 +9,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' From c29bb29d0fd2f9b8a59fd913d7638abc882fba87 Mon Sep 17 00:00:00 2001 From: Junrui Chen Date: Sat, 20 Aug 2022 22:53:50 +1000 Subject: [PATCH 03/15] add integration tests --- tests/integration/requirements.txt | 4 ++ tests/integration/targets/auth_azure/aliases | 2 + .../targets/auth_azure/defaults/main.yml | 7 +++ .../targets/auth_azure/meta/main.yml | 4 ++ .../tasks/azure_test_controller.yml | 32 ++++++++++++++ .../auth_azure/tasks/azure_test_target.yml | 37 ++++++++++++++++ .../targets/auth_azure/tasks/main.yml | 23 ++++++++++ .../mmock/azure_login_alt_mount.yml.j2 | 43 +++++++++++++++++++ .../mmock/azure_login_bad_request.yml.j2 | 19 ++++++++ .../mmock/azure_login_default_mount.yml.j2 | 43 +++++++++++++++++++ 10 files changed, 214 insertions(+) create mode 100644 tests/integration/targets/auth_azure/aliases create mode 100644 tests/integration/targets/auth_azure/defaults/main.yml create mode 100644 tests/integration/targets/auth_azure/meta/main.yml create mode 100644 tests/integration/targets/auth_azure/tasks/azure_test_controller.yml create mode 100644 tests/integration/targets/auth_azure/tasks/azure_test_target.yml create mode 100644 tests/integration/targets/auth_azure/tasks/main.yml create mode 100644 tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_alt_mount.yml.j2 create mode 100644 tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_bad_request.yml.j2 create mode 100644 tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_default_mount.yml.j2 diff --git a/tests/integration/requirements.txt b/tests/integration/requirements.txt index 7a9aaae0b..033733f7c 100644 --- a/tests/integration/requirements.txt +++ b/tests/integration/requirements.txt @@ -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' diff --git a/tests/integration/targets/auth_azure/aliases b/tests/integration/targets/auth_azure/aliases new file mode 100644 index 000000000..32ccc0d36 --- /dev/null +++ b/tests/integration/targets/auth_azure/aliases @@ -0,0 +1,2 @@ +vault/auth/azure +context/target diff --git a/tests/integration/targets/auth_azure/defaults/main.yml b/tests/integration/targets/auth_azure/defaults/main.yml new file mode 100644 index 000000000..8562ec026 --- /dev/null +++ b/tests/integration/targets/auth_azure/defaults/main.yml @@ -0,0 +1,7 @@ +--- +ansible_hashi_vault_url: '{{ vault_mmock_server_http }}' +ansible_hashi_vault_auth_method: azure + +auth_paths: + - azure + - azure-alt diff --git a/tests/integration/targets/auth_azure/meta/main.yml b/tests/integration/targets/auth_azure/meta/main.yml new file mode 100644 index 000000000..d3acb69e9 --- /dev/null +++ b/tests/integration/targets/auth_azure/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - setup_vault_test_plugins + - setup_vault_configure diff --git a/tests/integration/targets/auth_azure/tasks/azure_test_controller.yml b/tests/integration/targets/auth_azure/tasks/azure_test_controller.yml new file mode 100644 index 000000000..938af2155 --- /dev/null +++ b/tests/integration/targets/auth_azure/tasks/azure_test_controller.yml @@ -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: + 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 .+') diff --git a/tests/integration/targets/auth_azure/tasks/azure_test_target.yml b/tests/integration/targets/auth_azure/tasks/azure_test_target.yml new file mode 100644 index 000000000..f8b53ebc5 --- /dev/null +++ b/tests/integration/targets/auth_azure/tasks/azure_test_target.yml @@ -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 }}' + jwt: azure-jwt + role_id: not-important + 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: + - ('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) + register: response + vault_test_auth: + role_id: fail-me-role + want_exception: yes + + - assert: + fail_msg: "An invalid request somehow did not cause a failure." + that: + - response.inner is failed + - response.msg is search('expected audience .+ got .+') diff --git a/tests/integration/targets/auth_azure/tasks/main.yml b/tests/integration/targets/auth_azure/tasks/main.yml new file mode 100644 index 000000000..7d9ea0a90 --- /dev/null +++ b/tests/integration/targets/auth_azure/tasks/main.yml @@ -0,0 +1,23 @@ +--- +# 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 }}' + +# there's no setup for this auth method because its API is mocked + +- name: Run azure tests + loop: '{{ auth_paths | product(["target", "controller"]) | list }}' + include_tasks: + file: azure_test_{{ item[1] }}.yml + apply: + vars: + default_path: azure + this_path: '{{ item[0] }}' + module_defaults: + assert: + quiet: yes diff --git a/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_alt_mount.yml.j2 b/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_alt_mount.yml.j2 new file mode 100644 index 000000000..6e309c903 --- /dev/null +++ b/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_alt_mount.yml.j2 @@ -0,0 +1,43 @@ +#jinja2:variable_start_string:'[%', variable_end_string:'%]' +--- +request: + method: POST|PUT + path: "/v1/auth/azure-alt/login" +control: + priority: 10 +response: + statusCode: 200 + headers: + Content-Type: + - application/json + body: >- + { + "request_id": "{{fake.UUID}}", + "lease_id": "", + "lease_duration": 0, + "renewable": false, + "data": null, + "warnings": null, + "auth": { + "client_token": "s.{{fake.CharactersN(24)}}", + "accessor": "{{fake.CharactersN(24)}}", + "policies": [ + "default", + "azure-alt-sample-policy" + ], + "token_policies": [ + "default", + "azure-alt-sample-policy" + ], + "identity_policies": null, + "metadata": { + "role": "vault-role", + "resource_group_name": "", + "subscription_id": "" + }, + "orphan": true, + "entity_id": "{{fake.UUID}}", + "lease_duration": 1800, + "renewable": true + } + } diff --git a/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_bad_request.yml.j2 b/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_bad_request.yml.j2 new file mode 100644 index 000000000..53fda21b8 --- /dev/null +++ b/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_bad_request.yml.j2 @@ -0,0 +1,19 @@ +#jinja2:variable_start_string:'[%', variable_end_string:'%]' +--- +request: + method: POST|PUT + path: "/v1/auth/azure*/login" + body: '*fail-me-role*' +control: + priority: 11 +response: + statusCode: 400 + headers: + Content-Type: + - application/json + body: >- + { + "errors": [ + "oidc: expected audience \"https://management.azure.com/\" got [\"https://management.azure.com\"]" + ] + } diff --git a/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_default_mount.yml.j2 b/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_default_mount.yml.j2 new file mode 100644 index 000000000..a19f05cb6 --- /dev/null +++ b/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_default_mount.yml.j2 @@ -0,0 +1,43 @@ +#jinja2:variable_start_string:'[%', variable_end_string:'%]' +--- +request: + method: POST|PUT + path: "/v1/auth/azure/login" +control: + priority: 10 +response: + statusCode: 200 + headers: + Content-Type: + - application/json + body: >- + { + "request_id": "{{fake.UUID}}", + "lease_id": "", + "lease_duration": 0, + "renewable": false, + "data": null, + "warnings": null, + "auth": { + "client_token": "s.{{fake.CharactersN(24)}}", + "accessor": "{{fake.CharactersN(24)}}", + "policies": [ + "default", + "azure-sample-policy" + ], + "token_policies": [ + "default", + "azure-sample-policy" + ], + "identity_policies": null, + "metadata": { + "role": "vault-role", + "resource_group_name": "", + "subscription_id": "" + }, + "orphan": true, + "entity_id": "{{fake.UUID}}", + "lease_duration": 1800, + "renewable": true + } + } From 68fb225e88195b7f2de755ef6349add9f7f45a15 Mon Sep 17 00:00:00 2001 From: Junrui Chen Date: Sun, 21 Aug 2022 10:02:40 +1000 Subject: [PATCH 04/15] add changelog fragment, fix copyright, add version_added --- changelogs/fragments/293-support-azure-auth-method.yml | 3 +++ plugins/doc_fragments/auth.py | 4 ++++ .../plugins/module_utils/authentication/test_auth_azure.py | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/293-support-azure-auth-method.yml diff --git a/changelogs/fragments/293-support-azure-auth-method.yml b/changelogs/fragments/293-support-azure-auth-method.yml new file mode 100644 index 000000000..c1835f945 --- /dev/null +++ b/changelogs/fragments/293-support-azure-auth-method.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - plugins - 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). diff --git a/plugins/doc_fragments/auth.py b/plugins/doc_fragments/auth.py index 1bcd58350..6e0901952 100644 --- a/plugins/doc_fragments/auth.py +++ b/plugins/doc_fragments/auth.py @@ -104,21 +104,25 @@ class ModuleDocFragment(object): - Optional when using managed identity to authenticate to vault. required: False type: str + version_added: '3.2.0' azure_client_id: description: - The azure client id of the service principal or the managed identity, also known as application id, 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 azure client secret of the 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 be https://management.azure.com/. 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 diff --git a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py index a95baf747..f37721979 100644 --- a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py +++ b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2021 Brian Scholer (@briantist) +# 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 From 9d806296618acb947b52b7ae8f539bd8cb1073f9 Mon Sep 17 00:00:00 2001 From: Junrui Chen Date: Sun, 21 Aug 2022 10:17:10 +1000 Subject: [PATCH 05/15] fix sanity tests --- plugins/module_utils/_auth_method_azure.py | 42 ++++++--- .../authentication/test_auth_azure.py | 88 ++++++++++++++----- 2 files changed, 99 insertions(+), 31 deletions(-) diff --git a/plugins/module_utils/_auth_method_azure.py b/plugins/module_utils/_auth_method_azure.py index 1122afc2d..80094a0b4 100644 --- a/plugins/module_utils/_auth_method_azure.py +++ b/plugins/module_utils/_auth_method_azure.py @@ -35,7 +35,9 @@ class HashiVaultAuthMethodAzure(HashiVaultAuthMethodBase): ] def __init__(self, option_adapter, warning_callback, deprecate_callback): - super(HashiVaultAuthMethodAzure, self).__init__(option_adapter, warning_callback, deprecate_callback) + super(HashiVaultAuthMethodAzure, self).__init__( + option_adapter, warning_callback, deprecate_callback + ) def validate(self): params = {} @@ -43,32 +45,50 @@ def validate(self): params['role'] = self._options.get_option_default('role_id') if self._options.get_option_default('jwt'): params['jwt'] = self._options.get_option_default('jwt') - # if mount_point is not provided, it will use the default value defined in hvac library (e.g. `azure`) + # if mount_point is not provided, it will use the default value defined + # in hvac library (e.g. `azure`) if self._options.get_option_default('mount_point'): params['mount_point'] = self._options.get_option_default('mount_point') - # if jwt exists, use provided jwt directly, otherwise trying to get jwt from azure service principal or managed identity + # if jwt exists, use provided jwt directly, otherwise trying to get jwt + # from azure service principal or managed identity if not params.get('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') - # this logic 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_scope = self._options.get_option_default('azure_resource', 'https://management.azure.com/') + "/.default" + 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_default( + 'azure_resource', 'https://management.azure.com/' + ) + 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.") + 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) + 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) + azure_credentials = azure.identity.ManagedIdentityCredential( + client_id=azure_client_id + ) else: # system assigned managed identity azure_credentials = azure.identity.ManagedIdentityCredential() diff --git a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py index f37721979..35b819b78 100644 --- a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py +++ b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py @@ -3,7 +3,7 @@ # 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 -from __future__ import (absolute_import, division, print_function) +from __future__ import absolute_import, division, print_function __metaclass__ = type import pytest @@ -51,7 +51,6 @@ def azure_login_response(fixture_loader): class TestAuthAzure(object): - def test_auth_azure_is_auth_method_base(self, auth_azure): assert isinstance(auth_azure, HashiVaultAuthMethodAzure) assert issubclass(HashiVaultAuthMethodAzure, HashiVaultAuthMethodBase) @@ -62,7 +61,9 @@ def test_auth_azure_validate_use_jwt( self, auth_azure, adapter, role_id, mount_point, jwt ): adapter.set_options( - role_id=role_id, mount_point=mount_point, jwt=jwt, + role_id=role_id, + mount_point=mount_point, + jwt=jwt, ) auth_azure.validate() @@ -77,46 +78,81 @@ def test_auth_azure_validate_use_jwt( @pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x) @pytest.mark.parametrize('use_token', [True, False], ids=lambda x: 'use_token=%s' % x) def test_auth_azure_authenticate_use_jwt( - self, auth_azure, client, adapter, role_id, mount_point, jwt, use_token, azure_login_response + self, + auth_azure, + client, + adapter, + role_id, + mount_point, + jwt, + use_token, + azure_login_response, ): adapter.set_options( - role_id=role_id, mount_point=mount_point, jwt=jwt, + role_id=role_id, + mount_point=mount_point, + jwt=jwt, ) auth_azure.validate() params = auth_azure._auth_azure_login_params.copy() - with mock.patch.object(client.auth.azure, 'login', return_value=azure_login_response) as azure_login: + with mock.patch.object( + client.auth.azure, 'login', return_value=azure_login_response + ) as azure_login: response = auth_azure.authenticate(client, use_token=use_token) azure_login.assert_called_once_with(use_token=use_token, **params) - assert response['auth']['client_token'] == azure_login_response['auth']['client_token'] + assert ( + response['auth']['client_token'] + == azure_login_response['auth']['client_token'] + ) - def test_auth_azure_validate_use_identity_no_azure_identity_lib(self, auth_azure, mock_import_error): + def test_auth_azure_validate_use_identity_no_azure_identity_lib( + self, auth_azure, mock_import_error + ): with mock_import_error('azure.identity'): - with pytest.raises(HashiVaultValueError, match=r'azure-identity is required'): + with pytest.raises( + HashiVaultValueError, match=r'azure-identity is required' + ): auth_azure.validate() - def test_auth_azure_validate_use_service_principal(self, auth_azure, adapter, jwt, azure_tenant_id, azure_client_id, azure_client_secret): + def test_auth_azure_validate_use_service_principal( + self, + auth_azure, + adapter, + jwt, + azure_tenant_id, + azure_client_id, + azure_client_secret, + ): adapter.set_options( azure_tenant_id=azure_tenant_id, azure_client_id=azure_client_id, azure_client_secret=azure_client_secret, ) - with mock.patch('azure.identity.ClientSecretCredential') as mocked_credential_class: + with mock.patch( + 'azure.identity.ClientSecretCredential' + ) as mocked_credential_class: credential = mocked_credential_class.return_value credential.get_token.return_value.token = jwt auth_azure.validate() - assert mocked_credential_class.called_once_with(azure_tenant_id, azure_client_id, azure_client_secret) - assert credential.get_token.called_once_with('https://management.azure.com//.default') + assert mocked_credential_class.called_once_with( + azure_tenant_id, azure_client_id, azure_client_secret + ) + assert credential.get_token.called_once_with( + 'https://management.azure.com//.default' + ) params = auth_azure._auth_azure_login_params assert params['jwt'] == jwt - def test_auth_azure_validate_use_service_principal_no_tenant_id(self, auth_azure, adapter, azure_client_id, azure_client_secret): + def test_auth_azure_validate_use_service_principal_no_tenant_id( + self, auth_azure, adapter, azure_client_id, azure_client_secret + ): adapter.set_options( azure_client_id=azure_client_id, azure_client_secret=azure_client_secret, @@ -125,32 +161,44 @@ def test_auth_azure_validate_use_service_principal_no_tenant_id(self, auth_azure with pytest.raises(HashiVaultValueError, match='azure_tenant_id is required'): auth_azure.validate() - def test_auth_azure_validate_use_user_managed_identity(self, auth_azure, adapter, jwt, azure_client_id): + def test_auth_azure_validate_use_user_managed_identity( + self, auth_azure, adapter, jwt, azure_client_id + ): adapter.set_options( azure_client_id=azure_client_id, ) - with mock.patch('azure.identity.ManagedIdentityCredential') as mocked_credential_class: + with mock.patch( + 'azure.identity.ManagedIdentityCredential' + ) as mocked_credential_class: credential = mocked_credential_class.return_value credential.get_token.return_value.token = jwt auth_azure.validate() assert mocked_credential_class.called_once_with(azure_client_id) - assert credential.get_token.called_once_with('https://management.azure.com//.default') + assert credential.get_token.called_once_with( + 'https://management.azure.com//.default' + ) params = auth_azure._auth_azure_login_params assert params['jwt'] == jwt - def test_auth_azure_validate_use_system_managed_identity(self, auth_azure, adapter, jwt): + def test_auth_azure_validate_use_system_managed_identity( + self, auth_azure, adapter, jwt + ): adapter.set_options() - with mock.patch('azure.identity.ManagedIdentityCredential') as mocked_credential_class: + with mock.patch( + 'azure.identity.ManagedIdentityCredential' + ) as mocked_credential_class: credential = mocked_credential_class.return_value credential.get_token.return_value.token = jwt auth_azure.validate() assert mocked_credential_class.called_once_with() - assert credential.get_token.called_once_with('https://management.azure.com//.default') + assert credential.get_token.called_once_with( + 'https://management.azure.com//.default' + ) params = auth_azure._auth_azure_login_params assert params['jwt'] == jwt From 440b4e3ec2b5152194fe36286d03595b99881c83 Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Sun, 21 Aug 2022 11:13:02 -0400 Subject: [PATCH 06/15] Apply suggestions from code review --- plugins/doc_fragments/auth.py | 10 +++++----- plugins/module_utils/_auth_method_azure.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/doc_fragments/auth.py b/plugins/doc_fragments/auth.py index 6e0901952..36e42b126 100644 --- a/plugins/doc_fragments/auth.py +++ b/plugins/doc_fragments/auth.py @@ -100,25 +100,25 @@ class ModuleDocFragment(object): azure_tenant_id: description: - The azure tenant id of the service principal, also known as directory id of the active directory, should be a uuid. - - Required when using service principal to authenticate to vault, e.g. required when both C(azure_client_id) and C(azure_client_secret) are specified. - - Optional when using managed identity to authenticate to vault. + - 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 azure client id of the service principal or the managed identity, also known as application id, should be a uuid. + - 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 azure client secret of the service principal. + 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 be https://management.azure.com/. + 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/ diff --git a/plugins/module_utils/_auth_method_azure.py b/plugins/module_utils/_auth_method_azure.py index 80094a0b4..3591ed14b 100644 --- a/plugins/module_utils/_auth_method_azure.py +++ b/plugins/module_utils/_auth_method_azure.py @@ -21,7 +21,7 @@ class HashiVaultAuthMethodAzure(HashiVaultAuthMethodBase): - '''HashiVault auth emthod for azure''' + '''HashiVault auth method for Azure''' NAME = 'azure' OPTIONS = [ From 96fb078776a0bc2e58d844d6e05a7f97cd6ba88d Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Sun, 21 Aug 2022 11:21:24 -0400 Subject: [PATCH 07/15] add env, ini, and vars plugins entries for azure auth options --- plugins/doc_fragments/auth.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/plugins/doc_fragments/auth.py b/plugins/doc_fragments/auth.py index 36e42b126..f74c0325c 100644 --- a/plugins/doc_fragments/auth.py +++ b/plugins/doc_fragments/auth.py @@ -261,6 +261,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 From f4e60ab45509f9e528404505a1b08860960a449c Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Sun, 21 Aug 2022 12:04:16 -0400 Subject: [PATCH 08/15] make role_id explicitly required --- plugins/module_utils/_auth_method_azure.py | 11 +++++--- .../tasks/azure_test_controller.yml | 12 +++++++++ .../auth_azure/tasks/azure_test_target.yml | 15 ++++++++++- .../authentication/test_auth_azure.py | 26 ++++++++++++++----- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/plugins/module_utils/_auth_method_azure.py b/plugins/module_utils/_auth_method_azure.py index 3591ed14b..313762796 100644 --- a/plugins/module_utils/_auth_method_azure.py +++ b/plugins/module_utils/_auth_method_azure.py @@ -40,9 +40,14 @@ def __init__(self, option_adapter, warning_callback, deprecate_callback): ) def validate(self): - params = {} - if self._options.get_option_default('role_id'): - params['role'] = self._options.get_option_default('role_id') + params = { + 'role': self._options.get_option_default('role_id') + } + if not params['role']: + raise HashiVaultValueError( + 'role_id is required for azure authentication.' + ) + if self._options.get_option_default('jwt'): params['jwt'] = self._options.get_option_default('jwt') # if mount_point is not provided, it will use the default value defined diff --git a/tests/integration/targets/auth_azure/tasks/azure_test_controller.yml b/tests/integration/targets/auth_azure/tasks/azure_test_controller.yml index 938af2155..725984623 100644 --- a/tests/integration/targets/auth_azure/tasks/azure_test_controller.yml +++ b/tests/integration/targets/auth_azure/tasks/azure_test_controller.yml @@ -30,3 +30,15 @@ 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\.$') diff --git a/tests/integration/targets/auth_azure/tasks/azure_test_target.yml b/tests/integration/targets/auth_azure/tasks/azure_test_target.yml index f8b53ebc5..c52e396bf 100644 --- a/tests/integration/targets/auth_azure/tasks/azure_test_target.yml +++ b/tests/integration/targets/auth_azure/tasks/azure_test_target.yml @@ -7,7 +7,6 @@ auth_method: '{{ ansible_hashi_vault_auth_method }}' mount_point: '{{ omit if is_default_path else this_path }}' jwt: azure-jwt - role_id: not-important 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 @@ -15,6 +14,7 @@ - name: Check auth mount differing result register: response vault_test_auth: + role_id: not-important - assert: fail_msg: "A token from mount path '{{ this_path }}' had the wrong policy: {{ response.login.auth.policies }}" @@ -35,3 +35,16 @@ that: - response.inner is failed - response.msg is search('expected audience .+ got .+') + + - name: Failure expected when role_id is not given + register: response + vault_test_auth: + want_exception: yes + + - assert: + fail_msg: | + Missing role_id did not cause an expected failure. + {{ response }} + that: + - response.inner is failed + - response.msg is search('^role_id is required for azure authentication\.$') diff --git a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py index 35b819b78..46e3fa337 100644 --- a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py +++ b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py @@ -20,6 +20,11 @@ ) +@pytest.fixture +def role_id(): + return 'vault-role' + + @pytest.fixture def azure_tenant_id(): return 'tenant-id' @@ -55,7 +60,10 @@ def test_auth_azure_is_auth_method_base(self, auth_azure): assert isinstance(auth_azure, HashiVaultAuthMethodAzure) assert issubclass(HashiVaultAuthMethodAzure, HashiVaultAuthMethodBase) - @pytest.mark.parametrize('role_id', [None, 'vault-role'], ids=lambda x: 'role_id=%s' % x) + def test_auth_azure_validate_role_id(self, auth_azure, adapter): + with pytest.raises(HashiVaultValueError, match=r'^role_id is required for azure authentication\.$'): + auth_azure.validate() + @pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x) def test_auth_azure_validate_use_jwt( self, auth_azure, adapter, role_id, mount_point, jwt @@ -74,7 +82,6 @@ def test_auth_azure_validate_use_jwt( assert (mount_point is None and 'mount_point' not in params) or params['mount_point'] == mount_point assert params['jwt'] == jwt - @pytest.mark.parametrize('role_id', [None, 'vault-role'], ids=lambda x: 'role_id=%s' % x) @pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x) @pytest.mark.parametrize('use_token', [True, False], ids=lambda x: 'use_token=%s' % x) def test_auth_azure_authenticate_use_jwt( @@ -110,8 +117,9 @@ def test_auth_azure_authenticate_use_jwt( ) def test_auth_azure_validate_use_identity_no_azure_identity_lib( - self, auth_azure, mock_import_error + self, auth_azure, mock_import_error, adapter, role_id ): + adapter.set_options(role_id=role_id) with mock_import_error('azure.identity'): with pytest.raises( HashiVaultValueError, match=r'azure-identity is required' @@ -123,11 +131,13 @@ def test_auth_azure_validate_use_service_principal( auth_azure, adapter, jwt, + role_id, azure_tenant_id, azure_client_id, azure_client_secret, ): adapter.set_options( + role_id=role_id, azure_tenant_id=azure_tenant_id, azure_client_id=azure_client_id, azure_client_secret=azure_client_secret, @@ -151,9 +161,10 @@ def test_auth_azure_validate_use_service_principal( assert params['jwt'] == jwt def test_auth_azure_validate_use_service_principal_no_tenant_id( - self, auth_azure, adapter, azure_client_id, azure_client_secret + self, auth_azure, adapter, role_id, azure_client_id, azure_client_secret ): adapter.set_options( + role_id=role_id, azure_client_id=azure_client_id, azure_client_secret=azure_client_secret, ) @@ -162,9 +173,10 @@ def test_auth_azure_validate_use_service_principal_no_tenant_id( auth_azure.validate() def test_auth_azure_validate_use_user_managed_identity( - self, auth_azure, adapter, jwt, azure_client_id + self, auth_azure, adapter, role_id, jwt, azure_client_id ): adapter.set_options( + role_id=role_id, azure_client_id=azure_client_id, ) @@ -184,9 +196,9 @@ def test_auth_azure_validate_use_user_managed_identity( assert params['jwt'] == jwt def test_auth_azure_validate_use_system_managed_identity( - self, auth_azure, adapter, jwt + self, auth_azure, adapter, role_id, jwt ): - adapter.set_options() + adapter.set_options(role_id=role_id) with mock.patch( 'azure.identity.ManagedIdentityCredential' From 3f0eaaf9e50b1c1f7ad58d52b897094190b56557 Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Sun, 21 Aug 2022 13:02:27 -0400 Subject: [PATCH 09/15] validate method cleanup, unit test additions --- plugins/module_utils/_auth_method_azure.py | 21 ++++----- .../authentication/test_auth_azure.py | 43 ++++++++++++------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/plugins/module_utils/_auth_method_azure.py b/plugins/module_utils/_auth_method_azure.py index 313762796..36f44e07c 100644 --- a/plugins/module_utils/_auth_method_azure.py +++ b/plugins/module_utils/_auth_method_azure.py @@ -41,36 +41,33 @@ def __init__(self, option_adapter, warning_callback, deprecate_callback): def validate(self): params = { - 'role': self._options.get_option_default('role_id') + '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 self._options.get_option_default('jwt'): - params['jwt'] = self._options.get_option_default('jwt') # if mount_point is not provided, it will use the default value defined # in hvac library (e.g. `azure`) - if self._options.get_option_default('mount_point'): - params['mount_point'] = self._options.get_option_default('mount_point') + 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.get('jwt'): + 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' - ) + 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_default( - 'azure_resource', 'https://management.azure.com/' - ) + azure_resource = self._options.get_option('azure_resource') azure_scope = azure_resource + "/.default" try: diff --git a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py index 46e3fa337..793325837 100644 --- a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py +++ b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py @@ -21,8 +21,17 @@ @pytest.fixture -def role_id(): - return 'vault-role' +def option_dict(): + return { + 'auth_method': 'azure', + 'role_id': 'vault-role', + 'mount_point': None, + 'jwt': None, + 'azure_tenant_id': None, + 'azure_client_id': None, + 'azure_client_secret': None, + 'azure_resource': 'https://management.azure.com/', + } @pytest.fixture @@ -61,10 +70,13 @@ def test_auth_azure_is_auth_method_base(self, auth_azure): assert issubclass(HashiVaultAuthMethodAzure, HashiVaultAuthMethodBase) def test_auth_azure_validate_role_id(self, auth_azure, adapter): + adapter.set_options(role_id=None) with pytest.raises(HashiVaultValueError, match=r'^role_id is required for azure authentication\.$'): auth_azure.validate() @pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x) + @pytest.mark.parametrize('role_id', ['role1', 'role2'], ids=lambda x: 'role_id=%s' % x) + @pytest.mark.parametrize('jwt', ['jwt1', 'jwt2'], ids=lambda x: 'jwt=%s' % x) def test_auth_azure_validate_use_jwt( self, auth_azure, adapter, role_id, mount_point, jwt ): @@ -78,8 +90,8 @@ def test_auth_azure_validate_use_jwt( params = auth_azure._auth_azure_login_params - assert (role_id is None and 'role' not in params) or params['role'] == role_id assert (mount_point is None and 'mount_point' not in params) or params['mount_point'] == mount_point + assert params['role'] == role_id assert params['jwt'] == jwt @pytest.mark.parametrize('mount_point', [None, 'other'], ids=lambda x: 'mount_point=%s' % x) @@ -89,14 +101,12 @@ def test_auth_azure_authenticate_use_jwt( auth_azure, client, adapter, - role_id, mount_point, jwt, use_token, azure_login_response, ): adapter.set_options( - role_id=role_id, mount_point=mount_point, jwt=jwt, ) @@ -117,27 +127,29 @@ def test_auth_azure_authenticate_use_jwt( ) def test_auth_azure_validate_use_identity_no_azure_identity_lib( - self, auth_azure, mock_import_error, adapter, role_id + self, auth_azure, mock_import_error, adapter ): - adapter.set_options(role_id=role_id) + adapter.set_options() with mock_import_error('azure.identity'): with pytest.raises( HashiVaultValueError, match=r'azure-identity is required' ): auth_azure.validate() + @pytest.mark.parametrize('azure_tenant_id', ['tenant1', 'tenant2'], ids=lambda x: 'azure_tenant_id=%s' % x) + @pytest.mark.parametrize('azure_client_id', ['client1', 'client2'], ids=lambda x: 'azure_client_id=%s' % x) + @pytest.mark.parametrize('azure_client_secret', ['secret1', 'secret2'], ids=lambda x: 'azure_client_secret=%s' % x) + @pytest.mark.parametrize('jwt', ['jwt1', 'jwt2'], ids=lambda x: 'jwt=%s' % x) def test_auth_azure_validate_use_service_principal( self, auth_azure, adapter, jwt, - role_id, azure_tenant_id, azure_client_id, azure_client_secret, ): adapter.set_options( - role_id=role_id, azure_tenant_id=azure_tenant_id, azure_client_id=azure_client_id, azure_client_secret=azure_client_secret, @@ -161,10 +173,9 @@ def test_auth_azure_validate_use_service_principal( assert params['jwt'] == jwt def test_auth_azure_validate_use_service_principal_no_tenant_id( - self, auth_azure, adapter, role_id, azure_client_id, azure_client_secret + self, auth_azure, adapter, azure_client_id, azure_client_secret ): adapter.set_options( - role_id=role_id, azure_client_id=azure_client_id, azure_client_secret=azure_client_secret, ) @@ -172,11 +183,12 @@ def test_auth_azure_validate_use_service_principal_no_tenant_id( with pytest.raises(HashiVaultValueError, match='azure_tenant_id is required'): auth_azure.validate() + @pytest.mark.parametrize('azure_client_id', ['client1', 'client2'], ids=lambda x: 'azure_client_id=%s' % x) + @pytest.mark.parametrize('jwt', ['jwt1', 'jwt2'], ids=lambda x: 'jwt=%s' % x) def test_auth_azure_validate_use_user_managed_identity( - self, auth_azure, adapter, role_id, jwt, azure_client_id + self, auth_azure, adapter, jwt, azure_client_id ): adapter.set_options( - role_id=role_id, azure_client_id=azure_client_id, ) @@ -195,10 +207,11 @@ def test_auth_azure_validate_use_user_managed_identity( params = auth_azure._auth_azure_login_params assert params['jwt'] == jwt + @pytest.mark.parametrize('jwt', ['jwt1', 'jwt2'], ids=lambda x: 'jwt=%s' % x) def test_auth_azure_validate_use_system_managed_identity( - self, auth_azure, adapter, role_id, jwt + self, auth_azure, adapter, jwt ): - adapter.set_options(role_id=role_id) + adapter.set_options() with mock.patch( 'azure.identity.ManagedIdentityCredential' From 53d87d7bfd38e8f4358825f387ecf494b6437dbe Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Sun, 21 Aug 2022 13:04:53 -0400 Subject: [PATCH 10/15] changelog nits --- changelogs/fragments/293-support-azure-auth-method.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/fragments/293-support-azure-auth-method.yml b/changelogs/fragments/293-support-azure-auth-method.yml index c1835f945..33fa4db94 100644 --- a/changelogs/fragments/293-support-azure-auth-method.yml +++ b/changelogs/fragments/293-support-azure-auth-method.yml @@ -1,3 +1,3 @@ --- minor_changes: - - plugins - 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). + - 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). From 2cb302cf48366b10848884d8075305825ecf0481 Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Sun, 21 Aug 2022 13:14:31 -0400 Subject: [PATCH 11/15] docs and requirements --- docs/docsite/rst/user_guide.rst | 3 ++- meta/ee-requirements.txt | 2 +- plugins/doc_fragments/auth.py | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/docsite/rst/user_guide.rst b/docs/docsite/rst/user_guide.rst index fe997e579..a3f417800 100644 --- a/docs/docsite/rst/user_guide.rst +++ b/docs/docsite/rst/user_guide.rst @@ -31,7 +31,7 @@ The content in ``community.hashi_vault`` requires the `hvac = 1.15 boto3 # these are only needed if inferring AWS credentials or botocore # using a boto profile; including for completeness -azure-identity +azure-identity # only needed when using a servide principal or managed identity diff --git a/plugins/doc_fragments/auth.py b/plugins/doc_fragments/auth.py index f74c0325c..bf2236a72 100644 --- a/plugins/doc_fragments/auth.py +++ b/plugins/doc_fragments/auth.py @@ -18,6 +18,7 @@ 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 @@ -67,6 +68,7 @@ class ModuleDocFragment(object): description: - 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. From 103857ee3326a5c16a17a91796486e2a7c10243c Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Sun, 21 Aug 2022 13:18:04 -0400 Subject: [PATCH 12/15] add more copyright/license headers --- tests/integration/targets/auth_azure/defaults/main.yml | 3 +++ tests/integration/targets/auth_azure/meta/main.yml | 3 +++ .../targets/auth_azure/tasks/azure_test_controller.yml | 4 ++++ .../targets/auth_azure/tasks/azure_test_target.yml | 4 ++++ tests/integration/targets/auth_azure/tasks/main.yml | 3 +++ .../templates/mmock/azure_login_alt_mount.yml.j2 | 3 +++ .../templates/mmock/azure_login_bad_request.yml.j2 | 3 +++ .../templates/mmock/azure_login_default_mount.yml.j2 | 3 +++ 8 files changed, 26 insertions(+) diff --git a/tests/integration/targets/auth_azure/defaults/main.yml b/tests/integration/targets/auth_azure/defaults/main.yml index 8562ec026..16540fbcd 100644 --- a/tests/integration/targets/auth_azure/defaults/main.yml +++ b/tests/integration/targets/auth_azure/defaults/main.yml @@ -1,3 +1,6 @@ +# 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 diff --git a/tests/integration/targets/auth_azure/meta/main.yml b/tests/integration/targets/auth_azure/meta/main.yml index d3acb69e9..fa4fc1c74 100644 --- a/tests/integration/targets/auth_azure/meta/main.yml +++ b/tests/integration/targets/auth_azure/meta/main.yml @@ -1,3 +1,6 @@ +# 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 diff --git a/tests/integration/targets/auth_azure/tasks/azure_test_controller.yml b/tests/integration/targets/auth_azure/tasks/azure_test_controller.yml index 725984623..8b38c0d73 100644 --- a/tests/integration/targets/auth_azure/tasks/azure_test_controller.yml +++ b/tests/integration/targets/auth_azure/tasks/azure_test_controller.yml @@ -1,3 +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 +--- - name: "Test block" vars: is_default_path: "{{ this_path == default_path }}" diff --git a/tests/integration/targets/auth_azure/tasks/azure_test_target.yml b/tests/integration/targets/auth_azure/tasks/azure_test_target.yml index c52e396bf..0c637c706 100644 --- a/tests/integration/targets/auth_azure/tasks/azure_test_target.yml +++ b/tests/integration/targets/auth_azure/tasks/azure_test_target.yml @@ -1,3 +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 +--- - name: "Test block" vars: is_default_path: "{{ this_path == default_path }}" diff --git a/tests/integration/targets/auth_azure/tasks/main.yml b/tests/integration/targets/auth_azure/tasks/main.yml index 7d9ea0a90..95bde76e5 100644 --- a/tests/integration/targets/auth_azure/tasks/main.yml +++ b/tests/integration/targets/auth_azure/tasks/main.yml @@ -1,3 +1,6 @@ +# 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 --- # 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 diff --git a/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_alt_mount.yml.j2 b/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_alt_mount.yml.j2 index 6e309c903..b1588fd6e 100644 --- a/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_alt_mount.yml.j2 +++ b/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_alt_mount.yml.j2 @@ -1,4 +1,7 @@ #jinja2:variable_start_string:'[%', variable_end_string:'%]' +# 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 --- request: method: POST|PUT diff --git a/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_bad_request.yml.j2 b/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_bad_request.yml.j2 index 53fda21b8..d447dd015 100644 --- a/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_bad_request.yml.j2 +++ b/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_bad_request.yml.j2 @@ -1,4 +1,7 @@ #jinja2:variable_start_string:'[%', variable_end_string:'%]' +# 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 --- request: method: POST|PUT diff --git a/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_default_mount.yml.j2 b/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_default_mount.yml.j2 index a19f05cb6..af26ada83 100644 --- a/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_default_mount.yml.j2 +++ b/tests/integration/targets/setup_localenv_docker/templates/mmock/azure_login_default_mount.yml.j2 @@ -1,4 +1,7 @@ #jinja2:variable_start_string:'[%', variable_end_string:'%]' +# 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 --- request: method: POST|PUT From 6a0d1c182fc89d8489678c7353293b7d8ff17f3b Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Sun, 21 Aug 2022 13:24:59 -0400 Subject: [PATCH 13/15] fix sanity line length --- plugins/doc_fragments/auth.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/doc_fragments/auth.py b/plugins/doc_fragments/auth.py index bf2236a72..f3f0d19a1 100644 --- a/plugins/doc_fragments/auth.py +++ b/plugins/doc_fragments/auth.py @@ -102,7 +102,9 @@ class ModuleDocFragment(object): azure_tenant_id: description: - The azure tenant id of the service principal, also known as directory id of the active directory, 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. + - >- + 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 From c20b4490b027ec68d76ef5c1098cdfa1304904ca Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Sun, 21 Aug 2022 13:40:03 -0400 Subject: [PATCH 14/15] Update plugins/doc_fragments/auth.py forgot to commit this one before --- plugins/doc_fragments/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/doc_fragments/auth.py b/plugins/doc_fragments/auth.py index f3f0d19a1..a13dc71c4 100644 --- a/plugins/doc_fragments/auth.py +++ b/plugins/doc_fragments/auth.py @@ -101,7 +101,7 @@ class ModuleDocFragment(object): version_added: '0.2.0' azure_tenant_id: description: - - The azure tenant id of the service principal, also known as directory id of the active directory, should be a uuid. + - 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. From 7bb2964930a33a540f28f9a08494b03b46675771 Mon Sep 17 00:00:00 2001 From: Brian Scholer <1260690+briantist@users.noreply.github.com> Date: Sun, 21 Aug 2022 14:04:27 -0400 Subject: [PATCH 15/15] remove unused fixture --- .../plugins/module_utils/authentication/test_auth_azure.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py index 793325837..747a432df 100644 --- a/tests/unit/plugins/module_utils/authentication/test_auth_azure.py +++ b/tests/unit/plugins/module_utils/authentication/test_auth_azure.py @@ -34,11 +34,6 @@ def option_dict(): } -@pytest.fixture -def azure_tenant_id(): - return 'tenant-id' - - @pytest.fixture def azure_client_id(): return 'client-id'