Skip to content
This repository has been archived by the owner on Aug 3, 2024. It is now read-only.
/ ServerCommon Public archive

Commit

Permalink
Merge pull request #80 from NuGet/dev
Browse files Browse the repository at this point in the history
Merging Json Secret injection changes from dev to master
  • Loading branch information
agr authored Nov 1, 2017
2 parents e515143 + 785d1f9 commit e1a38b9
Show file tree
Hide file tree
Showing 12 changed files with 518 additions and 2 deletions.
21 changes: 21 additions & 0 deletions src/NuGet.Services.Configuration/ConfigurationBuilderExtensions.cs
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;
}
}
}
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;
}
}
}
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 src/NuGet.Services.Configuration/NonCachingOptionsSnapshot.cs
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Configuration.cs" />
<Compile Include="ConfigurationBuilderExtensions.cs" />
<Compile Include="ConfigurationFactory.cs" />
<Compile Include="ConfigurationKeyAttribute.cs" />
<Compile Include="ConfigurationKeyPrefixAttribute.cs" />
Expand All @@ -52,6 +53,9 @@
<Compile Include="DictionaryExtensions.cs" />
<Compile Include="IConfigurationFactory.cs" />
<Compile Include="IConfigurationProvider.cs" />
<Compile Include="KeyVaultInjectingConfigurationProvider.cs" />
<Compile Include="KeyVaultJsonInjectingConfigurationSource.cs" />
<Compile Include="NonCachingOptionsSnapshot.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\AssemblyInfo.*.cs" />
<Compile Include="SecretConfigurationReader.cs" />
Expand Down
6 changes: 4 additions & 2 deletions src/NuGet.Services.Configuration/project.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"dependencies": {
"Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0"
"Microsoft.Extensions.Configuration.Abstractions": "1.1.2",
"Microsoft.Extensions.Configuration.FileExtensions": "1.1.2",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.Extensions.Options": "1.1.2"
},
"frameworks": {
"net452": {
Expand Down
33 changes: 33 additions & 0 deletions src/NuGet.Services.KeyVault/CachingSecretReaderFactory.cs
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);
}
}
1 change: 1 addition & 0 deletions src/NuGet.Services.KeyVault/NuGet.Services.KeyVault.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="CachingSecretReader.cs" />
<Compile Include="CachingSecretReaderFactory.cs" />
<Compile Include="CertificateUtility.cs" />
<Compile Include="ISecretReaderFactory.cs" />
<Compile Include="EmptySecretReader.cs" />
Expand Down
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());
}
}
}
Loading

0 comments on commit e1a38b9

Please sign in to comment.