355 lines
15 KiB
PowerShell
355 lines
15 KiB
PowerShell
[CmdletBinding(SupportsShouldProcess)]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$PublishPath,
|
|
|
|
[Parameter(Mandatory)]
|
|
[ValidatePattern('^https://')]
|
|
[string]$BrokerEndpoint,
|
|
|
|
[Parameter(Mandatory)]
|
|
[ValidatePattern('^[0-9A-Fa-f ]{40,59}$')]
|
|
[string]$ClientCertificateThumbprint,
|
|
|
|
[Parameter(Mandatory)]
|
|
[ValidatePattern('^[0-9A-Fa-f ]{40,59}$')]
|
|
[string]$ServerCertificateThumbprint,
|
|
|
|
[string]$DomainNetbios = 'LCI',
|
|
|
|
[ValidateRange(2, 90)]
|
|
[int]$TimeoutSeconds = 90,
|
|
|
|
[switch]$DoNotSetAsDefaultCredentialProvider,
|
|
|
|
[switch]$InstallDotNetRuntime,
|
|
|
|
[string]$DotNetRuntimeInstallerPath
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$providerClassId = '{D789CFD8-5AD4-489F-9B83-7EB5D9D09335}'
|
|
$installRoot = Join-Path $env:ProgramFiles 'SGU\CredentialProvider'
|
|
$settingsPath = Join-Path $env:ProgramData 'SGU\CredentialProvider\settings.json'
|
|
$providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$providerClassId"
|
|
$classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32"
|
|
$defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
|
|
$interactiveLogonPolicyPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
|
|
$accountPictureSourcePath = Join-Path $PublishPath 'branding\user.png'
|
|
$accountPictureDirectory = Join-Path $env:ProgramData 'Microsoft\User Account Pictures'
|
|
$welcomeWallpaperSourcePath = Join-Path $PublishPath 'branding\darkblue.jpg'
|
|
$welcomeWallpaperScriptSourcePath = Join-Path $PublishPath 'branding\Set-SguWelcomeWallpaper.ps1'
|
|
$welcomeFontsSourcePath = Join-Path $PublishPath 'branding\fonts'
|
|
$welcomeWallpaperDirectory = Join-Path $env:ProgramData 'SGU\Branding'
|
|
|
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
|
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
throw 'Run this script from an elevated PowerShell session.'
|
|
}
|
|
|
|
function Test-DotNet10Runtime {
|
|
$dotnetCandidates = @(
|
|
(Get-Command dotnet -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -ErrorAction SilentlyContinue),
|
|
(Join-Path $env:ProgramFiles 'dotnet\dotnet.exe')
|
|
) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Leaf) } | Select-Object -Unique
|
|
|
|
foreach ($dotnet in $dotnetCandidates) {
|
|
if (& $dotnet --list-runtimes | Select-String '^Microsoft\.NETCore\.App 10\.') {
|
|
return $true
|
|
}
|
|
}
|
|
return $false
|
|
}
|
|
|
|
function Install-DefaultAccountPicture {
|
|
param([Parameter(Mandatory)][string]$SourcePath)
|
|
|
|
if (-not (Test-Path -LiteralPath $SourcePath -PathType Leaf)) {
|
|
return $false
|
|
}
|
|
|
|
Add-Type -AssemblyName System.Drawing
|
|
New-Item -ItemType Directory -Path $accountPictureDirectory -Force | Out-Null
|
|
|
|
function Save-AccountPicture {
|
|
param(
|
|
[Parameter(Mandatory)][Drawing.Image]$Image,
|
|
[Parameter(Mandatory)][string]$Path,
|
|
[Parameter(Mandatory)][Drawing.Imaging.ImageFormat]$Format
|
|
)
|
|
|
|
$stream = [IO.MemoryStream]::new()
|
|
try {
|
|
$Image.Save($stream, $Format)
|
|
[IO.File]::WriteAllBytes($Path, $stream.ToArray())
|
|
}
|
|
finally {
|
|
$stream.Dispose()
|
|
}
|
|
}
|
|
|
|
$source = [Drawing.Image]::FromFile($SourcePath)
|
|
try {
|
|
foreach ($size in @(192, 48, 40, 32)) {
|
|
$bitmap = [Drawing.Bitmap]::new($size, $size)
|
|
try {
|
|
$graphics = [Drawing.Graphics]::FromImage($bitmap)
|
|
try {
|
|
$graphics.Clear([Drawing.Color]::Transparent)
|
|
$graphics.InterpolationMode = [Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
$graphics.DrawImage($source, [Drawing.Rectangle]::new(0, 0, $size, $size))
|
|
Save-AccountPicture -Image $bitmap `
|
|
-Path (Join-Path $accountPictureDirectory "user-$size.png") `
|
|
-Format ([Drawing.Imaging.ImageFormat]::Png)
|
|
}
|
|
finally {
|
|
$graphics.Dispose()
|
|
}
|
|
}
|
|
finally {
|
|
$bitmap.Dispose()
|
|
}
|
|
}
|
|
|
|
Save-AccountPicture -Image $source `
|
|
-Path (Join-Path $accountPictureDirectory 'user.png') `
|
|
-Format ([Drawing.Imaging.ImageFormat]::Png)
|
|
Save-AccountPicture -Image $source `
|
|
-Path (Join-Path $accountPictureDirectory 'user.bmp') `
|
|
-Format ([Drawing.Imaging.ImageFormat]::Bmp)
|
|
}
|
|
finally {
|
|
$source.Dispose()
|
|
}
|
|
|
|
return $true
|
|
}
|
|
|
|
function Install-WelcomeWallpaperAssets {
|
|
if (-not (Test-Path -LiteralPath $welcomeWallpaperSourcePath -PathType Leaf) -or
|
|
-not (Test-Path -LiteralPath $welcomeWallpaperScriptSourcePath -PathType Leaf)) {
|
|
return $false
|
|
}
|
|
|
|
New-Item -ItemType Directory -Path $welcomeWallpaperDirectory -Force | Out-Null
|
|
Copy-Item -LiteralPath $welcomeWallpaperSourcePath `
|
|
-Destination (Join-Path $welcomeWallpaperDirectory 'darkblue.jpg') -Force
|
|
Copy-Item -LiteralPath $welcomeWallpaperScriptSourcePath `
|
|
-Destination (Join-Path $welcomeWallpaperDirectory 'Set-SguWelcomeWallpaper.ps1') -Force
|
|
if (Test-Path -LiteralPath $welcomeFontsSourcePath -PathType Container) {
|
|
$fontDestination = Join-Path $welcomeWallpaperDirectory 'fonts'
|
|
New-Item -ItemType Directory -Path $fontDestination -Force | Out-Null
|
|
Copy-Item -Path (Join-Path $welcomeFontsSourcePath '*') -Destination $fontDestination -Force
|
|
}
|
|
return $true
|
|
}
|
|
|
|
if (-not (Test-DotNet10Runtime)) {
|
|
if (-not $InstallDotNetRuntime) {
|
|
throw 'Microsoft .NET 10 x64 runtime is required. Re-run with -InstallDotNetRuntime or install it first.'
|
|
}
|
|
|
|
if ($DotNetRuntimeInstallerPath) {
|
|
if (-not (Test-Path -LiteralPath $DotNetRuntimeInstallerPath -PathType Leaf)) {
|
|
throw 'DotNetRuntimeInstallerPath does not exist.'
|
|
}
|
|
|
|
$runtimeInstaller = Start-Process -FilePath $DotNetRuntimeInstallerPath `
|
|
-ArgumentList @('/install', '/quiet', '/norestart') -Wait -PassThru
|
|
if ($runtimeInstaller.ExitCode -notin @(0, 1641, 3010)) {
|
|
throw "The Microsoft .NET 10 runtime installer returned $($runtimeInstaller.ExitCode)."
|
|
}
|
|
}
|
|
else {
|
|
$winget = Get-Command winget -ErrorAction SilentlyContinue
|
|
if (-not $winget) {
|
|
throw 'winget is unavailable. Supply the offline installer with -DotNetRuntimeInstallerPath.'
|
|
}
|
|
|
|
& $winget.Source install --id Microsoft.DotNet.Runtime.10 --exact --silent `
|
|
--accept-package-agreements --accept-source-agreements --disable-interactivity
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw 'winget could not install the Microsoft .NET 10 runtime.'
|
|
}
|
|
}
|
|
|
|
if (-not (Test-DotNet10Runtime)) {
|
|
throw 'The Microsoft .NET 10 runtime installation failed.'
|
|
}
|
|
}
|
|
|
|
$requiredFiles = @(
|
|
'SGU.CredentialProvider.dll',
|
|
'SGU.CredentialProvider.comhost.dll',
|
|
'SGU.CredentialProvider.runtimeconfig.json',
|
|
'SGU.CredentialProvider.deps.json',
|
|
'Lithnet.CredentialProvider.dll',
|
|
'SGU.AuthBroker.Core.dll'
|
|
)
|
|
foreach ($file in $requiredFiles) {
|
|
if (-not (Test-Path -LiteralPath (Join-Path $PublishPath $file))) {
|
|
throw "PublishPath is missing $file."
|
|
}
|
|
}
|
|
|
|
$resolvedPublishPath = (Resolve-Path -LiteralPath $PublishPath).Path.TrimEnd('\')
|
|
$packageManifest = Get-ChildItem -LiteralPath $resolvedPublishPath -Recurse -File |
|
|
Sort-Object FullName |
|
|
ForEach-Object {
|
|
$relativePath = $_.FullName.Substring($resolvedPublishPath.Length).TrimStart('\')
|
|
'{0}={1}' -f $relativePath, (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash
|
|
}
|
|
$manifestBytes = [Text.Encoding]::UTF8.GetBytes(($packageManifest -join "`n"))
|
|
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$packageHash = -join ($sha256.ComputeHash($manifestBytes) | ForEach-Object { $_.ToString('x2') })
|
|
}
|
|
finally {
|
|
$sha256.Dispose()
|
|
}
|
|
|
|
$versionId = $packageHash.Substring(0, 16)
|
|
$installPath = Join-Path $installRoot "versions\$versionId"
|
|
$completeMarker = Join-Path $installPath '.complete'
|
|
$completeMarkerValid = $false
|
|
if (Test-Path -LiteralPath $completeMarker -PathType Leaf) {
|
|
try {
|
|
$completeMarkerValid = [IO.File]::ReadAllText($completeMarker).Trim() -eq $packageHash
|
|
}
|
|
catch {
|
|
# Treat an unreadable marker as an incomplete installation. Never reuse
|
|
# a version directory unless its marker proves that every package byte
|
|
# represented by packageHash finished installing.
|
|
}
|
|
}
|
|
if ((Test-Path -LiteralPath $installPath) -and -not $completeMarkerValid) {
|
|
$installPath = '{0}-{1}' -f $installPath, ([Guid]::NewGuid().ToString('N').Substring(0, 8))
|
|
$completeMarker = Join-Path $installPath '.complete'
|
|
}
|
|
|
|
$clientThumbprint = $ClientCertificateThumbprint -replace ' ', ''
|
|
$serverThumbprint = $ServerCertificateThumbprint -replace ' ', ''
|
|
if ($clientThumbprint.Length -ne 40 -or $serverThumbprint.Length -ne 40) {
|
|
throw 'Certificate thumbprints must contain exactly 40 hexadecimal characters.'
|
|
}
|
|
$clientCertificate = Get-ChildItem Cert:\LocalMachine\My |
|
|
Where-Object Thumbprint -eq $clientThumbprint |
|
|
Select-Object -First 1
|
|
if (-not $clientCertificate -or -not $clientCertificate.HasPrivateKey) {
|
|
throw 'The client certificate with private key is not installed in LocalMachine\My.'
|
|
}
|
|
if (-not $clientCertificate.Verify()) {
|
|
throw 'The client certificate chain is not trusted or is outside its validity period. Import the issuing CA chain; for a self-signed lab certificate, trust its public .cer in LocalMachine\Root.'
|
|
}
|
|
|
|
$serverCertificate = Get-ChildItem Cert:\LocalMachine\Root, Cert:\LocalMachine\CA | Where-Object Thumbprint -eq $serverThumbprint
|
|
if (-not $serverCertificate) {
|
|
throw 'The broker server certificate or its issuing CA is not trusted by LocalMachine.'
|
|
}
|
|
|
|
if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credential Provider')) {
|
|
if (-not (Test-Path -LiteralPath $completeMarker)) {
|
|
New-Item -ItemType Directory -Path $installPath -Force | Out-Null
|
|
Copy-Item -Path (Join-Path $resolvedPublishPath '*') -Destination $installPath -Recurse -Force
|
|
[IO.File]::WriteAllText($completeMarker, $packageHash, [Text.UTF8Encoding]::new($false))
|
|
}
|
|
|
|
# The domain GPO selects the Windows default account picture. Install its
|
|
# branded bitmap during enrollment so no per-machine manual setup is needed.
|
|
Install-DefaultAccountPicture -SourcePath $accountPictureSourcePath | Out-Null
|
|
Install-WelcomeWallpaperAssets | Out-Null
|
|
|
|
New-Item -ItemType Directory -Path (Split-Path $settingsPath -Parent) -Force | Out-Null
|
|
$settingsJson = @{
|
|
BrokerEndpoint = $BrokerEndpoint
|
|
DomainNetbios = $DomainNetbios
|
|
TimeoutSeconds = $TimeoutSeconds
|
|
ClientCertificateThumbprint = $clientThumbprint
|
|
ServerCertificateThumbprint = $serverThumbprint
|
|
} | ConvertTo-Json
|
|
$utf8WithoutBom = New-Object System.Text.UTF8Encoding($false)
|
|
[System.IO.File]::WriteAllText($settingsPath, $settingsJson, $utf8WithoutBom)
|
|
|
|
$acl = Get-Acl -LiteralPath (Split-Path $settingsPath -Parent)
|
|
$acl.SetAccessRuleProtection($true, $false)
|
|
# Resolve built-in identities by SID instead of localized display names.
|
|
# "BUILTIN\Administrators" is not resolvable on every non-English client.
|
|
$systemSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-18')
|
|
$administratorsSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')
|
|
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
|
$systemSid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
|
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
|
$administratorsSid, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
|
Set-Acl -LiteralPath (Split-Path $settingsPath -Parent) -AclObject $acl
|
|
|
|
New-Item -Path $classRegistryPath -Force | Out-Null
|
|
Set-Item -Path $classRegistryPath -Value (Join-Path $installPath 'SGU.CredentialProvider.comhost.dll')
|
|
New-ItemProperty -Path $classRegistryPath -Name ThreadingModel -Value Both -PropertyType String -Force | Out-Null
|
|
|
|
New-Item -Path $providerRegistryPath -Force | Out-Null
|
|
# Windows PowerShell 5.1 interprets an UTF-8 script without a BOM using the
|
|
# current ANSI code page. Construct the middle dot from its Unicode value so
|
|
# the LogonUI registry label remains correct on every client locale.
|
|
Set-Item -Path $providerRegistryPath -Value ('La Salle {0} Acceso SGU' -f [char]0x00B7)
|
|
|
|
if (-not $DoNotSetAsDefaultCredentialProvider) {
|
|
if (-not (Test-Path -LiteralPath $defaultProviderPolicyPath)) {
|
|
New-Item -Path $defaultProviderPolicyPath -Force | Out-Null
|
|
}
|
|
New-ItemProperty -Path $defaultProviderPolicyPath `
|
|
-Name DefaultCredentialProvider `
|
|
-Value $providerClassId `
|
|
-PropertyType String `
|
|
-Force | Out-Null
|
|
}
|
|
|
|
# Do not leave a signed-out SGU identity exposed as a persistent user tile.
|
|
# The Microsoft password provider remains registered and supplies Other user.
|
|
if (-not (Test-Path -LiteralPath $interactiveLogonPolicyPath)) {
|
|
New-Item -Path $interactiveLogonPolicyPath -Force | Out-Null
|
|
}
|
|
New-ItemProperty -Path $interactiveLogonPolicyPath `
|
|
-Name DontDisplayLastUserName `
|
|
-Value 1 `
|
|
-PropertyType DWord `
|
|
-Force | Out-Null
|
|
if (-not (Test-Path -LiteralPath $defaultProviderPolicyPath)) {
|
|
New-Item -Path $defaultProviderPolicyPath -Force | Out-Null
|
|
}
|
|
New-ItemProperty -Path $defaultProviderPolicyPath `
|
|
-Name EnumerateLocalUsers `
|
|
-Value 0 `
|
|
-PropertyType DWord `
|
|
-Force | Out-Null
|
|
}
|
|
|
|
$defaultProviderConfigured = $false
|
|
try {
|
|
$defaultProviderConfigured = (Get-ItemPropertyValue `
|
|
-LiteralPath $defaultProviderPolicyPath `
|
|
-Name DefaultCredentialProvider `
|
|
-ErrorAction Stop) -eq $providerClassId
|
|
}
|
|
catch {
|
|
# An explicitly opted-out installation has no default-provider policy.
|
|
}
|
|
|
|
[pscustomobject]@{
|
|
ProviderClassId = $providerClassId
|
|
InstallPath = $installPath
|
|
SettingsPath = $settingsPath
|
|
Registered = Test-Path -LiteralPath $providerRegistryPath
|
|
DefaultProviderConfigured = $defaultProviderConfigured
|
|
LastSignedInUserHidden = (Get-ItemPropertyValue `
|
|
-LiteralPath $interactiveLogonPolicyPath `
|
|
-Name DontDisplayLastUserName) -eq 1
|
|
LocalUserEnumerationDisabled = (Get-ItemPropertyValue `
|
|
-LiteralPath $defaultProviderPolicyPath `
|
|
-Name EnumerateLocalUsers) -eq 0
|
|
SystemPasswordProviderPreserved = $true
|
|
WelcomeWallpaperAssetsInstalled =
|
|
(Test-Path -LiteralPath (Join-Path $welcomeWallpaperDirectory 'darkblue.jpg') -PathType Leaf) -and
|
|
(Test-Path -LiteralPath (Join-Path $welcomeWallpaperDirectory 'Set-SguWelcomeWallpaper.ps1') -PathType Leaf)
|
|
}
|