Repository: cloud-custodian/cloud-custodian

Issue:
azure - add support for MSI auth
If Custodian is running on a resource with MSI enable, it should be able to authenticated using the sp configured for that resource.

We can use an environment variable to indicate MSI and then use the local MSI endpoint to fetch an AAD token. We will need to be able to request different resource tokens (i.e. storage and arm) from the MSI endpoint. 


Discussion:

Resolving patch:
diff --git a/tools/c7n_azure/c7n_azure/constants.py b/tools/c7n_azure/c7n_azure/constants.py
--- a/tools/c7n_azure/c7n_azure/constants.py
+++ b/tools/c7n_azure/c7n_azure/constants.py
@@ -26,6 +26,8 @@
 
 ENV_ACCESS_TOKEN = 'AZURE_ACCESS_TOKEN'
 
+ENV_USE_MSI = 'AZURE_USE_MSI'
+
 ENV_FUNCTION_TENANT_ID = 'AZURE_FUNCTION_TENANT_ID'
 ENV_FUNCTION_CLIENT_ID = 'AZURE_FUNCTION_CLIENT_ID'
 ENV_FUNCTION_SUB_ID = 'AZURE_FUNCTION_SUBSCRIPTION_ID'
diff --git a/tools/c7n_azure/c7n_azure/session.py b/tools/c7n_azure/c7n_azure/session.py
--- a/tools/c7n_azure/c7n_azure/session.py
+++ b/tools/c7n_azure/c7n_azure/session.py
@@ -21,8 +21,9 @@
 from azure.common.credentials import (BasicTokenAuthentication,
                                       ServicePrincipalCredentials)
 
+from msrestazure.azure_active_directory import MSIAuthentication
+
 from c7n_azure import constants
-from c7n_azure.constants import RESOURCE_ACTIVE_DIRECTORY
 from c7n_azure.utils import ResourceIdParser, StringUtils
 
 try:
@@ -34,10 +35,11 @@
 class Session(object):
 
     def __init__(self, subscription_id=None, authorization_file=None,
-                 resource=RESOURCE_ACTIVE_DIRECTORY):
+                 resource=constants.RESOURCE_ACTIVE_DIRECTORY):
         """
         :param subscription_id: If provided overrides environment variables.
-
+        :param authorization_file: Path to file populated from 'get_functions_auth_string'
+        :param resource: Resource endpoint for OAuth token.
         """
 
         self.log = logging.getLogger('custodian.azure.session')
@@ -75,6 +77,10 @@ def _initialize_session(self):
             constants.ENV_ACCESS_TOKEN, constants.ENV_SUB_ID
         ]
 
+        msi_auth_variables = [
+            constants.ENV_USE_MSI, constants.ENV_SUB_ID
+        ]
+
         if self.authorization_file:
             self.credentials, self.subscription_id = self.load_auth_file(self.authorization_file)
             self.log.info("Creating session with authorization file")
@@ -100,6 +106,18 @@ def _initialize_session(self):
             self.tenant_id = os.environ[constants.ENV_TENANT_ID]
             self.log.info("Creating session with Service Principal Authentication")
 
+        elif all(k in os.environ for k in msi_auth_variables):
+            # MSI authentication
+            if constants.ENV_CLIENT_ID in os.environ:
+                self.credentials = MSIAuthentication(
+                    client_id=os.environ[constants.ENV_CLIENT_ID],
+                    resource=self.resource_namespace)
+            else:
+                self.credentials = MSIAuthentication(
+                    resource=self.resource_namespace)
+
+            self.subscription_id = os.environ[constants.ENV_SUB_ID]
+            self.log.info("Creating session with MSI Authentication")
         else:
             # Azure CLI authentication
             self._is_cli_auth = True


Test patch:
diff --git a/tools/c7n_azure/tests/test_session.py b/tools/c7n_azure/tests/test_session.py
--- a/tools/c7n_azure/tests/test_session.py
+++ b/tools/c7n_azure/tests/test_session.py
@@ -18,6 +18,7 @@
 import re
 
 from azure.common.credentials import ServicePrincipalCredentials, BasicTokenAuthentication
+from msrestazure.azure_active_directory import MSIAuthentication
 from azure_common import BaseTest
 from c7n_azure import constants
 from c7n_azure.session import Session
@@ -47,6 +48,33 @@ def test_initialize_session_principal(self):
                 self.assertIs(type(s.get_credentials()), ServicePrincipalCredentials)
                 self.assertEqual(s.get_subscription_id(), 'ea42f556-5106-4743-99b0-c129bfa71a47')
 
+    def test_initialize_msi_auth_system(self):
+        with patch('msrestazure.azure_active_directory.MSIAuthentication.__init__',
+                   autospec=True, return_value=None):
+            with patch.dict(os.environ,
+                            {
+                                constants.ENV_USE_MSI: 'true',
+                                constants.ENV_SUB_ID: 'ea42f556-5106-4743-99b0-c129bfa71a47'
+                            }, clear=True):
+                s = Session()
+
+                self.assertIs(type(s.get_credentials()), MSIAuthentication)
+                self.assertEqual(s.get_subscription_id(), 'ea42f556-5106-4743-99b0-c129bfa71a47')
+
+    def test_initialize_msi_auth_user(self):
+        with patch('msrestazure.azure_active_directory.MSIAuthentication.__init__',
+                   autospec=True, return_value=None):
+            with patch.dict(os.environ,
+                            {
+                                constants.ENV_USE_MSI: 'true',
+                                constants.ENV_SUB_ID: 'ea42f556-5106-4743-99b0-c129bfa71a47',
+                                constants.ENV_CLIENT_ID: 'client'
+                            }, clear=True):
+                s = Session()
+
+                self.assertIs(type(s.get_credentials()), MSIAuthentication)
+                self.assertEqual(s.get_subscription_id(), 'ea42f556-5106-4743-99b0-c129bfa71a47')
+
     def test_initialize_session_token(self):
         with patch.dict(os.environ,
                         {