From e1c2126e79aa214816f809c820496ad91d21c408 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 18:58:13 +0000 Subject: [PATCH 01/12] Add AWS Secrets Manager service with navigable JSON secret values Adds a new SecretsManager service that lets users browse and retrieve secrets via the aws: drive. JSON secrets are navigable as containers where each top-level key becomes a child item accessible via Get-Content. Filesystem hierarchy: aws:///secretsmanager/secrets// https://claude.ai/code/session_01QhMmDXjkSLNqcz11WmUwMT --- MountAws.UnitTests/SecretsManagerTests.cs | 30 +++++++ MountAws/MountAws.csproj | 1 + MountAws/Services/Core/RegionHandler.cs | 2 + .../Services/SecretsManager/Formats.ps1xml | 61 ++++++++++++++ MountAws/Services/SecretsManager/Routes.cs | 20 +++++ .../Services/SecretsManager/SecretHandler.cs | 84 +++++++++++++++++++ .../Services/SecretsManager/SecretItem.cs | 24 ++++++ .../SecretsManager/SecretValueHandler.cs | 37 ++++++++ .../SecretsManager/SecretValueItem.cs | 21 +++++ .../Services/SecretsManager/SecretsHandler.cs | 32 +++++++ .../SecretsManagerApiExtensions.cs | 37 ++++++++ .../SecretsManager/SecretsManagerRegistrar.cs | 15 ++++ .../SecretsManagerRootHandler.cs | 27 ++++++ 13 files changed, 391 insertions(+) create mode 100644 MountAws.UnitTests/SecretsManagerTests.cs create mode 100644 MountAws/Services/SecretsManager/Formats.ps1xml create mode 100644 MountAws/Services/SecretsManager/Routes.cs create mode 100644 MountAws/Services/SecretsManager/SecretHandler.cs create mode 100644 MountAws/Services/SecretsManager/SecretItem.cs create mode 100644 MountAws/Services/SecretsManager/SecretValueHandler.cs create mode 100644 MountAws/Services/SecretsManager/SecretValueItem.cs create mode 100644 MountAws/Services/SecretsManager/SecretsHandler.cs create mode 100644 MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs create mode 100644 MountAws/Services/SecretsManager/SecretsManagerRegistrar.cs create mode 100644 MountAws/Services/SecretsManager/SecretsManagerRootHandler.cs diff --git a/MountAws.UnitTests/SecretsManagerTests.cs b/MountAws.UnitTests/SecretsManagerTests.cs new file mode 100644 index 0000000..eb600ca --- /dev/null +++ b/MountAws.UnitTests/SecretsManagerTests.cs @@ -0,0 +1,30 @@ +using System; +using AwesomeAssertions; +using MountAnything; +using MountAnything.Routing; +using MountAws.Services.SecretsManager; +using Xunit; + +namespace MountAws.UnitTests; + +public class SecretsManagerTests +{ + private readonly Router _router; + + public SecretsManagerTests() + { + var provider = new MountAwsProvider(); + _router = provider.CreateRouter(); + } + + [Theory] + [InlineData("myprofile/us-east-1/secretsmanager", typeof(SecretsManagerRootHandler))] + [InlineData("myprofile/us-east-1/secretsmanager/secrets", typeof(SecretsHandler))] + [InlineData("myprofile/us-east-1/secretsmanager/secrets/my-secret", typeof(SecretHandler))] + [InlineData("myprofile/us-east-1/secretsmanager/secrets/my-secret/password", typeof(SecretValueHandler))] + public void SecretsManagerRoutesResolveToCorrectHandlers(string path, Type expectedHandlerType) + { + var resolver = _router.GetResolver(new ItemPath(path)); + resolver.HandlerType.Should().Be(expectedHandlerType); + } +} diff --git a/MountAws/MountAws.csproj b/MountAws/MountAws.csproj index 3d2608b..9680e2b 100644 --- a/MountAws/MountAws.csproj +++ b/MountAws/MountAws.csproj @@ -45,6 +45,7 @@ + diff --git a/MountAws/Services/Core/RegionHandler.cs b/MountAws/Services/Core/RegionHandler.cs index b7b37a8..d3d2aa8 100644 --- a/MountAws/Services/Core/RegionHandler.cs +++ b/MountAws/Services/Core/RegionHandler.cs @@ -11,6 +11,7 @@ using MountAws.Services.Lambda; using MountAws.Services.Route53; using MountAws.Services.S3; +using MountAws.Services.SecretsManager; using MountAws.Services.ServiceDiscovery; using MountAws.Services.Wafv2; @@ -49,6 +50,7 @@ protected override IEnumerable GetChildItemsImpl() yield return Services.Rds.RootHandler.CreateItem(Path); yield return Route53RootHandler.CreateItem(Path); yield return S3RootHandler.CreateItem(Path); + yield return SecretsManagerRootHandler.CreateItem(Path); yield return ServiceDiscoveryRootHandler.CreateItem(Path); yield return Wafv2RootHandler.CreateItem(Path); } diff --git a/MountAws/Services/SecretsManager/Formats.ps1xml b/MountAws/Services/SecretsManager/Formats.ps1xml new file mode 100644 index 0000000..e945a1d --- /dev/null +++ b/MountAws/Services/SecretsManager/Formats.ps1xml @@ -0,0 +1,61 @@ + + + + SecretItem + + MountAws.Services.SecretsManager.SecretItem + + + + + + + + + + + + + + + Name + + + Description + + + LastChangedDate + + + + + + + + SecretValueItem + + MountAws.Services.SecretsManager.SecretValueItem + + + + + + + + + + + + + Key + + + Value + + + + + + + + diff --git a/MountAws/Services/SecretsManager/Routes.cs b/MountAws/Services/SecretsManager/Routes.cs new file mode 100644 index 0000000..ff0f9df --- /dev/null +++ b/MountAws/Services/SecretsManager/Routes.cs @@ -0,0 +1,20 @@ +using MountAnything.Routing; + +namespace MountAws.Services.SecretsManager; + +public class Routes : IServiceRoutes +{ + public void AddServiceRoutes(Route regionRoute) + { + regionRoute.MapLiteral("secretsmanager", secretsManager => + { + secretsManager.MapLiteral("secrets", secrets => + { + secrets.Map(secret => + { + secret.Map(); + }); + }); + }); + } +} diff --git a/MountAws/Services/SecretsManager/SecretHandler.cs b/MountAws/Services/SecretsManager/SecretHandler.cs new file mode 100644 index 0000000..1d45804 --- /dev/null +++ b/MountAws/Services/SecretsManager/SecretHandler.cs @@ -0,0 +1,84 @@ +using System.Text; +using System.Text.Json; +using Amazon.SecretsManager; +using MountAnything; +using MountAnything.Content; + +namespace MountAws.Services.SecretsManager; + +public class SecretHandler : PathHandler, IContentReaderHandler +{ + private readonly IAmazonSecretsManager _secretsManager; + private readonly SecretsHandler _parentHandler; + + public SecretHandler(ItemPath path, IPathHandlerContext context, IAmazonSecretsManager secretsManager) : base(path, context) + { + _secretsManager = secretsManager; + _parentHandler = new SecretsHandler(path.Parent, context, secretsManager); + } + + protected override IItem? GetItemImpl() + { + return _parentHandler.GetChildItems() + .SingleOrDefault(i => i.ItemName.Equals(ItemName, StringComparison.OrdinalIgnoreCase)); + } + + protected override IEnumerable GetChildItemsImpl() + { + var secretString = GetSecretString(); + if (secretString == null) yield break; + + if (TryParseJsonObject(secretString, out var properties)) + { + foreach (var property in properties) + { + yield return new SecretValueItem(Path, property.Key, property.Value); + } + } + } + + public IStreamContentReader GetContentReader() + { + var secretString = GetSecretString(); + if (secretString == null) + { + throw new InvalidOperationException("Secret does not contain a string value"); + } + return new StreamContentReader(new MemoryStream(Encoding.UTF8.GetBytes(secretString))); + } + + private string? GetSecretString() + { + try + { + var response = _secretsManager.GetSecretValue(ItemName); + return response.SecretString; + } + catch + { + return null; + } + } + + private static bool TryParseJsonObject(string value, out Dictionary properties) + { + properties = new Dictionary(); + try + { + using var doc = JsonDocument.Parse(value); + if (doc.RootElement.ValueKind != JsonValueKind.Object) return false; + + foreach (var property in doc.RootElement.EnumerateObject()) + { + properties[property.Name] = property.Value.ValueKind == JsonValueKind.String + ? property.Value.GetString()! + : property.Value.GetRawText(); + } + return true; + } + catch (JsonException) + { + return false; + } + } +} diff --git a/MountAws/Services/SecretsManager/SecretItem.cs b/MountAws/Services/SecretsManager/SecretItem.cs new file mode 100644 index 0000000..757ef2f --- /dev/null +++ b/MountAws/Services/SecretsManager/SecretItem.cs @@ -0,0 +1,24 @@ +using Amazon.SecretsManager.Model; +using MountAnything; + +namespace MountAws.Services.SecretsManager; + +public class SecretItem : AwsItem +{ + public SecretItem(ItemPath parentPath, SecretListEntry secret) : base(parentPath, secret) + { + ItemName = secret.Name; + } + + public override string ItemName { get; } + public override bool IsContainer => true; + + public string Name => UnderlyingObject.Name; + public string? Description => UnderlyingObject.Description; + public string ARN => UnderlyingObject.ARN; + public DateTime? LastChangedDate => UnderlyingObject.LastChangedDate; + public DateTime? CreatedDate => UnderlyingObject.CreatedDate; + + public override string? WebUrl => + UrlBuilder.CombineWith($"secretsmanager/secret?name={Uri.EscapeDataString(ItemName)}"); +} diff --git a/MountAws/Services/SecretsManager/SecretValueHandler.cs b/MountAws/Services/SecretsManager/SecretValueHandler.cs new file mode 100644 index 0000000..acd38df --- /dev/null +++ b/MountAws/Services/SecretsManager/SecretValueHandler.cs @@ -0,0 +1,37 @@ +using System.Text; +using Amazon.SecretsManager; +using MountAnything; +using MountAnything.Content; + +namespace MountAws.Services.SecretsManager; + +public class SecretValueHandler : PathHandler, IContentReaderHandler +{ + private readonly SecretHandler _parentHandler; + + public SecretValueHandler(ItemPath path, IPathHandlerContext context, IAmazonSecretsManager secretsManager) : base(path, context) + { + _parentHandler = new SecretHandler(path.Parent, context, secretsManager); + } + + protected override IItem? GetItemImpl() + { + return _parentHandler.GetChildItems() + .SingleOrDefault(i => i.ItemName.Equals(ItemName, StringComparison.OrdinalIgnoreCase)); + } + + protected override IEnumerable GetChildItemsImpl() + { + yield break; + } + + public IStreamContentReader GetContentReader() + { + var item = GetItem() as SecretValueItem; + if (item == null) + { + throw new InvalidOperationException("Secret value does not exist"); + } + return new StreamContentReader(new MemoryStream(Encoding.UTF8.GetBytes(item.Value))); + } +} diff --git a/MountAws/Services/SecretsManager/SecretValueItem.cs b/MountAws/Services/SecretsManager/SecretValueItem.cs new file mode 100644 index 0000000..76f13ec --- /dev/null +++ b/MountAws/Services/SecretsManager/SecretValueItem.cs @@ -0,0 +1,21 @@ +using System.Management.Automation; +using MountAnything; + +namespace MountAws.Services.SecretsManager; + +public class SecretValueItem : AwsItem +{ + public SecretValueItem(ItemPath parentPath, string key, string value) : base(parentPath, new PSObject(new + { + Key = key, + Value = value + })) + { + ItemName = key; + Value = value; + } + + public override string ItemName { get; } + public override bool IsContainer => false; + public string Value { get; } +} diff --git a/MountAws/Services/SecretsManager/SecretsHandler.cs b/MountAws/Services/SecretsManager/SecretsHandler.cs new file mode 100644 index 0000000..e9b7f26 --- /dev/null +++ b/MountAws/Services/SecretsManager/SecretsHandler.cs @@ -0,0 +1,32 @@ +using Amazon.SecretsManager; +using MountAnything; +using MountAws.Services.Core; + +namespace MountAws.Services.SecretsManager; + +public class SecretsHandler : PathHandler +{ + private readonly IAmazonSecretsManager _secretsManager; + + public static IItem CreateItem(ItemPath parentPath) + { + return new GenericContainerItem(parentPath, "secrets", + "Navigate secrets as a virtual filesystem"); + } + + public SecretsHandler(ItemPath path, IPathHandlerContext context, IAmazonSecretsManager secretsManager) : base(path, context) + { + _secretsManager = secretsManager; + } + + protected override IItem? GetItemImpl() + { + return CreateItem(ParentPath); + } + + protected override IEnumerable GetChildItemsImpl() + { + return _secretsManager.ListSecrets() + .Select(s => new SecretItem(Path, s)); + } +} diff --git a/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs b/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs new file mode 100644 index 0000000..7fd97b4 --- /dev/null +++ b/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs @@ -0,0 +1,37 @@ +using Amazon.SecretsManager; +using Amazon.SecretsManager.Model; +using static MountAws.PagingHelper; + +namespace MountAws.Services.SecretsManager; + +public static class SecretsManagerApiExtensions +{ + public static IEnumerable ListSecrets(this IAmazonSecretsManager secretsManager) + { + return Paginate(nextToken => + { + var response = secretsManager.ListSecretsAsync(new ListSecretsRequest + { + NextToken = nextToken + }).GetAwaiter().GetResult(); + + return (response.SecretList, response.NextToken); + }); + } + + public static DescribeSecretResponse DescribeSecret(this IAmazonSecretsManager secretsManager, string secretId) + { + return secretsManager.DescribeSecretAsync(new DescribeSecretRequest + { + SecretId = secretId + }).GetAwaiter().GetResult(); + } + + public static GetSecretValueResponse GetSecretValue(this IAmazonSecretsManager secretsManager, string secretId) + { + return secretsManager.GetSecretValueAsync(new GetSecretValueRequest + { + SecretId = secretId + }).GetAwaiter().GetResult(); + } +} diff --git a/MountAws/Services/SecretsManager/SecretsManagerRegistrar.cs b/MountAws/Services/SecretsManager/SecretsManagerRegistrar.cs new file mode 100644 index 0000000..0549b42 --- /dev/null +++ b/MountAws/Services/SecretsManager/SecretsManagerRegistrar.cs @@ -0,0 +1,15 @@ +using Amazon; +using Amazon.SecretsManager; +using Amazon.Runtime; +using Autofac; + +namespace MountAws.Services.SecretsManager; + +public class SecretsManagerRegistrar : IServiceRegistrar +{ + public void Register(ContainerBuilder builder) + { + builder.RegisterType().As() + .UsingConstructor(typeof(AWSCredentials), typeof(RegionEndpoint)); + } +} diff --git a/MountAws/Services/SecretsManager/SecretsManagerRootHandler.cs b/MountAws/Services/SecretsManager/SecretsManagerRootHandler.cs new file mode 100644 index 0000000..c55174a --- /dev/null +++ b/MountAws/Services/SecretsManager/SecretsManagerRootHandler.cs @@ -0,0 +1,27 @@ +using MountAnything; +using MountAws.Services.Core; + +namespace MountAws.Services.SecretsManager; + +public class SecretsManagerRootHandler : PathHandler +{ + public static IItem CreateItem(ItemPath parentPath) + { + return new GenericContainerItem(parentPath, "secretsmanager", + "Navigate AWS Secrets Manager secrets in a virtual filesystem"); + } + + public SecretsManagerRootHandler(ItemPath path, IPathHandlerContext context) : base(path, context) + { + } + + protected override IItem? GetItemImpl() + { + return CreateItem(ParentPath); + } + + protected override IEnumerable GetChildItemsImpl() + { + yield return SecretsHandler.CreateItem(Path); + } +} From d82dc60acdb5117e99a36b9a2356a0232f836652 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 19:06:27 +0000 Subject: [PATCH 02/12] Add Get-ItemProperty and Set-ItemProperty support for JSON secrets For secrets containing JSON objects, Get-ItemProperty returns the key-value pairs as PowerShell properties and Set-ItemProperty updates individual keys in the JSON without replacing the entire secret. https://claude.ai/code/session_01QhMmDXjkSLNqcz11WmUwMT --- .../Services/SecretsManager/SecretHandler.cs | 41 ++++++++++++++++++- .../SecretsManagerApiExtensions.cs | 9 ++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/MountAws/Services/SecretsManager/SecretHandler.cs b/MountAws/Services/SecretsManager/SecretHandler.cs index 1d45804..142a02d 100644 --- a/MountAws/Services/SecretsManager/SecretHandler.cs +++ b/MountAws/Services/SecretsManager/SecretHandler.cs @@ -1,12 +1,14 @@ +using System.Management.Automation; using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using Amazon.SecretsManager; using MountAnything; using MountAnything.Content; namespace MountAws.Services.SecretsManager; -public class SecretHandler : PathHandler, IContentReaderHandler +public class SecretHandler : PathHandler, IContentReaderHandler, ISetItemPropertiesHandler { private readonly IAmazonSecretsManager _secretsManager; private readonly SecretsHandler _parentHandler; @@ -47,6 +49,43 @@ public IStreamContentReader GetContentReader() return new StreamContentReader(new MemoryStream(Encoding.UTF8.GetBytes(secretString))); } + public override IEnumerable GetItemProperties(HashSet propertyNames, Func pathResolver) + { + var secretString = GetSecretString(); + if (secretString == null || !TryParseJsonObject(secretString, out var properties)) + { + return base.GetItemProperties(propertyNames, pathResolver); + } + + var psObject = new PSObject(); + foreach (var property in properties) + { + psObject.Properties.Add(new PSNoteProperty(property.Key, property.Value)); + } + return psObject.AsItemProperties().WherePropertiesMatch(propertyNames); + } + + public void SetItemProperties(ICollection propertyValues) + { + var secretString = GetSecretString() + ?? throw new InvalidOperationException("Secret does not contain a string value"); + + var jsonNode = JsonNode.Parse(secretString) + ?? throw new InvalidOperationException("Secret value is not valid JSON"); + + if (jsonNode is not JsonObject jsonObject) + { + throw new InvalidOperationException("Secret value is not a JSON object"); + } + + foreach (var property in propertyValues) + { + jsonObject[property.Name] = JsonValue.Create(property.Value?.ToString()); + } + + _secretsManager.PutSecretValue(ItemName, jsonObject.ToJsonString()); + } + private string? GetSecretString() { try diff --git a/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs b/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs index 7fd97b4..9d74b5e 100644 --- a/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs +++ b/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs @@ -34,4 +34,13 @@ public static GetSecretValueResponse GetSecretValue(this IAmazonSecretsManager s SecretId = secretId }).GetAwaiter().GetResult(); } + + public static void PutSecretValue(this IAmazonSecretsManager secretsManager, string secretId, string secretString) + { + secretsManager.PutSecretValueAsync(new PutSecretValueRequest + { + SecretId = secretId, + SecretString = secretString + }).GetAwaiter().GetResult(); + } } From 7d591db8ab8a1d69ddae50ac7fb6386189f18032 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 19:50:55 +0000 Subject: [PATCH 03/12] Add Set-Content support for Secrets Manager secrets and values Enable writing secret values via Set-Content at both levels: - SecretHandler: replaces the entire secret string - SecretValueHandler: updates an individual JSON key within the secret https://claude.ai/code/session_01QhMmDXjkSLNqcz11WmUwMT --- .../Services/SecretsManager/SecretHandler.cs | 12 ++++++++- .../SecretsManager/SecretValueHandler.cs | 26 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/MountAws/Services/SecretsManager/SecretHandler.cs b/MountAws/Services/SecretsManager/SecretHandler.cs index 142a02d..4f19411 100644 --- a/MountAws/Services/SecretsManager/SecretHandler.cs +++ b/MountAws/Services/SecretsManager/SecretHandler.cs @@ -8,7 +8,7 @@ namespace MountAws.Services.SecretsManager; -public class SecretHandler : PathHandler, IContentReaderHandler, ISetItemPropertiesHandler +public class SecretHandler : PathHandler, IContentReaderHandler, IContentWriterHandler, ISetItemPropertiesHandler { private readonly IAmazonSecretsManager _secretsManager; private readonly SecretsHandler _parentHandler; @@ -65,6 +65,16 @@ public override IEnumerable GetItemProperties(HashSet pro return psObject.AsItemProperties().WherePropertiesMatch(propertyNames); } + public IStreamContentWriter GetContentWriter() + { + return new StreamContentWriter(stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + var secretString = reader.ReadToEnd(); + _secretsManager.PutSecretValue(ItemName, secretString); + }); + } + public void SetItemProperties(ICollection propertyValues) { var secretString = GetSecretString() diff --git a/MountAws/Services/SecretsManager/SecretValueHandler.cs b/MountAws/Services/SecretsManager/SecretValueHandler.cs index acd38df..e6a1dc1 100644 --- a/MountAws/Services/SecretsManager/SecretValueHandler.cs +++ b/MountAws/Services/SecretsManager/SecretValueHandler.cs @@ -1,16 +1,19 @@ using System.Text; +using System.Text.Json.Nodes; using Amazon.SecretsManager; using MountAnything; using MountAnything.Content; namespace MountAws.Services.SecretsManager; -public class SecretValueHandler : PathHandler, IContentReaderHandler +public class SecretValueHandler : PathHandler, IContentReaderHandler, IContentWriterHandler { + private readonly IAmazonSecretsManager _secretsManager; private readonly SecretHandler _parentHandler; public SecretValueHandler(ItemPath path, IPathHandlerContext context, IAmazonSecretsManager secretsManager) : base(path, context) { + _secretsManager = secretsManager; _parentHandler = new SecretHandler(path.Parent, context, secretsManager); } @@ -34,4 +37,25 @@ public IStreamContentReader GetContentReader() } return new StreamContentReader(new MemoryStream(Encoding.UTF8.GetBytes(item.Value))); } + + public IStreamContentWriter GetContentWriter() + { + var secretName = Path.Parent.Name; + return new StreamContentWriter(stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + var newValue = reader.ReadToEnd(); + + var response = _secretsManager.GetSecretValue(secretName); + var jsonNode = JsonNode.Parse(response.SecretString) + ?? throw new InvalidOperationException("Secret value is not valid JSON"); + if (jsonNode is not JsonObject jsonObject) + { + throw new InvalidOperationException("Secret value is not a JSON object"); + } + + jsonObject[ItemName] = JsonValue.Create(newValue); + _secretsManager.PutSecretValue(secretName, jsonObject.ToJsonString()); + }); + } } From 2588ed040b4a4e91e552669869f2f6b34b09f55d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 22:56:16 +0000 Subject: [PATCH 04/12] Add hierarchical folder navigation for secrets with / in names Secrets with "/" in their names (e.g., entsvc/aigenmodel/bria-attribution) are now browsable as a folder hierarchy under the secrets container. Uses the ItemNavigator pattern (same as CloudWatch log groups) with MapRegex routing to capture multi-segment secret paths. - SecretNavigator splits secret names into virtual folders and leaf secrets - SecretFolderItem represents intermediate path segments as containers - SecretHandler handles folders, secrets, and JSON keys via API probing - SecretValueHandler removed (logic merged into SecretHandler) - SecretItem.ItemName now uses last segment only (not full name) https://claude.ai/code/session_01QhMmDXjkSLNqcz11WmUwMT --- MountAws.UnitTests/SecretsManagerTests.cs | 4 +- .../Services/SecretsManager/Formats.ps1xml | 21 +++ MountAws/Services/SecretsManager/Routes.cs | 8 +- .../SecretsManager/SecretFolderItem.cs | 18 +++ .../Services/SecretsManager/SecretHandler.cs | 139 +++++++++++++++--- .../Services/SecretsManager/SecretItem.cs | 34 +++-- .../SecretsManager/SecretNavigator.cs | 35 +++++ .../Services/SecretsManager/SecretPath.cs | 16 ++ .../SecretsManager/SecretValueHandler.cs | 61 -------- .../Services/SecretsManager/SecretsHandler.cs | 10 +- .../SecretsManagerApiExtensions.cs | 12 ++ 11 files changed, 258 insertions(+), 100 deletions(-) create mode 100644 MountAws/Services/SecretsManager/SecretFolderItem.cs create mode 100644 MountAws/Services/SecretsManager/SecretNavigator.cs create mode 100644 MountAws/Services/SecretsManager/SecretPath.cs delete mode 100644 MountAws/Services/SecretsManager/SecretValueHandler.cs diff --git a/MountAws.UnitTests/SecretsManagerTests.cs b/MountAws.UnitTests/SecretsManagerTests.cs index eb600ca..b32013d 100644 --- a/MountAws.UnitTests/SecretsManagerTests.cs +++ b/MountAws.UnitTests/SecretsManagerTests.cs @@ -21,7 +21,9 @@ public SecretsManagerTests() [InlineData("myprofile/us-east-1/secretsmanager", typeof(SecretsManagerRootHandler))] [InlineData("myprofile/us-east-1/secretsmanager/secrets", typeof(SecretsHandler))] [InlineData("myprofile/us-east-1/secretsmanager/secrets/my-secret", typeof(SecretHandler))] - [InlineData("myprofile/us-east-1/secretsmanager/secrets/my-secret/password", typeof(SecretValueHandler))] + [InlineData("myprofile/us-east-1/secretsmanager/secrets/my-secret/password", typeof(SecretHandler))] + [InlineData("myprofile/us-east-1/secretsmanager/secrets/entsvc/aigenmodel/bria-attribution", typeof(SecretHandler))] + [InlineData("myprofile/us-east-1/secretsmanager/secrets/entsvc/aigenmodel", typeof(SecretHandler))] public void SecretsManagerRoutesResolveToCorrectHandlers(string path, Type expectedHandlerType) { var resolver = _router.GetResolver(new ItemPath(path)); diff --git a/MountAws/Services/SecretsManager/Formats.ps1xml b/MountAws/Services/SecretsManager/Formats.ps1xml index e945a1d..e030765 100644 --- a/MountAws/Services/SecretsManager/Formats.ps1xml +++ b/MountAws/Services/SecretsManager/Formats.ps1xml @@ -31,6 +31,27 @@ + + SecretFolderItem + + MountAws.Services.SecretsManager.SecretFolderItem + + + + + + + + + + + ItemName + + + + + + SecretValueItem diff --git a/MountAws/Services/SecretsManager/Routes.cs b/MountAws/Services/SecretsManager/Routes.cs index ff0f9df..7e87818 100644 --- a/MountAws/Services/SecretsManager/Routes.cs +++ b/MountAws/Services/SecretsManager/Routes.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.DependencyInjection; using MountAnything.Routing; namespace MountAws.Services.SecretsManager; @@ -10,9 +11,12 @@ public void AddServiceRoutes(Route regionRoute) { secretsManager.MapLiteral("secrets", secrets => { - secrets.Map(secret => + secrets.MapRegex(@"(?.+)", secret => { - secret.Map(); + secret.ConfigureServices((services, match) => + { + services.AddSingleton(new SecretPath(match.Values["SecretPath"])); + }); }); }); }); diff --git a/MountAws/Services/SecretsManager/SecretFolderItem.cs b/MountAws/Services/SecretsManager/SecretFolderItem.cs new file mode 100644 index 0000000..836ae21 --- /dev/null +++ b/MountAws/Services/SecretsManager/SecretFolderItem.cs @@ -0,0 +1,18 @@ +using System.Management.Automation; +using MountAnything; + +namespace MountAws.Services.SecretsManager; + +public class SecretFolderItem : AwsItem +{ + public SecretFolderItem(ItemPath parentPath, ItemPath directoryPath) : base(parentPath, new PSObject(new + { + Name = directoryPath.Name + })) + { + ItemName = directoryPath.Name; + } + + public override string ItemName { get; } + public override bool IsContainer => true; +} diff --git a/MountAws/Services/SecretsManager/SecretHandler.cs b/MountAws/Services/SecretsManager/SecretHandler.cs index 4f19411..94ce6a8 100644 --- a/MountAws/Services/SecretsManager/SecretHandler.cs +++ b/MountAws/Services/SecretsManager/SecretHandler.cs @@ -11,23 +11,61 @@ namespace MountAws.Services.SecretsManager; public class SecretHandler : PathHandler, IContentReaderHandler, IContentWriterHandler, ISetItemPropertiesHandler { private readonly IAmazonSecretsManager _secretsManager; - private readonly SecretsHandler _parentHandler; + private readonly SecretNavigator _navigator; + private readonly SecretPath _secretPath; - public SecretHandler(ItemPath path, IPathHandlerContext context, IAmazonSecretsManager secretsManager) : base(path, context) + public SecretHandler(ItemPath path, IPathHandlerContext context, IAmazonSecretsManager secretsManager, + SecretNavigator navigator, SecretPath secretPath) : base(path, context) { _secretsManager = secretsManager; - _parentHandler = new SecretsHandler(path.Parent, context, secretsManager); + _navigator = navigator; + _secretPath = secretPath; } protected override IItem? GetItemImpl() { - return _parentHandler.GetChildItems() - .SingleOrDefault(i => i.ItemName.Equals(ItemName, StringComparison.OrdinalIgnoreCase)); + var fullPath = _secretPath.Value; + + // 1. Try as an actual secret + var secret = _secretsManager.DescribeSecretOrDefault(fullPath); + if (secret != null) + { + return new SecretItem(ParentPath, secret); + } + + // 2. Check if the last segment is a JSON key of a parent secret + var itemPath = new ItemPath(fullPath); + if (!itemPath.Parent.IsRoot) + { + var parentSecret = _secretsManager.DescribeSecretOrDefault(itemPath.Parent.FullName); + if (parentSecret != null) + { + var secretString = GetSecretString(itemPath.Parent.FullName); + if (secretString != null && TryParseJsonObject(secretString, out var props) && + props.TryGetValue(itemPath.Name, out var value)) + { + return new SecretValueItem(ParentPath, itemPath.Name, value); + } + } + } + + // 3. Must be a folder + return new SecretFolderItem(ParentPath, itemPath); } protected override IEnumerable GetChildItemsImpl() { - var secretString = GetSecretString(); + return GetItem() switch + { + SecretItem => GetSecretChildren(), + SecretFolderItem => GetFolderChildren(), + _ => Enumerable.Empty() + }; + } + + private IEnumerable GetSecretChildren() + { + var secretString = GetSecretString(_secretPath.Value); if (secretString == null) yield break; if (TryParseJsonObject(secretString, out var properties)) @@ -39,9 +77,26 @@ protected override IEnumerable GetChildItemsImpl() } } + private IEnumerable GetFolderChildren() + { + return _navigator.ListChildItems(Path, new ItemPath(_secretPath.Value)); + } + public IStreamContentReader GetContentReader() { - var secretString = GetSecretString(); + var item = GetItem(); + return item switch + { + SecretItem => GetSecretContentReader(), + SecretValueItem valueItem => new StreamContentReader( + new MemoryStream(Encoding.UTF8.GetBytes(valueItem.Value))), + _ => throw new InvalidOperationException("Cannot read content from a folder") + }; + } + + private IStreamContentReader GetSecretContentReader() + { + var secretString = GetSecretString(_secretPath.Value); if (secretString == null) { throw new InvalidOperationException("Secret does not contain a string value"); @@ -49,9 +104,56 @@ public IStreamContentReader GetContentReader() return new StreamContentReader(new MemoryStream(Encoding.UTF8.GetBytes(secretString))); } + public IStreamContentWriter GetContentWriter() + { + var item = GetItem(); + return item switch + { + SecretItem => GetSecretContentWriter(), + SecretValueItem => GetSecretValueContentWriter(), + _ => throw new InvalidOperationException("Cannot write content to a folder") + }; + } + + private IStreamContentWriter GetSecretContentWriter() + { + return new StreamContentWriter(stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + var secretString = reader.ReadToEnd(); + _secretsManager.PutSecretValue(_secretPath.Value, secretString); + }); + } + + private IStreamContentWriter GetSecretValueContentWriter() + { + var itemPath = new ItemPath(_secretPath.Value); + var secretName = itemPath.Parent.FullName; + var keyName = itemPath.Name; + + return new StreamContentWriter(stream => + { + using var reader = new StreamReader(stream, Encoding.UTF8); + var newValue = reader.ReadToEnd(); + + var response = _secretsManager.GetSecretValue(secretName); + var jsonNode = JsonNode.Parse(response.SecretString) + ?? throw new InvalidOperationException("Secret value is not valid JSON"); + if (jsonNode is not JsonObject jsonObject) + { + throw new InvalidOperationException("Secret value is not a JSON object"); + } + + jsonObject[keyName] = JsonValue.Create(newValue); + _secretsManager.PutSecretValue(secretName, jsonObject.ToJsonString()); + }); + } + public override IEnumerable GetItemProperties(HashSet propertyNames, Func pathResolver) { - var secretString = GetSecretString(); + if (GetItem() is not SecretItem) return base.GetItemProperties(propertyNames, pathResolver); + + var secretString = GetSecretString(_secretPath.Value); if (secretString == null || !TryParseJsonObject(secretString, out var properties)) { return base.GetItemProperties(propertyNames, pathResolver); @@ -65,19 +167,14 @@ public override IEnumerable GetItemProperties(HashSet pro return psObject.AsItemProperties().WherePropertiesMatch(propertyNames); } - public IStreamContentWriter GetContentWriter() + public void SetItemProperties(ICollection propertyValues) { - return new StreamContentWriter(stream => + if (GetItem() is not SecretItem) { - using var reader = new StreamReader(stream, Encoding.UTF8); - var secretString = reader.ReadToEnd(); - _secretsManager.PutSecretValue(ItemName, secretString); - }); - } + throw new InvalidOperationException("Can only set properties on a secret"); + } - public void SetItemProperties(ICollection propertyValues) - { - var secretString = GetSecretString() + var secretString = GetSecretString(_secretPath.Value) ?? throw new InvalidOperationException("Secret does not contain a string value"); var jsonNode = JsonNode.Parse(secretString) @@ -93,14 +190,14 @@ public void SetItemProperties(ICollection propertyValues) jsonObject[property.Name] = JsonValue.Create(property.Value?.ToString()); } - _secretsManager.PutSecretValue(ItemName, jsonObject.ToJsonString()); + _secretsManager.PutSecretValue(_secretPath.Value, jsonObject.ToJsonString()); } - private string? GetSecretString() + private string? GetSecretString(string secretName) { try { - var response = _secretsManager.GetSecretValue(ItemName); + var response = _secretsManager.GetSecretValue(secretName); return response.SecretString; } catch diff --git a/MountAws/Services/SecretsManager/SecretItem.cs b/MountAws/Services/SecretsManager/SecretItem.cs index 757ef2f..ca3894d 100644 --- a/MountAws/Services/SecretsManager/SecretItem.cs +++ b/MountAws/Services/SecretsManager/SecretItem.cs @@ -1,24 +1,40 @@ +using System.Management.Automation; using Amazon.SecretsManager.Model; using MountAnything; namespace MountAws.Services.SecretsManager; -public class SecretItem : AwsItem +public class SecretItem : AwsItem { - public SecretItem(ItemPath parentPath, SecretListEntry secret) : base(parentPath, secret) + public SecretItem(ItemPath parentPath, SecretListEntry secret) : base(parentPath, new PSObject(secret)) { - ItemName = secret.Name; + ItemName = new ItemPath(secret.Name).Name; + Name = secret.Name; + Description = secret.Description; + ARN = secret.ARN; + LastChangedDate = secret.LastChangedDate; + CreatedDate = secret.CreatedDate; + } + + public SecretItem(ItemPath parentPath, DescribeSecretResponse secret) : base(parentPath, new PSObject(secret)) + { + ItemName = new ItemPath(secret.Name).Name; + Name = secret.Name; + Description = secret.Description; + ARN = secret.ARN; + LastChangedDate = secret.LastChangedDate; + CreatedDate = secret.CreatedDate; } public override string ItemName { get; } public override bool IsContainer => true; - public string Name => UnderlyingObject.Name; - public string? Description => UnderlyingObject.Description; - public string ARN => UnderlyingObject.ARN; - public DateTime? LastChangedDate => UnderlyingObject.LastChangedDate; - public DateTime? CreatedDate => UnderlyingObject.CreatedDate; + public string Name { get; } + public string? Description { get; } + public string ARN { get; } + public DateTime? LastChangedDate { get; } + public DateTime? CreatedDate { get; } public override string? WebUrl => - UrlBuilder.CombineWith($"secretsmanager/secret?name={Uri.EscapeDataString(ItemName)}"); + UrlBuilder.CombineWith($"secretsmanager/secret?name={Uri.EscapeDataString(Name)}"); } diff --git a/MountAws/Services/SecretsManager/SecretNavigator.cs b/MountAws/Services/SecretsManager/SecretNavigator.cs new file mode 100644 index 0000000..83ad4ab --- /dev/null +++ b/MountAws/Services/SecretsManager/SecretNavigator.cs @@ -0,0 +1,35 @@ +using Amazon.SecretsManager; +using Amazon.SecretsManager.Model; +using MountAnything; + +namespace MountAws.Services.SecretsManager; + +public class SecretNavigator : ItemNavigator +{ + private readonly IAmazonSecretsManager _secretsManager; + + public SecretNavigator(IAmazonSecretsManager secretsManager) + { + _secretsManager = secretsManager; + } + + protected override IItem CreateDirectoryItem(ItemPath parentPath, ItemPath directoryPath) + { + return new SecretFolderItem(parentPath, directoryPath); + } + + protected override IItem CreateItem(ItemPath parentPath, SecretListEntry model) + { + return new SecretItem(parentPath, model); + } + + protected override ItemPath GetPath(SecretListEntry model) + { + return new ItemPath(model.Name); + } + + protected override IEnumerable ListItems(ItemPath? pathPrefix) + { + return _secretsManager.ListSecrets(); + } +} diff --git a/MountAws/Services/SecretsManager/SecretPath.cs b/MountAws/Services/SecretsManager/SecretPath.cs new file mode 100644 index 0000000..4189b8f --- /dev/null +++ b/MountAws/Services/SecretsManager/SecretPath.cs @@ -0,0 +1,16 @@ +namespace MountAws.Services.SecretsManager; + +public class SecretPath +{ + public string Value { get; } + + public SecretPath(string value) + { + Value = value; + } + + public override string ToString() + { + return Value; + } +} diff --git a/MountAws/Services/SecretsManager/SecretValueHandler.cs b/MountAws/Services/SecretsManager/SecretValueHandler.cs deleted file mode 100644 index e6a1dc1..0000000 --- a/MountAws/Services/SecretsManager/SecretValueHandler.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Text; -using System.Text.Json.Nodes; -using Amazon.SecretsManager; -using MountAnything; -using MountAnything.Content; - -namespace MountAws.Services.SecretsManager; - -public class SecretValueHandler : PathHandler, IContentReaderHandler, IContentWriterHandler -{ - private readonly IAmazonSecretsManager _secretsManager; - private readonly SecretHandler _parentHandler; - - public SecretValueHandler(ItemPath path, IPathHandlerContext context, IAmazonSecretsManager secretsManager) : base(path, context) - { - _secretsManager = secretsManager; - _parentHandler = new SecretHandler(path.Parent, context, secretsManager); - } - - protected override IItem? GetItemImpl() - { - return _parentHandler.GetChildItems() - .SingleOrDefault(i => i.ItemName.Equals(ItemName, StringComparison.OrdinalIgnoreCase)); - } - - protected override IEnumerable GetChildItemsImpl() - { - yield break; - } - - public IStreamContentReader GetContentReader() - { - var item = GetItem() as SecretValueItem; - if (item == null) - { - throw new InvalidOperationException("Secret value does not exist"); - } - return new StreamContentReader(new MemoryStream(Encoding.UTF8.GetBytes(item.Value))); - } - - public IStreamContentWriter GetContentWriter() - { - var secretName = Path.Parent.Name; - return new StreamContentWriter(stream => - { - using var reader = new StreamReader(stream, Encoding.UTF8); - var newValue = reader.ReadToEnd(); - - var response = _secretsManager.GetSecretValue(secretName); - var jsonNode = JsonNode.Parse(response.SecretString) - ?? throw new InvalidOperationException("Secret value is not valid JSON"); - if (jsonNode is not JsonObject jsonObject) - { - throw new InvalidOperationException("Secret value is not a JSON object"); - } - - jsonObject[ItemName] = JsonValue.Create(newValue); - _secretsManager.PutSecretValue(secretName, jsonObject.ToJsonString()); - }); - } -} diff --git a/MountAws/Services/SecretsManager/SecretsHandler.cs b/MountAws/Services/SecretsManager/SecretsHandler.cs index e9b7f26..d591bbf 100644 --- a/MountAws/Services/SecretsManager/SecretsHandler.cs +++ b/MountAws/Services/SecretsManager/SecretsHandler.cs @@ -1,4 +1,3 @@ -using Amazon.SecretsManager; using MountAnything; using MountAws.Services.Core; @@ -6,7 +5,7 @@ namespace MountAws.Services.SecretsManager; public class SecretsHandler : PathHandler { - private readonly IAmazonSecretsManager _secretsManager; + private readonly SecretNavigator _navigator; public static IItem CreateItem(ItemPath parentPath) { @@ -14,9 +13,9 @@ public static IItem CreateItem(ItemPath parentPath) "Navigate secrets as a virtual filesystem"); } - public SecretsHandler(ItemPath path, IPathHandlerContext context, IAmazonSecretsManager secretsManager) : base(path, context) + public SecretsHandler(ItemPath path, IPathHandlerContext context, SecretNavigator navigator) : base(path, context) { - _secretsManager = secretsManager; + _navigator = navigator; } protected override IItem? GetItemImpl() @@ -26,7 +25,6 @@ public SecretsHandler(ItemPath path, IPathHandlerContext context, IAmazonSecrets protected override IEnumerable GetChildItemsImpl() { - return _secretsManager.ListSecrets() - .Select(s => new SecretItem(Path, s)); + return _navigator.ListChildItems(Path); } } diff --git a/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs b/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs index 9d74b5e..16e2312 100644 --- a/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs +++ b/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs @@ -35,6 +35,18 @@ public static GetSecretValueResponse GetSecretValue(this IAmazonSecretsManager s }).GetAwaiter().GetResult(); } + public static DescribeSecretResponse? DescribeSecretOrDefault(this IAmazonSecretsManager secretsManager, string secretId) + { + try + { + return secretsManager.DescribeSecret(secretId); + } + catch (ResourceNotFoundException) + { + return null; + } + } + public static void PutSecretValue(this IAmazonSecretsManager secretsManager, string secretId, string secretString) { secretsManager.PutSecretValueAsync(new PutSecretValueRequest From 76b41112993fb7f92fcae7869a0fded25e053ffa Mon Sep 17 00:00:00 2001 From: Andy Alm Date: Sat, 28 Feb 2026 16:09:48 -0800 Subject: [PATCH 05/12] Some fixes for navigating secrets. --- .../Services/SecretsManager/Formats.ps1xml | 26 ++++------------ .../SecretsManager/SecretFolderItem.cs | 15 ++++------ .../Services/SecretsManager/SecretHandler.cs | 30 +++++++++++-------- .../Services/SecretsManager/SecretItem.cs | 24 ++++++++++----- .../SecretsManager/SecretNavigator.cs | 7 ++++- .../SecretsManager/SecretValueItem.cs | 18 ++++------- .../SecretsManagerApiExtensions.cs | 26 ++++++++++++++++ .../SecretsManager/SecretsManagerItemTypes.cs | 8 +++++ 8 files changed, 89 insertions(+), 65 deletions(-) create mode 100644 MountAws/Services/SecretsManager/SecretsManagerItemTypes.cs diff --git a/MountAws/Services/SecretsManager/Formats.ps1xml b/MountAws/Services/SecretsManager/Formats.ps1xml index e030765..417e77f 100644 --- a/MountAws/Services/SecretsManager/Formats.ps1xml +++ b/MountAws/Services/SecretsManager/Formats.ps1xml @@ -13,6 +13,8 @@ + + @@ -21,31 +23,13 @@ Name - Description + ItemType - LastChangedDate + Description - - - - - - - SecretFolderItem - - MountAws.Services.SecretsManager.SecretFolderItem - - - - - - - - - - ItemName + LastChangedDate diff --git a/MountAws/Services/SecretsManager/SecretFolderItem.cs b/MountAws/Services/SecretsManager/SecretFolderItem.cs index 836ae21..8ef2b96 100644 --- a/MountAws/Services/SecretsManager/SecretFolderItem.cs +++ b/MountAws/Services/SecretsManager/SecretFolderItem.cs @@ -3,16 +3,11 @@ namespace MountAws.Services.SecretsManager; -public class SecretFolderItem : AwsItem +public class SecretFolderItem(ItemPath parentPath, ItemPath directoryPath) + : AwsItem(parentPath.Combine(directoryPath).Parent, new PSObject()) { - public SecretFolderItem(ItemPath parentPath, ItemPath directoryPath) : base(parentPath, new PSObject(new - { - Name = directoryPath.Name - })) - { - ItemName = directoryPath.Name; - } - - public override string ItemName { get; } + public override string ItemName { get; } = directoryPath.Name; public override bool IsContainer => true; + protected override string TypeName => typeof(SecretItem).FullName!; + public override string ItemType => SecretsManagerItemTypes.Directory; } diff --git a/MountAws/Services/SecretsManager/SecretHandler.cs b/MountAws/Services/SecretsManager/SecretHandler.cs index 94ce6a8..b3acb1b 100644 --- a/MountAws/Services/SecretsManager/SecretHandler.cs +++ b/MountAws/Services/SecretsManager/SecretHandler.cs @@ -24,33 +24,39 @@ public SecretHandler(ItemPath path, IPathHandlerContext context, IAmazonSecretsM protected override IItem? GetItemImpl() { - var fullPath = _secretPath.Value; + var fullPath = new ItemPath(_secretPath.Value); // 1. Try as an actual secret - var secret = _secretsManager.DescribeSecretOrDefault(fullPath); + var secret = _secretsManager.DescribeSecretOrDefault(fullPath.FullName); if (secret != null) { return new SecretItem(ParentPath, secret); } // 2. Check if the last segment is a JSON key of a parent secret - var itemPath = new ItemPath(fullPath); - if (!itemPath.Parent.IsRoot) + if (!fullPath.Parent.IsRoot) { - var parentSecret = _secretsManager.DescribeSecretOrDefault(itemPath.Parent.FullName); + var parentSecret = _secretsManager.DescribeSecretOrDefault(fullPath.Parent.FullName); if (parentSecret != null) { - var secretString = GetSecretString(itemPath.Parent.FullName); + var secretString = GetSecretString(fullPath.Parent.FullName); if (secretString != null && TryParseJsonObject(secretString, out var props) && - props.TryGetValue(itemPath.Name, out var value)) + props.TryGetValue(fullPath.Name, out _)) { - return new SecretValueItem(ParentPath, itemPath.Name, value); + return new SecretValueItem(ParentPath, fullPath.Parent.FullName, fullPath.Name); } } } - // 3. Must be a folder - return new SecretFolderItem(ParentPath, itemPath); + var childSecrets = _secretsManager.ListSecrets(_secretPath.Value); + if (childSecrets.Any()) + { + // if there are secrets that are a child of this path, then represent it as a folder + return new SecretFolderItem(ParentPath, fullPath); + } + + // doesn't match anything, so does not exist + return null; } protected override IEnumerable GetChildItemsImpl() @@ -72,7 +78,7 @@ private IEnumerable GetSecretChildren() { foreach (var property in properties) { - yield return new SecretValueItem(Path, property.Key, property.Value); + yield return new SecretValueItem(Path, _secretPath.Value, property.Key); } } } @@ -89,7 +95,7 @@ public IStreamContentReader GetContentReader() { SecretItem => GetSecretContentReader(), SecretValueItem valueItem => new StreamContentReader( - new MemoryStream(Encoding.UTF8.GetBytes(valueItem.Value))), + new MemoryStream(Encoding.UTF8.GetBytes(GetSecretString(valueItem.SecretName)!))), _ => throw new InvalidOperationException("Cannot read content from a folder") }; } diff --git a/MountAws/Services/SecretsManager/SecretItem.cs b/MountAws/Services/SecretsManager/SecretItem.cs index ca3894d..77d51a7 100644 --- a/MountAws/Services/SecretsManager/SecretItem.cs +++ b/MountAws/Services/SecretsManager/SecretItem.cs @@ -9,9 +9,10 @@ public class SecretItem : AwsItem public SecretItem(ItemPath parentPath, SecretListEntry secret) : base(parentPath, new PSObject(secret)) { ItemName = new ItemPath(secret.Name).Name; - Name = secret.Name; + SecretName = secret.Name; + ItemType = SecretsManagerItemTypes.SecretValue; Description = secret.Description; - ARN = secret.ARN; + Arn = secret.ARN; LastChangedDate = secret.LastChangedDate; CreatedDate = secret.CreatedDate; } @@ -19,22 +20,29 @@ public class SecretItem : AwsItem public SecretItem(ItemPath parentPath, DescribeSecretResponse secret) : base(parentPath, new PSObject(secret)) { ItemName = new ItemPath(secret.Name).Name; - Name = secret.Name; + SecretName = secret.Name; + ItemType = SecretsManagerItemTypes.Secret; Description = secret.Description; - ARN = secret.ARN; + Arn = secret.ARN; LastChangedDate = secret.LastChangedDate; CreatedDate = secret.CreatedDate; } public override string ItemName { get; } public override bool IsContainer => true; - - public string Name { get; } + protected override string TypeName => GetType().FullName!; + public override string ItemType { get; } + [ItemProperty] + public string SecretName { get; } + [ItemProperty] public string? Description { get; } - public string ARN { get; } + [ItemProperty] + public string Arn { get; } + [ItemProperty] public DateTime? LastChangedDate { get; } + [ItemProperty] public DateTime? CreatedDate { get; } public override string? WebUrl => - UrlBuilder.CombineWith($"secretsmanager/secret?name={Uri.EscapeDataString(Name)}"); + UrlBuilder.CombineWith($"secretsmanager/secret?name={Uri.EscapeDataString(SecretName)}"); } diff --git a/MountAws/Services/SecretsManager/SecretNavigator.cs b/MountAws/Services/SecretsManager/SecretNavigator.cs index 83ad4ab..b5c876b 100644 --- a/MountAws/Services/SecretsManager/SecretNavigator.cs +++ b/MountAws/Services/SecretsManager/SecretNavigator.cs @@ -30,6 +30,11 @@ protected override ItemPath GetPath(SecretListEntry model) protected override IEnumerable ListItems(ItemPath? pathPrefix) { - return _secretsManager.ListSecrets(); + if (pathPrefix == null || pathPrefix.IsRoot) + { + return _secretsManager.ListSecrets(); + } + + return _secretsManager.ListSecrets(pathPrefix.FullName); } } diff --git a/MountAws/Services/SecretsManager/SecretValueItem.cs b/MountAws/Services/SecretsManager/SecretValueItem.cs index 76f13ec..a0014a3 100644 --- a/MountAws/Services/SecretsManager/SecretValueItem.cs +++ b/MountAws/Services/SecretsManager/SecretValueItem.cs @@ -3,19 +3,11 @@ namespace MountAws.Services.SecretsManager; -public class SecretValueItem : AwsItem +public class SecretValueItem(ItemPath parentPath, string secretName, string key) : AwsItem(parentPath, new PSObject()) { - public SecretValueItem(ItemPath parentPath, string key, string value) : base(parentPath, new PSObject(new - { - Key = key, - Value = value - })) - { - ItemName = key; - Value = value; - } - - public override string ItemName { get; } + public override string ItemName { get; } = key; + [ItemProperty] + public string SecretName { get; } = secretName; public override bool IsContainer => false; - public string Value { get; } + public override string ItemType => SecretsManagerItemTypes.SecretValue; } diff --git a/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs b/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs index 16e2312..e4506c1 100644 --- a/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs +++ b/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs @@ -18,6 +18,28 @@ public static IEnumerable ListSecrets(this IAmazonSecretsManage return (response.SecretList, response.NextToken); }); } + + public static IEnumerable ListSecrets(this IAmazonSecretsManager secretsManager, string pathPrefix) + { + return Paginate(nextToken => + { + var response = secretsManager.ListSecretsAsync(new ListSecretsRequest + { + Filters = [ + new Filter + { + Key = FilterNameStringType.Name, + Values = [ + pathPrefix + ] + } + ], + NextToken = nextToken + }).GetAwaiter().GetResult(); + + return (response.SecretList, response.NextToken); + }); + } public static DescribeSecretResponse DescribeSecret(this IAmazonSecretsManager secretsManager, string secretId) { @@ -45,6 +67,10 @@ public static GetSecretValueResponse GetSecretValue(this IAmazonSecretsManager s { return null; } + catch (AmazonSecretsManagerException) + { + return null; + } } public static void PutSecretValue(this IAmazonSecretsManager secretsManager, string secretId, string secretString) diff --git a/MountAws/Services/SecretsManager/SecretsManagerItemTypes.cs b/MountAws/Services/SecretsManager/SecretsManagerItemTypes.cs new file mode 100644 index 0000000..88c57f7 --- /dev/null +++ b/MountAws/Services/SecretsManager/SecretsManagerItemTypes.cs @@ -0,0 +1,8 @@ +namespace MountAws.Services.SecretsManager; + +public static class SecretsManagerItemTypes +{ + public const string Directory = "Directory"; + public const string Secret = "Secret"; + public const string SecretValue = "SecretValue"; +} \ No newline at end of file From 4fa37bc5f0210f0e3aabc8e8a87d8750854bd54e Mon Sep 17 00:00:00 2001 From: Andy Alm Date: Sat, 28 Feb 2026 20:00:02 -0800 Subject: [PATCH 06/12] Add Name property to SecretItem and refactor table formatting for SecretsManager --- MountAws/Services/SecretsManager/Formats.ps1xml | 8 ++------ MountAws/Services/SecretsManager/SecretItem.cs | 2 ++ MountAws/Services/SecretsManager/SecretValueItem.cs | 4 +++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/MountAws/Services/SecretsManager/Formats.ps1xml b/MountAws/Services/SecretsManager/Formats.ps1xml index 417e77f..0a4f3ed 100644 --- a/MountAws/Services/SecretsManager/Formats.ps1xml +++ b/MountAws/Services/SecretsManager/Formats.ps1xml @@ -8,6 +8,7 @@ + @@ -20,7 +21,7 @@ - Name + ItemName ItemType @@ -45,8 +46,6 @@ - - @@ -54,9 +53,6 @@ Key - - Value - diff --git a/MountAws/Services/SecretsManager/SecretItem.cs b/MountAws/Services/SecretsManager/SecretItem.cs index 77d51a7..f6e20b5 100644 --- a/MountAws/Services/SecretsManager/SecretItem.cs +++ b/MountAws/Services/SecretsManager/SecretItem.cs @@ -33,6 +33,8 @@ public class SecretItem : AwsItem protected override string TypeName => GetType().FullName!; public override string ItemType { get; } [ItemProperty] + public string Name => ItemName; + [ItemProperty] public string SecretName { get; } [ItemProperty] public string? Description { get; } diff --git a/MountAws/Services/SecretsManager/SecretValueItem.cs b/MountAws/Services/SecretsManager/SecretValueItem.cs index a0014a3..e20346e 100644 --- a/MountAws/Services/SecretsManager/SecretValueItem.cs +++ b/MountAws/Services/SecretsManager/SecretValueItem.cs @@ -5,9 +5,11 @@ namespace MountAws.Services.SecretsManager; public class SecretValueItem(ItemPath parentPath, string secretName, string key) : AwsItem(parentPath, new PSObject()) { - public override string ItemName { get; } = key; + public override string ItemName => key; [ItemProperty] public string SecretName { get; } = secretName; public override bool IsContainer => false; public override string ItemType => SecretsManagerItemTypes.SecretValue; + + [ItemProperty] public string Key => key; } From 1eea79c0a09f6a4ce1f519b70dc369815b4d04fb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Mar 2026 04:10:08 +0000 Subject: [PATCH 07/12] Simplify secrets: remove child value items, use MapRecursive Secret values are no longer exposed as navigable child items. Instead, use Get-Content/Set-Content for raw values and Get/Set-ItemProperty for individual JSON keys. This removes the three-way dispatch complexity and extra API calls from SecretHandler. - SecretItem.IsContainer = false (secrets are leaf nodes) - SecretPath extends TypedItemPath, Routes uses MapRecursive - SecretHandler simplified to two-way folder/secret dispatch - SecretValueItem deleted - SecretValue item type removed https://claude.ai/code/session_01QhMmDXjkSLNqcz11WmUwMT --- .../Services/SecretsManager/Formats.ps1xml | 21 --- MountAws/Services/SecretsManager/Routes.cs | 9 +- .../Services/SecretsManager/SecretHandler.cs | 123 ++---------------- .../Services/SecretsManager/SecretItem.cs | 4 +- .../Services/SecretsManager/SecretPath.cs | 16 +-- .../SecretsManager/SecretValueItem.cs | 15 --- .../SecretsManager/SecretsManagerItemTypes.cs | 1 - 7 files changed, 21 insertions(+), 168 deletions(-) delete mode 100644 MountAws/Services/SecretsManager/SecretValueItem.cs diff --git a/MountAws/Services/SecretsManager/Formats.ps1xml b/MountAws/Services/SecretsManager/Formats.ps1xml index 0a4f3ed..8acbc3e 100644 --- a/MountAws/Services/SecretsManager/Formats.ps1xml +++ b/MountAws/Services/SecretsManager/Formats.ps1xml @@ -37,26 +37,5 @@ - - SecretValueItem - - MountAws.Services.SecretsManager.SecretValueItem - - - - - - - - - - - Key - - - - - - diff --git a/MountAws/Services/SecretsManager/Routes.cs b/MountAws/Services/SecretsManager/Routes.cs index 7e87818..254fdb8 100644 --- a/MountAws/Services/SecretsManager/Routes.cs +++ b/MountAws/Services/SecretsManager/Routes.cs @@ -1,4 +1,3 @@ -using Microsoft.Extensions.DependencyInjection; using MountAnything.Routing; namespace MountAws.Services.SecretsManager; @@ -11,13 +10,7 @@ public void AddServiceRoutes(Route regionRoute) { secretsManager.MapLiteral("secrets", secrets => { - secrets.MapRegex(@"(?.+)", secret => - { - secret.ConfigureServices((services, match) => - { - services.AddSingleton(new SecretPath(match.Values["SecretPath"])); - }); - }); + secrets.MapRecursive(); }); }); } diff --git a/MountAws/Services/SecretsManager/SecretHandler.cs b/MountAws/Services/SecretsManager/SecretHandler.cs index b3acb1b..bc0ddad 100644 --- a/MountAws/Services/SecretsManager/SecretHandler.cs +++ b/MountAws/Services/SecretsManager/SecretHandler.cs @@ -24,38 +24,20 @@ public SecretHandler(ItemPath path, IPathHandlerContext context, IAmazonSecretsM protected override IItem? GetItemImpl() { - var fullPath = new ItemPath(_secretPath.Value); - // 1. Try as an actual secret - var secret = _secretsManager.DescribeSecretOrDefault(fullPath.FullName); + var secret = _secretsManager.DescribeSecretOrDefault(_secretPath.Path.FullName); if (secret != null) { return new SecretItem(ParentPath, secret); } - // 2. Check if the last segment is a JSON key of a parent secret - if (!fullPath.Parent.IsRoot) - { - var parentSecret = _secretsManager.DescribeSecretOrDefault(fullPath.Parent.FullName); - if (parentSecret != null) - { - var secretString = GetSecretString(fullPath.Parent.FullName); - if (secretString != null && TryParseJsonObject(secretString, out var props) && - props.TryGetValue(fullPath.Name, out _)) - { - return new SecretValueItem(ParentPath, fullPath.Parent.FullName, fullPath.Name); - } - } - } - - var childSecrets = _secretsManager.ListSecrets(_secretPath.Value); + // 2. Check if it's a folder (prefix of other secrets) + var childSecrets = _secretsManager.ListSecrets(_secretPath.Path.FullName); if (childSecrets.Any()) { - // if there are secrets that are a child of this path, then represent it as a folder - return new SecretFolderItem(ParentPath, fullPath); + return new SecretFolderItem(ParentPath, _secretPath.Path); } - - // doesn't match anything, so does not exist + return null; } @@ -63,103 +45,31 @@ protected override IEnumerable GetChildItemsImpl() { return GetItem() switch { - SecretItem => GetSecretChildren(), - SecretFolderItem => GetFolderChildren(), + SecretFolderItem => _navigator.ListChildItems(Path, _secretPath.Path), _ => Enumerable.Empty() }; } - private IEnumerable GetSecretChildren() - { - var secretString = GetSecretString(_secretPath.Value); - if (secretString == null) yield break; - - if (TryParseJsonObject(secretString, out var properties)) - { - foreach (var property in properties) - { - yield return new SecretValueItem(Path, _secretPath.Value, property.Key); - } - } - } - - private IEnumerable GetFolderChildren() - { - return _navigator.ListChildItems(Path, new ItemPath(_secretPath.Value)); - } - public IStreamContentReader GetContentReader() { - var item = GetItem(); - return item switch - { - SecretItem => GetSecretContentReader(), - SecretValueItem valueItem => new StreamContentReader( - new MemoryStream(Encoding.UTF8.GetBytes(GetSecretString(valueItem.SecretName)!))), - _ => throw new InvalidOperationException("Cannot read content from a folder") - }; - } - - private IStreamContentReader GetSecretContentReader() - { - var secretString = GetSecretString(_secretPath.Value); - if (secretString == null) - { - throw new InvalidOperationException("Secret does not contain a string value"); - } + var secretString = GetSecretString() + ?? throw new InvalidOperationException("Secret does not contain a string value"); return new StreamContentReader(new MemoryStream(Encoding.UTF8.GetBytes(secretString))); } public IStreamContentWriter GetContentWriter() - { - var item = GetItem(); - return item switch - { - SecretItem => GetSecretContentWriter(), - SecretValueItem => GetSecretValueContentWriter(), - _ => throw new InvalidOperationException("Cannot write content to a folder") - }; - } - - private IStreamContentWriter GetSecretContentWriter() { return new StreamContentWriter(stream => { using var reader = new StreamReader(stream, Encoding.UTF8); var secretString = reader.ReadToEnd(); - _secretsManager.PutSecretValue(_secretPath.Value, secretString); - }); - } - - private IStreamContentWriter GetSecretValueContentWriter() - { - var itemPath = new ItemPath(_secretPath.Value); - var secretName = itemPath.Parent.FullName; - var keyName = itemPath.Name; - - return new StreamContentWriter(stream => - { - using var reader = new StreamReader(stream, Encoding.UTF8); - var newValue = reader.ReadToEnd(); - - var response = _secretsManager.GetSecretValue(secretName); - var jsonNode = JsonNode.Parse(response.SecretString) - ?? throw new InvalidOperationException("Secret value is not valid JSON"); - if (jsonNode is not JsonObject jsonObject) - { - throw new InvalidOperationException("Secret value is not a JSON object"); - } - - jsonObject[keyName] = JsonValue.Create(newValue); - _secretsManager.PutSecretValue(secretName, jsonObject.ToJsonString()); + _secretsManager.PutSecretValue(_secretPath.Path.FullName, secretString); }); } public override IEnumerable GetItemProperties(HashSet propertyNames, Func pathResolver) { - if (GetItem() is not SecretItem) return base.GetItemProperties(propertyNames, pathResolver); - - var secretString = GetSecretString(_secretPath.Value); + var secretString = GetSecretString(); if (secretString == null || !TryParseJsonObject(secretString, out var properties)) { return base.GetItemProperties(propertyNames, pathResolver); @@ -175,12 +85,7 @@ public override IEnumerable GetItemProperties(HashSet pro public void SetItemProperties(ICollection propertyValues) { - if (GetItem() is not SecretItem) - { - throw new InvalidOperationException("Can only set properties on a secret"); - } - - var secretString = GetSecretString(_secretPath.Value) + var secretString = GetSecretString() ?? throw new InvalidOperationException("Secret does not contain a string value"); var jsonNode = JsonNode.Parse(secretString) @@ -196,14 +101,14 @@ public void SetItemProperties(ICollection propertyValues) jsonObject[property.Name] = JsonValue.Create(property.Value?.ToString()); } - _secretsManager.PutSecretValue(_secretPath.Value, jsonObject.ToJsonString()); + _secretsManager.PutSecretValue(_secretPath.Path.FullName, jsonObject.ToJsonString()); } - private string? GetSecretString(string secretName) + private string? GetSecretString() { try { - var response = _secretsManager.GetSecretValue(secretName); + var response = _secretsManager.GetSecretValue(_secretPath.Path.FullName); return response.SecretString; } catch diff --git a/MountAws/Services/SecretsManager/SecretItem.cs b/MountAws/Services/SecretsManager/SecretItem.cs index f6e20b5..6bc4d2d 100644 --- a/MountAws/Services/SecretsManager/SecretItem.cs +++ b/MountAws/Services/SecretsManager/SecretItem.cs @@ -10,7 +10,7 @@ public class SecretItem : AwsItem { ItemName = new ItemPath(secret.Name).Name; SecretName = secret.Name; - ItemType = SecretsManagerItemTypes.SecretValue; + ItemType = SecretsManagerItemTypes.Secret; Description = secret.Description; Arn = secret.ARN; LastChangedDate = secret.LastChangedDate; @@ -29,7 +29,7 @@ public class SecretItem : AwsItem } public override string ItemName { get; } - public override bool IsContainer => true; + public override bool IsContainer => false; protected override string TypeName => GetType().FullName!; public override string ItemType { get; } [ItemProperty] diff --git a/MountAws/Services/SecretsManager/SecretPath.cs b/MountAws/Services/SecretsManager/SecretPath.cs index 4189b8f..5756bf7 100644 --- a/MountAws/Services/SecretsManager/SecretPath.cs +++ b/MountAws/Services/SecretsManager/SecretPath.cs @@ -1,16 +1,8 @@ +using MountAnything; + namespace MountAws.Services.SecretsManager; -public class SecretPath +public class SecretPath : TypedItemPath { - public string Value { get; } - - public SecretPath(string value) - { - Value = value; - } - - public override string ToString() - { - return Value; - } + public SecretPath(ItemPath path) : base(path) { } } diff --git a/MountAws/Services/SecretsManager/SecretValueItem.cs b/MountAws/Services/SecretsManager/SecretValueItem.cs deleted file mode 100644 index e20346e..0000000 --- a/MountAws/Services/SecretsManager/SecretValueItem.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Management.Automation; -using MountAnything; - -namespace MountAws.Services.SecretsManager; - -public class SecretValueItem(ItemPath parentPath, string secretName, string key) : AwsItem(parentPath, new PSObject()) -{ - public override string ItemName => key; - [ItemProperty] - public string SecretName { get; } = secretName; - public override bool IsContainer => false; - public override string ItemType => SecretsManagerItemTypes.SecretValue; - - [ItemProperty] public string Key => key; -} diff --git a/MountAws/Services/SecretsManager/SecretsManagerItemTypes.cs b/MountAws/Services/SecretsManager/SecretsManagerItemTypes.cs index 88c57f7..ae5eb60 100644 --- a/MountAws/Services/SecretsManager/SecretsManagerItemTypes.cs +++ b/MountAws/Services/SecretsManager/SecretsManagerItemTypes.cs @@ -4,5 +4,4 @@ public static class SecretsManagerItemTypes { public const string Directory = "Directory"; public const string Secret = "Secret"; - public const string SecretValue = "SecretValue"; } \ No newline at end of file From a1726fbf582213c804007026ede74c53be609936 Mon Sep 17 00:00:00 2001 From: Andy Alm Date: Sat, 28 Feb 2026 21:29:08 -0800 Subject: [PATCH 08/12] Simplified the SecretHandler --- .../Services/SecretsManager/SecretHandler.cs | 43 ++++++++----------- .../SecretsManagerApiExtensions.cs | 24 ----------- 2 files changed, 18 insertions(+), 49 deletions(-) diff --git a/MountAws/Services/SecretsManager/SecretHandler.cs b/MountAws/Services/SecretsManager/SecretHandler.cs index bc0ddad..c0acd6c 100644 --- a/MountAws/Services/SecretsManager/SecretHandler.cs +++ b/MountAws/Services/SecretsManager/SecretHandler.cs @@ -8,34 +8,27 @@ namespace MountAws.Services.SecretsManager; -public class SecretHandler : PathHandler, IContentReaderHandler, IContentWriterHandler, ISetItemPropertiesHandler +public class SecretHandler( + ItemPath path, + IPathHandlerContext context, + IAmazonSecretsManager secretsManager, + SecretNavigator navigator, + SecretPath secretPath) + : PathHandler(path, context), IContentReaderHandler, IContentWriterHandler, ISetItemPropertiesHandler { - private readonly IAmazonSecretsManager _secretsManager; - private readonly SecretNavigator _navigator; - private readonly SecretPath _secretPath; - - public SecretHandler(ItemPath path, IPathHandlerContext context, IAmazonSecretsManager secretsManager, - SecretNavigator navigator, SecretPath secretPath) : base(path, context) - { - _secretsManager = secretsManager; - _navigator = navigator; - _secretPath = secretPath; - } - protected override IItem? GetItemImpl() { - // 1. Try as an actual secret - var secret = _secretsManager.DescribeSecretOrDefault(_secretPath.Path.FullName); - if (secret != null) + var childSecrets = secretsManager + .ListSecrets(secretPath.Path.FullName) + .ToArray(); + + if (childSecrets.Length == 1 && childSecrets[0].Name == secretPath.Path.FullName) { - return new SecretItem(ParentPath, secret); + return new SecretItem(ParentPath, childSecrets[0]); } - - // 2. Check if it's a folder (prefix of other secrets) - var childSecrets = _secretsManager.ListSecrets(_secretPath.Path.FullName); if (childSecrets.Any()) { - return new SecretFolderItem(ParentPath, _secretPath.Path); + return new SecretFolderItem(ParentPath, secretPath.Path); } return null; @@ -45,7 +38,7 @@ protected override IEnumerable GetChildItemsImpl() { return GetItem() switch { - SecretFolderItem => _navigator.ListChildItems(Path, _secretPath.Path), + SecretFolderItem => navigator.ListChildItems(Path, secretPath.Path), _ => Enumerable.Empty() }; } @@ -63,7 +56,7 @@ public IStreamContentWriter GetContentWriter() { using var reader = new StreamReader(stream, Encoding.UTF8); var secretString = reader.ReadToEnd(); - _secretsManager.PutSecretValue(_secretPath.Path.FullName, secretString); + secretsManager.PutSecretValue(secretPath.Path.FullName, secretString); }); } @@ -101,14 +94,14 @@ public void SetItemProperties(ICollection propertyValues) jsonObject[property.Name] = JsonValue.Create(property.Value?.ToString()); } - _secretsManager.PutSecretValue(_secretPath.Path.FullName, jsonObject.ToJsonString()); + secretsManager.PutSecretValue(secretPath.Path.FullName, jsonObject.ToJsonString()); } private string? GetSecretString() { try { - var response = _secretsManager.GetSecretValue(_secretPath.Path.FullName); + var response = secretsManager.GetSecretValue(secretPath.Path.FullName); return response.SecretString; } catch diff --git a/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs b/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs index e4506c1..c6760a1 100644 --- a/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs +++ b/MountAws/Services/SecretsManager/SecretsManagerApiExtensions.cs @@ -41,14 +41,6 @@ public static IEnumerable ListSecrets(this IAmazonSecretsManage }); } - public static DescribeSecretResponse DescribeSecret(this IAmazonSecretsManager secretsManager, string secretId) - { - return secretsManager.DescribeSecretAsync(new DescribeSecretRequest - { - SecretId = secretId - }).GetAwaiter().GetResult(); - } - public static GetSecretValueResponse GetSecretValue(this IAmazonSecretsManager secretsManager, string secretId) { return secretsManager.GetSecretValueAsync(new GetSecretValueRequest @@ -57,22 +49,6 @@ public static GetSecretValueResponse GetSecretValue(this IAmazonSecretsManager s }).GetAwaiter().GetResult(); } - public static DescribeSecretResponse? DescribeSecretOrDefault(this IAmazonSecretsManager secretsManager, string secretId) - { - try - { - return secretsManager.DescribeSecret(secretId); - } - catch (ResourceNotFoundException) - { - return null; - } - catch (AmazonSecretsManagerException) - { - return null; - } - } - public static void PutSecretValue(this IAmazonSecretsManager secretsManager, string secretId, string secretString) { secretsManager.PutSecretValueAsync(new PutSecretValueRequest From 3a447d32739fde7d1bcb7e1af0035743b0df4aa5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Mar 2026 05:34:42 +0000 Subject: [PATCH 09/12] Add Secrets Manager documentation Covers path hierarchy, browsing, reading/writing secret values with Get-Content/Set-Content, and JSON property access with Get-ItemProperty/Set-ItemProperty. https://claude.ai/code/session_01QhMmDXjkSLNqcz11WmUwMT --- docs/Services/SecretsManager.md | 70 +++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/Services/SecretsManager.md diff --git a/docs/Services/SecretsManager.md b/docs/Services/SecretsManager.md new file mode 100644 index 0000000..65fa728 --- /dev/null +++ b/docs/Services/SecretsManager.md @@ -0,0 +1,70 @@ +# Secrets Manager on MountAws + +## Path Hierarchy + +``` +-- secretsmanager + |-- secrets + |-- my-secret # A secret with a simple name + |-- my-app # A virtual folder (prefix of other secrets) + |-- prod # Another virtual folder + |-- database-credentials # A secret named "my-app/prod/database-credentials" +``` + +Secret names containing `/` are automatically organized into a virtual folder hierarchy, so you can browse them like a directory tree. + +## Browsing Secrets + +```powershell +# List all top-level secrets and folders +dir aws:/default/us-east-1/secretsmanager/secrets + +# Navigate into a folder +cd aws:/default/us-east-1/secretsmanager/secrets/my-app/prod +dir +``` + +## Reading Secret Values + +Use `Get-Content` to read the raw secret string value: + +```powershell +Get-Content aws:/default/us-east-1/secretsmanager/secrets/my-secret +``` + +For secrets that contain a JSON object, use `Get-ItemProperty` to read individual key/value pairs: + +```powershell +# Get all properties +Get-ItemProperty aws:/default/us-east-1/secretsmanager/secrets/my-app/prod/database-credentials + +# Get a specific property +(Get-ItemProperty aws:/default/us-east-1/secretsmanager/secrets/my-app/prod/database-credentials).password +``` + +## Writing Secret Values + +Use `Set-Content` to write a new raw secret string value: + +```powershell +Set-Content aws:/default/us-east-1/secretsmanager/secrets/my-secret -Value "new-value" +``` + +For JSON secrets, use `Set-ItemProperty` to update individual keys without overwriting the entire secret: + +```powershell +Set-ItemProperty aws:/default/us-east-1/secretsmanager/secrets/my-app/prod/database-credentials -Name password -Value "new-password" +``` + +## Item Properties + +Each secret exposes the following properties: + +| Property | Description | +|---|---| +| Name | Last segment of the secret name | +| SecretName | Full secret name (e.g., `my-app/prod/database-credentials`) | +| Description | Secret description | +| Arn | Secret ARN | +| LastChangedDate | When the secret was last modified | +| CreatedDate | When the secret was created | From eb087242434e759ad20e0fd845e6e245e8f711aa Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Mar 2026 05:36:36 +0000 Subject: [PATCH 10/12] Add Secrets Manager to supported services list in README https://claude.ai/code/session_01QhMmDXjkSLNqcz11WmUwMT --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 23a7d74..af6869c 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ The following services are currently supported: * [RDS](docs/Services/RDS.md) * [Route53](docs/Services/Route53.md) * [S3](docs/Services/S3.md) + * [Secrets Manager](docs/Services/SecretsManager.md) * [Service Discovery (aka Cloud Map)](docs/Services/ServiceDiscovery.md) * [WAFv2](docs/Services/WAFv2.md) From 3e5e80021b720320c4564c71b42ffe34432890b1 Mon Sep 17 00:00:00 2001 From: Andy Alm Date: Sat, 28 Feb 2026 21:39:03 -0800 Subject: [PATCH 11/12] Simplified the SecretItem --- .../Services/SecretsManager/SecretItem.cs | 39 +++---------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/MountAws/Services/SecretsManager/SecretItem.cs b/MountAws/Services/SecretsManager/SecretItem.cs index 6bc4d2d..9a36512 100644 --- a/MountAws/Services/SecretsManager/SecretItem.cs +++ b/MountAws/Services/SecretsManager/SecretItem.cs @@ -4,46 +4,19 @@ namespace MountAws.Services.SecretsManager; -public class SecretItem : AwsItem +public class SecretItem(ItemPath parentPath, SecretListEntry secret) : AwsItem(parentPath, new PSObject(secret)) { - public SecretItem(ItemPath parentPath, SecretListEntry secret) : base(parentPath, new PSObject(secret)) - { - ItemName = new ItemPath(secret.Name).Name; - SecretName = secret.Name; - ItemType = SecretsManagerItemTypes.Secret; - Description = secret.Description; - Arn = secret.ARN; - LastChangedDate = secret.LastChangedDate; - CreatedDate = secret.CreatedDate; - } - - public SecretItem(ItemPath parentPath, DescribeSecretResponse secret) : base(parentPath, new PSObject(secret)) - { - ItemName = new ItemPath(secret.Name).Name; - SecretName = secret.Name; - ItemType = SecretsManagerItemTypes.Secret; - Description = secret.Description; - Arn = secret.ARN; - LastChangedDate = secret.LastChangedDate; - CreatedDate = secret.CreatedDate; - } - - public override string ItemName { get; } + public override string ItemName { get; } = new ItemPath(secret.Name).Name; public override bool IsContainer => false; protected override string TypeName => GetType().FullName!; - public override string ItemType { get; } + public override string ItemType => SecretsManagerItemTypes.Secret; + [ItemProperty] public string Name => ItemName; [ItemProperty] - public string SecretName { get; } - [ItemProperty] - public string? Description { get; } - [ItemProperty] - public string Arn { get; } - [ItemProperty] - public DateTime? LastChangedDate { get; } + public string SecretName => secret.Name; [ItemProperty] - public DateTime? CreatedDate { get; } + public string Arn => secret.ARN; public override string? WebUrl => UrlBuilder.CombineWith($"secretsmanager/secret?name={Uri.EscapeDataString(SecretName)}"); From 0e86ccaa56e3170f9d9bed07904a23afb66b5b5d Mon Sep 17 00:00:00 2001 From: Andy Alm Date: Sun, 1 Mar 2026 10:42:55 -0800 Subject: [PATCH 12/12] Update SecretsManager.md with some minor clarifications --- docs/Services/SecretsManager.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Services/SecretsManager.md b/docs/Services/SecretsManager.md index 65fa728..ca66789 100644 --- a/docs/Services/SecretsManager.md +++ b/docs/Services/SecretsManager.md @@ -39,7 +39,7 @@ For secrets that contain a JSON object, use `Get-ItemProperty` to read individua Get-ItemProperty aws:/default/us-east-1/secretsmanager/secrets/my-app/prod/database-credentials # Get a specific property -(Get-ItemProperty aws:/default/us-east-1/secretsmanager/secrets/my-app/prod/database-credentials).password +Get-ItemProperty aws:/default/us-east-1/secretsmanager/secrets/my-app/prod/database-credentials -Name password ``` ## Writing Secret Values @@ -58,7 +58,7 @@ Set-ItemProperty aws:/default/us-east-1/secretsmanager/secrets/my-app/prod/datab ## Item Properties -Each secret exposes the following properties: +Each secret item contains all of the properties contained on a [SecretListEntry](https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_SecretListEntry.html). This includes the following properties: | Property | Description | |---|---|