Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions src/azure-cli-core/azure/cli/core/_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
#This could mean either 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'
_REFRESH_TOKEN = 'refreshToken'

TOKEN_FIELDS_EXCLUDED_FROM_PERSISTENCE = ['familyName',
'givenName',
Expand Down Expand Up @@ -283,6 +284,21 @@ def get_expanded_subscription_info(self, subscription_id=None, name=None, passwo
result['endpoints'] = CLOUD.endpoints
return result

def get_refresh_credentials(self, resource=CLOUD.endpoints.management,
subscription_id=None):
account = self.get_subscription(subscription_id)
user_type = account[_USER_ENTITY][_USER_TYPE]
username_or_sp_id = account[_USER_ENTITY][_USER_NAME]

if user_type == _USER:
refresh_object = self._creds_cache.retrieve_token_entry_for_user(
username_or_sp_id, account[_TENANT_ID], resource)[_REFRESH_TOKEN]
else:
refresh_object = self._creds_cache \
.retrieve_cred_for_service_principal(username_or_sp_id)

return refresh_object

def get_installation_id(self):
installation_id = self._storage.get(_INSTALLATION_ID)
if not installation_id:
Expand Down Expand Up @@ -394,7 +410,7 @@ def persist_cached_creds(self):

self.adal_token_cache.has_state_changed = False

def retrieve_token_for_user(self, username, tenant, resource):
def retrieve_token_entry_for_user(self, username, tenant, resource):
authority = get_authority_url(tenant)
context = self._auth_ctx_factory(authority, cache=self.adal_token_cache)
token_entry = context.acquire_token(resource, username, _CLIENT_ID)
Expand All @@ -403,19 +419,29 @@ def retrieve_token_for_user(self, username, tenant, resource):

if self.adal_token_cache.has_state_changed:
self.persist_cached_creds()

return token_entry

def retrieve_token_for_user(self, username, tenant, resource):
token_entry = self.retrieve_token_entry_for_user(username, tenant, resource)
return (token_entry[_TOKEN_ENTRY_TOKEN_TYPE], token_entry[_ACCESS_TOKEN])

def retrieve_token_for_service_principal(self, sp_id, resource):
cred = self.retrieve_cred_for_service_principal(sp_id)
authority_url = get_authority_url(cred[0])
context = self._auth_ctx_factory(authority_url, None)
token_entry = context.acquire_token_with_client_credentials(resource,
cred[1],
cred[2])
return (token_entry[_TOKEN_ENTRY_TOKEN_TYPE], token_entry[_ACCESS_TOKEN])

def retrieve_cred_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 CLIError("Please run 'az account set' to select active account.")
cred = matched[0]
authority_url = get_authority_url(cred[_SERVICE_PRINCIPAL_TENANT])
context = self._auth_ctx_factory(authority_url, None)
token_entry = context.acquire_token_with_client_credentials(resource,
sp_id,
cred[_ACCESS_TOKEN])
return (token_entry[_TOKEN_ENTRY_TOKEN_TYPE], token_entry[_ACCESS_TOKEN])
# (Tenant, Username, Password)
return (cred[_SERVICE_PRINCIPAL_TENANT], sp_id, cred[_ACCESS_TOKEN])

def retrieve_secret_of_service_principal(self, sp_id):
matched = [x for x in self._service_principal_creds if sp_id == x[_SERVICE_PRINCIPAL_ID]]
Expand Down
110 changes: 100 additions & 10 deletions src/azure-cli-core/azure/cli/core/tests/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def setUpClass(cls):
cls.state1,
cls.tenant_id)
cls.raw_token1 = 'some...secrets'
cls.refresh_token1 = 'faked123'
cls.token_entry1 = {
"_clientId": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
"resource": "https://management.core.windows.net/",
Expand All @@ -35,7 +36,7 @@ def setUpClass(cls):
"identityProvider": "live.com",
"_authority": "https://login.microsoftonline.com/common",
"isMRRT": True,
"refreshToken": "faked123",
"refreshToken": cls.refresh_token1,
"accessToken": cls.raw_token1,
"userId": cls.user1
}
Expand Down Expand Up @@ -272,6 +273,7 @@ def test_load_cached_tokens(self, mock_read_file):
})
self.assertEqual(len(matched), 1)
self.assertEqual(matched[0]['accessToken'], self.raw_token1)
self.assertEqual(matched[0]['refreshToken'], self.refresh_token1)

@mock.patch('azure.cli.core._profile._load_tokens_from_file', autospec=True)
@mock.patch('azure.cli.core._profile.CredsCache.retrieve_token_for_user', autospec=True)
Expand Down Expand Up @@ -301,6 +303,27 @@ def test_get_login_credentials(self, mock_get_token, mock_read_cred_file):
'https://management.core.windows.net/')
self.assertEqual(mock_get_token.call_count, 1)

@mock.patch('azure.cli.core._profile._load_tokens_from_file', autospec=True)
@mock.patch('azure.cli.core._profile.CredsCache.retrieve_token_entry_for_user', autospec=True)
def test_get_refresh_credentials(self, mock_get_token_entry, mock_read_cred_file):
mock_read_cred_file.return_value = [Test_Profile.token_entry1]
mock_get_token_entry.return_value = Test_Profile.token_entry1
#setup
storage_mock = {'subscriptions': None}
profile = Profile(storage_mock)
consolidated = Profile._normalize_properties(self.user1,
[self.subscription1],
False)
profile._set_subscriptions(consolidated)
#action
refresh_object = profile.get_refresh_credentials()

self.assertEqual(refresh_object, self.refresh_token1)
self.assertEqual(mock_read_cred_file.call_count, 1)
mock_get_token_entry.assert_called_once_with(mock.ANY, self.user1, self.tenant_id,
'https://management.core.windows.net/')
self.assertEqual(mock_get_token_entry.call_count, 1)

@mock.patch('azure.cli.core._profile._load_tokens_from_file', autospec=True)
@mock.patch('azure.cli.core._profile.CredsCache.retrieve_token_for_user', autospec=True)
def test_get_login_credentials_for_graph_client(self, mock_get_token, mock_read_cred_file):
Expand All @@ -322,6 +345,51 @@ def test_get_login_credentials_for_graph_client(self, mock_get_token, mock_read_
'https://graph.windows.net/')
self.assertEqual(tenant_id, self.tenant_id)

@mock.patch('azure.cli.core._profile._load_tokens_from_file', autospec=True)
@mock.patch('azure.cli.core._profile.CredsCache.retrieve_token_entry_for_user', autospec=True)
def test_get_refresh_credentials_for_graph_client(self,
mock_get_token_entry,
mock_read_cred_file):
mock_read_cred_file.return_value = [Test_Profile.token_entry1]
mock_get_token_entry.return_value = Test_Profile.token_entry1
#setup
storage_mock = {'subscriptions': None}
profile = Profile(storage_mock)
consolidated = Profile._normalize_properties(self.user1, [self.subscription1],
False)
profile._set_subscriptions(consolidated)
#action
refresh_object = profile.get_refresh_credentials(
resource=CLOUD.endpoints.active_directory_graph_resource_id)

#verify
mock_get_token_entry.assert_called_once_with(mock.ANY, self.user1, self.tenant_id,
'https://graph.windows.net/')
self.assertEqual(refresh_object, Test_Profile.refresh_token1)

@mock.patch('azure.cli.core._profile._load_tokens_from_file', autospec=True)
def test_get_refresh_credentials_for_spn(self, mock_read_cred_file):
sp_user = Test_Profile.user1
sp_tenant = "mytenant"
sp_secret = "mysecret"
test_sp = {
"servicePrincipalId": sp_user,
"servicePrincipalTenant": sp_tenant,
"accessToken": sp_secret
}
mock_read_cred_file.return_value = [test_sp]
#setup
storage_mock = {'subscriptions': None}
profile = Profile(storage_mock)
consolidated = Profile._normalize_properties(self.user1, [self.subscription1],
True)
profile._set_subscriptions(consolidated)
#action
refresh_object = profile.get_refresh_credentials()
#verify
self.assertEqual(refresh_object, (sp_tenant, sp_user, sp_secret))
self.assertEqual(mock_read_cred_file.call_count, 1)

@mock.patch('azure.cli.core._profile._load_tokens_from_file', autospec=True)
@mock.patch('azure.cli.core._profile.CredsCache.persist_cached_creds', autospec=True)
def test_logout(self, mock_persist_creds, mock_read_cred_file):
Expand Down Expand Up @@ -431,10 +499,13 @@ def test_find_subscriptions_from_service_principal_id(self, mock_auth_context):

@mock.patch('azure.cli.core._profile._load_tokens_from_file', autospec=True)
def test_credscache_load_tokens_and_sp_creds(self, mock_read_file):
sp_id = "myapp"
sp_tenant = "mytenant"
sp_secret = "Secret"
test_sp = {
"servicePrincipalId": "myapp",
"servicePrincipalTenant": "mytenant",
"accessToken": "Secret"
"servicePrincipalId": sp_id,
"servicePrincipalTenant": sp_tenant,
"accessToken": sp_secret
}
mock_read_file.return_value = [self.token_entry1, test_sp]

Expand All @@ -445,20 +516,29 @@ def test_credscache_load_tokens_and_sp_creds(self, mock_read_file):
token_entries = [entry for _, entry in creds_cache.adal_token_cache.read_items()]
self.assertEqual(token_entries, [self.token_entry1])
self.assertEqual(creds_cache._service_principal_creds, [test_sp])
self.assertEqual(creds_cache.retrieve_cred_for_service_principal(sp_id),
(sp_tenant, sp_id, sp_secret))

@mock.patch('azure.cli.core._profile._load_tokens_from_file', autospec=True)
@mock.patch('os.fdopen', autospec=True)
@mock.patch('os.open', autospec=True)
def test_credscache_add_new_sp_creds(self, _, mock_open_for_write, mock_read_file):
sp1_id = "myapp"
sp1_tenant = "mytenant"
sp1_secret = "Secret"

sp2_id = "myapp2"
sp2_tenant = "mytenant2"
sp2_secret = "Secret2"
test_sp = {
"servicePrincipalId": "myapp",
"servicePrincipalTenant": "mytenant",
"accessToken": "Secret"
"servicePrincipalId": sp1_id,
"servicePrincipalTenant": sp1_tenant,
"accessToken": sp1_secret
}
test_sp2 = {
"servicePrincipalId": "myapp2",
"servicePrincipalTenant": "mytenant2",
"accessToken": "Secret2"
"servicePrincipalId": sp2_id,
"servicePrincipalTenant": sp2_tenant,
"accessToken": sp2_secret
}
mock_open_for_write.return_value = FileHandleStub()
mock_read_file.return_value = [self.token_entry1, test_sp]
Expand All @@ -474,6 +554,11 @@ def test_credscache_add_new_sp_creds(self, _, mock_open_for_write, mock_read_fil
token_entries = [entry for _, entry in creds_cache.adal_token_cache.read_items()]
self.assertEqual(token_entries, [self.token_entry1])
self.assertEqual(creds_cache._service_principal_creds, [test_sp, test_sp2])
self.assertEqual(creds_cache.retrieve_cred_for_service_principal(sp1_id),
(sp1_tenant, sp1_id, sp1_secret))
self.assertEqual(creds_cache.retrieve_cred_for_service_principal(sp2_id),
(sp2_tenant, sp2_id, sp2_secret))

mock_open_for_write.assert_called_with(mock.ANY, 'w+')

@mock.patch('azure.cli.core._profile._load_tokens_from_file', autospec=True)
Expand Down Expand Up @@ -512,6 +597,7 @@ def test_credscache_remove_creds(self, _, mock_open_for_write, mock_read_file):
def test_credscache_new_token_added_by_adal(self, mock_adal_auth_context, _, mock_open_for_write, mock_read_file): # pylint: disable=line-too-long
token_entry2 = {
"accessToken": "new token",
"refreshToken": "refresh token",
"tokenType": "Bearer",
"userId": self.user1
}
Expand All @@ -531,14 +617,18 @@ def get_auth_context(authority, **kwargs): # pylint: disable=unused-argument
mgmt_resource = 'https://management.core.windows.net/'
token_type, token = creds_cache.retrieve_token_for_user(self.user1, self.tenant_id,
mgmt_resource)

mock_adal_auth_context.acquire_token.assert_called_once_with(
'https://management.core.windows.net/',
self.user1,
mock.ANY)

token_entry = creds_cache.retrieve_token_entry_for_user(self.user1, self.tenant_id,
mgmt_resource)
#assert
mock_open_for_write.assert_called_with(mock.ANY, 'w+')
self.assertEqual(token, 'new token')
self.assertEqual(token_entry, token_entry2)
self.assertEqual(token_type, token_entry2['tokenType'])

class FileHandleStub(object): # pylint: disable=too-few-public-methods
Expand Down
14 changes: 14 additions & 0 deletions src/command_modules/azure-cli-acr/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,20 @@ List repositories in a given container registry
List repositories in a given container registry with credentials
az acr repository list -n myRegistry -u myUsername -p myPassword

Login to a container registry
-------------
::

Command
az acr login: Login to a container registry through Docker.

Arguments
--registry-url -u [Required]: The login server of the container registry.

Examples
Login to a container registry
az acr login -u myregistry.azurecr.io

Show tags of a given repository in a given container registry
-------------
::
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@
az acr update -n myRegistry --admin-enabled true
"""

helps['acr login'] = """
type: command
short-summary: Login to a container registry through Docker.
examples:
- name: Login to a registry
text:
az acr login -u myregistry.azurecr.io
"""

helps['acr repository list'] = """
type: command
examples:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
options_list=('--password', '-p'),
help='The password used to log into a container registry')

register_cli_argument('acr', 'registry_url',
options_list=('--registry-url', '-u'),
help='The login server of the container registry')

register_cli_argument('acr create', 'registry_name', completer=None,
validator=validate_registry_name)
register_cli_argument('acr create', 'resource_group_name',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from urllib.parse import urlencode, urlparse, urlunparse
from subprocess import call
from json import loads
import requests

from azure.cli.core._util import CLIError
from azure.cli.core.commands.parameters import get_resources_in_subscription
from azure.cli.core._profile import Profile

from ._constants import (
ACR_RESOURCE_PROVIDER,
Expand Down Expand Up @@ -84,6 +90,61 @@ def get_access_key_by_storage_account_name(storage_account_name, resource_group_

return client.list_keys(resource_group_name, storage_account_name).keys[0].value #pylint: disable=no-member

def docker_login_to_registry(registry_url):
'''Logs in the Docker client to a registry.
:param str registry: the registry to log in to
'''
profile = Profile()
_, _, tenant = profile.get_login_credentials()
refresh = profile.get_refresh_credentials()
base_endpoint = 'https://' + registry_url.rstrip('/')

challenge = requests.get(base_endpoint + '/v2/')
if challenge.status_code not in [401] or 'WWW-Authenticate' not in challenge.headers:
raise CLIError('Registry did not issue a challenge.')

authenticate = challenge.headers['WWW-Authenticate']

tokens = authenticate.split(' ', 2)
if len(tokens) < 2 or tokens[0].lower() != 'bearer':
raise CLIError('Registry does not support AAD login.')

params = {y[0]: y[1].strip('"') for y in
(x.strip().split('=', 2) for x in tokens[1].split(','))}
if 'realm' not in params or 'service' not in params:
raise CLIError('Registry does not support AAD login.')

authurl = urlparse(params['realm'])
authhost = urlunparse((authurl[0], authurl[1], '/oauth2/exchange', '', '', ''))

headers = {'Content-Type': 'application/x-www-form-urlencoded'}
if isinstance(refresh, str):
content = {
'service': params['service'],
'user_type': 'user',
'tenant': tenant,
'refresh_token': refresh
}
else:
content = {
'service': params['service'],
'user_type': 'spn',
'tenant': tenant,
'username': refresh[1],
'password': refresh[2]
}

response = requests.post(authhost, urlencode(content), headers=headers)

if response.status_code not in [200]:
raise CLIError(
"Access to registry was denied. Response code: {}".format(response.status_code))

refresh_token = loads(response.content.decode("utf-8"))["refresh_token"]

call(["docker", "login", registry_url, "--username",
"00000000-0000-0000-0000-000000000000", "--password", refresh_token])

def arm_deploy_template(resource_group_name,
registry_name,
location,
Expand Down
Loading