From 21ea92feca09a4dc1c9ad38ebbd3c60cde4444f5 Mon Sep 17 00:00:00 2001 From: Yunge Zhu <37337818+yungezz@users.noreply.github.com> Date: Thu, 24 May 2018 07:37:44 +0800 Subject: [PATCH] Fixes #36621, support adfs auth through adal (#37909) * update username/password auth to use adal lib * remove default client_id after discussion * fix lint error: trailing whitespace --- contrib/inventory/azure_rm.py | 47 +++++++++++++++++- .../rst/scenario_guides/guide_azure.rst | 22 ++++++++- lib/ansible/module_utils/azure_rm_common.py | 48 +++++++++++++++++-- .../utils/module_docs_fragments/azure.py | 6 +++ 4 files changed, 117 insertions(+), 6 deletions(-) diff --git a/contrib/inventory/azure_rm.py b/contrib/inventory/azure_rm.py index e5e9da5bed..5de5d244fe 100755 --- a/contrib/inventory/azure_rm.py +++ b/contrib/inventory/azure_rm.py @@ -50,6 +50,7 @@ Command line arguments: - ad_user - password - cloud_environment + - adfs_authority_url Environment variables: - AZURE_PROFILE @@ -60,6 +61,7 @@ Environment variables: - AZURE_AD_USER - AZURE_PASSWORD - AZURE_CLOUD_ENVIRONMENT + - AZURE_ADFS_AUTHORITY_URL Run for Specific Host ----------------------- @@ -209,6 +211,7 @@ HAS_AZURE_CLI_CORE = True CLIError = None try: + from msrestazure.azure_active_directory import AADTokenCredentials from msrestazure.azure_exceptions import CloudError from msrestazure.azure_active_directory import MSIAuthentication from msrestazure import azure_cloud @@ -219,6 +222,7 @@ try: from azure.mgmt.resource.resources import ResourceManagementClient from azure.mgmt.resource.subscriptions import SubscriptionClient from azure.mgmt.compute import ComputeManagementClient + from adal.authentication_context import AuthenticationContext except ImportError as exc: HAS_AZURE_EXC = exc HAS_AZURE = False @@ -245,6 +249,7 @@ AZURE_CREDENTIAL_ENV_MAPPING = dict( ad_user='AZURE_AD_USER', password='AZURE_PASSWORD', cloud_environment='AZURE_CLOUD_ENVIRONMENT', + adfs_authority_url='AZURE_ADFS_AUTHORITY_URL' ) AZURE_CONFIG_SETTINGS = dict( @@ -281,6 +286,8 @@ class AzureRM(object): self._compute_client = None self._resource_client = None self._network_client = None + self._adfs_authority_url = None + self._resource = None self.debug = False if args.debug: @@ -316,6 +323,17 @@ class AzureRM(object): self.log("setting subscription_id") self.subscription_id = self.credentials['subscription_id'] + # get authentication authority + # for adfs, user could pass in authority or not. + # for others, use default authority from cloud environment + if self.credentials.get('adfs_authority_url') is None: + self._adfs_authority_url = self._cloud_environment.endpoints.active_directory + else: + self._adfs_authority_url = self.credentials.get('adfs_authority_url') + + # get resource from cloud environment + self._resource = self._cloud_environment.endpoints.active_directory_resource_id + if self.credentials.get('credentials'): self.azure_credentials = self.credentials.get('credentials') elif self.credentials.get('client_id') and self.credentials.get('secret') and self.credentials.get('tenant'): @@ -323,6 +341,20 @@ class AzureRM(object): secret=self.credentials['secret'], tenant=self.credentials['tenant'], cloud_environment=self._cloud_environment) + + elif self.credentials.get('ad_user') is not None and \ + self.credentials.get('password') is not None and \ + self.credentials.get('client_id') is not None and \ + self.credentials.get('tenant') is not None: + + self.azure_credentials = self.acquire_token_with_username_password( + self._adfs_authority_url, + self._resource, + self.credentials['ad_user'], + self.credentials['password'], + self.credentials['client_id'], + self.credentials['tenant']) + elif self.credentials.get('ad_user') is not None and self.credentials.get('password') is not None: tenant = self.credentials.get('tenant') if not tenant: @@ -331,9 +363,12 @@ class AzureRM(object): self.credentials['password'], tenant=tenant, cloud_environment=self._cloud_environment) + else: self.fail("Failed to authenticate with provided credentials. Some attributes were missing. " - "Credentials must include client_id, secret and tenant or ad_user and password.") + "Credentials must include client_id, secret and tenant or ad_user and password, or " + "ad_user, password, client_id, tenant and adfs_authority_url(optional) for ADFS authentication, or " + "be logged in using AzureCLI.") def log(self, msg): if self.debug: @@ -453,6 +488,16 @@ class AzureRM(object): return None + def acquire_token_with_username_password(self, authority, resource, username, password, client_id, tenant): + authority_uri = authority + + if tenant is not None: + authority_uri = authority + '/' + tenant + + context = AuthenticationContext(authority_uri) + token_response = context.acquire_token_with_username_password(resource, username, password, client_id) + return AADTokenCredentials(token_response) + def _register(self, key): try: # We have to perform the one-time registration here. Otherwise, we receive an error the first diff --git a/docs/docsite/rst/scenario_guides/guide_azure.rst b/docs/docsite/rst/scenario_guides/guide_azure.rst index 90a9631644..1cbec34940 100644 --- a/docs/docsite/rst/scenario_guides/guide_azure.rst +++ b/docs/docsite/rst/scenario_guides/guide_azure.rst @@ -84,7 +84,16 @@ To pass Active Directory username/password via the environment, define the follo * AZURE_AD_USER * AZURE_PASSWORD -* AZURE_SUBSCRIPTION_ID + +To pass Active Directory username/password in ADFS via the environment, define the following variables: + +* AZURE_AD_USER +* AZURE_PASSWORD +* AZURE_CLIENT_ID +* AZURE_TENANT +* AZURE_ADFS_AUTHORITY_URL + +"AZURE_ADFS_AUTHORITY_URL" is optional. It's necessary only when you have own ADFS authority like https://xxx.com/adfs. Storing in a File ````````````````` @@ -118,7 +127,16 @@ Or, pass the following parameters for Active Directory username/password: * ad_user * password -* subscription_id + +Or, pass the following parameters for ADFS username/pasword: + +* ad_user +* password +* client_id +* tenant +* adfs_authority_url + +"adfs_authority_url" is optional. It's necessary only when you have own ADFS authority like https://xxx.com/adfs. Other Cloud Environments diff --git a/lib/ansible/module_utils/azure_rm_common.py b/lib/ansible/module_utils/azure_rm_common.py index 0546e87887..6e51b48039 100644 --- a/lib/ansible/module_utils/azure_rm_common.py +++ b/lib/ansible/module_utils/azure_rm_common.py @@ -35,7 +35,8 @@ AZURE_COMMON_ARGS = dict( password=dict(type='str', no_log=True), cloud_environment=dict(type='str', default='AzureCloud'), cert_validation_mode=dict(type='str', choices=['validate', 'ignore']), - api_profile=dict(type='str', default='latest') + api_profile=dict(type='str', default='latest'), + adfs_authority_url=dict(type='str', default=None) # debug=dict(type='bool', default=False), ) @@ -49,6 +50,7 @@ AZURE_CREDENTIAL_ENV_MAPPING = dict( password='AZURE_PASSWORD', cloud_environment='AZURE_CLOUD_ENVIRONMENT', cert_validation_mode='AZURE_CERT_VALIDATION_MODE', + adfs_authority_url='AZURE_ADFS_AUTHORITY_URL' ) # FUTURE: this should come from the SDK or an external location. @@ -127,6 +129,7 @@ except ImportError as exc: try: from enum import Enum + from msrestazure.azure_active_directory import AADTokenCredentials from msrestazure.azure_exceptions import CloudError from msrestazure.azure_active_directory import MSIAuthentication from msrestazure.tools import resource_id, is_valid_resource_id @@ -147,6 +150,7 @@ try: from azure.mgmt.web import WebSiteManagementClient from azure.mgmt.containerservice import ContainerServiceClient from azure.storage.cloudstorageaccount import CloudStorageAccount + from adal.authentication_context import AuthenticationContext except ImportError as exc: HAS_AZURE_EXC = exc HAS_AZURE = False @@ -268,6 +272,8 @@ class AzureRMModuleBase(object): self._dns_client = None self._web_client = None self._containerservice_client = None + self._adfs_authority_url = None + self._resource = None self.check_mode = self.module.check_mode self.api_profile = self.module.params.get('api_profile') @@ -318,6 +324,17 @@ class AzureRMModuleBase(object): self.log("setting subscription_id") self.subscription_id = self.credentials['subscription_id'] + # get authentication authority + # for adfs, user could pass in authority or not. + # for others, use default authority from cloud environment + if self.credentials.get('adfs_authority_url') is None: + self._adfs_authority_url = self._cloud_environment.endpoints.active_directory + else: + self._adfs_authority_url = self.credentials.get('adfs_authority_url') + + # get resource from cloud environment + self._resource = self._cloud_environment.endpoints.active_directory_resource_id + if self.credentials.get('credentials') is not None: # AzureCLI credentials self.azure_credentials = self.credentials['credentials'] @@ -330,6 +347,19 @@ class AzureRMModuleBase(object): cloud_environment=self._cloud_environment, verify=self._cert_validation_mode == 'validate') + elif self.credentials.get('ad_user') is not None and \ + self.credentials.get('password') is not None and \ + self.credentials.get('client_id') is not None and \ + self.credentials.get('tenant') is not None: + + self.azure_credentials = self.acquire_token_with_username_password( + self._adfs_authority_url, + self._resource, + self.credentials['ad_user'], + self.credentials['password'], + self.credentials['client_id'], + self.credentials['tenant']) + elif self.credentials.get('ad_user') is not None and self.credentials.get('password') is not None: tenant = self.credentials.get('tenant') if not tenant: @@ -342,8 +372,9 @@ class AzureRMModuleBase(object): verify=self._cert_validation_mode == 'validate') else: self.fail("Failed to authenticate with provided credentials. Some attributes were missing. " - "Credentials must include client_id, secret and tenant or ad_user and password or " - "be logged using AzureCLI.") + "Credentials must include client_id, secret and tenant or ad_user and password, or " + "ad_user, password, client_id, tenant and adfs_authority_url(optional) for ADFS authentication, or " + "be logged in using AzureCLI.") # common parameter validation if self.module.params.get('tags'): @@ -353,6 +384,17 @@ class AzureRMModuleBase(object): res = self.exec_module(**self.module.params) self.module.exit_json(**res) + def acquire_token_with_username_password(self, authority, resource, username, password, client_id, tenant): + authority_uri = authority + + if tenant is not None: + authority_uri = authority + '/' + tenant + + context = AuthenticationContext(authority_uri) + token_response = context.acquire_token_with_username_password(resource, username, password, client_id) + + return AADTokenCredentials(token_response) + def check_client_version(self, client_type): # Ensure Azure modules are at least 2.0.0rc5. package_version = AZURE_PKG_VERSIONS.get(client_type.__name__, None) diff --git a/lib/ansible/utils/module_docs_fragments/azure.py b/lib/ansible/utils/module_docs_fragments/azure.py index c826d533aa..a87eef753c 100644 --- a/lib/ansible/utils/module_docs_fragments/azure.py +++ b/lib/ansible/utils/module_docs_fragments/azure.py @@ -54,6 +54,12 @@ options: the C(AZURE_CLOUD_ENVIRONMENT) environment variable. default: AzureCloud version_added: 2.4 + adfs_authority_url: + description: + - Azure AD authority url. Use when authenticating with Username/password, and has your own ADFS authority. + required: false + default: null + version_added: 2.6 cert_validation_mode: description: - Controls the certificate validation behavior for Azure endpoints. By default, all modules will validate the server certificate, but