Private/New-BlankTemplateObject.ps1

function New-BlankTemplateObject {
    <#
        .SYNOPSIS
        Creates a new blank certificate template object in Active Directory.
 
        .DESCRIPTION
        This function creates a new blank certificate template object by adding a new pKICertificateTemplate
        object to the Certificate Templates container in the Active Directory Configuration partition.
        The template is created with minimal properties and must be further configured before use.
 
        This function uses System.DirectoryServices to directly interact with Active Directory without
        requiring additional PowerShell modules. It automatically discovers the Configuration partition
        and creates templates in the standard Certificate Templates container.
 
        .PARAMETER TemplateName
        The name(s) of the certificate template(s) to create. Must be valid LDAP common names.
        Multiple template names can be provided via pipeline input.
 
        .PARAMETER Server
        Optional. The domain controller to use for the operation. If not specified, the function
        will use the default domain controller for the current domain.
 
        .INPUTS
        System.String[]
        Template names can be provided via pipeline input.
 
        .OUTPUTS
        System.DirectoryServices.DirectoryEntry[]
        Returns the newly created certificate template DirectoryEntry objects.
 
        .EXAMPLE
        New-BlankTemplateObject -TemplateName "MyCustomTemplate"
        Creates a single blank certificate template named "MyCustomTemplate".
 
        .EXAMPLE
        "Template1", "Template2", "Template3" | New-BlankTemplateObject
        Creates multiple blank certificate templates using pipeline input.
 
        .EXAMPLE
        New-BlankTemplateObject -TemplateName "TestTemplate" -Server "dc01.contoso.com"
        Creates a blank template using a specific domain controller.
 
        .LINK
        https://docs.microsoft.com/en-us/windows/win32/adschema/c-pkicertificatetemplate
 
        .NOTES
        Requires permissions to create objects in the Certificate Templates container.
        The created templates will be blank and require additional configuration before use.
         
        WARNING: This function creates skeleton template objects that are not immediately usable.
        Additional properties must be configured before enabling templates for enrollment.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline, Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string[]]$TemplateName,
        
        [Parameter()]
        [string]$Server
    )


    begin {
        Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Starting $($MyInvocation.MyCommand) on $env:COMPUTERNAME..."
        
        # Load System.DirectoryServices assembly
        try {
            Add-Type -AssemblyName System.DirectoryServices
            Write-Verbose "System.DirectoryServices assembly loaded successfully"
        } catch {
            Write-Error "Failed to load System.DirectoryServices assembly: $($_.Exception.Message)"
            return
        }

        # Get the Configuration partition automatically via RootDSE
        try {
            if ($Server) {
                $rootDSE = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$Server/RootDSE")
                Write-Verbose "Connected to domain controller: $Server"
            } else {
                $rootDSE = New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE")
                Write-Verbose "Connected to default domain controller"
            }
            
            $configurationPartition = $rootDSE.configurationNamingContext
            Write-Verbose "Configuration Naming Context: $configurationPartition"
            
            $templatesContainer = "CN=Certificate Templates,CN=Public Key Services,CN=Services,$configurationPartition"
            Write-Verbose "Templates Container: $templatesContainer"
            
            if ($Server) {
                $templatePath = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$Server/$templatesContainer")
            } else {
                $templatePath = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$templatesContainer")
            }
            
            Write-Verbose "Successfully connected to Certificate Templates container"
        } catch {
            Write-Error "Failed to connect to Active Directory or locate Certificate Templates container: $($_.Exception.Message)"
            return
        }
    }

    process {
        foreach ($name in $TemplateName) {
            Write-Verbose "Creating certificate template: $name"
            
            $success = $false
            $currentName = $name
            $createdTemplate = $null
            
            while (-not $success) {
                try {
                    # Validate template name format
                    if ([string]::IsNullOrWhiteSpace($currentName)) {
                        throw "Template name cannot be null, empty, or whitespace"
                    }
                    
                    # Check if template already exists
                    try {
                        $existingTemplate = $templatePath.Children.Find("CN=$currentName", "pKICertificateTemplate")
                        if ($existingTemplate) {
                            throw "A certificate template named '$currentName' already exists"
                        }
                    } catch [System.DirectoryServices.DirectoryServicesCOMException] {
                        # Template doesn't exist - this is expected, continue with creation
                        Write-Verbose "Confirmed template '$currentName' does not exist"
                    }
                    
                    # Create the new template object
                    Write-Verbose "Adding new pKICertificateTemplate object: CN=$currentName"
                    $newTemplate = $templatePath.Children.Add("CN=$currentName", "pKICertificateTemplate")
                    
                    # Commit the changes to Active Directory
                    Write-Verbose "Committing template '$currentName' to Active Directory"
                    $newTemplate.CommitChanges()
                    
                    # Refresh the object to get updated properties
                    $newTemplate.RefreshCache()
                    
                    $createdTemplate = $newTemplate
                    $success = $true
                    
                    Write-Verbose "Successfully created certificate template: $currentName"
                    
                } catch [System.DirectoryServices.DirectoryServicesCOMException] {
                    $comError = $_.Exception
                    Write-Error "Failed to create template '$currentName': $($comError.Message) (HRESULT: 0x$($comError.ErrorCode.ToString('X8')))"
                    
                    # If running interactively, prompt for a new name
                    if ($Host.UI.RawUI.KeyAvailable -or $env:TERM_PROGRAM -eq "vscode") {
                        Write-Warning "Template name '$currentName' is invalid or already exists. Please enter a new name."
                        do {
                            $currentName = Read-Host -Prompt "New Template Name"
                        } while ([string]::IsNullOrWhiteSpace($currentName))
                    } else {
                        # Non-interactive mode - skip this template
                        Write-Error "Cannot create template '$currentName' in non-interactive mode. Skipping."
                        break
                    }
                    
                } catch {
                    Write-Error "Unexpected error creating template '$currentName': $($_.Exception.Message)"
                    break
                }
            }
            
            # Output the created template if successful
            if ($createdTemplate) {
                Write-Output $createdTemplate
            }
        }
    }

    end {
        # Clean up DirectoryEntry objects
        if ($templatePath) {
            $templatePath.Dispose()
            Write-Verbose "Disposed of templatePath DirectoryEntry object"
        }
        if ($rootDSE) {
            $rootDSE.Dispose()
            Write-Verbose "Disposed of rootDSE DirectoryEntry object"
        }
        
        Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Finishing $($MyInvocation.MyCommand) on $env:COMPUTERNAME..."
    }
}