Skip to content

Commit

Permalink
Merge pull request #4473 from wiktork/dev/wiktork/workloadId7.2
Browse files Browse the repository at this point in the history
[release/7.x] Port workload identity auth for Azure egress
  • Loading branch information
wiktork authored May 25, 2023
2 parents ba819f3 + cc81e59 commit 03556ba
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 31 deletions.
7 changes: 7 additions & 0 deletions documentation/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,13 @@
],
"description": "Client id of the Managed Identity used for authentication. The identity must have permissions to create containers and write to blob storage."
},
"UseWorkloadIdentityFromEnvironment": {
"type": [
"boolean",
"null"
],
"description": "Uses WorkloadIdentity for authentication. The environment variables AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE, and AZURE_AUTHORITY_HOST are used to authenticate."
},
"ContainerName": {
"type": "string",
"description": "The name of the container to which the blob will be egressed. If egressing to the root container, use the \"$root\" sentinel value.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ IEnumerable<ValidationResult> IValidatableObject.Validate(ValidationContext vali
IList<ValidationResult> results = new List<ValidationResult>();

// One of the authentication keys/tokens is required
if (string.IsNullOrEmpty(AccountKey) && string.IsNullOrEmpty(SharedAccessSignature) && string.IsNullOrEmpty(ManagedIdentityClientId))
if (string.IsNullOrEmpty(AccountKey) && string.IsNullOrEmpty(SharedAccessSignature) && string.IsNullOrEmpty(ManagedIdentityClientId) && !(UseWorkloadIdentityFromEnvironment == true))
{
results.Add(
new ValidationResult(
string.Format(
OptionsDisplayStrings.ErrorMessage_CredentialsMissing,
nameof(AccountKey),
nameof(SharedAccessSignature),
nameof(ManagedIdentityClientId))));
nameof(ManagedIdentityClientId),
nameof(UseWorkloadIdentityFromEnvironment))));
}

return results;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ internal sealed partial class AzureBlobEgressProviderOptions :
Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_AzureBlobEgressProviderOptions_ManagedIdentityClientId))]
public string ManagedIdentityClientId { get; set; }

[Display(
ResourceType = typeof(OptionsDisplayStrings),
Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_AzureBlobEgressProviderOptions_UseWorkloadIdentityFromEnvironment))]
public bool? UseWorkloadIdentityFromEnvironment { get; set; }

[Display(
ResourceType = typeof(OptionsDisplayStrings),
Description = nameof(OptionsDisplayStrings.DisplayAttributeDescription_AzureBlobEgressProviderOptions_ContainerName))]
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -530,12 +530,13 @@
<comment>The description provided for the DumpTempFolder parameter on StorageOptions.</comment>
</data>
<data name="ErrorMessage_CredentialsMissing" xml:space="preserve">
<value>The {0} field, {1} field, or {2} field is required.</value>
<comment>Gets the format string for rejecting validation due to 3 missing fields where at least one is required.
3 Format Parameters:
<value>The {0} field, {1} field, {2} field, or {3} field is required.</value>
<comment>Gets the format string for rejecting validation due to 4 missing fields where at least one is required.
4 Format Parameters:
0. fieldNameOne: The name of the first field that is missing
1. fieldNameTwo: The name of the second field that is missing
2. fieldNameThree: The name of the third field that is missing</comment>
2. fieldNameThree: The name of the third field that is missing
3. fieldNameFour: The name of the fourth field that is missing</comment>
</data>
<data name="DisplayAttributeDescription_CollectLogsOptions_Format" xml:space="preserve">
<value>The format of the logs artifact.</value>
Expand Down Expand Up @@ -804,4 +805,7 @@
<data name="DisplayAttributeDescription_MetricsOptions_Meters" xml:space="preserve">
<value>Names of meters to collect from the System.Diagnostics.Metrics provider.</value>
</data>
<data name="DisplayAttributeDescription_AzureBlobEgressProviderOptions_UseWorkloadIdentityFromEnvironment" xml:space="preserve">
<value>Uses WorkloadIdentity for authentication. The environment variables AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE, and AZURE_AUTHORITY_HOST are used to authenticate.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Azure;
using Azure.Identity;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Azure.Storage.Queues;
Expand Down Expand Up @@ -251,6 +252,36 @@ public async Task AzureBlobEgress_DoesNotThrowWhen_QueueDoesNotExistAndUsingRest
Assert.Empty(messages);
}

[ConditionalFact(Timeout = TestTimeouts.EgressUnitTestTimeoutMs)]
public void AzureBlobEgress_DefaultCredentials()
{
// These are auth methods we know about. If this test fails, it means a new auth method was added and we need to update the test.

var expectedAuthExclusions = new HashSet<string>
{
nameof(DefaultAzureCredentialOptions.ExcludeEnvironmentCredential),
nameof(DefaultAzureCredentialOptions.ExcludeWorkloadIdentityCredential),
nameof(DefaultAzureCredentialOptions.ExcludeManagedIdentityCredential),
nameof(DefaultAzureCredentialOptions.ExcludeAzureDeveloperCliCredential),
nameof(DefaultAzureCredentialOptions.ExcludeSharedTokenCacheCredential),
nameof(DefaultAzureCredentialOptions.ExcludeInteractiveBrowserCredential),
nameof(DefaultAzureCredentialOptions.ExcludeAzureCliCredential),
nameof(DefaultAzureCredentialOptions.ExcludeVisualStudioCredential),
nameof(DefaultAzureCredentialOptions.ExcludeVisualStudioCodeCredential),
nameof(DefaultAzureCredentialOptions.ExcludeAzurePowerShellCredential),
};

var actualAuthExclusions = typeof(DefaultAzureCredentialOptions).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
.Select(p => p.Name)
.Where(p => p.StartsWith("Exclude")).ToHashSet();

IEnumerable<string> unexpectedExclusions = actualAuthExclusions.Except(expectedAuthExclusions);
Assert.True(!unexpectedExclusions.Any(), $"New authentication mechanism was added to {nameof(DefaultAzureCredentialOptions)}. " +
$"Ensure that {nameof(AzureBlobEgressProvider)} initializes its {nameof(DefaultAzureCredentialOptions)} to exclude unexpected authentication types. " +
$"Possible new exclusions: {string.Join(',', unexpectedExclusions)}");

}

private Task<Stream> ProvideUploadStreamAsync(CancellationToken token)
{
return Task.FromResult<Stream>(new FileStream(_testUploadFile, FileMode.Open, FileAccess.Read));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Azure;
using Azure.Core;
using Azure.Identity;
using Azure.Storage;
using Azure.Storage.Blobs;
Expand Down Expand Up @@ -292,12 +293,12 @@ private static async Task<QueueClient> GetQueueClientAsync(AzureBlobEgressProvid

serviceClient = new QueueServiceClient(accountUri, credential, clientOptions);
}
else if (!string.IsNullOrEmpty(options.ManagedIdentityClientId))
else if (UseDefaultCredentials(options))
{
// Remove Query in case SAS token was specified
Uri accountUri = GetQueueAccountUri(options, out _);

DefaultAzureCredential credential = CreateManagedIdentityCredentials(options.ManagedIdentityClientId);
TokenCredential credential = CreateDefaultCredential(options);

serviceClient = new QueueServiceClient(accountUri, credential, clientOptions);
}
Expand Down Expand Up @@ -346,12 +347,12 @@ private static async Task<BlobContainerClient> GetBlobContainerClientAsync(Azure

serviceClient = new BlobServiceClient(accountUri, credential);
}
else if (!string.IsNullOrEmpty(options.ManagedIdentityClientId))
else if (UseDefaultCredentials(options))
{
// Remove Query in case SAS token was specified
Uri accountUri = GetBlobAccountUri(options, out _);

DefaultAzureCredential credential = CreateManagedIdentityCredentials(options.ManagedIdentityClientId);
TokenCredential credential = CreateDefaultCredential(options);

serviceClient = new BlobServiceClient(accountUri, credential);
}
Expand Down Expand Up @@ -427,22 +428,39 @@ private static string WrapMessage(string innerMessage)
}
}

private static DefaultAzureCredential CreateManagedIdentityCredentials(string clientId)
private static bool UseDefaultCredentials(AzureBlobEgressProviderOptions options) =>
!string.IsNullOrEmpty(options.ManagedIdentityClientId) || options.UseWorkloadIdentityFromEnvironment == true;

private static TokenCredential CreateDefaultCredential(AzureBlobEgressProviderOptions options)
{
var credential = new DefaultAzureCredential(
new DefaultAzureCredentialOptions
{
ManagedIdentityClientId = clientId,
ExcludeAzureCliCredential = true,
ExcludeAzurePowerShellCredential = true,
ExcludeEnvironmentCredential = true,
ExcludeInteractiveBrowserCredential = true,
ExcludeSharedTokenCacheCredential = true,
ExcludeVisualStudioCodeCredential = true,
ExcludeVisualStudioCredential = true
});

return credential;
DefaultAzureCredentialOptions credOptions = GetDefaultCredentialOptions();

if (options.UseWorkloadIdentityFromEnvironment == true)
{
credOptions.ExcludeWorkloadIdentityCredential = false;
}

if (!string.IsNullOrEmpty(options.ManagedIdentityClientId))
{
credOptions.ExcludeManagedIdentityCredential = false;
credOptions.ManagedIdentityClientId = options.ManagedIdentityClientId;
}

return new DefaultAzureCredential(credOptions);
}

private static DefaultAzureCredentialOptions GetDefaultCredentialOptions() =>
new DefaultAzureCredentialOptions
{
ExcludeAzureCliCredential = true,
ExcludeManagedIdentityCredential = true,
ExcludeWorkloadIdentityCredential= true,
ExcludeAzurePowerShellCredential = true,
ExcludeEnvironmentCredential = true,
ExcludeInteractiveBrowserCredential = true,
ExcludeSharedTokenCacheCredential = true,
ExcludeVisualStudioCodeCredential = true,
ExcludeVisualStudioCredential = true,
};
}
}
2 changes: 1 addition & 1 deletion src/Tools/dotnet-monitor/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions src/Tools/dotnet-monitor/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,8 @@
<comment>Gets a string similar to "File system egress failed.".</comment>
</data>
<data name="ErrorMessage_EgressMissingCredentials" xml:space="preserve">
<value>SharedAccessSignature, AccountKey, or ManagedIdentityClientId must be specified.</value>
<comment>Gets a string similar to "SharedAccessSignature, AccountKey, or ManagedIdentityClientId must be specified.".</comment>
</data>
<value>SharedAccessSignature, AccountKey, ManagedIdentityClientId, or UseWorkloadIdentityFromEnvironment must be specified.</value>
<comment>Gets a string similar to "SharedAccessSignature, AccountKey, ManagedIdentityClientId, or UseWorkflowIdentity must be specified.".</comment> </data>
<data name="ErrorMessage_EgressProviderDoesNotExist" xml:space="preserve">
<value>Egress provider '{0}' does not exist.</value>
<comment>Gets the format string for egress provider failure due to missing provider.
Expand Down Expand Up @@ -811,4 +810,4 @@
<value>:REDACTED:</value>
<comment>Gets a string similar to ":REDACTED:".</comment>
</data>
</root>
</root>

0 comments on commit 03556ba

Please sign in to comment.