Add create, update, and delete for Certificate Manager configuration

The module was read-only for PKI configuration, which is why standing up an
environment meant raw REST rather than cmdlets. Adds 15: New-, Set-, and Remove-
for certificate authorities, policies, profiles, applications, and PKI
subscribers.

New-InfisicalCertificateAuthority -Type Intermediate returns an authority that
can actually issue. Infisical creates a subordinate with status
pending-certificate and never invokes generateIntermediateCaCertificate from the
create path, so the cmdlet performs the remaining sequence: read the certificate
signing request, sign it with -ParentCaId, import the signed certificate and
chain back.

Policy and profile bodies are deeply nested, so the constraint objects and
per-enrollment-type config blocks are taken as dictionaries rather than as
dozens of parameters, matching how -Subject and -Metadata already work. Two
conversion details matter and are now pinned by tests:

  - PowerShell callers capitalise hashtable keys, and @{ Required = ... } was
    reaching the API as "Required", which its schema does not recognise. The
    constraint vocabulary is emitted lower-cased while every other key keeps its
    camelCase, since those are API field names supplied verbatim and lowercasing
    ttlDays or isCA would silently drop them.
  - An empty collection is omitted rather than sent. "allowed": [] reads to
    Infisical as "allow nothing", never what @{ Allowed = @() } was meant to
    express, and an entry carrying only "type" is rejected outright.

-EnrollmentConfig routes to the block matching -EnrollmentType so EST, ACME, and
SCEP settings arrive through one parameter. -AutoRenew is sent only when bound,
because a switch is false when absent and sending it unconditionally would
disable renewal on an update that never mentioned it.

-ProjectId is optional on all 15 and resolves as it does elsewhere. Every one
supports -WhatIf; Remove- cmdlets default to high confirm impact.

Also fixes Update-Changelog, which inserted the version heading above the notes
so the section the release workflow extracts held only the build line while the
notes stayed under Unreleased - every release published an empty changelog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 17:41:13 -04:00
parent 62131e7501
commit 139d1f3a05
18 changed files with 3007 additions and 7 deletions
@@ -0,0 +1,280 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Management.Automation;
using System.Reflection;
using Newtonsoft.Json;
using Xunit;
namespace PSInfisicalAPI.Tests
{
/// <summary>
/// The create/update/delete cmdlets for Certificate Manager configuration. Their bodies are built from
/// caller-supplied dictionaries, so the conversion into the exact JSON Infisical's schemas accept is what
/// these pin.
/// </summary>
public class PkiWriteCmdletTests
{
private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly;
private static readonly string[] WriteCmdletTypes = new[]
{
"NewInfisicalCertificateAuthorityCmdlet", "SetInfisicalCertificateAuthorityCmdlet", "RemoveInfisicalCertificateAuthorityCmdlet",
"NewInfisicalCertificatePolicyCmdlet", "SetInfisicalCertificatePolicyCmdlet", "RemoveInfisicalCertificatePolicyCmdlet",
"NewInfisicalCertificateProfileCmdlet", "SetInfisicalCertificateProfileCmdlet", "RemoveInfisicalCertificateProfileCmdlet",
"NewInfisicalCertificateApplicationCmdlet", "SetInfisicalCertificateApplicationCmdlet", "RemoveInfisicalCertificateApplicationCmdlet",
"NewInfisicalPkiSubscriberCmdlet", "SetInfisicalPkiSubscriberCmdlet", "RemoveInfisicalPkiSubscriberCmdlet"
};
private static Dictionary<string, object> ToJsonObject(IDictionary source)
{
Type baseType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.InfisicalPkiWriteCmdletBase", true);
MethodInfo method = baseType.GetMethod("ToJsonObject", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
return (Dictionary<string, object>)method.Invoke(null, new object[] { source });
}
private static List<Dictionary<string, object>> ToJsonObjectList(IEnumerable source)
{
Type baseType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.InfisicalPkiWriteCmdletBase", true);
MethodInfo method = baseType.GetMethod("ToJsonObjectList", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(method);
return (List<Dictionary<string, object>>)method.Invoke(null, new object[] { source });
}
[Fact]
public void Every_Write_Cmdlet_Declares_ShouldProcess()
{
List<string> offenders = new List<string>();
foreach (string typeName in WriteCmdletTypes)
{
Type type = ModuleAssembly.GetType(string.Concat("PSInfisicalAPI.Cmdlets.", typeName), true);
bool supportsShouldProcess = false;
foreach (CustomAttributeData attribute in type.GetCustomAttributesData())
{
if (attribute.AttributeType != typeof(CmdletAttribute)) { continue; }
foreach (CustomAttributeNamedArgument named in attribute.NamedArguments)
{
if (named.MemberName == "SupportsShouldProcess" && (bool)named.TypedValue.Value) { supportsShouldProcess = true; }
}
}
if (!supportsShouldProcess) { offenders.Add(typeName); }
}
Assert.Empty(offenders);
}
[Fact]
public void Every_Remove_Cmdlet_Defaults_To_High_Confirm_Impact()
{
List<string> offenders = new List<string>();
foreach (string typeName in WriteCmdletTypes)
{
if (!typeName.StartsWith("Remove", StringComparison.Ordinal)) { continue; }
Type type = ModuleAssembly.GetType(string.Concat("PSInfisicalAPI.Cmdlets.", typeName), true);
bool high = false;
foreach (CustomAttributeData attribute in type.GetCustomAttributesData())
{
if (attribute.AttributeType != typeof(CmdletAttribute)) { continue; }
foreach (CustomAttributeNamedArgument named in attribute.NamedArguments)
{
if (named.MemberName == "ConfirmImpact" && (ConfirmImpact)named.TypedValue.Value == ConfirmImpact.High) { high = true; }
}
}
if (!high) { offenders.Add(typeName); }
}
Assert.Empty(offenders);
}
[Fact]
public void Every_Write_Cmdlet_Resolves_The_Project_Instead_Of_Requiring_It()
{
List<string> offenders = new List<string>();
foreach (string typeName in WriteCmdletTypes)
{
Type type = ModuleAssembly.GetType(string.Concat("PSInfisicalAPI.Cmdlets.", typeName), true);
PropertyInfo projectId = type.GetProperty("ProjectId");
Assert.True(projectId != null, string.Concat(typeName, " has no ProjectId property"));
foreach (CustomAttributeData attribute in projectId.GetCustomAttributesData())
{
if (attribute.AttributeType != typeof(ParameterAttribute)) { continue; }
foreach (CustomAttributeNamedArgument named in attribute.NamedArguments)
{
if (named.MemberName == "Mandatory" && (bool)named.TypedValue.Value) { offenders.Add(typeName); }
}
}
}
Assert.Empty(offenders);
}
[Theory]
// PowerShell callers capitalise hashtable keys; Infisical's schema is lower case.
[InlineData("Required", "required")]
[InlineData("required", "required")]
[InlineData("Allowed", "allowed")]
[InlineData("DENIED", "denied")]
public void Constraint_Keys_Are_Emitted_In_The_Casing_The_Api_Requires(string supplied, string expected)
{
Hashtable source = new Hashtable { { supplied, new[] { "server_auth" } } };
Dictionary<string, object> result = ToJsonObject(source);
Assert.True(result.ContainsKey(expected), string.Concat("expected key '", expected, "' but got: ", JsonConvert.SerializeObject(result)));
}
[Fact]
public void Non_Constraint_Keys_Keep_Their_Camel_Case()
{
// Field names elsewhere in the body are camelCase and supplied verbatim; lowercasing them would
// silently drop settings the API would no longer recognise.
Hashtable source = new Hashtable { { "ttlDays", 90 }, { "keyAlgorithm", "RSA_2048" }, { "isCA", "denied" }, { "maxPathLength", 0 } };
Dictionary<string, object> result = ToJsonObject(source);
Assert.True(result.ContainsKey("ttlDays"));
Assert.True(result.ContainsKey("keyAlgorithm"));
Assert.True(result.ContainsKey("isCA"));
Assert.True(result.ContainsKey("maxPathLength"));
}
[Fact]
public void An_Empty_Collection_Is_Dropped_Rather_Than_Sent_As_Allow_Nothing()
{
Hashtable source = new Hashtable { { "Allowed", new string[0] }, { "Required", new[] { "server_auth" } } };
Dictionary<string, object> result = ToJsonObject(source);
Assert.False(result.ContainsKey("allowed"));
Assert.True(result.ContainsKey("required"));
}
[Fact]
public void A_Wholly_Empty_Constraint_Becomes_Null_So_It_Is_Omitted()
{
Assert.Null(ToJsonObject(new Hashtable()));
Assert.Null(ToJsonObject(new Hashtable { { "Allowed", new string[0] } }));
Assert.Null(ToJsonObject(null));
}
[Fact]
public void A_Constraint_List_Drops_Entries_That_Constrain_Nothing()
{
// An entry carrying only "type" is rejected by Infisical's refinement, and is what a caller writes
// when they meant to leave that dimension alone.
List<Hashtable> source = new List<Hashtable>
{
new Hashtable { { "Type", "dns_name" }, { "Allowed", new[] { "*.contoso.com" } } },
new Hashtable { { "Type", "ip_address" }, { "Allowed", new string[0] } }
};
List<Dictionary<string, object>> result = ToJsonObjectList(source);
Dictionary<string, object> only = Assert.Single(result);
Assert.Equal("dns_name", only["type"]);
Assert.True(only.ContainsKey("allowed"));
}
[Fact]
public void Nested_Dictionaries_Survive_Conversion()
{
Hashtable source = new Hashtable
{
{ "outer", new Hashtable { { "inner", new Hashtable { { "Required", new[] { "a" } } } } } }
};
Dictionary<string, object> result = ToJsonObject(source);
string json = JsonConvert.SerializeObject(result);
Assert.Contains("\"outer\"", json);
Assert.Contains("\"inner\"", json);
Assert.Contains("\"required\"", json);
}
[Fact]
public void Profile_Enrollment_Config_Is_Routed_To_The_Block_For_Its_Type()
{
Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.NewInfisicalCertificateProfileCmdlet", true);
MethodInfo build = cmdletType.GetMethod("BuildRequest", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(build);
Hashtable config = new Hashtable { { "passphrase", "secret" } };
object estRequest = build.Invoke(null, new object[] { "slug", null, "ca", "policy", "est", "ca", false, null, null, config, false });
Assert.NotNull(estRequest.GetType().GetProperty("EstConfig").GetValue(estRequest));
Assert.Null(estRequest.GetType().GetProperty("ApiConfig").GetValue(estRequest));
object scepRequest = build.Invoke(null, new object[] { "slug", null, "ca", "policy", "scep", "ca", false, null, null, config, false });
Assert.NotNull(scepRequest.GetType().GetProperty("ScepConfig").GetValue(scepRequest));
object apiRequest = build.Invoke(null, new object[] { "slug", null, "ca", "policy", "api", "ca", true, 14, null, null, true });
object apiConfig = apiRequest.GetType().GetProperty("ApiConfig").GetValue(apiRequest);
Assert.NotNull(apiConfig);
Dictionary<string, object> typed = (Dictionary<string, object>)apiConfig;
Assert.Equal(true, typed["autoRenew"]);
Assert.Equal(14, typed["renewBeforeDays"]);
}
[Fact]
public void Auto_Renew_Is_Only_Sent_When_The_Caller_Asked_For_It()
{
// A switch parameter is always false when absent, so sending it unconditionally would silently
// disable renewal on an update that never mentioned it.
Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.NewInfisicalCertificateProfileCmdlet", true);
MethodInfo build = cmdletType.GetMethod("BuildRequest", BindingFlags.NonPublic | BindingFlags.Static);
object unbound = build.Invoke(null, new object[] { "slug", null, "ca", "policy", "api", "ca", false, null, null, null, false });
Assert.Null(unbound.GetType().GetProperty("ApiConfig").GetValue(unbound));
object bound = build.Invoke(null, new object[] { "slug", null, "ca", "policy", "api", "ca", false, null, null, null, true });
Dictionary<string, object> config = (Dictionary<string, object>)bound.GetType().GetProperty("ApiConfig").GetValue(bound);
Assert.NotNull(config);
Assert.Equal(false, config["autoRenew"]);
}
[Fact]
public void Every_Management_Endpoint_Is_Registered()
{
string[] names = new[]
{
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.CreateInternalCertificateAuthority,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.UpdateInternalCertificateAuthority,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.DeleteInternalCertificateAuthority,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.GetCertificateAuthorityCsr,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.SignIntermediateCertificateAuthority,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.ImportCertificateAuthorityCertificate,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.CreateCertificatePolicy,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.UpdateCertificatePolicy,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.DeleteCertificatePolicy,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.CreateCertificateProfile,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.UpdateCertificateProfile,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.DeleteCertificateProfile,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.CreateCertificateApplication,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.UpdateCertificateApplication,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.DeleteCertificateApplication,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.AddCertificateApplicationProfiles,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.RemoveCertificateApplicationProfile,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.CreatePkiSubscriber,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.UpdatePkiSubscriber,
PSInfisicalAPI.Endpoints.InfisicalEndpointNames.DeletePkiSubscriber
};
foreach (string name in names)
{
IReadOnlyList<PSInfisicalAPI.Endpoints.InfisicalEndpointDefinition> candidates =
PSInfisicalAPI.Endpoints.InfisicalEndpointRegistry.GetCandidates(name);
Assert.True(candidates.Count > 0, string.Concat(name, " is not registered"));
Assert.All(candidates, c => Assert.True(c.RequiresAuthorization, string.Concat(name, " should require authorization")));
}
}
}
}
@@ -0,0 +1,159 @@
using System;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Pki;
namespace PSInfisicalAPI.Cmdlets
{
/// <summary>
/// Creates a certificate application: the grouping the Infisical console presents profiles, members, and
/// certificates under.
/// </summary>
[Cmdlet(VerbsCommon.New, "InfisicalCertificateApplication", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalCertificateApplication))]
public sealed class NewInfisicalCertificateApplicationCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "NewInfisicalCertificateApplicationCmdlet";
[Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Description { get; set; }
/// <summary>Certificate profiles to attach on creation.</summary>
[Parameter] public string[] ProfileId { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(Name, "Create certificate application")) { return; }
InfisicalCertificateApplicationWriteRequestDto request = new InfisicalCertificateApplicationWriteRequestDto
{
Name = Name,
Description = Description,
ProfileIds = ToStringList(ProfileId)
};
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
WriteObject(client.WriteCertificateApplication(connection, ProjectId, null, request));
}
catch (Exception exception)
{
WriteErrorForException(Component, "CreateCertificateApplication", exception);
}
}
}
/// <summary>
/// Renames a certificate application or changes its description, and attaches or detaches profiles.
/// </summary>
[Cmdlet(VerbsCommon.Set, "InfisicalCertificateApplication", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalCertificateApplication))]
public sealed class SetInfisicalCertificateApplicationCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "SetInfisicalCertificateApplicationCmdlet";
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("Id")]
public string ApplicationId { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Name { get; set; }
[Parameter] public string Description { get; set; }
/// <summary>Certificate profiles to attach. Profiles already attached are left alone.</summary>
[Parameter] public string[] AddProfileId { get; set; }
/// <summary>Certificate profiles to detach.</summary>
[Parameter] public string[] RemoveProfileId { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(ApplicationId, "Update certificate application")) { return; }
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
InfisicalCertificateApplication updated = null;
// The application record and its profile attachments are separate endpoints, so the name and
// description update is skipped entirely when only profiles were supplied.
if (!string.IsNullOrEmpty(Name) || !string.IsNullOrEmpty(Description))
{
InfisicalCertificateApplicationWriteRequestDto request = new InfisicalCertificateApplicationWriteRequestDto
{
Name = Name,
Description = Description
};
updated = client.WriteCertificateApplication(connection, ProjectId, ApplicationId, request);
}
if (ToStringList(AddProfileId) != null)
{
client.AddCertificateApplicationProfiles(connection, ProjectId, ApplicationId, ToStringList(AddProfileId));
}
foreach (string profileId in ToStringList(RemoveProfileId) ?? new System.Collections.Generic.List<string>())
{
client.RemoveCertificateApplicationProfile(connection, ProjectId, ApplicationId, profileId);
}
if (PassThru.IsPresent && updated != null) { WriteObject(updated); }
}
catch (Exception exception)
{
WriteErrorForException(Component, "UpdateCertificateApplication", exception);
}
}
}
/// <summary>
/// Deletes a certificate application.
/// </summary>
[Cmdlet(VerbsCommon.Remove, "InfisicalCertificateApplication", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public sealed class RemoveInfisicalCertificateApplicationCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "RemoveInfisicalCertificateApplicationCmdlet";
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("Id")]
public string ApplicationId { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(ApplicationId, "Delete certificate application")) { return; }
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
client.DeleteCertificateApplication(connection, ProjectId, ApplicationId);
if (PassThru.IsPresent) { WriteObject(ApplicationId); }
}
catch (Exception exception)
{
WriteErrorForException(Component, "DeleteCertificateApplication", exception);
}
}
}
}
@@ -0,0 +1,183 @@
using System;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Pki;
namespace PSInfisicalAPI.Cmdlets
{
/// <summary>
/// Creates an internal certificate authority, signing a subordinate with its parent so it comes back ready
/// to issue.
/// </summary>
[Cmdlet(VerbsCommon.New, "InfisicalCertificateAuthority", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalCertificateAuthority))]
public sealed class NewInfisicalCertificateAuthorityCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "NewInfisicalCertificateAuthorityCmdlet";
[Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
[Parameter(Mandatory = true)]
[ValidateSet("Root", "Intermediate")]
public string Type { get; set; }
[Parameter(Mandatory = true)] public string CommonName { get; set; }
/// <summary>Required for -Type Intermediate: the authority that signs this one.</summary>
[Parameter] public string ParentCaId { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Organization { get; set; }
[Parameter] public string OrganizationalUnit { get; set; }
[Parameter] public string Country { get; set; }
[Parameter] public string State { get; set; }
[Parameter] public string Locality { get; set; }
[Parameter] public string FriendlyName { get; set; }
[Parameter]
[ValidateSet("RSA_2048", "RSA_3072", "RSA_4096", "EC_prime256v1", "EC_secp384r1", "EC_secp521r1")]
public string KeyAlgorithm { get; set; } = "RSA_2048";
/// <summary>Expiry. Defaults to ten years for a root and five for a subordinate.</summary>
[Parameter] public DateTimeOffset? NotAfter { get; set; }
[Parameter] public DateTimeOffset? NotBefore { get; set; }
/// <summary>Subordinate authorities permitted beneath this one. Defaults to 1 for a root, 0 otherwise.</summary>
[Parameter] public int? MaxPathLength { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
bool isRoot = string.Equals(Type, "Root", StringComparison.OrdinalIgnoreCase);
if (!isRoot && string.IsNullOrEmpty(ParentCaId))
{
throw new PSInfisicalAPI.Errors.InfisicalConfigurationException(
"-ParentCaId is required for an intermediate certificate authority; it names the authority that signs this one.");
}
if (!ShouldProcess(Name, string.Concat("Create ", Type.ToLowerInvariant(), " certificate authority"))) { return; }
DateTimeOffset expiry = NotAfter ?? DateTimeOffset.UtcNow.AddYears(isRoot ? 10 : 5);
int pathLength = MaxPathLength ?? (isRoot ? 1 : 0);
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
InfisicalCertificateAuthority created = client.CreateInternalCertificateAuthority(
connection, ProjectId, Name, isRoot ? "root" : "intermediate", CommonName,
Organization, OrganizationalUnit, Country, State, Locality, KeyAlgorithm, FriendlyName,
ToApiTimestamp(NotBefore), ToApiTimestamp(expiry), pathLength);
if (created == null)
{
throw new PSInfisicalAPI.Errors.InfisicalApiException("Creating the certificate authority returned no record.");
}
if (!isRoot)
{
// Infisical creates a subordinate pending a certificate; without this it exists but cannot
// sign anything.
Logger.Information(Component, string.Concat("Signing '", Name, "' with parent certificate authority '", ParentCaId, "'."));
client.CompleteSubordinateCertificateAuthority(connection, ProjectId, created.Id, ParentCaId, ToApiTimestamp(expiry), pathLength);
InfisicalPkiClient readClient = new InfisicalPkiClient(HttpClient, Logger);
InfisicalCertificateAuthority refreshed = readClient.GetInternalCertificateAuthority(connection, created.Id, ProjectId);
if (refreshed != null) { created = refreshed; }
}
WriteObject(created);
}
catch (Exception exception)
{
WriteErrorForException(Component, "CreateCertificateAuthority", exception);
}
}
}
/// <summary>
/// Renames an internal certificate authority or changes its status.
/// </summary>
[Cmdlet(VerbsCommon.Set, "InfisicalCertificateAuthority", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalCertificateAuthority))]
public sealed class SetInfisicalCertificateAuthorityCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "SetInfisicalCertificateAuthorityCmdlet";
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("Id")]
public string CaId { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Name { get; set; }
[Parameter]
[ValidateSet("active", "disabled")]
public string Status { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(CaId, "Update certificate authority")) { return; }
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
InfisicalCertificateAuthority updated = client.UpdateInternalCertificateAuthority(connection, ProjectId, CaId, Name, Status);
if (PassThru.IsPresent) { WriteObject(updated); }
}
catch (Exception exception)
{
WriteErrorForException(Component, "UpdateCertificateAuthority", exception);
}
}
}
/// <summary>
/// Deletes an internal certificate authority.
/// </summary>
[Cmdlet(VerbsCommon.Remove, "InfisicalCertificateAuthority", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public sealed class RemoveInfisicalCertificateAuthorityCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "RemoveInfisicalCertificateAuthorityCmdlet";
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("Id")]
public string CaId { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(CaId, "Delete certificate authority")) { return; }
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
client.DeleteInternalCertificateAuthority(connection, ProjectId, CaId);
if (PassThru.IsPresent) { WriteObject(CaId); }
}
catch (Exception exception)
{
WriteErrorForException(Component, "DeleteCertificateAuthority", exception);
}
}
}
}
@@ -0,0 +1,193 @@
using System;
using System.Collections;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Pki;
namespace PSInfisicalAPI.Cmdlets
{
/// <summary>
/// Creates a certificate policy: the constraints a profile issues within.
/// </summary>
[Cmdlet(VerbsCommon.New, "InfisicalCertificatePolicy", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalCertificatePolicy))]
public sealed class NewInfisicalCertificatePolicyCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "NewInfisicalCertificatePolicyCmdlet";
[Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Description { get; set; }
/// <summary>Maximum certificate lifetime, for example '90d', '12m', or '1y'.</summary>
[Parameter] public string MaxValidity { get; set; }
/// <summary>Permitted key algorithms, for example RSA_2048 or EC_secp384r1.</summary>
[Parameter] public string[] KeyAlgorithm { get; set; }
/// <summary>Permitted signature algorithms.</summary>
[Parameter] public string[] SignatureAlgorithm { get; set; }
/// <summary>Key usage constraint, as @{ Required = @('digital_signature'); Denied = @(...) }.</summary>
[Parameter] public IDictionary KeyUsage { get; set; }
/// <summary>Extended key usage constraint, as @{ Required = @('server_auth','client_auth') }.</summary>
[Parameter] public IDictionary ExtendedKeyUsage { get; set; }
/// <summary>Subject attribute constraints, as @( @{ Type = 'organization'; Allowed = @('Contoso') } ).</summary>
[Parameter] public IDictionary[] Subject { get; set; }
/// <summary>Subject alternative name constraints, as @( @{ Type = 'dns_name'; Allowed = @('*.contoso.com') } ).</summary>
[Parameter] public IDictionary[] SubjectAlternativeName { get; set; }
/// <summary>Basic constraints, as @{ isCA = 'denied'; maxPathLength = 0 }.</summary>
[Parameter] public IDictionary BasicConstraints { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(Name, "Create certificate policy")) { return; }
InfisicalCertificatePolicyWriteRequestDto request = new InfisicalCertificatePolicyWriteRequestDto
{
Name = Name,
Description = Description,
Subject = ToJsonObjectList(Subject),
Sans = ToJsonObjectList(SubjectAlternativeName),
KeyUsages = ToJsonObject(KeyUsage),
ExtendedKeyUsages = ToJsonObject(ExtendedKeyUsage),
BasicConstraints = ToJsonObject(BasicConstraints)
};
if (!string.IsNullOrEmpty(MaxValidity))
{
request.Validity = new System.Collections.Generic.Dictionary<string, object> { { "max", MaxValidity } };
}
System.Collections.Generic.Dictionary<string, object> algorithms = new System.Collections.Generic.Dictionary<string, object>();
if (ToStringList(KeyAlgorithm) != null) { algorithms["keyAlgorithm"] = ToStringList(KeyAlgorithm); }
if (ToStringList(SignatureAlgorithm) != null) { algorithms["signature"] = ToStringList(SignatureAlgorithm); }
if (algorithms.Count > 0) { request.Algorithms = algorithms; }
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
WriteObject(client.WriteCertificatePolicy(connection, ProjectId, null, request));
}
catch (Exception exception)
{
WriteErrorForException(Component, "CreateCertificatePolicy", exception);
}
}
}
/// <summary>
/// Updates a certificate policy. Only the supplied constraints are sent; the rest are left as they are.
/// </summary>
[Cmdlet(VerbsCommon.Set, "InfisicalCertificatePolicy", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalCertificatePolicy))]
public sealed class SetInfisicalCertificatePolicyCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "SetInfisicalCertificatePolicyCmdlet";
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("Id")]
public string PolicyId { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Name { get; set; }
[Parameter] public string Description { get; set; }
[Parameter] public string MaxValidity { get; set; }
[Parameter] public string[] KeyAlgorithm { get; set; }
[Parameter] public string[] SignatureAlgorithm { get; set; }
[Parameter] public IDictionary KeyUsage { get; set; }
[Parameter] public IDictionary ExtendedKeyUsage { get; set; }
[Parameter] public IDictionary[] Subject { get; set; }
[Parameter] public IDictionary[] SubjectAlternativeName { get; set; }
[Parameter] public IDictionary BasicConstraints { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(PolicyId, "Update certificate policy")) { return; }
InfisicalCertificatePolicyWriteRequestDto request = new InfisicalCertificatePolicyWriteRequestDto
{
Name = Name,
Description = Description,
Subject = ToJsonObjectList(Subject),
Sans = ToJsonObjectList(SubjectAlternativeName),
KeyUsages = ToJsonObject(KeyUsage),
ExtendedKeyUsages = ToJsonObject(ExtendedKeyUsage),
BasicConstraints = ToJsonObject(BasicConstraints)
};
if (!string.IsNullOrEmpty(MaxValidity))
{
request.Validity = new System.Collections.Generic.Dictionary<string, object> { { "max", MaxValidity } };
}
System.Collections.Generic.Dictionary<string, object> algorithms = new System.Collections.Generic.Dictionary<string, object>();
if (ToStringList(KeyAlgorithm) != null) { algorithms["keyAlgorithm"] = ToStringList(KeyAlgorithm); }
if (ToStringList(SignatureAlgorithm) != null) { algorithms["signature"] = ToStringList(SignatureAlgorithm); }
if (algorithms.Count > 0) { request.Algorithms = algorithms; }
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
InfisicalCertificatePolicy updated = client.WriteCertificatePolicy(connection, ProjectId, PolicyId, request);
if (PassThru.IsPresent) { WriteObject(updated); }
}
catch (Exception exception)
{
WriteErrorForException(Component, "UpdateCertificatePolicy", exception);
}
}
}
/// <summary>
/// Deletes a certificate policy.
/// </summary>
[Cmdlet(VerbsCommon.Remove, "InfisicalCertificatePolicy", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public sealed class RemoveInfisicalCertificatePolicyCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "RemoveInfisicalCertificatePolicyCmdlet";
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("Id")]
public string PolicyId { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(PolicyId, "Delete certificate policy")) { return; }
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
client.DeleteCertificatePolicy(connection, ProjectId, PolicyId);
if (PassThru.IsPresent) { WriteObject(PolicyId); }
}
catch (Exception exception)
{
WriteErrorForException(Component, "DeleteCertificatePolicy", exception);
}
}
}
}
@@ -0,0 +1,213 @@
using System;
using System.Collections;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Pki;
namespace PSInfisicalAPI.Cmdlets
{
/// <summary>
/// Creates a certificate profile, binding an issuing certificate authority to a certificate policy and
/// exposing it for enrollment.
/// </summary>
[Cmdlet(VerbsCommon.New, "InfisicalCertificateProfile", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalCertificateProfile))]
public sealed class NewInfisicalCertificateProfileCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "NewInfisicalCertificateProfileCmdlet";
/// <summary>Lowercase letters, numbers, and hyphens only.</summary>
[Parameter(Mandatory = true, Position = 0)] public string Slug { get; set; }
[Parameter(Mandatory = true)] public string CertificatePolicyId { get; set; }
[Parameter] public string CaId { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Description { get; set; }
[Parameter]
[ValidateSet("api", "est", "acme", "scep")]
public string EnrollmentType { get; set; } = "api";
[Parameter]
[ValidateSet("ca", "self-signed")]
public string IssuerType { get; set; } = "ca";
/// <summary>Renew issued certificates automatically.</summary>
[Parameter] public SwitchParameter AutoRenew { get; set; }
/// <summary>Days before expiry at which automatic renewal runs, 1 to 30.</summary>
[Parameter] public int? RenewBeforeDays { get; set; }
/// <summary>Issuance defaults, as @{ ttlDays = 90; keyAlgorithm = 'RSA_2048' }.</summary>
[Parameter] public IDictionary Defaults { get; set; }
/// <summary>Enrollment configuration for -EnrollmentType est, acme, or scep.</summary>
[Parameter] public IDictionary EnrollmentConfig { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(Slug, "Create certificate profile")) { return; }
InfisicalCertificateProfileWriteRequestDto request = BuildRequest(
Slug, Description, CaId, CertificatePolicyId, EnrollmentType, IssuerType,
AutoRenew.IsPresent, RenewBeforeDays, Defaults, EnrollmentConfig,
MyInvocation.BoundParameters.ContainsKey("AutoRenew"));
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
WriteObject(client.WriteCertificateProfile(connection, ProjectId, null, request));
}
catch (Exception exception)
{
WriteErrorForException(Component, "CreateCertificateProfile", exception);
}
}
/// <summary>
/// Routes the enrollment configuration to the block matching the enrollment type, so callers supply one
/// dictionary rather than choosing between four mutually exclusive parameters.
/// </summary>
internal static InfisicalCertificateProfileWriteRequestDto BuildRequest(
string slug, string description, string caId, string certificatePolicyId,
string enrollmentType, string issuerType, bool autoRenew, int? renewBeforeDays,
IDictionary defaults, IDictionary enrollmentConfig, bool autoRenewBound)
{
InfisicalCertificateProfileWriteRequestDto request = new InfisicalCertificateProfileWriteRequestDto
{
Slug = slug,
Description = description,
CaId = caId,
CertificatePolicyId = certificatePolicyId,
EnrollmentType = enrollmentType,
IssuerType = issuerType,
Defaults = ToJsonObject(defaults)
};
System.Collections.Generic.Dictionary<string, object> config = ToJsonObject(enrollmentConfig);
if (string.Equals(enrollmentType, "est", StringComparison.OrdinalIgnoreCase)) { request.EstConfig = config; }
else if (string.Equals(enrollmentType, "acme", StringComparison.OrdinalIgnoreCase)) { request.AcmeConfig = config; }
else if (string.Equals(enrollmentType, "scep", StringComparison.OrdinalIgnoreCase)) { request.ScepConfig = config; }
else
{
if (config == null && (autoRenewBound || renewBeforeDays.HasValue))
{
config = new System.Collections.Generic.Dictionary<string, object>();
}
if (config != null)
{
if (autoRenewBound) { config["autoRenew"] = autoRenew; }
if (renewBeforeDays.HasValue) { config["renewBeforeDays"] = renewBeforeDays.Value; }
}
request.ApiConfig = config;
}
return request;
}
}
/// <summary>
/// Updates a certificate profile. Only the supplied values are sent.
/// </summary>
[Cmdlet(VerbsCommon.Set, "InfisicalCertificateProfile", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalCertificateProfile))]
public sealed class SetInfisicalCertificateProfileCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "SetInfisicalCertificateProfileCmdlet";
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("Id", "CertificateProfileId")]
public string ProfileId { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Slug { get; set; }
[Parameter] public string Description { get; set; }
[Parameter] public string CaId { get; set; }
[Parameter] public string CertificatePolicyId { get; set; }
[Parameter]
[ValidateSet("api", "est", "acme", "scep")]
public string EnrollmentType { get; set; }
[Parameter] public SwitchParameter AutoRenew { get; set; }
[Parameter] public int? RenewBeforeDays { get; set; }
[Parameter] public IDictionary Defaults { get; set; }
[Parameter] public IDictionary EnrollmentConfig { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(ProfileId, "Update certificate profile")) { return; }
InfisicalCertificateProfileWriteRequestDto request = NewInfisicalCertificateProfileCmdlet.BuildRequest(
Slug, Description, CaId, CertificatePolicyId,
string.IsNullOrEmpty(EnrollmentType) ? "api" : EnrollmentType,
null, AutoRenew.IsPresent, RenewBeforeDays, Defaults, EnrollmentConfig,
MyInvocation.BoundParameters.ContainsKey("AutoRenew"));
// Only sent when the caller asked for it; the API keeps the stored value otherwise.
if (!MyInvocation.BoundParameters.ContainsKey("EnrollmentType")) { request.EnrollmentType = null; }
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
InfisicalCertificateProfile updated = client.WriteCertificateProfile(connection, ProjectId, ProfileId, request);
if (PassThru.IsPresent) { WriteObject(updated); }
}
catch (Exception exception)
{
WriteErrorForException(Component, "UpdateCertificateProfile", exception);
}
}
}
/// <summary>
/// Deletes a certificate profile.
/// </summary>
[Cmdlet(VerbsCommon.Remove, "InfisicalCertificateProfile", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public sealed class RemoveInfisicalCertificateProfileCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "RemoveInfisicalCertificateProfileCmdlet";
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("Id", "CertificateProfileId")]
public string ProfileId { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(ProfileId, "Delete certificate profile")) { return; }
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
client.DeleteCertificateProfile(connection, ProjectId, ProfileId);
if (PassThru.IsPresent) { WriteObject(ProfileId); }
}
catch (Exception exception)
{
WriteErrorForException(Component, "DeleteCertificateProfile", exception);
}
}
}
}
@@ -0,0 +1,164 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Management.Automation;
namespace PSInfisicalAPI.Cmdlets
{
/// <summary>
/// Shared plumbing for the cmdlets that create, change, or remove Certificate Manager configuration.
/// <para>
/// Infisical's policy and profile bodies are deeply nested — allowed/required/denied constraint objects,
/// per-enrollment-type config blocks — and expressing every leaf as a parameter would produce cmdlets nobody
/// could read. Those structures are accepted as dictionaries instead, matching how <c>-Subject</c> and
/// <c>-Metadata</c> already work, and are converted here.
/// </para>
/// </summary>
public abstract class InfisicalPkiWriteCmdletBase : InfisicalCmdletBase
{
/// <summary>
/// Converts a caller's dictionary into the plain string-keyed form the serializer emits as a JSON
/// object. Nested dictionaries and collections are converted too, so a hashtable of hashtables round
/// trips into the nested body Infisical expects.
/// </summary>
/// <summary>
/// The constraint vocabulary Infisical expects in lower case. PowerShell callers naturally capitalise
/// hashtable keys, so <c>@{ Required = ... }</c> has to reach the API as <c>"required"</c> or the request
/// is rejected for a missing constraint. Every other key is passed through untouched, since the rest of
/// the body uses camelCase field names the caller supplies verbatim.
/// </summary>
private static readonly Dictionary<string, string> CanonicalKeys = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "allowed", "allowed" },
{ "required", "required" },
{ "denied", "denied" },
{ "type", "type" }
};
internal static Dictionary<string, object> ToJsonObject(IDictionary source)
{
if (source == null) { return null; }
Dictionary<string, object> result = new Dictionary<string, object>(StringComparer.Ordinal);
foreach (DictionaryEntry entry in source)
{
if (entry.Key == null) { continue; }
string key = Convert.ToString(entry.Key, CultureInfo.InvariantCulture);
if (string.IsNullOrWhiteSpace(key)) { continue; }
key = key.Trim();
string canonical;
if (CanonicalKeys.TryGetValue(key, out canonical)) { key = canonical; }
object value = ToJsonValue(entry.Value);
// An empty list is dropped rather than sent. "allowed": [] does not read as "unconstrained" to
// Infisical, it reads as "allow nothing", which is never what a caller writing @{ Allowed = @() }
// intends; omitting the key leaves that dimension genuinely unconstrained.
List<object> asList = value as List<object>;
if (asList != null && asList.Count == 0) { continue; }
result[key] = value;
}
return result.Count > 0 ? result : null;
}
/// <summary>
/// Converts a list of dictionaries, which is the shape Infisical uses for policy subject and SAN
/// constraints.
/// </summary>
internal static List<Dictionary<string, object>> ToJsonObjectList(IEnumerable source)
{
if (source == null) { return null; }
List<Dictionary<string, object>> result = new List<Dictionary<string, object>>();
foreach (object item in source)
{
IDictionary dictionary = UnwrapDictionary(item);
if (dictionary == null) { continue; }
Dictionary<string, object> converted = ToJsonObject(dictionary);
if (converted == null) { continue; }
// An entry carrying only "type" states a dimension without constraining it, which Infisical
// rejects outright. Dropping it is what the caller meant by leaving the lists empty.
bool hasConstraint = converted.ContainsKey("allowed") || converted.ContainsKey("required") || converted.ContainsKey("denied");
if (!hasConstraint) { continue; }
result.Add(converted);
}
return result.Count > 0 ? result : null;
}
private static object ToJsonValue(object value)
{
if (value == null) { return null; }
IDictionary nested = UnwrapDictionary(value);
if (nested != null) { return ToJsonObject(nested); }
if (value is string) { return value; }
IEnumerable enumerable = UnwrapEnumerable(value);
if (enumerable != null)
{
List<object> items = new List<object>();
foreach (object item in enumerable) { items.Add(ToJsonValue(item)); }
return items;
}
return value;
}
/// <summary>
/// PowerShell hands parameters over wrapped in PSObject often enough that unwrapping has to happen at
/// every level, not only at the top.
/// </summary>
private static IDictionary UnwrapDictionary(object value)
{
if (value == null) { return null; }
if (value is IDictionary direct) { return direct; }
PSObject wrapper = value as PSObject;
if (wrapper != null && wrapper.BaseObject is IDictionary wrapped) { return wrapped; }
return null;
}
private static IEnumerable UnwrapEnumerable(object value)
{
if (value is IEnumerable direct) { return direct; }
PSObject wrapper = value as PSObject;
if (wrapper != null && wrapper.BaseObject is IEnumerable wrapped) { return wrapped; }
return null;
}
/// <summary>
/// Formats an expiry the way Infisical's date validator accepts it.
/// </summary>
internal static string ToApiTimestamp(DateTimeOffset? value)
{
if (!value.HasValue) { return null; }
return value.Value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture);
}
internal static List<string> ToStringList(IEnumerable<string> values)
{
if (values == null) { return null; }
List<string> result = new List<string>();
foreach (string value in values)
{
if (!string.IsNullOrWhiteSpace(value)) { result.Add(value.Trim()); }
}
return result.Count > 0 ? result : null;
}
}
}
@@ -0,0 +1,189 @@
using System;
using System.Collections;
using System.Management.Automation;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Pki;
namespace PSInfisicalAPI.Cmdlets
{
/// <summary>
/// Creates a PKI subscriber: a named enrollment identity pinning one common name, its SAN allowlist, and its
/// key usages.
/// </summary>
[Cmdlet(VerbsCommon.New, "InfisicalPkiSubscriber", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalPkiSubscriber))]
public sealed class NewInfisicalPkiSubscriberCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "NewInfisicalPkiSubscriberCmdlet";
[Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
/// <summary>
/// The certificate signed for this subscriber must carry exactly this common name; Infisical rejects a
/// request whose CSR names anything else.
/// </summary>
[Parameter(Mandatory = true)] public string CommonName { get; set; }
[Parameter(Mandatory = true)] public string CaId { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Ttl { get; set; }
/// <summary>Allowlist of subject alternative names. A CSR naming anything outside it is rejected.</summary>
[Parameter] public string[] SubjectAlternativeName { get; set; }
[Parameter] public string[] KeyUsage { get; set; }
[Parameter] public string[] ExtendedKeyUsage { get; set; }
[Parameter]
[ValidateSet("active", "disabled")]
public string Status { get; set; } = "active";
[Parameter] public SwitchParameter EnableAutoRenewal { get; set; }
[Parameter] public int? AutoRenewalPeriodInDays { get; set; }
/// <summary>Additional subject attributes, as @{ organization = 'Contoso'; country = 'US' }.</summary>
[Parameter] public IDictionary Properties { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(Name, "Create PKI subscriber")) { return; }
InfisicalPkiSubscriberWriteRequestDto request = new InfisicalPkiSubscriberWriteRequestDto
{
Name = Name,
CommonName = CommonName,
CaId = CaId,
Status = Status,
Ttl = Ttl,
SubjectAlternativeNames = ToStringList(SubjectAlternativeName),
KeyUsages = ToStringList(KeyUsage),
ExtendedKeyUsages = ToStringList(ExtendedKeyUsage),
Properties = ToJsonObject(Properties)
};
if (MyInvocation.BoundParameters.ContainsKey("EnableAutoRenewal")) { request.EnableAutoRenewal = EnableAutoRenewal.IsPresent; }
if (AutoRenewalPeriodInDays.HasValue) { request.AutoRenewalPeriodInDays = AutoRenewalPeriodInDays.Value; }
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
WriteObject(client.WritePkiSubscriber(connection, ProjectId, null, request));
}
catch (Exception exception)
{
WriteErrorForException(Component, "CreatePkiSubscriber", exception);
}
}
}
/// <summary>
/// Updates a PKI subscriber. Only the supplied values are sent.
/// </summary>
[Cmdlet(VerbsCommon.Set, "InfisicalPkiSubscriber", SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalPkiSubscriber))]
public sealed class SetInfisicalPkiSubscriberCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "SetInfisicalPkiSubscriberCmdlet";
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("SubscriberName", "Slug")]
public string Name { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public string NewName { get; set; }
[Parameter] public string CommonName { get; set; }
[Parameter] public string CaId { get; set; }
[Parameter] public string Ttl { get; set; }
[Parameter] public string[] SubjectAlternativeName { get; set; }
[Parameter] public string[] KeyUsage { get; set; }
[Parameter] public string[] ExtendedKeyUsage { get; set; }
[Parameter]
[ValidateSet("active", "disabled")]
public string Status { get; set; }
[Parameter] public SwitchParameter EnableAutoRenewal { get; set; }
[Parameter] public int? AutoRenewalPeriodInDays { get; set; }
[Parameter] public IDictionary Properties { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(Name, "Update PKI subscriber")) { return; }
InfisicalPkiSubscriberWriteRequestDto request = new InfisicalPkiSubscriberWriteRequestDto
{
Name = NewName,
CommonName = CommonName,
CaId = CaId,
Status = Status,
Ttl = Ttl,
SubjectAlternativeNames = ToStringList(SubjectAlternativeName),
KeyUsages = ToStringList(KeyUsage),
ExtendedKeyUsages = ToStringList(ExtendedKeyUsage),
Properties = ToJsonObject(Properties)
};
if (MyInvocation.BoundParameters.ContainsKey("EnableAutoRenewal")) { request.EnableAutoRenewal = EnableAutoRenewal.IsPresent; }
if (AutoRenewalPeriodInDays.HasValue) { request.AutoRenewalPeriodInDays = AutoRenewalPeriodInDays.Value; }
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
InfisicalPkiSubscriber updated = client.WritePkiSubscriber(connection, ProjectId, Name, request);
if (PassThru.IsPresent) { WriteObject(updated); }
}
catch (Exception exception)
{
WriteErrorForException(Component, "UpdatePkiSubscriber", exception);
}
}
}
/// <summary>
/// Deletes a PKI subscriber.
/// </summary>
[Cmdlet(VerbsCommon.Remove, "InfisicalPkiSubscriber", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public sealed class RemoveInfisicalPkiSubscriberCmdlet : InfisicalPkiWriteCmdletBase
{
private const string Component = "RemoveInfisicalPkiSubscriberCmdlet";
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("SubscriberName", "Slug")]
public string Name { get; set; }
[Parameter] public string ProjectId { get; set; }
[Parameter] public SwitchParameter PassThru { get; set; }
protected override void ProcessRecord()
{
try
{
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
ProjectId = ResolveCertManagerProjectId(connection, ProjectId);
if (string.IsNullOrEmpty(ProjectId)) { return; }
if (!ShouldProcess(Name, "Delete PKI subscriber")) { return; }
InfisicalPkiManagementClient client = new InfisicalPkiManagementClient(HttpClient, Logger);
client.DeletePkiSubscriber(connection, ProjectId, Name);
if (PassThru.IsPresent) { WriteObject(Name); }
}
catch (Exception exception)
{
WriteErrorForException(Component, "DeletePkiSubscriber", exception);
}
}
}
}
@@ -62,6 +62,31 @@ namespace PSInfisicalAPI.Endpoints
public const string RetrieveCertificate = "RetrieveCertificate";
public const string GetCertificateBundle = "GetCertificateBundle";
public const string UpdateCertificateMetadata = "UpdateCertificateMetadata";
public const string CreateInternalCertificateAuthority = "CreateInternalCertificateAuthority";
public const string UpdateInternalCertificateAuthority = "UpdateInternalCertificateAuthority";
public const string DeleteInternalCertificateAuthority = "DeleteInternalCertificateAuthority";
public const string GetCertificateAuthorityCsr = "GetCertificateAuthorityCsr";
public const string SignIntermediateCertificateAuthority = "SignIntermediateCertificateAuthority";
public const string ImportCertificateAuthorityCertificate = "ImportCertificateAuthorityCertificate";
public const string CreateCertificatePolicy = "CreateCertificatePolicy";
public const string UpdateCertificatePolicy = "UpdateCertificatePolicy";
public const string DeleteCertificatePolicy = "DeleteCertificatePolicy";
public const string CreateCertificateProfile = "CreateCertificateProfile";
public const string UpdateCertificateProfile = "UpdateCertificateProfile";
public const string DeleteCertificateProfile = "DeleteCertificateProfile";
public const string CreateCertificateApplication = "CreateCertificateApplication";
public const string UpdateCertificateApplication = "UpdateCertificateApplication";
public const string DeleteCertificateApplication = "DeleteCertificateApplication";
public const string AddCertificateApplicationProfiles = "AddCertificateApplicationProfiles";
public const string RemoveCertificateApplicationProfile = "RemoveCertificateApplicationProfile";
public const string CreatePkiSubscriber = "CreatePkiSubscriber";
public const string UpdatePkiSubscriber = "UpdatePkiSubscriber";
public const string DeletePkiSubscriber = "DeletePkiSubscriber";
public const string SignCertificateBySubscriber = "SignCertificateBySubscriber";
public const string SignCertificateByCa = "SignCertificateByCa";
public const string IssueCertificateByProfile = "IssueCertificateByProfile";
@@ -715,6 +715,43 @@ namespace PSInfisicalAPI.Endpoints
ContainsSecretMaterialInResponse = true
});
#region PKI configuration management
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.CreateInternalCertificateAuthority, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/cert-manager/ca/internal", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.CreateInternalCertificateAuthority, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/pki/ca/internal", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.UpdateInternalCertificateAuthority, Resource = "Pki", Version = "v1", Method = "PATCH", Template = "/api/v1/cert-manager/ca/internal/{caId}", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.UpdateInternalCertificateAuthority, Resource = "Pki", Version = "v1", Method = "PATCH", Template = "/api/v1/pki/ca/internal/{caId}", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.DeleteInternalCertificateAuthority, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/cert-manager/ca/internal/{caId}", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.DeleteInternalCertificateAuthority, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/pki/ca/internal/{caId}", RequiresAuthorization = true });
// Standing up a subordinate takes three calls: Infisical creates it pending, then the CSR is signed
// by the parent and imported back.
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.GetCertificateAuthorityCsr, Resource = "Pki", Version = "v1", Method = "GET", Template = "/api/v1/pki/ca/{caId}/csr", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.SignIntermediateCertificateAuthority, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/pki/ca/{caId}/sign-intermediate", RequiresAuthorization = true, ContainsSecretMaterialInResponse = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.ImportCertificateAuthorityCertificate, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/pki/ca/{caId}/import-certificate", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.CreateCertificatePolicy, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/cert-manager/certificate-policies", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.UpdateCertificatePolicy, Resource = "Pki", Version = "v1", Method = "PATCH", Template = "/api/v1/cert-manager/certificate-policies/{policyId}", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.DeleteCertificatePolicy, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/cert-manager/certificate-policies/{policyId}", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.CreateCertificateProfile, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/cert-manager/certificate-profiles", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.UpdateCertificateProfile, Resource = "Pki", Version = "v1", Method = "PATCH", Template = "/api/v1/cert-manager/certificate-profiles/{profileId}", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.DeleteCertificateProfile, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/cert-manager/certificate-profiles/{profileId}", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.CreateCertificateApplication, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/cert-manager/applications", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.UpdateCertificateApplication, Resource = "Pki", Version = "v1", Method = "PATCH", Template = "/api/v1/cert-manager/applications/{applicationId}", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.DeleteCertificateApplication, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/cert-manager/applications/{applicationId}", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.AddCertificateApplicationProfiles, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/cert-manager/applications/{applicationId}/profiles", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.RemoveCertificateApplicationProfile, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/cert-manager/applications/{applicationId}/profiles/{profileId}", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.CreatePkiSubscriber, Resource = "Pki", Version = "v1", Method = "POST", Template = "/api/v1/pki/subscribers", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.UpdatePkiSubscriber, Resource = "Pki", Version = "v1", Method = "PATCH", Template = "/api/v1/pki/subscribers/{subscriberName}", RequiresAuthorization = true });
Add(map, new InfisicalEndpointDefinition { Name = InfisicalEndpointNames.DeletePkiSubscriber, Resource = "Pki", Version = "v1", Method = "DELETE", Template = "/api/v1/pki/subscribers/{subscriberName}", RequiresAuthorization = true });
#endregion
Add(map, new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.UpdateCertificateMetadata,
@@ -0,0 +1,443 @@
using System;
using System.Collections.Generic;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Endpoints;
using PSInfisicalAPI.Errors;
using PSInfisicalAPI.Http;
using PSInfisicalAPI.Logging;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Serialization;
namespace PSInfisicalAPI.Pki
{
/// <summary>
/// Create, update, and delete operations for the objects that make up a Certificate Manager environment:
/// certificate authorities, policies, profiles, applications, and PKI subscribers.
/// <para>
/// Kept apart from <see cref="InfisicalPkiClient"/>, which reads and issues. These are the operations that
/// change how a project is configured rather than what it has issued.
/// </para>
/// </summary>
internal sealed class InfisicalPkiManagementClient
{
private const string Component = "PkiManagementClient";
private readonly InfisicalApiInvoker _invoker;
private readonly IInfisicalLogger _logger;
private readonly JsonInfisicalSerializer _serializer;
public InfisicalPkiManagementClient(IInfisicalHttpClient httpClient, IInfisicalLogger logger)
{
if (httpClient == null) { throw new ArgumentNullException(nameof(httpClient)); }
_logger = logger ?? NullInfisicalLogger.Instance;
_invoker = new InfisicalApiInvoker(httpClient);
_serializer = new JsonInfisicalSerializer();
}
#region Certificate authority
public InfisicalCertificateAuthority CreateInternalCertificateAuthority(
InfisicalConnection connection,
string projectId,
string name,
string type,
string commonName,
string organization,
string organizationalUnit,
string country,
string state,
string locality,
string keyAlgorithm,
string friendlyName,
string notBefore,
string notAfter,
int? maxPathLength)
{
Require(connection, projectId);
if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("Name is required."); }
if (string.IsNullOrEmpty(type)) { throw new InfisicalConfigurationException("Type is required."); }
bool isRoot = string.Equals(type, "root", StringComparison.OrdinalIgnoreCase);
InfisicalCreateInternalCaRequestDto request = new InfisicalCreateInternalCaRequestDto
{
ProjectId = projectId,
Name = name,
Status = "active",
Configuration = new InfisicalCreateInternalCaConfigurationDto
{
Type = type,
CommonName = commonName ?? string.Empty,
Organization = organization ?? string.Empty,
OrganizationalUnit = organizationalUnit ?? string.Empty,
Country = country ?? string.Empty,
State = state ?? string.Empty,
Locality = locality ?? string.Empty,
KeyAlgorithm = keyAlgorithm,
FriendlyName = friendlyName,
// Infisical self-signs on creation only for a root, and only when given an expiry. A
// subordinate is created pending a certificate and signed separately.
NotBefore = isRoot ? notBefore : null,
NotAfter = isRoot ? notAfter : null,
MaxPathLength = isRoot ? maxPathLength : null
}
};
return Execute(
connection,
InfisicalEndpointNames.CreateInternalCertificateAuthority,
null,
_serializer.Serialize(request),
string.Concat("create ", type, " certificate authority '", name, "'"),
body =>
{
InfisicalInternalCaSingleResponseDto dto = _serializer.Deserialize<InfisicalInternalCaSingleResponseDto>(body);
InfisicalInternalCaResponseDto inner = dto != null ? (dto.CertificateAuthority ?? dto.Ca) : null;
return InfisicalCaMapper.Map(inner, projectId);
});
}
public InfisicalCertificateAuthority UpdateInternalCertificateAuthority(InfisicalConnection connection, string projectId, string caId, string name, string status)
{
Require(connection, projectId);
if (string.IsNullOrEmpty(caId)) { throw new InfisicalConfigurationException("CaId is required."); }
if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(status))
{
throw new InfisicalConfigurationException("Supply -Name or -Status; there is nothing else to update on an internal certificate authority.");
}
InfisicalUpdateInternalCaRequestDto request = new InfisicalUpdateInternalCaRequestDto { ProjectId = projectId, Name = name, Status = status };
return Execute(
connection,
InfisicalEndpointNames.UpdateInternalCertificateAuthority,
new Dictionary<string, string> { { "caId", caId } },
_serializer.Serialize(request),
string.Concat("update certificate authority '", caId, "'"),
body =>
{
InfisicalInternalCaSingleResponseDto dto = _serializer.Deserialize<InfisicalInternalCaSingleResponseDto>(body);
InfisicalInternalCaResponseDto inner = dto != null ? (dto.CertificateAuthority ?? dto.Ca) : null;
return InfisicalCaMapper.Map(inner, projectId);
});
}
public void DeleteInternalCertificateAuthority(InfisicalConnection connection, string projectId, string caId)
{
Require(connection, projectId);
if (string.IsNullOrEmpty(caId)) { throw new InfisicalConfigurationException("CaId is required."); }
Execute<object>(
connection,
InfisicalEndpointNames.DeleteInternalCertificateAuthority,
new Dictionary<string, string> { { "caId", caId } },
null,
string.Concat("delete certificate authority '", caId, "'"),
body => null,
BuildProjectQuery(projectId));
}
/// <summary>
/// Signs a newly created subordinate with its parent and imports the result, taking it from
/// pending-certificate to active. Infisical creates a subordinate without a certificate and exposes no
/// single call that does this.
/// </summary>
public void CompleteSubordinateCertificateAuthority(InfisicalConnection connection, string projectId, string subordinateCaId, string parentCaId, string notAfter, int maxPathLength)
{
Require(connection, projectId);
if (string.IsNullOrEmpty(subordinateCaId)) { throw new InfisicalConfigurationException("CaId is required."); }
if (string.IsNullOrEmpty(parentCaId)) { throw new InfisicalConfigurationException("ParentCaId is required."); }
if (string.IsNullOrEmpty(notAfter)) { throw new InfisicalConfigurationException("NotAfter is required to sign a subordinate certificate authority."); }
string csr = Execute(
connection,
InfisicalEndpointNames.GetCertificateAuthorityCsr,
new Dictionary<string, string> { { "caId", subordinateCaId } },
null,
string.Concat("read the certificate signing request for '", subordinateCaId, "'"),
body =>
{
InfisicalCaCsrResponseDto dto = _serializer.Deserialize<InfisicalCaCsrResponseDto>(body);
return dto != null ? dto.Csr : null;
});
if (string.IsNullOrEmpty(csr))
{
throw new InfisicalApiException(string.Concat("Certificate authority '", subordinateCaId, "' returned no certificate signing request to sign."));
}
InfisicalSignIntermediateRequestDto signRequest = new InfisicalSignIntermediateRequestDto
{
Csr = csr,
NotAfter = notAfter,
MaxPathLength = maxPathLength
};
InfisicalSignIntermediateResponseDto signed = Execute(
connection,
InfisicalEndpointNames.SignIntermediateCertificateAuthority,
new Dictionary<string, string> { { "caId", parentCaId } },
_serializer.Serialize(signRequest),
string.Concat("sign '", subordinateCaId, "' with '", parentCaId, "'"),
body => _serializer.Deserialize<InfisicalSignIntermediateResponseDto>(body));
if (signed == null || string.IsNullOrEmpty(signed.Certificate))
{
throw new InfisicalApiException("Signing the subordinate certificate authority returned no certificate.");
}
InfisicalImportCaCertificateRequestDto importRequest = new InfisicalImportCaCertificateRequestDto
{
Certificate = signed.Certificate,
CertificateChain = signed.CertificateChain
};
Execute<object>(
connection,
InfisicalEndpointNames.ImportCertificateAuthorityCertificate,
new Dictionary<string, string> { { "caId", subordinateCaId } },
_serializer.Serialize(importRequest),
string.Concat("import the signed certificate onto '", subordinateCaId, "'"),
body => null);
}
#endregion
#region Certificate policy
public InfisicalCertificatePolicy WriteCertificatePolicy(InfisicalConnection connection, string projectId, string policyId, InfisicalCertificatePolicyWriteRequestDto request)
{
Require(connection, projectId);
if (request == null) { throw new ArgumentNullException(nameof(request)); }
bool creating = string.IsNullOrEmpty(policyId);
request.ProjectId = projectId;
return Execute(
connection,
creating ? InfisicalEndpointNames.CreateCertificatePolicy : InfisicalEndpointNames.UpdateCertificatePolicy,
creating ? null : new Dictionary<string, string> { { "policyId", policyId } },
_serializer.Serialize(request),
creating ? string.Concat("create certificate policy '", request.Name, "'") : string.Concat("update certificate policy '", policyId, "'"),
body =>
{
InfisicalCertificatePolicySingleResponseDto dto = _serializer.Deserialize<InfisicalCertificatePolicySingleResponseDto>(body);
return InfisicalCertificatePolicyMapper.Map(dto != null ? dto.CertificatePolicy : null, projectId);
});
}
public void DeleteCertificatePolicy(InfisicalConnection connection, string projectId, string policyId)
{
Require(connection, projectId);
if (string.IsNullOrEmpty(policyId)) { throw new InfisicalConfigurationException("PolicyId is required."); }
Execute<object>(
connection,
InfisicalEndpointNames.DeleteCertificatePolicy,
new Dictionary<string, string> { { "policyId", policyId } },
null,
string.Concat("delete certificate policy '", policyId, "'"),
body => null,
BuildProjectQuery(projectId));
}
#endregion
#region Certificate profile
public InfisicalCertificateProfile WriteCertificateProfile(InfisicalConnection connection, string projectId, string profileId, InfisicalCertificateProfileWriteRequestDto request)
{
Require(connection, projectId);
if (request == null) { throw new ArgumentNullException(nameof(request)); }
bool creating = string.IsNullOrEmpty(profileId);
request.ProjectId = projectId;
return Execute(
connection,
creating ? InfisicalEndpointNames.CreateCertificateProfile : InfisicalEndpointNames.UpdateCertificateProfile,
creating ? null : new Dictionary<string, string> { { "profileId", profileId } },
_serializer.Serialize(request),
creating ? string.Concat("create certificate profile '", request.Slug, "'") : string.Concat("update certificate profile '", profileId, "'"),
body =>
{
InfisicalCertificateProfileSingleResponseDto dto = _serializer.Deserialize<InfisicalCertificateProfileSingleResponseDto>(body);
return InfisicalCertificateProfileMapper.Map(dto != null ? dto.CertificateProfile : null, projectId);
});
}
public void DeleteCertificateProfile(InfisicalConnection connection, string projectId, string profileId)
{
Require(connection, projectId);
if (string.IsNullOrEmpty(profileId)) { throw new InfisicalConfigurationException("ProfileId is required."); }
Execute<object>(
connection,
InfisicalEndpointNames.DeleteCertificateProfile,
new Dictionary<string, string> { { "profileId", profileId } },
null,
string.Concat("delete certificate profile '", profileId, "'"),
body => null,
BuildProjectQuery(projectId));
}
#endregion
#region Certificate application
public InfisicalCertificateApplication WriteCertificateApplication(InfisicalConnection connection, string projectId, string applicationId, InfisicalCertificateApplicationWriteRequestDto request)
{
Require(connection, projectId);
if (request == null) { throw new ArgumentNullException(nameof(request)); }
bool creating = string.IsNullOrEmpty(applicationId);
request.ProjectId = projectId;
return Execute(
connection,
creating ? InfisicalEndpointNames.CreateCertificateApplication : InfisicalEndpointNames.UpdateCertificateApplication,
creating ? null : new Dictionary<string, string> { { "applicationId", applicationId } },
_serializer.Serialize(request),
creating ? string.Concat("create certificate application '", request.Name, "'") : string.Concat("update certificate application '", applicationId, "'"),
body =>
{
InfisicalCertificateApplicationSingleResponseDto dto = _serializer.Deserialize<InfisicalCertificateApplicationSingleResponseDto>(body);
return InfisicalCertificateApplicationMapper.Map(dto != null ? dto.Application : null, projectId);
});
}
public void DeleteCertificateApplication(InfisicalConnection connection, string projectId, string applicationId)
{
Require(connection, projectId);
if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); }
Execute<object>(
connection,
InfisicalEndpointNames.DeleteCertificateApplication,
new Dictionary<string, string> { { "applicationId", applicationId } },
null,
string.Concat("delete certificate application '", applicationId, "'"),
body => null,
BuildProjectQuery(projectId));
}
public void AddCertificateApplicationProfiles(InfisicalConnection connection, string projectId, string applicationId, IEnumerable<string> profileIds)
{
Require(connection, projectId);
if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); }
List<string> ids = new List<string>();
if (profileIds != null) { foreach (string id in profileIds) { if (!string.IsNullOrEmpty(id)) { ids.Add(id); } } }
if (ids.Count == 0) { throw new InfisicalConfigurationException("At least one ProfileId is required."); }
InfisicalCertificateApplicationWriteRequestDto request = new InfisicalCertificateApplicationWriteRequestDto { ProjectId = projectId, ProfileIds = ids };
Execute<object>(
connection,
InfisicalEndpointNames.AddCertificateApplicationProfiles,
new Dictionary<string, string> { { "applicationId", applicationId } },
_serializer.Serialize(request),
string.Concat("attach ", ids.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " profile(s) to application '", applicationId, "'"),
body => null);
}
public void RemoveCertificateApplicationProfile(InfisicalConnection connection, string projectId, string applicationId, string profileId)
{
Require(connection, projectId);
if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); }
if (string.IsNullOrEmpty(profileId)) { throw new InfisicalConfigurationException("ProfileId is required."); }
Execute<object>(
connection,
InfisicalEndpointNames.RemoveCertificateApplicationProfile,
new Dictionary<string, string> { { "applicationId", applicationId }, { "profileId", profileId } },
null,
string.Concat("detach profile '", profileId, "' from application '", applicationId, "'"),
body => null,
BuildProjectQuery(projectId));
}
#endregion
#region PKI subscriber
public InfisicalPkiSubscriber WritePkiSubscriber(InfisicalConnection connection, string projectId, string subscriberName, InfisicalPkiSubscriberWriteRequestDto request)
{
Require(connection, projectId);
if (request == null) { throw new ArgumentNullException(nameof(request)); }
bool creating = string.IsNullOrEmpty(subscriberName);
request.ProjectId = projectId;
return Execute(
connection,
creating ? InfisicalEndpointNames.CreatePkiSubscriber : InfisicalEndpointNames.UpdatePkiSubscriber,
creating ? null : new Dictionary<string, string> { { "subscriberName", subscriberName } },
_serializer.Serialize(request),
creating ? string.Concat("create PKI subscriber '", request.Name, "'") : string.Concat("update PKI subscriber '", subscriberName, "'"),
// The subscriber routes return the record unwrapped, matching how GetPkiSubscriber reads it.
body => InfisicalPkiSubscriberMapper.Map(_serializer.Deserialize<InfisicalPkiSubscriberResponseDto>(body), projectId));
}
public void DeletePkiSubscriber(InfisicalConnection connection, string projectId, string subscriberName)
{
Require(connection, projectId);
if (string.IsNullOrEmpty(subscriberName)) { throw new InfisicalConfigurationException("Name is required."); }
Execute<object>(
connection,
InfisicalEndpointNames.DeletePkiSubscriber,
new Dictionary<string, string> { { "subscriberName", subscriberName } },
null,
string.Concat("delete PKI subscriber '", subscriberName, "'"),
body => null,
BuildProjectQuery(projectId));
}
#endregion
#region Plumbing
private static void Require(InfisicalConnection connection, string projectId)
{
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); }
}
private static List<KeyValuePair<string, string>> BuildProjectQuery(string projectId)
{
// DELETE carries no body, so the project has to travel on the query string for Infisical's
// project-injection middleware to see it.
return new List<KeyValuePair<string, string>> { new KeyValuePair<string, string>("projectId", projectId) };
}
private T Execute<T>(
InfisicalConnection connection,
string endpointName,
Dictionary<string, string> pathParameters,
string body,
string description,
Func<string, T> project,
List<KeyValuePair<string, string>> query = null)
{
try
{
_logger.Information(Component, string.Concat("Attempting to ", description, ". Please Wait..."));
InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, endpointName, endpointName, pathParameters, query, body);
string payload = response.Body;
response.Clear();
T result = project(payload);
_logger.Information(Component, string.Concat("Infisical operation succeeded: ", description, "."));
return result;
}
catch (Exception)
{
_logger.Error(Component, string.Concat("Infisical operation failed: ", description, "."));
throw;
}
}
#endregion
}
}
@@ -0,0 +1,152 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace PSInfisicalAPI.Pki
{
#region Certificate authority
internal sealed class InfisicalCreateInternalCaRequestDto
{
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
[JsonProperty("name")] public string Name { get; set; }
[JsonProperty("status")] public string Status { get; set; }
[JsonProperty("configuration")] public InfisicalCreateInternalCaConfigurationDto Configuration { get; set; }
}
internal sealed class InfisicalCreateInternalCaConfigurationDto
{
[JsonProperty("type")] public string Type { get; set; }
[JsonProperty("commonName")] public string CommonName { get; set; }
[JsonProperty("organization")] public string Organization { get; set; }
[JsonProperty("ou")] public string OrganizationalUnit { get; set; }
[JsonProperty("country")] public string Country { get; set; }
[JsonProperty("province")] public string State { get; set; }
[JsonProperty("locality")] public string Locality { get; set; }
[JsonProperty("keyAlgorithm")] public string KeyAlgorithm { get; set; }
[JsonProperty("friendlyName", NullValueHandling = NullValueHandling.Ignore)] public string FriendlyName { get; set; }
[JsonProperty("notBefore", NullValueHandling = NullValueHandling.Ignore)] public string NotBefore { get; set; }
[JsonProperty("notAfter", NullValueHandling = NullValueHandling.Ignore)] public string NotAfter { get; set; }
[JsonProperty("maxPathLength", NullValueHandling = NullValueHandling.Ignore)] public int? MaxPathLength { get; set; }
}
internal sealed class InfisicalUpdateInternalCaRequestDto
{
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; }
[JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] public string Status { get; set; }
}
internal sealed class InfisicalCaCsrResponseDto
{
[JsonProperty("csr")] public string Csr { get; set; }
}
internal sealed class InfisicalSignIntermediateRequestDto
{
[JsonProperty("csr")] public string Csr { get; set; }
[JsonProperty("notBefore", NullValueHandling = NullValueHandling.Ignore)] public string NotBefore { get; set; }
[JsonProperty("notAfter")] public string NotAfter { get; set; }
[JsonProperty("maxPathLength")] public int MaxPathLength { get; set; }
}
internal sealed class InfisicalSignIntermediateResponseDto
{
[JsonProperty("certificate")] public string Certificate { get; set; }
[JsonProperty("certificateChain")] public string CertificateChain { get; set; }
[JsonProperty("issuingCaCertificate")] public string IssuingCaCertificate { get; set; }
[JsonProperty("serialNumber")] public string SerialNumber { get; set; }
}
internal sealed class InfisicalImportCaCertificateRequestDto
{
[JsonProperty("certificate")] public string Certificate { get; set; }
[JsonProperty("certificateChain")] public string CertificateChain { get; set; }
}
#endregion
#region Certificate policy
internal sealed class InfisicalCertificatePolicyWriteRequestDto
{
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; }
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; }
[JsonProperty("subject", NullValueHandling = NullValueHandling.Ignore)] public List<Dictionary<string, object>> Subject { get; set; }
[JsonProperty("sans", NullValueHandling = NullValueHandling.Ignore)] public List<Dictionary<string, object>> Sans { get; set; }
[JsonProperty("keyUsages", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> KeyUsages { get; set; }
[JsonProperty("extendedKeyUsages", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> ExtendedKeyUsages { get; set; }
[JsonProperty("algorithms", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> Algorithms { get; set; }
[JsonProperty("validity", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> Validity { get; set; }
[JsonProperty("basicConstraints", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> BasicConstraints { get; set; }
}
internal sealed class InfisicalCertificatePolicySingleResponseDto
{
[JsonProperty("certificatePolicy")] public InfisicalCertificatePolicyResponseDto CertificatePolicy { get; set; }
}
#endregion
#region Certificate profile
internal sealed class InfisicalCertificateProfileWriteRequestDto
{
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
[JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] public string Slug { get; set; }
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; }
[JsonProperty("caId", NullValueHandling = NullValueHandling.Ignore)] public string CaId { get; set; }
[JsonProperty("certificatePolicyId", NullValueHandling = NullValueHandling.Ignore)] public string CertificatePolicyId { get; set; }
[JsonProperty("enrollmentType", NullValueHandling = NullValueHandling.Ignore)] public string EnrollmentType { get; set; }
[JsonProperty("issuerType", NullValueHandling = NullValueHandling.Ignore)] public string IssuerType { get; set; }
[JsonProperty("apiConfig", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> ApiConfig { get; set; }
[JsonProperty("scepConfig", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> ScepConfig { get; set; }
[JsonProperty("estConfig", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> EstConfig { get; set; }
[JsonProperty("acmeConfig", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> AcmeConfig { get; set; }
[JsonProperty("defaults", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> Defaults { get; set; }
}
internal sealed class InfisicalCertificateProfileSingleResponseDto
{
[JsonProperty("certificateProfile")] public InfisicalCertificateProfileResponseDto CertificateProfile { get; set; }
}
#endregion
#region Certificate application
internal sealed class InfisicalCertificateApplicationWriteRequestDto
{
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; }
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; }
[JsonProperty("profileIds", NullValueHandling = NullValueHandling.Ignore)] public List<string> ProfileIds { get; set; }
}
internal sealed class InfisicalCertificateApplicationSingleResponseDto
{
[JsonProperty("application")] public InfisicalCertificateApplicationResponseDto Application { get; set; }
}
#endregion
#region PKI subscriber
internal sealed class InfisicalPkiSubscriberWriteRequestDto
{
[JsonProperty("projectId", NullValueHandling = NullValueHandling.Ignore)] public string ProjectId { get; set; }
[JsonProperty("caId", NullValueHandling = NullValueHandling.Ignore)] public string CaId { get; set; }
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; }
[JsonProperty("commonName", NullValueHandling = NullValueHandling.Ignore)] public string CommonName { get; set; }
[JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] public string Status { get; set; }
[JsonProperty("ttl", NullValueHandling = NullValueHandling.Ignore)] public string Ttl { get; set; }
[JsonProperty("subjectAlternativeNames", NullValueHandling = NullValueHandling.Ignore)] public List<string> SubjectAlternativeNames { get; set; }
[JsonProperty("keyUsages", NullValueHandling = NullValueHandling.Ignore)] public List<string> KeyUsages { get; set; }
[JsonProperty("extendedKeyUsages", NullValueHandling = NullValueHandling.Ignore)] public List<string> ExtendedKeyUsages { get; set; }
[JsonProperty("enableAutoRenewal", NullValueHandling = NullValueHandling.Ignore)] public bool? EnableAutoRenewal { get; set; }
[JsonProperty("autoRenewalPeriodInDays", NullValueHandling = NullValueHandling.Ignore)] public int? AutoRenewalPeriodInDays { get; set; }
[JsonProperty("properties", NullValueHandling = NullValueHandling.Ignore)] public Dictionary<string, object> Properties { get; set; }
}
#endregion
}