Skip to content

Commit

Permalink
feat: add credential_type plugin config (#157)
Browse files Browse the repository at this point in the history
Feat:
- added creential_type plugin config key
- supported credential type: default, environment, managedidentity,
azurecli

Test:
- unit test cases
- e2e test cases
- tested environment credential, workload identity credential, managed
identity in pod of AKS
- tested Azure cli credential locally

Resolves #146 #154 
Signed-off-by: Junjie Gao <[email protected]>

---------

Signed-off-by: Junjie Gao <[email protected]>
  • Loading branch information
JeyJeyGao authored Apr 11, 2024
1 parent b838f3d commit 53bdb04
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 25 deletions.
52 changes: 52 additions & 0 deletions Notation.Plugin.AzureKeyVault.Tests/KeyVault/CredentialsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Xunit;
using Azure.Core;
using System.Collections.Generic;
using Notation.Plugin.Protocol;

namespace Notation.Plugin.AzureKeyVault.Credential.Tests
{
public class CredentialsTests
{
[Theory]
[InlineData("default")]
[InlineData("environment")]
[InlineData("workloadid")]
[InlineData("managedid")]
[InlineData("azurecli")]
public void GetCredentials_WithValidCredentialType_ReturnsExpectedCredential(string credentialType)
{
// Act
var result = Credentials.GetCredentials(credentialType);

// Assert
Assert.IsAssignableFrom<TokenCredential>(result);
}

[Fact]
public void GetCredentials_WithInvalidCredentialType_ThrowsValidationException()
{
// Arrange
string invalidCredentialType = "invalid";

// Act & Assert
var ex = Assert.Throws<ValidationException>(() => Credentials.GetCredentials(invalidCredentialType));
Assert.Equal($"Invalid credential key: {invalidCredentialType}", ex.Message);
}

[Fact]
public void GetCredentials_WithPluginConfig_ReturnsExpectedCredential()
{
// Arrange
var pluginConfig = new Dictionary<string, string>
{
{ "credential_type", "default" }
};

// Act
var result = Credentials.GetCredentials(pluginConfig);

// Assert
Assert.IsAssignableFrom<TokenCredential>(result);
}
}
}
27 changes: 14 additions & 13 deletions Notation.Plugin.AzureKeyVault.Tests/KeyVault/KeyVaultClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Azure.Security.KeyVault.Keys.Cryptography;
using Azure.Security.KeyVault.Secrets;
using Moq;
using Notation.Plugin.AzureKeyVault.Credential;
using Notation.Plugin.Protocol;
using Xunit;

Expand All @@ -23,7 +24,7 @@ public void TestConstructorWithKeyId()
{
string keyId = "https://myvault.vault.azure.net/keys/my-key/123";

KeyVaultClient keyVaultClient = new KeyVaultClient(keyId);
KeyVaultClient keyVaultClient = new KeyVaultClient(keyId, Credentials.GetCredentials("default"));

Assert.Equal("my-key", keyVaultClient.Name);
Assert.Equal("123", keyVaultClient.Version);
Expand All @@ -37,7 +38,7 @@ public void TestConstructorWithKeyVaultUrlNameVersion()
string name = "my-key";
string version = "123";

KeyVaultClient keyVaultClient = new KeyVaultClient(keyVaultUrl, name, version);
KeyVaultClient keyVaultClient = new KeyVaultClient(keyVaultUrl, name, version, Credentials.GetCredentials("default"));

Assert.Equal(name, keyVaultClient.Name);
Assert.Equal(version, keyVaultClient.Version);
Expand All @@ -51,32 +52,32 @@ public void TestConstructorWithKeyVaultUrlNameVersion()
[InlineData("http://myvault.vault.azure.net/keys/my-key/123")]
public void TestConstructorWithInvalidKeyId(string invalidKeyId)
{
Assert.Throws<ValidationException>(() => new KeyVaultClient(invalidKeyId));
Assert.Throws<ValidationException>(() => new KeyVaultClient(invalidKeyId, Credentials.GetCredentials("default")));
}

[Theory]
[InlineData("")]
public void TestConstructorWithEmptyKeyId(string invalidKeyId)
{
Assert.Throws<ArgumentNullException>(() => new KeyVaultClient(invalidKeyId));
Assert.Throws<ArgumentNullException>(() => new KeyVaultClient(invalidKeyId, Credentials.GetCredentials("default")));
}

private class TestableKeyVaultClient : KeyVaultClient
{
public TestableKeyVaultClient(string keyVaultUrl, string name, string version, CryptographyClient cryptoClient)
: base(keyVaultUrl, name, version)
public TestableKeyVaultClient(string keyVaultUrl, string name, string version, CryptographyClient cryptoClient, TokenCredential credenital)
: base(keyVaultUrl, name, version, credenital)
{
this._cryptoClient = new Lazy<CryptographyClient>(() => cryptoClient);
}

public TestableKeyVaultClient(string keyVaultUrl, string name, string version, CertificateClient certificateClient)
: base(keyVaultUrl, name, version)
public TestableKeyVaultClient(string keyVaultUrl, string name, string version, CertificateClient certificateClient, TokenCredential credenital)
: base(keyVaultUrl, name, version, credenital)
{
this._certificateClient = new Lazy<CertificateClient>(() => certificateClient);
}

public TestableKeyVaultClient(string keyVaultUrl, string name, string version, SecretClient secretClient)
: base(keyVaultUrl, name, version)
public TestableKeyVaultClient(string keyVaultUrl, string name, string version, SecretClient secretClient, TokenCredential credenital)
: base(keyVaultUrl, name, version, credenital)
{
this._secretClient = new Lazy<SecretClient>(() => secretClient);
}
Expand All @@ -88,7 +89,7 @@ private TestableKeyVaultClient CreateMockedKeyVaultClient(SignResult signResult)
mockCryptoClient.Setup(c => c.SignDataAsync(It.IsAny<SignatureAlgorithm>(), It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(signResult);

return new TestableKeyVaultClient("https://fake.vault.azure.net", "fake-key", "123", mockCryptoClient.Object);
return new TestableKeyVaultClient("https://fake.vault.azure.net", "fake-key", "123", mockCryptoClient.Object, Credentials.GetCredentials("default"));
}

private TestableKeyVaultClient CreateMockedKeyVaultClient(KeyVaultCertificate certificate)
Expand All @@ -97,15 +98,15 @@ private TestableKeyVaultClient CreateMockedKeyVaultClient(KeyVaultCertificate ce
mockCertificateClient.Setup(c => c.GetCertificateVersionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Response.FromValue(certificate, new Mock<Response>().Object));

return new TestableKeyVaultClient("https://fake.vault.azure.net", "fake-certificate", "123", mockCertificateClient.Object);
return new TestableKeyVaultClient("https://fake.vault.azure.net", "fake-certificate", "123", mockCertificateClient.Object, Credentials.GetCredentials("default"));
}

private TestableKeyVaultClient CreateMockedKeyVaultClient(KeyVaultSecret secret)
{
var mockSecretClient = new Mock<SecretClient>(new Uri("https://fake.vault.azure.net/secrets/fake-secret/123"), new Mock<TokenCredential>().Object);
mockSecretClient.Setup(c => c.GetSecretAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Response.FromValue(secret, new Mock<Response>().Object));
return new TestableKeyVaultClient("https://fake.vault.azure.net", "fake-certificate", "123", mockSecretClient.Object);
return new TestableKeyVaultClient("https://fake.vault.azure.net", "fake-certificate", "123", mockSecretClient.Object, Credentials.GetCredentials("default"));
}

[Fact]
Expand Down
5 changes: 4 additions & 1 deletion Notation.Plugin.AzureKeyVault/Command/DescribeKey.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json;
using Notation.Plugin.AzureKeyVault.Client;
using Notation.Plugin.AzureKeyVault.Credential;
using Notation.Plugin.Protocol;

namespace Notation.Plugin.AzureKeyVault.Command
Expand All @@ -25,7 +26,9 @@ public DescribeKey(string inputJson)
throw new ValidationException(invalidInputError);
}
this._request = request;
this._keyVaultClient = new KeyVaultClient(request.KeyId);
this._keyVaultClient = new KeyVaultClient(
id: request.KeyId,
credential: Credentials.GetCredentials(request.PluginConfig));
}

/// <summary>
Expand Down
5 changes: 4 additions & 1 deletion Notation.Plugin.AzureKeyVault/Command/GenerateSignature.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using Notation.Plugin.AzureKeyVault.Credential;
using Notation.Plugin.AzureKeyVault.Certificate;
using Notation.Plugin.AzureKeyVault.Client;
using Notation.Plugin.Protocol;
Expand All @@ -26,7 +27,9 @@ public GenerateSignature(string inputJson)
throw new ValidationException("Invalid input");
}
this._request = request;
this._keyVaultClient = new KeyVaultClient(request.KeyId);
this._keyVaultClient = new KeyVaultClient(
id: request.KeyId,
credential: Credentials.GetCredentials(request.PluginConfig));
}

/// <summary>
Expand Down
67 changes: 67 additions & 0 deletions Notation.Plugin.AzureKeyVault/KeyVault/Credentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using Azure.Core;
using Azure.Identity;
using Notation.Plugin.Protocol;

namespace Notation.Plugin.AzureKeyVault.Credential
{
public class Credentials
{
/// <summary>
/// Credential type key name in plugin config.
/// </summary>
public const string CredentialTypeKey = "credential_type";
/// <summary>
/// Default credential name.
/// </summary>
public const string DefaultCredentialName = "default";
/// <summary>
/// Environment credential name.
/// </summary>
public const string EnvironmentCredentialName = "environment";
/// <summary>
/// Workload identity credential name.
/// </summary>
public const string WorkloadIdentityCredentialName = "workloadid";
/// <summary>
/// Managed identity credential name.
/// </summary>
public const string ManagedIdentityCredentialName = "managedid";
/// <summary>
/// Azure CLI credential name.
/// </summary>
public const string AzureCliCredentialName = "azurecli";

/// <summary>
/// Get the credential based on the credential type.
/// </summary>
public static TokenCredential GetCredentials(string credentialType)
{
credentialType = credentialType.ToLower();
switch (credentialType)
{
case DefaultCredentialName:
return new DefaultAzureCredential();
case EnvironmentCredentialName:
return new EnvironmentCredential();
case WorkloadIdentityCredentialName:
return new WorkloadIdentityCredential();
case ManagedIdentityCredentialName:
return new ManagedIdentityCredential();
case AzureCliCredentialName:
return new AzureCliCredential();
default:
throw new ValidationException($"Invalid credential key: {credentialType}");
}
}

/// <summary>
/// Get the credential based on the plugin config.
/// </summary>
public static TokenCredential GetCredentials(Dictionary<string, string>? pluginConfig)
{
var credentialName = pluginConfig?.GetValueOrDefault(CredentialTypeKey, DefaultCredentialName) ??
DefaultCredentialName;
return GetCredentials(credentialName);
}
}
}
17 changes: 9 additions & 8 deletions Notation.Plugin.AzureKeyVault/KeyVault/KeyVaultClient.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using Azure.Identity;
using Azure.Core;
using Azure.Security.KeyVault.Certificates;
using Azure.Security.KeyVault.Keys.Cryptography;
using Azure.Security.KeyVault.Secrets;
Expand Down Expand Up @@ -62,7 +62,7 @@ private record KeyVaultMetadata(string KeyVaultUrl, string Name, string Version)
/// Constructor to create AzureKeyVault object from keyVaultUrl, name
/// and version.
/// </summary>
public KeyVaultClient(string keyVaultUrl, string name, string version)
public KeyVaultClient(string keyVaultUrl, string name, string version, TokenCredential credential)
{
if (string.IsNullOrEmpty(keyVaultUrl))
{
Expand All @@ -84,7 +84,6 @@ public KeyVaultClient(string keyVaultUrl, string name, string version)
this._keyId = $"{keyVaultUrl}/keys/{name}/{version}";

// initialize credential and lazy clients
var credential = new DefaultAzureCredential();
this._certificateClient = new Lazy<CertificateClient>(() => new CertificateClient(new Uri(keyVaultUrl), credential));
this._cryptoClient = new Lazy<CryptographyClient>(() => new CryptographyClient(new Uri(_keyId), credential));
this._secretClient = new Lazy<SecretClient>(() => new SecretClient(new Uri(keyVaultUrl), credential));
Expand All @@ -93,18 +92,20 @@ public KeyVaultClient(string keyVaultUrl, string name, string version)
/// <summary>
/// Constructor to create AzureKeyVault object from key identifier or
/// certificate identifier.
///
/// </summary>
/// <param name="id">
/// Key identifier or certificate identifier. (e.g. https://<vaultname>.vault.azure.net/keys/<name>/<version>)
/// </param>
/// </summary>
public KeyVaultClient(string id) : this(ParseId(id)) { }
/// <param name="credential">
/// TokenCredential object to authenticate with Azure Key Vault.
/// </param>
public KeyVaultClient(string id, TokenCredential credential) : this(ParseId(id), credential) { }

/// <summary>
/// A helper constructor to create KeyVaultClient from KeyVaultMetadata.
/// </summary>
private KeyVaultClient(KeyVaultMetadata metadata)
: this(metadata.KeyVaultUrl, metadata.Name, metadata.Version) { }
private KeyVaultClient(KeyVaultMetadata metadata, TokenCredential credential)
: this(metadata.KeyVaultUrl, metadata.Name, metadata.Version, credential) { }

/// <summary>
/// A helper function to parse key identifier or certificate identifier
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Azure Provider for the [Notation CLI](https://github.com/notaryproject/notation)

The `notation-azure-kv` plugin allows you to sign the Notation-generated payload with a certificate in Azure Key Vault (AKV). The certificate and private key are stored in AKV and the plugin will request signing and obtain the leaf certificate from AKV.

The plugin supports several [authentication methods](https://learn.microsoft.com/dotnet/api/azure.identity.defaultazurecredential). The [Azure CLI](https://learn.microsoft.com/cli/azure/authenticate-azure-cli) or the [Managed Identity](https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview) credential is suggested.
The plugin supports several [credential types](./docs/plugin-config.md#credential_type).

## Installation the AKV plugin
Before you begin, make sure the latest version of the [Notation CLI has been installed](https://notaryproject.dev/docs/installation/cli/).
Expand Down
18 changes: 18 additions & 0 deletions docs/plugin-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ notation sign <registry>/<repository>@<digest> \
--plugin-config "self_signed=true"
```

## credential_type
Set the preferred credential type. Currently, the following credential types are supported:
- [default](https://learn.microsoft.com/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet)
- [environment](https://learn.microsoft.com/dotnet/api/azure.identity.environmentcredential?view=azure-dotnet)
- [workloadid](https://learn.microsoft.com/dotnet/api/azure.identity.workloadidentitycredential?view=azure-dotnet)
- [managedid](https://learn.microsoft.com/dotnet/api/azure.identity.managedidentitycredential?view=azure-dotnet)

Default: **default** (default credential)

Example
```
notation sign <registry>/<repository>@<digest> \
--plugin azure-kv \
--id <key_identifier> \
--plugin-config "self_signed=true"
--plugin-config "credential_type=managedid"
```

## Permission management
The `notation-azure-kv` plugin support multiple level of permissions setting to satisfy different permission use cases.

Expand Down
Loading

0 comments on commit 53bdb04

Please sign in to comment.