M6: Secrets mutation - New/Update/Remove cmdlets + client methods + DTO tests

This commit is contained in:
GraceSolutions
2026-06-03 17:30:29 -04:00
parent 84ece43d29
commit 3d93fb1173
8 changed files with 474 additions and 1 deletions
@@ -0,0 +1,73 @@
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
namespace PSInfisicalAPI.Tests
{
public class SecretMutationDtoTests
{
private static readonly System.Reflection.Assembly ModuleAssembly =
typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly;
private static object MakeDto(string typeName)
{
System.Type t = ModuleAssembly.GetType(typeName, true);
return System.Activator.CreateInstance(t);
}
[Fact]
public void CreateRequestDto_Serializes_With_Expected_Field_Names()
{
object dto = MakeDto("PSInfisicalAPI.Secrets.InfisicalSecretCreateRequestDto");
dto.GetType().GetProperty("WorkspaceId").SetValue(dto, "wks-1");
dto.GetType().GetProperty("Environment").SetValue(dto, "prod");
dto.GetType().GetProperty("SecretPath").SetValue(dto, "/db");
dto.GetType().GetProperty("Type").SetValue(dto, "shared");
dto.GetType().GetProperty("SecretValue").SetValue(dto, "p@ss");
dto.GetType().GetProperty("SecretComment").SetValue(dto, "comment");
JObject json = JObject.Parse(JsonConvert.SerializeObject(dto));
Assert.Equal("wks-1", (string)json["workspaceId"]);
Assert.Equal("prod", (string)json["environment"]);
Assert.Equal("/db", (string)json["secretPath"]);
Assert.Equal("shared", (string)json["type"]);
Assert.Equal("p@ss", (string)json["secretValue"]);
Assert.Equal("comment", (string)json["secretComment"]);
Assert.False(json.ContainsKey("skipMultilineEncoding"));
Assert.False(json.ContainsKey("tagIds"));
}
[Fact]
public void UpdateRequestDto_Omits_Null_Optional_Fields()
{
object dto = MakeDto("PSInfisicalAPI.Secrets.InfisicalSecretUpdateRequestDto");
dto.GetType().GetProperty("WorkspaceId").SetValue(dto, "wks-1");
dto.GetType().GetProperty("Environment").SetValue(dto, "prod");
dto.GetType().GetProperty("NewSecretName").SetValue(dto, "renamed");
JObject json = JObject.Parse(JsonConvert.SerializeObject(dto));
Assert.Equal("renamed", (string)json["newSecretName"]);
Assert.False(json.ContainsKey("secretValue"));
Assert.False(json.ContainsKey("secretComment"));
Assert.False(json.ContainsKey("type"));
Assert.False(json.ContainsKey("secretPath"));
}
[Fact]
public void DeleteRequestDto_Serializes_With_Expected_Field_Names()
{
object dto = MakeDto("PSInfisicalAPI.Secrets.InfisicalSecretDeleteRequestDto");
dto.GetType().GetProperty("WorkspaceId").SetValue(dto, "wks-1");
dto.GetType().GetProperty("Environment").SetValue(dto, "prod");
dto.GetType().GetProperty("SecretPath").SetValue(dto, "/db");
dto.GetType().GetProperty("Type").SetValue(dto, "shared");
JObject json = JObject.Parse(JsonConvert.SerializeObject(dto));
Assert.Equal("wks-1", (string)json["workspaceId"]);
Assert.Equal("prod", (string)json["environment"]);
Assert.Equal("/db", (string)json["secretPath"]);
Assert.Equal("shared", (string)json["type"]);
}
}
}
@@ -0,0 +1,73 @@
using System;
using System.Management.Automation;
using System.Security;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Secrets;
using PSInfisicalAPI.Security;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.New, "InfisicalSecret", SupportsShouldProcess = true, DefaultParameterSetName = "PlainText")]
[OutputType(typeof(InfisicalSecret))]
public sealed class NewInfisicalSecretCmdlet : InfisicalCmdletBase
{
[Parameter(Mandatory = true, Position = 0)] public string SecretName { get; set; }
[Parameter(Mandatory = true, Position = 1, ParameterSetName = "PlainText")]
public string SecretValue { get; set; }
[Parameter(Mandatory = true, Position = 1, ParameterSetName = "SecureString")]
public SecureString SecureSecretValue { get; set; }
[Parameter] public string SecretComment { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Environment { get; set; }
[Parameter] public string SecretPath { get; set; }
[Parameter] public string ApiVersion { get; set; }
[Parameter] public InfisicalSecretType Type { get; set; } = InfisicalSecretType.Shared;
[Parameter] public SwitchParameter SkipMultilineEncoding { get; set; }
[Parameter] public string[] TagIds { get; set; }
protected override void ProcessRecord()
{
try
{
if (!ShouldProcess(SecretName, "Create Infisical secret"))
{
return;
}
string plainValue = SecureSecretValue != null
? SecureStringUtility.UsePlainText(SecureSecretValue, p => p)
: SecretValue;
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
InfisicalCreateSecretRequest request = new InfisicalCreateSecretRequest
{
SecretName = SecretName,
SecretValue = plainValue,
SecretComment = SecretComment,
ProjectId = ProjectId,
Environment = Environment,
SecretPath = SecretPath,
Type = Type.ToString(),
ApiVersion = ApiVersion,
SkipMultilineEncoding = SkipMultilineEncoding.IsPresent ? (bool?)true : null,
TagIds = TagIds
};
InfisicalSecretsClient client = new InfisicalSecretsClient(HttpClient, Logger);
InfisicalSecret secret = client.Create(connection, request);
if (secret != null)
{
WriteObject(secret);
}
}
catch (Exception exception)
{
ThrowTerminatingForException("NewInfisicalSecretCmdlet", "CreateSecret", exception);
}
}
}
}
@@ -0,0 +1,56 @@
using System;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Secrets;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.Remove, "InfisicalSecret", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public sealed class RemoveInfisicalSecretCmdlet : InfisicalCmdletBase
{
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
public string SecretName { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Environment { get; set; }
[Parameter] public string SecretPath { get; set; }
[Parameter] public string ApiVersion { get; set; }
[Parameter] public InfisicalSecretType Type { get; set; } = InfisicalSecretType.Shared;
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
if (!ShouldProcess(SecretName, "Remove Infisical secret"))
{
return;
}
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
InfisicalDeleteSecretRequest request = new InfisicalDeleteSecretRequest
{
SecretName = SecretName,
ProjectId = ProjectId,
Environment = Environment,
SecretPath = SecretPath,
Type = Type.ToString(),
ApiVersion = ApiVersion
};
InfisicalSecretsClient client = new InfisicalSecretsClient(HttpClient, Logger);
client.Delete(connection, request);
if (PassThru.IsPresent)
{
WriteObject(SecretName);
}
}
catch (Exception exception)
{
ThrowTerminatingForException("RemoveInfisicalSecretCmdlet", "DeleteSecret", exception);
}
}
}
}
@@ -0,0 +1,73 @@
using System;
using System.Management.Automation;
using System.Security;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Secrets;
using PSInfisicalAPI.Security;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsData.Update, "InfisicalSecret", SupportsShouldProcess = true, DefaultParameterSetName = "PlainText")]
[OutputType(typeof(InfisicalSecret))]
public sealed class UpdateInfisicalSecretCmdlet : InfisicalCmdletBase
{
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
public string SecretName { get; set; }
[Parameter(ParameterSetName = "PlainText")] public string SecretValue { get; set; }
[Parameter(ParameterSetName = "SecureString")] public SecureString SecureSecretValue { get; set; }
[Parameter] public string NewSecretName { get; set; }
[Parameter] public string SecretComment { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Environment { get; set; }
[Parameter] public string SecretPath { get; set; }
[Parameter] public string ApiVersion { get; set; }
[Parameter] public InfisicalSecretType Type { get; set; } = InfisicalSecretType.Shared;
[Parameter] public SwitchParameter SkipMultilineEncoding { get; set; }
[Parameter] public string[] TagIds { get; set; }
protected override void ProcessRecord()
{
try
{
if (!ShouldProcess(SecretName, "Update Infisical secret"))
{
return;
}
string plainValue = SecureSecretValue != null
? SecureStringUtility.UsePlainText(SecureSecretValue, p => p)
: SecretValue;
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
InfisicalUpdateSecretRequest request = new InfisicalUpdateSecretRequest
{
SecretName = SecretName,
NewSecretName = NewSecretName,
SecretValue = plainValue,
SecretComment = SecretComment,
ProjectId = ProjectId,
Environment = Environment,
SecretPath = SecretPath,
Type = Type.ToString(),
ApiVersion = ApiVersion,
SkipMultilineEncoding = SkipMultilineEncoding.IsPresent ? (bool?)true : null,
TagIds = TagIds
};
InfisicalSecretsClient client = new InfisicalSecretsClient(HttpClient, Logger);
InfisicalSecret secret = client.Update(connection, request);
if (secret != null)
{
WriteObject(secret);
}
}
catch (Exception exception)
{
ThrowTerminatingForException("UpdateInfisicalSecretCmdlet", "UpdateSecret", exception);
}
}
}
}
@@ -55,4 +55,37 @@ namespace PSInfisicalAPI.Secrets
{
[JsonProperty("secret")] public InfisicalSecretResponseDto Secret { get; set; }
}
internal sealed class InfisicalSecretCreateRequestDto
{
[JsonProperty("workspaceId")] public string WorkspaceId { get; set; }
[JsonProperty("environment")] public string Environment { get; set; }
[JsonProperty("secretPath", NullValueHandling = NullValueHandling.Ignore)] public string SecretPath { get; set; }
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public string Type { get; set; }
[JsonProperty("secretValue")] public string SecretValue { get; set; }
[JsonProperty("secretComment", NullValueHandling = NullValueHandling.Ignore)] public string SecretComment { get; set; }
[JsonProperty("skipMultilineEncoding", NullValueHandling = NullValueHandling.Ignore)] public bool? SkipMultilineEncoding { get; set; }
[JsonProperty("tagIds", NullValueHandling = NullValueHandling.Ignore)] public string[] TagIds { get; set; }
}
internal sealed class InfisicalSecretUpdateRequestDto
{
[JsonProperty("workspaceId")] public string WorkspaceId { get; set; }
[JsonProperty("environment")] public string Environment { get; set; }
[JsonProperty("secretPath", NullValueHandling = NullValueHandling.Ignore)] public string SecretPath { get; set; }
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public string Type { get; set; }
[JsonProperty("secretValue", NullValueHandling = NullValueHandling.Ignore)] public string SecretValue { get; set; }
[JsonProperty("secretComment", NullValueHandling = NullValueHandling.Ignore)] public string SecretComment { get; set; }
[JsonProperty("newSecretName", NullValueHandling = NullValueHandling.Ignore)] public string NewSecretName { get; set; }
[JsonProperty("skipMultilineEncoding", NullValueHandling = NullValueHandling.Ignore)] public bool? SkipMultilineEncoding { get; set; }
[JsonProperty("tagIds", NullValueHandling = NullValueHandling.Ignore)] public string[] TagIds { get; set; }
}
internal sealed class InfisicalSecretDeleteRequestDto
{
[JsonProperty("workspaceId")] public string WorkspaceId { get; set; }
[JsonProperty("environment")] public string Environment { get; set; }
[JsonProperty("secretPath", NullValueHandling = NullValueHandling.Ignore)] public string SecretPath { get; set; }
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public string Type { get; set; }
}
}
@@ -30,4 +30,43 @@ namespace PSInfisicalAPI.Secrets
public bool? ExpandSecretReferences { get; set; }
public bool? IncludeImports { get; set; }
}
public sealed class InfisicalCreateSecretRequest
{
public string SecretName { get; set; }
public string SecretValue { get; set; }
public string SecretComment { get; set; }
public string ProjectId { get; set; }
public string Environment { get; set; }
public string SecretPath { get; set; }
public string Type { get; set; }
public string ApiVersion { get; set; }
public bool? SkipMultilineEncoding { get; set; }
public string[] TagIds { get; set; }
}
public sealed class InfisicalUpdateSecretRequest
{
public string SecretName { get; set; }
public string NewSecretName { get; set; }
public string SecretValue { get; set; }
public string SecretComment { get; set; }
public string ProjectId { get; set; }
public string Environment { get; set; }
public string SecretPath { get; set; }
public string Type { get; set; }
public string ApiVersion { get; set; }
public bool? SkipMultilineEncoding { get; set; }
public string[] TagIds { get; set; }
}
public sealed class InfisicalDeleteSecretRequest
{
public string SecretName { get; set; }
public string ProjectId { get; set; }
public string Environment { get; set; }
public string SecretPath { get; set; }
public string Type { get; set; }
public string ApiVersion { get; set; }
}
}
@@ -136,6 +136,129 @@ namespace PSInfisicalAPI.Secrets
}
}
public InfisicalSecret Create(InfisicalConnection connection, InfisicalCreateSecretRequest request)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (request == null) { throw new ArgumentNullException(nameof(request)); }
if (string.IsNullOrEmpty(request.SecretName)) { throw new InfisicalConfigurationException("SecretName is required."); }
if (request.SecretValue == null) { throw new InfisicalConfigurationException("SecretValue is required."); }
string resolvedProjectId = FirstNonEmpty(request.ProjectId, connection.ProjectId);
string resolvedEnvironment = FirstNonEmpty(request.Environment, connection.Environment);
if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); }
if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); }
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "secretName", request.SecretName } };
InfisicalSecretCreateRequestDto dtoRequest = new InfisicalSecretCreateRequestDto
{
WorkspaceId = resolvedProjectId,
Environment = resolvedEnvironment,
SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"),
Type = string.IsNullOrEmpty(request.Type) ? "shared" : request.Type.ToLowerInvariant(),
SecretValue = request.SecretValue,
SecretComment = request.SecretComment,
SkipMultilineEncoding = request.SkipMultilineEncoding,
TagIds = request.TagIds
};
string body = _serializer.Serialize(dtoRequest);
try
{
_logger.Information(Component, string.Concat("Attempting to create Infisical secret '", request.SecretName, "'. Please Wait..."));
InfisicalHttpResponse response = SendWithVersionFallback(connection, InfisicalEndpointNames.CreateSecret, request.ApiVersion, "CreateSecret", pathParameters, null, body);
InfisicalSecretSingleResponseDto dto = _serializer.Deserialize<InfisicalSecretSingleResponseDto>(response.Body);
response.Clear();
InfisicalSecret mapped = InfisicalSecretMapper.Map(dto != null ? dto.Secret : null);
_logger.Information(Component, "Infisical secret creation was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical secret creation failed.");
throw;
}
}
public InfisicalSecret Update(InfisicalConnection connection, InfisicalUpdateSecretRequest request)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (request == null) { throw new ArgumentNullException(nameof(request)); }
if (string.IsNullOrEmpty(request.SecretName)) { throw new InfisicalConfigurationException("SecretName is required."); }
string resolvedProjectId = FirstNonEmpty(request.ProjectId, connection.ProjectId);
string resolvedEnvironment = FirstNonEmpty(request.Environment, connection.Environment);
if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); }
if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); }
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "secretName", request.SecretName } };
InfisicalSecretUpdateRequestDto dtoRequest = new InfisicalSecretUpdateRequestDto
{
WorkspaceId = resolvedProjectId,
Environment = resolvedEnvironment,
SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"),
Type = string.IsNullOrEmpty(request.Type) ? "shared" : request.Type.ToLowerInvariant(),
SecretValue = request.SecretValue,
SecretComment = request.SecretComment,
NewSecretName = request.NewSecretName,
SkipMultilineEncoding = request.SkipMultilineEncoding,
TagIds = request.TagIds
};
string body = _serializer.Serialize(dtoRequest);
try
{
_logger.Information(Component, string.Concat("Attempting to update Infisical secret '", request.SecretName, "'. Please Wait..."));
InfisicalHttpResponse response = SendWithVersionFallback(connection, InfisicalEndpointNames.UpdateSecret, request.ApiVersion, "UpdateSecret", pathParameters, null, body);
InfisicalSecretSingleResponseDto dto = _serializer.Deserialize<InfisicalSecretSingleResponseDto>(response.Body);
response.Clear();
InfisicalSecret mapped = InfisicalSecretMapper.Map(dto != null ? dto.Secret : null);
_logger.Information(Component, "Infisical secret update was successful.");
return mapped;
}
catch (Exception)
{
_logger.Error(Component, "Infisical secret update failed.");
throw;
}
}
public void Delete(InfisicalConnection connection, InfisicalDeleteSecretRequest request)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (request == null) { throw new ArgumentNullException(nameof(request)); }
if (string.IsNullOrEmpty(request.SecretName)) { throw new InfisicalConfigurationException("SecretName is required."); }
string resolvedProjectId = FirstNonEmpty(request.ProjectId, connection.ProjectId);
string resolvedEnvironment = FirstNonEmpty(request.Environment, connection.Environment);
if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); }
if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); }
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "secretName", request.SecretName } };
InfisicalSecretDeleteRequestDto dtoRequest = new InfisicalSecretDeleteRequestDto
{
WorkspaceId = resolvedProjectId,
Environment = resolvedEnvironment,
SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"),
Type = string.IsNullOrEmpty(request.Type) ? "shared" : request.Type.ToLowerInvariant()
};
string body = _serializer.Serialize(dtoRequest);
try
{
_logger.Information(Component, string.Concat("Attempting to delete Infisical secret '", request.SecretName, "'. Please Wait..."));
InfisicalHttpResponse response = SendWithVersionFallback(connection, InfisicalEndpointNames.DeleteSecret, request.ApiVersion, "DeleteSecret", pathParameters, null, body);
response.Clear();
_logger.Information(Component, "Infisical secret deletion was successful.");
}
catch (Exception)
{
_logger.Error(Component, "Infisical secret deletion failed.");
throw;
}
}
private InfisicalHttpResponse SendWithVersionFallback(
InfisicalConnection connection,
string endpointName,