diff --git a/azure-cli.pyproj b/azure-cli.pyproj
index 7a7dcf8216a..0744e604dd2 100644
--- a/azure-cli.pyproj
+++ b/azure-cli.pyproj
@@ -45,6 +45,7 @@
+
@@ -54,6 +55,7 @@
+
diff --git a/requirements.txt b/requirements.txt
index 0f8de54df5d..5a1ec7e75d7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,5 +10,5 @@ requests==2.9.1
six==1.10.0
vcrpy==1.7.4
-#Same as: -e git+https://github.com/yugangw-msft/azure-activedirectory-library-for-python.git@0.2.0#egg=azure-activedirectory-library-for-python
-http://40.112.211.51:8080/packages/adal-0.2.0.zip
+#-e git+https://github.com/yugangw-msft/azure-activedirectory-library-for-python@v2.1#egg=azure-activedirectory-library-for-python
+http://40.112.211.51:8080/packages/adal-0.2.1.zip
diff --git a/scripts/command_modules/install.py b/scripts/command_modules/install.py
index a683b2c4120..cf2a74218b4 100644
--- a/scripts/command_modules/install.py
+++ b/scripts/command_modules/install.py
@@ -4,14 +4,12 @@
from _common import get_all_command_modules, exec_command, print_summary
-dev_null_file = open(os.devnull, 'w')
-
all_command_modules = get_all_command_modules()
print("Installing command modules.")
failed_module_names = []
for name, fullpath in all_command_modules:
- success = exec_command("pip install -e "+fullpath, stdout=dev_null_file)
+ success = exec_command("pip install -e "+fullpath)
if not success:
failed_module_names.append(name)
diff --git a/setup.py b/setup.py
index 5dc58d49a47..ebcb9c01ea8 100644
--- a/setup.py
+++ b/setup.py
@@ -60,6 +60,7 @@
]
DEPENDENCIES = [
+ 'adal==0.2.1', #from internal index server.
'applicationinsights',
'argcomplete',
'azure==2.0.0rc1',
@@ -81,15 +82,15 @@ def _post_install(dir):
# Upgrade/update will install if it doesn't exist.
# We do this so these components are updated when the user updates the CLI.
if INSTALL_FROM_PUBLIC:
- pip.main(['install', '--upgrade', 'azure-cli-components', '--disable-pip-version-check'])
- check_call(['az', 'components', 'update', '-n', 'profile', '--disable-version-check'])
+ pip.main(['install', '--upgrade', 'azure-cli-component', '--disable-pip-version-check'])
+ check_call(['az', 'component', 'update', '-n', 'profile'])
else:
# use private PyPI server.
- pip.main(['install', '--upgrade', 'azure-cli-components', '--extra-index-url',
+ pip.main(['install', '--upgrade', 'azure-cli-component', '--extra-index-url',
PRIVATE_PYPI_URL, '--trusted-host', PRIVATE_PYPI_HOST,
'--disable-pip-version-check'])
- check_call(['az', 'components', 'update', '-n', 'profile', '-p', '--disable-version-check'])
+ check_call(['az', 'component', 'update', '-n', 'profile', '-p'])
class OnInstall(install):
def run(self):
diff --git a/src/azure/cli/_azure_env.py b/src/azure/cli/_azure_env.py
new file mode 100644
index 00000000000..2aad9a54b91
--- /dev/null
+++ b/src/azure/cli/_azure_env.py
@@ -0,0 +1,43 @@
+CLIENT_ID = '04b07795-8ddb-461a-bbee-02f9e1bf7b46'
+
+ENV_DEFAULT = 'AzureCloud'
+ENV_US_GOVERNMENT = 'AzureUSGovernment'
+ENV_CHINA = 'AzureChinaCloud'
+
+COMMON_TENANT = 'common'
+
+#ported from https://github.com/Azure/azure-xplat-cli/blob/dev/lib/util/profile/environment.js
+class ENDPOINT_URLS: #pylint: disable=too-few-public-methods,old-style-class,no-init
+ MANAGEMENT = 'management'
+ ACTIVE_DIRECTORY_AUTHORITY = 'active_directory_authority'
+
+_environments = {
+ ENV_DEFAULT: {
+ ENDPOINT_URLS.MANAGEMENT: 'https://management.core.windows.net/',
+ ENDPOINT_URLS.ACTIVE_DIRECTORY_AUTHORITY : 'https://login.microsoftonline.com'
+ },
+ ENV_CHINA: {
+ ENDPOINT_URLS.MANAGEMENT: 'https://management.core.chinacloudapi.cn/',
+ ENDPOINT_URLS.ACTIVE_DIRECTORY_AUTHORITY: 'https://login.chinacloudapi.cn'
+ },
+ ENV_US_GOVERNMENT: {
+ ENDPOINT_URLS.MANAGEMENT: 'https://management.core.usgovcloudapi.net/',
+ ENDPOINT_URLS.ACTIVE_DIRECTORY_AUTHORITY: 'https://login.microsoftonline.com'
+ }
+}
+
+def get_env(env_name=None):
+ if env_name is None:
+ env_name = ENV_DEFAULT
+ elif env_name not in _environments:
+ raise ValueError
+ return _environments[env_name]
+
+def get_authority_url(tenant=None, env_name=None):
+ env = get_env(env_name)
+ return env[ENDPOINT_URLS.ACTIVE_DIRECTORY_AUTHORITY] + '/' + (tenant or COMMON_TENANT)
+
+def get_management_endpoint_url(env_name=None):
+ env = get_env(env_name)
+ return env[ENDPOINT_URLS.MANAGEMENT]
+
diff --git a/src/azure/cli/_profile.py b/src/azure/cli/_profile.py
index af5b5866778..5945913f4f2 100644
--- a/src/azure/cli/_profile.py
+++ b/src/azure/cli/_profile.py
@@ -1,98 +1,374 @@
-import collections
+from __future__ import print_function
+import collections
+from codecs import open as codecs_open
+import json
+import os.path
from msrest.authentication import BasicTokenAuthentication
-from .main import CONFIG
+import adal
+from azure.mgmt.resource.subscriptions import (SubscriptionClient,
+ SubscriptionClientConfiguration)
+from .main import ACCOUNT
+from ._locale import L
+from ._azure_env import (get_authority_url, CLIENT_ID, get_management_endpoint_url,
+ ENV_DEFAULT, COMMON_TENANT)
+
+#Names below are used by azure-xplat-cli to persist account information into
+#~/.azure/azureProfile.json or osx/keychainer or windows secure storage,
+#which azuer-cli will share.
+#Please do not rename them unless you know what you are doing.
+_IS_DEFAULT_SUBSCRIPTION = 'isDefault'
+_SUBSCRIPTION_ID = 'id'
+_SUBSCRIPTION_NAME = 'name'
+_TENANT_ID = 'tenantId'
+_USER_ENTITY = 'user'
+_USER_NAME = 'name'
+_SUBSCRIPTIONS = 'subscriptions'
+_ENVIRONMENT_NAME = 'environmentName'
+_STATE = 'state'
+_USER_TYPE = 'type'
+_USER = 'user'
+_SERVICE_PRINCIPAL = 'servicePrincipal'
+_SERVICE_PRINCIPAL_ID = 'servicePrincipalId'
+_SERVICE_PRINCIPAL_TENANT = 'servicePrincipalTenant'
+_TOKEN_ENTRY_USER_ID = 'userId'
+#This could mean real access token, or client secret of a service principal
+#This naming is no good, but can't change because xplat-cli does so.
+_ACCESS_TOKEN = 'accessToken'
+
+_AUTH_CTX_FACTORY = lambda authority, cache: adal.AuthenticationContext(authority, cache=cache)
+
+def _read_file_content(file_path):
+ file_text = None
+ if os.path.isfile(file_path):
+ with codecs_open(file_path, 'r', encoding='ascii') as file_to_read:
+ file_text = file_to_read.read()
+ return file_text
class Profile(object):
+ def __init__(self, storage=None, auth_ctx_factory=None):
+ self._storage = storage or ACCOUNT
+ factory = auth_ctx_factory or _AUTH_CTX_FACTORY
+ self._creds_cache = CredsCache(factory)
+ self._subscription_finder = SubscriptionFinder(factory, self._creds_cache.adal_token_cache)
+
+ def find_subscriptions_on_login(self, #pylint: disable=too-many-arguments
+ interactive,
+ username,
+ password,
+ is_service_principal,
+ tenant):
+ self._creds_cache.remove_cached_creds(username)
+ subscriptions = []
+ if interactive:
+ subscriptions = self._subscription_finder.find_through_interactive_flow()
+ else:
+ if is_service_principal:
+ if not tenant:
+ raise ValueError(L('Please supply tenant using "--tenant"'))
+
+ subscriptions = self._subscription_finder.find_from_service_principal_id(username,
+ password,
+ tenant)
+ else:
+ subscriptions = self._subscription_finder.find_from_user_account(username, password)
- def __init__(self, storage=CONFIG):
- self._storage = storage
+ if not subscriptions:
+ raise RuntimeError(L('No subscriptions found for this account.'))
+
+ if is_service_principal:
+ self._creds_cache.save_service_principal_cred(username,
+ password,
+ tenant)
+ if self._creds_cache.adal_token_cache.has_state_changed:
+ self._creds_cache.persist_cached_creds()
+ consolidated = Profile._normalize_properties(self._subscription_finder.user_id,
+ subscriptions,
+ is_service_principal,
+ ENV_DEFAULT)
+ self._set_subscriptions(consolidated)
+ return consolidated
@staticmethod
- def normalize_properties(user, subscriptions):
+ def _normalize_properties(user, subscriptions, is_service_principal, environment):
consolidated = []
for s in subscriptions:
consolidated.append({
- 'id': s.id.rpartition('/')[2],
- 'name': s.display_name,
- 'state': s.state,
- 'user': user,
- 'active': False
+ _SUBSCRIPTION_ID: s.id.rpartition('/')[2],
+ _SUBSCRIPTION_NAME: s.display_name,
+ _STATE: s.state,
+ _USER_ENTITY: {
+ _USER_NAME: user,
+ _USER_TYPE: _SERVICE_PRINCIPAL if is_service_principal else _USER
+ },
+ _IS_DEFAULT_SUBSCRIPTION: False,
+ _TENANT_ID: s.tenant_id,
+ _ENVIRONMENT_NAME: environment
})
return consolidated
- def set_subscriptions(self, new_subscriptions, access_token):
- existing_ones = self.load_subscriptions()
- active_one = next((x for x in existing_ones if x['active']), None)
- active_subscription_id = active_one['id'] if active_one else None
+ def _set_subscriptions(self, new_subscriptions):
+ existing_ones = self.load_cached_subscriptions()
+ active_one = next((x for x in existing_ones if x.get(_IS_DEFAULT_SUBSCRIPTION)), None)
+ active_subscription_id = active_one[_SUBSCRIPTION_ID] if active_one else None
#merge with existing ones
- dic = collections.OrderedDict((x['id'], x) for x in existing_ones)
- dic.update((x['id'], x) for x in new_subscriptions)
+ dic = collections.OrderedDict((x[_SUBSCRIPTION_ID], x) for x in existing_ones)
+ dic.update((x[_SUBSCRIPTION_ID], x) for x in new_subscriptions)
subscriptions = list(dic.values())
if active_one:
new_active_one = next(
- (x for x in new_subscriptions if x['id'] == active_subscription_id), None)
+ (x for x in new_subscriptions if x[_SUBSCRIPTION_ID] == active_subscription_id),
+ None)
for s in subscriptions:
- s['active'] = False
+ s[_IS_DEFAULT_SUBSCRIPTION] = False
if not new_active_one:
new_active_one = new_subscriptions[0]
- new_active_one['active'] = True
+ new_active_one[_IS_DEFAULT_SUBSCRIPTION] = True
else:
- new_subscriptions[0]['active'] = True
+ new_subscriptions[0][_IS_DEFAULT_SUBSCRIPTION] = True
+
+ self._cache_subscriptions_to_local_storage(subscriptions)
+
+ def set_active_subscription(self, subscription_id_or_name):
+ subscriptions = self.load_cached_subscriptions()
+
+ subscription_id_or_name = subscription_id_or_name.lower()
+ result = [x for x in subscriptions
+ if subscription_id_or_name == x[_SUBSCRIPTION_ID].lower() or
+ subscription_id_or_name == x[_SUBSCRIPTION_NAME].lower()]
+
+ if len(result) != 1:
+ raise ValueError('The subscription of "{}" does not exist or has more than'
+ ' one match.'.format(subscription_id_or_name))
+
+ for s in subscriptions:
+ s[_IS_DEFAULT_SUBSCRIPTION] = False
+ result[0][_IS_DEFAULT_SUBSCRIPTION] = True
+
+ self._cache_subscriptions_to_local_storage(subscriptions)
+
+ def logout(self, user_or_sp):
+ subscriptions = self.load_cached_subscriptions()
+ result = [x for x in subscriptions
+ if user_or_sp.lower() == x[_USER_ENTITY][_USER_NAME].lower()]
+ subscriptions = [x for x in subscriptions if x not in result]
+
+ #reset the active subscription if needed
+ result = [x for x in subscriptions if x.get(_IS_DEFAULT_SUBSCRIPTION)]
+ if not result and subscriptions:
+ subscriptions[0][_IS_DEFAULT_SUBSCRIPTION] = True
+
+ self._cache_subscriptions_to_local_storage(subscriptions)
- #before adal/python is available, persist tokens with other profile info
- for s in new_subscriptions:
- s['access_token'] = access_token
+ self._creds_cache.remove_cached_creds(user_or_sp)
- self._save_subscriptions(subscriptions)
+
+ def load_cached_subscriptions(self):
+ return self._storage.get(_SUBSCRIPTIONS) or []
+
+ def _cache_subscriptions_to_local_storage(self, subscriptions):
+ self._storage[_SUBSCRIPTIONS] = subscriptions
def get_login_credentials(self):
- subscriptions = self.load_subscriptions()
+ subscriptions = self.load_cached_subscriptions()
if not subscriptions:
raise ValueError('Please run login to setup account.')
- active = [x for x in subscriptions if x['active']]
+ active = [x for x in subscriptions if x.get(_IS_DEFAULT_SUBSCRIPTION)]
if len(active) != 1:
raise ValueError('Please run "account set" to select active account.')
+ active_account = active[0]
+
+ user_type = active_account[_USER_ENTITY][_USER_TYPE]
+ username_or_sp_id = active_account[_USER_ENTITY][_USER_NAME]
+ if user_type == _USER:
+ access_token = self._creds_cache.retrieve_token_for_user(username_or_sp_id,
+ active_account[_TENANT_ID])
+ else:
+ access_token = self._creds_cache.retrieve_token_for_service_principal(
+ username_or_sp_id)
return BasicTokenAuthentication(
- {'access_token': active[0]['access_token']}), active[0]['id']
+ {'access_token': access_token}), active_account[_SUBSCRIPTION_ID]
- def set_active_subscription(self, subscription_id_or_name):
- subscriptions = self.load_subscriptions()
- subscription_id_or_name = subscription_id_or_name.lower()
- result = [x for x in subscriptions
- if subscription_id_or_name == x['id'].lower() or
- subscription_id_or_name == x['name'].lower()]
+class SubscriptionFinder(object):
+ '''finds all subscriptions for a user or service principal'''
+ def __init__(self, auth_context_factory, adal_token_cache, arm_client_factory=None):
+ self._adal_token_cache = adal_token_cache
+ self._auth_context_factory = auth_context_factory
+ self._resource = get_management_endpoint_url(ENV_DEFAULT)
+ self.user_id = None # will figure out after log user in
+ self._arm_client_factory = arm_client_factory or \
+ (lambda config: SubscriptionClient(config)) #pylint: disable=unnecessary-lambda
- if len(result) != 1:
- raise ValueError('The subscription of "{}" does not exist or has more than'
- ' one match.'.format(subscription_id_or_name))
+ def find_from_user_account(self, username, password):
+ context = self._create_auth_context(COMMON_TENANT)
+ token_entry = context.acquire_token_with_username_password(
+ self._resource,
+ username,
+ password,
+ CLIENT_ID)
+ self.user_id = token_entry[_TOKEN_ENTRY_USER_ID]
+ result = self._find_using_common_tenant(token_entry[_ACCESS_TOKEN])
+ return result
+
+ def find_through_interactive_flow(self):
+ context = self._create_auth_context(COMMON_TENANT)
+ code = context.acquire_user_code(self._resource, CLIENT_ID)
+ print(code['message'])
+ token_entry = context.acquire_token_with_device_code(self._resource, code, CLIENT_ID)
+ self.user_id = token_entry[_TOKEN_ENTRY_USER_ID]
+ result = self._find_using_common_tenant(token_entry[_ACCESS_TOKEN])
+ return result
+
+ def find_from_service_principal_id(self, client_id, secret, tenant):
+ context = self._create_auth_context(tenant, False)
+ token_entry = context.acquire_token_with_client_credentials(
+ self._resource,
+ client_id,
+ secret)
+ self.user_id = client_id
+ result = self._find_using_specific_tenant(tenant, token_entry[_ACCESS_TOKEN])
+ return result
+
+ def _create_auth_context(self, tenant, use_token_cache=True):
+ token_cache = self._adal_token_cache if use_token_cache else None
+ authority = get_authority_url(tenant, ENV_DEFAULT)
+ return self._auth_context_factory(authority, token_cache)
+
+ def _find_using_common_tenant(self, access_token):
+ all_subscriptions = []
+ token_credential = BasicTokenAuthentication({'access_token': access_token})
+ client = self._arm_client_factory(SubscriptionClientConfiguration(token_credential))
+ tenants = client.tenants.list()
+ for t in tenants:
+ tenant_id = t.tenant_id
+ temp_context = self._create_auth_context(tenant_id)
+ temp_credentials = temp_context.acquire_token(self._resource, self.user_id, CLIENT_ID)
+ subscriptions = self._find_using_specific_tenant(
+ tenant_id,
+ temp_credentials[_ACCESS_TOKEN])
+ all_subscriptions.extend(subscriptions)
+
+ return all_subscriptions
+ def _find_using_specific_tenant(self, tenant, access_token):
+ token_credential = BasicTokenAuthentication({'access_token': access_token})
+ client = self._arm_client_factory(SubscriptionClientConfiguration(token_credential))
+ subscriptions = client.subscriptions.list()
+ all_subscriptions = []
for s in subscriptions:
- s['active'] = False
- result[0]['active'] = True
+ setattr(s, 'tenant_id', tenant)
+ all_subscriptions.append(s)
+ return all_subscriptions
- self._save_subscriptions(subscriptions)
+class CredsCache(object):
+ '''Caches AAD tokena and service principal secrets, and persistence will
+ also be handled
+ '''
+ def __init__(self, auth_ctx_factory=None):
+ self._token_file = os.path.expanduser('~/.azure/accessTokens.json')
+ self._service_principal_creds = []
+ self._auth_ctx_factory = auth_ctx_factory or _AUTH_CTX_FACTORY
+ self.adal_token_cache = None
+ self._load_creds()
+ self._resource = get_management_endpoint_url(ENV_DEFAULT)
- def logout(self, user):
- subscriptions = self.load_subscriptions()
- result = [x for x in subscriptions if user.lower() == x['user'].lower()]
- subscriptions = [x for x in subscriptions if x not in result]
+ def persist_cached_creds(self):
+ #be compatible with azure-xplat-cli, use 'ascii' so to save w/o a BOM
+ with codecs_open(self._token_file, 'w', encoding='ascii') as cred_file:
+ items = self.adal_token_cache.read_items()
+ all_creds = [entry for _, entry in items]
+ all_creds.extend(self._service_principal_creds)
+ cred_file.write(json.dumps(all_creds))
+ self.adal_token_cache.has_state_changed = False
- #reset the active subscription if needed
- result = [x for x in subscriptions if x['active']]
- if not result and subscriptions:
- subscriptions[0]['active'] = True
+ def retrieve_token_for_user(self, username, tenant):
+ authority = get_authority_url(tenant, ENV_DEFAULT)
+ context = self._auth_ctx_factory(authority, cache=self.adal_token_cache)
+ token_entry = context.acquire_token(self._resource, username, CLIENT_ID)
+ if not token_entry: #TODO: consider to letting adal-python throw
+ raise ValueError('Could not retrieve token from local cache, please run \'login\'.')
+
+ if self.adal_token_cache.has_state_changed:
+ self.persist_cached_creds()
+ return token_entry[_ACCESS_TOKEN]
+
+ def retrieve_token_for_service_principal(self, sp_id):
+ matched = [x for x in self._service_principal_creds if sp_id == x[_SERVICE_PRINCIPAL_ID]]
+ if not matched:
+ raise ValueError(L('Please run "account set" to select active account.'))
+ cred = matched[0]
+ authority_url = get_authority_url(cred[_SERVICE_PRINCIPAL_TENANT], ENV_DEFAULT)
+ context = self._auth_ctx_factory(authority_url, None)
+ token_entry = context.acquire_token_with_client_credentials(self._resource,
+ sp_id,
+ cred[_ACCESS_TOKEN])
+ return token_entry[_ACCESS_TOKEN]
+
+ def _load_creds(self):
+ if self.adal_token_cache is not None:
+ return self.adal_token_cache
+
+ json_text = _read_file_content(self._token_file)
+ if json_text:
+ json_text = json_text.replace('\n', '')
+ else:
+ json_text = '[]'
+
+ all_entries = json.loads(json_text)
+ self._load_service_principal_creds(all_entries)
+ real_token = [x for x in all_entries if x not in self._service_principal_creds]
+ self.adal_token_cache = adal.TokenCache(json.dumps(real_token))
+ return self.adal_token_cache
+
+ def save_service_principal_cred(self, client_id, secret, tenant):
+ entry = {
+ _SERVICE_PRINCIPAL_ID: client_id,
+ _SERVICE_PRINCIPAL_TENANT: tenant,
+ _ACCESS_TOKEN: secret
+ }
+
+ matched = [x for x in self._service_principal_creds
+ if client_id == x[_SERVICE_PRINCIPAL_ID] and
+ tenant == x[_SERVICE_PRINCIPAL_TENANT]]
+ state_changed = False
+ if matched:
+ if matched[0][_ACCESS_TOKEN] != secret:
+ matched[0] = entry
+ state_changed = True
+ else:
+ self._service_principal_creds.append(entry)
+ state_changed = True
+
+ if state_changed:
+ self.persist_cached_creds()
+
+ def _load_service_principal_creds(self, creds):
+ for c in creds:
+ if c.get(_SERVICE_PRINCIPAL_ID):
+ self._service_principal_creds.append(c)
+ return self._service_principal_creds
- self._save_subscriptions(subscriptions)
+ def remove_cached_creds(self, user_or_sp):
+ state_changed = False
+ #clear AAD tokens
+ tokens = self.adal_token_cache.find({_TOKEN_ENTRY_USER_ID: user_or_sp})
+ if tokens:
+ state_changed = True
+ self.adal_token_cache.remove(tokens)
- def load_subscriptions(self):
- return self._storage.get('subscriptions') or []
+ #clear service principal creds
+ matched = [x for x in self._service_principal_creds
+ if x[_SERVICE_PRINCIPAL_ID] == user_or_sp]
+ if matched:
+ state_changed = True
+ self._service_principal_creds = [x for x in self._service_principal_creds
+ if x not in matched]
- def _save_subscriptions(self, subscriptions):
- self._storage['subscriptions'] = subscriptions
+ if state_changed:
+ self.persist_cached_creds()
diff --git a/src/azure/cli/_session.py b/src/azure/cli/_session.py
index 39881574ef6..60c2d32f185 100644
--- a/src/azure/cli/_session.py
+++ b/src/azure/cli/_session.py
@@ -16,9 +16,10 @@ class Session(collections.MutableMapping):
be followed by a call to `save_with_retry` or `save`.
'''
- def __init__(self):
+ def __init__(self, encoding=None):
self.filename = None
self.data = {}
+ self._encoding = encoding if encoding else 'utf-8-sig'
def load(self, filename, max_age=0):
self.filename = filename
@@ -28,14 +29,14 @@ def load(self, filename, max_age=0):
st = os.stat(self.filename)
if st.st_mtime + max_age < time.clock():
self.save()
- with codecs_open(self.filename, 'r', encoding='utf-8-sig') as f:
+ with codecs_open(self.filename, 'r', encoding=self._encoding) as f:
self.data = json.load(f)
except (OSError, IOError):
self.save()
def save(self):
if self.filename:
- with codecs_open(self.filename, 'w', encoding='utf-8-sig') as f:
+ with codecs_open(self.filename, 'w', encoding=self._encoding) as f:
json.dump(self.data, f)
def save_with_retry(self, retries=5):
diff --git a/src/azure/cli/main.py b/src/azure/cli/main.py
index 5113d806d0c..78dc3169fc5 100644
--- a/src/azure/cli/main.py
+++ b/src/azure/cli/main.py
@@ -7,6 +7,10 @@
from ._session import Session
from ._output import OutputProducer
+#ACCOUNT contains subscriptions information
+# this file will be shared with azure-xplat-cli, which assumes ascii
+ACCOUNT = Session('ascii')
+
# CONFIG provides external configuration options
CONFIG = Session()
@@ -14,8 +18,12 @@
SESSION = Session()
def main(args, file=sys.stdout): #pylint: disable=redefined-builtin
- CONFIG.load(os.path.expanduser('~/az.json'))
- SESSION.load(os.path.expanduser('~/az.sess'), max_age=3600)
+ azure_folder = os.path.expanduser('~/.azure')
+ if not os.path.exists(azure_folder):
+ os.makedirs(azure_folder)
+ ACCOUNT.load(os.path.join(azure_folder, 'azureProfile.json'))
+ CONFIG.load(os.path.join(azure_folder, 'az.json'))
+ SESSION.load(os.path.join(azure_folder, 'az.sess'), max_age=3600)
configure_logging(args, CONFIG)
diff --git a/src/azure/cli/tests/test_profile.py b/src/azure/cli/tests/test_profile.py
index dd582130d80..2116e98ff3f 100644
--- a/src/azure/cli/tests/test_profile.py
+++ b/src/azure/cli/tests/test_profile.py
@@ -1,18 +1,37 @@
+import json
import unittest
-from azure.cli._profile import Profile
+import mock
+import adal
+from azure.cli._profile import Profile, CredsCache, SubscriptionFinder, _AUTH_CTX_FACTORY
+from azure.cli._azure_env import ENV_DEFAULT
class Test_Profile(unittest.TestCase):
@classmethod
def setUpClass(cls):
+ cls.tenant_id = 'microsoft.com'
cls.user1 = 'foo@foo.com'
cls.id1 = 'subscriptions/1'
cls.display_name1 = 'foo account'
cls.state1 = 'enabled'
cls.subscription1 = SubscriptionStub(cls.id1,
cls.display_name1,
- cls.state1)
- cls.token1 = 'token1'
+ cls.state1,
+ cls.tenant_id)
+ cls.raw_token1 = 'some...secrets'
+ cls.token_entry1 = {
+ "_clientId": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
+ "resource": "https://management.core.windows.net/",
+ "tokenType": "Bearer",
+ "expiresOn": "2016-03-31T04:26:56.610Z",
+ "expiresIn": 3599,
+ "identityProvider": "live.com",
+ "_authority": "https://login.microsoftonline.com/common",
+ "isMRRT": True,
+ "refreshToken": "faked123",
+ "accessToken": cls.raw_token1,
+ "userId": cls.user1
+ }
cls.user2 = 'bar@bar.com'
cls.id2 = 'subscriptions/2'
@@ -20,122 +39,262 @@ def setUpClass(cls):
cls.state2 = 'suspended'
cls.subscription2 = SubscriptionStub(cls.id2,
cls.display_name2,
- cls.state2)
- cls.token2 = 'token2'
+ cls.state2,
+ cls.tenant_id)
def test_normalize(self):
- consolidated = Profile.normalize_properties(self.user1,
- [self.subscription1])
- self.assertEqual(consolidated[0], {
+ consolidated = Profile._normalize_properties(self.user1,
+ [self.subscription1],
+ False,
+ ENV_DEFAULT)
+ expected = {
+ 'environmentName': 'AzureCloud',
'id': '1',
'name': self.display_name1,
'state': self.state1,
- 'user': self.user1,
- 'active': False
- })
+ 'user': {
+ 'name':self.user1,
+ 'type':'user'
+ },
+ 'isDefault': False,
+ 'tenantId': self.tenant_id
+ }
+ self.assertEqual(expected, consolidated[0])
def test_update_add_two_different_subscriptions(self):
storage_mock = {'subscriptions': None}
profile = Profile(storage_mock)
#add the first and verify
- consolidated = Profile.normalize_properties(self.user1,
- [self.subscription1])
- profile.set_subscriptions(consolidated, self.token1)
+ consolidated = Profile._normalize_properties(self.user1,
+ [self.subscription1],
+ False,
+ ENV_DEFAULT)
+ profile._set_subscriptions(consolidated)
self.assertEqual(len(storage_mock['subscriptions']), 1)
subscription1 = storage_mock['subscriptions'][0]
- self.assertEqual(subscription1, {
+ self.assertEqual(subscription1, {
+ 'environmentName': 'AzureCloud',
'id': '1',
'name': self.display_name1,
'state': self.state1,
- 'user': self.user1,
- 'access_token': self.token1,
- 'active': True
+ 'user': {
+ 'name': self.user1,
+ 'type': 'user'
+ },
+ 'isDefault': True,
+ 'tenantId': self.tenant_id
})
#add the second and verify
- consolidated = Profile.normalize_properties(self.user2,
- [self.subscription2])
- profile.set_subscriptions(consolidated, self.token2)
+ consolidated = Profile._normalize_properties(self.user2,
+ [self.subscription2],
+ False,
+ ENV_DEFAULT)
+ profile._set_subscriptions(consolidated)
self.assertEqual(len(storage_mock['subscriptions']), 2)
subscription2 = storage_mock['subscriptions'][1]
- self.assertEqual(subscription2, {
+ self.assertEqual(subscription2, {
+ 'environmentName': 'AzureCloud',
'id': '2',
'name': self.display_name2,
'state': self.state2,
- 'user': self.user2,
- 'access_token': self.token2,
- 'active': True
+ 'user': {
+ 'name': self.user2,
+ 'type': 'user'
+ },
+ 'isDefault': True,
+ 'tenantId': self.tenant_id
})
#verify the old one stays, but no longer active
self.assertEqual(storage_mock['subscriptions'][0]['name'],
subscription1['name'])
- self.assertEqual(storage_mock['subscriptions'][0]['access_token'],
- self.token1)
- self.assertFalse(storage_mock['subscriptions'][0]['active'])
+ self.assertFalse(storage_mock['subscriptions'][0]['isDefault'])
def test_update_with_same_subscription_added_twice(self):
storage_mock = {'subscriptions': None}
profile = Profile(storage_mock)
#add one twice and verify we will have one but with new token
- consolidated = Profile.normalize_properties(self.user1,
- [self.subscription1])
- profile.set_subscriptions(consolidated, self.token1)
+ consolidated = Profile._normalize_properties(self.user1,
+ [self.subscription1],
+ False,
+ ENV_DEFAULT)
+ profile._set_subscriptions(consolidated)
new_subscription1 = SubscriptionStub(self.id1,
self.display_name1,
- self.state1)
- consolidated = Profile.normalize_properties(self.user1,
- [new_subscription1])
- profile.set_subscriptions(consolidated, self.token2)
+ self.state1,
+ self.tenant_id)
+ consolidated = Profile._normalize_properties(self.user1,
+ [new_subscription1],
+ False,
+ ENV_DEFAULT)
+ profile._set_subscriptions(consolidated)
self.assertEqual(len(storage_mock['subscriptions']), 1)
- self.assertEqual(storage_mock['subscriptions'][0]['access_token'],
- self.token2)
- self.assertTrue(storage_mock['subscriptions'][0]['active'])
+ self.assertTrue(storage_mock['subscriptions'][0]['isDefault'])
def test_set_active_subscription(self):
storage_mock = {'subscriptions': None}
profile = Profile(storage_mock)
- consolidated = Profile.normalize_properties(self.user1,
- [self.subscription1])
- profile.set_subscriptions(consolidated, self.token1)
+ consolidated = Profile._normalize_properties(self.user1,
+ [self.subscription1],
+ False,
+ ENV_DEFAULT)
+ profile._set_subscriptions(consolidated)
- consolidated = Profile.normalize_properties(self.user2,
- [self.subscription2])
- profile.set_subscriptions(consolidated, self.token2)
+ consolidated = profile._normalize_properties(self.user2,
+ [self.subscription2],
+ False,
+ ENV_DEFAULT)
+ profile._set_subscriptions(consolidated)
subscription1 = storage_mock['subscriptions'][0]
subscription2 = storage_mock['subscriptions'][1]
- self.assertTrue(subscription2['active'])
+ self.assertTrue(subscription2['isDefault'])
profile.set_active_subscription(subscription1['id'])
- self.assertFalse(subscription2['active'])
- self.assertTrue(subscription1['active'])
+ self.assertFalse(subscription2['isDefault'])
+ self.assertTrue(subscription1['isDefault'])
+
+ @mock.patch('azure.cli._profile._read_file_content', return_value=None)
+ def test_create_token_cache(self, mock_read_file):
+ profile = Profile()
+ cache = profile._creds_cache.adal_token_cache
+ self.assertFalse(cache.read_items())
+ self.assertTrue(mock_read_file.called)
- def test_get_login_credentials(self):
+ @mock.patch('azure.cli._profile._read_file_content', autospec=True)
+ def test_load_cached_tokens(self, mock_read_file):
+ mock_read_file.return_value = json.dumps([Test_Profile.token_entry1])
+ profile = Profile()
+ cache = profile._creds_cache.adal_token_cache
+ matched = cache.find({
+ "_authority": "https://login.microsoftonline.com/common",
+ "_clientId": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
+ "userId": self.user1
+ })
+ self.assertEqual(len(matched), 1)
+ self.assertEqual(matched[0]['accessToken'], self.raw_token1)
+
+ @mock.patch('azure.cli._profile._read_file_content', autospec=True)
+ @mock.patch('azure.cli._profile.CredsCache.retrieve_token_for_user', autospec=True)
+ def test_get_login_credentials(self, mock_get_token, mock_read_cred_file):
+ mock_read_cred_file.return_value = json.dumps([Test_Profile.token_entry1])
+ mock_get_token.return_value = Test_Profile.raw_token1
+ #setup
storage_mock = {'subscriptions': None}
profile = Profile(storage_mock)
-
- consolidated = Profile.normalize_properties(self.user1,
- [self.subscription1])
- profile.set_subscriptions(consolidated, self.token1)
+ consolidated = Profile._normalize_properties(self.user1,
+ [self.subscription1],
+ False,
+ ENV_DEFAULT)
+ profile._set_subscriptions(consolidated)
+ #action
cred, subscription_id = profile.get_login_credentials()
- self.assertEqual(cred.token['access_token'], self.token1)
+ #verify
self.assertEqual(subscription_id, '1')
+ self.assertEqual(cred.token['access_token'], self.raw_token1)
+ self.assertEqual(mock_read_cred_file.call_count, 1)
+ self.assertEqual(mock_get_token.call_count, 1)
+
+ @mock.patch('azure.cli._profile._read_file_content', autospec=True)
+ @mock.patch('azure.cli._profile.CredsCache.persist_cached_creds', autospec=True)
+ def test_logout(self, mock_persist_creds, mock_read_cred_file):
+ #setup
+ mock_read_cred_file.return_value = json.dumps([Test_Profile.token_entry1])
+
+ storage_mock = {'subscriptions': None}
+ profile = Profile(storage_mock)
+ consolidated = Profile._normalize_properties(self.user1,
+ [self.subscription1],
+ False,
+ ENV_DEFAULT)
+ profile._set_subscriptions(consolidated)
+ self.assertEqual(1, len(storage_mock['subscriptions']))
+ #action
+ profile.logout(self.user1)
+
+ #verify
+ self.assertEqual(0, len(storage_mock['subscriptions']))
+ self.assertEqual(mock_read_cred_file.call_count, 1)
+ self.assertEqual(mock_persist_creds.call_count, 1)
+
+ def test_find_subscriptions_thru_username_password(self):
+ finder = SubscriptionFinder(lambda _,_2:AuthenticationContextStub(Test_Profile),
+ None,
+ lambda _: ArmClientStub(Test_Profile))
+ subs = finder.find_from_user_account('foo', 'bar')
+ self.assertEqual([self.subscription1], subs)
+
+ def test_find_through_interactive_flow(self):
+ finder = SubscriptionFinder(lambda _,_2:AuthenticationContextStub(Test_Profile),
+ None,
+ lambda _: ArmClientStub(Test_Profile))
+ subs = finder.find_through_interactive_flow()
+ self.assertEqual([self.subscription1], subs)
+ def test_find_from_service_principal_id(self):
+ finder = SubscriptionFinder(lambda _,_2:AuthenticationContextStub(Test_Profile),
+ None,
+ lambda _: ArmClientStub(Test_Profile))
+ subs = finder.find_from_service_principal_id('my app', 'my secret', self.tenant_id)
+ self.assertEqual([self.subscription1], subs)
class SubscriptionStub:
- def __init__(self, id, display_name, state):
+ def __init__(self, id, display_name, state, tenant_id):
self.id = id
self.display_name = display_name
self.state = state
+ self.tenant_id = tenant_id
+
+class AuthenticationContextStub:
+ def __init__(self, test_profile_cls, return_token1=True):
+ #we need to reference some pre-defined test artifacts in Test_Profile
+ self._test_profile_cls = test_profile_cls
+ if not return_token1:
+ raise ValueError('Please update to return other test tokens')
+
+ def acquire_token_with_username_password(self, _, _2, _3, _4):
+ return self._test_profile_cls.token_entry1
+
+ def acquire_token_with_device_code(self, _, _2, _3):
+ return self._test_profile_cls.token_entry1
+
+ def acquire_token_with_client_credentials(self, _, _2, _3):
+ return self._test_profile_cls.token_entry1
+
+ def acquire_token(self, _, _2, _3):
+ return self._test_profile_cls.token_entry1
+
+ def acquire_user_code(self, _, _2):
+ return {'message': 'secret code for you'}
+
+class ArmClientStub:
+ class TenantStub:
+ def __init__(self, tenant_id):
+ self.tenant_id = tenant_id
+
+ class OperationsStub:
+ def __init__(self, list_result):
+ self._list_result = list_result
+
+ def list(self):
+ return self._list_result
+
+ def __init__(self, test_profile_cls, use_tenant1_and_subscription1=True):
+ self._test_profile_cls = test_profile_cls
+ if use_tenant1_and_subscription1:
+ self.tenants = ArmClientStub.OperationsStub([ArmClientStub.TenantStub(test_profile_cls.tenant_id)])
+ self.subscriptions = ArmClientStub.OperationsStub([test_profile_cls.subscription1])
+ else:
+ raise ValueError('Please update to return other test subscriptions')
if __name__ == '__main__':
unittest.main()
diff --git a/src/azure/cli/utils/command_test_util.py b/src/azure/cli/utils/command_test_util.py
index 1d81dd0f290..19fbe5577f1 100644
--- a/src/azure/cli/utils/command_test_util.py
+++ b/src/azure/cli/utils/command_test_util.py
@@ -41,14 +41,24 @@ def generate_tests(self):
def gen_test(test_name, command, expected_result):
def load_subscriptions_mock(self): #pylint: disable=unused-argument
- return [{"id": "00000000-0000-0000-0000-000000000000",
- "user": "example@example.com",
- "access_token": "access_token",
- "state": "Enabled",
- "name": "Example",
- "active": True}]
+ return [{
+ "id": "00000000-0000-0000-0000-000000000000",
+ "user": {
+ "name": "example@example.com",
+ "type": "user"
+ },
+ "state": "Enabled",
+ "name": "Example",
+ "tenantId": "123",
+ "isDefault": True}]
- @mock.patch('azure.cli._profile.Profile.load_subscriptions', load_subscriptions_mock)
+ def get_user_access_token_mock(_, _1, _2): #pylint: disable=unused-argument
+ return 'top-secret-token-for-you'
+
+ @mock.patch('azure.cli._profile.Profile.load_cached_subscriptions',
+ load_subscriptions_mock)
+ @mock.patch('azure.cli._profile.CredsCache.retrieve_token_for_user',
+ get_user_access_token_mock)
@self.my_vcr.use_cassette(test_name + '.yaml',
filter_headers=CommandTestGenerator.FILTER_HEADERS)
def test(self):
diff --git a/src/command_modules/azure-cli-component/README.rst b/src/command_modules/azure-cli-component/README.rst
new file mode 100644
index 00000000000..19b0715755a
--- /dev/null
+++ b/src/command_modules/azure-cli-component/README.rst
@@ -0,0 +1,7 @@
+Microsoft Azure CLI 'component' Command Module
+==================================
+
+This package is for the 'component' module.
+i.e. 'az component'
+
+This package has [not] been tested [much] with Python 2.7, 3.4 and 3.5.
diff --git a/src/command_modules/azure-cli-components/azure/__init__.py b/src/command_modules/azure-cli-component/azure/__init__.py
similarity index 100%
rename from src/command_modules/azure-cli-components/azure/__init__.py
rename to src/command_modules/azure-cli-component/azure/__init__.py
diff --git a/src/command_modules/azure-cli-components/azure/cli/__init__.py b/src/command_modules/azure-cli-component/azure/cli/__init__.py
similarity index 100%
rename from src/command_modules/azure-cli-components/azure/cli/__init__.py
rename to src/command_modules/azure-cli-component/azure/cli/__init__.py
diff --git a/src/command_modules/azure-cli-components/azure/cli/command_modules/__init__.py b/src/command_modules/azure-cli-component/azure/cli/command_modules/__init__.py
similarity index 100%
rename from src/command_modules/azure-cli-components/azure/cli/command_modules/__init__.py
rename to src/command_modules/azure-cli-component/azure/cli/command_modules/__init__.py
diff --git a/src/command_modules/azure-cli-components/azure/cli/command_modules/components/__init__.py b/src/command_modules/azure-cli-component/azure/cli/command_modules/component/__init__.py
similarity index 94%
rename from src/command_modules/azure-cli-components/azure/cli/command_modules/components/__init__.py
rename to src/command_modules/azure-cli-component/azure/cli/command_modules/component/__init__.py
index cc776f3bc62..40076054a91 100644
--- a/src/command_modules/azure-cli-components/azure/cli/command_modules/components/__init__.py
+++ b/src/command_modules/azure-cli-component/azure/cli/command_modules/component/__init__.py
@@ -20,16 +20,15 @@
@command_table.command('component list')
@command_table.description(L('List the installed components.'))
def list_components(args): #pylint: disable=unused-argument
- components = sorted(["%s (%s)" % (dist.key.replace(COMPONENT_PREFIX, ''), dist.version)
- for dist in pip.get_installed_distributions(local_only=True)
- if dist.key.startswith(COMPONENT_PREFIX)])
- print('\n'.join(components))
+ return sorted([{'name': dist.key.replace(COMPONENT_PREFIX, ''), 'version': dist.version}
+ for dist in pip.get_installed_distributions(local_only=True)
+ if dist.key.startswith(COMPONENT_PREFIX)], key=lambda x: x['name'])
def _install_or_update(component_name, version, link, private, upgrade=False):
if not component_name:
raise IncorrectUsageError(L('Specify a component name.'))
found = bool([dist for dist in pip.get_installed_distributions(local_only=True)
- if dist.key == COMPONENT_PREFIX+component_name])
+ if dist.key == COMPONENT_PREFIX + component_name])
if found and not upgrade:
raise RuntimeError("Component already installed.")
else:
diff --git a/src/command_modules/azure-cli-components/requirements.txt b/src/command_modules/azure-cli-component/requirements.txt
similarity index 100%
rename from src/command_modules/azure-cli-components/requirements.txt
rename to src/command_modules/azure-cli-component/requirements.txt
diff --git a/src/command_modules/azure-cli-components/setup.py b/src/command_modules/azure-cli-component/setup.py
similarity index 96%
rename from src/command_modules/azure-cli-components/setup.py
rename to src/command_modules/azure-cli-component/setup.py
index d70ee6a69a2..5a626f600e0 100644
--- a/src/command_modules/azure-cli-components/setup.py
+++ b/src/command_modules/azure-cli-component/setup.py
@@ -45,7 +45,7 @@
README = f.read()
setup(
- name='azure-cli-components',
+ name='azure-cli-component',
version=VERSION,
description='Microsoft Azure Command-Line Tools',
long_description=README,
@@ -56,7 +56,7 @@
classifiers=CLASSIFIERS,
namespace_packages = ['azure.cli.command_modules'],
packages=[
- 'azure.cli.command_modules.components',
+ 'azure.cli.command_modules.component',
],
install_requires=DEPENDENCIES,
)
diff --git a/src/command_modules/azure-cli-components/README.rst b/src/command_modules/azure-cli-components/README.rst
deleted file mode 100644
index 281a5b7c8b8..00000000000
--- a/src/command_modules/azure-cli-components/README.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-Microsoft Azure CLI 'components' Command Module
-==================================
-
-This package is for the 'components' module.
-i.e. 'az components'
-
-This package has [not] been tested [much] with Python 2.7, 3.4 and 3.5.
diff --git a/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/account.py b/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/account.py
index 982aff888d7..a0821d2e626 100644
--- a/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/account.py
+++ b/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/account.py
@@ -8,7 +8,7 @@
COMMAND_TABLES.append(command_table)
@command_table.command('account list', description=L('List the imported subscriptions.'))
-def list_subscriptions(args): #pylint: disable=unused-argument
+def list_subscriptions(_):
"""
type: command
long-summary: |
@@ -20,7 +20,7 @@ def list_subscriptions(args): #pylint: disable=unused-argument
text: example details
"""
profile = Profile()
- subscriptions = profile.load_subscriptions()
+ subscriptions = profile.load_cached_subscriptions()
return subscriptions
diff --git a/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/login.py b/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/login.py
index a970b98483d..56f0b479c0f 100644
--- a/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/login.py
+++ b/src/command_modules/azure-cli-profile/azure/cli/command_modules/profile/login.py
@@ -1,13 +1,3 @@
-from __future__ import print_function
-
-from msrest.authentication import BasicTokenAuthentication
-from adal import (acquire_token_with_username_password,
- acquire_token_with_client_credentials,
- acquire_user_code,
- acquire_token_with_device_code)
-from azure.mgmt.resource.subscriptions import (SubscriptionClient,
- SubscriptionClientConfiguration)
-
from azure.cli._profile import Profile
from azure.cli.commands import CommandTable
from azure.cli._locale import L
@@ -27,12 +17,14 @@
@command_table.option('--password -p',
help=L('user password or client secret, will prompt if not given.'))
@command_table.option('--service-principal',
+ action='store_true',
help=L('the credential represents a service principal.'))
@command_table.option('--tenant -t', help=L('the tenant associated with the service principal.'))
def login(args):
interactive = False
username = args.get('username')
+ password = None
if username:
password = args.get('password')
if not password:
@@ -41,41 +33,14 @@ def login(args):
else:
interactive = True
+ is_service_principal = args.get('service-principal')
tenant = args.get('tenant')
- authority = _get_authority_url(tenant)
- if interactive:
- user_code = acquire_user_code(authority)
- print(user_code['message'])
- credentials = acquire_token_with_device_code(authority, user_code)
- username = credentials['userId']
- else:
- if args.get('service-principal'):
- if not tenant:
- raise ValueError(L('Please supply tenant using "--tenant"'))
-
- credentials = acquire_token_with_client_credentials(
- authority,
- username,
- password)
- else:
- credentials = acquire_token_with_username_password(
- authority,
- username,
- password)
- token_credential = BasicTokenAuthentication({'access_token': credentials['accessToken']})
- client = SubscriptionClient(SubscriptionClientConfiguration(token_credential))
- subscriptions = client.subscriptions.list()
-
- if not subscriptions:
- raise RuntimeError(L('No subscriptions found for this account.'))
-
- #keep useful properties and not json serializable
profile = Profile()
- consolidated = Profile.normalize_properties(username, subscriptions)
- profile.set_subscriptions(consolidated, credentials['accessToken'])
-
+ subscriptions = profile.find_subscriptions_on_login(
+ interactive,
+ username,
+ password,
+ is_service_principal,
+ tenant)
return list(subscriptions)
-
-def _get_authority_url(tenant=None):
- return 'https://login.microsoftonline.com/{}'.format(tenant or 'common')
diff --git a/src/command_modules/azure-cli-profile/setup.py b/src/command_modules/azure-cli-profile/setup.py
index 5f5a164d88a..100c2c14203 100644
--- a/src/command_modules/azure-cli-profile/setup.py
+++ b/src/command_modules/azure-cli-profile/setup.py
@@ -39,7 +39,7 @@
DEPENDENCIES = [
'azure-cli',
'azure==2.0.0rc1',
- 'adal==0.2.0' #from internal index server.
+ 'adal==0.2.1', #from internal index server.
]
with open('README.rst', 'r', encoding='utf-8') as f: