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);
            }
        }
Exemple #7
0
        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);
        }