9bf0c29add
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.
526 lines
16 KiB
Markdown
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
|