From a195901a10f10a058d3317675f3b5d5325e6657a Mon Sep 17 00:00:00 2001 From: GraceSolutions Date: Thu, 4 Jun 2026 19:34:43 -0400 Subject: [PATCH] feat(pki): add Certificate Application + Enrollment models and client methods Adds InfisicalCertificateApplication and InfisicalCertificateApplicationEnrollment models (with SCEP/EST/ACME/API sub-blocks) and DTO/mapper layer. Mapper computes the SHA-1 RA certificate thumbprint from the enrollment PEM so it can be fed directly into MDM payloads. InfisicalPkiClient gains ListCertificateApplications, GetCertificateApplication, GetCertificateApplicationByName, ListCertificateApplicationProfiles, GetCertificateApplicationEnrollment, and GenerateScepDynamicChallenge. InfisicalApiInvoker accepts an optional extraHeaders argument so callers can attach x-infisical-project-id and override Accept (used by the plain-text SCEP challenge endpoint). New endpoint names and registry entries cover /api/v1/cert-manager/applications/** and /scep/applications/**/profiles/**/challenge. --- .../Endpoints/InfisicalEndpointNames.cs | 8 + .../Endpoints/InfisicalEndpointRegistry.cs | 61 +++++ .../Http/InfisicalApiInvoker.cs | 22 +- .../Models/InfisicalCertificateApplication.cs | 31 +++ ...fisicalCertificateApplicationEnrollment.cs | 55 +++++ .../InfisicalCertificateApplicationDtos.cs | 92 +++++++ .../InfisicalCertificateApplicationMapper.cs | 175 ++++++++++++++ src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs | 227 ++++++++++++++++++ 8 files changed, 666 insertions(+), 5 deletions(-) create mode 100644 src/PSInfisicalAPI/Models/InfisicalCertificateApplication.cs create mode 100644 src/PSInfisicalAPI/Models/InfisicalCertificateApplicationEnrollment.cs create mode 100644 src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationDtos.cs create mode 100644 src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationMapper.cs diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs index fa32617..678514f 100644 --- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs +++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs @@ -63,5 +63,13 @@ namespace PSInfisicalAPI.Endpoints public const string GetCertificatePolicy = "GetCertificatePolicy"; public const string ListCertificateAuthorities = "ListCertificateAuthorities"; + + public const string ListCertificateApplications = "ListCertificateApplications"; + public const string GetCertificateApplication = "GetCertificateApplication"; + public const string GetCertificateApplicationByName = "GetCertificateApplicationByName"; + public const string ListCertificateApplicationProfiles = "ListCertificateApplicationProfiles"; + public const string GetCertificateApplicationEnrollment = "GetCertificateApplicationEnrollment"; + + public const string GenerateScepDynamicChallenge = "GenerateScepDynamicChallenge"; } } diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs index 242a202..e3f5ac4 100644 --- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs +++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs @@ -703,6 +703,67 @@ namespace PSInfisicalAPI.Endpoints Template = "/api/v1/cert-manager/ca", RequiresAuthorization = true }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListCertificateApplications, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/applications", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GetCertificateApplication, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/applications/{applicationId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GetCertificateApplicationByName, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/applications/by-name/{name}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListCertificateApplicationProfiles, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/applications/{applicationId}/profiles", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GetCertificateApplicationEnrollment, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/applications/{applicationId}/profiles/{profileId}/enrollment", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GenerateScepDynamicChallenge, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/scep/applications/{applicationId}/profiles/{profileId}/challenge", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); } public static InfisicalEndpointDefinition Get(string name) diff --git a/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs b/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs index a3e2b1a..7e88cf7 100644 --- a/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs +++ b/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs @@ -23,7 +23,8 @@ namespace PSInfisicalAPI.Http string operationName, IDictionary pathParameters, IEnumerable> queryParameters, - string body) + string body, + IDictionary extraHeaders = null) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } if (string.IsNullOrEmpty(endpointName)) { throw new ArgumentNullException(nameof(endpointName)); } @@ -31,7 +32,7 @@ namespace PSInfisicalAPI.Http InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(endpointName); Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, pathParameters, queryParameters); - InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, operationName, uri, body); + InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, operationName, uri, body, extraHeaders); if (response.StatusCode >= 200 && response.StatusCode < 300) { @@ -49,7 +50,8 @@ namespace PSInfisicalAPI.Http string operationName, IDictionary pathParameters, IEnumerable> queryParameters, - string body) + string body, + IDictionary extraHeaders = null) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } if (string.IsNullOrEmpty(endpointName)) { throw new ArgumentNullException(nameof(endpointName)); } @@ -61,7 +63,7 @@ namespace PSInfisicalAPI.Http { InfisicalEndpointDefinition definition = candidates[index]; Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, pathParameters, queryParameters); - InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, operationName, uri, body); + InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, operationName, uri, body, extraHeaders); if (response.StatusCode >= 200 && response.StatusCode < 300) { @@ -95,7 +97,8 @@ namespace PSInfisicalAPI.Http InfisicalEndpointDefinition definition, string operationName, Uri uri, - string body) + string body, + IDictionary extraHeaders = null) { Dictionary headers = new Dictionary(StringComparer.OrdinalIgnoreCase); headers["Accept"] = "application/json"; @@ -118,6 +121,15 @@ namespace PSInfisicalAPI.Http }); } + if (extraHeaders != null) + { + foreach (KeyValuePair entry in extraHeaders) + { + if (string.IsNullOrEmpty(entry.Key)) { continue; } + headers[entry.Key] = entry.Value; + } + } + InfisicalHttpRequest request = new InfisicalHttpRequest { OperationName = operationName, diff --git a/src/PSInfisicalAPI/Models/InfisicalCertificateApplication.cs b/src/PSInfisicalAPI/Models/InfisicalCertificateApplication.cs new file mode 100644 index 0000000..170e366 --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalCertificateApplication.cs @@ -0,0 +1,31 @@ +using System; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalCertificateApplication + { + public string Id { get; set; } + public string ProjectId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public int? ProfileCount { get; set; } + public int? MemberCount { get; set; } + public int? CertificateCount { get; set; } + public DateTimeOffset? CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + } + + public sealed class InfisicalCertificateApplicationProfileAttachment + { + public string ApplicationId { get; set; } + public string ProfileId { get; set; } + public string ProfileSlug { get; set; } + public string ProfileDescription { get; set; } + public string ApiConfigId { get; set; } + public string EstConfigId { get; set; } + public string AcmeConfigId { get; set; } + public string ScepConfigId { get; set; } + public DateTimeOffset? CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalCertificateApplicationEnrollment.cs b/src/PSInfisicalAPI/Models/InfisicalCertificateApplicationEnrollment.cs new file mode 100644 index 0000000..1a5a9ef --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalCertificateApplicationEnrollment.cs @@ -0,0 +1,55 @@ +using System; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalCertificateApplicationEnrollment + { + public string ApplicationId { get; set; } + public string ProfileId { get; set; } + public InfisicalCertificateApplicationApiEnrollment Api { get; set; } + public InfisicalCertificateApplicationEstEnrollment Est { get; set; } + public InfisicalCertificateApplicationAcmeEnrollment Acme { get; set; } + public InfisicalCertificateApplicationScepEnrollment Scep { get; set; } + public bool ApiConfigured { get { return Api != null; } } + public bool EstConfigured { get; set; } + public bool AcmeConfigured { get; set; } + public bool ScepConfigured { get; set; } + } + + public sealed class InfisicalCertificateApplicationApiEnrollment + { + public string Id { get; set; } + public bool? AutoRenew { get; set; } + public int? RenewBeforeDays { get; set; } + } + + public sealed class InfisicalCertificateApplicationEstEnrollment + { + public string Id { get; set; } + public bool? DisableBootstrapCaValidation { get; set; } + public string EstEndpointUrl { get; set; } + } + + public sealed class InfisicalCertificateApplicationAcmeEnrollment + { + public string Id { get; set; } + public bool? SkipDnsOwnershipVerification { get; set; } + public bool? SkipEabBinding { get; set; } + public string DirectoryUrl { get; set; } + } + + public sealed class InfisicalCertificateApplicationScepEnrollment + { + public string Id { get; set; } + public string ChallengeType { get; set; } + public bool? IncludeCaCertInResponse { get; set; } + public bool? AllowCertBasedRenewal { get; set; } + public int? DynamicChallengeExpiryMinutes { get; set; } + public int? DynamicChallengeMaxPending { get; set; } + public string ScepEndpointUrl { get; set; } + public string ChallengeEndpointUrl { get; set; } + public string RaCertificatePem { get; set; } + public string RaCertificateThumbprint { get; set; } + public DateTimeOffset? RaCertExpiresAtUtc { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationDtos.cs new file mode 100644 index 0000000..356c616 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationDtos.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PSInfisicalAPI.Pki +{ + internal sealed class InfisicalCertificateApplicationResponseDto + { + [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("profileCount")] public int? ProfileCount { get; set; } + [JsonProperty("memberCount")] public int? MemberCount { get; set; } + [JsonProperty("certificateCount")] public int? CertificateCount { get; set; } + [JsonProperty("createdAt")] public string CreatedAt { get; set; } + [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + } + + internal sealed class InfisicalCertificateApplicationListResponseDto + { + [JsonProperty("applications")] public List Applications { get; set; } + [JsonProperty("total")] public int? Total { get; set; } + } + + internal sealed class InfisicalCertificateApplicationProfileAttachmentDto + { + [JsonProperty("applicationId")] public string ApplicationId { get; set; } + [JsonProperty("profileId")] public string ProfileId { get; set; } + [JsonProperty("profileSlug")] public string ProfileSlug { get; set; } + [JsonProperty("profileDescription")] public string ProfileDescription { get; set; } + [JsonProperty("apiConfigId")] public string ApiConfigId { get; set; } + [JsonProperty("estConfigId")] public string EstConfigId { get; set; } + [JsonProperty("acmeConfigId")] public string AcmeConfigId { get; set; } + [JsonProperty("scepConfigId")] public string ScepConfigId { get; set; } + [JsonProperty("createdAt")] public string CreatedAt { get; set; } + [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + } + + internal sealed class InfisicalCertificateApplicationProfilesResponseDto + { + [JsonProperty("profiles")] public List Profiles { get; set; } + } + + internal sealed class InfisicalCertificateApplicationEnrollmentResponseDto + { + [JsonProperty("applicationId")] public string ApplicationId { get; set; } + [JsonProperty("profileId")] public string ProfileId { get; set; } + [JsonProperty("api")] public InfisicalCertificateApplicationApiEnrollmentDto Api { get; set; } + [JsonProperty("est")] public InfisicalCertificateApplicationEstEnrollmentDto Est { get; set; } + [JsonProperty("acme")] public InfisicalCertificateApplicationAcmeEnrollmentDto Acme { get; set; } + [JsonProperty("scep")] public InfisicalCertificateApplicationScepEnrollmentDto Scep { get; set; } + [JsonProperty("estConfigured")] public bool? EstConfigured { get; set; } + [JsonProperty("acmeConfigured")] public bool? AcmeConfigured { get; set; } + [JsonProperty("scepConfigured")] public bool? ScepConfigured { get; set; } + } + + internal sealed class InfisicalCertificateApplicationApiEnrollmentDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("autoRenew")] public bool? AutoRenew { get; set; } + [JsonProperty("renewBeforeDays")] public int? RenewBeforeDays { get; set; } + } + + internal sealed class InfisicalCertificateApplicationEstEnrollmentDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("disableBootstrapCaValidation")] public bool? DisableBootstrapCaValidation { get; set; } + [JsonProperty("estEndpointUrl")] public string EstEndpointUrl { get; set; } + } + + internal sealed class InfisicalCertificateApplicationAcmeEnrollmentDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("skipDnsOwnershipVerification")] public bool? SkipDnsOwnershipVerification { get; set; } + [JsonProperty("skipEabBinding")] public bool? SkipEabBinding { get; set; } + [JsonProperty("directoryUrl")] public string DirectoryUrl { get; set; } + } + + internal sealed class InfisicalCertificateApplicationScepEnrollmentDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("challengeType")] public string ChallengeType { get; set; } + [JsonProperty("includeCaCertInResponse")] public bool? IncludeCaCertInResponse { get; set; } + [JsonProperty("allowCertBasedRenewal")] public bool? AllowCertBasedRenewal { get; set; } + [JsonProperty("dynamicChallengeExpiryMinutes")] public int? DynamicChallengeExpiryMinutes { get; set; } + [JsonProperty("dynamicChallengeMaxPending")] public int? DynamicChallengeMaxPending { get; set; } + [JsonProperty("scepEndpointUrl")] public string ScepEndpointUrl { get; set; } + [JsonProperty("challengeEndpointUrl")] public string ChallengeEndpointUrl { get; set; } + [JsonProperty("raCertificatePem")] public string RaCertificatePem { get; set; } + [JsonProperty("raCertExpiresAt")] public string RaCertExpiresAt { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationMapper.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationMapper.cs new file mode 100644 index 0000000..7798243 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationMapper.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Pki +{ + internal static class InfisicalCertificateApplicationMapper + { + public static InfisicalCertificateApplication Map(InfisicalCertificateApplicationResponseDto dto, string fallbackProjectId) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplication + { + Id = dto.Id, + ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId, + Name = dto.Name, + Description = dto.Description, + ProfileCount = dto.ProfileCount, + MemberCount = dto.MemberCount, + CertificateCount = dto.CertificateCount, + CreatedAtUtc = ParseTimestamp(dto.CreatedAt), + UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt) + }; + } + + public static InfisicalCertificateApplication[] MapMany(IEnumerable items, string fallbackProjectId) + { + if (items == null) { return Array.Empty(); } + List results = new List(); + foreach (InfisicalCertificateApplicationResponseDto dto in items) + { + InfisicalCertificateApplication mapped = Map(dto, fallbackProjectId); + if (mapped != null) { results.Add(mapped); } + } + + return results.ToArray(); + } + + public static InfisicalCertificateApplicationProfileAttachment MapAttachment(InfisicalCertificateApplicationProfileAttachmentDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplicationProfileAttachment + { + ApplicationId = dto.ApplicationId, + ProfileId = dto.ProfileId, + ProfileSlug = dto.ProfileSlug, + ProfileDescription = dto.ProfileDescription, + ApiConfigId = dto.ApiConfigId, + EstConfigId = dto.EstConfigId, + AcmeConfigId = dto.AcmeConfigId, + ScepConfigId = dto.ScepConfigId, + CreatedAtUtc = ParseTimestamp(dto.CreatedAt), + UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt) + }; + } + + public static InfisicalCertificateApplicationProfileAttachment[] MapAttachments(IEnumerable items) + { + if (items == null) { return Array.Empty(); } + List results = new List(); + foreach (InfisicalCertificateApplicationProfileAttachmentDto dto in items) + { + InfisicalCertificateApplicationProfileAttachment mapped = MapAttachment(dto); + if (mapped != null) { results.Add(mapped); } + } + + return results.ToArray(); + } + + public static InfisicalCertificateApplicationEnrollment MapEnrollment(InfisicalCertificateApplicationEnrollmentResponseDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplicationEnrollment + { + ApplicationId = dto.ApplicationId, + ProfileId = dto.ProfileId, + Api = MapApi(dto.Api), + Est = MapEst(dto.Est), + Acme = MapAcme(dto.Acme), + Scep = MapScep(dto.Scep), + EstConfigured = dto.EstConfigured.GetValueOrDefault(), + AcmeConfigured = dto.AcmeConfigured.GetValueOrDefault(), + ScepConfigured = dto.ScepConfigured.GetValueOrDefault() + }; + } + + private static InfisicalCertificateApplicationApiEnrollment MapApi(InfisicalCertificateApplicationApiEnrollmentDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplicationApiEnrollment { Id = dto.Id, AutoRenew = dto.AutoRenew, RenewBeforeDays = dto.RenewBeforeDays }; + } + + private static InfisicalCertificateApplicationEstEnrollment MapEst(InfisicalCertificateApplicationEstEnrollmentDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplicationEstEnrollment { Id = dto.Id, DisableBootstrapCaValidation = dto.DisableBootstrapCaValidation, EstEndpointUrl = dto.EstEndpointUrl }; + } + + private static InfisicalCertificateApplicationAcmeEnrollment MapAcme(InfisicalCertificateApplicationAcmeEnrollmentDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplicationAcmeEnrollment { Id = dto.Id, SkipDnsOwnershipVerification = dto.SkipDnsOwnershipVerification, SkipEabBinding = dto.SkipEabBinding, DirectoryUrl = dto.DirectoryUrl }; + } + + private static InfisicalCertificateApplicationScepEnrollment MapScep(InfisicalCertificateApplicationScepEnrollmentDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplicationScepEnrollment + { + Id = dto.Id, + ChallengeType = dto.ChallengeType, + IncludeCaCertInResponse = dto.IncludeCaCertInResponse, + AllowCertBasedRenewal = dto.AllowCertBasedRenewal, + DynamicChallengeExpiryMinutes = dto.DynamicChallengeExpiryMinutes, + DynamicChallengeMaxPending = dto.DynamicChallengeMaxPending, + ScepEndpointUrl = dto.ScepEndpointUrl, + ChallengeEndpointUrl = dto.ChallengeEndpointUrl, + RaCertificatePem = dto.RaCertificatePem, + RaCertificateThumbprint = ComputeThumbprint(dto.RaCertificatePem), + RaCertExpiresAtUtc = ParseTimestamp(dto.RaCertExpiresAt) + }; + } + + internal static string ComputeThumbprint(string pem) + { + if (string.IsNullOrEmpty(pem)) { return null; } + try + { + byte[] der = Convert.FromBase64String(StripPemArmor(pem)); + using (X509Certificate2 cert = new X509Certificate2(der)) + { + return cert.Thumbprint; + } + } + catch + { + return null; + } + } + + private static string StripPemArmor(string pem) + { + StringBuilder sb = new StringBuilder(pem.Length); + using (StringReader reader = new StringReader(pem)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + string trimmed = line.Trim(); + if (trimmed.Length == 0) { continue; } + if (trimmed.StartsWith("-----", StringComparison.Ordinal)) { continue; } + sb.Append(trimmed); + } + } + + return sb.ToString(); + } + + 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 e3e06b6..cda0afc 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs @@ -640,6 +640,233 @@ namespace PSInfisicalAPI.Pki } } + public InfisicalCertificateApplication[] ListCertificateApplications(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>(); + 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))); } + + Dictionary headers = BuildProjectHeader(resolvedProjectId); + + try + { + _logger.Information(Component, "Attempting to list Infisical certificate applications. Please Wait..."); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListCertificateApplications, "ListCertificateApplications", null, query, null, headers); + string body = response.Body; + response.Clear(); + + List source = ParseApplicationListBody(body); + InfisicalCertificateApplication[] mapped = InfisicalCertificateApplicationMapper.MapMany(source, resolvedProjectId); + _logger.Information(Component, "Infisical certificate application list retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate application list retrieval failed."); + throw; + } + } + + public InfisicalCertificateApplication GetCertificateApplication(InfisicalConnection connection, string applicationId, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); } + + Dictionary pathParameters = new Dictionary { { "applicationId", applicationId } }; + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + Dictionary headers = !string.IsNullOrEmpty(resolvedProjectId) ? BuildProjectHeader(resolvedProjectId) : null; + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve Infisical certificate application '", applicationId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.GetCertificateApplication, "GetCertificateApplication", pathParameters, null, null, headers); + string body = response.Body; + response.Clear(); + + InfisicalCertificateApplicationResponseDto inner = ParseApplicationSingleBody(body); + InfisicalCertificateApplication mapped = InfisicalCertificateApplicationMapper.Map(inner, resolvedProjectId); + _logger.Information(Component, "Infisical certificate application retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate application retrieval failed."); + throw; + } + } + + public InfisicalCertificateApplication GetCertificateApplicationByName(InfisicalConnection connection, string name, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("ApplicationName is required."); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + Dictionary pathParameters = new Dictionary { { "name", name } }; + Dictionary headers = BuildProjectHeader(resolvedProjectId); + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve Infisical certificate application '", name, "' by name. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.GetCertificateApplicationByName, "GetCertificateApplicationByName", pathParameters, null, null, headers); + string body = response.Body; + response.Clear(); + + InfisicalCertificateApplicationResponseDto inner = ParseApplicationSingleBody(body); + InfisicalCertificateApplication mapped = InfisicalCertificateApplicationMapper.Map(inner, resolvedProjectId); + _logger.Information(Component, "Infisical certificate application (by name) retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate application (by name) retrieval failed."); + throw; + } + } + + public InfisicalCertificateApplicationProfileAttachment[] ListCertificateApplicationProfiles(InfisicalConnection connection, string applicationId, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); } + + Dictionary pathParameters = new Dictionary { { "applicationId", applicationId } }; + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + Dictionary headers = !string.IsNullOrEmpty(resolvedProjectId) ? BuildProjectHeader(resolvedProjectId) : null; + + try + { + _logger.Information(Component, string.Concat("Attempting to list profile attachments for Infisical certificate application '", applicationId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListCertificateApplicationProfiles, "ListCertificateApplicationProfiles", pathParameters, null, null, headers); + string body = response.Body; + response.Clear(); + + List source = ParseApplicationProfilesBody(body); + InfisicalCertificateApplicationProfileAttachment[] mapped = InfisicalCertificateApplicationMapper.MapAttachments(source); + _logger.Information(Component, "Infisical certificate application profile attachment listing was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate application profile attachment listing failed."); + throw; + } + } + + public InfisicalCertificateApplicationEnrollment GetCertificateApplicationEnrollment(InfisicalConnection connection, string applicationId, string profileId, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); } + if (string.IsNullOrEmpty(profileId)) { throw new InfisicalConfigurationException("ProfileId is required."); } + + Dictionary pathParameters = new Dictionary + { + { "applicationId", applicationId }, + { "profileId", profileId } + }; + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + Dictionary headers = !string.IsNullOrEmpty(resolvedProjectId) ? BuildProjectHeader(resolvedProjectId) : null; + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve enrollment for application '", applicationId, "' / profile '", profileId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.GetCertificateApplicationEnrollment, "GetCertificateApplicationEnrollment", pathParameters, null, null, headers); + InfisicalCertificateApplicationEnrollmentResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalCertificateApplicationEnrollment mapped = InfisicalCertificateApplicationMapper.MapEnrollment(dto); + _logger.Information(Component, "Infisical certificate application enrollment retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate application enrollment retrieval failed."); + throw; + } + } + + public string GenerateScepDynamicChallenge(InfisicalConnection connection, string applicationId, string profileId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); } + if (string.IsNullOrEmpty(profileId)) { throw new InfisicalConfigurationException("ProfileId is required."); } + + Dictionary pathParameters = new Dictionary + { + { "applicationId", applicationId }, + { "profileId", profileId } + }; + Dictionary headers = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Accept", "text/plain" } + }; + + try + { + _logger.Information(Component, string.Concat("Attempting to generate SCEP dynamic challenge for application '", applicationId, "' / profile '", profileId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.GenerateScepDynamicChallenge, "GenerateScepDynamicChallenge", pathParameters, null, string.Empty, headers); + string body = response.Body != null ? response.Body.Trim() : null; + response.Clear(); + + if (string.IsNullOrEmpty(body)) { throw new InfisicalApiException("SCEP dynamic challenge response was empty."); } + _logger.Information(Component, "Infisical SCEP dynamic challenge generation was successful."); + return body; + } + catch (Exception) + { + _logger.Error(Component, "Infisical SCEP dynamic challenge generation failed."); + throw; + } + } + + private static Dictionary BuildProjectHeader(string projectId) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "x-infisical-project-id", projectId } + }; + } + + private List ParseApplicationListBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Array) + { + return token.ToObject>(); + } + + InfisicalCertificateApplicationListResponseDto wrapper = token.ToObject(); + return wrapper != null ? wrapper.Applications : null; + } + + private InfisicalCertificateApplicationResponseDto ParseApplicationSingleBody(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["application"] is JObject inner) { return inner.ToObject(); } + return obj.ToObject(); + } + + private List ParseApplicationProfilesBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Array) + { + return token.ToObject>(); + } + + InfisicalCertificateApplicationProfilesResponseDto wrapper = token.ToObject(); + return wrapper != null ? wrapper.Profiles : null; + } + internal static InfisicalCertificateSearchRequestDto BuildSearchRequest(InfisicalCertificateSearchQuery query) { return new InfisicalCertificateSearchRequestDto