Skip to content

Commit

Permalink
Fixes ansible#36621, support adfs auth through adal (ansible#37909)
Browse files Browse the repository at this point in the history
* update username/password auth to use adal lib

* remove default client_id after discussion

* fix lint error: trailing whitespace
  • Loading branch information
yungezz authored and nitzmahone committed May 23, 2018
1 parent e93fbed commit 21ea92f
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 6 deletions.
47 changes: 46 additions & 1 deletion contrib/inventory/azure_rm.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
- ad_user
- password
- cloud_environment
- adfs_authority_url
Environment variables:
- AZURE_PROFILE
Expand All @@ -60,6 +61,7 @@
- AZURE_AD_USER
- AZURE_PASSWORD
- AZURE_CLOUD_ENVIRONMENT
- AZURE_ADFS_AUTHORITY_URL
Run for Specific Host
-----------------------
Expand Down Expand Up @@ -209,6 +211,7 @@
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
Expand All @@ -219,6 +222,7 @@
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
Expand All @@ -245,6 +249,7 @@
ad_user='AZURE_AD_USER',
password='AZURE_PASSWORD',
cloud_environment='AZURE_CLOUD_ENVIRONMENT',
adfs_authority_url='AZURE_ADFS_AUTHORITY_URL'
)

AZURE_CONFIG_SETTINGS = dict(
Expand Down Expand Up @@ -281,6 +286,8 @@ def __init__(self, args):
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:
Expand Down Expand Up @@ -316,13 +323,38 @@ def __init__(self, args):
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'):
self.azure_credentials = ServicePrincipalCredentials(client_id=self.credentials['client_id'],
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:
Expand All @@ -331,9 +363,12 @@ def __init__(self, args):
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:
Expand Down Expand Up @@ -453,6 +488,16 @@ def _get_credentials(self, params):

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
Expand Down
22 changes: 20 additions & 2 deletions docs/docsite/rst/scenario_guides/guide_azure.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
`````````````````
Expand Down Expand Up @@ -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
Expand Down
48 changes: 45 additions & 3 deletions lib/ansible/module_utils/azure_rm_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
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),
)

Expand All @@ -49,6 +50,7 @@
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.
Expand Down Expand Up @@ -127,6 +129,7 @@

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
Expand All @@ -147,6 +150,7 @@
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
Expand Down Expand Up @@ -268,6 +272,8 @@ def __init__(self, derived_arg_spec, bypass_checks=False, no_log=False,
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')
Expand Down Expand Up @@ -318,6 +324,17 @@ def __init__(self, derived_arg_spec, bypass_checks=False, no_log=False,
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']
Expand All @@ -330,6 +347,19 @@ def __init__(self, derived_arg_spec, bypass_checks=False, no_log=False,
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:
Expand All @@ -342,8 +372,9 @@ def __init__(self, derived_arg_spec, bypass_checks=False, no_log=False,
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'):
Expand All @@ -353,6 +384,17 @@ def __init__(self, derived_arg_spec, bypass_checks=False, no_log=False,
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)
Expand Down
6 changes: 6 additions & 0 deletions lib/ansible/utils/module_docs_fragments/azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ class ModuleDocFragment(object):
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
Expand Down

0 comments on commit 21ea92f

Please sign in to comment.