Files
jdescopingtool/PLANS/securestore-auto-init-plan.md
Joseph Doherty 9bf0c29add refactor(configmanager): simplify SecureStore UI with unified info view
Consolidate SecureStoreLockedFormView and SecureStoreUnlockedFormView into
a single SecureStoreInfoFormView that displays store status and metadata.
Simplifies MainWindowViewModel by removing redundant state management.
Also adds design docs for RegexTransformer feature.
2026-01-22 09:40:38 -05:00

526 lines
16 KiB
Markdown

# 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
/// <summary>
/// Ensures all required keys exist in the store, creating blank values for any missing keys.
/// </summary>
/// <param name="requiredKeys">List of keys that must exist.</param>
/// <returns>List of keys that were added.</returns>
IReadOnlyList<string> EnsureRequiredKeys(IEnumerable<string> requiredKeys);
```
---
## Task 6: Implement EnsureRequiredKeys in SecureStoreManager
**File:** `NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs`
**Add implementation:**
```csharp
/// <inheritdoc />
public IReadOnlyList<string> EnsureRequiredKeys(IEnumerable<string> requiredKeys)
{
ThrowIfDisposed();
if (_secretsManager == null)
throw new InvalidOperationException("No store is currently open.");
var addedKeys = new List<string>();
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;
/// <summary>
/// View model for the SecureStore info panel shown when the store node is selected.
/// </summary>
public class SecureStoreInfoFormViewModel : ViewModelBase
{
/// <summary>
/// Gets the instruction text to display.
/// </summary>
public string InstructionText => "Select a secret from the tree to view or edit its value.";
/// <summary>
/// Gets the store path for display.
/// </summary>
public string StorePath { get; }
/// <summary>
/// Gets the key file path for display.
/// </summary>
public string KeyFilePath { get; }
/// <summary>
/// Gets the number of secrets in the store.
/// </summary>
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
/// <summary>
/// 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.
/// </summary>
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