This repository has been archived by the owner on Aug 3, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #80 from NuGet/dev
Merging Json Secret injection changes from dev to master
- Loading branch information
Showing
12 changed files
with
518 additions
and
2 deletions.
There are no files selected for viewing
21 changes: 21 additions & 0 deletions
21
src/NuGet.Services.Configuration/ConfigurationBuilderExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using Microsoft.Extensions.Configuration; | ||
using NuGet.Services.KeyVault; | ||
|
||
namespace NuGet.Services.Configuration | ||
{ | ||
public static class ConfigurationBuilderExtensions | ||
{ | ||
public static IConfigurationBuilder AddInjectedJsonFile(this IConfigurationBuilder configurationBuilder, string path, ISecretInjector secretInjector) | ||
{ | ||
configurationBuilder = configurationBuilder ?? throw new ArgumentNullException(nameof(configurationBuilder)); | ||
|
||
configurationBuilder.Add(new KeyVaultJsonInjectingConfigurationSource(path, secretInjector)); | ||
|
||
return configurationBuilder; | ||
} | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
src/NuGet.Services.Configuration/KeyVaultInjectingConfigurationProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using Microsoft.Extensions.Primitives; | ||
using NuGet.Services.KeyVault; | ||
|
||
namespace NuGet.Services.Configuration | ||
{ | ||
using Extensions = Microsoft.Extensions.Configuration; | ||
|
||
/// <summary> | ||
/// Configuration provider that wraps around another provider and does KeyVault secret injection. | ||
/// </summary> | ||
/// <remarks> | ||
/// This relies on configuration objects not to be cached for proper secret rotation from KeyVault. | ||
/// One needs <see cref="NonCachingOptionsSnapshot{TOptions}"/> as a <see cref="Microsoft.Extensions.Options.IOptionsSnapshot{TOptions}"/> implementation | ||
/// to make sure no caching happens. | ||
/// </remarks> | ||
public class KeyVaultInjectingConfigurationProvider : Extensions.IConfigurationProvider | ||
{ | ||
private readonly Extensions.IConfigurationProvider _originalProvider; | ||
private readonly ISecretInjector _secretInjector; | ||
|
||
public KeyVaultInjectingConfigurationProvider(Extensions.IConfigurationProvider originalProvider, ISecretInjector secretInjector) | ||
{ | ||
_originalProvider = originalProvider ?? throw new ArgumentNullException(nameof(originalProvider)); | ||
_secretInjector = secretInjector ?? throw new ArgumentNullException(nameof(secretInjector)); | ||
} | ||
|
||
public IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath) | ||
=> _originalProvider.GetChildKeys(earlierKeys, parentPath); | ||
|
||
public IChangeToken GetReloadToken() | ||
=> _originalProvider.GetReloadToken(); | ||
|
||
public void Load() | ||
=> _originalProvider.Load(); | ||
|
||
public void Set(string key, string value) | ||
=> _originalProvider.Set(key, value); | ||
|
||
public bool TryGet(string key, out string value) | ||
{ | ||
if (_originalProvider.TryGet(key, out value)) | ||
{ | ||
value = _secretInjector.InjectAsync(value).ConfigureAwait(false).GetAwaiter().GetResult(); | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
src/NuGet.Services.Configuration/KeyVaultJsonInjectingConfigurationSource.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.Configuration.Json; | ||
using NuGet.Services.KeyVault; | ||
|
||
namespace NuGet.Services.Configuration | ||
{ | ||
using Extensions = Microsoft.Extensions.Configuration; | ||
|
||
/// <summary> | ||
/// Configuration source for the <see cref="KeyVaultInjectingConfigurationProvider"/> that wraps it around <see cref="JsonConfigurationProvider"/> | ||
/// to inject secrets to data read from json configuration | ||
/// </summary> | ||
public class KeyVaultJsonInjectingConfigurationSource : IConfigurationSource | ||
{ | ||
private readonly string _path; | ||
private readonly ISecretInjector _secretInjector; | ||
|
||
public KeyVaultJsonInjectingConfigurationSource(string path, ISecretInjector secretInjector) | ||
{ | ||
_path = path ?? throw new ArgumentNullException(nameof(path)); | ||
_secretInjector = secretInjector ?? throw new ArgumentNullException(nameof(secretInjector)); | ||
} | ||
|
||
public Extensions.IConfigurationProvider Build(IConfigurationBuilder builder) | ||
{ | ||
var jsonSource = new JsonConfigurationSource { FileProvider = null, Path = _path, Optional = false, ReloadOnChange = false }; | ||
jsonSource.ResolveFileProvider(); | ||
var jsonProvider = jsonSource.Build(builder); | ||
|
||
return new KeyVaultInjectingConfigurationProvider(jsonProvider, _secretInjector); | ||
} | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
src/NuGet.Services.Configuration/NonCachingOptionsSnapshot.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace NuGet.Services.Configuration | ||
{ | ||
/// <summary> | ||
/// <see cref="IOptionsSnapshot{TOptions}"/> implementation that does not use default implementation's | ||
/// cache for the <typeparamref name="TOptions"/> objects and always instantiates and binds a new one. | ||
/// </summary> | ||
/// <typeparam name="TOptions">The actual data object</typeparam> | ||
/// <example> | ||
/// To use, add the following line before services.AddOptions() call: | ||
/// services.Add(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(NonCachingOptionsSnapshot<>))); | ||
/// </example> | ||
public class NonCachingOptionsSnapshot<TOptions> : IOptionsSnapshot<TOptions> | ||
where TOptions : class, new() | ||
{ | ||
private readonly TOptions _value; | ||
|
||
public NonCachingOptionsSnapshot(IEnumerable<IConfigureOptions<TOptions>> setups) | ||
{ | ||
setups = setups ?? throw new ArgumentNullException(nameof(setups)); | ||
|
||
_value = new TOptions(); | ||
foreach (var setup in setups) | ||
{ | ||
setup.Configure(_value); | ||
} | ||
} | ||
|
||
public TOptions Value => _value; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
|
||
namespace NuGet.Services.KeyVault | ||
{ | ||
/// <summary> | ||
/// Wraps existing secret reader factory to provide a caching layer for the <see cref="ISecretReader"/>. | ||
/// </summary> | ||
public class CachingSecretReaderFactory : ISecretReaderFactory | ||
{ | ||
private readonly ISecretReaderFactory _underlyingFactory; | ||
private readonly TimeSpan _cachingTimeout; | ||
|
||
/// <summary> | ||
/// Initializes the instance. | ||
/// </summary> | ||
/// <param name="underlyingFactory">Actual factory we are wrapping</param> | ||
/// <param name="cachingTimeout">The max caching time for secrets</param> | ||
public CachingSecretReaderFactory(ISecretReaderFactory underlyingFactory, TimeSpan cachingTimeout) | ||
{ | ||
_underlyingFactory = underlyingFactory ?? throw new ArgumentNullException(nameof(underlyingFactory)); | ||
_cachingTimeout = cachingTimeout; | ||
} | ||
|
||
public ISecretInjector CreateSecretInjector(ISecretReader secretReader) | ||
=> _underlyingFactory.CreateSecretInjector(secretReader); | ||
|
||
public ISecretReader CreateSecretReader() | ||
=> new CachingSecretReader(_underlyingFactory.CreateSecretReader(), (int)_cachingTimeout.TotalSeconds); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
tests/NuGet.Services.Configuration.Tests/KeyVaultInjectingConfigurationProviderFacts.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
using Moq; | ||
using NuGet.Services.KeyVault; | ||
using Xunit; | ||
|
||
namespace NuGet.Services.Configuration.Tests | ||
{ | ||
public class KeyVaultInjectingConfigurationProviderFacts | ||
{ | ||
[Fact] | ||
public void PassesThroughGetChildKeys() | ||
{ | ||
var originalProviderMock = new Mock<Microsoft.Extensions.Configuration.IConfigurationProvider>(); | ||
var secretInjectorMock = new Mock<ISecretInjector>(); | ||
|
||
var keys = new string[] { "someKey" }; | ||
var parentPath = "Section1"; | ||
|
||
Microsoft.Extensions.Configuration.IConfigurationProvider provider = new KeyVaultInjectingConfigurationProvider(originalProviderMock.Object, secretInjectorMock.Object); | ||
provider.GetChildKeys(keys, parentPath); | ||
|
||
originalProviderMock.Verify(p => p.GetChildKeys(keys, parentPath), Times.Once()); | ||
originalProviderMock.Verify(p => p.GetChildKeys(It.IsAny<IEnumerable<string>>(), It.IsAny<string>()), Times.Once()); | ||
secretInjectorMock.Verify(x => x.InjectAsync(It.IsAny<string>()), Times.Never()); | ||
} | ||
|
||
[Fact] | ||
public void PassesThroughGetReloadToken() | ||
{ | ||
var originalProviderMock = new Mock<Microsoft.Extensions.Configuration.IConfigurationProvider>(); | ||
var secretInjectorMock = new Mock<ISecretInjector>(); | ||
|
||
var provider = new KeyVaultInjectingConfigurationProvider(originalProviderMock.Object, secretInjectorMock.Object); | ||
provider.GetReloadToken(); | ||
|
||
originalProviderMock.Verify(p => p.GetReloadToken(), Times.Once()); | ||
secretInjectorMock.Verify(x => x.InjectAsync(It.IsAny<string>()), Times.Never()); | ||
} | ||
|
||
[Fact] | ||
public void PassesThroughLoad() | ||
{ | ||
var originalProviderMock = new Mock<Microsoft.Extensions.Configuration.IConfigurationProvider>(); | ||
var secretInjectorMock = new Mock<ISecretInjector>(); | ||
|
||
var provider = new KeyVaultInjectingConfigurationProvider(originalProviderMock.Object, secretInjectorMock.Object); | ||
provider.Load(); | ||
|
||
originalProviderMock.Verify(p => p.Load(), Times.Once()); | ||
secretInjectorMock.Verify(x => x.InjectAsync(It.IsAny<string>()), Times.Never()); | ||
} | ||
|
||
[Fact] | ||
public void PassesThroughSet() | ||
{ | ||
var originalProviderMock = new Mock<Microsoft.Extensions.Configuration.IConfigurationProvider>(); | ||
var secretInjectorMock = new Mock<ISecretInjector>(); | ||
|
||
var key = "SomeKey"; | ||
var value = "SomeValue"; | ||
|
||
var provider = new KeyVaultInjectingConfigurationProvider(originalProviderMock.Object, secretInjectorMock.Object); | ||
provider.Set(key, value); | ||
|
||
originalProviderMock.Verify(p => p.Set(key, value), Times.Once()); | ||
originalProviderMock.Verify(p => p.Set(It.IsAny<string>(), It.IsAny<string>()), Times.Once()); | ||
secretInjectorMock.Verify(x => x.InjectAsync(It.IsAny<string>()), Times.Never()); | ||
} | ||
|
||
[Fact] | ||
public void InjectsSecrets() | ||
{ | ||
var originalProviderMock = new Mock<Microsoft.Extensions.Configuration.IConfigurationProvider>(MockBehavior.Strict); | ||
var secretInjectorMock = new Mock<ISecretInjector>(); | ||
|
||
const string key = "SomeKey"; | ||
const string uninjectedValue = "Value=$$Secret$$"; | ||
const string injectedValue = "Value=SecretValue"; | ||
|
||
var originalOutValue = uninjectedValue; | ||
originalProviderMock.Setup(p => p.TryGet(key, out originalOutValue)).Returns(true).Verifiable(); | ||
secretInjectorMock.Setup(i => i.InjectAsync(uninjectedValue)).ReturnsAsync(injectedValue).Verifiable(); | ||
|
||
var provider = new KeyVaultInjectingConfigurationProvider(originalProviderMock.Object, secretInjectorMock.Object); | ||
var found = provider.TryGet(key, out string value); | ||
|
||
Assert.True(found); | ||
Assert.Equal(injectedValue, value); | ||
|
||
originalProviderMock.Verify(p => p.TryGet(key, out originalOutValue), Times.Once()); | ||
secretInjectorMock.Verify(i => i.InjectAsync(uninjectedValue), Times.Once()); | ||
secretInjectorMock.Verify(x => x.InjectAsync(It.IsAny<string>()), Times.Once()); | ||
} | ||
} | ||
} |
Oops, something went wrong.