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 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/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/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/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). 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" 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 + } + } +} 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)