diff --git a/scripts/Deploy-PepApi.ps1 b/scripts/Deploy-PepApi.ps1 index a7325fd..92d4193 100644 --- a/scripts/Deploy-PepApi.ps1 +++ b/scripts/Deploy-PepApi.ps1 @@ -1,4 +1,4 @@ -<# +<# Deploy PepApi as a Windows Service Examples: @@ -17,6 +17,12 @@ Param( [string]$InstallDir = "C:\Services\PepApi", [string]$Urls = "http://*:8085", [switch]$OpenFirewall, + [switch]$RunAsCurrentUser, + [PSCredential]$ServiceCredential, + [string]$DomainUser, + [string]$Domain, + [string]$User, + [switch]$UseLocalSystem, [int]$PublishTimeoutSeconds = 180, [int]$ServiceStopTimeoutSeconds = 30, [int]$ServiceStartTimeoutSeconds = 30 @@ -46,6 +52,68 @@ function Ensure-Dir($path) { } } +function Resolve-TargetUser() { + if ($UseLocalSystem.IsPresent) { return $null } + if ($ServiceCredential) { return $ServiceCredential.UserName } + if ($DomainUser) { return $DomainUser } + if ($Domain -and $User) { return "$Domain\$User" } + if ($RunAsCurrentUser.IsPresent -or (-not $ServiceCredential -and -not $RunAsCurrentUser.IsPresent -and -not $UseLocalSystem.IsPresent)) { + return "$env:USERDOMAIN\$env:USERNAME" + } + return $null +} + +function Preflight-Checks() { + # Require elevation + $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + if (-not $isAdmin) { + throw "Please run this script in an elevated PowerShell (Run as Administrator)." + } + + $targetUser = Resolve-TargetUser + if ($null -ne $targetUser) { + # Best-effort check: Log on as a service right + try { + $tmp = [System.IO.Path]::GetTempFileName() + secedit /export /cfg $tmp | Out-Null + $cfg = Get-Content -LiteralPath $tmp -ErrorAction Stop + Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue + $sid = ([System.Security.Principal.NTAccount]$targetUser).Translate([System.Security.Principal.SecurityIdentifier]).Value + $line = $cfg | Where-Object { $_ -match '^SeServiceLogonRight' } + $hasRight = $false + if ($line) { + $vals = ($line -split '=')[1] + if ($vals -match [Regex]::Escape($sid)) { $hasRight = $true } + } + if (-not $hasRight) { + Write-Warning "Account '$targetUser' may not have 'Log on as a service'. If start fails, grant it via Local Security Policy > Local Policies > User Rights Assignment." + } + } catch { + Write-Warning "Could not verify 'Log on as a service' for '$targetUser'. If service start fails, grant the right and redeploy." + } + + # Heuristic: verify read access to install directory + try { + Ensure-Dir $InstallDir + $acl = Get-Acl -LiteralPath $InstallDir + $readOk = $false + foreach ($rule in $acl.Access) { + $id = $rule.IdentityReference.Value + if ($id -ieq $targetUser -or $id -match '^(BUILTIN\\Users|NT AUTHORITY\\Authenticated Users)$') { + if ($rule.AccessControlType -eq 'Allow' -and ($rule.FileSystemRights.ToString() -match 'ReadAndExecute|FullControl|Modify|Read')) { + $readOk = $true; break + } + } + } + if (-not $readOk) { + Write-Warning "Account '$targetUser' may not have read access to '$InstallDir'. If start fails, grant permissions, e.g.: icacls \"$InstallDir\" /grant \"$($targetUser):(RX)\" /t" + } + } catch { + Write-Warning "Could not verify NTFS permissions on '$InstallDir' for '$targetUser': $($_.Exception.Message)" + } + } +} + function Stop-And-DeleteService($name) { $svc = Get-Service -Name $name -ErrorAction SilentlyContinue if ($null -ne $svc) { @@ -94,7 +162,6 @@ function Publish-App() { Write-Host "Update the following settings:" -ForegroundColor Yellow Write-Host " - ConnectionStrings:PepDB (SQL Server connection)" -ForegroundColor White Write-Host " - PepSettings:NestDirectory (network path to nests)" -ForegroundColor White - Write-Host " - PepSettings:MaterialsFile (path to material.lfn)" -ForegroundColor White Write-Host "============================================" Write-Host "" } @@ -114,19 +181,61 @@ function Stop-ExeLocks($path) { } } -function Create-Service($name, $bin, $urls) { +function Create-Service($name, $bin, $urls, [PSCredential]$credential, [bool]$useCurrentUser) { $binPath = '"' + $bin + '" --urls ' + $urls Write-Host "Creating service '$name' with binPath: $binPath" + + $useUser = $false + $userName = $null + $plainPassword = $null + + if ($credential) { + $useUser = $true + $userName = $credential.UserName + $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($credential.Password) + try { + $plainPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) + } finally { + [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) + } + } elseif ($useCurrentUser) { + $useUser = $true + $userName = "$env:USERDOMAIN\$env:USERNAME" + $secure = Read-Host -AsSecureString -Prompt "Enter password for $userName" + $bstr2 = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) + try { + $plainPassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr2) + } finally { + [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr2) + } + } # Note: space after '=' is required for sc.exe syntax - sc.exe create $name binPath= "$binPath" start= auto DisplayName= "$name" | Out-Null + if ($useUser) { + Write-Host "Configuring service to run as: $userName" + sc.exe create $name binPath= "$binPath" start= auto DisplayName= "$name" obj= "$userName" password= "$plainPassword" | Out-Null + } else { + sc.exe create $name binPath= "$binPath" start= auto DisplayName= "$name" | Out-Null + } # Set recovery to restart on failure sc.exe failure $name reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null } function Start-ServiceSafe($name) { Write-Host "Starting service '$name'..." - Start-Service -Name $name - (Get-Service -Name $name).WaitForStatus('Running',[TimeSpan]::FromSeconds($ServiceStartTimeoutSeconds)) | Out-Null + try { + Start-Service -Name $name -ErrorAction Stop + } catch { + Write-Warning ("Start-Service failed: {0}" -f $_.Exception.Message) + Write-Host "Retrying start via sc.exe ..." + Start-Sleep -Seconds 2 + try { & sc.exe start $name | Out-Null } catch {} + } + try { + $svc = Get-Service -Name $name -ErrorAction Stop + $svc.WaitForStatus('Running',[TimeSpan]::FromSeconds($ServiceStartTimeoutSeconds)) | Out-Null + } catch { + Write-Warning ("Could not confirm service status: {0}" -f $_.Exception.Message) + } sc.exe query $name | Write-Host } @@ -142,9 +251,16 @@ Write-Host "Install Dir: $InstallDir" Write-Host "URLs: $Urls" Write-Host "Configuration: $PublishConfiguration" Write-Host "Open Firewall: $OpenFirewall" +Write-Host "Run As Current: $RunAsCurrentUser" +Write-Host "Credential Given: $([bool]$ServiceCredential)" +if ($DomainUser) { Write-Host "Domain User: $DomainUser" } +elseif ($Domain -or $User) { Write-Host "Domain/User: $Domain/$User" } +Write-Host "Use LocalSystem: $UseLocalSystem" Write-Host "================================================" Write-Host "" +Preflight-Checks + Stop-And-DeleteService -name $ServiceName Stop-ExeLocks -path (Join-Path $InstallDir 'PepApi.Core.exe') try { Remove-Item -LiteralPath (Join-Path $InstallDir 'PepApi.Core.exe') -Force -ErrorAction SilentlyContinue } catch {} @@ -155,7 +271,28 @@ if (-not (Test-Path -LiteralPath $exe)) { throw "Expected published executable not found: $exe" } -Create-Service -name $ServiceName -bin $exe -urls $Urls +# If domain credentials were supplied as strings, build a PSCredential +if (-not $ServiceCredential) { + $du = $null + if ($DomainUser) { + $du = $DomainUser + } elseif ($Domain -and $User) { + $du = "$Domain\$User" + } + if ($du) { + $pw = Read-Host -AsSecureString -Prompt "Enter password for $du" + try { $ServiceCredential = [pscredential]::new($du, $pw) } catch {} + } +} + +# Default to current user if nothing specified and not forced to LocalSystem +$effectiveUseCurrent = $RunAsCurrentUser.IsPresent +if (-not $ServiceCredential -and -not $effectiveUseCurrent -and -not $UseLocalSystem.IsPresent) { + Write-Host "No credentials provided. Defaulting to run as current user." -ForegroundColor Yellow + $effectiveUseCurrent = $true +} + +Create-Service -name $ServiceName -bin $exe -urls $Urls -credential $ServiceCredential -useCurrentUser $effectiveUseCurrent if ($OpenFirewall) { $port = ($Urls -split ':')[-1]