-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Azure.Identity adding msi implementation for AppService and CloudShell #6699
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
daviwil
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
| private SemaphoreSlim _initLock = new SemaphoreSlim(1, 1); | ||
|
|
||
|
|
||
| private readonly Uri ImdsEndptoint = new Uri("http://169.254.169.254/metadata/identity/oauth2/token"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two constants are probably not needed now that they're in ManagedIdentityClient
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
| // if ONLY the env var MSI_ENDPOINT is set the MsiType is CloudShell | ||
| else | ||
| { | ||
| s_msiType = MsiType.AppService; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MsiType.CloudShell?
| // if ONLY the env var MSI_ENDPOINT is set the MsiType is CloudShell | ||
| else | ||
| { | ||
| s_msiType = MsiType.AppService; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MsiType.CloudShell?
|
|
||
| request.UriBuilder.Uri = ImdsEndpoint; | ||
|
|
||
| request.UriBuilder.AppendQuery("api-version", ImdsApiVersion); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's okay if we get a 400 due to no query string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like specifying the API version is good. Possibly future versions won't require the Metadata header. I know that this is unlikely but adding the api-version is very low cost.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I'll add it in the Python library as well.
|
|
||
| request.Method = HttpPipelineMethod.Get; | ||
|
|
||
| request.Headers.Add("secret", s_secret); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docs claim the secret rotates. I don't know whether that requires an application restart so I get the value from the environment for each request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch. updated
| private Request CreateAppServiceAuthRequest(string[] scopes, string clientId) | ||
| { | ||
| // covert the scopes to a resource string | ||
| string resource = ScopeUtilities.ScopesToResource(scopes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the managed identity endpoints require one scope per request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ScopesToResource enforces only one scope is specified. I left all the signatures as a string[] to keep it uniform with the outermost api.
public static string ScopesToResource(string[] scopes)
{
if (scopes == null) throw new ArgumentNullException(nameof(scopes));
if (scopes.Length != 1) throw new ArgumentException("To convert to a resource string the specified array must be exactly length 1", nameof(scopes));
KrzysztofCwalina
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, it looks good. The main top level comment I would have is that the code could use more comments :-)
| { | ||
| var response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| if (response.Status == 200 || response.Status == 201) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ClientOptions has a property called ResponseClassifier. Should it be used here to determine if the code is success? I assume you don't want to do it, but then why do we have ResponseClassifier on options?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The behavior of this client and AadIdentityClient differ in what codes are failure, and what codes should be retried. Currently we're using the IdentityClientOptions class for both clients but this needs to be changed. I've added issue #6711 to track this.
| return result; | ||
| } | ||
|
|
||
| throw response.CreateRequestFailedException(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add some custom message here? i.e. is there some action we could recommend users take when this fails? The default message just shows the the response line and headers (not even the body, I think).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add an exception type so that these failures can be easily distinguished from service client failures, as both would originate from the same user call to a service client method. I've added issue #6712 to track this.
| } | ||
| } | ||
|
|
||
| return new AccessToken(accessToken, expiresOn); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens id access_token does not exist in the JSON payload? i.e. is it ok to pass null here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the access_token to not exist here then the Token property on AccessToken will be null and this will be handled higher up in the stack. So while it didn't get a token, it should not throw an exception from here.
| { | ||
| if (cancellationToken != default) | ||
| { | ||
| Task.Delay(1000, cancellationToken).GetAwaiter().GetResult(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub, is this reliable? BTW, we are doing it as Thread.Sleep does not have an overload that takes a cancellation token.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's "reliable" in that it'll sleep for at least a second. It is effectively sync-over-async, though, in that this will block the current thread until the async operation completes, which necessitates a thread pool thread briefly, so if this were production code and this was running on a thread pool thread, I'd be more concerned.
Thread.Sleep doesn't take a cancellation token as it just maps to the OS' thread sleep functionality, e.g. ::Sleep(milliseconds) on Windows, where there's no notion of cancellation. We could of course add another cancelable overload in the future, at which point we'd not just use the OS functionality but isntead do something like the below...
If this were production code, and you wanted to synchronously block the current thread for X milliseconds or until a cancellation request occurred, I'd probably instead advocate for something more like:
private static readonly ManualResetEventSlim s_sleeper = new ManualResetEventSlim();
...
s_sleeper.Wait(1000, cancellationToken);but for test code, what you have is fine.
| internal class AadClient | ||
| { | ||
| private static Lazy<IdentityClient> s_sharedClient = new Lazy<IdentityClient>(() => new IdentityClient()); | ||
| private static Lazy<AadClient> s_sharedClient = new Lazy<AadClient>(() => new AadClient()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any scenarios under which s_sharedClient is rendered unusable? E.g. a fatal error that it is not possible to recover from without recreating the client instance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The client is mostly stateless. The only errors which would render the client useless would be through miss configuration of the process, which would mean the s_sharedClient would never be usable and error on all calls.
| { | ||
| var response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| if (response.Status == 200 || response.Status == 201) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of curiosity, under what circumstances would we get a 201 response?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic needs to be moved into the ClientOptions. I've added issue #6711 to track this.
| { | ||
| s_endpoint = new Uri(endpointEnvVar); | ||
|
|
||
| // if BOTH the env vars MSI_ENDPOINT and MSI_SECRET are set the MsiType is AppService |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What environment variables do aks/aci/acs use?
| // this indicates that the request timed out and that imds is not available. Otherwise the operation | ||
| // was user cancelled. In this case we don't wan't to handle the exception so s_identityAvailable will | ||
| // remain unset, as we still haven't determined if the imds endpoint is available. | ||
| catch (OperationCanceledException ex) when (ex.CancellationToken == imdsTimeout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you check cancellationToken.IsCancellationRequested?
No description provided.