зеркало из https://github.com/microsoft/azure-cli.git
Merge branch 'master' of https://github.com/tjprescott/azure-cli into StorageAutoCommands
# Conflicts: # azure-cli.pyproj # src/azure/cli/tests/test_commands.py
This commit is contained in:
Коммит
3f3627b9fd
|
@ -55,6 +55,7 @@
|
|||
<Compile Include="azure\cli\tests\test_connection_verify.py" />
|
||||
<Compile Include="azure\cli\tests\test_application.py" />
|
||||
<Compile Include="azure\cli\tests\test_output.py" />
|
||||
<Compile Include="azure\cli\_azure_env.py" />
|
||||
<Compile Include="azure\cli\_debug.py" />
|
||||
<Compile Include="azure\cli\tests\test_help.py" />
|
||||
<Compile Include="azure\cli\tests\test_output.py" />
|
||||
|
|
|
@ -8,5 +8,5 @@ pyyaml==3.11
|
|||
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
|
||||
|
|
2
setup.py
2
setup.py
|
@ -55,7 +55,7 @@ CLASSIFIERS = [
|
|||
]
|
||||
|
||||
DEPENDENCIES = [
|
||||
'adal==0.2.0', #from internal index server.
|
||||
'adal==0.2.1', #from internal index server.
|
||||
'applicationinsights',
|
||||
'argcomplete',
|
||||
'azure==2.0.0rc1',
|
||||
|
|
|
@ -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]
|
||||
|
|
@ -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 __init__(self, storage=CONFIG):
|
||||
self._storage = storage
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
#before adal/python is available, persist tokens with other profile info
|
||||
for s in new_subscriptions:
|
||||
s['access_token'] = access_token
|
||||
|
||||
self._save_subscriptions(subscriptions)
|
||||
|
||||
def get_login_credentials(self):
|
||||
subscriptions = self.load_subscriptions()
|
||||
if not subscriptions:
|
||||
raise ValueError('Please run login to setup account.')
|
||||
|
||||
active = [x for x in subscriptions if x['active']]
|
||||
if len(active) != 1:
|
||||
raise ValueError('Please run "account set" to select active account.')
|
||||
|
||||
return BasicTokenAuthentication(
|
||||
{'access_token': active[0]['access_token']}), active[0]['id']
|
||||
self._cache_subscriptions_to_local_storage(subscriptions)
|
||||
|
||||
def set_active_subscription(self, subscription_id_or_name):
|
||||
subscriptions = self.load_subscriptions()
|
||||
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['id'].lower() or
|
||||
subscription_id_or_name == x['name'].lower()]
|
||||
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['active'] = False
|
||||
result[0]['active'] = True
|
||||
s[_IS_DEFAULT_SUBSCRIPTION] = False
|
||||
result[0][_IS_DEFAULT_SUBSCRIPTION] = True
|
||||
|
||||
self._save_subscriptions(subscriptions)
|
||||
self._cache_subscriptions_to_local_storage(subscriptions)
|
||||
|
||||
def logout(self, user):
|
||||
subscriptions = self.load_subscriptions()
|
||||
result = [x for x in subscriptions if user.lower() == x['user'].lower()]
|
||||
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['active']]
|
||||
result = [x for x in subscriptions if x.get(_IS_DEFAULT_SUBSCRIPTION)]
|
||||
if not result and subscriptions:
|
||||
subscriptions[0]['active'] = True
|
||||
subscriptions[0][_IS_DEFAULT_SUBSCRIPTION] = True
|
||||
|
||||
self._save_subscriptions(subscriptions)
|
||||
self._cache_subscriptions_to_local_storage(subscriptions)
|
||||
|
||||
def load_subscriptions(self):
|
||||
return self._storage.get('subscriptions') or []
|
||||
self._creds_cache.remove_cached_creds(user_or_sp)
|
||||
|
||||
def _save_subscriptions(self, subscriptions):
|
||||
self._storage['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_cached_subscriptions()
|
||||
if not subscriptions:
|
||||
raise ValueError('Please run login to setup account.')
|
||||
|
||||
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': access_token}), active_account[_SUBSCRIPTION_ID]
|
||||
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
setattr(s, 'tenant_id', tenant)
|
||||
all_subscriptions.append(s)
|
||||
return all_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 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
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
#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]
|
||||
|
||||
if state_changed:
|
||||
self.persist_cached_creds()
|
||||
|
|
|
@ -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 @@ class Session(collections.MutableMapping):
|
|||
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):
|
||||
|
|
|
@ -5,7 +5,7 @@ from .._locale import L
|
|||
command_table = CommandTable()
|
||||
|
||||
@command_table.command('account list', description=L('List the imported subscriptions.'))
|
||||
def list_subscriptions(args, unexpected): #pylint: disable=unused-argument
|
||||
def list_subscriptions(_):
|
||||
"""
|
||||
type: command
|
||||
long-summary: |
|
||||
|
@ -17,7 +17,7 @@ def list_subscriptions(args, unexpected): #pylint: disable=unused-argument
|
|||
text: example details
|
||||
"""
|
||||
profile = Profile()
|
||||
subscriptions = profile.load_subscriptions()
|
||||
subscriptions = profile.load_cached_subscriptions()
|
||||
|
||||
return subscriptions
|
||||
|
||||
|
|
|
@ -1,12 +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 .._profile import Profile
|
||||
from ..commands import CommandTable
|
||||
#TODO: update adal-python to support it
|
||||
|
@ -22,12 +13,14 @@ command_table = CommandTable()
|
|||
@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:
|
||||
|
@ -36,41 +29,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')
|
||||
|
|
|
@ -7,6 +7,10 @@ from ._logging import configure_logging, logger
|
|||
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 @@ CONFIG = Session()
|
|||
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)
|
||||
|
||||
|
|
|
@ -5,15 +5,25 @@ interactions:
|
|||
Accept-Encoding: [identity]
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Azure-Storage/0.30.0 (Python CPython 3.5.1; Windows 10)]
|
||||
x-ms-date: ['Tue, 05 Apr 2016 23:25:43 GMT']
|
||||
x-ms-date: ['Wed, 06 Apr 2016 17:23:28 GMT']
|
||||
x-ms-version: ['2015-04-05']
|
||||
method: HEAD
|
||||
uri: https://travistestresourcegr3014.blob.core.windows.net/testcontainer1234/testblob1
|
||||
response:
|
||||
body: {string: ''}
|
||||
headers:
|
||||
Date: ['Tue, 05 Apr 2016 23:25:43 GMT']
|
||||
Accept-Ranges: [bytes]
|
||||
Content-Length: ['1328']
|
||||
Content-MD5: [OsIq1b99LRzuhyybmkSywA==]
|
||||
Content-Type: [application/octet-stream]
|
||||
Date: ['Wed, 06 Apr 2016 17:23:28 GMT']
|
||||
ETag: ['"0x8D35DAB16B93ECE"']
|
||||
Last-Modified: ['Tue, 05 Apr 2016 23:36:19 GMT']
|
||||
Server: [Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0]
|
||||
x-ms-blob-type: [BlockBlob]
|
||||
x-ms-lease-state: [available]
|
||||
x-ms-lease-status: [unlocked]
|
||||
x-ms-version: ['2015-04-05']
|
||||
status: {code: 404, message: The specified blob does not exist.}
|
||||
x-ms-write-protection: ['false']
|
||||
status: {code: 200, message: OK}
|
||||
version: 1
|
||||
|
|
|
@ -5,21 +5,24 @@ interactions:
|
|||
Accept-Encoding: [identity]
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Azure-Storage/0.30.0 (Python CPython 3.5.1; Windows 10)]
|
||||
x-ms-date: ['Tue, 05 Apr 2016 23:25:57 GMT']
|
||||
x-ms-date: ['Wed, 06 Apr 2016 17:24:56 GMT']
|
||||
x-ms-version: ['2015-04-05']
|
||||
method: GET
|
||||
uri: https://travistestresourcegr3014.blob.core.windows.net/testcontainer1234?restype=container&comp=list
|
||||
response:
|
||||
body: {string: "\uFEFF<?xml version=\"1.0\" encoding=\"utf-8\"?><EnumerationResults\
|
||||
\ ServiceEndpoint=\"https://travistestresourcegr3014.blob.core.windows.net/\"\
|
||||
\ ContainerName=\"testcontainer1234\"><Blobs><Blob><Name>testblobl</Name><Properties><Last-Modified>Thu,\
|
||||
\ ContainerName=\"testcontainer1234\"><Blobs><Blob><Name>testblob1</Name><Properties><Last-Modified>Tue,\
|
||||
\ 05 Apr 2016 23:36:19 GMT</Last-Modified><Etag>0x8D35DAB16B93ECE</Etag><Content-Length>1328</Content-Length><Content-Type>application/octet-stream</Content-Type><Content-Encoding\
|
||||
\ /><Content-Language /><Content-MD5>OsIq1b99LRzuhyybmkSywA==</Content-MD5><Cache-Control\
|
||||
\ /><Content-Disposition /><BlobType>BlockBlob</BlobType><LeaseStatus>unlocked</LeaseStatus><LeaseState>available</LeaseState></Properties></Blob><Blob><Name>testblobl</Name><Properties><Last-Modified>Thu,\
|
||||
\ 24 Mar 2016 16:04:34 GMT</Last-Modified><Etag>0x8D353FDFE4824F8</Etag><Content-Length>89344384</Content-Length><Content-Type>application/octet-stream</Content-Type><Content-Encoding\
|
||||
\ /><Content-Language /><Content-MD5 /><Cache-Control /><Content-Disposition\
|
||||
\ /><BlobType>BlockBlob</BlobType><LeaseStatus>unlocked</LeaseStatus><LeaseState>available</LeaseState></Properties></Blob></Blobs><NextMarker\
|
||||
\ /></EnumerationResults>"}
|
||||
headers:
|
||||
Content-Type: [application/xml]
|
||||
Date: ['Tue, 05 Apr 2016 23:25:57 GMT']
|
||||
Date: ['Wed, 06 Apr 2016 17:24:55 GMT']
|
||||
Server: [Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0]
|
||||
x-ms-version: ['2015-04-05']
|
||||
status: {code: 200, message: OK}
|
||||
|
|
|
@ -5,7 +5,7 @@ interactions:
|
|||
Accept-Encoding: [identity]
|
||||
Connection: [keep-alive]
|
||||
User-Agent: [Azure-Storage/0.30.0 (Python CPython 3.5.1; Windows 10)]
|
||||
x-ms-date: ['Tue, 05 Apr 2016 23:27:52 GMT']
|
||||
x-ms-date: ['Wed, 06 Apr 2016 17:23:37 GMT']
|
||||
x-ms-version: ['2015-04-05']
|
||||
method: HEAD
|
||||
uri: https://travistestresourcegr3014.blob.core.windows.net/testcontainer1234/testblob1
|
||||
|
@ -16,13 +16,14 @@ interactions:
|
|||
Content-Length: ['1328']
|
||||
Content-MD5: [OsIq1b99LRzuhyybmkSywA==]
|
||||
Content-Type: [application/octet-stream]
|
||||
Date: ['Tue, 05 Apr 2016 23:27:53 GMT']
|
||||
ETag: ['"0x8D35DA9E623241D"']
|
||||
Last-Modified: ['Tue, 05 Apr 2016 23:27:48 GMT']
|
||||
Date: ['Wed, 06 Apr 2016 17:23:37 GMT']
|
||||
ETag: ['"0x8D35DAB16B93ECE"']
|
||||
Last-Modified: ['Tue, 05 Apr 2016 23:36:19 GMT']
|
||||
Server: [Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0]
|
||||
x-ms-blob-type: [BlockBlob]
|
||||
x-ms-lease-state: [available]
|
||||
x-ms-lease-status: [unlocked]
|
||||
x-ms-version: ['2015-04-05']
|
||||
x-ms-write-protection: ['false']
|
||||
status: {code: 200, message: OK}
|
||||
version: 1
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
"command_specs.test_spec_storage.storage_account_usage": "Current Value : 19\nLimit : 100\nUnit : Count\nName :\n Localized Value : Storage Accounts\n Value : StorageAccounts\n\n\n",
|
||||
"command_specs.test_spec_storage.storage_blob_delete": "",
|
||||
"command_specs.test_spec_storage.storage_blob_download": "",
|
||||
"command_specs.test_spec_storage.storage_blob_exists": "False\n\n\n",
|
||||
"command_specs.test_spec_storage.storage_blob_list": "Next Marker : \nItems :\n Content : None\n Metadata : None\n Name : testblobl\n Snapshot : None\n Properties :\n Append Blob Committed Block Count : None\n Blob Type : BlockBlob\n Content Length : 89344384\n Etag : 0x8D353FDFE4824F8\n Last Modified : 2016-03-24T16:04:34+00:00\n Page Blob Sequence Number : None\n Content Settings :\n Cache Control : None\n Content Disposition : None\n Content Encoding : None\n Content Language : None\n Content Md5 : None\n Content Type : application/octet-stream\n Copy :\n Completion Time : None\n Id : None\n Progress : None\n Source : None\n Status : None\n Status Description : None\n Lease :\n Duration : None\n State : available\n Status : unlocked\n\n\n",
|
||||
"command_specs.test_spec_storage.storage_blob_show": "Content : None\nName : testblob1\nSnapshot : None\nMetadata :\n None\nProperties :\n Append Blob Committed Block Count : None\n Blob Type : BlockBlob\n Content Length : 1328\n Etag : \"0x8D35DA9E623241D\"\n Last Modified : 2016-04-05T23:27:48+00:00\n Page Blob Sequence Number : None\n Content Settings :\n Cache Control : None\n Content Disposition : None\n Content Encoding : None\n Content Language : None\n Content Md5 : OsIq1b99LRzuhyybmkSywA==\n Content Type : application/octet-stream\n Copy :\n Completion Time : None\n Id : None\n Progress : None\n Source : None\n Status : None\n Status Description : None\n Lease :\n Duration : None\n State : available\n Status : unlocked\n\n\n",
|
||||
"command_specs.test_spec_storage.storage_blob_exists": "True\n\n\n",
|
||||
"command_specs.test_spec_storage.storage_blob_list": "Next Marker : \nItems :\n Content : None\n Metadata : None\n Name : testblob1\n Snapshot : None\n Properties :\n Append Blob Committed Block Count : None\n Blob Type : BlockBlob\n Content Length : 1328\n Etag : 0x8D35DAB16B93ECE\n Last Modified : 2016-04-05T23:36:19+00:00\n Page Blob Sequence Number : None\n Content Settings :\n Cache Control : None\n Content Disposition : None\n Content Encoding : None\n Content Language : None\n Content Md5 : OsIq1b99LRzuhyybmkSywA==\n Content Type : application/octet-stream\n Copy :\n Completion Time : None\n Id : None\n Progress : None\n Source : None\n Status : None\n Status Description : None\n Lease :\n Duration : None\n State : available\n Status : unlocked\n Content : None\n Metadata : None\n Name : testblobl\n Snapshot : None\n Properties :\n Append Blob Committed Block Count : None\n Blob Type : BlockBlob\n Content Length : 89344384\n Etag : 0x8D353FDFE4824F8\n Last Modified : 2016-03-24T16:04:34+00:00\n Page Blob Sequence Number : None\n Content Settings :\n Cache Control : None\n Content Disposition : None\n Content Encoding : None\n Content Language : None\n Content Md5 : None\n Content Type : application/octet-stream\n Copy :\n Completion Time : None\n Id : None\n Progress : None\n Source : None\n Status : None\n Status Description : None\n Lease :\n Duration : None\n State : available\n Status : unlocked\n\n\n",
|
||||
"command_specs.test_spec_storage.storage_blob_show": "Content : None\nName : testblob1\nSnapshot : None\nMetadata :\n None\nProperties :\n Append Blob Committed Block Count : None\n Blob Type : BlockBlob\n Content Length : 1328\n Etag : \"0x8D35DAB16B93ECE\"\n Last Modified : 2016-04-05T23:36:19+00:00\n Page Blob Sequence Number : None\n Content Settings :\n Cache Control : None\n Content Disposition : None\n Content Encoding : None\n Content Language : None\n Content Md5 : OsIq1b99LRzuhyybmkSywA==\n Content Type : application/octet-stream\n Copy :\n Completion Time : None\n Id : None\n Progress : None\n Source : None\n Status : None\n Status Description : None\n Lease :\n Duration : None\n State : available\n Status : unlocked\n\n\n",
|
||||
"command_specs.test_spec_storage.storage_blob_upload_block_blob": "",
|
||||
"command_specs.test_spec_storage.storage_container_create": "",
|
||||
"command_specs.test_spec_storage.storage_container_delete": "",
|
||||
|
|
|
@ -59,11 +59,23 @@ my_vcr = vcr.VCR(
|
|||
class TestSequenceMeta(type):
|
||||
|
||||
def __new__(mcs, name, bases, dict):
|
||||
|
||||
|
||||
def gen_test(test_name, command, expected_result):
|
||||
|
||||
|
||||
def load_subscriptions_mock(self):
|
||||
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}]
|
||||
|
||||
def get_user_access_token_mock(_, _1, _2):
|
||||
return 'top-secret-token-for-you'
|
||||
|
||||
def _test_impl(self, expected_result):
|
||||
""" Test implementation, augmented with prompted recording of expected result
|
||||
|
@ -105,7 +117,8 @@ class TestSequenceMeta(type):
|
|||
|
||||
if cassette_found and expected_result != None:
|
||||
# playback mode - can be fully automated
|
||||
@mock.patch('azure.cli._profile.Profile.load_subscriptions', load_subscriptions_mock)
|
||||
@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)
|
||||
@my_vcr.use_cassette(cassette_path, filter_headers=FILTER_HEADERS)
|
||||
def test(self):
|
||||
_test_impl(self, expected_result)
|
||||
|
@ -122,12 +135,12 @@ class TestSequenceMeta(type):
|
|||
@my_vcr.use_cassette(cassette_path, filter_headers=FILTER_HEADERS)
|
||||
def test(self):
|
||||
_test_impl(self, expected_result)
|
||||
return test
|
||||
else:
|
||||
# yaml file failed to delete or bug exists
|
||||
raise RuntimeError('Unable to generate test for {} due to inconsistent data. ' \
|
||||
+ 'Please manually remove the associated .yaml cassette and/or the test\'s ' \
|
||||
+ 'entry in expected_results.res and try again.')
|
||||
return test
|
||||
|
||||
try:
|
||||
with open(EXPECTED_RESULTS_PATH, 'r') as file:
|
||||
|
@ -139,7 +152,7 @@ class TestSequenceMeta(type):
|
|||
test_name = 'test_{}'.format(test_def['test_name'])
|
||||
command = test_def['command']
|
||||
expected_result = TEST_EXPECTED.get(test_path, None)
|
||||
|
||||
|
||||
dict[test_name] = gen_test(test_path, command, expected_result)
|
||||
return type.__new__(mcs, name, bases, dict)
|
||||
|
||||
|
|
|
@ -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 @@ class Test_Profile(unittest.TestCase):
|
|||
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'])
|
||||
|
||||
def test_get_login_credentials(self):
|
||||
@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)
|
||||
|
||||
@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()
|
||||
|
|
Загрузка…
Ссылка в новой задаче