feat(scep): rework Get-InfisicalScepMdmProfile into FromEnrollment/FromProfile/Manual parameter sets

FromEnrollment (new default) consumes an InfisicalCertificateApplicationEnrollment and auto-fills ServerUrl from scep.scepEndpointUrl, CAThumbprint from the RA certificate thumbprint, and mints a fresh dynamic challenge automatically when challengeType=dynamic and -Challenge is not supplied. FromProfile preserves the legacy projection from an InfisicalCertificateProfile but now requires -ApplicationId so the server URL is built against /scep/applications/{appId}/profiles/{profileId}/pkiclient.exe. Manual requires explicit -ServerUrl, -Challenge, and -UniqueId. Module manifest, help XML, and build.ps1 expectedCmds list updated to register the three new cmdlets. CHANGELOG updated.
This commit is contained in:
GraceSolutions
2026-06-04 19:35:16 -04:00
parent 148a09f0d9
commit 3c39a99b9a
5 changed files with 257 additions and 53 deletions
@@ -6,24 +6,39 @@ using System.Runtime.InteropServices;
using System.Security;
using PSInfisicalAPI.Connections;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Pki;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "InfisicalScepMdmProfile")]
[Cmdlet(VerbsCommon.Get, "InfisicalScepMdmProfile", DefaultParameterSetName = "FromEnrollment")]
[OutputType(typeof(InfisicalScepMdmProfile))]
public sealed class GetInfisicalScepMdmProfileCmdlet : InfisicalCmdletBase
{
private const string Component = "GetInfisicalScepMdmProfileCmdlet";
[Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)]
[Parameter(ParameterSetName = "FromEnrollment", Mandatory = true, ValueFromPipeline = true, Position = 0)]
[Alias("Enrollment")]
public InfisicalCertificateApplicationEnrollment EnrollmentObject { get; set; }
[Parameter(ParameterSetName = "FromProfile", Mandatory = true, ValueFromPipeline = true, Position = 0)]
[Alias("Profile", "CertificateProfile")]
public InfisicalCertificateProfile InputObject { get; set; }
[Parameter(Mandatory = true)]
[Parameter(ParameterSetName = "FromProfile", Mandatory = true)]
[Alias("AppId")]
public string ApplicationId { get; set; }
[Parameter(ParameterSetName = "FromEnrollment")]
[Parameter(ParameterSetName = "FromProfile")]
[Parameter(ParameterSetName = "Manual", Mandatory = true)]
public SecureString Challenge { get; set; }
[Parameter(ParameterSetName = "Manual", Mandatory = true)]
[Parameter(ParameterSetName = "FromProfile")]
[Parameter(ParameterSetName = "FromEnrollment")]
public string ServerUrl { get; set; }
[Parameter] public string UniqueId { get; set; }
[Parameter] public string ServerUrl { get; set; }
[Parameter]
[ValidateSet("Device", "User")]
@@ -53,45 +68,21 @@ namespace PSInfisicalAPI.Cmdlets
{
try
{
if (InputObject == null) { throw new InvalidOperationException("InputObject is required."); }
if (string.IsNullOrEmpty(InputObject.Id)) { throw new InvalidOperationException("InputObject.Id is required."); }
InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
string resolvedServerUrl = !string.IsNullOrEmpty(ServerUrl) ? ServerUrl : BuildDefaultServerUrl(connection, InputObject.Id);
string resolvedUniqueId = !string.IsNullOrEmpty(UniqueId) ? UniqueId : SanitizeForCspId(!string.IsNullOrEmpty(InputObject.Slug) ? InputObject.Slug : InputObject.Id);
InfisicalCertificateProfileDefaults defaults = InputObject.Defaults;
string resolvedKeyAlgorithm = !string.IsNullOrEmpty(KeyAlgorithm) ? KeyAlgorithm : MapKeyAlgorithm(defaults != null ? defaults.KeyAlgorithm : null);
string resolvedEku = !string.IsNullOrEmpty(EkuMapping) ? EkuMapping : JoinEkuOids(defaults != null ? defaults.ExtendedKeyUsages : null);
InfisicalScepMdmProfile result = new InfisicalScepMdmProfile
if (string.Equals(ParameterSetName, "FromEnrollment", StringComparison.Ordinal))
{
UniqueId = resolvedUniqueId,
Scope = Scope,
ServerUrl = resolvedServerUrl,
Challenge = SecureStringToPlainText(Challenge),
SubjectName = SubjectName,
SubjectAlternativeNames = SubjectAlternativeNames,
EkuMapping = resolvedEku,
KeyUsage = KeyUsage,
KeyLength = KeyLength,
KeyAlgorithm = resolvedKeyAlgorithm,
HashAlgorithm = HashAlgorithm,
KeyProtection = KeyProtection,
ContainerName = ContainerName,
ValidPeriod = ValidPeriod,
ValidPeriodUnits = ValidPeriodUnits,
RetryCount = RetryCount,
RetryDelay = RetryDelay,
TemplateName = TemplateName,
CAThumbprint = CAThumbprint,
CustomTextToShowInPrompt = CustomTextToShowInPrompt,
SourceProfileId = InputObject.Id,
SourceProfileSlug = InputObject.Slug
};
WriteObject(BuildFromEnrollment(connection));
return;
}
Logger.Verbose(Component, string.Concat("Built SCEP MDM profile for source profile '", InputObject.Slug ?? InputObject.Id, "' targeting ", result.ServerUrl, " (UniqueId=", result.UniqueId, ", Scope=", result.Scope, ")."));
WriteObject(result);
if (string.Equals(ParameterSetName, "FromProfile", StringComparison.Ordinal))
{
WriteObject(BuildFromProfile(connection));
return;
}
WriteObject(BuildManual(connection));
}
catch (Exception exception)
{
@@ -99,11 +90,109 @@ namespace PSInfisicalAPI.Cmdlets
}
}
private static string BuildDefaultServerUrl(InfisicalConnection connection, string profileId)
private InfisicalScepMdmProfile BuildFromEnrollment(InfisicalConnection connection)
{
if (EnrollmentObject == null) { throw new InvalidOperationException("EnrollmentObject is required."); }
if (string.IsNullOrEmpty(EnrollmentObject.ApplicationId)) { throw new InvalidOperationException("EnrollmentObject.ApplicationId is required."); }
if (string.IsNullOrEmpty(EnrollmentObject.ProfileId)) { throw new InvalidOperationException("EnrollmentObject.ProfileId is required."); }
InfisicalCertificateApplicationScepEnrollment scep = EnrollmentObject.Scep;
if (scep == null) { throw new InvalidOperationException("Enrollment does not have SCEP configured."); }
string resolvedServerUrl = FirstNonEmpty(ServerUrl, scep.ScepEndpointUrl, BuildDefaultServerUrl(connection, EnrollmentObject.ApplicationId, EnrollmentObject.ProfileId));
string resolvedUniqueId = !string.IsNullOrEmpty(UniqueId) ? UniqueId : SanitizeForCspId(EnrollmentObject.ProfileId);
string resolvedThumbprint = !string.IsNullOrEmpty(CAThumbprint) ? CAThumbprint : scep.RaCertificateThumbprint;
string resolvedChallenge = ResolveChallengeFromEnrollment(connection, scep);
InfisicalScepMdmProfile result = NewProfileShell(resolvedUniqueId, resolvedServerUrl, resolvedChallenge, resolvedThumbprint, null, null);
result.SourceProfileId = EnrollmentObject.ProfileId;
Logger.Verbose(Component, string.Concat("Built SCEP MDM profile from enrollment for application '", EnrollmentObject.ApplicationId, "' / profile '", EnrollmentObject.ProfileId, "' targeting ", result.ServerUrl, " (UniqueId=", result.UniqueId, ", Scope=", result.Scope, ", ChallengeType=", scep.ChallengeType ?? "<unknown>", ")."));
return result;
}
private InfisicalScepMdmProfile BuildFromProfile(InfisicalConnection connection)
{
if (InputObject == null) { throw new InvalidOperationException("InputObject is required."); }
if (string.IsNullOrEmpty(InputObject.Id)) { throw new InvalidOperationException("InputObject.Id is required."); }
if (string.IsNullOrEmpty(ApplicationId)) { throw new InvalidOperationException("ApplicationId is required when binding by certificate profile."); }
if (Challenge == null) { throw new InvalidOperationException("Challenge is required when building from a certificate profile."); }
string resolvedServerUrl = !string.IsNullOrEmpty(ServerUrl) ? ServerUrl : BuildDefaultServerUrl(connection, ApplicationId, InputObject.Id);
string resolvedUniqueId = !string.IsNullOrEmpty(UniqueId) ? UniqueId : SanitizeForCspId(!string.IsNullOrEmpty(InputObject.Slug) ? InputObject.Slug : InputObject.Id);
InfisicalCertificateProfileDefaults defaults = InputObject.Defaults;
string resolvedKeyAlgorithm = !string.IsNullOrEmpty(KeyAlgorithm) ? KeyAlgorithm : MapKeyAlgorithm(defaults != null ? defaults.KeyAlgorithm : null);
string resolvedEku = !string.IsNullOrEmpty(EkuMapping) ? EkuMapping : JoinEkuOids(defaults != null ? defaults.ExtendedKeyUsages : null);
InfisicalScepMdmProfile result = NewProfileShell(resolvedUniqueId, resolvedServerUrl, SecureStringToPlainText(Challenge), CAThumbprint, resolvedKeyAlgorithm, resolvedEku);
result.SourceProfileId = InputObject.Id;
result.SourceProfileSlug = InputObject.Slug;
Logger.Verbose(Component, string.Concat("Built SCEP MDM profile for source profile '", InputObject.Slug ?? InputObject.Id, "' targeting ", result.ServerUrl, " (UniqueId=", result.UniqueId, ", Scope=", result.Scope, ")."));
return result;
}
private InfisicalScepMdmProfile BuildManual(InfisicalConnection connection)
{
if (string.IsNullOrEmpty(UniqueId)) { throw new InvalidOperationException("UniqueId is required in Manual mode."); }
string resolvedChallenge = SecureStringToPlainText(Challenge);
InfisicalScepMdmProfile result = NewProfileShell(UniqueId, ServerUrl, resolvedChallenge, CAThumbprint, KeyAlgorithm, EkuMapping);
Logger.Verbose(Component, string.Concat("Built SCEP MDM profile in Manual mode targeting ", result.ServerUrl, " (UniqueId=", result.UniqueId, ", Scope=", result.Scope, ")."));
return result;
}
private InfisicalScepMdmProfile NewProfileShell(string uniqueId, string serverUrl, string challenge, string thumbprint, string keyAlgorithm, string ekuMapping)
{
return new InfisicalScepMdmProfile
{
UniqueId = uniqueId,
Scope = Scope,
ServerUrl = serverUrl,
Challenge = challenge,
SubjectName = SubjectName,
SubjectAlternativeNames = SubjectAlternativeNames,
EkuMapping = ekuMapping,
KeyUsage = KeyUsage,
KeyLength = KeyLength,
KeyAlgorithm = keyAlgorithm,
HashAlgorithm = HashAlgorithm,
KeyProtection = KeyProtection,
ContainerName = ContainerName,
ValidPeriod = ValidPeriod,
ValidPeriodUnits = ValidPeriodUnits,
RetryCount = RetryCount,
RetryDelay = RetryDelay,
TemplateName = TemplateName,
CAThumbprint = thumbprint,
CustomTextToShowInPrompt = CustomTextToShowInPrompt
};
}
private string ResolveChallengeFromEnrollment(InfisicalConnection connection, InfisicalCertificateApplicationScepEnrollment scep)
{
if (Challenge != null) { return SecureStringToPlainText(Challenge); }
string challengeType = scep.ChallengeType ?? string.Empty;
if (string.Equals(challengeType, "dynamic", StringComparison.OrdinalIgnoreCase))
{
InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger);
Logger.Verbose(Component, "Minting SCEP dynamic challenge for enrollment.");
return client.GenerateScepDynamicChallenge(connection, EnrollmentObject.ApplicationId, EnrollmentObject.ProfileId);
}
throw new InvalidOperationException(string.Concat("Enrollment uses challengeType '", challengeType, "'. Supply -Challenge with the configured static challenge password."));
}
private static string BuildDefaultServerUrl(InfisicalConnection connection, string applicationId, string profileId)
{
if (connection == null || connection.BaseUri == null) { throw new InvalidOperationException("Active Infisical connection is required to derive ServerUrl."); }
string baseUrl = connection.BaseUri.GetLeftPart(UriPartial.Authority);
return string.Concat(baseUrl, "/scep/", profileId, "/pkiclient.exe");
return string.Concat(baseUrl, "/scep/applications/", applicationId, "/profiles/", profileId, "/pkiclient.exe");
}
private static string FirstNonEmpty(params string[] values)
{
if (values == null) { return null; }
foreach (string value in values) { if (!string.IsNullOrEmpty(value)) { return value; } }
return null;
}
private static string SanitizeForCspId(string input)