Skip to content
This repository has been archived by the owner on Apr 10, 2024. It is now read-only.

support local creds probing #119

Closed
wants to merge 15 commits into from
Closed

Conversation

yugangw-msft
Copy link
Contributor

@yugangw-msft yugangw-msft commented Oct 23, 2018

For #118.
The probe sequence:

  1. Manager service identity
    only "system assigned identity" is probed, as user assigned identity would require extra arguments, and under that context users are better off using strong type credentials
  2. Environment variable AZURE_CONNECTION_STRING with sdk auth file content in json, which is proposed in the design review meeting in June.
    e.g. {AZURE_CLIENT_ID='...', AZURE_CLIENT_SECRET='...', AZURE_TENANT_ID='...', AZURE_CLOUD_NAME='...'}
    AZURE_CLOUD_NAME is optional. We default to public cloud
  3. Individual environment variables for a service principal
    Like 2, rather using individual environment variables
  4. Azure Authentication File (originating from java sdk)
  5. Azure CLI credentials

Subscription id will be optional as the chainer will find them
Per suggestion, the example code includes both management plane and data plane client codes

@yugangw-msft
Copy link
Contributor Author

//cc: @lmazuel @jianghaolu

@yugangw-msft
Copy link
Contributor Author

program_files_folder = os.environ.get('ProgramFiles(x86)') or os.environ.get('ProgramFiles')
probing_paths = [os.path.join(program_files_folder, 'Microsoft SDKs', 'Azure', 'CLI2', 'wbin', 'az.cmd')]
else:
probing_paths = ['/usr/bin/az', '/usr/local/bin/az']
Copy link
Member

@lmazuel lmazuel Oct 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work if CLI was installed by pip. You could subprocess "which az", or use "shutil.which('az')" starting Python 3 (probably the right approach is "shutil.which('az')" on Python 3 and fallback to subprocess otherwise).
https://docs.python.org/3/library/shutil.html?highlight=.which#shutil.which

Copy link
Member

@johanste johanste Oct 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the presence of a ${HOME}/.azure directory as the hint to try the CLI creds?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...or, alternatively, use the ${PATH} environment variable...


def signed_session(self, session=None):
# Token cache is handled by the VM extension, call each time to avoid expiration
self.set_token()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should, the CLI be called each request to refresh the token?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you meant sub-shelling is kind of slow? Yes, I can optimize by leveraging the expiresOn information returned from the get-access-token command

from msrestazure.azure_local_creds_prober import AzureLocalCredentialProber

prober = AzureLocalCredentialProber()
client = StorageManagementClient(prober, prober.subscription_id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to use the factory style I have right now:

from azure.common.client_factory import get_client_from_cli_profile
from azure.mgmt.compute import ComputeManagementClient

client = get_client_from_cli_profile(ComputeManagementClient)

but something more generic like:

from azure.common.client_factory import get_client
from azure.mgmt.compute import ComputeManagementClient

client = get_client(ComputeManagementClient)

class AzureLocalCredentialProber(object):
'''
Probing logics:
1. Managed service identity
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The progression should be from more specific to less specific. This is the way I'd define the specificity of the different sources of information:

Environment variables > local file > local endpoint available. 

Following this, does this: 2, 3, 1, 4 make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started with the most secure option, then follow up with SDK home made ones. But you have a point, so I will adjust. Also it is unlikely people would set env variable in a VM with identity

@joshgav
Copy link

joshgav commented Oct 23, 2018

Explicit environment variables should come before managed identity like @johanste says cause we should prefer explicit intent. That's what the specs say too :).

Rather than a global "Prober", could each credentials type include a static Probe() method which returns a boolean? That would grant more flexibility to the provider and allow us to add new providers and probes in the future.

Can we document the names of keys in the connection string and names of environment variables? We should use the same names in all SDKs.

@joshgav
Copy link

joshgav commented Oct 23, 2018

We will need to get tokens for resources other than AzureRM, e.g. Storage and Key Vault. Can the constructor be parameterized to take a resource URL and default to "https://management.azure.com"?

@yugangw-msft
Copy link
Contributor Author

We will need to get tokens for resources other than AzureRM, e.g. Storage and Key Vault

Each specific SDK has the resource uri defined inside, e.g. go-sdk , so this is not a concern.
Since we are at it, let us see how to get sovereign clouds work. In Python's autorest runtime, those endpoints are pre-defined as a set of instances. I believe Java does too (cc: @jianghaolu). If other language SDKs have the similar supports, then the probing can default to public azure while client app can override with an enum.

@yugangw-msft
Copy link
Contributor Author

Hi folks, i have addressed all feedback, please take another look.
The sample code wrapped in the unit test works for both storage management plane and data plane. Note the new storage data plan involves a highly unique requirement on client applications on how to supply the access token. I will cross check with my CLI colleague for whether or not I am doing the most optimal way.
The method now exposes the resource keyword arguments with the defaulting value on arm.

def probe(self):
creds = None
if self.enabled and os.environ.get('AZURE_CONNECTION_STRING'):
auth_info = json.loads(os.environ.get('AZURE_CONNECTION_STRING'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the connection string JSON valid? I thought it was something like:
AZURE_CONNECTION_STRING="client_id=1234;client_secret=2345;tenant_id=3456;subscription_id=4567;"

return None

def probe_subscription(self, creds):
return json.loads(os.environ.get('AZURE_CONNECTION_STRING'))['subscriptionId']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question about JSON, and I thought too it should be subscription_id

else:
cloud_environment = AZURE_PUBLIC_CLOUD
resource = self.resource or cloud_environment.endpoints.management
creds = ServicePrincipalCredentials(client_id=auth_info['clientId'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client_id?

try:
creds = MSIAuthentication()
# MSI is not yet supported in sovereigns
setattr(creds, 'cloud_environment', AZURE_PUBLIC_CLOUD)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1/ PUBLIC AZURE is the default anyway 2/ we read cloud_environemnent from the kwargs, so should be passed to the init if we want to

creds = MSIAuthentication()
# MSI is not yet supported in sovereigns
setattr(creds, 'cloud_environment', AZURE_PUBLIC_CLOUD)
creds.resource = self.resource or AZURE_PUBLIC_CLOUD.endpoints.management
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently default is:
self.resource = kwargs.get('resource', self.cloud_environment.endpoints.active_directory_resource_id)

Is it wrong?

def probe_subscription(self, creds): # pylint: disable=no-self-use
subscription_id = None
try:
from azure.mgmt.resource.subscriptions import SubscriptionClient
Copy link
Member

@lmazuel lmazuel Nov 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to assume resource client is available. Especially the code here will use latest API version, which is a bad idea for stack.
I would prefer to hard code this one with a requests call, like I did for auto-registration of RP

_LOGGER.warning('You also have accesses to a few other subscriptions "%S".'
' You can supply subscription_id on invoking probers')
except ImportError: # should be rare
_LOGGER.warning('Failed to load azure.mgmt.resource.subscriptions to find the default'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This problem will disapear with a direct call to azure using requests

else:
cloud_environment = AZURE_PUBLIC_CLOUD

resource = self.resource or cloud_environment.endpoints.management
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now I use
resource = self._cloud_environment.endpoints.active_directory_resource_id

Is it wrong?

process = Popen(['which', 'az'], stdout=PIPE, stderr=PIPE)
stdout, stderr = process.communicate()
process.wait()
if not stderr:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If stderr, log it as warning

_LOGGER.warning('More than one Azure CLI are installed at "%s"'
' Pick the 1st one.', ', '.join(installed_clis))

if not cli_path:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should provide a cli_path optional kwargs to cover weird scenario

Detect CLI installations
'''
def probe(self): # pylint: disable=no-self-use
uname = platform.uname()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would take a different approach:

  • Let try to call "az"
  • If it fails, try to find it and retry

It's more Pythonic in the design (EAFP), and if "az" is in the Path it might work out of the box anyway.

a. app service
b. virtual machine
4. Azure CLI, through "az account get-access-token"
'''
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def get_creds_through_local_probing(**kwargs):
'''
Probing logics:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have the concept of critical failure? If we recognize some env variables and something is wrong, is this enough to fail completly and not fallback?
@johanste @joshgav

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

@lmazuel
Copy link
Member

lmazuel commented Sep 16, 2019

Will be done in azure-identity but not msrestazure

@lmazuel lmazuel closed this Sep 16, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants