# Implementation Plan: SecureStore Auto-Initialization & Tree Flattening ## Overview This plan implements automatic SecureStore initialization on config load and flattens the tree hierarchy from "Secure Stores > secrets > [keys]" to "Secure Store > [keys]". ## Design Decisions (from brainstorming) 1. **Auto-creation**: Fully automatic on config folder load when `AutoCreateStore=true` 2. **Path updates**: Write actual paths back to appsettings.json after creating files 3. **Tree structure**: Single "Secure Store" node with secrets as direct children 4. **Authentication**: Keyfile-only (remove password option entirely) 5. **Lock concept**: Removed - store is always open while config folder is loaded 6. **Icon**: Key icon (🔑) for "Secure Store" node 7. **Right panel**: Show instructions "Select a secret to edit" when store node selected 8. **Error handling**: Show error dialog if keyfile missing/corrupted --- ## Task 1: Update TreeNodeType enum **File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs` **Changes:** - Remove `SecureStoresFolder` enum value (no longer needed) - Keep `SecureStore` for the single store node - Keep `Secret` for individual secrets **Before (lines 5-13):** ```csharp public enum TreeNodeType { Folder, SettingsSection, Pipeline, SecureStoresFolder, // The "Secure Stores" folder SecureStore, // Individual store files Secret // Individual secrets within a store } ``` **After:** ```csharp public enum TreeNodeType { Folder, SettingsSection, Pipeline, SecureStore, // The single secure store node Secret // Individual secrets within a store } ``` --- ## Task 2: Remove lock-related properties from TreeNodeViewModel **File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs` **Remove these properties (lines 55-79):** - `IsUnlocked` property and backing field - `IsLocked` computed property - `LockIcon` computed property **Keep:** - `StorePath` and `KeyFilePath` (needed for store identification) - `SecretKey` (needed for secret nodes) --- ## Task 3: Remove password-related methods from ISecureStoreManager **File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs` **Remove these methods:** - `CreateStoreWithPassword(string storePath, string password)` (lines 30-35) - `OpenStoreWithPassword(string storePath, string password)` (lines 44-49) --- ## Task 4: Remove password-related methods from SecureStoreManager **File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs` **Remove these methods:** - `CreateStoreWithPassword()` (lines 75-96) - `OpenStoreWithPassword()` (lines 120-140) --- ## Task 5: Add EnsureRequiredKeys method to ISecureStoreManager **File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs` **Add new method:** ```csharp /// /// Ensures all required keys exist in the store, creating blank values for any missing keys. /// /// List of keys that must exist. /// List of keys that were added. IReadOnlyList EnsureRequiredKeys(IEnumerable requiredKeys); ``` --- ## Task 6: Implement EnsureRequiredKeys in SecureStoreManager **File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs` **Add implementation:** ```csharp /// public IReadOnlyList EnsureRequiredKeys(IEnumerable requiredKeys) { ThrowIfDisposed(); if (_secretsManager == null) throw new InvalidOperationException("No store is currently open."); var addedKeys = new List(); foreach (var key in requiredKeys) { if (!_keys.Contains(key)) { _logger.LogInformation("Adding missing required key: {Key}", key); SetSecret(key, string.Empty); addedKeys.Add(key); } } if (addedKeys.Count > 0) { Save(); } return addedKeys.AsReadOnly(); } ``` --- ## Task 7: Delete locked/unlocked form files **Delete these files entirely:** ViewModels: - `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreLockedFormViewModel.cs` - `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreUnlockedFormViewModel.cs` Views: - `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreLockedFormView.axaml` - `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreLockedFormView.axaml.cs` - `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreUnlockedFormView.axaml` - `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreUnlockedFormView.axaml.cs` --- ## Task 8: Create SecureStoreInfoFormViewModel **File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreInfoFormViewModel.cs` (new file) **Content:** ```csharp namespace JdeScoping.ConfigManager.ViewModels.Forms; /// /// View model for the SecureStore info panel shown when the store node is selected. /// public class SecureStoreInfoFormViewModel : ViewModelBase { /// /// Gets the instruction text to display. /// public string InstructionText => "Select a secret from the tree to view or edit its value."; /// /// Gets the store path for display. /// public string StorePath { get; } /// /// Gets the key file path for display. /// public string KeyFilePath { get; } /// /// Gets the number of secrets in the store. /// public int SecretCount { get; } public SecureStoreInfoFormViewModel(string storePath, string keyFilePath, int secretCount) { StorePath = storePath; KeyFilePath = keyFilePath; SecretCount = secretCount; } } ``` --- ## Task 9: Create SecureStoreInfoFormView **File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreInfoFormView.axaml` (new file) **Content:** Simple panel with instruction text and optional store info display. **File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreInfoFormView.axaml.cs` (new file) **Content:** Code-behind for the view. --- ## Task 10: Update MainWindowViewModel - Remove lock/unlock commands and state **File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs` **Remove:** - `_openStores` dictionary (line 41) - `_selectedStoreNode` field (line 42) - `UnlockStoreCommand` and related methods - `LockStoreCommand` and related methods - `RaiseSecureStoreCommandsCanExecuteChanged()` method - `CreateLockedStoreFormViewModel()` method - `CreateUnlockedStoreFormViewModel()` method - `FindParentStoreNode()` method (if only used for lock state) --- ## Task 11: Update MainWindowViewModel - Add auto-initialization logic **File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs` **Add new method `InitializeSecureStoreAsync()`:** ```csharp /// /// Initializes the SecureStore automatically on config load. /// Creates the store if it doesn't exist and AutoCreateStore is true. /// Opens the store and ensures all required keys exist. /// private async Task InitializeSecureStoreAsync() { if (_appSettings?.SecureStore == null) return; if (string.IsNullOrEmpty(ConfigFolderPath) || ConfigFolderPath == "No folder selected") return; var secureStoreConfig = _appSettings.SecureStore; // Resolve paths relative to config folder var storePath = Path.IsPathRooted(secureStoreConfig.StorePath) ? secureStoreConfig.StorePath : Path.Combine(ConfigFolderPath, secureStoreConfig.StorePath); var keyFilePath = Path.IsPathRooted(secureStoreConfig.KeyFilePath) ? secureStoreConfig.KeyFilePath : Path.Combine(ConfigFolderPath, secureStoreConfig.KeyFilePath); try { if (!File.Exists(storePath)) { if (!secureStoreConfig.AutoCreateStore) { _logger?.LogWarning("SecureStore not found and AutoCreateStore is false"); return; } // Create new store with keyfile _logger?.LogInformation("Creating SecureStore at {StorePath}", storePath); _secureStoreManager.CreateStore(storePath, keyFilePath); // Update appsettings.json with actual paths secureStoreConfig.StorePath = GetRelativePath(ConfigFolderPath, storePath); secureStoreConfig.KeyFilePath = GetRelativePath(ConfigFolderPath, keyFilePath); await SaveAppSettingsAsync(); } else { // Open existing store if (!File.Exists(keyFilePath)) { await _dialogService!.ShowErrorAsync( "SecureStore Error", $"Key file not found: {keyFilePath}\n\nThe SecureStore cannot be opened without its key file."); return; } _secureStoreManager.OpenStore(storePath, keyFilePath); } // Ensure all required keys exist if (secureStoreConfig.RequiredKeys?.Count > 0) { var addedKeys = _secureStoreManager.EnsureRequiredKeys(secureStoreConfig.RequiredKeys); if (addedKeys.Count > 0) { _logger?.LogInformation("Added {Count} missing required keys", addedKeys.Count); } } } catch (Exception ex) { _logger?.LogError(ex, "Failed to initialize SecureStore"); await _dialogService!.ShowErrorAsync( "SecureStore Error", $"Failed to initialize SecureStore:\n\n{ex.Message}"); } } private static string GetRelativePath(string basePath, string fullPath) { var baseUri = new Uri(basePath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar); var fullUri = new Uri(fullPath); return Uri.UnescapeDataString(baseUri.MakeRelativeUri(fullUri).ToString().Replace('/', Path.DirectorySeparatorChar)); } ``` --- ## Task 12: Update MainWindowViewModel - Modify BuildTreeNodes **File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs` **Replace the Secure Stores section in `BuildTreeNodes()` (around lines 464-467):** **Before:** ```csharp // Secure Stores folder var secureStoresFolder = new TreeNodeViewModel("Secure Stores", "🔑", TreeNodeType.SecureStoresFolder) { IsExpanded = true }; LoadConfiguredSecureStore(secureStoresFolder); TreeNodes.Add(secureStoresFolder); ``` **After:** ```csharp // Secure Store node (single store, secrets as direct children) var secureStoreNode = CreateSecureStoreNode(); if (secureStoreNode != null) { TreeNodes.Add(secureStoreNode); } ``` **Add new method `CreateSecureStoreNode()`:** ```csharp private TreeNodeViewModel? CreateSecureStoreNode() { if (_appSettings?.SecureStore == null) return null; if (string.IsNullOrEmpty(ConfigFolderPath) || ConfigFolderPath == "No folder selected") return null; var storePath = Path.IsPathRooted(_appSettings.SecureStore.StorePath) ? _appSettings.SecureStore.StorePath : Path.Combine(ConfigFolderPath, _appSettings.SecureStore.StorePath); var keyFilePath = Path.IsPathRooted(_appSettings.SecureStore.KeyFilePath) ? _appSettings.SecureStore.KeyFilePath : Path.Combine(ConfigFolderPath, _appSettings.SecureStore.KeyFilePath); var storeNode = new TreeNodeViewModel("Secure Store", "🔑", TreeNodeType.SecureStore) { StorePath = storePath, KeyFilePath = keyFilePath, SectionKey = storePath, IsExpanded = true }; // Add secrets as direct children if store is open if (_secureStoreManager.IsStoreOpen) { foreach (var key in _secureStoreManager.GetKeys().OrderBy(k => k)) { var secretNode = new TreeNodeViewModel(key, "🔐", TreeNodeType.Secret) { SecretKey = key }; storeNode.Children.Add(secretNode); } } return storeNode; } ``` --- ## Task 13: Update MainWindowViewModel - Modify OnSelectedNodeChanged **File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs` **Update the switch statement in `OnSelectedNodeChanged()` (around lines 530-557):** **Remove:** - `case TreeNodeType.SecureStoresFolder:` block - Lock/unlock logic in `case TreeNodeType.SecureStore:` block **Replace with:** ```csharp case TreeNodeType.SecureStore: SelectedFormViewModel = CreateSecureStoreInfoFormViewModel(); return; case TreeNodeType.Secret: SelectedFormViewModel = CreateSecretFormViewModel(_selectedNode); return; ``` **Add method:** ```csharp private SecureStoreInfoFormViewModel CreateSecureStoreInfoFormViewModel() { var storePath = _appSettings?.SecureStore?.StorePath ?? "Unknown"; var keyFilePath = _appSettings?.SecureStore?.KeyFilePath ?? "Unknown"; var secretCount = _secureStoreManager.IsStoreOpen ? _secureStoreManager.GetKeys().Count : 0; return new SecureStoreInfoFormViewModel(storePath, keyFilePath, secretCount); } ``` --- ## Task 14: Update MainWindowViewModel - Call InitializeSecureStoreAsync on load **File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs` **Find the method that loads the config folder (likely `LoadConfigFolderAsync` or similar).** **Add call to `InitializeSecureStoreAsync()` after loading appsettings.json but before building tree nodes:** ```csharp // After loading appsettings _appSettings = await _configFileService.LoadAppSettingsAsync(appSettingsPath, ct); // Initialize SecureStore (auto-create if needed, open, sync required keys) await InitializeSecureStoreAsync(); // Then build tree nodes BuildTreeNodes(); ``` --- ## Task 15: Remove LoadConfiguredSecureStore method **File:** `NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs` **Delete the `LoadConfiguredSecureStore()` method entirely (lines 474-514).** This is replaced by `CreateSecureStoreNode()` and `InitializeSecureStoreAsync()`. --- ## Task 16: Update DataTemplates for form views **File:** `NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml` (or wherever DataTemplates are defined) **Remove:** - DataTemplate for `SecureStoreLockedFormViewModel` - DataTemplate for `SecureStoreUnlockedFormViewModel` **Add:** - DataTemplate for `SecureStoreInfoFormViewModel` --- ## Task 17: Update tests **File:** `NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs` **Update or remove tests related to:** - `SecureStoresFolder` node type - Lock/unlock commands - Password-based store operations **Add tests for:** - Auto-creation of SecureStore on config load - Sync of missing required keys - Flattened tree structure --- ## Task 18: Build and verify ```bash dotnet build NEW/JdeScoping.slnx dotnet test NEW/JdeScoping.slnx ``` --- ## Execution Order 1. Tasks 1-2: TreeNodeViewModel changes (enum, remove lock properties) 2. Tasks 3-6: SecureStoreManager interface and implementation changes 3. Tasks 7-9: Delete old form files, create new info form 4. Tasks 10-15: MainWindowViewModel changes (largest task) 5. Task 16: Update XAML DataTemplates 6. Task 17: Update tests 7. Task 18: Build and verify --- ## Risk Areas 1. **Missing references**: Removing lock/unlock commands may break menu items or toolbar buttons 2. **View DataTemplates**: Must update form selector to use new `SecureStoreInfoFormViewModel` 3. **Test coverage**: Existing tests may expect lock/unlock behavior ## Verification Checklist - [ ] App builds without errors - [ ] All tests pass - [ ] Opening a config folder without SecureStore creates it automatically - [ ] Opening a config folder with existing SecureStore opens it - [ ] Missing required keys are added as blank values - [ ] Tree shows "Secure Store" with secrets directly underneath - [ ] Clicking "Secure Store" shows instruction panel - [ ] Clicking a secret shows the secret editor - [ ] Error dialog shown when keyfile is missing