feat(deploy): support running service under user credentials or LocalSystem
- Add parameters: -RunAsCurrentUser, -ServiceCredential, -DomainUser, -Domain, -User, -UseLocalSystem - Preflight checks for elevation, service logon right, and directory permissions - Create service with provided credentials or prompt for current user password by default - Improve start reliability and status reporting - Update guidance output to reflect new config
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
<#
|
<#
|
||||||
Deploy PepApi as a Windows Service
|
Deploy PepApi as a Windows Service
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -17,6 +17,12 @@ Param(
|
|||||||
[string]$InstallDir = "C:\Services\PepApi",
|
[string]$InstallDir = "C:\Services\PepApi",
|
||||||
[string]$Urls = "http://*:8085",
|
[string]$Urls = "http://*:8085",
|
||||||
[switch]$OpenFirewall,
|
[switch]$OpenFirewall,
|
||||||
|
[switch]$RunAsCurrentUser,
|
||||||
|
[PSCredential]$ServiceCredential,
|
||||||
|
[string]$DomainUser,
|
||||||
|
[string]$Domain,
|
||||||
|
[string]$User,
|
||||||
|
[switch]$UseLocalSystem,
|
||||||
[int]$PublishTimeoutSeconds = 180,
|
[int]$PublishTimeoutSeconds = 180,
|
||||||
[int]$ServiceStopTimeoutSeconds = 30,
|
[int]$ServiceStopTimeoutSeconds = 30,
|
||||||
[int]$ServiceStartTimeoutSeconds = 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) {
|
function Stop-And-DeleteService($name) {
|
||||||
$svc = Get-Service -Name $name -ErrorAction SilentlyContinue
|
$svc = Get-Service -Name $name -ErrorAction SilentlyContinue
|
||||||
if ($null -ne $svc) {
|
if ($null -ne $svc) {
|
||||||
@@ -94,7 +162,6 @@ function Publish-App() {
|
|||||||
Write-Host "Update the following settings:" -ForegroundColor Yellow
|
Write-Host "Update the following settings:" -ForegroundColor Yellow
|
||||||
Write-Host " - ConnectionStrings:PepDB (SQL Server connection)" -ForegroundColor White
|
Write-Host " - ConnectionStrings:PepDB (SQL Server connection)" -ForegroundColor White
|
||||||
Write-Host " - PepSettings:NestDirectory (network path to nests)" -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 "============================================"
|
||||||
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
|
$binPath = '"' + $bin + '" --urls ' + $urls
|
||||||
Write-Host "Creating service '$name' with binPath: $binPath"
|
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
|
# 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
|
# Set recovery to restart on failure
|
||||||
sc.exe failure $name reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null
|
sc.exe failure $name reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null
|
||||||
}
|
}
|
||||||
|
|
||||||
function Start-ServiceSafe($name) {
|
function Start-ServiceSafe($name) {
|
||||||
Write-Host "Starting service '$name'..."
|
Write-Host "Starting service '$name'..."
|
||||||
Start-Service -Name $name
|
try {
|
||||||
(Get-Service -Name $name).WaitForStatus('Running',[TimeSpan]::FromSeconds($ServiceStartTimeoutSeconds)) | Out-Null
|
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
|
sc.exe query $name | Write-Host
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,9 +251,16 @@ Write-Host "Install Dir: $InstallDir"
|
|||||||
Write-Host "URLs: $Urls"
|
Write-Host "URLs: $Urls"
|
||||||
Write-Host "Configuration: $PublishConfiguration"
|
Write-Host "Configuration: $PublishConfiguration"
|
||||||
Write-Host "Open Firewall: $OpenFirewall"
|
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 "================================================"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
|
Preflight-Checks
|
||||||
|
|
||||||
Stop-And-DeleteService -name $ServiceName
|
Stop-And-DeleteService -name $ServiceName
|
||||||
Stop-ExeLocks -path (Join-Path $InstallDir 'PepApi.Core.exe')
|
Stop-ExeLocks -path (Join-Path $InstallDir 'PepApi.Core.exe')
|
||||||
try { Remove-Item -LiteralPath (Join-Path $InstallDir 'PepApi.Core.exe') -Force -ErrorAction SilentlyContinue } catch {}
|
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"
|
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) {
|
if ($OpenFirewall) {
|
||||||
$port = ($Urls -split ':')[-1]
|
$port = ($Urls -split ':')[-1]
|
||||||
|
|||||||
Reference in New Issue
Block a user