Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions MountAws.UnitTests/SecretsManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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(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));
resolver.HandlerType.Should().Be(expectedHandlerType);
}
}
1 change: 1 addition & 0 deletions MountAws/MountAws.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<PackageReference Include="AWSSDK.DynamoDBv2" Version="3.7.*" />
<PackageReference Include="AWSSDK.ElastiCache" Version="3.7.*" />
<PackageReference Include="AWSSDK.Lambda" Version="3.7.*" />
<PackageReference Include="AWSSDK.SecretsManager" Version="3.7.*" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions MountAws/Services/Core/RegionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -49,6 +50,7 @@ protected override IEnumerable<IItem> 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);
}
Expand Down
41 changes: 41 additions & 0 deletions MountAws/Services/SecretsManager/Formats.ps1xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Configuration>
<ViewDefinitions>
<View>
<Name>SecretItem</Name>
<ViewSelectedBy>
<TypeName>MountAws.Services.SecretsManager.SecretItem</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Label>Name</Label>
</TableColumnHeader>
<TableColumnHeader>
</TableColumnHeader>
<TableColumnHeader>
</TableColumnHeader>
<TableColumnHeader>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>ItemName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>ItemType</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Description</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>LastChangedDate</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
17 changes: 17 additions & 0 deletions MountAws/Services/SecretsManager/Routes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using MountAnything.Routing;

namespace MountAws.Services.SecretsManager;

public class Routes : IServiceRoutes
{
public void AddServiceRoutes(Route regionRoute)
{
regionRoute.MapLiteral<SecretsManagerRootHandler>("secretsmanager", secretsManager =>
{
secretsManager.MapLiteral<SecretsHandler>("secrets", secrets =>
{
secrets.MapRecursive<SecretHandler, SecretPath>();
});
});
}
}
13 changes: 13 additions & 0 deletions MountAws/Services/SecretsManager/SecretFolderItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Management.Automation;
using MountAnything;

namespace MountAws.Services.SecretsManager;

public class SecretFolderItem(ItemPath parentPath, ItemPath directoryPath)
: AwsItem(parentPath.Combine(directoryPath).Parent, new PSObject())
{
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;
}
134 changes: 134 additions & 0 deletions MountAws/Services/SecretsManager/SecretHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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(
ItemPath path,
IPathHandlerContext context,
IAmazonSecretsManager secretsManager,
SecretNavigator navigator,
SecretPath secretPath)
: PathHandler(path, context), IContentReaderHandler, IContentWriterHandler, ISetItemPropertiesHandler
{
protected override IItem? GetItemImpl()
{
var childSecrets = secretsManager
.ListSecrets(secretPath.Path.FullName)
.ToArray();

if (childSecrets.Length == 1 && childSecrets[0].Name == secretPath.Path.FullName)
{
return new SecretItem(ParentPath, childSecrets[0]);
}
if (childSecrets.Any())
{
return new SecretFolderItem(ParentPath, secretPath.Path);
}

return null;
}

protected override IEnumerable<IItem> GetChildItemsImpl()
{
return GetItem() switch
{
SecretFolderItem => navigator.ListChildItems(Path, secretPath.Path),
_ => Enumerable.Empty<IItem>()
};
}

public IStreamContentReader GetContentReader()
{
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()
{
return new StreamContentWriter(stream =>
{
using var reader = new StreamReader(stream, Encoding.UTF8);
var secretString = reader.ReadToEnd();
secretsManager.PutSecretValue(secretPath.Path.FullName, secretString);
});
}

public override IEnumerable<IItemProperty> GetItemProperties(HashSet<string> propertyNames, Func<ItemPath, string> 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<IItemProperty> 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(secretPath.Path.FullName, jsonObject.ToJsonString());
}

private string? GetSecretString()
{
try
{
var response = secretsManager.GetSecretValue(secretPath.Path.FullName);
return response.SecretString;
}
catch
{
return null;
}
}

private static bool TryParseJsonObject(string value, out Dictionary<string, string> properties)
{
properties = new Dictionary<string, string>();
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;
}
}
}
23 changes: 23 additions & 0 deletions MountAws/Services/SecretsManager/SecretItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Management.Automation;
using Amazon.SecretsManager.Model;
using MountAnything;

namespace MountAws.Services.SecretsManager;

public class SecretItem(ItemPath parentPath, SecretListEntry secret) : AwsItem(parentPath, new PSObject(secret))
{
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 => SecretsManagerItemTypes.Secret;

[ItemProperty]
public string Name => ItemName;
[ItemProperty]
public string SecretName => secret.Name;
[ItemProperty]
public string Arn => secret.ARN;

public override string? WebUrl =>
UrlBuilder.CombineWith($"secretsmanager/secret?name={Uri.EscapeDataString(SecretName)}");
}
40 changes: 40 additions & 0 deletions MountAws/Services/SecretsManager/SecretNavigator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
using MountAnything;

namespace MountAws.Services.SecretsManager;

public class SecretNavigator : ItemNavigator<SecretListEntry, IItem>
{
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<SecretListEntry> ListItems(ItemPath? pathPrefix)
{
if (pathPrefix == null || pathPrefix.IsRoot)
{
return _secretsManager.ListSecrets();
}

return _secretsManager.ListSecrets(pathPrefix.FullName);
}
}
8 changes: 8 additions & 0 deletions MountAws/Services/SecretsManager/SecretPath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using MountAnything;

namespace MountAws.Services.SecretsManager;

public class SecretPath : TypedItemPath
{
public SecretPath(ItemPath path) : base(path) { }
}
30 changes: 30 additions & 0 deletions MountAws/Services/SecretsManager/SecretsHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using MountAnything;
using MountAws.Services.Core;

namespace MountAws.Services.SecretsManager;

public class SecretsHandler : PathHandler
{
private readonly SecretNavigator _navigator;

public static IItem CreateItem(ItemPath parentPath)
{
return new GenericContainerItem(parentPath, "secrets",
"Navigate secrets as a virtual filesystem");
}

public SecretsHandler(ItemPath path, IPathHandlerContext context, SecretNavigator navigator) : base(path, context)
{
_navigator = navigator;
}

protected override IItem? GetItemImpl()
{
return CreateItem(ParentPath);
}

protected override IEnumerable<IItem> GetChildItemsImpl()
{
return _navigator.ListChildItems(Path);
}
}
Loading