public async Task GetFunctionSecrets_WhenNoSecretFileExists_CreatesDefaultSecretAndPersistsFile() { using (var directory = new TempDirectory()) { string functionName = "TestFunction"; string expectedTraceMessage = string.Format(Resources.TraceFunctionSecretGeneration, functionName); Mock <IKeyValueConverterFactory> mockValueConverterFactory = GetConverterFactoryMock(false, false); IDictionary <string, string> functionSecrets; var traceWriter = new TestTraceWriter(System.Diagnostics.TraceLevel.Verbose); ISecretsRepository repository = new FileSystemSecretsRepository(directory.Path); using (var secretManager = new SecretManager(repository, mockValueConverterFactory.Object, null)) { functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName); } bool functionSecretsExists = File.Exists(Path.Combine(directory.Path, "testfunction.json")); Assert.NotNull(functionSecrets); Assert.True(functionSecretsExists); Assert.Equal(1, functionSecrets.Count); Assert.Equal(ScriptConstants.DefaultFunctionKeyName, functionSecrets.Keys.First()); } }
public async Task GetHostSecrets_WhenTooManyBackups_ThrowsException() { using (var directory = new TempDirectory()) { string functionName = "testfunction"; string expectedTraceMessage = string.Format(Resources.ErrorTooManySecretBackups, ScriptConstants.MaximumSecretBackupCount, functionName, string.Format(Resources.ErrorSameSecrets, "test0,test1")); string functionSecretsJson = @"{ 'keys': [ { 'name': 'Key1', 'value': 'FunctionValue1', 'encrypted': true }, { 'name': 'Key2', 'value': 'FunctionValue2', 'encrypted': false } ] }"; IDictionary <string, string> functionSecrets; ISecretsRepository repository = new FileSystemSecretsRepository(directory.Path); using (var secretManager = new SecretManager(repository, _logger, new TestMetricsLogger(), _hostNameProvider)) { InvalidOperationException ioe = null; try { for (int i = 0; i < ScriptConstants.MaximumSecretBackupCount + 20; i++) { File.WriteAllText(Path.Combine(directory.Path, functionName + ".json"), functionSecretsJson); // If we haven't hit the exception yet, pause to ensure the file contents are being flushed. if (i >= ScriptConstants.MaximumSecretBackupCount) { await Task.Delay(500); } // reset hostname provider and set a new hostname to force another backup _hostNameProvider.Reset(); string hostName = "test" + (i % 2).ToString(); _testEnvironment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteHostName, hostName); functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName); } } catch (InvalidOperationException ex) { ioe = ex; } } Assert.True(Directory.GetFiles(directory.Path, $"{functionName}.{ScriptConstants.Snapshot}*").Length >= ScriptConstants.MaximumSecretBackupCount); Assert.True(_loggerProvider.GetAllLogMessages().Any( t => t.Level == LogLevel.Debug && t.FormattedMessage.IndexOf(expectedTraceMessage, StringComparison.OrdinalIgnoreCase) > -1), "Expected Trace message not found"); } }
public async Task AddOrUpdateFunctionSecret_ClearsCache_WhenFunctionSecretAdded() { using (var directory = new TempDirectory()) { CreateTestSecrets(directory.Path); Mock <IKeyValueConverterFactory> mockValueConverterFactory = GetConverterFactoryMock(false); KeyOperationResult result; ISecretsRepository repository = new FileSystemSecretsRepository(directory.Path); using (var secretManager = new SecretManager(repository, mockValueConverterFactory.Object, _logger, new TestMetricsLogger(), _hostNameProvider)) { var keys = await secretManager.GetFunctionSecretsAsync("testfunction"); Assert.Equal(2, keys.Count); // add a new key result = await secretManager.AddOrUpdateFunctionSecretAsync("function-key-3", "9876", "TestFunction", ScriptSecretsType.Function); } string secretsJson = File.ReadAllText(Path.Combine(directory.Path, "testfunction.json")); var persistedSecrets = ScriptSecretSerializer.DeserializeSecrets <FunctionSecrets>(secretsJson); Assert.Equal(OperationResult.Created, result.Result); Assert.Equal(result.Secret, "9876"); var logs = _loggerProvider.GetAllLogMessages(); Assert.Equal(1, logs.Count(p => string.Equals(p.FormattedMessage, "Function keys change detected. Clearing cache for function 'TestFunction'.", StringComparison.OrdinalIgnoreCase))); Assert.Equal(1, logs.Count(p => p.FormattedMessage == "Function secret 'function-key-3' for 'TestFunction' Created.")); } }
public async Task MergedSecrets_PrioritizesFunctionSecrets() { using (var directory = new TempDirectory()) { string hostSecrets = @"{ 'masterKey': { 'name': 'master', 'value': '1234', 'encrypted': false }, 'functionKeys': [ { 'name': 'Key1', 'value': 'HostValue1', 'encrypted': false }, { 'name': 'Key3', 'value': 'HostValue3', 'encrypted': false } ] }"; string functionSecrets = @"{ 'keys': [ { 'name': 'Key1', 'value': 'FunctionValue1', 'encrypted': false }, { 'name': 'Key2', 'value': 'FunctionValue2', 'encrypted': false } ] }"; File.WriteAllText(Path.Combine(directory.Path, ScriptConstants.HostMetadataFileName), hostSecrets); File.WriteAllText(Path.Combine(directory.Path, "testfunction.json"), functionSecrets); IDictionary <string, string> result; ISecretsRepository repository = new FileSystemSecretsRepository(directory.Path); using (var secretManager = new SecretManager(_settingsManager, repository, NullLogger.Instance)) { result = await secretManager.GetFunctionSecretsAsync("testfunction", true); } Assert.Contains("Key1", result.Keys); Assert.Contains("Key2", result.Keys); Assert.Contains("Key3", result.Keys); Assert.Equal("FunctionValue1", result["Key1"]); Assert.Equal("FunctionValue2", result["Key2"]); Assert.Equal("HostValue3", result["Key3"]); } }
public async Task GetFunctiontSecrets_WhenNonDecryptedSecrets_SavesAndRefreshes() { string key = TestHelpers.GenerateKeyHexString(); using (new TestScopedEnvironmentVariable(EnvironmentSettingNames.WebSiteAuthEncryptionKey, key)) { using (var directory = new TempDirectory()) { string functionName = "testfunction"; string expectedTraceMessage = string.Format(Resources.TraceNonDecryptedFunctionSecretRefresh, functionName, string.Empty); string functionSecretsJson = @"{ 'keys': [ { 'name': 'Key1', 'value': 'cryptoError', 'encrypted': true }, { 'name': 'Key2', 'value': '1234', 'encrypted': false } ] }"; File.WriteAllText(Path.Combine(directory.Path, functionName + ".json"), functionSecretsJson); Mock <IKeyValueConverterFactory> mockValueConverterFactory = GetConverterFactoryMock(true, false); IDictionary <string, string> functionSecrets; ISecretsRepository repository = new FileSystemSecretsRepository(directory.Path); using (var secretManager = new SecretManager(repository, mockValueConverterFactory.Object, null, new TestMetricsLogger(), _hostNameProvider)) { functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName); } Assert.NotNull(functionSecrets); Assert.NotEqual(functionSecrets["Key1"], "cryptoError"); var result = JsonConvert.DeserializeObject <FunctionSecrets>(File.ReadAllText(Path.Combine(directory.Path, functionName + ".json"))); Assert.Equal(result.GetFunctionKey("Key1", functionName).Value, "!" + functionSecrets["Key1"]); Assert.Equal(1, Directory.GetFiles(directory.Path, $"{functionName}.{ScriptConstants.Snapshot}*").Length); result = JsonConvert.DeserializeObject <FunctionSecrets>(File.ReadAllText(Path.Combine(directory.Path, functionName + ".json"))); string snapShotFileName = Directory.GetFiles(directory.Path, $"{functionName}.{ScriptConstants.Snapshot}*")[0]; result = JsonConvert.DeserializeObject <FunctionSecrets>(File.ReadAllText(Path.Combine(directory.Path, snapShotFileName))); Assert.NotEqual(result.DecryptionKeyId, key); } } }
public async Task GetHostSecrets_WhenTooManyBackups_ThrowsException() { using (var directory = new TempDirectory()) { string functionName = "testfunction"; string expectedTraceMessage = string.Format(Resources.ErrorTooManySecretBackups, ScriptConstants.MaximumSecretBackupCount, functionName); string functionSecretsJson = @"{ 'keys': [ { 'name': 'Key1', 'value': 'FunctionValue1', 'encrypted': true }, { 'name': 'Key2', 'value': 'FunctionValue2', 'encrypted': false } ] }"; IDictionary <string, string> functionSecrets; ISecretsRepository repository = new FileSystemSecretsRepository(directory.Path); using (var secretManager = new SecretManager(_settingsManager, repository, null)) { await Assert.ThrowsAsync <InvalidOperationException>(async() => { for (int i = 0; i < ScriptConstants.MaximumSecretBackupCount + 20; i++) { File.WriteAllText(Path.Combine(directory.Path, functionName + ".json"), functionSecretsJson); // If we haven't hit the exception yet, pause to ensure the file contents are being flushed. if (i >= ScriptConstants.MaximumSecretBackupCount) { await Task.Delay(500); } functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName); } }); } Assert.True(Directory.GetFiles(directory.Path, $"{functionName}.{ScriptConstants.Snapshot}*").Length >= ScriptConstants.MaximumSecretBackupCount); } }
public async Task GetFunctionSecrets_UpdatesStaleSecrets() { using (var directory = new TempDirectory()) { string functionName = "testfunction"; string expectedTraceMessage = string.Format(Resources.TraceStaleFunctionSecretRefresh, functionName); string functionSecretsJson = @"{ 'keys': [ { 'name': 'Key1', 'value': 'FunctionValue1', 'encrypted': false }, { 'name': 'Key2', 'value': 'FunctionValue2', 'encrypted': false } ] }"; File.WriteAllText(Path.Combine(directory.Path, functionName + ".json"), functionSecretsJson); Mock <IKeyValueConverterFactory> mockValueConverterFactory = GetConverterFactoryMock(); IDictionary <string, string> functionSecrets; var traceWriter = new TestTraceWriter(TraceLevel.Verbose); ISecretsRepository repository = new FileSystemSecretsRepository(directory.Path); using (var secretManager = new SecretManager(repository, mockValueConverterFactory.Object, traceWriter, null)) { functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName); } // Read the persisted content var result = JsonConvert.DeserializeObject <FunctionSecrets>(File.ReadAllText(Path.Combine(directory.Path, functionName + ".json"))); bool functionSecretsConverted = functionSecrets.Values.Zip(result.Keys, (r1, r2) => string.Equals("!" + r1, r2.Value)).All(r => r); Assert.Equal(2, result.Keys.Count); Assert.True(functionSecretsConverted, "Function secrets were not persisted"); Assert.True(traceWriter.Traces.Any(t => t.Level == TraceLevel.Verbose && t.Message.IndexOf(expectedTraceMessage) > -1)); } }
public async Task GetFunctiontSecrets_AddsMetrics() { using (var directory = new TempDirectory()) { string functionName = "testfunction"; string expectedTraceMessage = string.Format(Resources.TraceNonDecryptedFunctionSecretRefresh, functionName); string functionSecretsJson = @"{ 'keys': [ { 'name': 'Key1', 'value': 'cryptoError', 'encrypted': true }, { 'name': 'Key2', 'value': '1234', 'encrypted': false } ] }"; File.WriteAllText(Path.Combine(directory.Path, functionName + ".json"), functionSecretsJson); Mock <IKeyValueConverterFactory> mockValueConverterFactory = GetConverterFactoryMock(true, false); IDictionary <string, string> functionSecrets; ISecretsRepository repository = new FileSystemSecretsRepository(directory.Path); TestMetricsLogger metricsLogger = new TestMetricsLogger(); using (var secretManager = new SecretManager(repository, mockValueConverterFactory.Object, null, metricsLogger)) { functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName); } string eventName = string.Format(MetricEventNames.SecretManagerGetFunctionSecrets, repository.GetType().Name.ToLower()); metricsLogger.EventsBegan.Single(e => e.StartsWith(eventName)); metricsLogger.EventsBegan.Single(e => e.Contains("testfunction")); metricsLogger.EventsEnded.Single(e => e.ToString().StartsWith(eventName)); metricsLogger.EventsEnded.Single(e => e.ToString().Contains("testfunction")); } }
public async Task GetFunctiontSecrets_WhenNonDecryptedSecrets_SavesAndRefreshes() { using (var directory = new TempDirectory()) { string functionName = "testfunction"; string expectedTraceMessage = string.Format(Resources.TraceNonDecryptedFunctionSecretRefresh, functionName); string functionSecretsJson = @"{ 'keys': [ { 'name': 'Key1', 'value': 'cryptoError', 'encrypted': true }, { 'name': 'Key2', 'value': '1234', 'encrypted': false } ] }"; File.WriteAllText(Path.Combine(directory.Path, functionName + ".json"), functionSecretsJson); Mock <IKeyValueConverterFactory> mockValueConverterFactory = GetConverterFactoryMock(true, false); IDictionary <string, string> functionSecrets; ISecretsRepository repository = new FileSystemSecretsRepository(directory.Path); using (var secretManager = new SecretManager(repository, mockValueConverterFactory.Object, null)) { functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName); } Assert.NotNull(functionSecrets); Assert.Equal(functionSecrets["Key1"], "cryptoError"); var result = JsonConvert.DeserializeObject <FunctionSecrets>(File.ReadAllText(Path.Combine(directory.Path, functionName + ".json"))); Assert.Equal(result.GetFunctionKey("Key1", functionName).Value, "!cryptoError"); Assert.Equal(1, Directory.GetFiles(directory.Path, $"{functionName}.{ScriptConstants.Snapshot}*").Length); } }
public async Task GetFunctionSecretsAsync_WaitsForNewSecrets() { using (var directory = new TempDirectory()) { string functionName = "testfunction"; string functionSecretsJson = @"{ 'keys': [ { 'name': 'Key1', 'value': 'FunctionValue1', 'encrypted': false }, { 'name': 'Key2', 'value': 'FunctionValue2', 'encrypted': false } ] }"; string filePath = Path.Combine(directory.Path, functionName + ".json"); File.WriteAllText(filePath, functionSecretsJson); IDictionary <string, string> functionSecrets = null; ISecretsRepository repository = new FileSystemSecretsRepository(directory.Path); using (var secretManager = new SecretManager(_settingsManager, repository, null)) { await Task.WhenAll( Task.Run(async() => { // Lock the file using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Write, FileShare.None)) { await Task.Delay(500); } }), Task.Run(async() => { await Task.Delay(100); functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName); })); Assert.Equal(functionSecrets["Key1"], "FunctionValue1"); } using (var secretManager = new SecretManager(_settingsManager, repository, null)) { await Assert.ThrowsAsync <IOException>(async() => { await Task.WhenAll( Task.Run(async() => { // Lock the file using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Write, FileShare.None)) { await Task.Delay(3000); } }), Task.Run(async() => { await Task.Delay(100); functionSecrets = await secretManager.GetFunctionSecretsAsync(functionName); })); }); } } }
public async Task <string> GetFunctionSecretAsync(string functionName) { var secrets = await SecretManager.GetFunctionSecretsAsync(functionName); return(secrets.First().Value); }