diff --git a/src/ApiService/ApiService/ApiService.csproj b/src/ApiService/ApiService/ApiService.csproj index f56fac7967..3e4e2c2b15 100644 --- a/src/ApiService/ApiService/ApiService.csproj +++ b/src/ApiService/ApiService/ApiService.csproj @@ -9,6 +9,7 @@ + diff --git a/src/ApiService/ApiService/OneFuzzTypes/Model.cs b/src/ApiService/ApiService/OneFuzzTypes/Model.cs index acbf6b3797..e8c8e74df7 100644 --- a/src/ApiService/ApiService/OneFuzzTypes/Model.cs +++ b/src/ApiService/ApiService/OneFuzzTypes/Model.cs @@ -496,3 +496,31 @@ public record AdoTemplate(); public record TeamsTemplate(); public record GithubIssuesTemplate(); + + +public record SecretAddress(Uri Url); + + +/// This class allows us to store some data that are intended to be secret +/// The secret field stores either the raw data or the address of that data +/// This class allows us to maintain backward compatibility with existing +/// NotificationTemplate classes +public record SecretData(T Secret) +{ + public override string ToString() + { + if (Secret is SecretAddress) + { + if (Secret is null) + { + return string.Empty; + } + else + { + return Secret.ToString()!; + } + } + else + return "[REDACTED]"; + } +} diff --git a/src/ApiService/ApiService/Program.cs b/src/ApiService/ApiService/Program.cs index 50a5315274..38ae84c6ff 100644 --- a/src/ApiService/ApiService/Program.cs +++ b/src/ApiService/ApiService/Program.cs @@ -82,15 +82,18 @@ public static void Main() .AddScoped() .AddScoped() .AddScoped() - + .AddScoped() //Move out expensive resources into separate class, and add those as Singleton // ArmClient, Table Client(s), Queue Client(s), HttpClient, etc. .AddSingleton() .AddSingleton() + .AddHttpClient() ) .Build(); host.Run(); } + + } diff --git a/src/ApiService/ApiService/onefuzzlib/Creds.cs b/src/ApiService/ApiService/onefuzzlib/Creds.cs index c1236b7b7e..2ac067aca8 100644 --- a/src/ApiService/ApiService/onefuzzlib/Creds.cs +++ b/src/ApiService/ApiService/onefuzzlib/Creds.cs @@ -31,9 +31,9 @@ public class Creds : ICreds public Creds(IServiceConfig config) { - _armClient = new ArmClient(this.GetIdentity(), this.GetSubcription()); - _azureCredential = new DefaultAzureCredential(); _config = config; + _azureCredential = new DefaultAzureCredential(); + _armClient = new ArmClient(this.GetIdentity(), this.GetSubcription()); } public DefaultAzureCredential GetIdentity() diff --git a/src/ApiService/ApiService/onefuzzlib/Secrets.cs b/src/ApiService/ApiService/onefuzzlib/Secrets.cs new file mode 100644 index 0000000000..eeb4d40b22 --- /dev/null +++ b/src/ApiService/ApiService/onefuzzlib/Secrets.cs @@ -0,0 +1,139 @@ +using System.Text.Json; +using System.Threading.Tasks; +using Azure.Security.KeyVault.Secrets; +using Microsoft.OneFuzz.Service.OneFuzzLib.Orm; + +namespace Microsoft.OneFuzz.Service; + +public interface ISecretsOperations +{ + public (Uri, string) ParseSecretUrl(Uri secretsUrl); + public Task?> SaveToKeyvault(SecretData secretData); + public Task GetSecretStringValue(SecretData data); + + public Task StoreInKeyvault(Uri keyvaultUrl, string secretName, string secretValue); + public Task GetSecret(Uri secretUrl); + public Task GetSecretObj(Uri secretUrl); + public Task DeleteSecret(Uri secretUrl); + public Task DeleteRemoteSecretData(SecretData data); + public Uri GetKeyvaultAddress(); + +} + +public class SecretsOperations : ISecretsOperations +{ + private readonly ICreds _creds; + private readonly IServiceConfig _config; + public SecretsOperations(ICreds creds, IServiceConfig config) + { + _creds = creds; + _config = config; + } + + public (Uri, string) ParseSecretUrl(Uri secretsUrl) + { + // format: https://{vault-name}.vault.azure.net/secrets/{secret-name}/{version} + var vaultUrl = $"{secretsUrl.Scheme}://{secretsUrl.Host}"; + var secretName = secretsUrl.Segments[secretsUrl.Segments.Length - 2].Trim('/'); + return (new Uri(vaultUrl), secretName); + } + + public async Task?> SaveToKeyvault(SecretData secretData) + { + if (secretData == null || secretData.Secret is null) + return null; + + if (secretData.Secret is SecretAddress) + { + return secretData as SecretData; + } + else + { + var secretName = Guid.NewGuid(); + string secretValue; + if (secretData.Secret is string) + { + secretValue = (secretData.Secret as string)!.Trim(); + } + else + { + secretValue = JsonSerializer.Serialize(secretData.Secret, EntityConverter.GetJsonSerializerOptions()); + } + + var kv = await StoreInKeyvault(GetKeyvaultAddress(), secretName.ToString(), secretValue); + return new SecretData(new SecretAddress(kv.Id)); + } + } + + public async Task GetSecretStringValue(SecretData data) + { + if (data.Secret is null) + { + return null; + } + + if (data.Secret is SecretAddress) + { + var secret = await GetSecret((data.Secret as SecretAddress)!.Url); + return secret.Value; + } + else + { + return data.Secret.ToString(); + } + } + + public Uri GetKeyvaultAddress() + { + // https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name + var keyvaultName = _config!.OneFuzzKeyvault; + return new Uri($"https://{keyvaultName}.vault.azure.net"); + } + + + public async Task StoreInKeyvault(Uri keyvaultUrl, string secretName, string secretValue) + { + var keyvaultClient = new SecretClient(keyvaultUrl, _creds.GetIdentity()); + var r = await keyvaultClient.SetSecretAsync(secretName, secretValue); + return r.Value; + } + + public async Task GetSecret(Uri secretUrl) + { + var (vaultUrl, secretName) = ParseSecretUrl(secretUrl); + var keyvaultClient = new SecretClient(vaultUrl, _creds.GetIdentity()); + return await keyvaultClient.GetSecretAsync(secretName); + } + + public async Task GetSecretObj(Uri secretUrl) + { + var secret = await GetSecret(secretUrl); + if (secret is null) + return default(T); + else + return JsonSerializer.Deserialize(secret.Value, EntityConverter.GetJsonSerializerOptions()); + } + + public async Task DeleteSecret(Uri secretUrl) + { + var (vaultUrl, secretName) = ParseSecretUrl(secretUrl); + var keyvaultClient = new SecretClient(vaultUrl, _creds.GetIdentity()); + return await keyvaultClient.StartDeleteSecretAsync(secretName); + } + + public async Task DeleteRemoteSecretData(SecretData data) + { + if (data.Secret is SecretAddress) + { + if (data.Secret is not null) + return await DeleteSecret((data.Secret as SecretAddress)!.Url); + else + return null; + } + else + { + return null; + } + } + +} diff --git a/src/ApiService/ApiService/packages.lock.json b/src/ApiService/ApiService/packages.lock.json index 8544dc1224..35c0b219f0 100644 --- a/src/ApiService/ApiService/packages.lock.json +++ b/src/ApiService/ApiService/packages.lock.json @@ -107,6 +107,18 @@ "System.Text.Json": "4.7.2" } }, + "Azure.Security.KeyVault.Secrets": { + "type": "Direct", + "requested": "[4.3.0, )", + "resolved": "4.3.0", + "contentHash": "GRnmQzTXDVABry1rC8PwuVOHSDCUGn4Om1ABTCzWfHdDSOwRydtQ13ucJ1Z0YtdajklNwxEL6lhHGhFCI0diAw==", + "dependencies": { + "Azure.Core": "1.23.0", + "System.Memory": "4.5.4", + "System.Text.Json": "4.7.2", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, "Azure.Storage.Blobs": { "type": "Direct", "requested": "[12.11.0, )",