Bulk v4 batch routes + strongly-typed -Secrets IDictionary[string,string][]
- Endpoint registry: register POST/PATCH/DELETE /api/v4/secrets/batch as preferred candidates for BulkCreate/Update/Delete; v3 raw routes retained as automatic fallback. - DTOs: add projectId (required for v4) alongside workspaceId on the three batch request envelopes; both serialized when set, both ignored when null. - SecretsClient: populate ProjectId in CreateBatch/UpdateBatch/DeleteBatch so v4 succeeds on first attempt. - Cmdlets: -Secrets on New/Update-InfisicalSecret changed from Hashtable[] to IDictionary<string,string>[] for stronger typing and tab-completion; converter rewritten to accept IEnumerable<IDictionary<string,string>>. TagIds parsed from comma-separated string; nested Metadata dropped from bulk hashtable surface (still settable programmatically on bulk items). - Tests: 166 passing (was 161). Bulk endpoints now resolve to v4 primary with v3 fallback; new tests verify projectId envelope serialization, dual-key omission, and TagIds trimming.
This commit is contained in:
+10
-1
@@ -6,10 +6,19 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 2026.06.04.0005
|
||||||
|
|
||||||
|
- Build produced from commit e0a6ef02df3e.
|
||||||
|
|
||||||
|
## Unreleased (carried forward)
|
||||||
|
|
||||||
|
- **Bulk v4 batch routes**: Endpoint registry now registers `POST|PATCH|DELETE /api/v4/secrets/batch` as the preferred candidates for `BulkCreateSecret`/`BulkUpdateSecret`/`BulkDeleteSecret`; the existing v3 raw routes (`/api/v3/secrets/batch/raw`) remain as automatic fallback. Batch request DTOs serialize both `projectId` (required by v4) and `workspaceId` (accepted by v3) when populated.
|
||||||
|
- **Strongly-typed bulk input**: `-Secrets` on `New-InfisicalSecret` and `Update-InfisicalSecret` is now `IDictionary<string, string>[]` instead of `Hashtable[]`. `InfisicalBulkSecretConverter` accepts `IEnumerable<IDictionary<string, string>>` and parses `TagIds` from a comma-separated string. Nested `Metadata`/`SecretMetadata` dictionaries are no longer accepted in the bulk hashtable surface (set `SecretMetadata` programmatically on `InfisicalBulkCreateSecretItem`/`InfisicalBulkUpdateSecretItem` if needed).
|
||||||
|
|
||||||
## 2026.06.03.2207
|
## 2026.06.03.2207
|
||||||
|
|
||||||
- Build produced from commit 09c3d5c68bbc.
|
- Build produced from commit 09c3d5c68bbc.
|
||||||
- **M9 — Bulk, Duplicate & Inheritance**:
|
- **M9 — Bulk, Duplicate & Inheritance**:
|
||||||
- **Bulk parameter sets** added to `New-InfisicalSecret`, `Update-InfisicalSecret`, and `Remove-InfisicalSecret` accepting `-Secrets Hashtable[]`; client methods `CreateBatch`/`UpdateBatch`/`DeleteBatch` wrap `POST|PATCH|DELETE /api/v3/secrets/batch/raw`.
|
- **Bulk parameter sets** added to `New-InfisicalSecret`, `Update-InfisicalSecret`, and `Remove-InfisicalSecret` accepting `-Secrets Hashtable[]`; client methods `CreateBatch`/`UpdateBatch`/`DeleteBatch` wrap `POST|PATCH|DELETE /api/v3/secrets/batch/raw`.
|
||||||
- **`Copy-InfisicalSecret`** cmdlet added, wrapping `POST /api/v4/secrets/duplicate` with source/destination environment + path parameters and per-attribute copy toggles.
|
- **`Copy-InfisicalSecret`** cmdlet added, wrapping `POST /api/v4/secrets/duplicate` with source/destination environment + path parameters and per-attribute copy toggles.
|
||||||
- **Connection inheritance** centralized in `InfisicalCmdletBase` (`ResolveProjectId`/`ResolveEnvironment`/`ResolveSecretPath`/`ResolveApiVersion`/`ResolveOrganizationId`). Explicit parameters always win; missing values fall back to the active connection and emit a `-Verbose` line.
|
- **Connection inheritance** centralized in `InfisicalCmdletBase` (`ResolveProjectId`/`ResolveEnvironment`/`ResolveSecretPath`/`ResolveApiVersion`/`ResolveOrganizationId`). Explicit parameters always win; missing values fall back to the active connection and emit a `-Verbose` line.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@{
|
@{
|
||||||
RootModule = 'PSInfisicalAPI.psm1'
|
RootModule = 'PSInfisicalAPI.psm1'
|
||||||
ModuleVersion = '2026.06.03.2207'
|
ModuleVersion = '2026.06.04.0005'
|
||||||
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
|
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
|
||||||
Author = 'Grace Solutions'
|
Author = 'Grace Solutions'
|
||||||
CompanyName = 'Grace Solutions'
|
CompanyName = 'Grace Solutions'
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
|
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
|
||||||
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
|
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
|
||||||
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
|
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
|
||||||
CommitHash = '09c3d5c68bbc'
|
CommitHash = 'e0a6ef02df3e'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections;
|
using System.Collections.Generic;
|
||||||
using PSInfisicalAPI.Errors;
|
using PSInfisicalAPI.Errors;
|
||||||
using PSInfisicalAPI.Secrets;
|
using PSInfisicalAPI.Secrets;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -10,16 +10,16 @@ namespace PSInfisicalAPI.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ToCreateItems_Maps_Standard_Keys()
|
public void ToCreateItems_Maps_Standard_Keys()
|
||||||
{
|
{
|
||||||
Hashtable entry = new Hashtable
|
Dictionary<string, string> entry = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "SecretName", "API_KEY" },
|
{ "SecretName", "API_KEY" },
|
||||||
{ "SecretValue", "abc" },
|
{ "SecretValue", "abc" },
|
||||||
{ "SecretComment", "primary" },
|
{ "SecretComment", "primary" },
|
||||||
{ "SkipMultilineEncoding", true },
|
{ "SkipMultilineEncoding", "true" },
|
||||||
{ "TagIds", new[] { "tag-1", "tag-2" } }
|
{ "TagIds", "tag-1,tag-2" }
|
||||||
};
|
};
|
||||||
|
|
||||||
InfisicalBulkCreateSecretItem[] items = InfisicalBulkSecretConverter.ToCreateItems(new[] { entry });
|
InfisicalBulkCreateSecretItem[] items = InfisicalBulkSecretConverter.ToCreateItems(new IDictionary<string, string>[] { entry });
|
||||||
Assert.Single(items);
|
Assert.Single(items);
|
||||||
Assert.Equal("API_KEY", items[0].SecretName);
|
Assert.Equal("API_KEY", items[0].SecretName);
|
||||||
Assert.Equal("abc", items[0].SecretValue);
|
Assert.Equal("abc", items[0].SecretValue);
|
||||||
@@ -31,13 +31,13 @@ namespace PSInfisicalAPI.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ToCreateItems_Accepts_Name_Alias_For_SecretName()
|
public void ToCreateItems_Accepts_Name_Alias_For_SecretName()
|
||||||
{
|
{
|
||||||
Hashtable entry = new Hashtable
|
Dictionary<string, string> entry = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "Name", "API_KEY" },
|
{ "Name", "API_KEY" },
|
||||||
{ "Value", "abc" }
|
{ "Value", "abc" }
|
||||||
};
|
};
|
||||||
|
|
||||||
InfisicalBulkCreateSecretItem[] items = InfisicalBulkSecretConverter.ToCreateItems(new[] { entry });
|
InfisicalBulkCreateSecretItem[] items = InfisicalBulkSecretConverter.ToCreateItems(new IDictionary<string, string>[] { entry });
|
||||||
Assert.Single(items);
|
Assert.Single(items);
|
||||||
Assert.Equal("API_KEY", items[0].SecretName);
|
Assert.Equal("API_KEY", items[0].SecretName);
|
||||||
Assert.Equal("abc", items[0].SecretValue);
|
Assert.Equal("abc", items[0].SecretValue);
|
||||||
@@ -46,23 +46,23 @@ namespace PSInfisicalAPI.Tests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ToCreateItems_Without_SecretName_Throws()
|
public void ToCreateItems_Without_SecretName_Throws()
|
||||||
{
|
{
|
||||||
Hashtable entry = new Hashtable { { "Value", "abc" } };
|
Dictionary<string, string> entry = new Dictionary<string, string> { { "Value", "abc" } };
|
||||||
|
|
||||||
Assert.Throws<InfisicalConfigurationException>(() =>
|
Assert.Throws<InfisicalConfigurationException>(() =>
|
||||||
InfisicalBulkSecretConverter.ToCreateItems(new[] { entry }));
|
InfisicalBulkSecretConverter.ToCreateItems(new IDictionary<string, string>[] { entry }));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToUpdateItems_Maps_NewSecretName()
|
public void ToUpdateItems_Maps_NewSecretName()
|
||||||
{
|
{
|
||||||
Hashtable entry = new Hashtable
|
Dictionary<string, string> entry = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "SecretName", "API_KEY" },
|
{ "SecretName", "API_KEY" },
|
||||||
{ "NewSecretName", "RENAMED" },
|
{ "NewSecretName", "RENAMED" },
|
||||||
{ "SecretValue", "new-value" }
|
{ "SecretValue", "new-value" }
|
||||||
};
|
};
|
||||||
|
|
||||||
InfisicalBulkUpdateSecretItem[] items = InfisicalBulkSecretConverter.ToUpdateItems(new[] { entry });
|
InfisicalBulkUpdateSecretItem[] items = InfisicalBulkSecretConverter.ToUpdateItems(new IDictionary<string, string>[] { entry });
|
||||||
Assert.Single(items);
|
Assert.Single(items);
|
||||||
Assert.Equal("API_KEY", items[0].SecretName);
|
Assert.Equal("API_KEY", items[0].SecretName);
|
||||||
Assert.Equal("RENAMED", items[0].NewSecretName);
|
Assert.Equal("RENAMED", items[0].NewSecretName);
|
||||||
@@ -70,20 +70,17 @@ namespace PSInfisicalAPI.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToCreateItems_Maps_Metadata_Dictionary()
|
public void ToCreateItems_Trims_TagId_Whitespace_And_Skips_Empty()
|
||||||
{
|
{
|
||||||
Hashtable meta = new Hashtable { { "owner", "platform" }, { "tier", "1" } };
|
Dictionary<string, string> entry = new Dictionary<string, string>
|
||||||
Hashtable entry = new Hashtable
|
|
||||||
{
|
{
|
||||||
{ "SecretName", "API_KEY" },
|
{ "SecretName", "API_KEY" },
|
||||||
{ "SecretValue", "abc" },
|
{ "SecretValue", "abc" },
|
||||||
{ "Metadata", meta }
|
{ "TagIds", " tag-1 , , tag-2 " }
|
||||||
};
|
};
|
||||||
|
|
||||||
InfisicalBulkCreateSecretItem[] items = InfisicalBulkSecretConverter.ToCreateItems(new[] { entry });
|
InfisicalBulkCreateSecretItem[] items = InfisicalBulkSecretConverter.ToCreateItems(new IDictionary<string, string>[] { entry });
|
||||||
Assert.NotNull(items[0].SecretMetadata);
|
Assert.Equal(new[] { "tag-1", "tag-2" }, items[0].TagIds);
|
||||||
Assert.Equal("platform", items[0].SecretMetadata["owner"]);
|
|
||||||
Assert.Equal("1", items[0].SecretMetadata["tier"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -59,12 +59,14 @@ namespace PSInfisicalAPI.Tests
|
|||||||
|
|
||||||
object dto = MakeDto("PSInfisicalAPI.Secrets.InfisicalSecretBatchCreateRequestDto");
|
object dto = MakeDto("PSInfisicalAPI.Secrets.InfisicalSecretBatchCreateRequestDto");
|
||||||
dto.GetType().GetProperty("WorkspaceId").SetValue(dto, "wks-1");
|
dto.GetType().GetProperty("WorkspaceId").SetValue(dto, "wks-1");
|
||||||
|
dto.GetType().GetProperty("ProjectId").SetValue(dto, "wks-1");
|
||||||
dto.GetType().GetProperty("Environment").SetValue(dto, "prod");
|
dto.GetType().GetProperty("Environment").SetValue(dto, "prod");
|
||||||
dto.GetType().GetProperty("SecretPath").SetValue(dto, "/db");
|
dto.GetType().GetProperty("SecretPath").SetValue(dto, "/db");
|
||||||
dto.GetType().GetProperty("Secrets").SetValue(dto, list);
|
dto.GetType().GetProperty("Secrets").SetValue(dto, list);
|
||||||
|
|
||||||
JObject json = JObject.Parse(JsonConvert.SerializeObject(dto));
|
JObject json = JObject.Parse(JsonConvert.SerializeObject(dto));
|
||||||
Assert.Equal("wks-1", (string)json["workspaceId"]);
|
Assert.Equal("wks-1", (string)json["workspaceId"]);
|
||||||
|
Assert.Equal("wks-1", (string)json["projectId"]);
|
||||||
Assert.Equal("prod", (string)json["environment"]);
|
Assert.Equal("prod", (string)json["environment"]);
|
||||||
Assert.Equal("/db", (string)json["secretPath"]);
|
Assert.Equal("/db", (string)json["secretPath"]);
|
||||||
JArray secrets = (JArray)json["secrets"];
|
JArray secrets = (JArray)json["secrets"];
|
||||||
@@ -73,6 +75,31 @@ namespace PSInfisicalAPI.Tests
|
|||||||
Assert.Equal("V1", (string)secrets[0]["secretValue"]);
|
Assert.Equal("V1", (string)secrets[0]["secretValue"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BatchCreateRequest_Omits_Null_WorkspaceId_When_Only_ProjectId_Set()
|
||||||
|
{
|
||||||
|
object dto = MakeDto("PSInfisicalAPI.Secrets.InfisicalSecretBatchCreateRequestDto");
|
||||||
|
dto.GetType().GetProperty("ProjectId").SetValue(dto, "wks-1");
|
||||||
|
dto.GetType().GetProperty("Environment").SetValue(dto, "prod");
|
||||||
|
|
||||||
|
JObject json = JObject.Parse(JsonConvert.SerializeObject(dto));
|
||||||
|
Assert.False(json.ContainsKey("workspaceId"));
|
||||||
|
Assert.Equal("wks-1", (string)json["projectId"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BatchUpdateRequest_Includes_ProjectId_Alongside_WorkspaceId()
|
||||||
|
{
|
||||||
|
object dto = MakeDto("PSInfisicalAPI.Secrets.InfisicalSecretBatchUpdateRequestDto");
|
||||||
|
dto.GetType().GetProperty("WorkspaceId").SetValue(dto, "wks-1");
|
||||||
|
dto.GetType().GetProperty("ProjectId").SetValue(dto, "wks-1");
|
||||||
|
dto.GetType().GetProperty("Environment").SetValue(dto, "prod");
|
||||||
|
|
||||||
|
JObject json = JObject.Parse(JsonConvert.SerializeObject(dto));
|
||||||
|
Assert.Equal("wks-1", (string)json["workspaceId"]);
|
||||||
|
Assert.Equal("wks-1", (string)json["projectId"]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void BatchDeleteRequest_Serializes_With_Secret_Keys()
|
public void BatchDeleteRequest_Serializes_With_Secret_Keys()
|
||||||
{
|
{
|
||||||
@@ -86,11 +113,13 @@ namespace PSInfisicalAPI.Tests
|
|||||||
|
|
||||||
object dto = MakeDto("PSInfisicalAPI.Secrets.InfisicalSecretBatchDeleteRequestDto");
|
object dto = MakeDto("PSInfisicalAPI.Secrets.InfisicalSecretBatchDeleteRequestDto");
|
||||||
dto.GetType().GetProperty("WorkspaceId").SetValue(dto, "wks-1");
|
dto.GetType().GetProperty("WorkspaceId").SetValue(dto, "wks-1");
|
||||||
|
dto.GetType().GetProperty("ProjectId").SetValue(dto, "wks-1");
|
||||||
dto.GetType().GetProperty("Environment").SetValue(dto, "prod");
|
dto.GetType().GetProperty("Environment").SetValue(dto, "prod");
|
||||||
dto.GetType().GetProperty("Secrets").SetValue(dto, list);
|
dto.GetType().GetProperty("Secrets").SetValue(dto, list);
|
||||||
|
|
||||||
JObject json = JObject.Parse(JsonConvert.SerializeObject(dto));
|
JObject json = JObject.Parse(JsonConvert.SerializeObject(dto));
|
||||||
Assert.Equal("wks-1", (string)json["workspaceId"]);
|
Assert.Equal("wks-1", (string)json["workspaceId"]);
|
||||||
|
Assert.Equal("wks-1", (string)json["projectId"]);
|
||||||
JArray secrets = (JArray)json["secrets"];
|
JArray secrets = (JArray)json["secrets"];
|
||||||
Assert.Single(secrets);
|
Assert.Single(secrets);
|
||||||
Assert.Equal("K1", (string)secrets[0]["secretKey"]);
|
Assert.Equal("K1", (string)secrets[0]["secretKey"]);
|
||||||
|
|||||||
@@ -71,9 +71,9 @@ namespace PSInfisicalAPI.Tests
|
|||||||
[InlineData(InfisicalEndpointNames.LdapAuthLogin, "POST", "/api/v1/auth/ldap-auth/login")]
|
[InlineData(InfisicalEndpointNames.LdapAuthLogin, "POST", "/api/v1/auth/ldap-auth/login")]
|
||||||
[InlineData(InfisicalEndpointNames.AzureAuthLogin, "POST", "/api/v1/auth/azure-auth/login")]
|
[InlineData(InfisicalEndpointNames.AzureAuthLogin, "POST", "/api/v1/auth/azure-auth/login")]
|
||||||
[InlineData(InfisicalEndpointNames.GcpIamAuthLogin, "POST", "/api/v1/auth/gcp-auth/login")]
|
[InlineData(InfisicalEndpointNames.GcpIamAuthLogin, "POST", "/api/v1/auth/gcp-auth/login")]
|
||||||
[InlineData(InfisicalEndpointNames.BulkCreateSecret, "POST", "/api/v3/secrets/batch/raw")]
|
[InlineData(InfisicalEndpointNames.BulkCreateSecret, "POST", "/api/v4/secrets/batch")]
|
||||||
[InlineData(InfisicalEndpointNames.BulkUpdateSecret, "PATCH", "/api/v3/secrets/batch/raw")]
|
[InlineData(InfisicalEndpointNames.BulkUpdateSecret, "PATCH", "/api/v4/secrets/batch")]
|
||||||
[InlineData(InfisicalEndpointNames.BulkDeleteSecret, "DELETE", "/api/v3/secrets/batch/raw")]
|
[InlineData(InfisicalEndpointNames.BulkDeleteSecret, "DELETE", "/api/v4/secrets/batch")]
|
||||||
[InlineData(InfisicalEndpointNames.DuplicateSecret, "POST", "/api/v4/secrets/duplicate")]
|
[InlineData(InfisicalEndpointNames.DuplicateSecret, "POST", "/api/v4/secrets/duplicate")]
|
||||||
public void Registered_Endpoints_Have_Expected_Shape(string name, string method, string template)
|
public void Registered_Endpoints_Have_Expected_Shape(string name, string method, string template)
|
||||||
{
|
{
|
||||||
@@ -81,5 +81,19 @@ namespace PSInfisicalAPI.Tests
|
|||||||
Assert.Equal(method, definition.Method);
|
Assert.Equal(method, definition.Method);
|
||||||
Assert.Equal(template, definition.Template);
|
Assert.Equal(template, definition.Template);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(InfisicalEndpointNames.BulkCreateSecret, "POST", "/api/v3/secrets/batch/raw")]
|
||||||
|
[InlineData(InfisicalEndpointNames.BulkUpdateSecret, "PATCH", "/api/v3/secrets/batch/raw")]
|
||||||
|
[InlineData(InfisicalEndpointNames.BulkDeleteSecret, "DELETE", "/api/v3/secrets/batch/raw")]
|
||||||
|
public void Bulk_Endpoints_Retain_V3_Fallback_Candidate(string name, string method, string template)
|
||||||
|
{
|
||||||
|
System.Collections.Generic.IReadOnlyList<InfisicalEndpointDefinition> candidates = InfisicalEndpointRegistry.GetCandidates(name);
|
||||||
|
Assert.Equal(2, candidates.Count);
|
||||||
|
Assert.Equal("v4", candidates[0].Version);
|
||||||
|
Assert.Equal("v3", candidates[1].Version);
|
||||||
|
Assert.Equal(method, candidates[1].Method);
|
||||||
|
Assert.Equal(template, candidates[1].Template);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections.Generic;
|
||||||
using System.Management.Automation;
|
using System.Management.Automation;
|
||||||
using System.Security;
|
using System.Security;
|
||||||
using PSInfisicalAPI.Connections;
|
using PSInfisicalAPI.Connections;
|
||||||
@@ -24,7 +24,7 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
public SecureString SecureSecretValue { get; set; }
|
public SecureString SecureSecretValue { get; set; }
|
||||||
|
|
||||||
[Parameter(Mandatory = true, Position = 0, ParameterSetName = "Bulk", ValueFromPipeline = true)]
|
[Parameter(Mandatory = true, Position = 0, ParameterSetName = "Bulk", ValueFromPipeline = true)]
|
||||||
public Hashtable[] Secrets { get; set; }
|
public IDictionary<string, string>[] Secrets { get; set; }
|
||||||
|
|
||||||
[Parameter] public string SecretComment { get; set; }
|
[Parameter] public string SecretComment { get; set; }
|
||||||
[Parameter] public string ProjectId { get; set; }
|
[Parameter] public string ProjectId { get; set; }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections.Generic;
|
||||||
using System.Management.Automation;
|
using System.Management.Automation;
|
||||||
using System.Security;
|
using System.Security;
|
||||||
using PSInfisicalAPI.Connections;
|
using PSInfisicalAPI.Connections;
|
||||||
@@ -21,7 +21,7 @@ namespace PSInfisicalAPI.Cmdlets
|
|||||||
[Parameter(ParameterSetName = "SecureString")] public SecureString SecureSecretValue { get; set; }
|
[Parameter(ParameterSetName = "SecureString")] public SecureString SecureSecretValue { get; set; }
|
||||||
|
|
||||||
[Parameter(Mandatory = true, Position = 0, ParameterSetName = "Bulk", ValueFromPipeline = true)]
|
[Parameter(Mandatory = true, Position = 0, ParameterSetName = "Bulk", ValueFromPipeline = true)]
|
||||||
public Hashtable[] Secrets { get; set; }
|
public IDictionary<string, string>[] Secrets { get; set; }
|
||||||
|
|
||||||
[Parameter] public string NewSecretName { get; set; }
|
[Parameter] public string NewSecretName { get; set; }
|
||||||
[Parameter] public string SecretComment { get; set; }
|
[Parameter] public string SecretComment { get; set; }
|
||||||
|
|||||||
@@ -198,6 +198,18 @@ namespace PSInfisicalAPI.Endpoints
|
|||||||
ContainsSecretMaterialInResponse = true
|
ContainsSecretMaterialInResponse = true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Add(map, new InfisicalEndpointDefinition
|
||||||
|
{
|
||||||
|
Name = InfisicalEndpointNames.BulkCreateSecret,
|
||||||
|
Resource = "Secrets",
|
||||||
|
Version = "v4",
|
||||||
|
Method = "POST",
|
||||||
|
Template = "/api/v4/secrets/batch",
|
||||||
|
RequiresAuthorization = true,
|
||||||
|
ContainsSecretMaterialInRequest = true,
|
||||||
|
ContainsSecretMaterialInResponse = true
|
||||||
|
});
|
||||||
|
|
||||||
Add(map, new InfisicalEndpointDefinition
|
Add(map, new InfisicalEndpointDefinition
|
||||||
{
|
{
|
||||||
Name = InfisicalEndpointNames.BulkCreateSecret,
|
Name = InfisicalEndpointNames.BulkCreateSecret,
|
||||||
@@ -210,6 +222,18 @@ namespace PSInfisicalAPI.Endpoints
|
|||||||
ContainsSecretMaterialInResponse = true
|
ContainsSecretMaterialInResponse = true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Add(map, new InfisicalEndpointDefinition
|
||||||
|
{
|
||||||
|
Name = InfisicalEndpointNames.BulkUpdateSecret,
|
||||||
|
Resource = "Secrets",
|
||||||
|
Version = "v4",
|
||||||
|
Method = "PATCH",
|
||||||
|
Template = "/api/v4/secrets/batch",
|
||||||
|
RequiresAuthorization = true,
|
||||||
|
ContainsSecretMaterialInRequest = true,
|
||||||
|
ContainsSecretMaterialInResponse = true
|
||||||
|
});
|
||||||
|
|
||||||
Add(map, new InfisicalEndpointDefinition
|
Add(map, new InfisicalEndpointDefinition
|
||||||
{
|
{
|
||||||
Name = InfisicalEndpointNames.BulkUpdateSecret,
|
Name = InfisicalEndpointNames.BulkUpdateSecret,
|
||||||
@@ -222,6 +246,18 @@ namespace PSInfisicalAPI.Endpoints
|
|||||||
ContainsSecretMaterialInResponse = true
|
ContainsSecretMaterialInResponse = true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Add(map, new InfisicalEndpointDefinition
|
||||||
|
{
|
||||||
|
Name = InfisicalEndpointNames.BulkDeleteSecret,
|
||||||
|
Resource = "Secrets",
|
||||||
|
Version = "v4",
|
||||||
|
Method = "DELETE",
|
||||||
|
Template = "/api/v4/secrets/batch",
|
||||||
|
RequiresAuthorization = true,
|
||||||
|
ContainsSecretMaterialInRequest = true,
|
||||||
|
ContainsSecretMaterialInResponse = true
|
||||||
|
});
|
||||||
|
|
||||||
Add(map, new InfisicalEndpointDefinition
|
Add(map, new InfisicalEndpointDefinition
|
||||||
{
|
{
|
||||||
Name = InfisicalEndpointNames.BulkDeleteSecret,
|
Name = InfisicalEndpointNames.BulkDeleteSecret,
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using PSInfisicalAPI.Errors;
|
using PSInfisicalAPI.Errors;
|
||||||
using PSInfisicalAPI.Security;
|
|
||||||
|
|
||||||
namespace PSInfisicalAPI.Secrets
|
namespace PSInfisicalAPI.Secrets
|
||||||
{
|
{
|
||||||
public static class InfisicalBulkSecretConverter
|
public static class InfisicalBulkSecretConverter
|
||||||
{
|
{
|
||||||
public static InfisicalBulkCreateSecretItem[] ToCreateItems(IEnumerable input)
|
public static InfisicalBulkCreateSecretItem[] ToCreateItems(IEnumerable<IDictionary<string, string>> input)
|
||||||
{
|
{
|
||||||
if (input == null) { return new InfisicalBulkCreateSecretItem[0]; }
|
if (input == null) { return new InfisicalBulkCreateSecretItem[0]; }
|
||||||
|
|
||||||
List<InfisicalBulkCreateSecretItem> list = new List<InfisicalBulkCreateSecretItem>();
|
List<InfisicalBulkCreateSecretItem> list = new List<InfisicalBulkCreateSecretItem>();
|
||||||
foreach (object element in input)
|
foreach (IDictionary<string, string> entry in input)
|
||||||
{
|
{
|
||||||
Hashtable table = AsHashtable(element);
|
if (entry == null) { continue; }
|
||||||
|
IDictionary<string, string> table = Normalize(entry);
|
||||||
InfisicalBulkCreateSecretItem item = new InfisicalBulkCreateSecretItem
|
InfisicalBulkCreateSecretItem item = new InfisicalBulkCreateSecretItem
|
||||||
{
|
{
|
||||||
SecretName = GetString(table, "SecretName", "Name", "Key", "SecretKey"),
|
SecretName = GetString(table, "SecretName", "Name", "Key", "SecretKey"),
|
||||||
SecretValue = GetSecretValue(table, "SecretValue", "Value"),
|
SecretValue = GetString(table, "SecretValue", "Value"),
|
||||||
SecretComment = GetString(table, "SecretComment", "Comment"),
|
SecretComment = GetString(table, "SecretComment", "Comment"),
|
||||||
SkipMultilineEncoding = GetBool(table, "SkipMultilineEncoding"),
|
SkipMultilineEncoding = GetBool(table, "SkipMultilineEncoding"),
|
||||||
TagIds = GetStringArray(table, "TagIds"),
|
TagIds = GetStringArray(table, "TagIds")
|
||||||
SecretMetadata = GetStringDictionary(table, "SecretMetadata", "Metadata")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(item.SecretName))
|
if (string.IsNullOrEmpty(item.SecretName))
|
||||||
@@ -37,23 +35,23 @@ namespace PSInfisicalAPI.Secrets
|
|||||||
return list.ToArray();
|
return list.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InfisicalBulkUpdateSecretItem[] ToUpdateItems(IEnumerable input)
|
public static InfisicalBulkUpdateSecretItem[] ToUpdateItems(IEnumerable<IDictionary<string, string>> input)
|
||||||
{
|
{
|
||||||
if (input == null) { return new InfisicalBulkUpdateSecretItem[0]; }
|
if (input == null) { return new InfisicalBulkUpdateSecretItem[0]; }
|
||||||
|
|
||||||
List<InfisicalBulkUpdateSecretItem> list = new List<InfisicalBulkUpdateSecretItem>();
|
List<InfisicalBulkUpdateSecretItem> list = new List<InfisicalBulkUpdateSecretItem>();
|
||||||
foreach (object element in input)
|
foreach (IDictionary<string, string> entry in input)
|
||||||
{
|
{
|
||||||
Hashtable table = AsHashtable(element);
|
if (entry == null) { continue; }
|
||||||
|
IDictionary<string, string> table = Normalize(entry);
|
||||||
InfisicalBulkUpdateSecretItem item = new InfisicalBulkUpdateSecretItem
|
InfisicalBulkUpdateSecretItem item = new InfisicalBulkUpdateSecretItem
|
||||||
{
|
{
|
||||||
SecretName = GetString(table, "SecretName", "Name", "Key", "SecretKey"),
|
SecretName = GetString(table, "SecretName", "Name", "Key", "SecretKey"),
|
||||||
NewSecretName = GetString(table, "NewSecretName", "NewName"),
|
NewSecretName = GetString(table, "NewSecretName", "NewName"),
|
||||||
SecretValue = GetSecretValue(table, "SecretValue", "Value"),
|
SecretValue = GetString(table, "SecretValue", "Value"),
|
||||||
SecretComment = GetString(table, "SecretComment", "Comment"),
|
SecretComment = GetString(table, "SecretComment", "Comment"),
|
||||||
SkipMultilineEncoding = GetBool(table, "SkipMultilineEncoding"),
|
SkipMultilineEncoding = GetBool(table, "SkipMultilineEncoding"),
|
||||||
TagIds = GetStringArray(table, "TagIds"),
|
TagIds = GetStringArray(table, "TagIds")
|
||||||
SecretMetadata = GetStringDictionary(table, "SecretMetadata", "Metadata")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(item.SecretName))
|
if (string.IsNullOrEmpty(item.SecretName))
|
||||||
@@ -67,98 +65,53 @@ namespace PSInfisicalAPI.Secrets
|
|||||||
return list.ToArray();
|
return list.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Hashtable AsHashtable(object element)
|
private static IDictionary<string, string> Normalize(IDictionary<string, string> source)
|
||||||
{
|
{
|
||||||
if (element is Hashtable hashtable) { return hashtable; }
|
Dictionary<string, string> normalized = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
if (element is IDictionary dictionary)
|
foreach (KeyValuePair<string, string> kvp in source)
|
||||||
{
|
{
|
||||||
Hashtable converted = new Hashtable(StringComparer.OrdinalIgnoreCase);
|
if (string.IsNullOrEmpty(kvp.Key)) { continue; }
|
||||||
foreach (DictionaryEntry entry in dictionary)
|
normalized[kvp.Key] = kvp.Value;
|
||||||
{
|
|
||||||
if (entry.Key == null) { continue; }
|
|
||||||
converted[entry.Key.ToString()] = entry.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return converted;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InfisicalConfigurationException("Bulk secret entries must be Hashtable or IDictionary values.");
|
private static string GetString(IDictionary<string, string> table, params string[] keys)
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetString(Hashtable table, params string[] keys)
|
|
||||||
{
|
{
|
||||||
foreach (string key in keys)
|
foreach (string key in keys)
|
||||||
{
|
{
|
||||||
if (table.ContainsKey(key) && table[key] != null)
|
string value;
|
||||||
|
if (table.TryGetValue(key, out value) && !string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
return table[key].ToString();
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetSecretValue(Hashtable table, params string[] keys)
|
private static bool? GetBool(IDictionary<string, string> table, string key)
|
||||||
{
|
{
|
||||||
foreach (string key in keys)
|
string value;
|
||||||
{
|
if (!table.TryGetValue(key, out value) || string.IsNullOrEmpty(value)) { return null; }
|
||||||
if (!table.ContainsKey(key)) { continue; }
|
|
||||||
object value = table[key];
|
|
||||||
if (value == null) { return null; }
|
|
||||||
if (value is System.Security.SecureString secure)
|
|
||||||
{
|
|
||||||
return SecureStringUtility.UsePlainText(secure, plain => plain);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool? GetBool(Hashtable table, string key)
|
|
||||||
{
|
|
||||||
if (!table.ContainsKey(key) || table[key] == null) { return null; }
|
|
||||||
object value = table[key];
|
|
||||||
if (value is bool b) { return b; }
|
|
||||||
bool parsed;
|
bool parsed;
|
||||||
return bool.TryParse(value.ToString(), out parsed) ? parsed : (bool?)null;
|
return bool.TryParse(value, out parsed) ? parsed : (bool?)null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string[] GetStringArray(Hashtable table, string key)
|
private static string[] GetStringArray(IDictionary<string, string> table, string key)
|
||||||
{
|
{
|
||||||
if (!table.ContainsKey(key) || table[key] == null) { return null; }
|
string value;
|
||||||
object value = table[key];
|
if (!table.TryGetValue(key, out value) || string.IsNullOrEmpty(value)) { return null; }
|
||||||
if (value is string[] direct) { return direct; }
|
string[] parts = value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (value is IEnumerable enumerable && !(value is string))
|
List<string> trimmed = new List<string>(parts.Length);
|
||||||
|
foreach (string part in parts)
|
||||||
{
|
{
|
||||||
List<string> items = new List<string>();
|
string item = part.Trim();
|
||||||
foreach (object item in enumerable) { if (item != null) { items.Add(item.ToString()); } }
|
if (!string.IsNullOrEmpty(item)) { trimmed.Add(item); }
|
||||||
return items.ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new[] { value.ToString() };
|
return trimmed.Count == 0 ? null : trimmed.ToArray();
|
||||||
}
|
|
||||||
|
|
||||||
private static Dictionary<string, string> GetStringDictionary(Hashtable table, params string[] keys)
|
|
||||||
{
|
|
||||||
foreach (string key in keys)
|
|
||||||
{
|
|
||||||
if (!table.ContainsKey(key) || table[key] == null) { continue; }
|
|
||||||
if (table[key] is IDictionary dictionary)
|
|
||||||
{
|
|
||||||
Dictionary<string, string> result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (DictionaryEntry entry in dictionary)
|
|
||||||
{
|
|
||||||
if (entry.Key == null) { continue; }
|
|
||||||
result[entry.Key.ToString()] = entry.Value != null ? entry.Value.ToString() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ namespace PSInfisicalAPI.Secrets
|
|||||||
|
|
||||||
internal sealed class InfisicalSecretBatchCreateRequestDto
|
internal sealed class InfisicalSecretBatchCreateRequestDto
|
||||||
{
|
{
|
||||||
[JsonProperty("workspaceId")] public string WorkspaceId { get; set; }
|
[JsonProperty("workspaceId", NullValueHandling = NullValueHandling.Ignore)] public string WorkspaceId { get; set; }
|
||||||
|
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
|
||||||
[JsonProperty("environment")] public string Environment { get; set; }
|
[JsonProperty("environment")] public string Environment { get; set; }
|
||||||
[JsonProperty("secretPath", NullValueHandling = NullValueHandling.Ignore)] public string SecretPath { get; set; }
|
[JsonProperty("secretPath", NullValueHandling = NullValueHandling.Ignore)] public string SecretPath { get; set; }
|
||||||
[JsonProperty("secrets")] public List<InfisicalSecretBatchCreateItemDto> Secrets { get; set; }
|
[JsonProperty("secrets")] public List<InfisicalSecretBatchCreateItemDto> Secrets { get; set; }
|
||||||
@@ -125,7 +126,8 @@ namespace PSInfisicalAPI.Secrets
|
|||||||
|
|
||||||
internal sealed class InfisicalSecretBatchUpdateRequestDto
|
internal sealed class InfisicalSecretBatchUpdateRequestDto
|
||||||
{
|
{
|
||||||
[JsonProperty("workspaceId")] public string WorkspaceId { get; set; }
|
[JsonProperty("workspaceId", NullValueHandling = NullValueHandling.Ignore)] public string WorkspaceId { get; set; }
|
||||||
|
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
|
||||||
[JsonProperty("environment")] public string Environment { get; set; }
|
[JsonProperty("environment")] public string Environment { get; set; }
|
||||||
[JsonProperty("secretPath", NullValueHandling = NullValueHandling.Ignore)] public string SecretPath { get; set; }
|
[JsonProperty("secretPath", NullValueHandling = NullValueHandling.Ignore)] public string SecretPath { get; set; }
|
||||||
[JsonProperty("mode", NullValueHandling = NullValueHandling.Ignore)] public string Mode { get; set; }
|
[JsonProperty("mode", NullValueHandling = NullValueHandling.Ignore)] public string Mode { get; set; }
|
||||||
@@ -134,7 +136,8 @@ namespace PSInfisicalAPI.Secrets
|
|||||||
|
|
||||||
internal sealed class InfisicalSecretBatchDeleteRequestDto
|
internal sealed class InfisicalSecretBatchDeleteRequestDto
|
||||||
{
|
{
|
||||||
[JsonProperty("workspaceId")] public string WorkspaceId { get; set; }
|
[JsonProperty("workspaceId", NullValueHandling = NullValueHandling.Ignore)] public string WorkspaceId { get; set; }
|
||||||
|
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
|
||||||
[JsonProperty("environment")] public string Environment { get; set; }
|
[JsonProperty("environment")] public string Environment { get; set; }
|
||||||
[JsonProperty("secretPath", NullValueHandling = NullValueHandling.Ignore)] public string SecretPath { get; set; }
|
[JsonProperty("secretPath", NullValueHandling = NullValueHandling.Ignore)] public string SecretPath { get; set; }
|
||||||
[JsonProperty("secrets")] public List<InfisicalSecretBatchDeleteItemDto> Secrets { get; set; }
|
[JsonProperty("secrets")] public List<InfisicalSecretBatchDeleteItemDto> Secrets { get; set; }
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ namespace PSInfisicalAPI.Secrets
|
|||||||
InfisicalSecretBatchCreateRequestDto dtoRequest = new InfisicalSecretBatchCreateRequestDto
|
InfisicalSecretBatchCreateRequestDto dtoRequest = new InfisicalSecretBatchCreateRequestDto
|
||||||
{
|
{
|
||||||
WorkspaceId = resolvedProjectId,
|
WorkspaceId = resolvedProjectId,
|
||||||
|
ProjectId = resolvedProjectId,
|
||||||
Environment = resolvedEnvironment,
|
Environment = resolvedEnvironment,
|
||||||
SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"),
|
SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"),
|
||||||
Secrets = items
|
Secrets = items
|
||||||
@@ -309,6 +310,7 @@ namespace PSInfisicalAPI.Secrets
|
|||||||
InfisicalSecretBatchUpdateRequestDto dtoRequest = new InfisicalSecretBatchUpdateRequestDto
|
InfisicalSecretBatchUpdateRequestDto dtoRequest = new InfisicalSecretBatchUpdateRequestDto
|
||||||
{
|
{
|
||||||
WorkspaceId = resolvedProjectId,
|
WorkspaceId = resolvedProjectId,
|
||||||
|
ProjectId = resolvedProjectId,
|
||||||
Environment = resolvedEnvironment,
|
Environment = resolvedEnvironment,
|
||||||
SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"),
|
SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"),
|
||||||
Mode = request.Mode,
|
Mode = request.Mode,
|
||||||
@@ -355,6 +357,7 @@ namespace PSInfisicalAPI.Secrets
|
|||||||
InfisicalSecretBatchDeleteRequestDto dtoRequest = new InfisicalSecretBatchDeleteRequestDto
|
InfisicalSecretBatchDeleteRequestDto dtoRequest = new InfisicalSecretBatchDeleteRequestDto
|
||||||
{
|
{
|
||||||
WorkspaceId = resolvedProjectId,
|
WorkspaceId = resolvedProjectId,
|
||||||
|
ProjectId = resolvedProjectId,
|
||||||
Environment = resolvedEnvironment,
|
Environment = resolvedEnvironment,
|
||||||
SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"),
|
SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"),
|
||||||
Secrets = items
|
Secrets = items
|
||||||
|
|||||||
Reference in New Issue
Block a user