Hello community, here is the log from the commit of package python-msrestazure for openSUSE:Factory checked in at 2018-05-13 16:03:40 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-msrestazure (Old) and /work/SRC/openSUSE:Factory/.python-msrestazure.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-msrestazure" Sun May 13 16:03:40 2018 rev:5 rq:601546 version:0.4.28 Changes: -------- --- /work/SRC/openSUSE:Factory/python-msrestazure/python-msrestazure.changes 2018-02-14 09:27:33.936299577 +0100 +++ /work/SRC/openSUSE:Factory/.python-msrestazure.new/python-msrestazure.changes 2018-05-13 16:03:42.677295231 +0200 @@ -1,0 +2,12 @@ +Tue Apr 24 11:06:26 UTC 2018 - adrian.glaub...@suse.com + +- New upstream release + + Version 0.4.28 + + For detailed information about changes see the + README.rst file provided with this package +- Move LICENSE.txt from %doc to %license section +- Refresh patches for new version + + m_drop-compatible-releases-operator.patch +- Update Requires from setup.py + +------------------------------------------------------------------- Old: ---- msrestazure-0.4.20.tar.gz New: ---- msrestazure-0.4.28.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-msrestazure.spec ++++++ --- /var/tmp/diff_new_pack.WMNJqk/_old 2018-05-13 16:03:43.441267359 +0200 +++ /var/tmp/diff_new_pack.WMNJqk/_new 2018-05-13 16:03:43.445267213 +0200 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-msrestazure -Version: 0.4.20 +Version: 0.4.28 Release: 0 Summary: AutoRest swagger generator License: MIT @@ -32,10 +32,9 @@ BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: python-adal < 1.0.0 -Requires: python-adal >= 0.4.7 -Requires: python-keyring >= 5.6 +Requires: python-adal >= 0.5.0 Requires: python-msrest < 2.0.0 -Requires: python-msrest >= 0.4.25 +Requires: python-msrest >= 0.4.28 BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildArch: noarch @@ -59,7 +58,8 @@ %files %{python_files} %defattr(-,root,root,-) -%doc LICENSE.md README.rst +%doc README.rst +%license LICENSE.md %{python_sitelib}/* %changelog ++++++ m_drop-compatible-releases-operator.patch ++++++ --- /var/tmp/diff_new_pack.WMNJqk/_old 2018-05-13 16:03:43.509264878 +0200 +++ /var/tmp/diff_new_pack.WMNJqk/_new 2018-05-13 16:03:43.513264732 +0200 @@ -1,11 +1,11 @@ -diff -Nru msrestazure-0.4.20.orig/setup.py msrestazure-0.4.20/setup.py ---- msrestazure-0.4.20.orig/setup.py 2018-01-08 23:26:51.000000000 +0100 -+++ msrestazure-0.4.20/setup.py 2018-01-26 13:33:02.600469484 +0100 -@@ -51,6 +51,6 @@ +diff -Nru msrestazure-0.4.28.orig/setup.py msrestazure-0.4.28/setup.py +--- msrestazure-0.4.28.orig/setup.py 2018-04-24 01:45:02.000000000 +0200 ++++ msrestazure-0.4.28/setup.py 2018-04-24 13:00:24.806436987 +0200 +@@ -50,6 +50,6 @@ + 'Topic :: Software Development'], install_requires=[ - "msrest>=0.4.25,<2.0.0", - "keyring>=5.6", -- "adal~=0.4.7" -+ "adal>=0.4.7" + "msrest>=0.4.28,<2.0.0", +- "adal~=0.5.0" ++ "adal>=0.5.0" ], ) ++++++ msrestazure-0.4.20.tar.gz -> msrestazure-0.4.28.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msrestazure-0.4.20/PKG-INFO new/msrestazure-0.4.28/PKG-INFO --- old/msrestazure-0.4.20/PKG-INFO 2018-01-08 23:27:27.000000000 +0100 +++ new/msrestazure-0.4.28/PKG-INFO 2018-04-24 01:45:43.000000000 +0200 @@ -1,12 +1,11 @@ Metadata-Version: 1.1 Name: msrestazure -Version: 0.4.20 +Version: 0.4.28 Summary: AutoRest swagger generator Python client runtime. Azure-specific module. Home-page: https://github.com/Azure/msrestazure-for-python Author: Microsoft Corporation Author-email: azpysdkh...@microsoft.com License: MIT License -Description-Content-Type: UNKNOWN Description: AutoRest: Python Client Runtime - Azure Module =============================================== @@ -29,6 +28,76 @@ Release History --------------- + 2018-04-23 Version 0.4.28 + +++++++++++++++++++++++++ + + **Disclaimer** + + Do to some stability issues with "keyring" dependency that highly change from one system to another, + this package is no longer a dependency of "msrestazure". + If you were using the secured token cache of `ServicePrincipalCredentials` and `UserPassCredentials`, + the feature is still available, but you need to install manually "keyring". The functionnality will activate automatically. + + 2018-04-18 Version 0.4.27 + +++++++++++++++++++++++++ + + **Features** + + - Implements new features of msrest 0.4.28 on session improvement. See msrest ChangeLog for details. + + Update msrest dependency to 0.4.28 + + 2018-04-17 Version 0.4.26 + +++++++++++++++++++++++++ + + **Bugfixes** + + - IMDS/MSI: Retry on more error codes (#87) + - IMDS/MSI: fix a boundary case on timeout (#86) + + 2018-03-29 Version 0.4.25 + +++++++++++++++++++++++++ + + **Features** + + - MSIAuthentication now uses IMDS endpoint if available + - MSIAuthentication can be used in any environment that defines MSI_ENDPOINT env variable + + 2018-03-26 Version 0.4.24 + +++++++++++++++++++++++++ + + **Bugfix** + + - Fix parse_resource_id() tool to be case-insensitive to keywords when matching #81 + - Add missing baseclass init call for AdalAuthentication #82 + + 2018-03-19 Version 0.4.23 + +++++++++++++++++++++++++ + + **Bugfix** + + - Fix LRO result if POST uses AsyncOperation header (Autorest.Python 3.0 only) #79 + + 2018-02-27 Version 0.4.22 + +++++++++++++++++++++++++ + + **Bugfix** + + - Remove a possible infinite loop with MSIAuthentication #77 + + **Disclaimer** + + From this version, MSIAuthentication will fail instantly if you try to get MSI token + from a VM where the extension is not installed, or not yet ready. + You need to do your own retry mechanism if you think the extension is provisioning and + the call might succeed later. + This behavior is consistent with other Azure SDK implementation of MSI scenarios. + + 2018-01-26 Version 0.4.21 + +++++++++++++++++++++++++ + + - Update allowed ADAL dependency to 0.5.x + 2018-01-08 Version 0.4.20 +++++++++++++++++++++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msrestazure-0.4.20/README.rst new/msrestazure-0.4.28/README.rst --- old/msrestazure-0.4.20/README.rst 2018-01-08 23:26:51.000000000 +0100 +++ new/msrestazure-0.4.28/README.rst 2018-04-24 01:45:02.000000000 +0200 @@ -20,6 +20,76 @@ Release History --------------- +2018-04-23 Version 0.4.28 ++++++++++++++++++++++++++ + +**Disclaimer** + +Do to some stability issues with "keyring" dependency that highly change from one system to another, +this package is no longer a dependency of "msrestazure". +If you were using the secured token cache of `ServicePrincipalCredentials` and `UserPassCredentials`, +the feature is still available, but you need to install manually "keyring". The functionnality will activate automatically. + +2018-04-18 Version 0.4.27 ++++++++++++++++++++++++++ + +**Features** + +- Implements new features of msrest 0.4.28 on session improvement. See msrest ChangeLog for details. + +Update msrest dependency to 0.4.28 + +2018-04-17 Version 0.4.26 ++++++++++++++++++++++++++ + +**Bugfixes** + +- IMDS/MSI: Retry on more error codes (#87) +- IMDS/MSI: fix a boundary case on timeout (#86) + +2018-03-29 Version 0.4.25 ++++++++++++++++++++++++++ + +**Features** + +- MSIAuthentication now uses IMDS endpoint if available +- MSIAuthentication can be used in any environment that defines MSI_ENDPOINT env variable + +2018-03-26 Version 0.4.24 ++++++++++++++++++++++++++ + +**Bugfix** + +- Fix parse_resource_id() tool to be case-insensitive to keywords when matching #81 +- Add missing baseclass init call for AdalAuthentication #82 + +2018-03-19 Version 0.4.23 ++++++++++++++++++++++++++ + +**Bugfix** + +- Fix LRO result if POST uses AsyncOperation header (Autorest.Python 3.0 only) #79 + +2018-02-27 Version 0.4.22 ++++++++++++++++++++++++++ + +**Bugfix** + +- Remove a possible infinite loop with MSIAuthentication #77 + +**Disclaimer** + +From this version, MSIAuthentication will fail instantly if you try to get MSI token +from a VM where the extension is not installed, or not yet ready. +You need to do your own retry mechanism if you think the extension is provisioning and +the call might succeed later. +This behavior is consistent with other Azure SDK implementation of MSI scenarios. + +2018-01-26 Version 0.4.21 ++++++++++++++++++++++++++ + +- Update allowed ADAL dependency to 0.5.x + 2018-01-08 Version 0.4.20 +++++++++++++++++++++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msrestazure-0.4.20/msrestazure/azure_active_directory.py new/msrestazure-0.4.28/msrestazure/azure_active_directory.py --- old/msrestazure-0.4.20/msrestazure/azure_active_directory.py 2018-01-08 23:26:51.000000000 +0100 +++ new/msrestazure-0.4.28/msrestazure/azure_active_directory.py 2018-04-24 01:45:02.000000000 +0200 @@ -42,7 +42,7 @@ MismatchingStateError, OAuth2Error, TokenExpiredError) -from requests import RequestException, ConnectionError +from requests import RequestException, ConnectionError, HTTPError import requests import requests_oauthlib as oauth @@ -57,11 +57,12 @@ from msrest.exceptions import AuthenticationError, raise_with_traceback from msrestazure.azure_cloud import AZURE_CHINA_CLOUD, AZURE_PUBLIC_CLOUD +from msrestazure.azure_configuration import AzureConfiguration _LOGGER = logging.getLogger(__name__) if not keyring: - _LOGGER.warning("Cannot load keyring on your system: %s", KEYRING_EXCEPTION) + _LOGGER.warning("Cannot load 'keyring' on your system (either not installed, or not configured correctly): %s", KEYRING_EXCEPTION) def _build_url(uri, paths, scheme): """Combine URL parts. @@ -110,6 +111,7 @@ class AADMixin(OAuthTokenAuthentication): """Mixin for Authentication object. Provides some AAD functionality: + - State validation - Token caching and retrieval - Default AAD configuration @@ -124,6 +126,7 @@ """Configure authentication endpoint. Optional kwargs may include: + - cloud_environment (msrestazure.azure_cloud.Cloud): A targeted cloud environment - china (bool): Configure auth for China-based service, default is 'False'. @@ -191,13 +194,6 @@ if self.token.get('expires_at'): countdown = float(self.token['expires_at']) - time.time() self.token['expires_in'] = countdown - kwargs = {} - if self.token.get('refresh_token'): - kwargs['auto_refresh_url'] = self.token_uri - kwargs['auto_refresh_kwargs'] = {'client_id': self.id, - 'resource': self.resource} - kwargs['token_updater'] = self._default_token_cache - return kwargs def _default_token_cache(self, token): """Store token for future sessions. @@ -225,28 +221,23 @@ self.token = ast.literal_eval(str(token)) self.signed_session() - def signed_session(self): + def signed_session(self, session=None): """Create token-friendly Requests session, using auto-refresh. Used internally when a request is made. - :rtype: requests_oauthlib.OAuth2Session - :raises: TokenExpiredError if token can no longer be refreshed. - """ - kwargs = self._parse_token() - try: - new_session = oauth.OAuth2Session( - self.id, - token=self.token, - **kwargs) - return new_session - except TokenExpiredError as err: - raise_with_traceback(Expired, "", err) + If a session object is provided, configure it directly. Otherwise, + create a new session and return it. + :param session: The session to configure for authentication + :type session: requests.Session + """ + self._parse_token() + return super(AADMixin, self).signed_session(session) + def clear_cached_token(self): """Clear any stored tokens. :raises: KeyError if failed to clear token. - :rtype: None """ try: keyring.delete_password(self.cred_store, self.store_key) @@ -254,32 +245,13 @@ raise_with_traceback(KeyError, "Unable to clear token.") -class AADRefreshMixin(object): - """ - Additional token refresh logic - """ - - def refresh_session(self): - """Return updated session if token has expired, attempts to - refresh using newly acquired token. - - :rtype: requests.Session. - """ - if self.token.get('refresh_token'): - try: - return self.signed_session() - except Expired: - pass - self.set_token() - return self.signed_session() - - class AADTokenCredentials(AADMixin): """ Credentials objects for AAD token retrieved through external process e.g. Python ADAL lib. Optional kwargs may include: + - cloud_environment (msrestazure.azure_cloud.Cloud): A targeted cloud environment - china (bool): Configure auth for China-based service, default is 'False'. @@ -313,12 +285,12 @@ """Create AADTokenCredentials from a cached token if it has not yet expired. """ - session = cls(None, None, client_id=client_id, cached=True) + session = cls(None, client_id=client_id, cached=True) session._retrieve_stored_token() return session -class UserPassCredentials(AADRefreshMixin, AADMixin): +class UserPassCredentials(AADMixin): """Credentials object for Headless Authentication, i.e. AAD authentication via username and password. @@ -327,6 +299,7 @@ that 2-factor auth be disabled. Optional kwargs may include: + - cloud_environment (msrestazure.azure_cloud.Cloud): A targeted cloud environment - china (bool): Configure auth for China-based service, default is 'False'. @@ -392,7 +365,8 @@ if self.secret: optional['client_secret'] = self.secret try: - token = session.fetch_token(self.token_uri, client_id=self.id, + token = session.fetch_token(self.token_uri, + client_id=self.id, username=self.username, password=self.password, resource=self.resource, @@ -404,13 +378,47 @@ raise_with_traceback(AuthenticationError, "", err) self.token = token + self._default_token_cache(self.token) + + def refresh_session(self, session=None): + """Return updated session if token has expired, attempts to + refresh using newly acquired token. + + If a session object is provided, configure it directly. Otherwise, + create a new session and return it. + + :param session: The session to configure for authentication + :type session: requests.Session + :rtype: requests.Session. + """ + with self._setup_session() as session: + optional = {} + if self.secret: + optional['client_secret'] = self.secret + try: + token = session.refresh_token(self.token_uri, + client_id=self.id, + username=self.username, + password=self.password, + resource=self.resource, + verify=self.verify, + proxies=self.proxies, + timeout=self.timeout, + **optional) + except (RequestException, OAuth2Error, InvalidGrantError) as err: + raise_with_traceback(AuthenticationError, "", err) + + self.token = token + self._default_token_cache(self.token) + return self.signed_session(session) -class ServicePrincipalCredentials(AADRefreshMixin, AADMixin): +class ServicePrincipalCredentials(AADMixin): """Credentials object for Service Principle Authentication. Authenticates via a Client ID and Secret. Optional kwargs may include: + - cloud_environment (msrestazure.azure_cloud.Cloud): A targeted cloud environment - china (bool): Configure auth for China-based service, default is 'False'. @@ -462,7 +470,8 @@ """ with self._setup_session() as session: try: - token = session.fetch_token(self.token_uri, client_id=self.id, + token = session.fetch_token(self.token_uri, + client_id=self.id, resource=self.resource, client_secret=self.secret, response_type="client_credentials", @@ -473,9 +482,29 @@ raise_with_traceback(AuthenticationError, "", err) else: self.token = token + self._default_token_cache(self.token) + + def refresh_session(self, session=None): + """Alias to signed_session(). + + SP flow does not contain refresh_token, so this method is just asking a new + token to AD. + + If a session object is provided, configure it directly. Otherwise, + create a new session and return it. + + :param session: The session to configure for authentication + :type session: requests.Session + :rtype: requests.Session. + """ + self.set_token() + return self.signed_session(session) + # For backward compatibility of import, but I doubt someone uses that... class InteractiveCredentials(object): + """This class has been removed and using it will raise a NotImplementedError error. + """ def __init__(self, *args, **kwargs): raise NotImplementedError("InteractiveCredentials was not functionning and was removed. Please use ADAL and device code instead.") @@ -483,64 +512,68 @@ """A wrapper to use ADAL for Python easily to authenticate on Azure. .. versionadded:: 0.4.5 - """ - def __init__(self, adal_method, *args, **kwargs): - """Take an ADAL `acquire_token` method and its parameters. + Take an ADAL `acquire_token` method and its parameters. - :Example: + :Example: - .. code:: python + .. code:: python - context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') - RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource - token = context.acquire_token_with_client_credentials( - RESOURCE, - "http://PythonSDK", - "Key-Configured-In-Portal") + context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') + RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource + token = context.acquire_token_with_client_credentials( + RESOURCE, + "http://PythonSDK", + "Key-Configured-In-Portal") - can be written here: + can be written here: - .. code:: python + .. code:: python - context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') - RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource - credentials = AdalAuthentication( - context.acquire_token_with_client_credentials, - RESOURCE, - "http://PythonSDK", - "Key-Configured-In-Portal") + context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') + RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource + credentials = AdalAuthentication( + context.acquire_token_with_client_credentials, + RESOURCE, + "http://PythonSDK", + "Key-Configured-In-Portal") - or using a lambda if you prefer: + or using a lambda if you prefer: - .. code:: python + .. code:: python - context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') - RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource - credentials = AdalAuthentication( - lambda: context.acquire_token_with_client_credentials( - RESOURCE, - "http://PythonSDK", - "Key-Configured-In-Portal" - ) + context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') + RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource + credentials = AdalAuthentication( + lambda: context.acquire_token_with_client_credentials( + RESOURCE, + "http://PythonSDK", + "Key-Configured-In-Portal" ) + ) - :param adal_method: A lambda with no args, or `acquire_token` method with args using args/kwargs - :param args: Optional args for the method - :param kwargs: Optional kwargs for the method - """ + :param callable adal_method: A lambda with no args, or `acquire_token` method with args using args/kwargs + :param args: Optional positional args for the method + :param kwargs: Optional kwargs for the method + """ + + def __init__(self, adal_method, *args, **kwargs): + super(AdalAuthentication, self).__init__() self._adal_method = adal_method self._args = args self._kwargs = kwargs - def signed_session(self): - """Get a signed session for requests. + def signed_session(self, session=None): + """Create requests session with any required auth headers applied. - Usually called by the Azure SDKs for you to authenticate queries. + If a session object is provided, configure it directly. Otherwise, + create a new session and return it. + :param session: The session to configure for authentication + :type session: requests.Session :rtype: requests.Session """ - session = super(AdalAuthentication, self).signed_session() + session = super(AdalAuthentication, self).signed_session(session) try: raw_token = self._adal_method(*self._args, **self._kwargs) @@ -558,17 +591,18 @@ session.headers['Authorization'] = header return session - def get_msi_token(resource, port=50342, msi_conf=None): - """Get MSI token from inside a VM/VMSS. + """Get MSI token if MSI_ENDPOINT is set. + + IF MSI_ENDPOINT is not set, will try legacy access through 'http://localhost:{}/oauth2/token'.format(port). If msi_conf is used, must be a dict of one key in ["client_id", "object_id", "msi_res_id"] :param str resource: The resource where the token would be use. - :param int port: The port is not the default 50342 is used. - :param dict[str, str] msi_conf: msi_conf if to request a token through a User Assigned Identity (if not specified, assume System Assigned) + :param int port: The port if not the default 50342 is used. Ignored if MSI_ENDPOINT is set. + :param dict[str,str] msi_conf: msi_conf if to request a token through a User Assigned Identity (if not specified, assume System Assigned) """ - request_uri = 'http://localhost:{}/oauth2/token'.format(port) + request_uri = os.environ.get("MSI_ENDPOINT", 'http://localhost:{}/oauth2/token'.format(port)) payload = { 'resource': resource } @@ -577,28 +611,15 @@ raise ValueError("{} are mutually exclusive".format(list(msi_conf.keys()))) payload.update(msi_conf) - # retry as the token endpoint might not be available yet, one example is you use CLI in a - # custom script extension of VMSS, which might get provisioned before the MSI extensioon - while True: - err = None - try: - result = requests.post(request_uri, data=payload, headers={'Metadata': 'true'}) - _LOGGER.debug("MSI: Retrieving a token from %s, with payload %s", request_uri, payload) - if result.status_code != 200: - err = result.text - except Exception as ex: # pylint: disable=broad-except - err = str(ex) - - if err: - # we might need some error code checking to avoid silly waiting. The bottom line is users can - # always press ctrl+c to stop it - _LOGGER.warning("MSI: Failed to retrieve a token from '%s' with an error of '%s'. This could be caused " - "by the MSI extension not yet fullly provisioned. Will retry in 60 seconds...", - request_uri, err) - time.sleep(60) - else: - _LOGGER.debug('MSI: token retrieved') - break + try: + result = requests.post(request_uri, data=payload, headers={'Metadata': 'true'}) + _LOGGER.debug("MSI: Retrieving a token from %s, with payload %s", request_uri, payload) + result.raise_for_status() + except Exception as ex: # pylint: disable=broad-except + _LOGGER.warning("MSI: Failed to retrieve a token from '%s' with an error of '%s'. This could be caused " + "by the MSI extension not yet fullly provisioned.", + request_uri, ex) + raise token_entry = result.json() return token_entry['token_type'], token_entry['access_token'], token_entry @@ -606,8 +627,9 @@ """Get a MSI token from inside a webapp or functions. Env variable will look like: - MSI_ENDPOINT = http://127.0.0.1:41741/MSI/token/ - MSI_SECRET = 69418689F1E342DD946CB82994CDA3CB + + - MSI_ENDPOINT = http://127.0.0.1:41741/MSI/token/ + - MSI_SECRET = 69418689F1E342DD946CB82994CDA3CB """ try: msi_endpoint = os.environ['MSI_ENDPOINT'] @@ -653,6 +675,7 @@ """Credentials object for MSI authentication,. Optional kwargs may include: + - client_id: Identifies, by Azure AD client id, a specific explicit identity to use when authenticating to Azure AD. Mutually exclusive with object_id and msi_res_id. - object_id: Identifies, by Azure AD object id, a specific explicit identity to use when authenticating to Azure AD. Mutually exclusive with client_id and msi_res_id. - msi_res_id: Identifies, by ARM resource id, a specific explicit identity to use when authenticating to Azure AD. Mutually exclusive with client_id and object_id. @@ -660,27 +683,118 @@ - resource (str): Alternative authentication resource, default is 'https://management.core.windows.net/'. - :param int port: MSI local port if VM/VMSS context (ignored otherwise) + .. versionadded:: 0.4.14 """ def __init__(self, port=50342, **kwargs): super(MSIAuthentication, self).__init__(None) + if port != 50342: + warnings.warn("The 'port' argument is no longer used, and will be removed in a future release", DeprecationWarning) self.port = port + self.msi_conf = {k:v for k,v in kwargs.items() if k in ["client_id", "object_id", "msi_res_id"]} self.cloud_environment = kwargs.get('cloud_environment', AZURE_PUBLIC_CLOUD) self.resource = kwargs.get('resource', self.cloud_environment.endpoints.active_directory_resource_id) - def set_token(self): if _is_app_service(): if self.msi_conf: raise AuthenticationError("User Assigned Entity is not available on WebApp yet.") + elif "MSI_ENDPOINT" not in os.environ: + # Use IMDS if no MSI_ENDPOINT + self._vm_msi = _ImdsTokenProvider(self.resource, self.msi_conf) + + def set_token(self): + if _is_app_service(): self.scheme, _, self.token = get_msi_token_webapp(self.resource) - else: + elif "MSI_ENDPOINT" in os.environ: self.scheme, _, self.token = get_msi_token(self.resource, self.port, self.msi_conf) + else: + token_entry = self._vm_msi.get_token() + self.scheme, self.token = token_entry['token_type'], token_entry + + def signed_session(self, session=None): + """Create requests session with any required auth headers applied. + + If a session object is provided, configure it directly. Otherwise, + create a new session and return it. - def signed_session(self): + :param session: The session to configure for authentication + :type session: requests.Session + :rtype: requests.Session + """ # Token cache is handled by the VM extension, call each time to avoid expiration self.set_token() - return super(MSIAuthentication, self).signed_session() + return super(MSIAuthentication, self).signed_session(session) + + +class _ImdsTokenProvider(object): + """A help class handling token acquisitions through Azure IMDS plugin. + """ + + def __init__(self, resource, msi_conf=None): + self._user_agent = AzureConfiguration(None).user_agent + self.identity_type, self.identity_id = None, None + if msi_conf: + if len(msi_conf.keys()) > 1: + raise ValueError('"client_id", "object_id", "msi_res_id" are mutually exclusive') + elif len(msi_conf.keys()) == 1: + self.identity_type, self.identity_id = next(iter(msi_conf.items())) + # default to system assigned identity on an empty configuration object + + self.cache = {} + self.resource = resource + + def get_token(self): + import datetime + # let us hit the cache first + token_entry = self.cache.get(self.resource, None) + if token_entry: + expires_on = int(token_entry['expires_on']) + expires_on_datetime = datetime.datetime.fromtimestamp(expires_on) + expiration_margin = 5 # in minutes + if datetime.datetime.now() + datetime.timedelta(minutes=expiration_margin) <= expires_on_datetime: + _LOGGER.info("MSI: token is found in cache.") + return token_entry + _LOGGER.info("MSI: cache is found but expired within %s minutes, so getting a new one.", expiration_margin) + self.cache.pop(self.resource) + + token_entry = self._retrieve_token_from_imds_with_retry() + self.cache[self.resource] = token_entry + return token_entry + + def _retrieve_token_from_imds_with_retry(self): + import random + import json + # 169.254.169.254 is a well known ip address hosting the web service that provides the Azure IMDS metadata + request_uri = 'http://169.254.169.254/metadata/identity/oauth2/token' + payload = { + 'resource': self.resource, + 'api-version': '2018-02-01' + } + if self.identity_id: + payload[self.identity_type] = self.identity_id + + retry, max_retry = 1, 20 + # simplified version of https://en.wikipedia.org/wiki/Exponential_backoff + slots = [100 * ((2 << x) - 1) / 1000 for x in range(max_retry)] + while retry <= max_retry: + result = requests.get(request_uri, params=payload, headers={'Metadata': 'true', 'User-Agent':self._user_agent}) + _LOGGER.debug("MSI: Retrieving a token from %s, with payload %s", request_uri, payload) + if result.status_code in [404, 429] or (499 < result.status_code < 600): + wait = random.choice(slots[:retry]) + _LOGGER.warning("MSI: Wait: %ss and retry: %s", wait, retry) + time.sleep(wait) + retry += 1 + elif result.status_code != 200: + raise HTTPError(request=result.request, response=result.raw) + else: + break + + if retry > max_retry: + raise TimeoutError('MSI: Failed to acquire tokens after {} times'.format(max_retry)) + + _LOGGER.debug('MSI: Token retrieved') + token_entry = json.loads(result.content.decode()) + return token_entry \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msrestazure-0.4.20/msrestazure/azure_configuration.py new/msrestazure-0.4.28/msrestazure/azure_configuration.py --- old/msrestazure-0.4.20/msrestazure/azure_configuration.py 2018-01-08 23:26:51.000000000 +0100 +++ new/msrestazure-0.4.28/msrestazure/azure_configuration.py 2018-04-24 01:45:02.000000000 +0200 @@ -72,7 +72,6 @@ :param str filepath: Path to save file to. :raises: ValueError if supplied filepath cannot be written to. - :rtype: None """ self._config.add_section("Azure") self._config.set("Azure", @@ -85,7 +84,6 @@ :param str filepath: Path to existing config file. :raises: ValueError if supplied config file is invalid. - :rtype: None """ try: self._config.read(filepath) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msrestazure-0.4.20/msrestazure/azure_operation.py new/msrestazure-0.4.28/msrestazure/azure_operation.py --- old/msrestazure-0.4.20/msrestazure/azure_operation.py 2018-01-08 23:26:51.000000000 +0100 +++ new/msrestazure-0.4.28/msrestazure/azure_operation.py 2018-04-24 01:45:02.000000000 +0200 @@ -309,6 +309,13 @@ if not self.status: raise BadResponse("No status found in body") + # Status can contains information, see ARM spec: + # https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/Addendum.md#operation-resource-format + # "properties": { + # /\* The resource provider can choose the values here, but it should only be + # returned on a successful operation (status being "Succeeded"). \*/ + #}, + # So try to parse it try: self.resource = self.get_outputs(response) except Exception: @@ -328,6 +335,9 @@ """Initiates long running operation and polls status in separate thread. + This class is used in old SDK and has been replaced. See "polling" + submodule now. + :param callable send_cmd: The API request to initiate the operation. :param callable update_cmd: The API reuqest to check the status of the operation. @@ -492,7 +502,7 @@ :param int timeout: Perion of time to wait for the long running operation to complete. - :raises CloudError: Server problem with the query. + :raises ~msrestazure.azure_exceptions.CloudError: Server problem with the query. """ if self._thread is None: return diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msrestazure-0.4.20/msrestazure/polling/arm_polling.py new/msrestazure-0.4.28/msrestazure/polling/arm_polling.py --- old/msrestazure-0.4.20/msrestazure/polling/arm_polling.py 2018-01-08 23:26:51.000000000 +0100 +++ new/msrestazure-0.4.28/msrestazure/polling/arm_polling.py 2018-04-24 01:45:02.000000000 +0200 @@ -269,6 +269,18 @@ if not self.status: raise BadResponse("No status found in body") + # Status can contains information, see ARM spec: + # https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/Addendum.md#operation-resource-format + # "properties": { + # /\* The resource provider can choose the values here, but it should only be + # returned on a successful operation (status being "Succeeded"). \*/ + #}, + # So try to parse it + try: + self.resource = self._deserialize(response) + except Exception: + self.resource = None + def set_async_url_if_present(self, response): async_url = get_header_url(response, 'azure-asyncoperation') if async_url: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msrestazure-0.4.20/msrestazure/tools.py new/msrestazure-0.4.28/msrestazure/tools.py --- old/msrestazure-0.4.20/msrestazure/tools.py 2018-01-08 23:26:51.000000000 +0100 +++ new/msrestazure-0.4.28/msrestazure/tools.py 2018-04-24 01:45:02.000000000 +0200 @@ -32,15 +32,18 @@ _LOGGER = logging.getLogger(__name__) _ARMID_RE = re.compile( - '/subscriptions/(?P<subscription>[^/]*)(/resource[gG]roups/(?P<resource_group>[^/]*))?' + '(?i)/subscriptions/(?P<subscription>[^/]*)(/resourceGroups/(?P<resource_group>[^/]*))?' '/providers/(?P<namespace>[^/]*)/(?P<type>[^/]*)/(?P<name>[^/]*)(?P<children>.*)') -_CHILDREN_RE = re.compile('(/providers/(?P<child_namespace>[^/]*))?/' +_CHILDREN_RE = re.compile('(?i)(/providers/(?P<child_namespace>[^/]*))?/' '(?P<child_type>[^/]*)/(?P<child_name>[^/]*)') def register_rp_hook(r, *args, **kwargs): """This is a requests hook to register RP automatically. + You should not use this command manually, this is added automatically + by the SDK. + See requests documentation for details of the signature of this function. http://docs.python-requests.org/en/master/user/advanced/#event-hooks """ @@ -115,6 +118,7 @@ - child_namespace_{level}: Namespace for the child resoure of that level - child_type_{level}: Type of the child resource of that level - child_name_{level}: Name of the child resource of that level + - last_child_num: Level of the last child - resource_parent: Computed parent in the following pattern: providers/{namespace}\ /{parent}/{type}/{name} - resource_namespace: Same as namespace. Note that this may be different than the \ @@ -122,7 +126,7 @@ - resource_type: Type of the target resource (not the parent) - resource_name: Name of the target resource (not the parent) - :rtype: dict + :rtype: dict[str,str] """ if not rid: return {} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msrestazure-0.4.20/msrestazure/version.py new/msrestazure-0.4.28/msrestazure/version.py --- old/msrestazure-0.4.20/msrestazure/version.py 2018-01-08 23:26:51.000000000 +0100 +++ new/msrestazure-0.4.28/msrestazure/version.py 2018-04-24 01:45:02.000000000 +0200 @@ -24,4 +24,5 @@ # # -------------------------------------------------------------------------- -msrestazure_version = "0.4.20" +#: version of the package. Use msrestazure.__version__ instead. +msrestazure_version = "0.4.28" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msrestazure-0.4.20/msrestazure.egg-info/PKG-INFO new/msrestazure-0.4.28/msrestazure.egg-info/PKG-INFO --- old/msrestazure-0.4.20/msrestazure.egg-info/PKG-INFO 2018-01-08 23:27:27.000000000 +0100 +++ new/msrestazure-0.4.28/msrestazure.egg-info/PKG-INFO 2018-04-24 01:45:43.000000000 +0200 @@ -1,12 +1,11 @@ Metadata-Version: 1.1 Name: msrestazure -Version: 0.4.20 +Version: 0.4.28 Summary: AutoRest swagger generator Python client runtime. Azure-specific module. Home-page: https://github.com/Azure/msrestazure-for-python Author: Microsoft Corporation Author-email: azpysdkh...@microsoft.com License: MIT License -Description-Content-Type: UNKNOWN Description: AutoRest: Python Client Runtime - Azure Module =============================================== @@ -29,6 +28,76 @@ Release History --------------- + 2018-04-23 Version 0.4.28 + +++++++++++++++++++++++++ + + **Disclaimer** + + Do to some stability issues with "keyring" dependency that highly change from one system to another, + this package is no longer a dependency of "msrestazure". + If you were using the secured token cache of `ServicePrincipalCredentials` and `UserPassCredentials`, + the feature is still available, but you need to install manually "keyring". The functionnality will activate automatically. + + 2018-04-18 Version 0.4.27 + +++++++++++++++++++++++++ + + **Features** + + - Implements new features of msrest 0.4.28 on session improvement. See msrest ChangeLog for details. + + Update msrest dependency to 0.4.28 + + 2018-04-17 Version 0.4.26 + +++++++++++++++++++++++++ + + **Bugfixes** + + - IMDS/MSI: Retry on more error codes (#87) + - IMDS/MSI: fix a boundary case on timeout (#86) + + 2018-03-29 Version 0.4.25 + +++++++++++++++++++++++++ + + **Features** + + - MSIAuthentication now uses IMDS endpoint if available + - MSIAuthentication can be used in any environment that defines MSI_ENDPOINT env variable + + 2018-03-26 Version 0.4.24 + +++++++++++++++++++++++++ + + **Bugfix** + + - Fix parse_resource_id() tool to be case-insensitive to keywords when matching #81 + - Add missing baseclass init call for AdalAuthentication #82 + + 2018-03-19 Version 0.4.23 + +++++++++++++++++++++++++ + + **Bugfix** + + - Fix LRO result if POST uses AsyncOperation header (Autorest.Python 3.0 only) #79 + + 2018-02-27 Version 0.4.22 + +++++++++++++++++++++++++ + + **Bugfix** + + - Remove a possible infinite loop with MSIAuthentication #77 + + **Disclaimer** + + From this version, MSIAuthentication will fail instantly if you try to get MSI token + from a VM where the extension is not installed, or not yet ready. + You need to do your own retry mechanism if you think the extension is provisioning and + the call might succeed later. + This behavior is consistent with other Azure SDK implementation of MSI scenarios. + + 2018-01-26 Version 0.4.21 + +++++++++++++++++++++++++ + + - Update allowed ADAL dependency to 0.5.x + 2018-01-08 Version 0.4.20 +++++++++++++++++++++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msrestazure-0.4.20/msrestazure.egg-info/requires.txt new/msrestazure-0.4.28/msrestazure.egg-info/requires.txt --- old/msrestazure-0.4.20/msrestazure.egg-info/requires.txt 2018-01-08 23:27:27.000000000 +0100 +++ new/msrestazure-0.4.28/msrestazure.egg-info/requires.txt 2018-04-24 01:45:43.000000000 +0200 @@ -1,3 +1,2 @@ -msrest<2.0.0,>=0.4.25 -keyring>=5.6 -adal~=0.4.7 +msrest<2.0.0,>=0.4.28 +adal~=0.5.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msrestazure-0.4.20/setup.py new/msrestazure-0.4.28/setup.py --- old/msrestazure-0.4.20/setup.py 2018-01-08 23:26:51.000000000 +0100 +++ new/msrestazure-0.4.28/setup.py 2018-04-24 01:45:02.000000000 +0200 @@ -28,7 +28,7 @@ setup( name='msrestazure', - version='0.4.20', + version='0.4.28', author='Microsoft Corporation', author_email='azpysdkh...@microsoft.com', packages=find_packages(exclude=["tests", "tests.*"]), @@ -49,8 +49,7 @@ 'License :: OSI Approved :: MIT License', 'Topic :: Software Development'], install_requires=[ - "msrest>=0.4.25,<2.0.0", - "keyring>=5.6", - "adal~=0.4.7" + "msrest>=0.4.28,<2.0.0", + "adal~=0.5.0" ], )