// 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 System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.ProjectSystem.Properties; using Microsoft.VisualStudio.Threading; using Task = System.Threading.Tasks.Task; namespace Microsoft.VisualStudio.SecretManager { /// /// Provides an thread-safe access the secrets.json file based on the UserSecretsId property in a configured project. /// internal class ProjectLocalSecretsManager : Shell.IVsProjectSecrets, Shell.SVsProjectLocalSecrets { private const string UserSecretsPropertyName = "UserSecretsId"; private readonly AsyncSemaphore _semaphore; private readonly IProjectPropertiesProvider _propertiesProvider; private readonly Lazy _services; public ProjectLocalSecretsManager(IProjectPropertiesProvider propertiesProvider, Lazy serviceProvider) { _propertiesProvider = propertiesProvider ?? throw new ArgumentNullException(nameof(propertiesProvider)); _services = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); _semaphore = new AsyncSemaphore(1); } public string SanitizeName(string name) => name; public IReadOnlyCollection GetInvalidCharactersFrom(string name) => Array.Empty(); public async Task AddSecretAsync(string name, string value, CancellationToken cancellationToken = default) { EnsureKeyNameIsValid(name); await TaskScheduler.Default; using (await _semaphore.EnterAsync(cancellationToken)) using (var store = await GetOrCreateStoreAsync(cancellationToken)) { if (store.ContainsKey(name)) { throw new ArgumentException(Resources.Error_SecretAlreadyExists, nameof(name)); } store.Set(name, value); await store.SaveAsync(cancellationToken); } } public async Task SetSecretAsync(string name, string value, CancellationToken cancellationToken = default) { EnsureKeyNameIsValid(name); await TaskScheduler.Default; using (await _semaphore.EnterAsync(cancellationToken)) using (var store = await GetOrCreateStoreAsync(cancellationToken)) { store.Set(name, value); await store.SaveAsync(cancellationToken); } } public async Task GetSecretAsync(string name, CancellationToken cancellationToken = default) { EnsureKeyNameIsValid(name); await TaskScheduler.Default; using (await _semaphore.EnterAsync(cancellationToken)) using (var store = await GetOrCreateStoreAsync(cancellationToken)) { return store.Get(name); } } public async Task> GetSecretNamesAsync(CancellationToken cancellationToken = default) { await TaskScheduler.Default; using (await _semaphore.EnterAsync(cancellationToken)) using (var store = await GetOrCreateStoreAsync(cancellationToken)) { return store.ReadOnlyKeys; } } public async Task> GetSecretsAsync(CancellationToken cancellationToken = default) { await TaskScheduler.Default; using (await _semaphore.EnterAsync(cancellationToken)) using (var store = await GetOrCreateStoreAsync(cancellationToken)) { return store.Values; } } public async Task RemoveSecretAsync(string name, CancellationToken cancellationToken = default) { EnsureKeyNameIsValid(name); await TaskScheduler.Default; using (await _semaphore.EnterAsync(cancellationToken)) using (var store = await GetOrCreateStoreAsync(cancellationToken)) { if (store.Remove(name)) { await store.SaveAsync(cancellationToken); return true; } return false; } } private void EnsureKeyNameIsValid(string name) { if (name == null) { throw new ArgumentNullException(nameof(name)); } if (name.Length == 0) { throw new ArgumentException(nameof(name)); } } private async Task GetOrCreateStoreAsync(CancellationToken cancellationToken) { var userSecretsId = await _propertiesProvider.GetCommonProperties().GetEvaluatedPropertyValueAsync(UserSecretsPropertyName); if (string.IsNullOrEmpty(userSecretsId)) { userSecretsId = Guid.NewGuid().ToString(); await _propertiesProvider.GetCommonProperties().SetPropertyValueAsync(UserSecretsPropertyName, userSecretsId); } var store = new SecretStore(userSecretsId); await store.LoadAsync(cancellationToken); return store; } } }