From 9efdafb7fb577fa81b574f31806b732777b45651 Mon Sep 17 00:00:00 2001 From: GraceSolutions Date: Thu, 4 Jun 2026 16:53:52 -0400 Subject: [PATCH] Add Get-InfisicalCertificatePolicy cmdlet Covers GET /api/v1/cert-manager/certificate-policies (List default with optional -Limit, -Offset) and GET /api/v1/cert-manager/certificate-policies/{certificatePolicyId} (ById). New InfisicalCertificatePolicy model surfaces subject, SANs, key usages, extended key usages, algorithms, and validity. Polymorphic string-or-array fields (allowed/required/keyAlgorithm) are normalized to arrays; sans is normalized whether the API returns an object or an array. Manifest, build expected list, and MAML help updated. --- CHANGELOG.md | 1 + .../en-US/PSInfisicalAPI.dll-Help.xml | 42 ++++++ build.ps1 | 3 +- .../GetInfisicalCertificatePolicyCmdlet.cs | 54 +++++++ .../Endpoints/InfisicalEndpointNames.cs | 3 + .../Endpoints/InfisicalEndpointRegistry.cs | 20 +++ .../Models/InfisicalCertificatePolicy.cs | 50 +++++++ .../Pki/InfisicalCertificatePolicyDtos.cs | 58 ++++++++ .../Pki/InfisicalCertificatePolicyMapper.cs | 138 ++++++++++++++++++ src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs | 88 +++++++++++ 10 files changed, 456 insertions(+), 1 deletion(-) create mode 100644 src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs create mode 100644 src/PSInfisicalAPI/Models/InfisicalCertificatePolicy.cs create mode 100644 src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyDtos.cs create mode 100644 src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyMapper.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c0b34b..0f1f8a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos - Infisical API error responses are now parsed to surface the server-side `message`, `error`, and `reqId` fields. The 4xx/5xx exception message includes the human-readable explanation (e.g. "The project is of type secret-manager") instead of an opaque `Infisical API returned 400 (Bad Request)`. The `InfisicalApiException` gains `ApiErrorMessage` and `ApiRequestId` properties; `InfisicalErrorDetails` carries the same fields so PowerShell error records and logger output expose them. - `Get-InfisicalCertificateProfile` added with `List` (default) and `ById` parameter sets. List binds to `GET /api/v1/cert-manager/certificate-profiles` (optional `-Limit`, `-Offset`, `-IncludeConfigs`); ById binds to `GET /api/v1/cert-manager/certificate-profiles/{certificateProfileId}`. New `InfisicalCertificateProfile` model surfaces ca/policy ids, slug, enrollment type, per-profile defaults (ttl, key/extended key usages), and the embedded CA/policy/apiConfig summaries. +- `Get-InfisicalCertificatePolicy` added with `List` (default) and `ById` parameter sets. List binds to `GET /api/v1/cert-manager/certificate-policies` (optional `-Limit`, `-Offset`); ById binds to `GET /api/v1/cert-manager/certificate-policies/{certificatePolicyId}`. New `InfisicalCertificatePolicy` model surfaces subject, SANs, key usages, extended key usages, algorithms, and validity. Polymorphic string-or-array fields (`allowed`, `required`, `keyAlgorithm`) are normalized to arrays; `sans` is normalized whether the API returns an object or an array. ## 2026.06.04.1920 diff --git a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml index 15a12f8..0501b7c 100644 --- a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml +++ b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml @@ -1170,6 +1170,48 @@ $GetInfisicalCertificateProfileResult = Get-InfisicalCertificateProfile @GetInfi + + + Get-InfisicalCertificatePolicy + Lists or retrieves Infisical certificate policies in a project. + Get + InfisicalCertificatePolicy + + + Default (List parameter set) returns every certificate policy configured on the project via /api/v1/cert-manager/certificate-policies, with optional -Limit and -Offset. When -PolicyId is supplied (ById parameter set) the cmdlet returns one policy by its id. -ProjectId defaults to the session-pinned project in both modes. + + + Notes + + Policies define the allowed/required subject, SANs, key usages, extended key usages, key algorithms, signature algorithm, and validity windows that certificate profiles enforce. Each profile binds exactly one policy via its CertificatePolicyId. + + + + + EXAMPLE 1 + Get-InfisicalCertificatePolicy + Lists every certificate policy defined on the session-pinned project. + + + EXAMPLE 2 + Get-InfisicalCertificatePolicy -PolicyId '3e69306a-e7c1-4fd2-a140-7fb300e53c43' + Retrieves a single certificate policy by id from the session-pinned project. + + + EXAMPLE 3 + $GetInfisicalCertificatePolicyListResult = Get-InfisicalCertificatePolicy | Where-Object { $_.Name -ieq 'codesigning' } + +$GetInfisicalCertificatePolicyParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificatePolicyParameters.PolicyId = $GetInfisicalCertificatePolicyListResult[0].Id +$GetInfisicalCertificatePolicyParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalCertificatePolicyParameters.Verbose = $True + +$GetInfisicalCertificatePolicyResult = Get-InfisicalCertificatePolicy @GetInfisicalCertificatePolicyParameters + Filters policies whose name equals 'codesigning' and refetches the canonical record for the first match using a splatted parameter set. + + + + Search-InfisicalCertificate diff --git a/build.ps1 b/build.ps1 index d5db70a..8d035ba 100644 --- a/build.ps1 +++ b/build.ps1 @@ -132,6 +132,7 @@ function Write-Manifest { 'Get-InfisicalCertificateAuthority', 'Get-InfisicalPkiSubscriber', 'Get-InfisicalCertificateProfile', + 'Get-InfisicalCertificatePolicy', 'Get-InfisicalCertificate', 'Search-InfisicalCertificate', 'Request-InfisicalCertificate', @@ -203,7 +204,7 @@ if (`$cmds.Count -eq 0) { throw "No cmdlets were exported by the PSInfisicalAPI module." } -`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificate','Search-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate') +`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Search-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate') foreach (`$expected in `$expectedCmds) { if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) { throw "Cmdlet not found: `$expected" diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs new file mode 100644 index 0000000..01b59bb --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs @@ -0,0 +1,54 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalCertificatePolicy", DefaultParameterSetName = "List")] + [OutputType(typeof(InfisicalCertificatePolicy))] + public sealed class GetInfisicalCertificatePolicyCmdlet : InfisicalCmdletBase + { + [Parameter(ParameterSetName = "ById", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("Id", "CertificatePolicyId")] + public string PolicyId { get; set; } + + [Parameter] public string ProjectId { get; set; } + + [Parameter(ParameterSetName = "List")] public int? Limit { get; set; } + + [Parameter(ParameterSetName = "List")] public int? Offset { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + string resolvedProjectId = ResolveProjectId(connection, ProjectId); + + if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal)) + { + InfisicalCertificatePolicy policy = client.GetCertificatePolicy(connection, PolicyId, resolvedProjectId); + if (policy != null) + { + WriteObject(policy); + } + + return; + } + + InfisicalCertificatePolicy[] all = client.ListCertificatePolicies(connection, resolvedProjectId, Limit, Offset); + foreach (InfisicalCertificatePolicy policy in all) + { + WriteObject(policy); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalCertificatePolicyCmdlet", "GetCertificatePolicy", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs index 779bba9..78f934d 100644 --- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs +++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs @@ -57,5 +57,8 @@ namespace PSInfisicalAPI.Endpoints public const string ListCertificateProfiles = "ListCertificateProfiles"; public const string GetCertificateProfile = "GetCertificateProfile"; + + public const string ListCertificatePolicies = "ListCertificatePolicies"; + public const string GetCertificatePolicy = "GetCertificatePolicy"; } } diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs index e1ae4fe..985bab4 100644 --- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs +++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs @@ -662,6 +662,26 @@ namespace PSInfisicalAPI.Endpoints Template = "/api/v1/cert-manager/certificate-profiles/{certificateProfileId}", RequiresAuthorization = true }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListCertificatePolicies, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/certificate-policies", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GetCertificatePolicy, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/certificate-policies/{certificatePolicyId}", + RequiresAuthorization = true + }); } public static InfisicalEndpointDefinition Get(string name) diff --git a/src/PSInfisicalAPI/Models/InfisicalCertificatePolicy.cs b/src/PSInfisicalAPI/Models/InfisicalCertificatePolicy.cs new file mode 100644 index 0000000..5708648 --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalCertificatePolicy.cs @@ -0,0 +1,50 @@ +using System; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalCertificatePolicy + { + public string Id { get; set; } + public string ProjectId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public InfisicalCertificatePolicySubject Subject { get; set; } + public InfisicalCertificatePolicySan[] Sans { get; set; } + public InfisicalCertificatePolicyUsages KeyUsages { get; set; } + public InfisicalCertificatePolicyUsages ExtendedKeyUsages { get; set; } + public InfisicalCertificatePolicyAlgorithms Algorithms { get; set; } + public InfisicalCertificatePolicyValidity Validity { get; set; } + public DateTimeOffset? CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + } + + public sealed class InfisicalCertificatePolicySubject + { + public string Type { get; set; } + public string[] Allowed { get; set; } + } + + public sealed class InfisicalCertificatePolicySan + { + public string Type { get; set; } + public string[] Allowed { get; set; } + public string[] Required { get; set; } + } + + public sealed class InfisicalCertificatePolicyUsages + { + public string[] Allowed { get; set; } + public string[] Required { get; set; } + } + + public sealed class InfisicalCertificatePolicyAlgorithms + { + public string Signature { get; set; } + public string[] KeyAlgorithms { get; set; } + } + + public sealed class InfisicalCertificatePolicyValidity + { + public string Max { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyDtos.cs new file mode 100644 index 0000000..485ea5a --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyDtos.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace PSInfisicalAPI.Pki +{ + internal sealed class InfisicalCertificatePolicyResponseDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("projectId")] public string ProjectId { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("description")] public string Description { get; set; } + [JsonProperty("subject")] public InfisicalCertificatePolicySubjectDto Subject { get; set; } + [JsonProperty("sans")] public JToken SansRaw { get; set; } + [JsonProperty("keyUsages")] public InfisicalCertificatePolicyUsagesDto KeyUsages { get; set; } + [JsonProperty("extendedKeyUsages")] public InfisicalCertificatePolicyUsagesDto ExtendedKeyUsages { get; set; } + [JsonProperty("algorithms")] public InfisicalCertificatePolicyAlgorithmsDto Algorithms { get; set; } + [JsonProperty("validity")] public InfisicalCertificatePolicyValidityDto Validity { get; set; } + [JsonProperty("createdAt")] public string CreatedAt { get; set; } + [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + } + + internal sealed class InfisicalCertificatePolicySubjectDto + { + [JsonProperty("type")] public string Type { get; set; } + [JsonProperty("allowed")] public JToken AllowedRaw { get; set; } + } + + internal sealed class InfisicalCertificatePolicySanDto + { + [JsonProperty("type")] public string Type { get; set; } + [JsonProperty("allowed")] public JToken AllowedRaw { get; set; } + [JsonProperty("required")] public JToken RequiredRaw { get; set; } + } + + internal sealed class InfisicalCertificatePolicyUsagesDto + { + [JsonProperty("allowed")] public JToken AllowedRaw { get; set; } + [JsonProperty("required")] public JToken RequiredRaw { get; set; } + } + + internal sealed class InfisicalCertificatePolicyAlgorithmsDto + { + [JsonProperty("signature")] public string Signature { get; set; } + [JsonProperty("keyAlgorithm")] public JToken KeyAlgorithmRaw { get; set; } + } + + internal sealed class InfisicalCertificatePolicyValidityDto + { + [JsonProperty("max")] public string Max { get; set; } + } + + internal sealed class InfisicalCertificatePolicyListResponseDto + { + [JsonProperty("certificatePolicies")] public List CertificatePolicies { get; set; } + [JsonProperty("totalCount")] public int? TotalCount { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyMapper.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyMapper.cs new file mode 100644 index 0000000..69d3f74 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyMapper.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json.Linq; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Pki +{ + internal static class InfisicalCertificatePolicyMapper + { + public static InfisicalCertificatePolicy Map(InfisicalCertificatePolicyResponseDto dto, string fallbackProjectId) + { + if (dto == null) + { + return null; + } + + return new InfisicalCertificatePolicy + { + Id = dto.Id, + ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId, + Name = dto.Name, + Description = dto.Description, + Subject = MapSubject(dto.Subject), + Sans = MapSans(dto.SansRaw), + KeyUsages = MapUsages(dto.KeyUsages), + ExtendedKeyUsages = MapUsages(dto.ExtendedKeyUsages), + Algorithms = MapAlgorithms(dto.Algorithms), + Validity = MapValidity(dto.Validity), + CreatedAtUtc = ParseTimestamp(dto.CreatedAt), + UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt) + }; + } + + public static InfisicalCertificatePolicy[] MapMany(IEnumerable items, string fallbackProjectId) + { + if (items == null) + { + return Array.Empty(); + } + + List results = new List(); + foreach (InfisicalCertificatePolicyResponseDto dto in items) + { + InfisicalCertificatePolicy mapped = Map(dto, fallbackProjectId); + if (mapped != null) + { + results.Add(mapped); + } + } + + return results.ToArray(); + } + + private static InfisicalCertificatePolicySubject MapSubject(InfisicalCertificatePolicySubjectDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificatePolicySubject + { + Type = dto.Type, + Allowed = InfisicalCertificateProfileMapper.FlattenStringOrStringArray(dto.AllowedRaw) + }; + } + + private static InfisicalCertificatePolicySan[] MapSans(JToken token) + { + if (token == null || token.Type == JTokenType.Null) { return null; } + + List results = new List(); + if (token.Type == JTokenType.Array) + { + foreach (JToken child in (JArray)token) + { + InfisicalCertificatePolicySan mapped = MapSanObject(child); + if (mapped != null) { results.Add(mapped); } + } + } + else if (token.Type == JTokenType.Object) + { + InfisicalCertificatePolicySan mapped = MapSanObject(token); + if (mapped != null) { results.Add(mapped); } + } + + return results.Count > 0 ? results.ToArray() : null; + } + + private static InfisicalCertificatePolicySan MapSanObject(JToken token) + { + if (token == null || token.Type != JTokenType.Object) { return null; } + InfisicalCertificatePolicySanDto dto = token.ToObject(); + if (dto == null) { return null; } + return new InfisicalCertificatePolicySan + { + Type = dto.Type, + Allowed = InfisicalCertificateProfileMapper.FlattenStringOrStringArray(dto.AllowedRaw), + Required = InfisicalCertificateProfileMapper.FlattenStringOrStringArray(dto.RequiredRaw) + }; + } + + private static InfisicalCertificatePolicyUsages MapUsages(InfisicalCertificatePolicyUsagesDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificatePolicyUsages + { + Allowed = InfisicalCertificateProfileMapper.FlattenStringOrStringArray(dto.AllowedRaw), + Required = InfisicalCertificateProfileMapper.FlattenStringOrStringArray(dto.RequiredRaw) + }; + } + + private static InfisicalCertificatePolicyAlgorithms MapAlgorithms(InfisicalCertificatePolicyAlgorithmsDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificatePolicyAlgorithms + { + Signature = dto.Signature, + KeyAlgorithms = InfisicalCertificateProfileMapper.FlattenStringOrStringArray(dto.KeyAlgorithmRaw) + }; + } + + private static InfisicalCertificatePolicyValidity MapValidity(InfisicalCertificatePolicyValidityDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificatePolicyValidity { Max = dto.Max }; + } + + private static DateTimeOffset? ParseTimestamp(string value) + { + if (string.IsNullOrEmpty(value)) { return null; } + DateTimeOffset parsed; + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsed)) + { + return parsed; + } + + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs index da03488..50efb1e 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs @@ -427,6 +427,94 @@ namespace PSInfisicalAPI.Pki return obj.ToObject(); } + public InfisicalCertificatePolicy[] ListCertificatePolicies(InfisicalConnection connection, string projectId, int? limit, int? offset) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + List> query = new List> + { + new KeyValuePair("projectId", resolvedProjectId) + }; + if (limit.HasValue) { query.Add(new KeyValuePair("limit", limit.Value.ToString(CultureInfo.InvariantCulture))); } + if (offset.HasValue) { query.Add(new KeyValuePair("offset", offset.Value.ToString(CultureInfo.InvariantCulture))); } + + try + { + _logger.Information(Component, "Attempting to list Infisical certificate policies. Please Wait..."); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.ListCertificatePolicies, "ListCertificatePolicies", null, query, null); + string body = response.Body; + response.Clear(); + + List source = ParseCertificatePolicyListBody(body); + InfisicalCertificatePolicy[] mapped = InfisicalCertificatePolicyMapper.MapMany(source, resolvedProjectId); + _logger.Information(Component, "Infisical certificate policy list retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate policy list retrieval failed."); + throw; + } + } + + public InfisicalCertificatePolicy GetCertificatePolicy(InfisicalConnection connection, string certificatePolicyId, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(certificatePolicyId)) { throw new InfisicalConfigurationException("CertificatePolicyId is required."); } + + Dictionary pathParameters = new Dictionary { { "certificatePolicyId", certificatePolicyId } }; + List> query = null; + if (!string.IsNullOrEmpty(projectId)) + { + query = new List> { new KeyValuePair("projectId", projectId) }; + } + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve Infisical certificate policy '", certificatePolicyId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.GetCertificatePolicy, "GetCertificatePolicy", pathParameters, query, null); + string body = response.Body; + response.Clear(); + + InfisicalCertificatePolicyResponseDto inner = ParseCertificatePolicySingleBody(body); + string fallbackProjectId = !string.IsNullOrEmpty(projectId) ? projectId : connection.ProjectId; + InfisicalCertificatePolicy mapped = InfisicalCertificatePolicyMapper.Map(inner, fallbackProjectId); + _logger.Information(Component, "Infisical certificate policy retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate policy retrieval failed."); + throw; + } + } + + private List ParseCertificatePolicyListBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Array) + { + return token.ToObject>(); + } + + InfisicalCertificatePolicyListResponseDto wrapper = token.ToObject(); + return wrapper != null ? wrapper.CertificatePolicies : null; + } + + private InfisicalCertificatePolicyResponseDto ParseCertificatePolicySingleBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type != JTokenType.Object) { return null; } + JObject obj = (JObject)token; + + if (obj["certificatePolicy"] is JObject inner) { return inner.ToObject(); } + return obj.ToObject(); + } + public InfisicalCertificateBundle GetCertificateBundle(InfisicalConnection connection, string serialNumber) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); }