From 6d9c0b957e0d3e0b5ece0f88e0922ba4a49914b2 Mon Sep 17 00:00:00 2001 From: LuigiLink Date: Fri, 10 Jul 2026 13:24:01 +0200 Subject: [PATCH 1/5] refactor: restructure scripts/ to src/ and split modules into SPSTrust.Common Align SPSTrust with the SPSUpdate / SPSCleanDependencies package layout. - Move scripts/ to src/. - Split the monolithic sps.util.psm1 / util.psm1 into a single SPSTrust.Common module (Public/ + Private/ + psm1 loader + psd1 manifest): - Public: the 16 trust SPS* functions, Get-SPSServer, and a generic Clear-SPSLogFolder (replacing the unused Clear-SPSLog). - Private: Invoke-SPSCommand (CredSSP remoting helper), hidden from callers. - Version is now the single source of truth in the manifest (ModuleVersion = 2.0.0); the script banner reads it from Get-Module instead of a hard-coded string. - Migrate config from JSON to a PowerShell data file (.psd1) with CONTOSO-PROD.example.psd1; -ConfigFile now validates *.psd1 and the script loads it via Import-PowerShellDataFile. - Wire Clear-SPSLogFolder into the cleanup phase with a new -LogRetentionDays parameter (default 180, 0 disables pruning). Refs #2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/Config/CONTOSO-PROD.json | 39 - scripts/Modules/sps.util.psm1 | 965 ------------------ scripts/Modules/util.psm1 | 142 --- src/Config/CONTOSO-PROD.example.psd1 | 57 ++ .../Private/Invoke-SPSCommand.ps1 | 68 ++ .../Public/Clear-SPSLogFolder.ps1 | 79 ++ .../Export-SPSSecurityTokenCertificate.ps1 | 45 + .../Public/Export-SPSTrustedRootAuthority.ps1 | 45 + .../SPSTrust.Common/Public/Get-SPSFarmId.ps1 | 23 + .../Get-SPSPublishedServiceAppPermission.ps1 | 63 ++ .../Get-SPSPublishedServiceAppProxy.ps1 | 46 + .../Get-SPSPublishedServiceApplication.ps1 | 53 + .../SPSTrust.Common/Public/Get-SPSServer.ps1 | 22 + .../Get-SPSTopologyServiceAppPermission.ps1 | 45 + .../Public/Get-SPSTrustedRootAuthority.ps1 | 62 ++ .../Get-SPSTrustedServiceTokenIssuer.ps1 | 62 ++ .../New-SPSPublishedServiceAppProxy.ps1 | 72 ++ .../Public/Publish-SPSServiceApplication.ps1 | 56 + .../Remove-SPSPublishedServiceAppProxy.ps1 | 39 + .../Set-SPSPublishedServiceAppPermission.ps1 | 77 ++ .../Set-SPSTopologyServiceAppPermission.ps1 | 65 ++ .../Public/Set-SPSTrustedRootAuthority.ps1 | 106 ++ .../Set-SPSTrustedServiceTokenIssuer.ps1 | 106 ++ .../SPSTrust.Common/SPSTrust.Common.psd1 | 45 + .../SPSTrust.Common/SPSTrust.Common.psm1 | 26 + {scripts => src}/SPSTrust.ps1 | 78 +- 26 files changed, 1314 insertions(+), 1172 deletions(-) delete mode 100644 scripts/Config/CONTOSO-PROD.json delete mode 100644 scripts/Modules/sps.util.psm1 delete mode 100644 scripts/Modules/util.psm1 create mode 100644 src/Config/CONTOSO-PROD.example.psd1 create mode 100644 src/Modules/SPSTrust.Common/Private/Invoke-SPSCommand.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Clear-SPSLogFolder.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Export-SPSSecurityTokenCertificate.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Export-SPSTrustedRootAuthority.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Get-SPSFarmId.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Get-SPSPublishedServiceAppPermission.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Get-SPSPublishedServiceAppProxy.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Get-SPSPublishedServiceApplication.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Get-SPSServer.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Get-SPSTopologyServiceAppPermission.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Get-SPSTrustedRootAuthority.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Get-SPSTrustedServiceTokenIssuer.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/New-SPSPublishedServiceAppProxy.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Publish-SPSServiceApplication.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Remove-SPSPublishedServiceAppProxy.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Set-SPSPublishedServiceAppPermission.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Set-SPSTopologyServiceAppPermission.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Set-SPSTrustedRootAuthority.ps1 create mode 100644 src/Modules/SPSTrust.Common/Public/Set-SPSTrustedServiceTokenIssuer.ps1 create mode 100644 src/Modules/SPSTrust.Common/SPSTrust.Common.psd1 create mode 100644 src/Modules/SPSTrust.Common/SPSTrust.Common.psm1 rename {scripts => src}/SPSTrust.ps1 (89%) diff --git a/scripts/Config/CONTOSO-PROD.json b/scripts/Config/CONTOSO-PROD.json deleted file mode 100644 index f9ec5fd..0000000 --- a/scripts/Config/CONTOSO-PROD.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema#", - "contentVersion": "1.0.0.0", - "ConfigurationName": "PROD", - "ApplicationName": "contoso", - "Domain": "contoso.com", - "CertFileShared": "\\\\srvfileshared.contoso.com\\certsfolder", - "Trusts": [ - { - "LocalFarm": "SEARCH", - "RemoteFarms": ["CONTENT", "SERVICES"], - "Services": ["CONTOSOPRODSCH"] - }, - { - "LocalFarm": "SERVICES", - "RemoteFarms": ["CONTENT", "SEARCH"], - "Services": ["CONTOSOPRODUPS", "CONTOSOPRODMMS", "CONTOSOPRODSSA"] - }, - { - "LocalFarm": "CONTENT", - "RemoteFarms": ["SEARCH", "SERVICES"], - "Services": ["Content"] - } - ], - "Farms": [ - { - "Name": "SEARCH", - "Server": "srvcontososearch" - }, - { - "Name": "SERVICES", - "Server": "srvcontososervices" - }, - { - "Name": "CONTENT", - "Server": "srvcontosocontent" - } - ] -} diff --git a/scripts/Modules/sps.util.psm1 b/scripts/Modules/sps.util.psm1 deleted file mode 100644 index a52cc2b..0000000 --- a/scripts/Modules/sps.util.psm1 +++ /dev/null @@ -1,965 +0,0 @@ -function Get-SPSFarmId { - [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount - ) - - Write-Verbose "Getting Farm ID of Farm '$Server'" - $result = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - (Get-SPFarm).ID.Guid - } - return $result -} -function Get-SPSTrustedRootAuthority { - [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter()] - [System.String] - $CertificateThumbprint, - - [Parameter()] - [System.String] - $CertificateFilePath, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount - ) - - Write-Verbose "Getting Trusted Root Authority with name '$Name'" - - if (-not ($PSBoundParameters.ContainsKey("CertificateThumbprint")) -and ` - -not($PSBoundParameters.ContainsKey("CertificateFilePath"))) { - Write-Verbose -Message ("At least one of the following parameters must be specified: " + ` - "CertificateThumbprint, CertificateFilePath.") - } - $result = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - - $rootCert = Get-SPTrustedRootAuthority -Identity $params.Name -ErrorAction SilentlyContinue - $ensure = 'Absent' - - if ($null -eq $rootCert) { - return @{ - Name = $params.Name - CertificateThumbprint = [string]::Empty - CertificateFilePath = '' - Ensure = $ensure - } - } - else { - $ensure = 'Present' - return @{ - Name = $params.Name - CertificateThumbprint = $rootCert.Certificate.Thumbprint - CertificateFilePath = '' - Ensure = $ensure - } - } - } - return $result -} -function Set-SPSTrustedRootAuthority { - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter()] - [System.String] - $CertificateThumbprint, - - [Parameter()] - [String] - $CertificateFilePath, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount, - - [Parameter()] - [ValidateSet('Present', 'Absent')] - [System.String] - $Ensure = 'Present' - ) - - Write-Verbose -Message "Setting SPTrustedRootAuthority '$Name'" - if ($Ensure -eq 'Present') { - if (-not ($PSBoundParameters.ContainsKey("CertificateThumbprint")) -and ` - -not($PSBoundParameters.ContainsKey("CertificateFilePath"))) { - $message = ("At least one of the following parameters must be specified: " + ` - "CertificateThumbprint, CertificateFilePath.") - throw $message - } - - if ($PSBoundParameters.ContainsKey("CertificateFilePath") -and ` - -not ($PSBoundParameters.ContainsKey("CertificateThumbprint"))) { - if (-not (Test-Path -Path $CertificateFilePath)) { - $message = ("Specified CertificateFilePath does not exist: $CertificateFilePath") - throw $message - } - } - } - - $null = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments @($PSBoundParameters, $MyInvocation.MyCommand.Source) ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - - if ($params.Ensure -eq 'Absent') { - Write-Verbose -Message "Removing SPTrustedRootAuthority '$params.Name'" - Remove-SPTrustedRootAuthority -Identity $params.Name -Confirm:$false -ErrorAction SilentlyContinue - } - else { - if ($params.ContainsKey("CertificateFilePath")) { - Write-Verbose -Message "Importing certificate from CertificateFilePath" - try { - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 - $cert.Import($params.CertificateFilePath) - } - catch { - $message = "An error occured: $($_.Exception.Message)" - throw $message - } - - if ($null -eq $cert) { - $message = "Import of certificate failed." - throw $message - } - - if ($params.ContainsKey("CertificateThumbprint")) { - if (-not $params.CertificateThumbprint.Equals($cert.Thumbprint)) { - $message = "Imported certificate thumbprint ($($cert.Thumbprint)) does not match expected thumbprint ($($params.CertificateThumbprint))." - throw $message - } - } - } - else { - Write-Verbose -Message "Importing certificate from CertificateThumbprint" - $cert = Get-Item -Path "CERT:\LocalMachine\My\$($params.CertificateThumbprint)" ` - -ErrorAction SilentlyContinue - - if ($null -eq $cert) { - $message = "Certificate not found in the local Certificate Store" - throw $message - } - } - - if ($cert.HasPrivateKey) { - Write-Verbose -Message "Certificate has private key. Removing private key." - $pubKeyBytes = $cert.Export("cert") - $cert2 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 - $cert2.Import($pubKeyBytes) - $cert = $cert2 - } - - Write-Verbose -Message "Updating Root Authority" - New-SPTrustedRootAuthority -Name $params.Name -Certificate $cert - } - } -} -function Get-SPSTrustedServiceTokenIssuer { - [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter()] - [System.String] - $CertificateThumbprint, - - [Parameter()] - [System.String] - $CertificateFilePath, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount - ) - - Write-Verbose "Getting Trusted Service Token Issuer with name '$Name'" - - if (-not ($PSBoundParameters.ContainsKey("CertificateThumbprint")) -and ` - -not($PSBoundParameters.ContainsKey("CertificateFilePath"))) { - Write-Verbose -Message ("At least one of the following parameters must be specified: " + ` - "CertificateThumbprint, CertificateFilePath.") - } - $result = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - - $rootCert = Get-SPTrustedServiceTokenIssuer -Identity $params.Name -ErrorAction SilentlyContinue - $ensure = 'Absent' - - if ($null -eq $rootCert) { - return @{ - Name = $params.Name - CertificateThumbprint = [string]::Empty - CertificateFilePath = '' - Ensure = $ensure - } - } - else { - $ensure = 'Present' - return @{ - Name = $params.Name - CertificateThumbprint = $rootCert.Certificate.Thumbprint - CertificateFilePath = '' - Ensure = $ensure - } - } - } - return $result -} -function Set-SPSTrustedServiceTokenIssuer { - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter()] - [System.String] - $CertificateThumbprint, - - [Parameter()] - [String] - $CertificateFilePath, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount, - - [Parameter()] - [ValidateSet('Present', 'Absent')] - [System.String] - $Ensure = 'Present' - ) - - Write-Verbose -Message "Setting SPTrustedServiceTokenIssuer '$Name'" - if ($Ensure -eq 'Present') { - if (-not ($PSBoundParameters.ContainsKey("CertificateThumbprint")) -and ` - -not($PSBoundParameters.ContainsKey("CertificateFilePath"))) { - $message = ("At least one of the following parameters must be specified: " + ` - "CertificateThumbprint, CertificateFilePath.") - throw $message - } - - if ($PSBoundParameters.ContainsKey("CertificateFilePath") -and ` - -not ($PSBoundParameters.ContainsKey("CertificateThumbprint"))) { - if (-not (Test-Path -Path $CertificateFilePath)) { - $message = ("Specified CertificateFilePath does not exist: $CertificateFilePath") - throw $message - } - } - } - - $null = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments @($PSBoundParameters, $MyInvocation.MyCommand.Source) ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - - if ($params.Ensure -eq 'Absent') { - Write-Verbose -Message "Removing SPTrustedIdentityTokenIssuer '$params.Name'" - Remove-SPTrustedIdentityTokenIssuer -Identity $params.Name -Confirm:$false -ErrorAction SilentlyContinue - } - else { - if ($params.ContainsKey("CertificateFilePath")) { - Write-Verbose -Message "Importing certificate from CertificateFilePath" - try { - $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 - $cert.Import($params.CertificateFilePath) - } - catch { - $message = "An error occured: $($_.Exception.Message)" - throw $message - } - - if ($null -eq $cert) { - $message = "Import of certificate failed." - throw $message - } - - if ($params.ContainsKey("CertificateThumbprint")) { - if (-not $params.CertificateThumbprint.Equals($cert.Thumbprint)) { - $message = "Imported certificate thumbprint ($($cert.Thumbprint)) does not match expected thumbprint ($($params.CertificateThumbprint))." - throw $message - } - } - } - else { - Write-Verbose -Message "Importing certificate from CertificateThumbprint" - $cert = Get-Item -Path "CERT:\LocalMachine\My\$($params.CertificateThumbprint)" ` - -ErrorAction SilentlyContinue - - if ($null -eq $cert) { - $message = "Certificate not found in the local Certificate Store" - throw $message - } - } - - if ($cert.HasPrivateKey) { - Write-Verbose -Message "Certificate has private key. Removing private key." - $pubKeyBytes = $cert.Export("cert") - $cert2 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 - $cert2.Import($pubKeyBytes) - $cert = $cert2 - } - - Write-Verbose -Message "Adding STS SPTrustedServiceTokenIssuer" - New-SPTrustedServiceTokenIssuer -Name $params.Name -Certificate $cert - } - } -} -function Export-SPSTrustedRootAuthority { - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter()] - [String] - $CertificateFilePath, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount - ) - - Write-Verbose -Message "Exporting SPTrustedRootAuthority '$Name'" - - $null = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments @($PSBoundParameters, $MyInvocation.MyCommand.Source) ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - $trustType = 'ROOT' - $certPath = "$($params.CertificateFilePath)\$($params.Name)_$($trustType).cer" - $certRoot = Get-SPCertificateAuthority -ErrorAction SilentlyContinue - - if ($null -eq $certRoot) { - $message = "ROOT Certificate not found in Farm $params.Name" - throw $message - } - else { - $spRootCertificate = $certRoot.RootCertificate - [byte[]]$rawcert = $spRootCertificate.RawData - - Write-Verbose -Message "Saving SPTrustedRootAuthority to '$certPath'" - $rawcert | Set-Content -Path $certPath -Encoding Byte; - } - } -} -function Get-SPSPublishedServiceApplication { - [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount - ) - - Write-Verbose -Message "Getting service application publish status '$Name'" - - $result = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - - $serviceApp = Get-SPServiceApplication -Name $params.Name -ErrorAction SilentlyContinue - if ($null -eq $serviceApp) { - Write-Verbose -Message "The service application $Name does not exist" - $sharedEnsure = "Absent" - } - if ($null -eq $serviceApp.Uri) { - Write-Verbose -Message ("Only Business Data Connectivity, Machine Translation, Managed Metadata, " + ` - "User Profile, Search, Secure Store are supported to be published via DSC.") - $sharedEnsure = "Absent" - } - else { - if ($serviceApp.Shared -eq $true) { - $sharedEnsure = "Present" - } - elseif ($serviceApp.Shared -eq $false) { - $sharedEnsure = "Absent" - } - } - return @{ - Name = $params.Name - Ensure = $sharedEnsure - Uri = $serviceApp.Uri.tostring() - Type = $serviceApp.GetType().FullName - } - } - return $result -} -function Get-SPSPublishedServiceAppProxy { - [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount - ) - - Write-Verbose -Message "Getting service application proxy '$Name'" - - $result = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - - $serviceAppProxies = Get-SPServiceApplicationProxy -ErrorAction SilentlyContinue - if ($null -ne $serviceAppProxies) { - $serviceAppProxy = $serviceAppProxies | Where-Object -FilterScript { - $_.Name -eq $params.Name - } - if ($null -ne $serviceAppProxy) { - $currentEnsure = 'Present' - } - else { - Write-Verbose -Message "The service application proxy $Name does not exist" - $currentEnsure = 'Absent' - } - } - return @{ - Name = $params.Name - Ensure = $currentEnsure - } - } - return $result -} -function New-SPSPublishedServiceAppProxy { - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $ServiceUri, - - [Parameter(Mandatory = $true)] - [System.String] - $ServiceType, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount - ) - - Write-Verbose -Message "Adding service application proxy '$Name' on '$Server'" - Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - - Receive-SPServiceApplicationConnectionInfo -FarmUrl $params.ServiceUri | Out-Null - - switch ($params.ServiceType) { - { $_.contains('SearchServiceApplication') } { - New-SPEnterpriseSearchServiceApplicationProxy -Name $params.Name ` - -Uri $params.ServiceUri ` - -Verbose - } - { $_.contains('UserProfileApplication') } { - New-SPProfileServiceApplicationProxy -Name $params.Name ` - -Uri $params.ServiceUri ` - -DefaultProxyGroup ` - -Verbose - } - { $_.contains('MetadataWebServiceApplication') } { - New-SPMetadataServiceApplicationProxy -Name $params.Name ` - -Uri $params.ServiceUri ` - -DefaultProxyGroup ` - -Verbose - $mmsProxy = Get-SPMetadataServiceApplicationProxy $params.Name - $mmsProxy.Properties.IsDefaultSiteCollectionTaxonomy = $true - $mmsProxy.Properties.IsContentTypePushdownEnabled = $true - $mmsProxy.Properties.IsDefaultKeywordTaxonomy = $true - $mmsProxy.Update() - } - { $_.contains('SPSecurityTokenServiceApplication') } { - New-SPSecureStoreServiceApplicationProxy -Name $params.Name ` - -Uri $params.ServiceUri ` - -DefaultProxyGroup ` - -Verbose - } - { $_.contains('TranslationServiceApplication') } { - New-SPTranslationServiceApplicationProxy -Name $params.Name ` - -Uri $params.ServiceUri ` - -DefaultProxyGroup ` - -Verbose - } - } - } -} -function Remove-SPSPublishedServiceAppProxy { - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount - ) - - Write-Verbose -Message "Removing service application proxy '$Name' on '$Server'" - Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - - $serviceAppProxies = Get-SPServiceApplicationProxy -ErrorAction SilentlyContinue - if ($null -ne $serviceAppProxies) { - $serviceAppProxy = $serviceAppProxies | Where-Object -FilterScript { - $_.Name -eq $params.Name - } - if ($null -ne $serviceAppProxy) { - Remove-SPServiceApplicationProxy $serviceAppProxy -RemoveData -Confirm:$false -Verbose - Write-Verbose -Message "The service application proxy $($params.Name) was successfully removed" - } - else { - Write-Verbose -Message "The service application proxy $($params.Name) does not exist" - } - } - } -} -function Publish-SPSServiceApplication { - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount, - - [Parameter()] - [ValidateSet('Present', 'Absent')] - [System.String] - $Ensure = 'Present' - ) - - Write-Verbose -Message "Setting service application publish status '$Name'" - Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - - $serviceApp = Get-SPServiceApplication -Name $params.Name -ErrorAction SilentlyContinue - if ($null -eq $serviceApp) { - $message = ("The service application $($params.Name) does not exist") - throw $message - } - - if ($null -eq $serviceApp.Uri) { - $message = ("Only Business Data Connectivity, Machine Translation, Managed Metadata, " + ` - "User Profile, Search, Secure Store are supported to be published via DSC.") - throw $message - } - - if ($params.Ensure -eq 'Present') { - Write-Verbose -Message "Publishing Service Application $($params.Name)" - if ($serviceApp.DefaultEndpoint.DisplayName -eq 'http') { - $httpsEndpoint = $serviceApp.Endpoints | Where-Object -FilterScript { $_.DisplayName -eq 'https' } - Set-SPServiceApplication $serviceApp -DefaultEndpoint $httpsEndpoint -Confirm:$false -Verbose - } - Publish-SPServiceApplication -Identity $serviceApp -Verbose - } - - if ($params.Ensure -eq 'Absent') { - Write-Verbose -Message "Unpublishing Service Application $($params.Name)" - Unpublish-SPServiceApplication -Identity $serviceApp -Verbose - } - } -} -function Get-SPSTopologyServiceAppPermission { - [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $FarmId, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount - ) - - Write-Verbose -Message "Getting Topology Service permissions for FarmID '$FarmId'" - $result = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - $ensure = 'Absent' - - $security = Get-SPTopologyServiceApplication | Get-SPServiceApplicationSecurity - $getAccessRule = $security.AccessRules | Where-Object -FilterScript { $_.Name -eq "c:0%.c|system|$($params.FarmId)" } - - if ($null -eq $getAccessRule) { - return @{ - Server = $params.Server - Ensure = $ensure - } - } - else { - $ensure = 'Present' - return @{ - Server = $params.Server - Ensure = $ensure - } - } - } - return $result -} -function Set-SPSTopologyServiceAppPermission { - [CmdletBinding()] - param - ( - # Farm ID for which the permissions are being set - [Parameter(Mandatory = $true)] - [System.String] - $FarmId, - - # Server where the command will be executed - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - # Credentials to use for executing the command - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount, - - # Ensure parameter to specify whether to add (Present) or remove (Absent) permissions - [Parameter()] - [ValidateSet('Present', 'Absent')] - [System.String] - $Ensure = 'Present' - ) - - Write-Verbose -Message "Setting Topology Service permissions for FarmID '$FarmId'" - - # Invoke the command on the specified server with the provided credentials - Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - - # Get the security settings for the Topology Service Application - $security = Get-SPTopologyServiceApplication | Get-SPServiceApplicationSecurity - - # Get the claim provider and define the claim type for the Farm ID - $claimProvider = (Get-SPClaimProvider System).ClaimProvider - $claimType = 'http://schemas.microsoft.com/sharepoint/2009/08/claims/farmid' - - # Create a new claims principal for the Farm ID - $principal = New-SPClaimsPrincipal -ClaimType $claimType ` - -ClaimProvider $claimProvider ` - -ClaimValue $($params.FarmId) - - # Grant or revoke permissions based on the Ensure parameter - if ($params.Ensure -eq 'Present') { - Grant-SPObjectSecurity -Identity $security ` - -Principal $principal ` - -Rights 'Full Control' ` - -Verbose - } - elseif ($params.Ensure -eq 'Absent') { - Revoke-SPObjectSecurity -Identity $security ` - -Principal $principal ` - -Verbose - } - - # Apply the updated security settings to the Topology Service Application - Get-SPTopologyServiceApplication | Set-SPServiceApplicationSecurity -ObjectSecurity $security -Verbose - } -} - -function Get-SPSPublishedServiceAppPermission { - [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $FarmId, - - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount - ) - - Write-Verbose -Message "Getting Topology Service permissions for FarmID '$FarmId'" - $result = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - $ensure = 'Absent' - - $serviceApp = Get-SPServiceApplication -Name $params.Name -ErrorAction SilentlyContinue - if ($null -eq $serviceApp) { - $message = ("The service application $($params.Name) does not exist") - throw $message - } - - if ($null -eq $serviceApp.Uri) { - $message = ("Only Business Data Connectivity, Machine Translation, Managed Metadata, " + ` - "User Profile, Search, Secure Store are supported to be published via DSC.") - throw $message - } - - $security = Get-SPServiceApplication $serviceApp | Get-SPServiceApplicationSecurity - $getAccessRule = $security.AccessRules | Where-Object -FilterScript { $_.Name -eq "c:0%.c|system|$($params.FarmId)" } - - if ($null -eq $getAccessRule) { - return @{ - Server = $params.Server - Name = $params.Name - Ensure = $ensure - } - } - else { - $ensure = 'Present' - return @{ - Server = $params.Server - Name = $params.Name - Ensure = $ensure - } - } - } - return $result -} -function Set-SPSPublishedServiceAppPermission { - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $FarmId, - - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount, - - # Ensure parameter to specify whether to add (Present) or remove (Absent) permissions - [Parameter()] - [ValidateSet('Present', 'Absent')] - [System.String] - $Ensure = 'Present' - ) - - Write-Verbose -Message "Setting Topology Service permissions for FarmID '$FarmId'" - Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - - $serviceApp = Get-SPServiceApplication -Name $params.Name -ErrorAction SilentlyContinue - if ($null -eq $serviceApp) { - $message = ("The service application $($params.Name) does not exist") - throw $message - } - - if ($null -eq $serviceApp.Uri) { - $message = ("Only Business Data Connectivity, Machine Translation, Managed Metadata, " + ` - "User Profile, Search, Secure Store are supported to be published via DSC.") - throw $message - } - - if ($serviceApp.GetType().ToString() -eq 'Microsoft.Office.Server.Administration.UserProfileApplication') { - Write-Verbose -Message 'The User Profile Application requires domain credentials for connection access' - Write-Verbose -Message 'Check that web app pool account exists in the connect permissions' - } - else { - $security = Get-SPServiceApplication $serviceApp | Get-SPServiceApplicationSecurity - $claimProvider = (Get-SPClaimProvider System).ClaimProvider - $claimType = 'http://schemas.microsoft.com/sharepoint/2009/08/claims/farmid' - $spoRights = $security.NamedAccessRights.Name - $principal = New-SPClaimsPrincipal -ClaimType $claimType ` - -ClaimProvider $claimProvider ` - -ClaimValue $($params.FarmId) - - # Grant or revoke permissions based on the Ensure parameter - if ($params.Ensure -eq 'Present') { - Grant-SPObjectSecurity -Identity $security ` - -Principal $principal ` - -Rights $spoRights ` - -Verbose - } - elseif ($params.Ensure -eq 'Absent') { - Revoke-SPObjectSecurity -Identity $security ` - -Principal $principal ` - -Verbose - } - - # Apply the updated security settings to the Targeted Service Application - Set-SPServiceApplicationSecurity $serviceApp -ObjectSecurity $security -Verbose - } - } -} -function Export-SPSSecurityTokenCertificate { - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [System.String] - $Server, - - [Parameter()] - [String] - $CertificateFilePath, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $InstallAccount - ) - - Write-Verbose -Message "Exporting SPSecurityTokenCertificate '$Name'" - - $null = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments @($PSBoundParameters, $MyInvocation.MyCommand.Source) ` - -Server $Server ` - -ScriptBlock { - $params = $args[0] - $trustType = 'STS' - $certPath = "$($params.CertificateFilePath)\$($params.Name)_$($trustType).cer" - $certSTS = Get-SPSecurityTokenServiceConfig -ErrorAction SilentlyContinue - - if ($null -eq $certSTS) { - $message = "STS Certificate not found in Farm $params.Name" - throw $message - } - else { - $spSTSCertificate = $certSTS.LocalLoginProvider.SigningCertificate - [byte[]]$rawcert = $spSTSCertificate.RawData - - Write-Verbose -Message "Saving SPSecurityTokenCertificate to '$certPath'" - $rawcert | Set-Content -Path $certPath -Encoding Byte; - } - } -} diff --git a/scripts/Modules/util.psm1 b/scripts/Modules/util.psm1 deleted file mode 100644 index 98380c8..0000000 --- a/scripts/Modules/util.psm1 +++ /dev/null @@ -1,142 +0,0 @@ -#region Import Modules -# Import the custom module 'sps.util.psm1' from the script's directory -$scriptModulePath = Split-Path -Parent $MyInvocation.MyCommand.Definition -Import-Module -Name (Join-Path -Path $scriptModulePath -ChildPath 'sps.util.psm1') -Force -#endregion - -function Invoke-SPSCommand { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $Credential, # Credential to be used for executing the command - [Parameter()] - [Object[]] - $Arguments, # Optional arguments for the script block - [Parameter(Mandatory = $true)] - [ScriptBlock] - $ScriptBlock, # Script block containing the commands to execute - [Parameter(Mandatory = $true)] - [System.String] - $Server # Target server where the commands will be executed - ) - $VerbosePreference = 'Continue' - # Base script to ensure the SharePoint snap-in is loaded - $baseScript = @" - if (`$null -eq (Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue)) - { - Add-PSSnapin Microsoft.SharePoint.PowerShell - } -"@ - - $invokeArgs = @{ - ScriptBlock = [ScriptBlock]::Create($baseScript + $ScriptBlock.ToString()) - } - # Add arguments if provided - if ($null -ne $Arguments) { - $invokeArgs.Add("ArgumentList", $Arguments) - } - # Ensure a credential is provided - if ($null -eq $Credential) { - throw 'You need to specify a Credential' - } - else { - Write-Verbose -Message ("Executing using a provided credential and local PSSession " + "as user $($Credential.UserName)") - # Running garbage collection to resolve issues related to Azure DSC extension use - [GC]::Collect() - # Create a new PowerShell session on the target server using the provided credentials - $session = New-PSSession -ComputerName $Server ` - -Credential $Credential ` - -Authentication CredSSP ` - -Name "Microsoft.SharePoint.PSSession" ` - -SessionOption (New-PSSessionOption -OperationTimeout 0 -IdleTimeout 60000) ` - -ErrorAction Continue - - # Add the session to the invocation arguments if the session is created successfully - if ($session) { - $invokeArgs.Add("Session", $session) - } - try { - # Invoke the command on the target server - return Invoke-Command @invokeArgs -Verbose - } - catch { - throw $_ # Throw any caught exceptions - } - finally { - # Remove the session to clean up - if ($session) { - Remove-PSSession -Session $session - } - } - } -} - -function Get-SPSServer { - [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] - param ( - [Parameter(Mandatory = $true)] - [System.String] - $Server, # Name of the SharePoint server - [Parameter()] - [System.Management.Automation.PSCredential] - $InstallAccount # Credential for accessing the SharePoint server - ) - Write-Verbose "Getting SharePoint Servers of Farm '$Server'" - # Use the Invoke-SPSCommand function to get SharePoint servers - $result = Invoke-SPSCommand -Credential $InstallAccount ` - -Arguments $PSBoundParameters ` - -Server $Server ` - -ScriptBlock { - (Get-SPServer | Where-Object -FilterScript { $_.Role -ne 'Invalid' }).Name - } - return $result -} - -function Clear-SPSLog { - param ( - [Parameter(Mandatory = $true)] - [System.String] - $path, # Path to the log files - [Parameter()] - [System.UInt32] - $Retention = 180 # Number of days to retain log files - ) - # Check if the log file path exists - if (Test-Path $path) { - # Get the current date - $Now = Get-Date - # Define LastWriteTime parameter based on $Retention - $LastWrite = $Now.AddDays(-$Retention) - # Get files based on last write filter and specified folder - $files = Get-ChildItem -Path $path -Filter "$($logFileName)*" | Where-Object -FilterScript { - $_.LastWriteTime -le "$LastWrite" - } - # If files are found, proceed to delete them - if ($files) { - Write-Output '--------------------------------------------------------------' - Write-Output "Cleaning log files in $path ..." - foreach ($file in $files) { - if ($null -ne $file) { - Write-Output "Deleting file $file ..." - Remove-Item $file.FullName | Out-Null - } - else { - Write-Output 'No more log files to delete' - Write-Output '--------------------------------------------------------------' - } - } - } - else { - Write-Output '--------------------------------------------------------------' - Write-Output "$path - No needs to delete log files" - Write-Output '--------------------------------------------------------------' - } - } - else { - Write-Output '--------------------------------------------------------------' - Write-Output "$path does not exist" - Write-Output '--------------------------------------------------------------' - } -} diff --git a/src/Config/CONTOSO-PROD.example.psd1 b/src/Config/CONTOSO-PROD.example.psd1 new file mode 100644 index 0000000..cf00f99 --- /dev/null +++ b/src/Config/CONTOSO-PROD.example.psd1 @@ -0,0 +1,57 @@ +@{ + # SPSTrust configuration file (PowerShell data file). + # Copy this example to '-.psd1' (e.g. CONTOSO-PROD.psd1) + # and adjust the values to match your environment, then run: + # .\SPSTrust.ps1 -ConfigFile '.\Config\CONTOSO-PROD.psd1' -FarmAccount (Get-Credential) + + # Logical environment name (e.g. PROD, PPRD, DEV). Used in log file names. + ConfigurationName = 'PROD' + + # Application / customer short name. Used in log file names. + ApplicationName = 'contoso' + + # DNS domain suffix appended to each farm 'Server' value to build the FQDN. + Domain = 'contoso.com' + + # UNC path to the shared folder used to exchange STS/ROOT certificates between farms. + CertFileShared = '\\srvfileshared.contoso.com\certsfolder' + + # Trust relationships. For each local (publishing) farm, list the remote + # (consuming) farms and the service applications to publish/consume. + # Use the service name 'Content' for a content farm that only exchanges + # ROOT trust (no STS, no published service application). + Trusts = @( + @{ + LocalFarm = 'SEARCH' + RemoteFarms = @('CONTENT', 'SERVICES') + Services = @('CONTOSOPRODSCH') + } + @{ + LocalFarm = 'SERVICES' + RemoteFarms = @('CONTENT', 'SEARCH') + Services = @('CONTOSOPRODUPS', 'CONTOSOPRODMMS', 'CONTOSOPRODSSA') + } + @{ + LocalFarm = 'CONTENT' + RemoteFarms = @('SEARCH', 'SERVICES') + Services = @('Content') + } + ) + + # Farm inventory. 'Name' is the logical farm name referenced by Trusts; + # 'Server' is the short host name of a server in that farm (Domain is appended). + Farms = @( + @{ + Name = 'SEARCH' + Server = 'srvcontososearch' + } + @{ + Name = 'SERVICES' + Server = 'srvcontososervices' + } + @{ + Name = 'CONTENT' + Server = 'srvcontosocontent' + } + ) +} diff --git a/src/Modules/SPSTrust.Common/Private/Invoke-SPSCommand.ps1 b/src/Modules/SPSTrust.Common/Private/Invoke-SPSCommand.ps1 new file mode 100644 index 0000000..ccbafb7 --- /dev/null +++ b/src/Modules/SPSTrust.Common/Private/Invoke-SPSCommand.ps1 @@ -0,0 +1,68 @@ +function Invoke-SPSCommand { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, # Credential to be used for executing the command + [Parameter()] + [Object[]] + $Arguments, # Optional arguments for the script block + [Parameter(Mandatory = $true)] + [ScriptBlock] + $ScriptBlock, # Script block containing the commands to execute + [Parameter(Mandatory = $true)] + [System.String] + $Server # Target server where the commands will be executed + ) + $VerbosePreference = 'Continue' + # Base script to ensure the SharePoint snap-in is loaded + $baseScript = @" + if (`$null -eq (Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue)) + { + Add-PSSnapin Microsoft.SharePoint.PowerShell + } +"@ + + $invokeArgs = @{ + ScriptBlock = [ScriptBlock]::Create($baseScript + $ScriptBlock.ToString()) + } + # Add arguments if provided + if ($null -ne $Arguments) { + $invokeArgs.Add("ArgumentList", $Arguments) + } + # Ensure a credential is provided + if ($null -eq $Credential) { + throw 'You need to specify a Credential' + } + else { + Write-Verbose -Message ("Executing using a provided credential and local PSSession " + "as user $($Credential.UserName)") + # Running garbage collection to resolve issues related to Azure DSC extension use + [GC]::Collect() + # Create a new PowerShell session on the target server using the provided credentials + $session = New-PSSession -ComputerName $Server ` + -Credential $Credential ` + -Authentication CredSSP ` + -Name "Microsoft.SharePoint.PSSession" ` + -SessionOption (New-PSSessionOption -OperationTimeout 0 -IdleTimeout 60000) ` + -ErrorAction Continue + + # Add the session to the invocation arguments if the session is created successfully + if ($session) { + $invokeArgs.Add("Session", $session) + } + try { + # Invoke the command on the target server + return Invoke-Command @invokeArgs -Verbose + } + catch { + throw $_ # Throw any caught exceptions + } + finally { + # Remove the session to clean up + if ($session) { + Remove-PSSession -Session $session + } + } + } +} + diff --git a/src/Modules/SPSTrust.Common/Public/Clear-SPSLogFolder.ps1 b/src/Modules/SPSTrust.Common/Public/Clear-SPSLogFolder.ps1 new file mode 100644 index 0000000..6b31579 --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Clear-SPSLogFolder.ps1 @@ -0,0 +1,79 @@ +function Clear-SPSLogFolder { + <# + .SYNOPSIS + Deletes old files from a folder based on a retention window. + + .DESCRIPTION + Clear-SPSLogFolder removes files older than the requested number of days from + the specified folder (recursively). The retention window is evaluated against + each file's LastWriteTime. It is the toolkit's single rotation implementation, + reused for both the transcript logs (Logs\) and the archived results snapshots + (Results\history\, via -Extension '*.json'). + + A Retention of 0 disables pruning (nothing is deleted). The function emits + banner lines on stdout so it stays visible inside the Start-Transcript output + produced by SPSTrust. + + .PARAMETER Path + Directory to scan. Subdirectories are scanned recursively. + + .PARAMETER Retention + Number of days to keep. Files older than this are deleted. Defaults to 90 days. + A value of 0 disables pruning. + + .PARAMETER Extension + File name pattern to filter on. Defaults to '*.log'. + + .EXAMPLE + Clear-SPSLogFolder -Path 'D:\Tools\Logs' -Retention 30 + + .EXAMPLE + Clear-SPSLogFolder -Path $historyFolder -Retention 30 -Extension '*.json' + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter()] + [System.UInt32] + $Retention = 90, + + [Parameter()] + [System.String] + $Extension = '*.log' + ) + + if ($Retention -eq 0) { + Write-Verbose -Message 'Clear-SPSLogFolder: retention disabled (Retention = 0), nothing to prune.' + return + } + + if (-not (Test-Path -Path $Path)) { + return + } + + $lastWrite = (Get-Date).AddDays(-$Retention) + + $files = Get-ChildItem -Path $Path -Include $Extension -Recurse -File -ErrorAction SilentlyContinue | + Where-Object -FilterScript { $_.LastWriteTime -le $lastWrite } + + Write-Output '--------------------------------------------------------------' + if ($files) { + Write-Output "Cleaning files ($Extension) older than $Retention days in $Path ..." + foreach ($file in $files) { + if ($null -ne $file) { + if ($PSCmdlet.ShouldProcess($file.FullName, 'Remove file')) { + Write-Output "Deleting file $($file.FullName) ..." + Remove-Item -Path $file.FullName -Force -ErrorAction SilentlyContinue | Out-Null + } + } + } + } + else { + Write-Output "$Path - No files ($Extension) to delete" + } + Write-Output '--------------------------------------------------------------' +} diff --git a/src/Modules/SPSTrust.Common/Public/Export-SPSSecurityTokenCertificate.ps1 b/src/Modules/SPSTrust.Common/Public/Export-SPSSecurityTokenCertificate.ps1 new file mode 100644 index 0000000..920bd1e --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Export-SPSSecurityTokenCertificate.ps1 @@ -0,0 +1,45 @@ +function Export-SPSSecurityTokenCertificate { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter()] + [String] + $CertificateFilePath, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + Write-Verbose -Message "Exporting SPSecurityTokenCertificate '$Name'" + + $null = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments @($PSBoundParameters, $MyInvocation.MyCommand.Source) ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + $trustType = 'STS' + $certPath = "$($params.CertificateFilePath)\$($params.Name)_$($trustType).cer" + $certSTS = Get-SPSecurityTokenServiceConfig -ErrorAction SilentlyContinue + + if ($null -eq $certSTS) { + $message = "STS Certificate not found in Farm $params.Name" + throw $message + } + else { + $spSTSCertificate = $certSTS.LocalLoginProvider.SigningCertificate + [byte[]]$rawcert = $spSTSCertificate.RawData + + Write-Verbose -Message "Saving SPSecurityTokenCertificate to '$certPath'" + $rawcert | Set-Content -Path $certPath -Encoding Byte; + } + } +} diff --git a/src/Modules/SPSTrust.Common/Public/Export-SPSTrustedRootAuthority.ps1 b/src/Modules/SPSTrust.Common/Public/Export-SPSTrustedRootAuthority.ps1 new file mode 100644 index 0000000..1661b60 --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Export-SPSTrustedRootAuthority.ps1 @@ -0,0 +1,45 @@ +function Export-SPSTrustedRootAuthority { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter()] + [String] + $CertificateFilePath, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + Write-Verbose -Message "Exporting SPTrustedRootAuthority '$Name'" + + $null = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments @($PSBoundParameters, $MyInvocation.MyCommand.Source) ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + $trustType = 'ROOT' + $certPath = "$($params.CertificateFilePath)\$($params.Name)_$($trustType).cer" + $certRoot = Get-SPCertificateAuthority -ErrorAction SilentlyContinue + + if ($null -eq $certRoot) { + $message = "ROOT Certificate not found in Farm $params.Name" + throw $message + } + else { + $spRootCertificate = $certRoot.RootCertificate + [byte[]]$rawcert = $spRootCertificate.RawData + + Write-Verbose -Message "Saving SPTrustedRootAuthority to '$certPath'" + $rawcert | Set-Content -Path $certPath -Encoding Byte; + } + } +} diff --git a/src/Modules/SPSTrust.Common/Public/Get-SPSFarmId.ps1 b/src/Modules/SPSTrust.Common/Public/Get-SPSFarmId.ps1 new file mode 100644 index 0000000..0c4c27d --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Get-SPSFarmId.ps1 @@ -0,0 +1,23 @@ +function Get-SPSFarmId { + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + Write-Verbose "Getting Farm ID of Farm '$Server'" + $result = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + (Get-SPFarm).ID.Guid + } + return $result +} diff --git a/src/Modules/SPSTrust.Common/Public/Get-SPSPublishedServiceAppPermission.ps1 b/src/Modules/SPSTrust.Common/Public/Get-SPSPublishedServiceAppPermission.ps1 new file mode 100644 index 0000000..999efe8 --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Get-SPSPublishedServiceAppPermission.ps1 @@ -0,0 +1,63 @@ +function Get-SPSPublishedServiceAppPermission { + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $FarmId, + + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + Write-Verbose -Message "Getting Topology Service permissions for FarmID '$FarmId'" + $result = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + $ensure = 'Absent' + + $serviceApp = Get-SPServiceApplication -Name $params.Name -ErrorAction SilentlyContinue + if ($null -eq $serviceApp) { + $message = ("The service application $($params.Name) does not exist") + throw $message + } + + if ($null -eq $serviceApp.Uri) { + $message = ("Only Business Data Connectivity, Machine Translation, Managed Metadata, " + ` + "User Profile, Search, Secure Store are supported to be published via DSC.") + throw $message + } + + $security = Get-SPServiceApplication $serviceApp | Get-SPServiceApplicationSecurity + $getAccessRule = $security.AccessRules | Where-Object -FilterScript { $_.Name -eq "c:0%.c|system|$($params.FarmId)" } + + if ($null -eq $getAccessRule) { + return @{ + Server = $params.Server + Name = $params.Name + Ensure = $ensure + } + } + else { + $ensure = 'Present' + return @{ + Server = $params.Server + Name = $params.Name + Ensure = $ensure + } + } + } + return $result +} diff --git a/src/Modules/SPSTrust.Common/Public/Get-SPSPublishedServiceAppProxy.ps1 b/src/Modules/SPSTrust.Common/Public/Get-SPSPublishedServiceAppProxy.ps1 new file mode 100644 index 0000000..95d04ff --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Get-SPSPublishedServiceAppProxy.ps1 @@ -0,0 +1,46 @@ +function Get-SPSPublishedServiceAppProxy { + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + Write-Verbose -Message "Getting service application proxy '$Name'" + + $result = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + + $serviceAppProxies = Get-SPServiceApplicationProxy -ErrorAction SilentlyContinue + if ($null -ne $serviceAppProxies) { + $serviceAppProxy = $serviceAppProxies | Where-Object -FilterScript { + $_.Name -eq $params.Name + } + if ($null -ne $serviceAppProxy) { + $currentEnsure = 'Present' + } + else { + Write-Verbose -Message "The service application proxy $Name does not exist" + $currentEnsure = 'Absent' + } + } + return @{ + Name = $params.Name + Ensure = $currentEnsure + } + } + return $result +} diff --git a/src/Modules/SPSTrust.Common/Public/Get-SPSPublishedServiceApplication.ps1 b/src/Modules/SPSTrust.Common/Public/Get-SPSPublishedServiceApplication.ps1 new file mode 100644 index 0000000..e4c4f3c --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Get-SPSPublishedServiceApplication.ps1 @@ -0,0 +1,53 @@ +function Get-SPSPublishedServiceApplication { + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + Write-Verbose -Message "Getting service application publish status '$Name'" + + $result = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + + $serviceApp = Get-SPServiceApplication -Name $params.Name -ErrorAction SilentlyContinue + if ($null -eq $serviceApp) { + Write-Verbose -Message "The service application $Name does not exist" + $sharedEnsure = "Absent" + } + if ($null -eq $serviceApp.Uri) { + Write-Verbose -Message ("Only Business Data Connectivity, Machine Translation, Managed Metadata, " + ` + "User Profile, Search, Secure Store are supported to be published via DSC.") + $sharedEnsure = "Absent" + } + else { + if ($serviceApp.Shared -eq $true) { + $sharedEnsure = "Present" + } + elseif ($serviceApp.Shared -eq $false) { + $sharedEnsure = "Absent" + } + } + return @{ + Name = $params.Name + Ensure = $sharedEnsure + Uri = $serviceApp.Uri.tostring() + Type = $serviceApp.GetType().FullName + } + } + return $result +} diff --git a/src/Modules/SPSTrust.Common/Public/Get-SPSServer.ps1 b/src/Modules/SPSTrust.Common/Public/Get-SPSServer.ps1 new file mode 100644 index 0000000..58a246d --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Get-SPSServer.ps1 @@ -0,0 +1,22 @@ +function Get-SPSServer { + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param ( + [Parameter(Mandatory = $true)] + [System.String] + $Server, # Name of the SharePoint server + [Parameter()] + [System.Management.Automation.PSCredential] + $InstallAccount # Credential for accessing the SharePoint server + ) + Write-Verbose "Getting SharePoint Servers of Farm '$Server'" + # Use the Invoke-SPSCommand function to get SharePoint servers + $result = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + (Get-SPServer | Where-Object -FilterScript { $_.Role -ne 'Invalid' }).Name + } + return $result +} + diff --git a/src/Modules/SPSTrust.Common/Public/Get-SPSTopologyServiceAppPermission.ps1 b/src/Modules/SPSTrust.Common/Public/Get-SPSTopologyServiceAppPermission.ps1 new file mode 100644 index 0000000..3fe4a89 --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Get-SPSTopologyServiceAppPermission.ps1 @@ -0,0 +1,45 @@ +function Get-SPSTopologyServiceAppPermission { + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $FarmId, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + Write-Verbose -Message "Getting Topology Service permissions for FarmID '$FarmId'" + $result = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + $ensure = 'Absent' + + $security = Get-SPTopologyServiceApplication | Get-SPServiceApplicationSecurity + $getAccessRule = $security.AccessRules | Where-Object -FilterScript { $_.Name -eq "c:0%.c|system|$($params.FarmId)" } + + if ($null -eq $getAccessRule) { + return @{ + Server = $params.Server + Ensure = $ensure + } + } + else { + $ensure = 'Present' + return @{ + Server = $params.Server + Ensure = $ensure + } + } + } + return $result +} diff --git a/src/Modules/SPSTrust.Common/Public/Get-SPSTrustedRootAuthority.ps1 b/src/Modules/SPSTrust.Common/Public/Get-SPSTrustedRootAuthority.ps1 new file mode 100644 index 0000000..f3f69ab --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Get-SPSTrustedRootAuthority.ps1 @@ -0,0 +1,62 @@ +function Get-SPSTrustedRootAuthority { + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter()] + [System.String] + $CertificateThumbprint, + + [Parameter()] + [System.String] + $CertificateFilePath, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + Write-Verbose "Getting Trusted Root Authority with name '$Name'" + + if (-not ($PSBoundParameters.ContainsKey("CertificateThumbprint")) -and ` + -not($PSBoundParameters.ContainsKey("CertificateFilePath"))) { + Write-Verbose -Message ("At least one of the following parameters must be specified: " + ` + "CertificateThumbprint, CertificateFilePath.") + } + $result = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + + $rootCert = Get-SPTrustedRootAuthority -Identity $params.Name -ErrorAction SilentlyContinue + $ensure = 'Absent' + + if ($null -eq $rootCert) { + return @{ + Name = $params.Name + CertificateThumbprint = [string]::Empty + CertificateFilePath = '' + Ensure = $ensure + } + } + else { + $ensure = 'Present' + return @{ + Name = $params.Name + CertificateThumbprint = $rootCert.Certificate.Thumbprint + CertificateFilePath = '' + Ensure = $ensure + } + } + } + return $result +} diff --git a/src/Modules/SPSTrust.Common/Public/Get-SPSTrustedServiceTokenIssuer.ps1 b/src/Modules/SPSTrust.Common/Public/Get-SPSTrustedServiceTokenIssuer.ps1 new file mode 100644 index 0000000..e169813 --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Get-SPSTrustedServiceTokenIssuer.ps1 @@ -0,0 +1,62 @@ +function Get-SPSTrustedServiceTokenIssuer { + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter()] + [System.String] + $CertificateThumbprint, + + [Parameter()] + [System.String] + $CertificateFilePath, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + Write-Verbose "Getting Trusted Service Token Issuer with name '$Name'" + + if (-not ($PSBoundParameters.ContainsKey("CertificateThumbprint")) -and ` + -not($PSBoundParameters.ContainsKey("CertificateFilePath"))) { + Write-Verbose -Message ("At least one of the following parameters must be specified: " + ` + "CertificateThumbprint, CertificateFilePath.") + } + $result = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + + $rootCert = Get-SPTrustedServiceTokenIssuer -Identity $params.Name -ErrorAction SilentlyContinue + $ensure = 'Absent' + + if ($null -eq $rootCert) { + return @{ + Name = $params.Name + CertificateThumbprint = [string]::Empty + CertificateFilePath = '' + Ensure = $ensure + } + } + else { + $ensure = 'Present' + return @{ + Name = $params.Name + CertificateThumbprint = $rootCert.Certificate.Thumbprint + CertificateFilePath = '' + Ensure = $ensure + } + } + } + return $result +} diff --git a/src/Modules/SPSTrust.Common/Public/New-SPSPublishedServiceAppProxy.ps1 b/src/Modules/SPSTrust.Common/Public/New-SPSPublishedServiceAppProxy.ps1 new file mode 100644 index 0000000..4ea698d --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/New-SPSPublishedServiceAppProxy.ps1 @@ -0,0 +1,72 @@ +function New-SPSPublishedServiceAppProxy { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $ServiceUri, + + [Parameter(Mandatory = $true)] + [System.String] + $ServiceType, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + Write-Verbose -Message "Adding service application proxy '$Name' on '$Server'" + Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + + Receive-SPServiceApplicationConnectionInfo -FarmUrl $params.ServiceUri | Out-Null + + switch ($params.ServiceType) { + { $_.contains('SearchServiceApplication') } { + New-SPEnterpriseSearchServiceApplicationProxy -Name $params.Name ` + -Uri $params.ServiceUri ` + -Verbose + } + { $_.contains('UserProfileApplication') } { + New-SPProfileServiceApplicationProxy -Name $params.Name ` + -Uri $params.ServiceUri ` + -DefaultProxyGroup ` + -Verbose + } + { $_.contains('MetadataWebServiceApplication') } { + New-SPMetadataServiceApplicationProxy -Name $params.Name ` + -Uri $params.ServiceUri ` + -DefaultProxyGroup ` + -Verbose + $mmsProxy = Get-SPMetadataServiceApplicationProxy $params.Name + $mmsProxy.Properties.IsDefaultSiteCollectionTaxonomy = $true + $mmsProxy.Properties.IsContentTypePushdownEnabled = $true + $mmsProxy.Properties.IsDefaultKeywordTaxonomy = $true + $mmsProxy.Update() + } + { $_.contains('SPSecurityTokenServiceApplication') } { + New-SPSecureStoreServiceApplicationProxy -Name $params.Name ` + -Uri $params.ServiceUri ` + -DefaultProxyGroup ` + -Verbose + } + { $_.contains('TranslationServiceApplication') } { + New-SPTranslationServiceApplicationProxy -Name $params.Name ` + -Uri $params.ServiceUri ` + -DefaultProxyGroup ` + -Verbose + } + } + } +} diff --git a/src/Modules/SPSTrust.Common/Public/Publish-SPSServiceApplication.ps1 b/src/Modules/SPSTrust.Common/Public/Publish-SPSServiceApplication.ps1 new file mode 100644 index 0000000..82549b0 --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Publish-SPSServiceApplication.ps1 @@ -0,0 +1,56 @@ +function Publish-SPSServiceApplication { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message "Setting service application publish status '$Name'" + Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + + $serviceApp = Get-SPServiceApplication -Name $params.Name -ErrorAction SilentlyContinue + if ($null -eq $serviceApp) { + $message = ("The service application $($params.Name) does not exist") + throw $message + } + + if ($null -eq $serviceApp.Uri) { + $message = ("Only Business Data Connectivity, Machine Translation, Managed Metadata, " + ` + "User Profile, Search, Secure Store are supported to be published via DSC.") + throw $message + } + + if ($params.Ensure -eq 'Present') { + Write-Verbose -Message "Publishing Service Application $($params.Name)" + if ($serviceApp.DefaultEndpoint.DisplayName -eq 'http') { + $httpsEndpoint = $serviceApp.Endpoints | Where-Object -FilterScript { $_.DisplayName -eq 'https' } + Set-SPServiceApplication $serviceApp -DefaultEndpoint $httpsEndpoint -Confirm:$false -Verbose + } + Publish-SPServiceApplication -Identity $serviceApp -Verbose + } + + if ($params.Ensure -eq 'Absent') { + Write-Verbose -Message "Unpublishing Service Application $($params.Name)" + Unpublish-SPServiceApplication -Identity $serviceApp -Verbose + } + } +} diff --git a/src/Modules/SPSTrust.Common/Public/Remove-SPSPublishedServiceAppProxy.ps1 b/src/Modules/SPSTrust.Common/Public/Remove-SPSPublishedServiceAppProxy.ps1 new file mode 100644 index 0000000..a67cf10 --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Remove-SPSPublishedServiceAppProxy.ps1 @@ -0,0 +1,39 @@ +function Remove-SPSPublishedServiceAppProxy { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount + ) + + Write-Verbose -Message "Removing service application proxy '$Name' on '$Server'" + Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + + $serviceAppProxies = Get-SPServiceApplicationProxy -ErrorAction SilentlyContinue + if ($null -ne $serviceAppProxies) { + $serviceAppProxy = $serviceAppProxies | Where-Object -FilterScript { + $_.Name -eq $params.Name + } + if ($null -ne $serviceAppProxy) { + Remove-SPServiceApplicationProxy $serviceAppProxy -RemoveData -Confirm:$false -Verbose + Write-Verbose -Message "The service application proxy $($params.Name) was successfully removed" + } + else { + Write-Verbose -Message "The service application proxy $($params.Name) does not exist" + } + } + } +} diff --git a/src/Modules/SPSTrust.Common/Public/Set-SPSPublishedServiceAppPermission.ps1 b/src/Modules/SPSTrust.Common/Public/Set-SPSPublishedServiceAppPermission.ps1 new file mode 100644 index 0000000..0c5dc38 --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Set-SPSPublishedServiceAppPermission.ps1 @@ -0,0 +1,77 @@ +function Set-SPSPublishedServiceAppPermission { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $FarmId, + + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount, + + # Ensure parameter to specify whether to add (Present) or remove (Absent) permissions + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message "Setting Topology Service permissions for FarmID '$FarmId'" + Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + + $serviceApp = Get-SPServiceApplication -Name $params.Name -ErrorAction SilentlyContinue + if ($null -eq $serviceApp) { + $message = ("The service application $($params.Name) does not exist") + throw $message + } + + if ($null -eq $serviceApp.Uri) { + $message = ("Only Business Data Connectivity, Machine Translation, Managed Metadata, " + ` + "User Profile, Search, Secure Store are supported to be published via DSC.") + throw $message + } + + if ($serviceApp.GetType().ToString() -eq 'Microsoft.Office.Server.Administration.UserProfileApplication') { + Write-Verbose -Message 'The User Profile Application requires domain credentials for connection access' + Write-Verbose -Message 'Check that web app pool account exists in the connect permissions' + } + else { + $security = Get-SPServiceApplication $serviceApp | Get-SPServiceApplicationSecurity + $claimProvider = (Get-SPClaimProvider System).ClaimProvider + $claimType = 'http://schemas.microsoft.com/sharepoint/2009/08/claims/farmid' + $spoRights = $security.NamedAccessRights.Name + $principal = New-SPClaimsPrincipal -ClaimType $claimType ` + -ClaimProvider $claimProvider ` + -ClaimValue $($params.FarmId) + + # Grant or revoke permissions based on the Ensure parameter + if ($params.Ensure -eq 'Present') { + Grant-SPObjectSecurity -Identity $security ` + -Principal $principal ` + -Rights $spoRights ` + -Verbose + } + elseif ($params.Ensure -eq 'Absent') { + Revoke-SPObjectSecurity -Identity $security ` + -Principal $principal ` + -Verbose + } + + # Apply the updated security settings to the Targeted Service Application + Set-SPServiceApplicationSecurity $serviceApp -ObjectSecurity $security -Verbose + } + } +} diff --git a/src/Modules/SPSTrust.Common/Public/Set-SPSTopologyServiceAppPermission.ps1 b/src/Modules/SPSTrust.Common/Public/Set-SPSTopologyServiceAppPermission.ps1 new file mode 100644 index 0000000..387eb0c --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Set-SPSTopologyServiceAppPermission.ps1 @@ -0,0 +1,65 @@ +function Set-SPSTopologyServiceAppPermission { + [CmdletBinding()] + param + ( + # Farm ID for which the permissions are being set + [Parameter(Mandatory = $true)] + [System.String] + $FarmId, + + # Server where the command will be executed + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + # Credentials to use for executing the command + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount, + + # Ensure parameter to specify whether to add (Present) or remove (Absent) permissions + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message "Setting Topology Service permissions for FarmID '$FarmId'" + + # Invoke the command on the specified server with the provided credentials + Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments $PSBoundParameters ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + + # Get the security settings for the Topology Service Application + $security = Get-SPTopologyServiceApplication | Get-SPServiceApplicationSecurity + + # Get the claim provider and define the claim type for the Farm ID + $claimProvider = (Get-SPClaimProvider System).ClaimProvider + $claimType = 'http://schemas.microsoft.com/sharepoint/2009/08/claims/farmid' + + # Create a new claims principal for the Farm ID + $principal = New-SPClaimsPrincipal -ClaimType $claimType ` + -ClaimProvider $claimProvider ` + -ClaimValue $($params.FarmId) + + # Grant or revoke permissions based on the Ensure parameter + if ($params.Ensure -eq 'Present') { + Grant-SPObjectSecurity -Identity $security ` + -Principal $principal ` + -Rights 'Full Control' ` + -Verbose + } + elseif ($params.Ensure -eq 'Absent') { + Revoke-SPObjectSecurity -Identity $security ` + -Principal $principal ` + -Verbose + } + + # Apply the updated security settings to the Topology Service Application + Get-SPTopologyServiceApplication | Set-SPServiceApplicationSecurity -ObjectSecurity $security -Verbose + } +} + diff --git a/src/Modules/SPSTrust.Common/Public/Set-SPSTrustedRootAuthority.ps1 b/src/Modules/SPSTrust.Common/Public/Set-SPSTrustedRootAuthority.ps1 new file mode 100644 index 0000000..16cc309 --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Set-SPSTrustedRootAuthority.ps1 @@ -0,0 +1,106 @@ +function Set-SPSTrustedRootAuthority { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter()] + [System.String] + $CertificateThumbprint, + + [Parameter()] + [String] + $CertificateFilePath, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message "Setting SPTrustedRootAuthority '$Name'" + if ($Ensure -eq 'Present') { + if (-not ($PSBoundParameters.ContainsKey("CertificateThumbprint")) -and ` + -not($PSBoundParameters.ContainsKey("CertificateFilePath"))) { + $message = ("At least one of the following parameters must be specified: " + ` + "CertificateThumbprint, CertificateFilePath.") + throw $message + } + + if ($PSBoundParameters.ContainsKey("CertificateFilePath") -and ` + -not ($PSBoundParameters.ContainsKey("CertificateThumbprint"))) { + if (-not (Test-Path -Path $CertificateFilePath)) { + $message = ("Specified CertificateFilePath does not exist: $CertificateFilePath") + throw $message + } + } + } + + $null = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments @($PSBoundParameters, $MyInvocation.MyCommand.Source) ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + + if ($params.Ensure -eq 'Absent') { + Write-Verbose -Message "Removing SPTrustedRootAuthority '$params.Name'" + Remove-SPTrustedRootAuthority -Identity $params.Name -Confirm:$false -ErrorAction SilentlyContinue + } + else { + if ($params.ContainsKey("CertificateFilePath")) { + Write-Verbose -Message "Importing certificate from CertificateFilePath" + try { + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $cert.Import($params.CertificateFilePath) + } + catch { + $message = "An error occured: $($_.Exception.Message)" + throw $message + } + + if ($null -eq $cert) { + $message = "Import of certificate failed." + throw $message + } + + if ($params.ContainsKey("CertificateThumbprint")) { + if (-not $params.CertificateThumbprint.Equals($cert.Thumbprint)) { + $message = "Imported certificate thumbprint ($($cert.Thumbprint)) does not match expected thumbprint ($($params.CertificateThumbprint))." + throw $message + } + } + } + else { + Write-Verbose -Message "Importing certificate from CertificateThumbprint" + $cert = Get-Item -Path "CERT:\LocalMachine\My\$($params.CertificateThumbprint)" ` + -ErrorAction SilentlyContinue + + if ($null -eq $cert) { + $message = "Certificate not found in the local Certificate Store" + throw $message + } + } + + if ($cert.HasPrivateKey) { + Write-Verbose -Message "Certificate has private key. Removing private key." + $pubKeyBytes = $cert.Export("cert") + $cert2 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $cert2.Import($pubKeyBytes) + $cert = $cert2 + } + + Write-Verbose -Message "Updating Root Authority" + New-SPTrustedRootAuthority -Name $params.Name -Certificate $cert + } + } +} diff --git a/src/Modules/SPSTrust.Common/Public/Set-SPSTrustedServiceTokenIssuer.ps1 b/src/Modules/SPSTrust.Common/Public/Set-SPSTrustedServiceTokenIssuer.ps1 new file mode 100644 index 0000000..9a9d042 --- /dev/null +++ b/src/Modules/SPSTrust.Common/Public/Set-SPSTrustedServiceTokenIssuer.ps1 @@ -0,0 +1,106 @@ +function Set-SPSTrustedServiceTokenIssuer { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Server, + + [Parameter()] + [System.String] + $CertificateThumbprint, + + [Parameter()] + [String] + $CertificateFilePath, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $InstallAccount, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + ) + + Write-Verbose -Message "Setting SPTrustedServiceTokenIssuer '$Name'" + if ($Ensure -eq 'Present') { + if (-not ($PSBoundParameters.ContainsKey("CertificateThumbprint")) -and ` + -not($PSBoundParameters.ContainsKey("CertificateFilePath"))) { + $message = ("At least one of the following parameters must be specified: " + ` + "CertificateThumbprint, CertificateFilePath.") + throw $message + } + + if ($PSBoundParameters.ContainsKey("CertificateFilePath") -and ` + -not ($PSBoundParameters.ContainsKey("CertificateThumbprint"))) { + if (-not (Test-Path -Path $CertificateFilePath)) { + $message = ("Specified CertificateFilePath does not exist: $CertificateFilePath") + throw $message + } + } + } + + $null = Invoke-SPSCommand -Credential $InstallAccount ` + -Arguments @($PSBoundParameters, $MyInvocation.MyCommand.Source) ` + -Server $Server ` + -ScriptBlock { + $params = $args[0] + + if ($params.Ensure -eq 'Absent') { + Write-Verbose -Message "Removing SPTrustedIdentityTokenIssuer '$params.Name'" + Remove-SPTrustedIdentityTokenIssuer -Identity $params.Name -Confirm:$false -ErrorAction SilentlyContinue + } + else { + if ($params.ContainsKey("CertificateFilePath")) { + Write-Verbose -Message "Importing certificate from CertificateFilePath" + try { + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $cert.Import($params.CertificateFilePath) + } + catch { + $message = "An error occured: $($_.Exception.Message)" + throw $message + } + + if ($null -eq $cert) { + $message = "Import of certificate failed." + throw $message + } + + if ($params.ContainsKey("CertificateThumbprint")) { + if (-not $params.CertificateThumbprint.Equals($cert.Thumbprint)) { + $message = "Imported certificate thumbprint ($($cert.Thumbprint)) does not match expected thumbprint ($($params.CertificateThumbprint))." + throw $message + } + } + } + else { + Write-Verbose -Message "Importing certificate from CertificateThumbprint" + $cert = Get-Item -Path "CERT:\LocalMachine\My\$($params.CertificateThumbprint)" ` + -ErrorAction SilentlyContinue + + if ($null -eq $cert) { + $message = "Certificate not found in the local Certificate Store" + throw $message + } + } + + if ($cert.HasPrivateKey) { + Write-Verbose -Message "Certificate has private key. Removing private key." + $pubKeyBytes = $cert.Export("cert") + $cert2 = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 + $cert2.Import($pubKeyBytes) + $cert = $cert2 + } + + Write-Verbose -Message "Adding STS SPTrustedServiceTokenIssuer" + New-SPTrustedServiceTokenIssuer -Name $params.Name -Certificate $cert + } + } +} diff --git a/src/Modules/SPSTrust.Common/SPSTrust.Common.psd1 b/src/Modules/SPSTrust.Common/SPSTrust.Common.psd1 new file mode 100644 index 0000000..a3a978a --- /dev/null +++ b/src/Modules/SPSTrust.Common/SPSTrust.Common.psd1 @@ -0,0 +1,45 @@ +@{ + RootModule = 'SPSTrust.Common.psm1' + ModuleVersion = '2.0.0' + GUID = 'e1cce4f2-12de-4923-8e67-f37a081944aa' + Author = 'Jean-Cyril DROUHIN' + CompanyName = 'luigilink' + Copyright = '(c) Jean-Cyril DROUHIN. All rights reserved.' + Description = 'Shared functions for the SPSTrust toolkit (configure trust relationships between SharePoint Server farms: exchange STS/ROOT certificates, publish service applications, grant Topology and published service-application permissions, and connect service application proxies across farms).' + + PowerShellVersion = '5.1' + + FunctionsToExport = @( + 'Clear-SPSLogFolder' + 'Export-SPSSecurityTokenCertificate' + 'Export-SPSTrustedRootAuthority' + 'Get-SPSFarmId' + 'Get-SPSPublishedServiceAppPermission' + 'Get-SPSPublishedServiceAppProxy' + 'Get-SPSPublishedServiceApplication' + 'Get-SPSServer' + 'Get-SPSTopologyServiceAppPermission' + 'Get-SPSTrustedRootAuthority' + 'Get-SPSTrustedServiceTokenIssuer' + 'New-SPSPublishedServiceAppProxy' + 'Publish-SPSServiceApplication' + 'Remove-SPSPublishedServiceAppProxy' + 'Set-SPSPublishedServiceAppPermission' + 'Set-SPSTopologyServiceAppPermission' + 'Set-SPSTrustedRootAuthority' + 'Set-SPSTrustedServiceTokenIssuer' + ) + + CmdletsToExport = @() + VariablesToExport = @() + AliasesToExport = @() + + PrivateData = @{ + PSData = @{ + Tags = @('SharePoint', 'SharePointServer', 'Trust', 'ServiceApplication', 'CredSSP') + LicenseUri = 'https://github.com/luigilink/SPSTrust/blob/main/LICENSE' + ProjectUri = 'https://github.com/luigilink/SPSTrust' + ReleaseNotes = 'https://github.com/luigilink/SPSTrust/blob/main/RELEASE-NOTES.md' + } + } +} diff --git a/src/Modules/SPSTrust.Common/SPSTrust.Common.psm1 b/src/Modules/SPSTrust.Common/SPSTrust.Common.psm1 new file mode 100644 index 0000000..d53182b --- /dev/null +++ b/src/Modules/SPSTrust.Common/SPSTrust.Common.psm1 @@ -0,0 +1,26 @@ +# ===================================================================================== +# SPSTrust.Common - module loader +# +# Dot-sources every *.ps1 in Private/ and Public/, then exports only the public +# function names (read from the Public folder). Private functions (the CredSSP +# remoting helper Invoke-SPSCommand) remain accessible inside the module but are +# hidden from callers. +# ===================================================================================== + +$script:ModuleRoot = $PSScriptRoot + +$private = @(Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'Private\*.ps1') -ErrorAction SilentlyContinue) +$public = @(Get-ChildItem -Path (Join-Path -Path $PSScriptRoot -ChildPath 'Public\*.ps1') -ErrorAction SilentlyContinue) + +foreach ($file in @($private + $public)) { + try { + . $file.FullName + } + catch { + Write-Error -Message "Failed to import function file '$($file.FullName)': $_" + } +} + +if ($public.Count -gt 0) { + Export-ModuleMember -Function $public.BaseName +} diff --git a/scripts/SPSTrust.ps1 b/src/SPSTrust.ps1 similarity index 89% rename from scripts/SPSTrust.ps1 rename to src/SPSTrust.ps1 index f1b9304..d48b7b0 100644 --- a/scripts/SPSTrust.ps1 +++ b/src/SPSTrust.ps1 @@ -4,10 +4,13 @@ .DESCRIPTION SPSTrust.ps1 is a PowerShell script that configures SharePoint trust relationships between farms. - Compatible with PowerShell version 5.0 and later. + Shared logic lives in the SPSTrust.Common module (src/Modules/SPSTrust.Common); the script + version is sourced from that module's manifest (ModuleVersion). + Compatible with PowerShell version 5.1 and later. .PARAMETER ConfigFile - Specifies the path to the JSON configuration file, containing details about the application, environment, and certificate paths. + Specifies the path to the PowerShell data (.psd1) configuration file, containing details about + the application, environment, domain, certificate share, farms, and trust relationships. .PARAMETER FarmAccount Specifies the credential for the service account that runs the script. @@ -15,15 +18,19 @@ .PARAMETER CleanServices Optional switch to remove published services on each trusted farm. + .PARAMETER LogRetentionDays + Number of days of transcript log files to keep in the Logs folder. Defaults to 180. + Set to 0 to disable pruning. + .EXAMPLE - SPSTrust.ps1 -ConfigFile 'contoso-PROD.json' -FarmAccount (Get-Credential) - SPSTrust.ps1 -ConfigFile 'contoso-PROD.json' -FarmAccount (Get-Credential) -CleanServices + SPSTrust.ps1 -ConfigFile '.\Config\CONTOSO-PROD.psd1' -FarmAccount (Get-Credential) + SPSTrust.ps1 -ConfigFile '.\Config\CONTOSO-PROD.psd1' -FarmAccount (Get-Credential) -CleanServices .NOTES FileName: SPSTrust.ps1 Author: luigilink (Jean-Cyril DROUHIN) - Date: October 17, 2024 - Version: 1.1.0 + Date: July 10, 2026 + Version: Defined by the SPSTrust.Common module manifest (ModuleVersion) .LINK https://spjc.fr/ @@ -31,7 +38,7 @@ #> param( [Parameter(Position = 1, Mandatory = $true)] - [ValidateScript({ (Test-Path $_) -and ($_ -like '*.json') })] + [ValidateScript({ (Test-Path $_) -and ($_ -like '*.psd1') })] [System.String] $ConfigFile, # Path to the configuration file @@ -41,9 +48,15 @@ param( [Parameter(Position = 3)] [switch] - $CleanServices # Switch parameter to clean services + $CleanServices, # Switch parameter to clean services + + [Parameter()] + [System.UInt32] + $LogRetentionDays = 180 # Number of days of transcript logs to keep ) +#requires -Version 5.1 + #region Initialization # Clear the host console Clear-Host @@ -52,11 +65,20 @@ Clear-Host $Host.UI.RawUI.WindowTitle = "SPSTrust script running on $env:COMPUTERNAME" # Define the path to the helper module -$scriptRootPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$scriptRootPath = $PSScriptRoot $script:HelperModulePath = Join-Path -Path $scriptRootPath -ChildPath 'Modules' -# Import the helper module -Import-Module -Name (Join-Path -Path $script:HelperModulePath -ChildPath 'util.psm1') -Force +# Import the helper module (SPSTrust.Common) +try { + Import-Module -Name (Join-Path -Path $script:HelperModulePath -ChildPath 'SPSTrust.Common\SPSTrust.Common.psd1') -Force -ErrorAction Stop +} +catch { + Write-Error -Message @" +Failed to import SPSTrust.Common module from path: $($script:HelperModulePath) +Exception: $_ +"@ + Exit +} # Ensure the script is running with administrator privileges if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) { @@ -66,13 +88,13 @@ if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdent # Load the configuration file try { if (Test-Path $ConfigFile) { - $jsonEnvCfg = Get-Content $ConfigFile | ConvertFrom-Json - $Application = $jsonEnvCfg.ApplicationName - $Environment = $jsonEnvCfg.ConfigurationName - $certFolder = $jsonEnvCfg.CertFileShared - $scriptFQDN = $jsonEnvCfg.Domain - $spFarmsObj = $jsonEnvCfg.Farms - $spTrustsObj = $jsonEnvCfg.Trusts + $EnvironmentConfig = Import-PowerShellDataFile -Path $ConfigFile + $Application = $EnvironmentConfig.ApplicationName + $Environment = $EnvironmentConfig.ConfigurationName + $certFolder = $EnvironmentConfig.CertFileShared + $scriptFQDN = $EnvironmentConfig.Domain + $spFarmsObj = $EnvironmentConfig.Farms + $spTrustsObj = $EnvironmentConfig.Trusts } else { Throw "Configuration file '$ConfigFile' not found." @@ -84,7 +106,7 @@ catch { } # Define variables -$SPSTrustVersion = '1.0.0' +$SPSTrustVersion = (Get-Module -Name 'SPSTrust.Common').Version.ToString() $getDateFormatted = Get-Date -Format yyyy-MM-dd $spsTrustFileName = "$($Application)-$($Environment)-$($getDateFormatted)" $currentUser = ([Security.Principal.WindowsIdentity]::GetCurrent()).Name @@ -181,7 +203,7 @@ foreach ($spFarm in $spFarmsObj) { # 1.2 Establish trust on the publishing farm - Import STS and ROOT certificates foreach ($spTrust in $spTrustsObj) { - $spServer = $jsonEnvCfg.Farms | Where-Object -FilterScript { $_.Name -eq $spTrust.LocalFarm } + $spServer = $EnvironmentConfig.Farms | Where-Object -FilterScript { $_.Name -eq $spTrust.LocalFarm } $spTargetServer = "$($spServer.Server).$($scriptFQDN)" $spRemoteServers = $spTrust.RemoteFarms $spServices = $spTrust.Services @@ -301,7 +323,7 @@ Exception: $_ # 2. On the publishing farm, publish the service application foreach ($spTrust in $spTrustsObj) { - $spServer = $jsonEnvCfg.Farms | Where-Object -FilterScript { $_.Name -eq $spTrust.LocalFarm } + $spServer = $EnvironmentConfig.Farms | Where-Object -FilterScript { $_.Name -eq $spTrust.LocalFarm } $spTargetServer = "$($spServer.Server).$($scriptFQDN)" $spServices = $spTrust.Services @@ -361,7 +383,7 @@ Exception: $_ # 3. On the publishing farm, set the permission to the appropriate service applications for the consuming farm. foreach ($spTrust in $spTrustsObj) { - $spServer = $jsonEnvCfg.Farms | Where-Object -FilterScript { $_.Name -eq $spTrust.LocalFarm } + $spServer = $EnvironmentConfig.Farms | Where-Object -FilterScript { $_.Name -eq $spTrust.LocalFarm } $spTargetServer = "$($spServer.Server).$($scriptFQDN)" $spRemoteFarms = $spTrust.RemoteFarms $spServices = $spTrust.Services @@ -370,7 +392,7 @@ foreach ($spTrust in $spTrustsObj) { foreach ($spRemoteFarm in $spRemoteFarms) { # Get the Farm ID for the remote server try { - $spServer = $jsonEnvCfg.Farms | Where-Object -FilterScript { $_.Name -eq $spRemoteFarm } + $spServer = $EnvironmentConfig.Farms | Where-Object -FilterScript { $_.Name -eq $spRemoteFarm } $spRemoteServer = "$($spServer.Server).$($scriptFQDN)" Write-Output "[$($spRemoteServer)] Getting GUID property of SPFarm Object" $spFarmID = Get-SPSFarmId -Server $spRemoteServer -InstallAccount $FarmAccount @@ -453,7 +475,7 @@ Exception: $_ foreach ($spRemoteFarm in $spRemoteFarms) { # Get the Farm ID for the remote server try { - $spServer = $jsonEnvCfg.Farms | Where-Object -FilterScript { $_.Name -eq $spRemoteFarm } + $spServer = $EnvironmentConfig.Farms | Where-Object -FilterScript { $_.Name -eq $spRemoteFarm } $spRemoteServer = "$($spServer.Server).$($scriptFQDN)" Write-Output "[$($spRemoteServer)] Getting GUID property of SPFarm Object" $spFarmID = Get-SPSFarmId -Server $spRemoteServer -InstallAccount $FarmAccount @@ -537,7 +559,7 @@ Exception: $_ # 4. Connect each published service application on remote farm foreach ($spTrust in $spTrustsObj) { - $spServer = $jsonEnvCfg.Farms | Where-Object -FilterScript { $_.Name -eq $spTrust.LocalFarm } + $spServer = $EnvironmentConfig.Farms | Where-Object -FilterScript { $_.Name -eq $spTrust.LocalFarm } $spTargetServer = "$($spServer.Server).$($scriptFQDN)" $spRemoteFarms = $spTrust.RemoteFarms $spServices = $spTrust.Services @@ -552,7 +574,7 @@ foreach ($spTrust in $spTrustsObj) { # Loop through each remote server in the $spRemoteServers array foreach ($spRemoteFarm in $spRemoteFarms) { # Find the server configuration for the current remote server - $spServer = $jsonEnvCfg.Farms | Where-Object -FilterScript { $_.Name -eq $spRemoteFarm } + $spServer = $EnvironmentConfig.Farms | Where-Object -FilterScript { $_.Name -eq $spRemoteFarm } $spRemoteServer = "$($spServer.Server).$($scriptFQDN)" try { @@ -621,6 +643,10 @@ Exception: $_ # Clean-Up Trap { Continue } + +# Prune old transcript logs based on the retention window +Clear-SPSLogFolder -Path $pathLogsFolder -Retention $LogRetentionDays -Extension '*.log' + $DateEnded = Get-Date Write-Output '-----------------------------------------------' Write-Output "| SPSTrust Script Completed" From 294654184a05f27745e3ba33c5744d869db6971c Mon Sep 17 00:00:00 2001 From: LuigiLink Date: Fri, 10 Jul 2026 13:26:34 +0200 Subject: [PATCH 2/5] test: add Pester suite and PSScriptAnalyzerSettings - tests/Modules/SPSTrust.Common.Tests.ps1: manifest/loader shape, public export surface (18 functions), private Invoke-SPSCommand hidden, and behavioural tests for Clear-SPSLogFolder (retention 0, missing path, age window, extension filter). - tests/SPSTrust.Tests.ps1: entry-script existence/syntax/metadata, param contract (ConfigFile .psd1 + mandatory, FarmAccount, CleanServices switch, LogRetentionDays), and example .psd1 config integrity (required keys, farm/trust cross-references). - PSScriptAnalyzerSettings.psd1: exclude PSUseShouldProcessForStateChangingFunctions with a documented rationale (thin CredSSP remoting wrappers driven by -Ensure). 54 tests pass; PSScriptAnalyzer reports no findings on src/. Refs #3 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- PSScriptAnalyzerSettings.psd1 | 15 ++ tests/Modules/SPSTrust.Common.Tests.ps1 | 180 ++++++++++++++++++++++++ tests/SPSTrust.Tests.ps1 | 158 +++++++++++++++++++++ 3 files changed, 353 insertions(+) create mode 100644 PSScriptAnalyzerSettings.psd1 create mode 100644 tests/Modules/SPSTrust.Common.Tests.ps1 create mode 100644 tests/SPSTrust.Tests.ps1 diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..b8b9afc --- /dev/null +++ b/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,15 @@ +@{ + # Settings consumed by Invoke-ScriptAnalyzer in CI and the local lint task. + # + # PSUseShouldProcessForStateChangingFunctions is excluded because the public + # Set-*/New-*/Remove-*/Publish-* functions are thin wrappers that marshal a + # script block to a remote farm through Invoke-SPSCommand (CredSSP). Their + # create/remove behaviour is already driven explicitly by an -Ensure parameter, + # and the destructive path is gated at the entry-script level by the + # -CleanServices switch. Adding SupportsShouldProcess/ShouldProcess plumbing to + # each remoting wrapper would add no real safety and is inconsistent with the + # rest of the SPS* toolkit. The rule is therefore excluded project-wide. + ExcludeRules = @( + 'PSUseShouldProcessForStateChangingFunctions' + ) +} diff --git a/tests/Modules/SPSTrust.Common.Tests.ps1 b/tests/Modules/SPSTrust.Common.Tests.ps1 new file mode 100644 index 0000000..d176774 --- /dev/null +++ b/tests/Modules/SPSTrust.Common.Tests.ps1 @@ -0,0 +1,180 @@ +# Pester tests for the SPSTrust.Common module. +# Resolve repo root - works on both local and CI/CD. + +BeforeAll { + $repoRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent + $script:moduleRoot = Join-Path -Path $repoRoot -ChildPath 'src/Modules/SPSTrust.Common' + $script:moduleManifest = Join-Path -Path $script:moduleRoot -ChildPath 'SPSTrust.Common.psd1' + $script:moduleFile = Join-Path -Path $script:moduleRoot -ChildPath 'SPSTrust.Common.psm1' + $script:moduleName = 'SPSTrust.Common' + + # Stub SharePoint cmdlets so the module can be imported on non-Windows / no-SharePoint + # hosts. Real behaviour is exercised on a SharePoint farm; these tests only validate + # shape & contracts. + $spsStubs = @( + 'Get-SPServer', 'Get-SPFarm', 'Get-SPServiceApplication', 'Get-SPTrustedRootAuthority', + 'Get-SPTrustedServiceTokenIssuer', 'Add-PSSnapin', 'Get-PSSnapin', 'New-PSSession', + 'Remove-PSSession', 'Invoke-Command' + ) + foreach ($name in $spsStubs) { + if (-not (Get-Command -Name $name -ErrorAction SilentlyContinue)) { + $sb = [ScriptBlock]::Create("function global:$name { param() }") + & $sb + } + } + + # The module is import-safe by design (the admin/powercfg/snap-in prelude lives in the + # entry script, not the module). Surface real import errors instead of hiding them. + Import-Module -Name $script:moduleManifest -Force -DisableNameChecking +} + +AfterAll { + Remove-Module -Name 'SPSTrust.Common' -Force -ErrorAction SilentlyContinue +} + +Describe 'SPSTrust.Common Module' { + + It 'module manifest exists' { + $script:moduleManifest | Should -Exist + } + + It 'loader module file exists' { + $script:moduleFile | Should -Exist + } + + It 'loader has valid PowerShell syntax' { + $parseErrors = $null + $tokens = $null + $null = [System.Management.Automation.Language.Parser]::ParseInput( + (Get-Content -Path $script:moduleFile -Raw), [ref]$tokens, [ref]$parseErrors) + $parseErrors | Should -BeNullOrEmpty + } + + It 'manifest declares a ModuleVersion' { + (Test-ModuleManifest -Path $script:moduleManifest).Version | Should -Not -BeNullOrEmpty + } + + It 'manifest ModuleVersion is 2.0.0 or higher' { + (Test-ModuleManifest -Path $script:moduleManifest).Version | Should -BeGreaterOrEqual ([version]'2.0.0') + } + + It 'module loads successfully' { + Get-Module -Name $script:moduleName | Should -Not -BeNullOrEmpty + } +} + +Describe 'SPSTrust.Common Public Functions' { + + $publicFunctions = @( + 'Clear-SPSLogFolder', + 'Export-SPSSecurityTokenCertificate', + 'Export-SPSTrustedRootAuthority', + 'Get-SPSFarmId', + 'Get-SPSPublishedServiceAppPermission', + 'Get-SPSPublishedServiceAppProxy', + 'Get-SPSPublishedServiceApplication', + 'Get-SPSServer', + 'Get-SPSTopologyServiceAppPermission', + 'Get-SPSTrustedRootAuthority', + 'Get-SPSTrustedServiceTokenIssuer', + 'New-SPSPublishedServiceAppProxy', + 'Publish-SPSServiceApplication', + 'Remove-SPSPublishedServiceAppProxy', + 'Set-SPSPublishedServiceAppPermission', + 'Set-SPSTopologyServiceAppPermission', + 'Set-SPSTrustedRootAuthority', + 'Set-SPSTrustedServiceTokenIssuer' + ) + + It 'exports <_>' -ForEach $publicFunctions { + Get-Command -Name $_ -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + } + + It 'manifest FunctionsToExport matches the exported set' { + $expectedExports = @( + 'Clear-SPSLogFolder', + 'Export-SPSSecurityTokenCertificate', + 'Export-SPSTrustedRootAuthority', + 'Get-SPSFarmId', + 'Get-SPSPublishedServiceAppPermission', + 'Get-SPSPublishedServiceAppProxy', + 'Get-SPSPublishedServiceApplication', + 'Get-SPSServer', + 'Get-SPSTopologyServiceAppPermission', + 'Get-SPSTrustedRootAuthority', + 'Get-SPSTrustedServiceTokenIssuer', + 'New-SPSPublishedServiceAppProxy', + 'Publish-SPSServiceApplication', + 'Remove-SPSPublishedServiceAppProxy', + 'Set-SPSPublishedServiceAppPermission', + 'Set-SPSTopologyServiceAppPermission', + 'Set-SPSTrustedRootAuthority', + 'Set-SPSTrustedServiceTokenIssuer' + ) | Sort-Object + $exported = (Get-Module -Name $script:moduleName).ExportedFunctions.Keys | Sort-Object + $exported | Should -Be $expectedExports + } +} + +Describe 'SPSTrust.Common Private Helpers' { + + $privateHelpers = @( + 'Invoke-SPSCommand' + ) + + It 'defines internal helper <_> (module scope)' -ForEach $privateHelpers { + $helper = $_ + InModuleScope -ModuleName 'SPSTrust.Common' -Parameters @{ helper = $helper } { + param($helper) + Get-Command -Name $helper -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty + } + } + + It 'does not export the private helper to callers <_>' -ForEach $privateHelpers { + (Get-Module -Name $script:moduleName).ExportedFunctions.Keys | Should -Not -Contain $_ + } +} + +Describe 'Clear-SPSLogFolder' { + + BeforeEach { + $script:tempRoot = Join-Path -Path $TestDrive -ChildPath ([guid]::NewGuid().ToString('N')) + $null = New-Item -Path $script:tempRoot -ItemType Directory -Force + } + + It 'does nothing when Retention is 0' { + $old = Join-Path -Path $script:tempRoot -ChildPath 'old.log' + Set-Content -Path $old -Value 'x' + (Get-Item $old).LastWriteTime = (Get-Date).AddDays(-400) + Clear-SPSLogFolder -Path $script:tempRoot -Retention 0 -Extension '*.log' + $old | Should -Exist + } + + It 'returns silently when the path does not exist' { + $missing = Join-Path -Path $script:tempRoot -ChildPath 'nope' + { Clear-SPSLogFolder -Path $missing -Retention 30 } | Should -Not -Throw + } + + It 'deletes files older than the retention window' { + $old = Join-Path -Path $script:tempRoot -ChildPath 'old.log' + $new = Join-Path -Path $script:tempRoot -ChildPath 'new.log' + Set-Content -Path $old -Value 'x' + Set-Content -Path $new -Value 'x' + (Get-Item $old).LastWriteTime = (Get-Date).AddDays(-200) + Clear-SPSLogFolder -Path $script:tempRoot -Retention 180 -Extension '*.log' + $old | Should -Not -Exist + $new | Should -Exist + } + + It 'only targets files matching the extension filter' { + $log = Join-Path -Path $script:tempRoot -ChildPath 'old.log' + $json = Join-Path -Path $script:tempRoot -ChildPath 'old.json' + Set-Content -Path $log -Value 'x' + Set-Content -Path $json -Value 'x' + (Get-Item $log).LastWriteTime = (Get-Date).AddDays(-200) + (Get-Item $json).LastWriteTime = (Get-Date).AddDays(-200) + Clear-SPSLogFolder -Path $script:tempRoot -Retention 180 -Extension '*.log' + $log | Should -Not -Exist + $json | Should -Exist + } +} diff --git a/tests/SPSTrust.Tests.ps1 b/tests/SPSTrust.Tests.ps1 new file mode 100644 index 0000000..2e45edb --- /dev/null +++ b/tests/SPSTrust.Tests.ps1 @@ -0,0 +1,158 @@ +# Pester tests for SPSTrust.ps1 +# Resolve repo root - works on CI/CD (GitHub Actions) and local runs + +BeforeAll { + $repoRoot = Split-Path -Path $PSScriptRoot -Parent + $script:scriptPath = Join-Path -Path $repoRoot -ChildPath 'src/SPSTrust.ps1' + $script:scriptContent = Get-Content -Path $script:scriptPath -Raw -ErrorAction SilentlyContinue + $script:configExample = Join-Path -Path $repoRoot -ChildPath 'src/Config/CONTOSO-PROD.example.psd1' +} + +Describe 'SPSTrust.ps1 File Existence' { + + It 'SPSTrust.ps1 exists' { + $script:scriptPath | Should -Exist + } + + It 'is a PowerShell script file' { + (Get-Item $script:scriptPath).Extension | Should -Be '.ps1' + } + + It 'has valid PowerShell syntax' { + $parseErrors = $null + $tokens = $null + $null = [System.Management.Automation.Language.Parser]::ParseInput( + $script:scriptContent, [ref]$tokens, [ref]$parseErrors) + $parseErrors | Should -BeNullOrEmpty + } +} + +Describe 'SPSTrust.ps1 Metadata' { + + It 'Should contain a SYNOPSIS' { + $script:scriptContent | Should -Match '\.SYNOPSIS' + } + + It 'Should contain a DESCRIPTION' { + $script:scriptContent | Should -Match '\.DESCRIPTION' + } + + It 'Should contain an EXAMPLE' { + $script:scriptContent | Should -Match '\.EXAMPLE' + } + + It 'Should declare an Author in NOTES' { + $script:scriptContent | Should -Match 'Author:\s*luigilink' + } + + It 'Should source its Version from the SPSTrust.Common manifest' { + $script:scriptContent | Should -Match 'Version:\s*Defined by the SPSTrust\.Common module manifest' + } + + It 'Should require PowerShell 5.1 or higher' { + $script:scriptContent | Should -Match '#requires\s+-Version\s+5\.1' + } + + It 'Should read its version from Get-Module at runtime' { + $script:scriptContent | Should -Match "Get-Module -Name 'SPSTrust\.Common'" + } + + It 'Should import the SPSTrust.Common manifest' { + $script:scriptContent | Should -Match 'SPSTrust\.Common\\SPSTrust\.Common\.psd1' + } + + It 'Should load config via Import-PowerShellDataFile' { + $script:scriptContent | Should -Match 'Import-PowerShellDataFile' + } +} + +Describe 'SPSTrust.ps1 Parameters' { + + BeforeAll { + $ast = [System.Management.Automation.Language.Parser]::ParseInput( + $script:scriptContent, [ref]$null, [ref]$null) + $script:paramBlock = $ast.ParamBlock + } + + It 'Should define a param block' { + $script:paramBlock | Should -Not -BeNullOrEmpty + } + + It 'Should expose a mandatory ConfigFile parameter' { + $configParam = $script:paramBlock.Parameters | Where-Object { + $_.Name.VariablePath.UserPath -eq 'ConfigFile' + } + $configParam | Should -Not -BeNullOrEmpty + + $paramAttr = $configParam.Attributes | Where-Object { + $_ -is [System.Management.Automation.Language.AttributeAst] -and + $_.TypeName.Name -eq 'Parameter' + } + $mandatoryArg = $paramAttr.NamedArguments | + Where-Object { $_.ArgumentName -eq 'Mandatory' } + $mandatoryArg | Should -Not -BeNullOrEmpty + } + + It 'ConfigFile should validate a .psd1 path' { + $script:scriptContent | Should -Match "\`$_ -like '\*\.psd1'" + } + + It 'Should expose a mandatory FarmAccount parameter' { + $farmParam = $script:paramBlock.Parameters | Where-Object { + $_.Name.VariablePath.UserPath -eq 'FarmAccount' + } + $farmParam | Should -Not -BeNullOrEmpty + } + + It 'Should expose a CleanServices switch' { + $cleanParam = $script:paramBlock.Parameters | Where-Object { + $_.Name.VariablePath.UserPath -eq 'CleanServices' + } + $cleanParam | Should -Not -BeNullOrEmpty + $cleanParam.StaticType.Name | Should -Be 'SwitchParameter' + } + + It 'Should expose a LogRetentionDays parameter' { + $logParam = $script:paramBlock.Parameters | Where-Object { + $_.Name.VariablePath.UserPath -eq 'LogRetentionDays' + } + $logParam | Should -Not -BeNullOrEmpty + } +} + +Describe 'SPSTrust example configuration' { + + It 'example config file exists' { + $script:configExample | Should -Exist + } + + It 'is a valid PowerShell data file' { + { Import-PowerShellDataFile -Path $script:configExample } | Should -Not -Throw + } + + It 'declares the required top-level keys' { + $cfg = Import-PowerShellDataFile -Path $script:configExample + foreach ($key in @('ConfigurationName', 'ApplicationName', 'Domain', 'CertFileShared', 'Trusts', 'Farms')) { + $cfg.ContainsKey($key) | Should -BeTrue + } + } + + It 'every Trust references farms that exist in the Farms inventory' { + $cfg = Import-PowerShellDataFile -Path $script:configExample + $farmNames = $cfg.Farms.Name + foreach ($trust in $cfg.Trusts) { + $trust.LocalFarm | Should -BeIn $farmNames + foreach ($remote in $trust.RemoteFarms) { + $remote | Should -BeIn $farmNames + } + } + } + + It 'every Farm entry has a Name and a Server' { + $cfg = Import-PowerShellDataFile -Path $script:configExample + foreach ($farm in $cfg.Farms) { + $farm.Name | Should -Not -BeNullOrEmpty + $farm.Server | Should -Not -BeNullOrEmpty + } + } +} From aa2a84004ab2f4a1f278edaba8f23c82fa61879f Mon Sep 17 00:00:00 2001 From: LuigiLink Date: Fri, 10 Jul 2026 13:27:15 +0200 Subject: [PATCH 3/5] ci: modernise workflows and add Pester CI - release.yml: bump actions/checkout@v4 -> @v7 and softprops/action-gh-release@v2 -> @v3; package the *contents* of src/ (archive extracts to SPSTrust.ps1 + Modules/, no src/ wrapper); add explicit 'permissions: contents: write'. - wiki.yml: bump actions/checkout@v4 -> @v7. - pester.yml (new): run the Pester suite and a PSScriptAnalyzer code-quality job on pull requests touching src/, tests/ or PSScriptAnalyzerSettings.psd1. Refs #4 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pester.yml | 93 +++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 17 +++++-- .github/workflows/wiki.yml | 4 +- 3 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/pester.yml diff --git a/.github/workflows/pester.yml b/.github/workflows/pester.yml new file mode 100644 index 0000000..4b5dd97 --- /dev/null +++ b/.github/workflows/pester.yml @@ -0,0 +1,93 @@ +# This is the SPSTrust CI Pester workflow to run tests on pull requests + +name: SPSTrust CI Pester Tests + +on: + pull_request: + branches: + - main + paths: + - 'src/**' + - 'tests/**' + - 'PSScriptAnalyzerSettings.psd1' + +# The "Publish Test Results" step creates a check run, which requires write +# access to checks. The default GITHUB_TOKEN is read-only on repositories +# created with the modern default, so grant the minimum needed here. +permissions: + contents: read + checks: write + pull-requests: write + +jobs: + test: + name: Run Pester Tests + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Install Pester + shell: pwsh + run: | + Install-Module -Name Pester -MinimumVersion 5.3.0 -Force -SkipPublisherCheck -Scope CurrentUser + Import-Module Pester + + - name: Run Pester Tests + shell: pwsh + run: | + $config = New-PesterConfiguration + $config.Run.Path = './tests' + $config.Run.Exit = $true + $config.TestResult.Enabled = $true + $config.TestResult.OutputPath = './test-results.xml' + $config.Output.Verbosity = 'Detailed' + + Invoke-Pester -Configuration $config + + - name: Upload test results + uses: actions/upload-artifact@v7 + if: always() + with: + name: test-results + path: test-results.xml + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action/composite@v2 + if: always() + continue-on-error: true + with: + files: | + test-results.xml + check_name: Pester Test Results + + code-quality: + name: Code Quality Check + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Install PSScriptAnalyzer + shell: pwsh + run: | + Install-Module -Name PSScriptAnalyzer -Force -SkipPublisherCheck -Scope CurrentUser + + - name: Run PSScriptAnalyzer + shell: pwsh + run: | + # Analyze our own code only: the entry script and the SPSTrust.Common module. + $results = @() + $results += Invoke-ScriptAnalyzer -Path ./src/SPSTrust.ps1 -Settings ./PSScriptAnalyzerSettings.psd1 + $results += Invoke-ScriptAnalyzer -Path ./src/Modules/SPSTrust.Common -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 + + if ($results) { + $results | Format-Table -AutoSize + Write-Error "PSScriptAnalyzer found issues" + exit 1 + } + else { + Write-Host "No issues found by PSScriptAnalyzer" + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 530c511..5c86f66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,8 @@ -# This is a basic workflow to help you get started with Actions +# This is the SPSTrust CI Release workflow to create a release from a tag push. +# Triggered by pushing a v* tag (e.g. v2.0.0). It packages the *contents* of the +# src/ folder into a ZIP (so the archive extracts straight to SPSTrust.ps1 and +# Modules/, with no src/ wrapper) and publishes a GitHub Release using +# RELEASE-NOTES.md as the body. name: SPSTrust CI Release @@ -8,6 +12,9 @@ on: tags: - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 +permissions: + contents: write + jobs: build: runs-on: ubuntu-latest @@ -15,17 +22,17 @@ jobs: # Checkout code - name: Checkout code id: checkout_code - uses: actions/checkout@v4 + uses: actions/checkout@v7 # Create a ZIP file with project name and tag version - - name: Create ZIP file of scripts + - name: Create ZIP file of src contents run: | zip_name="SPSTrust-${{ github.ref_name }}.zip" - zip -r $zip_name scripts/ + ( cd src && zip -r "../$zip_name" . ) shell: bash # Create Release with tag version - name: Create GitHub Release id: create_release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml index d82068e..6e07090 100644 --- a/.github/workflows/wiki.yml +++ b/.github/workflows/wiki.yml @@ -21,13 +21,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: ${{github.repository}} path: ${{github.repository}} - name: Checkout Wiki - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: ${{github.repository}}.wiki path: ${{github.repository}}.wiki From 147c2a57c150df7e80ba765e98d18ff23688bfbd Mon Sep 17 00:00:00 2001 From: LuigiLink Date: Fri, 10 Jul 2026 13:29:17 +0200 Subject: [PATCH 4/5] docs: enrich wiki (sidebar + release process), fix bugs, lighten README Wiki (#5): - Add _Sidebar.md navigation and Release-Process.md (SemVer policy, manifest-driven version, tag/release checklist) mirroring the SPSCleanDependencies standard. - Rewrite Home, Getting-Started, Configuration and Usage for the 2.0.0 layout: src/ package, .psd1 configuration (Import-PowerShellDataFile), module overview, four-stage flow, and the -LogRetentionDays parameter. - Fix copy/paste bugs: Usage referenced .\SPSWeather.ps1 (now SPSTrust.ps1) and the -CleanServices example omitted the mandatory -FarmAccount. README (#6): - Remove the inaccurate 'class-based resources' wording (SPSTrust is a script, not a DSC module); keep it short and defer to the wiki. Refs #5, #6 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 38 +++++++----- wiki/Configuration.md | 128 +++++++++++++++++++++------------------- wiki/Getting-Started.md | 53 +++++++++++++---- wiki/Home.md | 26 ++++++-- wiki/Release-Process.md | 100 +++++++++++++++++++++++++++++++ wiki/Usage.md | 43 ++++++++++---- wiki/_Sidebar.md | 16 +++++ 7 files changed, 300 insertions(+), 104 deletions(-) create mode 100644 wiki/Release-Process.md create mode 100644 wiki/_Sidebar.md diff --git a/README.md b/README.md index 0f38e51..70f458f 100644 --- a/README.md +++ b/README.md @@ -3,34 +3,40 @@ ![Latest release date](https://img.shields.io/github/release-date/luigilink/SPSTrust.svg?style=flat) ![Total downloads](https://img.shields.io/github/downloads/luigilink/SPSTrust/total.svg?style=flat) ![Issues opened](https://img.shields.io/github/issues/luigilink/SPSTrust.svg?style=flat) -[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](code_of_conduct.md) +[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) ## Description -SPSTrust is a PowerShell script tool to configure trust Farm in your SharePoint environment. The script follows the documentation: [Share service applications across farms in SharePoint Server](https://learn.microsoft.com/en-us/sharepoint/administration/share-service-applications-across-farms). +SPSTrust is a PowerShell script tool to configure trust relationships between SharePoint +Server farms — exchanging STS/ROOT certificates, publishing service applications, granting +Topology and published service-application permissions, and connecting service application +proxies across farms. -It's compatible with all supported versions for SharePoint OnPremises (2016 to Subscription Edition). +It follows the Microsoft guidance [Share service applications across farms in SharePoint Server](https://learn.microsoft.com/en-us/sharepoint/administration/share-service-applications-across-farms) +and is compatible with all supported on-premises versions (SharePoint Server 2016 to +Subscription Edition). -[Download the latest release, Click here!](https://github.com/luigilink/SPSTrust/releases/latest) +[Download the latest release here!](https://github.com/luigilink/SPSTrust/releases/latest) ## Requirements -### Windows Management Framework 5.0 +- PowerShell 5.1 or later +- CredSSP configured between the servers +- Administrative privileges on the SharePoint servers +- The **same** farm service account used across all farms being trusted -Required because this module now implements class-based resources. -Class-based resources can only work on computers with Windows -Management Framework 5.0 or above. -The preferred version is PowerShell 5.1 or higher, which ships with Windows 10 or Windows Server 2016. -This is discussed further on the [SPSTrust Wiki Getting-Started](https://github.com/luigilink/SPSTrust/wiki/Getting-Started) - -## CredSSP - -Impersonation is handled using the `Invoke-Command` cmdlet in PowerShell, together with the creation of a "remote" session via `New-PSSession`. In the SPSTrust script, we authenticate as the InstallAccount and specify CredSSP as the authentication mechanism. This is explained further in the [SPSTrust Wiki Getting-Started](https://github.com/luigilink/SPSTrust/wiki/Getting-Started) +See the [Getting Started](https://github.com/luigilink/SPSTrust/wiki/Getting-Started) wiki page for details. ## Documentation -For detailed usage, configuration, and getting started information, visit the [SPSTrust Wiki](https://github.com/luigilink/SPSTrust/wiki) +For usage, configuration, and getting-started information, visit the +[SPSTrust Wiki](https://github.com/luigilink/SPSTrust/wiki): + +- [Getting Started](https://github.com/luigilink/SPSTrust/wiki/Getting-Started) +- [Configuration](https://github.com/luigilink/SPSTrust/wiki/Configuration) +- [Usage](https://github.com/luigilink/SPSTrust/wiki/Usage) +- [Release Process](https://github.com/luigilink/SPSTrust/wiki/Release-Process) ## Changelog -A full list of changes in each version can be found in the [change log](CHANGELOG.md) +A full list of changes in each version can be found in the [change log](CHANGELOG.md). diff --git a/wiki/Configuration.md b/wiki/Configuration.md index 0a339cd..fd759ed 100644 --- a/wiki/Configuration.md +++ b/wiki/Configuration.md @@ -1,94 +1,99 @@ # Configuration -To customize the script for your environment, you need to prepare a JSON configuration file. Below is a sample structure for the file: - -```json -{ - "$schema": "http://json-schema.org/schema#", - "contentVersion": "1.0.0.0", - "ConfigurationName": "PROD", - "ApplicationName": "contoso", - "Domain": "contoso.com", - "CertFileShared": "\\\\srvfileshared.contoso.com\\certsfolder", - "Trusts": [ - { - "LocalFarm": "SEARCH", - "RemoteFarms": ["CONTENT", "SERVICES"], - "Services": ["CONTOSOPRODSCH"] - }, - { - "LocalFarm": "SERVICES", - "RemoteFarms": ["CONTENT", "SEARCH"], - "Services": ["CONTOSOPRODUPS", "CONTOSOPRODMMS", "CONTOSOPRODSSA"] - }, - { - "LocalFarm": "CONTENT", - "RemoteFarms": ["SEARCH", "SERVICES"], - "Services": ["Content"] - } - ], - "Farms": [ - { - "Name": "SEARCH", - "Server": "srvcontososearch" - }, - { - "Name": "SERVICES", - "Server": "srvcontososervices" - }, - { - "Name": "CONTENT", - "Server": "srvcontosocontent" - } - ] +SPSTrust is configured with a **PowerShell data file** (`.psd1`). Copy the bundled +`Config/CONTOSO-PROD.example.psd1` to `Config/-.psd1` and adjust +the values for your environment. Below is the sample structure: + +```powershell +@{ + ConfigurationName = 'PROD' + ApplicationName = 'contoso' + Domain = 'contoso.com' + CertFileShared = '\\srvfileshared.contoso.com\certsfolder' + + Trusts = @( + @{ + LocalFarm = 'SEARCH' + RemoteFarms = @('CONTENT', 'SERVICES') + Services = @('CONTOSOPRODSCH') + } + @{ + LocalFarm = 'SERVICES' + RemoteFarms = @('CONTENT', 'SEARCH') + Services = @('CONTOSOPRODUPS', 'CONTOSOPRODMMS', 'CONTOSOPRODSSA') + } + @{ + LocalFarm = 'CONTENT' + RemoteFarms = @('SEARCH', 'SERVICES') + Services = @('Content') + } + ) + + Farms = @( + @{ Name = 'SEARCH'; Server = 'srvcontososearch' } + @{ Name = 'SERVICES'; Server = 'srvcontososervices' } + @{ Name = 'CONTENT'; Server = 'srvcontosocontent' } + ) } ``` +> [!NOTE] +> Earlier releases (1.x) used a JSON configuration file. Starting with 2.0.0 the +> configuration is a PowerShell data file (`.psd1`) loaded with `Import-PowerShellDataFile`. +> Convert your JSON to the equivalent `.psd1` hashtable shown above. + ## Configuration and Application -`ConfigurationName` is used to populate the content of `Environment` PowerShell Variable. -`ApplicationName` is used to populate the content of `Application` PowerShell Variable. +- `ConfigurationName` populates the `Environment` variable (e.g. `PROD`, `PPRD`, `DEV`). +- `ApplicationName` populates the `Application` variable (e.g. `contoso`). -## Certificate Configuration +Together they name the transcript log file (`--.log`). + +## Domain -Certificate File Shared Path: `\\srvfileshared.contoso.com\certsfolder` +`Domain` is the DNS suffix appended to each farm's `Server` value to build the fully +qualified server name targeted by the remote sessions (e.g. `srvcontososearch` + +`contoso.com` → `srvcontososearch.contoso.com`). + +## Certificate Configuration -The certificate file is stored in a shared folder, accessible to all relevant servers, ensuring secure communication across the server farms. +`CertFileShared` is the UNC path of the shared folder used to exchange STS/ROOT +certificates between farms (e.g. `\\srvfileshared.contoso.com\certsfolder`). It must be +reachable by all relevant servers. > [!IMPORTANT] -> The credential used in the FarmAccount parameter needs write permission on this file share +> The credential passed to `-FarmAccount` needs **write** permission on this file share. ## Trust Relationships -This configuration defines specific trust relationships between different server farms in the PROD environment. Each trust relationship specifies a local farm, the remote farms it trusts, and the services available to those farms. +Each entry in `Trusts` defines one **local (publishing) farm**, the **remote (consuming) +farms** that trust it, and the **services** it exposes. ### Trust Definitions 1. **SEARCH Farm** - - **Trusted Remote Farms**: `CONTENT`, `SERVICES` - **Exposed Services**: `CONTOSOPRODSCH` - 2. **SERVICES Farm** - - **Trusted Remote Farms**: `CONTENT`, `SEARCH` - **Exposed Services**: - `CONTOSOPRODUPS` (User Profile Service) - `CONTOSOPRODMMS` (Managed Metadata Service) - `CONTOSOPRODSSA` (Search Service Application) - 3. **CONTENT Farm** - **Trusted Remote Farms**: `SEARCH`, `SERVICES` - **Exposed Service**: `Content` -These trust relationships allow each farm to communicate securely with one another, sharing specific services as needed. +> [!NOTE] +> The special service name `Content` marks a content farm that only exchanges ROOT +> trust — no STS trust, no published service application, and no proxy are created for it. > [!IMPORTANT] -> You need to use the same service account to configure trust between farms +> You must use the **same service account** to configure trust between all farms. ## Farm Server Details -Each farm is associated with a dedicated server, as outlined below: +Each farm is associated with a dedicated server: | Farm Name | Server Name | | --------- | ------------------ | @@ -96,14 +101,15 @@ Each farm is associated with a dedicated server, as outlined below: | SERVICES | srvcontososervices | | CONTENT | srvcontosocontent | -These servers are configured to handle specific workloads as per their assigned farm roles. - ## Notes -- File Path Format: Ensure that the file path syntax (`\\`) is correctly configured for shared network access. -- Farm Names: The `LocalFarm` and `RemoteFarms` properties should match the Farms names exactly to maintain trust relationships. -- Service Availability: Each farm's services are restricted to only trusted farms as specified, ensuring a secure, segmented setup for production use. +- **UNC syntax**: in a `.psd1` single-quoted string, backslashes are literal — use the + path as-is (`\\server\share`), with no JSON-style escaping. +- **Farm names**: `LocalFarm` and `RemoteFarms` values must match the `Farms[].Name` + values exactly to resolve the trust relationships. +- **Service availability**: each farm exposes its services only to the trusted farms you + list, keeping the setup segmented. ## Next Step -For the next steps, go to the [Usage](./Usage) page. +Continue to the [Usage](./Usage) page. diff --git a/wiki/Getting-Started.md b/wiki/Getting-Started.md index 062b29f..d4b7378 100644 --- a/wiki/Getting-Started.md +++ b/wiki/Getting-Started.md @@ -2,40 +2,73 @@ ## Prerequisites -- PowerShell 5.0 or later -- CredSSP configured +- PowerShell 5.1 or later +- CredSSP configured between the servers - Administrative privileges on the SharePoint Server +- The **same** farm service account across every farm you want to trust + +## How it works + +SPSTrust is an idempotent orchestration script. Given your farm topology and the +trust relationships you declare in a configuration file, it performs four stages: + +1. **Exchange certificates** — export each farm's STS and ROOT certificates to a + shared folder, then import them on the publishing farms. +2. **Publish service applications** — publish each declared service application on + its local (publishing) farm. +3. **Grant permissions** — grant the consuming farms access to the Application + Discovery and Load Balancing (Topology) service and to each published service + application. +4. **Connect proxies** — create the service application proxy on each consuming farm. + +The `-CleanServices` switch reverses every stage (remove proxies, revoke +permissions, unpublish, and remove trust). + +Shared logic lives in the `SPSTrust.Common` PowerShell module (`Modules/SPSTrust.Common`), +which the entry script imports at runtime. Remote actions are executed on the target +servers through `New-PSSession` with **CredSSP** authentication. ## Configure CredSSP ### Option 1: Manually configure CredSSP -You can manually configure CredSSP through the use of some PowerShell cmdlet's (and potentially group policy to configure the allowed delegate computers). Some basic instructions can be found at [https://technet.microsoft.com/en-us/magazine/ff700227.aspx](https://technet.microsoft.com/en-us/magazine/ff700227.aspx). +You can manually configure CredSSP through a few PowerShell cmdlets (and potentially +group policy to configure the allowed delegate computers). Some basic instructions can +be found at [Understanding CredSSP](https://learn.microsoft.com/en-us/windows/win32/secauthn/credential-security-support-provider). ### Option 2: Configure CredSSP through a DSC resource -It is possible to use a DSC resource to configure your CredSSP settings on a server, and include this in all of your SharePoint server configurations. This is done through the use of the [xCredSSP](https://github.com/PowerShell/xCredSSP) resource. The below example shows how this can be used. +It is possible to use a DSC resource to configure your CredSSP settings on a server and +include this in all of your SharePoint server configurations. This is done through the +[xCredSSP](https://github.com/dsccommunity/xCredSSP) resource. The example below shows +how this can be used. ```powershell xCredSSP CredSSPServer { Ensure = "Present"; Role = "Server" } xCredSSP CredSSPClient { Ensure = "Present"; Role = "Client"; DelegateComputers = $CredSSPDelegates } ``` -In the above example, `$CredSSPDelegates` can be a wildcard name (such as "\*.contoso.com" to allow all servers in the contoso.com domain), or a list of specific servers (such as "server1", "server 2" to allow only specific servers). +In the above example, `$CredSSPDelegates` can be a wildcard name (such as `*.contoso.com` +to allow all servers in the contoso.com domain), or a list of specific servers (such as +`server1`, `server2` to allow only specific servers). ## Installation -1. [Download the latest release](https://github.com/luigilink/SPSTrust/releases/latest) and unzip to a directory on your SharePoint Server. -2. Prepare your JSON configuration file, see [Configuration](./Configuration) page for more details. -3. Run the script with the following command: +1. [Download the latest release](https://github.com/luigilink/SPSTrust/releases/latest) + and unzip it to a directory on one of your SharePoint Servers. The archive extracts to + `SPSTrust.ps1` and a `Modules/` folder. +2. Prepare your configuration file. Copy `Config/CONTOSO-PROD.example.psd1` to + `Config/-.psd1` and edit it — see the + [Configuration](./Configuration) page for details. +3. Run the script: ```powershell -.\SPSTrust.ps1 -ConfigFile 'contoso-PROD.json' -FarmAccount (Get-Credential) +.\SPSTrust.ps1 -ConfigFile '.\Config\CONTOSO-PROD.psd1' -FarmAccount (Get-Credential) ``` ## Next Step -For the next steps, go to the [Configuration](./Configuration) page. +Continue to the [Configuration](./Configuration) page. ## Change log diff --git a/wiki/Home.md b/wiki/Home.md index 65b8006..aed6d1a 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -1,13 +1,27 @@ # SPSTrust - SharePoint Trust Farm Tool -SPSTrust is a PowerShell script tool to configure trust Farm in your SharePoint environment. +SPSTrust is a PowerShell script tool to configure trust relationships between SharePoint Server farms — exchanging STS/ROOT certificates, publishing service applications, granting Topology and published service-application permissions, and connecting service application proxies across farms. + +It follows the Microsoft guidance [Share service applications across farms in SharePoint Server](https://learn.microsoft.com/en-us/sharepoint/administration/share-service-applications-across-farms) and is compatible with all supported on-premises versions (SharePoint Server 2016 to Subscription Edition). ## Key Features -- Flexible configuration via JSON files +- Declarative, JSON-free configuration via a PowerShell data file (`.psd1`) +- Idempotent: safe to re-run — it only creates what is missing +- `-CleanServices` switch to tear down published services and trust +- Shared logic packaged in the reusable `SPSTrust.Common` module +- Transcript logging with automatic retention/rotation + +## Documentation + +- [🚀 Getting Started](./Getting-Started) +- [⚙️ Configuration](./Configuration) +- [📖 Usage](./Usage) +- [📦 Release Process](./Release-Process) -For details on usage, configuration, and parameters, explore the links below: +## Requirements -- [Getting Started](./Getting-Started) -- [Configuration](./Configuration) -- [Usage](./Usage) +- PowerShell 5.1 or later +- CredSSP configured between the servers +- Administrative privileges on the SharePoint servers +- The same farm service account used across all farms being trusted diff --git a/wiki/Release-Process.md b/wiki/Release-Process.md new file mode 100644 index 0000000..71426c1 --- /dev/null +++ b/wiki/Release-Process.md @@ -0,0 +1,100 @@ +# Release Process + +This page documents how to ship a new version of SPSTrust. The process is centered on a +single source of truth — the `ModuleVersion` field of `SPSTrust.Common.psd1` — and a `v*` +git tag that triggers the GitHub release workflow. + +## Versioning policy + +SPSTrust follows [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html). + +| Bump | When | +|---|---| +| MAJOR (X.0.0) | Breaking change in the package layout, the configuration schema, or a public module function signature. | +| MINOR (X.Y.0) | New backward-compatible feature (new parameter, new public function, new capability). | +| PATCH (X.Y.Z) | Bug fix or documentation-only change. | + +## Release checklist + +### 1. Bump the version + +Edit **one** value in `src/Modules/SPSTrust.Common/SPSTrust.Common.psd1`: + +```powershell +ModuleVersion = '2.0.0' # was '1.0.0' +``` + +This single change propagates automatically to the script banner +(`$SPSTrustVersion` is read from `(Get-Module SPSTrust.Common).Version`). + +### 2. Update `CHANGELOG.md` + +Add a dated section for the version being released, following +[Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +### 3. Replace `RELEASE-NOTES.md` + +`RELEASE-NOTES.md` is used **verbatim** as the body of the GitHub Release. It must contain +**only** the section of the version being released (no stacked history). + +### 4. Validate locally + +```powershell +Import-Module .\src\Modules\SPSTrust.Common\SPSTrust.Common.psd1 -Force +(Get-Module SPSTrust.Common).Version # should match the bumped version +Invoke-Pester -Path .\tests +Invoke-ScriptAnalyzer -Path .\src -Recurse -Settings .\PSScriptAnalyzerSettings.psd1 +``` + +### 5. Commit on a release branch + +```bash +git checkout -b Release/2.0.0 +git add -A +git commit -m "release: v2.0.0" +git push -u origin Release/2.0.0 +``` + +Test the branch ZIP on a real farm first, then open a Pull Request, review, and merge to `main`. + +### 6. Tag from `main` + +```bash +git checkout main +git pull +git tag v2.0.0 +git push origin v2.0.0 +``` + +The `.github/workflows/release.yml` workflow runs automatically. It: + +1. Packages the **contents** of `src/` into `SPSTrust-v2.0.0.zip` (the archive extracts + straight to `SPSTrust.ps1` and `Modules\`, with no `src/` wrapper). +2. Publishes a GitHub Release using `RELEASE-NOTES.md` as the body. +3. Attaches the ZIP and `LICENSE` to the release. + +### 7. Verify + +- **Releases**: — the new release is listed with the expected body and ZIP. +- **Actions**: — `release.yml` and `pester.yml` ran green. +- **Wiki**: — `wiki.yml` synced any `wiki/` changes pushed in the same release. + +## Undoing a release + +If you tagged too early: + +```bash +git tag -d v2.0.0 +git push origin --delete v2.0.0 +``` + +Then delete the auto-created Release on GitHub, fix what needs fixing, commit, and re-tag from the new HEAD. + +> ⚠️ **Don't move a published tag** that has been live for more than a few minutes. Prefer publishing a `vX.Y.(Z+1)` patch release instead of rewriting `vX.Y.Z`. + +## See also + +- [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +- [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html) +- [Getting Started](Getting-Started) +- [Usage](Usage) diff --git a/wiki/Usage.md b/wiki/Usage.md index 9bf7a4c..7c3437c 100644 --- a/wiki/Usage.md +++ b/wiki/Usage.md @@ -2,24 +2,45 @@ ## Parameters -| Parameter | Description | -| ---------------- | ------------------------------------------------- | -| `-ConfigFile` | Specifies the path to the configuration file. | -| `-FarmAccount` | Specifies the service account who runs the script | -| `-CleanServices` | Remove published services on each trusted farm | +| Parameter | Required | Description | +| ------------------- | -------- | --------------------------------------------------------------------------- | +| `-ConfigFile` | Yes | Path to the `.psd1` configuration file. | +| `-FarmAccount` | Yes | Credential of the service account that runs the script (same on all farms). | +| `-CleanServices` | No | Switch. Removes published services and trust on each trusted farm. | +| `-LogRetentionDays` | No | Days of transcript logs to keep in `Logs\`. Defaults to `180`. `0` disables pruning. | -### Basic Usage Example +### Basic usage example -Run the script with a specified configuration and farm account: +Establish trust with a specified configuration and farm account: ```powershell -.\SPSWeather.ps1 -ConfigFile 'contoso-PROD.json' -FarmAccount (Get-Credential) +.\SPSTrust.ps1 -ConfigFile '.\Config\CONTOSO-PROD.psd1' -FarmAccount (Get-Credential) ``` -### Clean Services Usage Example +### Clean services usage example -Remove published services on each trusted farm: +Remove published services and trust on each trusted farm (note that `-FarmAccount` +is still required): ```powershell -.\SPSWeather.ps1 -ConfigFile 'contoso-PROD.json' -CleanServices +.\SPSTrust.ps1 -ConfigFile '.\Config\CONTOSO-PROD.psd1' -FarmAccount (Get-Credential) -CleanServices ``` + +### Custom log retention + +Keep 30 days of transcript logs instead of the default 180: + +```powershell +.\SPSTrust.ps1 -ConfigFile '.\Config\CONTOSO-PROD.psd1' -FarmAccount (Get-Credential) -LogRetentionDays 30 +``` + +## Output + +- **Transcript logs** are written to a `Logs\` folder next to `SPSTrust.ps1`, named + `--.log`. Logs older than `-LogRetentionDays` are + pruned at the end of each run. +- Progress and per-farm results are streamed to the console (and captured in the transcript). + +## Next Step + +See the [Release Process](./Release-Process) page for how new versions are shipped. diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md new file mode 100644 index 0000000..4cdbf18 --- /dev/null +++ b/wiki/_Sidebar.md @@ -0,0 +1,16 @@ +**Navigation** + +- [🏠 Home](Home) +- [🚀 Getting Started](Getting-Started) +- [⚙️ Configuration](Configuration) +- [📖 Usage](Usage) +- [📦 Release Process](Release-Process) + +--- + +**Project** + +- [Repository](https://github.com/luigilink/SPSTrust) +- [Releases](https://github.com/luigilink/SPSTrust/releases) +- [Issues](https://github.com/luigilink/SPSTrust/issues) +- [Changelog](https://github.com/luigilink/SPSTrust/blob/main/CHANGELOG.md) From 62a246590718da9cb09e98a82796afc4e8aebb91 Mon Sep 17 00:00:00 2001 From: LuigiLink Date: Fri, 10 Jul 2026 13:30:16 +0200 Subject: [PATCH 5/5] release: v2.0.0 changelog and release notes - Document the 2.0.0 major release in CHANGELOG.md (Added/Changed/Fixed/Removed), highlighting the breaking scripts/ -> src/ layout and JSON -> .psd1 config changes. - Replace RELEASE-NOTES.md with the 2.0.0 section only (used verbatim as the GitHub Release body), including an upgrade note about converting JSON config to .psd1. The module manifest ModuleVersion was already set to 2.0.0 in the restructure commit. Refs #7 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 46 ++++++++++++++++++++++++++++++++++++++ RELEASE-NOTES.md | 57 +++++++++++++++++++++++------------------------- 2 files changed, 73 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6cc406..9e3fea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,52 @@ The format is based on and uses the types of changes according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.0] - 2026-07-10 + +### Added + +- `src/Modules/SPSTrust.Common` module (single source of truth for the version via + `ModuleVersion`), split into: + - `Public/` — the 16 trust functions plus `Get-SPSServer` and a generic + `Clear-SPSLogFolder`. + - `Private/` — `Invoke-SPSCommand` (CredSSP remoting helper), hidden from callers. + - `SPSTrust.Common.psm1` loader and `SPSTrust.Common.psd1` manifest. +- `-LogRetentionDays` parameter (default 180, `0` disables pruning); transcript logs are + now rotated at the end of each run via `Clear-SPSLogFolder`. +- `src/Config/CONTOSO-PROD.example.psd1` example configuration. +- Pester test suite under `tests/` and `PSScriptAnalyzerSettings.psd1`. +- `.github/workflows/pester.yml` running the Pester suite and a PSScriptAnalyzer + code-quality job on pull requests. +- Wiki: `_Sidebar.md` navigation and `Release-Process.md`. + +### Changed + +- **BREAKING**: repository layout moved from `scripts/` to `src/`. Release archives now + extract straight to `SPSTrust.ps1` and `Modules/` (no `scripts/` wrapper). +- **BREAKING**: configuration format changed from JSON to a PowerShell data file + (`.psd1`). `-ConfigFile` now validates a `*.psd1` path and is loaded with + `Import-PowerShellDataFile`. +- The script version is now sourced from the `SPSTrust.Common` manifest instead of a + hard-coded string. +- `release.yml`: bump `actions/checkout@v4` → `@v7` and `softprops/action-gh-release@v2` + → `@v3`; package the contents of `src/`; add explicit `permissions: contents: write`. +- `wiki.yml`: bump `actions/checkout@v4` → `@v7`. +- README trimmed to defer to the wiki; removed the inaccurate "class-based resources" + wording. +- Wiki pages (Home, Getting-Started, Configuration, Usage) rewritten for the new layout + and `.psd1` configuration. + +### Fixed + +- Wiki `Usage` examples referenced `.\SPSWeather.ps1` instead of `SPSTrust.ps1`, and the + `-CleanServices` example omitted the mandatory `-FarmAccount`. + +### Removed + +- `scripts/` folder and the monolithic `sps.util.psm1` / `util.psm1` modules (replaced by + `SPSTrust.Common`). +- Unused `Clear-SPSLog` helper (superseded by `Clear-SPSLogFolder`). + ## [1.0.0] - 2023-11-05 ### Added diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 33cf1a4..a13ce49 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,39 +1,36 @@ # SPSTrust - Release Notes -## [1.0.0] - 2023-11-05 +## [2.0.0] - 2026-07-10 + +> [!IMPORTANT] +> This is a major release with breaking changes. The package layout moved from +> `scripts/` to `src/`, and the configuration format changed from JSON to a PowerShell +> data file (`.psd1`). Convert your JSON configuration to the equivalent `.psd1` hashtable +> (see the [Configuration](https://github.com/luigilink/SPSTrust/wiki/Configuration) wiki +> page) before upgrading. ### Added -- Add RELEASE-NOTES.md file -- Add CHANGELOG.md file -- Add CONTRIBUTING.md file -- Add release.yml file -- Add scripts folder with first version of SPSTrust -- README.md - - Add code_of_conduct.md badge -- Add CODE_OF_CONDUCT.md file -- Add Issue Templates files: - - 1_bug_report.yml - - 2_feature_request.yml - - 3_documentation_request.yml - - 4_improvement_request.yml - - config.yml -- Wiki Documentation in repository - Add : - - wiki/Home.md - - wiki/Getting-Started.md - - wiki/Configuration.md - - wiki/Usage.md - - .github/workflows/wiki.yml +- New reusable `SPSTrust.Common` module (Public/Private layout, manifest-driven version). +- `-LogRetentionDays` parameter with automatic transcript log rotation. +- Example `.psd1` configuration, Pester test suite, PSScriptAnalyzer settings, and a + Pester CI workflow. +- Wiki sidebar and Release Process page. ### Changed -- SPSTrust.ps1: - - Update parameter description - - Add [ValidateScript({ Test-Path $_ -and $_ -like '*.json' })] in ConfigFile parameter - - Add missing comments - - Add CleanServices : - - Publish the service application section - - Permissions on Application Discovery and Load Balancing Service Application - - Permission to a published service application for a consuming farm +- **BREAKING**: `scripts/` → `src/`; release archives extract to `SPSTrust.ps1` + `Modules/`. +- **BREAKING**: configuration is now a `.psd1` data file loaded with `Import-PowerShellDataFile`. +- Script version sourced from the module manifest. +- Workflows bumped (`checkout@v7`, `action-gh-release@v3`); README trimmed to defer to the wiki. + +### Fixed + +- Wiki `Usage` examples (wrong script name and a missing mandatory `-FarmAccount`). + +### Removed + +- `scripts/` folder, the monolithic `sps.util.psm1` / `util.psm1` modules, and the unused + `Clear-SPSLog` helper. -A full list of changes in each version can be found in the [change log](CHANGELOG.md) +A full list of changes in each version can be found in the [change log](CHANGELOG.md).