Stti.psm1
#Requires -Version 7 #Requires -Modules SqlServer $initialUserCertificateThumbprint = "F0425D76A28A1180C5AB667B847EE9BF161B7B81" $initialUserCertificatePassword = "0#dJB87I8UsL" | ConvertTo-SecureString -AsPlainText -Force $initialUserId = "dbfd9d8b-a443-49d2-96b0-c4a15d5dba65" $defaultPackageSource = "https://raqrepository.halvotec.de/packages/stti/v3/index.json" $defaultBasePath = "C:\STTI\" $sqlVersionSpecificParameters = @{} enum SttiInstanceRoles { Worker Web } enum LogLevels { Debug = 1 Verbose = 2 Info = 3 Warn = 4 Error = 5 } enum SttiUserRoles{ OperationsAdmin SecurityAdmin DataAdmin HealthWatcher SubmissionAppUser } enum SttiAppLogLevel{ Debug Information Warning Error Fatal } enum SttiClientCommunicationType{ Api File } enum SttiSecretStore{ SttiSecretRepository } class SttiPackage{ [System.Version] $Version [string] $Path [bool] $IsLocal SttiPackage([Version] $version, [string] $path, [bool] $isLocal){ $this.Version = $version $this.Path = $path $this.IsLocal = $isLocal } } class SttiInstanceConfig{ [string] $Name [string] $SqlServer [string] $Database [SttiInstanceRoles[]] $Roles [string] $Path [bool] $StoreDbInInstanceDataPath [string] $WorkerSslCertificateSubject [string] $WorkerSslCertificateStore [uint] $WorkerPort [string] $WebSslCertificateSubject [string] $WebSslCertificateStore [uint] $WebPort [string] $WorkerHostname [string] $EncryptionCertificateThumbprint [string] $EncryptionMasterKeyName [string] $HttpProxyAddress [bool] $WriteLogFiles #obsolete [string] hidden $ServiceUsername } class SttiInstanceInstallStatus{ [bool] $Installed [SttiInstanceRoles[]] $Roles [Datetime] $Timestamp [string] $Version SttiInstanceInstallStatus(){ $this.Installed = $false } [string] ToString(){ if ($this.Installed -eq $true){ return "Installed Version $($this.Version) with Roles [$($this.Roles)] at $($this.Timestamp)" } else { return "Not installed" } } } class SttiInstanceStatus{ [bool] $DatabaseExists [bool] $InstanceExists [SttiInstanceInstallStatus] $InstallStatus [bool] $WorkerRoleRunning [bool] $WebRoleRunning SttiInstanceStatus(){ $this.DatabaseExists = $false $this.InstanceExists = $false $this.WebRoleRunning = $false $this.WorkerRoleRunning = $false $this.InstallStatus = $null } } class SttiInstanceDefaults{ [PSCredential] $ServiceCredential [string] $CertificateThumbprint [bool] $UseLocalServiceUser } class SttiDefaults{ [PSCredential] $CustomerCredential [string] $PackageSource [hashtable] $InstanceDefaults = @{} [SttiInstanceDefaults] ForInstance($Instance) { if ($Instance -is [string]){ $InstanceName = $Instance.ToUpperInvariant() } elseif ($Instance -is [SttiInstanceConfig]) { $InstanceName = $Instance.Name } else{ throw "Parameter $Instance is of invalid type" } if (-not $this.InstanceDefaults.ContainsKey($InstanceName)){ $this.InstanceDefaults[$InstanceName] = [SttiInstanceDefaults]::new() } return $this.InstanceDefaults[$InstanceName] } } class SttiShellSession{ [SttiInstanceConfig] $Instance [int] $ApiCallTimeoutSec } class SttiSystemSettings{ [string] $InstanceName [string] $DataDirectory [bool] $UseProxy [string] $ProxyAddress [PSCustomObject] hidden $Dto SttiSystemSettings([PSCustomObject] $dto){ if ($null -eq $dto){ throw 'Parameter dto must not be null' } $this.Dto = $dto $this.InstanceName = $dto.instance $this.DataDirectory = $dto.dataDirectory $this.UseProxy = $dto.proxy.useProxy $this.ProxyAddress = $dto.proxy.address } [string] AsJson (){ return $this.Dto | ConvertTo-Json -Depth 10 } } class SttiModule{ [string] $ModuleId [string] $Name [PSCustomObject] hidden $Dto SttiModule([PSCustomObject] $dto){ if ($null -eq $dto){ throw 'Parameter dto must not be null' } $this.Dto = $dto $this.ModuleId = $dto.id $this.Name = $dto.name } static [SttiModule[]] FromArray([PSCustomObject[]] $dtoArray){ if ($null -eq $dtoArray){ return $null } return $dtoArray.ForEach({return [SttiModule]::new($_)}) } [string] AsJson (){ return $this.Dto | ConvertTo-Json -Depth 10 } } class SttiModuleSettings{ [string[]] $Environments [string] $ActiveEnvironment [string] $SourceEnvironment [PSCustomObject] $Settings [PSCustomObject] hidden $Configurations [PSCustomObject] hidden $Dto SttiModuleSettings([PSCustomObject] $dto, [string] $SourceEnvironment){ if ($null -eq $dto){ throw 'Parameter dto must not be null' } $this.Dto = $dto $this.ActiveEnvironment = $dto.currentEnvironment if (![string]::IsNullOrEmpty($SourceEnvironment)){ $this.SourceEnvironment = $SourceEnvironment } else{ $this.SourceEnvironment = $this.ActiveEnvironment } $this.Environments = $dto.configurations | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name $this.Configurations = $dto.configurations $this.Settings = $this.GetSettings($this.SourceEnvironment) } [PSCustomObject] GetSettings([string] $environment){ if (!($this.Environments?.Contains($environment) ?? $false)){ throw "Environment $environment does not exist" } return $this.Configurations.$environment.settings } static [SttiModuleSettings[]] FromArray([PSCustomObject[]] $dtoArray, [string] $SourceEnvironment){ if ($null -eq $dtoArray){ return $null } return $dtoArray.ForEach({return [SttiModuleSettings]::new($_, $SourceEnvironment)}) } [string] AsJson (){ return $this.Dto | ConvertTo-Json -Depth 10 } } class SttiModuleSecrets{ [string[]] $Environments [string] $ActiveEnvironment [string] $SourceEnvironment [PSCustomObject] $Secrets [PSCustomObject] hidden $Configurations [PSCustomObject] hidden $Dto SttiModuleSecrets([PSCustomObject] $dto, [string] $SourceEnvironment){ if ($null -eq $dto){ throw 'Parameter dto must not be null' } $this.Dto = $dto $this.ActiveEnvironment = $dto.currentEnvironment if (![string]::IsNullOrEmpty($SourceEnvironment)){ $this.SourceEnvironment = $SourceEnvironment } else{ $this.SourceEnvironment = $this.ActiveEnvironment } $this.Environments = $dto.configurations | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name $this.Configurations = $dto.configurations $this.Secrets = $this.GetSecrets($this.SourceEnvironment) } [PSCustomObject] GetSecrets([string] $environment){ if (!($this.Environments?.Contains($environment) ?? $false)){ throw "Environment $environment does not exist" } return $this.Configurations.$environment.secrets } static [SttiModuleSecrets[]] FromArray([PSCustomObject[]] $dtoArray, [string] $SourceEnvironment){ if ($null -eq $dtoArray){ return $null } return $dtoArray.ForEach({return [SttiModuleSecrets]::new($_, $SourceEnvironment)}) } [string] AsJson (){ return $this.Dto | ConvertTo-Json -Depth 10 } } class SttiClient{ [string] $ClientId [string] $DisplayName [bool] $IsActive [PSCustomObject] hidden $Dto SttiClient([PSCustomObject] $dto){ if ($null -eq $dto){ throw 'Parameter dto must not be null' } $this.Dto = $dto $this.ClientId = $dto.id $this.DisplayName = $dto.displayName $this.IsActive = $dto.isActive } static [SttiClient[]] FromArray([PSCustomObject[]] $dtoArray){ if ($null -eq $dtoArray){ return $null } return $dtoArray.ForEach({return [SttiClient]::new($_)}) } [string] AsJson (){ return $this.Dto | ConvertTo-Json -Depth 10 } } class SttiClientSettings{ [string] $ClientId [string] $DisplayName [bool] $IsActive [SttiClientCommunicationType] $Communication [PSCustomObject] hidden $Dto SttiClientSettings([PSCustomObject] $dto){ if ($null -eq $dto){ throw 'Parameter dto must not be null' } $this.Dto = $dto $this.ClientId = $dto.id $this.DisplayName = $dto.displayName $this.Communication = $dto.communication $this.IsActive = $dto.isActive } [string] AsJson (){ return $this.Dto | ConvertTo-Json -Depth 10 } } class SttiCredential{ [string] $Type [string] $CredentialId [string] $Description [PSCustomObject] hidden $Dto SttiCredential([PSCustomObject] $dto){ if ($null -eq $dto){ throw 'Parameter dto must not be null' } $this.Type = 'SttiCredential' $this.Dto = $dto $this.CredentialId = $dto.id $this.Description = $dto.description } [string] AsJson (){ return $this.Dto | ConvertTo-Json -Depth 10 } static [SttiCredential[]] FromArray([PSCustomObject[]] $dtos){ if ($null -eq $dtos){ return $null } return $dtos.ForEach( { $dto = $_ switch ($_.type) { 'SttiPasswordCredential' { [SttiUsernamePasswordCredential]::new($dto) } 'SttiCertificateCredential' { [SttiCertificateCredential]::new($dto) } Default {[SttiCertificateCredential]::new($dto)} } }) } } class SttiCertificateCredential : SttiCredential{ [string] $Thumbprint [PSCustomObject] hidden $Dto SttiCertificateCredential([PSCustomObject] $dto) : base($dto){ $this.Type = 'SttiCertificateCredential' $this.Thumbprint = $dto.thumbprint } } class SttiUsernamePasswordCredential : SttiCredential{ [string] $Username [PSCustomObject] hidden $Dto SttiUsernamePasswordCredential([PSCustomObject] $dto) : base($dto){ $this.Type = 'SttiUsernamePasswordCredential' $this.Username = $dto.username } } class SttiEncryptionKey{ [string] $KeyId [string] $Key [string] $Algorithm [bool] $Disabled [PSCustomObject] hidden $Dto SttiEncryptionKey([PSCustomObject] $dto){ if ($null -eq $dto){ throw 'Parameter dto must not be null' } $this.Dto = $dto $this.KeyId = $dto.keyId $this.Key = $dto.key $this.Algorithm = $dto.algorithm $this.Disabled = $dto.disabled } [string] AsJson (){ return $this.Dto | ConvertTo-Json -Depth 10 } static [SttiEncryptionKey[]] FromArray([PSCustomObject[]] $dtos){ if ($null -eq $dtos){ return $null } return $dtos.ForEach({[SttiEncryptionKey]::new($_)}) } } class SttiUser{ [string] $UserId [string] $DisplayName [bool] $IsActive [PSCustomObject] hidden $Dto SttiUser([PSCustomObject] $dto){ if ($null -eq $dto){ throw 'Parameter dto must not be null' } $this.Dto = $dto $this.UserId = $dto.id $this.DisplayName = $dto.displayName $this.IsActive = $dto.isActive } [string] AsJson (){ return $this.Dto | ConvertTo-Json -Depth 10 } static [SttiUser[]] FromArray([PSCustomObject[]] $dtos){ if ($null -eq $dtos){ return $null } return $dtos.ForEach({[SttiUser]::new($_)}) } } class SttiUserSettings{ [string] $UserId [string] $DisplayName [bool] $IsActive [SttiUserRoles[]] $Roles [string] $ClientIds [PSCustomObject] hidden $Dto SttiUserSettings([PSCustomObject] $dto){ if ($null -eq $dto){ throw 'Parameter dto must not be null' } $this.Dto = $dto $this.UserId = $dto.id $this.DisplayName = $dto.displayName $this.IsActive = $dto.isActive $this.Roles = Convert-ObjectToRoles($dto.roles) $this.ClientIds = $dto.clientId } [string] AsJson (){ return $this.Dto | ConvertTo-Json -Depth 10 } static [SttiUserSettings[]] FromArray([PSCustomObject[]] $dtos){ if ($null -eq $dtos){ return $null } return $dtos.ForEach({[SttiUserSettings]::new($_)}) } } class SttiSecret{ [string] $SecretId [SttiSecretStore] $SecretStore [string] $Type [SttiSecretRelationMetadata] $RelatedTo [Nullable[datetime]] $ValidFrom [Nullable[datetime]] $ValidThru [string] $CreatedBy [Nullable[datetime]] $CreatedAt [PSCustomObject] hidden $Dto SttiSecret([PSCustomObject] $dto, [SttiSecretStore] $SecretStore){ if ($null -eq $dto){ throw 'Parameter dto must not be null' } $this.Dto = $dto $this.SecretId = $dto.id $this.Type = $dto.type $this.RelatedTo = [SttiSecretRelationMetadata]::new($dto.relatedTo) $this.ValidFrom = $dto.validFrom $this.ValidThru = $dto.validThrough $this.CreatedBy = $dto.createdBy $this.CreatedAt = $dto.createdAt if ($PSBoundParameters.ContainsKey('SecretStore')){ $this.SecretStore = $SecretStore } } [string] AsJson (){ return $this.Dto | ConvertTo-Json -Depth 10 -Compress } [SttiSecretRef] GetReference(){ return [SttiSecretRef]::new($this.SecretId, $this.SecretStore.ToString()) } static [SttiSecret[]] FromArray([PSCustomObject[]] $dtos, [SttiSecretStore] $SecretStore){ if ($null -eq $dtos){ return $null } return $dtos.ForEach( { $dto = $_ switch ($_.type) { 'AsymmetricKeySecret' { [SttiAssymetricKeySecret]::new($dto, $SecretStore) } Default {[SttiSecret]::new($dto, $SecretStore)} } }) } } class SttiAssymetricKeySecret : SttiSecret{ [string] $Algorithm [Nullable[int]] $KeySize [string] hidden $PrivateKey [string] $Certificate SttiAssymetricKeySecret([PSCustomObject] $dto, [SttiSecretStore] $SecretStore) : base($dto, $SecretStore) { $this.Algorithm = $dto.algorithm $this.KeySize = $dto.keySize $this.Certificate = $dto.certificate } } class SttiSecretRef{ [string] $id [string] $storeLocation SttiSecretRef([string] $id, [string] $storeLocation){ $this.id = $id $this.storeLocation = $storeLocation } } class SttiSecretRelationMetadata{ [string] $Module [string] $Client [string] $Environment [string] $User [PSCustomObject] hidden $Dto SttiSecretRelationMetadata([PSCustomObject] $dto){ if ($null -eq $dto){ return } $this.dto = $dto $this.Module = $dto.module $this.Environment = $dto.environment $this.Client = $dto.clientId $this.User = $dto.userId } [string] AsJson (){ return $this.Dto ?? $this | ConvertTo-Json -Depth 10 -Compress } [string] ToString(){ return $this.AsJson() } } #region Installation function Test-UserHasAdminRole { #Check if the user is in the administrator group. Warns and stops if the user is not. if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { throw "You are not running this as local administrator. Run it again in an elevated prompt." } } function Test-SttiModuleConsistency { # Check if the current loaded module has a different version than the module in the shared module directory #$userModulePath = "$HOME\Documents\PowerShell\Modules" $loadedModule = (Get-Module stti) ?? (Get-Module stti -ListAvailable -All | Select-Object -Last 1) $sharedModulePath = "$($Env:ProgramFiles)\PowerShell\Modules" $latestSharedModule = Get-Module stti -ListAvailable | Where-Object {$_.Path -ilike "$sharedModulePath\*" -and $_.ModuleType -eq "Manifest"} |Sort-Object Version | Select-Object -Last 1 if ($null -eq $latestSharedModule){ Write-Warning "The stti module was not installed in the allusers scope. Use the command Install-Module stti -Scope AllUsers." } else{ if ($loadedModule.Path -inotlike "$sharedModulePath\*"){ Write-Warning "The stti module was loaded from the current users module directory." } if ($loadedModule.Version -ne $latestSharedModule.Version){ Write-Warning "The loaded stti module has a different version than the module in the AllUsers scope." $loadedModule | Format-List | Out-String -Stream | Write-Warning $latestSharedModule | Format-List | Out-String -Stream | Write-Warning } } } function Test-SttiPsVersion { # Check if required powershell version and features are active $psVersion = $PSVersionTable.PSVersion if ($psVersion.Major -lt 7){ throw "Powershell version 7 or higher is required but installed is $psVersion" } if ($psVersion.Major -eq 7 -and $psVersion.Minor -eq 0){ if (-not [ExperimentalFeature]::IsEnabled("PSNullConditionalOperators")){ Write-Warning "Enter Enable-ExperimentalFeature PSNullConditionalOperators -Scope AllUsers then restart Powershell." throw "Powershell version 7.0 requires experimental feature PSNullConditionalOperators" } } } function Install-SttiInstance { [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter()] [ValidateNotNullOrEmpty()] [SttiPackage] $Package = (Get-SttiPackage -Version latest), [Parameter(Mandatory)] [PSCredential] $ServiceCredential, [switch] $SkipCredentialTest, [switch] $SkipSslCertificateTest, [switch] $UseExistingDatabase, [switch] $UseLocalServiceUser, [switch] $SkipInstallInitialUserCertificate = ($UseExistingDatabase -and ![string]::IsNullOrEmpty((Get-SttiDefaultValues)?.ForInstance($Instance)?.CertificateThumbprint)), [string] $InstallCertificateThumbprint = ($SkipInstallInitialUserCertificate ? (Get-SttiDefaultValues)?.ForInstance($Instance)?.CertificateThumbprint : $initialUserCertificateThumbprint), [switch] $Force ) Start-SttiInstallLogFile -Instance $Instance -Level Verbose Write-SttiInstallLogHeader try { Test-UserHasAdminRole -ErrorAction Stop Write-SttiLog "Start install instance $($Instance.Name) with version $($Package.Version) from $($Package.Path)" -Level Info Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Verbose $errorAction = $Force ? "Continue" : "Stop" $serviceUsername = $ServiceCredential.UserName $installStatus = (Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop).InstallStatus if ($installStatus.Installed -eq $true){ Write-SttiLog "The instance $($Instance.Name) is already installed" -Level Error -ErrorAction $errorAction } $steps = @( @{Name="Test-Credential"; Expression={Test-Credential $ServiceCredential -ErrorAction Stop > $null}; CheckExpression={!$SkipCredentialTest}}, @{Name="Test-SttiInstanceBinding"; Expression={Test-SttiInstanceBinding $Instance -ErrorAction Stop > $null}}, @{Name="Test-SttiDatabaseConnection"; Expression={Test-SttiDatabaseConnection $Instance -ServiceCredential:$ServiceCredential -SkipPermissionCheck:$UseExistingDatabase -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}}, @{Name="Test-SttiDatabase"; Expression={Test-SttiDatabase $Instance -ServiceCredential:$ServiceCredential -UseExistingDatabase:$UseExistingDatabase -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}}, @{Name="Test-SttiSslCertificateForWorkerRole"; Expression={Test-SttiSslCertificateForRole -Instance $Instance -InstanceRole Worker -ServiceCredential:$ServiceCredential -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$SkipSslCertificateTest}}, @{Name="Test-SttiSslCertificateForWebRole"; Expression={Test-SttiSslCertificateForRole -Instance $Instance -InstanceRole Web -ServiceCredential:$ServiceCredential -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Web) -and !$SkipSslCertificateTest}}, @{Name="Write-SttiInstanceStatus"; Expression={Write-SttiInstanceStatus -Instance $Instance}}, @{Name="Clear-SttiInstanceDirectory"; Expression={Clear-SttiInstanceDirectory -Instance $Instance -ErrorAction $errorAction}}, @{Name="Build-SttiInstanceDirectory"; Expression={Build-SttiInstanceDirectory -Instance $Instance -ServiceUsername $serviceUsername -ErrorAction $errorAction}}, @{Name="Expand-SttiPackage"; Expression={Expand-SttiPackage -Instance $Instance -Package $Package -ErrorAction $errorAction}}, @{Name="Install-SttiDatabaseEncryptionCertificate"; Expression={Install-SttiDatabaseEncryptionCertificate -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$Instance.EncryptionCertificateThumbprint}}, @{Name="Edit-SttiInstanceConfigFiles"; Expression={Edit-SttiInstanceConfigFiles -Instance $Instance -ErrorAction $errorAction}}, @{Name="New-SttiDatabase"; Expression={New-SttiDatabase -Instance $instance -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$UseExistingDatabase}}, @{Name="Update-SttiDatabaseUser"; Expression={Update-SttiDatabaseUser -Instance $instance -ServiceUsername $serviceUsername -SkipServerChanges:$UseExistingDatabase -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$UseLocalServiceUser}}, @{Name="Add-SttiDatabaseEncryption"; Expression={Add-SttiDatabaseEncryption -Instance $instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}}, @{Name="Invoke-SttiDatabaseMigration"; Expression={Invoke-SttiDatabaseMigration -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}}, @{Name="Add-SttiInstanceServices"; Expression={Add-SttiInstanceServices -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}}, @{Name="Add-SttiFirewallRules"; Expression={Add-SttiFirewallRules -Instance $Instance -ErrorAction $errorAction}}, @{Name="Start-SttiInstance"; Expression={Start-SttiInstance -Instance $Instance -ErrorAction $errorAction}}, @{Name="Install-SttiInitialUserCertificate"; Expression={Install-SttiInitialUserCertificate -Instance $Instance -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$SkipInstallInitialUserCertificate}} @{Name="Update-SttiInstanceSettings"; Expression={Update-SttiInstanceSettings -Instance $Instance -CertificateThumbprint $InstallCertificateThumbprint -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}} @{Name="Restart-SttiInstance"; Expression={Restart-SttiInstance -Instance $Instance -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}} @{Name="Set-SttiInstanceInstallStatus"; Expression={Set-SttiInstanceInstallStatus -Instance $Instance -Installed $true -Roles $Instance.Roles -Version $Package.Version -ErrorAction Stop > $null}}, @{Name="Write-SttiInstanceStatus"; Expression={Write-SttiInstanceStatus -Instance $Instance}} ) Invoke-SttiDeploymentSteps -Activity "Install STTI instance $($Instance.Name)" -Steps $steps -ErrorAction $errorAction Write-SttiLog "Finished install instance $($Instance.Name)" -Extraline -Level Info } catch { Write-SttiLog "An error occurred during installation of the instance" -Level Warn Write-SttiLog $_ -Level Error Write-SttiLog $_.Exception -Level Info Write-SttiLog $_.ScriptStackTrace -Level Info } finally{ Write-SttiInstallLogFooter Stop-SttiInstallLogFile } } #Install-SttiInstance -Instance (Get-SttiInstanceConfig dev) -Package (Get-SttiPackage "latest") function Update-SttiInstance { [CmdletBinding()] Param( # Parameter help description [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [ValidateNotNullOrEmpty()] [SttiPackage] $Package = (Get-SttiPackage -VersionNumber latest), [Parameter(Mandatory)] [PSCredential] $ServiceCredential, [switch] $SkipCredentialTest, [switch] $SkipSslCertificateTest, [switch] $UseLocalServiceUser, [switch] $Force ) DynamicParam { if ($Instance.Roles.Contains([SttiInstanceRoles]::Worker)){ $attributes = [System.Collections.ObjectModel.Collection[System.Attribute]]::new() $attributes.Add([System.Management.Automation.ParameterAttribute]@{ Mandatory = $true }) $parameters = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new() $parameters.Add('CertificateThumbprint', [System.Management.Automation.RuntimeDefinedParameter]::new( 'CertificateThumbprint', [string], $attributes )) return $parameters } } Begin{ $CertificateThumbprint = $PSBoundParameters['CertificateThumbprint'] } Process{ Start-SttiInstallLogFile -Instance $Instance -Level Verbose Write-SttiInstallLogHeader try { Test-UserHasAdminRole -ErrorAction Stop Write-SttiLog "Start update instance $($Instance.Name) with version $($Package.Version) from $($Package.Path)" -Level Info Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Verbose $errorAction = $Force ? "Continue" : "Stop" $serviceUsername = $ServiceCredential.UserName $installStatus = (Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop).InstallStatus if ($installStatus.Installed -eq $false -and $Force -eq $false){ Write-SttiLog "The instance $($Instance.Name) is not installed" -Level Error -ErrorAction $errorAction } $steps = @( @{Name="Test-Credential"; Expression={Test-Credential $ServiceCredential -ErrorAction Stop > $null}; CheckExpression={!$SkipCredentialTest}}, @{Name="Test-SttiInstanceBinding"; Expression={Test-SttiInstanceBinding $Instance -ErrorAction Stop > $null}}, @{Name="Test-SttiDatabaseConnection"; Expression={Test-SttiDatabaseConnection $Instance -ServiceCredential:$ServiceCredential -SkipPermissionCheck -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}}, @{Name="Test-SttiDatabase"; Expression={Test-SttiDatabase $Instance -ServiceCredential:$ServiceCredential -UseExistingDatabase -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}}, @{Name="Test-SttiSslCertificateForWorkerRole"; Expression={Test-SttiSslCertificateForRole -Instance $Instance -InstanceRole Worker -ServiceCredential:$ServiceCredential -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$SkipSslCertificateTest}}, @{Name="Test-SttiSslCertificateForWebRole"; Expression={Test-SttiSslCertificateForRole -Instance $Instance -InstanceRole Web -ServiceCredential:$ServiceCredential -ErrorAction Stop > $null}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Web) -and !$SkipSslCertificateTest}}, @{Name="Write-SttiInstanceStatus"; Expression={Write-SttiInstanceStatus -Instance $Instance}}, @{Name="Stop-SttiInstance"; Expression={Stop-SttiInstance -Instance $Instance -ErrorAction $errorAction}}, @{Name="Clear-SttiInstanceDirectory"; Expression={Clear-SttiInstanceDirectory -Instance $Instance -ErrorAction $errorAction}}, @{Name="Build-SttiInstanceDirectory"; Expression={Build-SttiInstanceDirectory -Instance $Instance -ServiceUsername $serviceUsername -ErrorAction $errorAction}}, @{Name="Expand-SttiPackage"; Expression={Expand-SttiPackage -Instance $Instance -Package $Package -ErrorAction $errorAction}}, @{Name="Edit-SttiInstanceConfigFiles"; Expression={Edit-SttiInstanceConfigFiles -Instance $Instance -ErrorAction $errorAction}}, @{Name="Update-SttiDatabaseUser"; Expression={Update-SttiDatabaseUser -Instance $instance -ServiceUsername $serviceUsername -SkipServerChanges -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker) -and !$UseLocalServiceUser}}, @{Name="Add-SttiDatabaseEncryption"; Expression={Add-SttiDatabaseEncryption -Instance $instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}}, @{Name="Invoke-SttiDatabaseMigration"; Expression={Invoke-SttiDatabaseMigration -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}}, @{Name="Add-SttiInstanceServices"; Expression={Add-SttiInstanceServices -Instance $Instance -ServiceCredential $ServiceCredential -ErrorAction $errorAction}}, @{Name="Add-SttiFirewallRules"; Expression={Add-SttiFirewallRules -Instance $Instance -ErrorAction $errorAction}}, @{Name="Start-SttiInstance"; Expression={Start-SttiInstance -Instance $Instance -ErrorAction $errorAction}}, @{Name="Update-SttiInstanceSettings"; Expression={Update-SttiInstanceSettings -Instance $Instance -CertificateThumbprint $CertificateThumbprint -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}}, @{Name="Restart-SttiInstance"; Expression={Restart-SttiInstance -Instance $Instance -ErrorAction $errorAction}; CheckExpression={$Instance.Roles.Contains([SttiInstanceRoles]::Worker)}} @{Name="Set-SttiInstanceInstallStatus"; Expression={Set-SttiInstanceInstallStatus -Instance $Instance -Installed $true -Roles $Instance.Roles -Version $Package.Version -ErrorAction Stop > $null}}, @{Name="Write-SttiInstanceStatus"; Expression={Write-SttiInstanceStatus -Instance $Instance}} ) Invoke-SttiDeploymentSteps -Activity "Update STTI instance $($Instance.Name)" -Steps $steps -ErrorAction $errorAction Write-SttiLog "Finished update instance $($Instance.Name)" -Extraline -Level Info } catch { Write-SttiLog "An error occurred during update of the instance" -Level Warn Write-SttiLog $_ -Level Error Write-SttiLog $_.Exception -Level Info Write-SttiLog $_.ScriptStackTrace -Level Info } finally{ Write-SttiInstallLogFooter Stop-SttiInstallLogFile } } } #Update-SttiInstance -Instance (Get-SttiInstanceConfig dev) -Package (Get-SttiPackage "latest") function Uninstall-SttiInstance { [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [switch] $Force ) Start-SttiInstallLogFile -Instance $Instance -Level Verbose Write-SttiInstallLogHeader try { Test-UserHasAdminRole -ErrorAction Stop Write-SttiLog "Start uninstall instance $($Instance.Name)" -Level Info Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Verbose $errorAction = $Force ? "Continue" : "Stop" $installStatus = (Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop).InstallStatus if ($installStatus.Installed -eq $false -and $Force -eq $false){ Write-SttiLog "The instance $($Instance.Name) is not installed" -Level Error -ErrorAction $errorAction } $steps = @( @{Name="Write-SttiInstanceStatus"; Expression={Write-SttiInstanceStatus -Instance $Instance}}, @{Name="Stop-SttiInstance"; Expression={Stop-SttiInstance -Instance $Instance -ErrorAction $errorAction}}, @{Name="Remove-SttiInstanceServices"; Expression={Remove-SttiInstanceServices -Instance $Instance -ErrorAction $errorAction}}, @{Name="Remove-SttiFirewallRules"; Expression={Remove-SttiFirewallRules -Instance $Instance -ErrorAction $errorAction}}, @{Name="Clear-SttiInstanceDirectory"; Expression={Clear-SttiInstanceDirectory -Instance $Instance -ErrorAction $errorAction}}, @{Name="Set-SttiInstanceInstallStatus"; Expression={Set-SttiInstanceInstallStatus -Instance $Instance -Installed $false -Roles $null -Version $null -ErrorAction Stop > $null}}, @{Name="Write-SttiInstanceStatus"; Expression={Write-SttiInstanceStatus -Instance $Instance}} ) Invoke-SttiDeploymentSteps -Activity "Uninstall STTI instance $($Instance.Name)" -Steps $steps -ErrorAction $errorAction Write-SttiLog "Finished uninstall instance $($Instance.Name)" -Extraline -Level Info } catch { Write-SttiLog "An error occurred during uninstall of the instance" -Level Warn Write-SttiLog $_ -Level Error Write-SttiLog $_.Exception -Level Info Write-SttiLog $_.ScriptStackTrace -Level Info } finally{ Write-SttiInstallLogFooter Stop-SttiInstallLogFile } } #Uninstall-SttiInstance -Instance (Get-SttiInstanceConfig dev) function Invoke-SttiDeploymentSteps{ [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Activity, [Parameter(Mandatory)] [pscustomobject] $Steps ) try{ # check if the prerequisites for the step are met $checkPrereqs = { param( [PSCustomObject] $Step ) if ($Step.CheckExpression){ if (!(& $Step.CheckExpression)){ Write-SttiLog "Prerequisite expression {$($Step.CheckExpression)} for step $($Step.Name) evaluated to false" -Level Verbose return $false } else{ return $true } } return $true } $filteredSteps = $steps.Where({& $checkPrereqs -Step $_ }) for ($i = 0; $i -lt $filteredSteps.Count; $i++){ $step = $filteredSteps[$i] Write-Progress -Id 1 -Activity $activity -CurrentOperation $step.Name -PercentComplete (($i + 1) / $filteredSteps.Count * 100) & $step.Expression } } finally{ Write-Progress -Id 1 -Activity $activity -Completed } } function Get-SttiInstanceInstallStatus{ [OutputType([SttiInstanceInstallStatus])] [CmdletBinding()] Param( [Parameter(Mandatory, ParameterSetName="InstanceName")] [string] $Name, [Parameter(Mandatory, ParameterSetName="Instance")] [SttiInstanceConfig] $Instance, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [X509Certificate] $Certificate ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $installStatus = [SttiInstanceInstallStatus]::new() if ($null -eq $Instance){ $Instance = Get-SttiInstanceConfig -Name $Name -ErrorAction Stop if ($null -eq $Instance){ return $installStatus } } $instanceInstallStatusFile = Get-SttiInstanceInstallStatusFileName -Name $Instance.Name if (-not (Test-Path $instanceInstallStatusFile)){ Write-SttiLog "The instance installation status $instanceInstallStatusFile does not exists" -Level Verbose return $installStatus } return [SttiInstanceInstallStatus](Get-Content $instanceInstallStatusFile -Raw | ConvertFrom-Json) } #Get-SttiInstanceInstallStatus -Name dev #Get-SttiInstanceInstallStatus -Instance (Get-SttiInstanceConfig -Name dev) function Set-SttiInstanceInstallStatus{ [OutputType([SttiInstanceInstallStatus])] [CmdletBinding()] Param( [Parameter(Mandatory, ParameterSetName="InstanceName")] [string] $Name, [Parameter(Mandatory, ParameterSetName="Instance")] [SttiInstanceConfig] $Instance, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [bool] $Installed, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [string] $Version, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [SttiInstanceRoles[]] $Roles ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug if ($null -ne $Instance){ $Name = $Instance.Name } $config = Get-SttiInstanceConfig -Name $Name -ErrorAction Stop if ($null -eq $config){ throw "The instance configuration does not exist" } $installStatus = Get-SttiInstanceInstallStatus -Name $Name -ErrorAction Stop if ($PSBoundParameters.ContainsKey("Installed")){ $installStatus.Installed = $Installed } if ($PSBoundParameters.ContainsKey("Version")){ $installStatus.Version = $Version } if ($PSBoundParameters.ContainsKey("Roles")){ $installStatus.Roles = $Roles } $installStatus.Timestamp = Get-Date $instanceInstallStatusFile = Get-SttiInstanceInstallStatusFileName $Name $installStatus | ConvertTo-Json -ErrorAction Stop | Out-File $instanceInstallStatusFile -Encoding UTF8 -ErrorAction Stop $PSCmdlet.WriteObject($installStatus) } #Set-SttiInstanceInstallStatus -Name dev -Installed $true -Roles Web, Worker -Version 1.0.0.0 #Set-SttiInstanceInstallStatus -Name dev -Installed $false -Roles @() -Version $null function Clear-SttiInstanceDirectory{ [CmdletBinding()] Param( # Parameter help description [Parameter(Mandatory)] [SttiInstanceConfig] $Instance ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $instanceDirectory = Get-Item -Path $Instance.Path -ErrorAction Stop $instanceDirectory | Get-ChildItem -Exclude "data", "config.json", "install.json" -ErrorAction Stop | Remove-Item -Recurse -Force -ErrorAction Stop } catch{ Write-SttiLog $_ -Level Error } } #Clear-SttiInstanceDirectory -Instance (Get-SttiInstanceConfig dev) function Build-SttiInstanceDirectory{ [CmdletBinding()] Param( # Parameter help description [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [string] $ServiceUsername ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $instanceDirectory = Get-Item -Path $Instance.Path New-Item -Path $instanceDirectory -Name "bin" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null $dataDirectory = Get-Item -Path ([System.IO.Path]::Combine($instanceDirectory, "data")) -ErrorAction SilentlyContinue || New-Item -Path $instanceDirectory -Name "data" -ItemType Directory -ErrorAction Stop New-Item -Path $dataDirectory -Name "logs" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null New-Item -Path $dataDirectory -Name "clients" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null if ($Instance.StoreDbInInstanceDataPath -eq $true){ New-Item -Path $dataDirectory -Name "db" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null } # Grant permissions for service user # Read permissions for instance directory Write-SttiLog "Set instance directory permissions" -Level Verbose $instanceDirectoryAcl = Get-Acl -Path $instanceDirectory -ErrorAction Stop $instanceDirectoryAcl.SetAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new($ServiceUsername, "Read", [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit, [System.Security.AccessControl.PropagationFlags]::None, "Allow")) Set-Acl -Path $instanceDirectory -AclObject $instanceDirectoryAcl -ErrorAction Stop # Full control for data directory Write-SttiLog "Set data directory permissions" -Level Verbose $dataDirectoryAcl = Get-Acl -Path $dataDirectory -ErrorAction Stop $dataDirectoryAcl.SetAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new($ServiceUsername, "FullControl", [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit, [System.Security.AccessControl.PropagationFlags]::None, "Allow")) Set-Acl -Path $dataDirectory -AclObject $dataDirectoryAcl -ErrorAction Stop } catch{ Write-SttiLog $_ -Level Error } } #Build-SttiInstanceDirectory -Instance (Get-SttiInstanceConfig dev) function Expand-SttiPackage{ [CmdletBinding()] Param( # Parameter help description [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [SttiPackage] $Package ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $tempPath = [System.IO.Path]::GetTempPath() $tempWorkingDir = New-Item -Name (Get-Random) -Path $tempPath -ItemType Directory -ErrorAction Stop Expand-Archive $Package.Path -DestinationPath $tempWorkingDir -ErrorAction Stop $tempContentPath = [System.IO.Path]::Combine($tempWorkingDir.FullName, "Content") $binPath = [System.IO.Path]::Combine($Instance.Path, "bin\") Get-ChildItem -Path $tempContentPath | Copy-Item -Destination $binPath -Recurse -Force -ErrorAction Stop $tempWorkingDir | Remove-Item -Recurse -Force -ErrorAction Continue } catch{ Write-SttiLog $_ -Level Error } } #Expand-SttiPackage -Instance (Get-SttiInstanceConfig dev) -Package (Get-SttiPackage 0.0.0.1) function Edit-SttiInstanceConfigFiles{ [CmdletBinding()] Param( # Parameter help description [Parameter(Mandatory)] [SttiInstanceConfig] $Instance ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $sqlConnectionString = Get-SttiDatabaseConnectionString -Instance $Instance try{ # Set database connection Get-ChildItem $Instance.Path -Exclude "data" -Include "appsettings.json" -Recurse -PipelineVariable "currentFile" -ErrorAction Stop | Copy-Item -Destination {"$($_.FullName).bak"} -Force -PassThru -PipelineVariable "backupFile" -ErrorAction Stop # create backup file | ForEach-Object { Get-Content -Path $currentFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } # Read content as string in one block and deserialize json | ForEach-Object { if ($currentFile.FullName -like "*\halvotec.stti.worker\*"){ $port = $Instance.WorkerPort $sslCertificateSubject = $Instance.WorkerSslCertificateSubject $sslCertificateStore = $Instance.WorkerSslCertificateStore } elseif ($currentFile.FullName -like "*\halvotec.stti.web\*"){ $port = $Instance.WebPort $sslCertificateSubject = $Instance.WebSslCertificateSubject $sslCertificateStore = $Instance.WebSslCertificateStore } if ($_.Stti -and $_.Stti.Connections -and $_.Stti.Connections.DBConnection) { $_.Stti.Connections.DBConnection = $sqlConnectionString } if ($_.SttiWeb -and $_.SttiWeb.SttiWorkerService ) { $_.SttiWeb.SttiWorkerService = "https://$($Instance.WorkerHostname):$($Instance.WorkerPort)" } if ($_.Kestrel){ if ($_.Kestrel.Endpoints -and $_.Kestrel.Endpoints.HttpsDefault -and $_.Kestrel.Endpoints.HttpsDefault.Url){ $_.Kestrel.Endpoints.HttpsDefault.Url = "https://*:$($port)" } if ($_.Kestrel.Certificates -and $_.Kestrel.Certificates.Default){ $_.Kestrel.Certificates.Default = [PSCustomObject]@{ Subject = $sslCertificateSubject Store = "My" Location = $sslCertificateStore AllowInvalid = $true } } } if ($currentFile.FullName -like "*\halvotec.stti.worker\*" -and $_.Stti -and $null -ne $_.Stti.FileLoggingEnabled){ $_.Stti.FileLoggingEnabled = $Instance.WriteLogFiles $_.Stti.LogsFolder = "$($Instance.Path)\data\logs" } $_ } # Replace connection string | ForEach-Object { ConvertTo-Json -InputObject $_ -Depth 50 -ErrorAction Stop | Out-File $currentFile -Encoding utf8 -ErrorAction Stop } # Serialize to Json and write to file | ForEach-Object { Remove-Item $backupFile -ErrorAction Continue } # Remove backup file } catch{ Write-SttiLog $_ -Level Error } } #Edit-SttiInstanceConfigFiles -Instance (Get-SttiInstanceConfig dev) function Add-SttiInstanceServices{ [CmdletBinding()] Param( # Parameter help description [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [PSCredential] $ServiceCredential ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ Write-SttiLog "Grant LogonAsService right to service user" -Level Verbose Grant-LogonAsServiceRight $ServiceCredential.UserName -ErrorAction Stop $workerServiceName = (Get-SttiServiceName -Instance $Instance -Role Worker) if ($Instance.Roles.Contains([SttiInstanceRoles]::Worker)){ $workerDependencies = @() # check if sql server is a local instance $sqlServerParsed = [Regex]::Match($Instance.SqlServer,'([^\\]+)(?:\\([^\\]+))?') if ($sqlServerParsed.Success){ ($null, $sqlComputername, $sqlInstance) = $sqlServerParsed.Groups.Value if ([string]::IsNullOrWhiteSpace($sqlInstance)){ $sqlInstance = 'MSSQLSERVER' } $isDatabaseLocal = ($sqlComputername -like "localhost*" -or $sqlComputername -like "(local)*" -or $sqlComputername -like "$($env:COMPUTERNAME)*") } else{ Write-SttiLog "SqlServer name $($Instance.SqlServer) could not be parsed" -Level Warn } if ($isDatabaseLocal){ Write-SttiLog "SqlServer $($Instance.SqlServer) is local" -Level Verbose $sqlLocalInstances = Get-ItemProperty 'HKLM:\Software\Microsoft\Microsoft SQL Server\Instance Names\SQL' -ErrorAction SilentlyContinue | Get-Member -Type Properties | Where-Object Name -NotLike 'PS*' | Select-Object -ExpandProperty Name $foundSqlInstance = $sqlLocalInstances -icontains $sqlInstance if ($foundSqlInstance ){ $sqlServiceName = ($sqlInstance -like "MSSQLSERVER" ? "MSSQLSERVER" : "MSSQL`$$sqlInstance") if ($null -ne (Get-Service -Name $sqlServiceName -ErrorAction SilentlyContinue)){ $workerDependencies += $sqlServiceName } else{ Write-SttiLog "Could not find local sql server service $sqlServiceName for instance $sqlInstance" -Level Warn } } else{ Write-SttiLog "Sql server instance $sqlInstance could not be found in local instances $sqlLocalInstances" -Level Warn } } if ($null -eq (Get-Service -Name $workerServiceName -ErrorAction SilentlyContinue)){ $workerBinaryPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Worker\Halvotec.Stti.Worker.exe") Write-SttiLog "Create service for worker role" -Level Verbose New-Service -Name $workerServiceName -BinaryPathName $workerBinaryPath -StartupType Automatic -Credential $ServiceCredential -Description "Halvotec STTI $($Instance.Name) Worker" -ErrorAction Stop > $null } else{ Write-SttiLog "Update service for worker role" -Level Verbose Set-Service -Name $workerServiceName -Credential $ServiceCredential -ErrorAction Stop > $null } try{ Write-SttiLog "Set worker service dependencies $workerDependencies" -Level Verbose # Array with empty string is necessary to reset dependencies if ($workerDependencies.count -eq 0){ $workerDependencies = @('') } $workerServiceCimInstance = Get-CimInstance win32_Service -Filter "Name='$workerServiceName'" $workerServiceCimInstance | Invoke-CimMethod -MethodName Change -Arguments @{ServiceDependencies=$workerDependencies} > null } catch{ Write-SttiLog "Could not set worker service dependencies: $_" -Level Warn } } elseif ($null -ne (Get-Service -Name $workerServiceName -ErrorAction SilentlyContinue)){ Write-SttiLog "Remove service for worker role" -Level Verbose Remove-Service -Name $workerServiceName -ErrorAction Stop } $webServiceName = (Get-SttiServiceName -Instance $Instance -Role Web) if ($Instance.Roles.Contains([SttiInstanceRoles]::Web)){ if ($null -eq (Get-Service -Name $webServiceName -ErrorAction SilentlyContinue)){ $webBinaryPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Web\Halvotec.Stti.Web.exe") Write-SttiLog "Create service for web role" -Level Verbose New-Service -Name $webServiceName -BinaryPathName $webBinaryPath -StartupType Automatic -Credential $ServiceCredential -Description "Halvotec STTI $($Instance.Name) Web" -ErrorAction Stop > $null } else{ Write-SttiLog "Update service for web role" -Level Verbose Set-Service -Name $webServiceName -Credential $ServiceCredential -ErrorAction Stop > $null } } elseif ($null -ne (Get-Service -Name $webServiceName -ErrorAction SilentlyContinue)){ Write-SttiLog "Remove service for web role" -Level Verbose Remove-Service -Name $webServiceName -ErrorAction Stop } } catch{ Write-SttiLog $_ -Level Error } } #Install-SttiInstanceServices -Instance (Get-SttiInstanceConfig dev) -ServiceCredential (Get-Credential) function Remove-SttiInstanceServices{ [CmdletBinding()] Param( # Parameter help description [Parameter(Mandatory)] [SttiInstanceConfig] $Instance ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $workerServiceName = (Get-SttiServiceName -Instance $Instance -Role Worker) Write-SttiLog "Remove service for worker" -Level Verbose Get-Service -Name $workerServiceName -ErrorAction SilentlyContinue | Remove-Service -ErrorAction Stop } catch{ Write-SttiLog $_ -Level Error } try{ $webServiceName = (Get-SttiServiceName -Instance $Instance -Role Web) Write-SttiLog "Remove service for web" -Level Verbose Get-Service -Name $webServiceName -ErrorAction SilentlyContinue | Remove-Service -ErrorAction Stop } catch{ Write-SttiLog $_ -Level Error } } #Uninstall-SttiInstanceServices -Instance (Get-SttiInstanceConfig dev) function Get-SttiServiceName{ [OutputType([string])] [CmdletBinding()] Param( # Parameter help description [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [SttiInstanceRoles] $Role ) return "STTI.$($Instance.Name).$Role" } function Update-SttiInstanceSettings{ [CmdletBinding()] Param( # Parameter help description [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ Retry-Command -ScriptBlock { Set-SttiSystemSettings -Instance $Instance -CertificateThumbprint $CertificateThumbprint ` -InstanceName $Instance.Name -DataDirectory "$($Instance.Path)\data" ` -UseProxy (![string]::IsNullOrEmpty($Instance.HttpProxyAddress)) -ProxyAddress $Instance.HttpProxyAddress ` -ErrorAction Stop > $null } -RetryCount 5 -TimeoutInSecs 10 -ErrorAction Stop -FailureMessage 'Set-SttiSystemSettings failed' -SuccessMessage 'Set-SttiSystemSettings succeeded' } catch{ Write-SttiLog $_ -Level Error } } #Uninstall-SttiInstanceServices -Instance (Get-SttiInstanceConfig dev) #endregion Installation #region Certificate management function New-SttiUserCertificate{ [OutputType([X509Certificate])] [CmdletBinding()] Param( [Parameter(Mandatory)] [string] $Subject, [string] $FriendlyName = $Subject, [DateTime] $NotAfter = (Get-Date).AddYears(20) ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $location = "Cert:CurrentUser\My" $cert = New-SelfSignedCertificate -Subject $Subject -FriendlyName $FriendlyName -CertStoreLocation $location -KeyExportPolicy Exportable -Type Custom -KeyAlgorithm RSA -KeyLength 2048 -HashAlgorithm SHA256 -NotAfter $NotAfter -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.2") -ErrorAction Stop Write-SttiLog "Created new cert with thumbprint $($cert.Thumbprint) for $Subject in $location" -Level Info return $cert } function New-SttiDatabaseEncryptionCertificate{ [OutputType([X509Certificate])] [CmdletBinding()] Param( [Parameter(Mandatory)] [string] $Subject, [string] $FriendlyName = $Subject, [DateTime] $NotAfter = (Get-Date).AddYears(50) ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $location = "Cert:CurrentUser\My" $cert = New-SelfSignedCertificate -Subject $Subject -FriendlyName $FriendlyName -CertStoreLocation $location -KeyExportPolicy Exportable -Type DocumentEncryptionCert -KeyUsage KeyEncipherment -KeySpec KeyExchange -KeyLength 2048 -NotAfter $NotAfter -ErrorAction Stop Write-SttiLog "Created new cert with thumbprint $($cert.Thumbprint) for $Subject in $location" -Level Info return $cert } function New-SttiSelfSignedCertificateAsPem{ [CmdletBinding()] Param( [Parameter(Mandatory)] $SubjectAttributes, #[string] $FriendlyName = $Subject, #[string] $KeyAlgorithm = 'RSA', [Parameter(Mandatory)] # exported with [Org.BouncyCastle.Crypto.Operators.Asn1SignatureFactory]::SignatureAlgNames|ForEach-Object {"""$_"""}|Join-String -Separator "," [ValidateSet("MD2WITHRSAENCRYPTION","MD2WITHRSA","MD5WITHRSAENCRYPTION","MD5WITHRSA","SHA1WITHRSAENCRYPTION","SHA-1WITHRSAENCRYPTION","SHA1WITHRSA","SHA-1WITHRSA","SHA224WITHRSAENCRYPTION","SHA-224WITHRSAENCRYPTION","SHA224WITHRSA","SHA-224WITHRSA","SHA256WITHRSAENCRYPTION","SHA-256WITHRSAENCRYPTION","SHA256WITHRSA","SHA-256WITHRSA","SHA384WITHRSAENCRYPTION","SHA-384WITHRSAENCRYPTION","SHA384WITHRSA","SHA-384WITHRSA","SHA512WITHRSAENCRYPTION","SHA-512WITHRSAENCRYPTION","SHA512WITHRSA","SHA-512WITHRSA","SHA512(224)WITHRSAENCRYPTION","SHA-512(224)WITHRSAENCRYPTION","SHA512(224)WITHRSA","SHA-512(224)WITHRSA","SHA512(256)WITHRSAENCRYPTION","SHA-512(256)WITHRSAENCRYPTION","SHA512(256)WITHRSA","SHA-512(256)WITHRSA","SHA3-224WITHRSAENCRYPTION","SHA3-256WITHRSAENCRYPTION","SHA3-384WITHRSAENCRYPTION","SHA3-512WITHRSAENCRYPTION","SHA3-224WITHRSA","SHA3-256WITHRSA","SHA3-384WITHRSA","SHA3-512WITHRSA","SHA1WITHRSAANDMGF1","SHA224WITHRSAANDMGF1","SHA256WITHRSAANDMGF1","SHA384WITHRSAANDMGF1","SHA512WITHRSAANDMGF1","RIPEMD160WITHRSAENCRYPTION","RIPEMD160WITHRSA","RIPEMD128WITHRSAENCRYPTION","RIPEMD128WITHRSA","RIPEMD256WITHRSAENCRYPTION","RIPEMD256WITHRSA","SHA1WITHDSA","DSAWITHSHA1","SHA224WITHDSA","SHA256WITHDSA","SHA384WITHDSA","SHA512WITHDSA","SHA1WITHECDSA","ECDSAWITHSHA1","SHA224WITHECDSA","SHA256WITHECDSA","SHA384WITHECDSA","SHA512WITHECDSA","GOST3411WITHGOST3410","GOST3411WITHGOST3410-94","GOST3411WITHECGOST3410","GOST3411WITHECGOST3410-2001","GOST3411WITHGOST3410-2001","GOST3411-2012-256WITHECGOST3410","GOST3411-2012-256WITHECGOST3410-2012-256","GOST3411-2012-512WITHECGOST3410","GOST3411-2012-512WITHECGOST3410-2012-512","Ed25519","Ed448","SHA256WITHSM2","SM3WITHSM2")] [string] $SignatureAlgorithm, [int] $KeyLength = 2048, [DateTime] $NotAfter = (Get-Date).AddYears(50) ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug Add-Type -Assembly "$PSScriptRoot\Stti.Installer.dll" Add-Type -Assembly "$PSScriptRoot\BouncyCastle.Cryptography.dll" $pems = [Stti.Installer.CertificateGenerator]::GenerateCertificateAsPem($subjectAttributes, $KeyLength, $SignatureAlgorithm, $NotAfter) return @{CertificatePem = $pems.Item1; KeyPem = $pems.Item2} } function Import-SttiPfxCertificate{ [OutputType([X509Certificate])] [CmdletBinding()] Param( [Parameter(Mandatory)] [string] $FilePath, [string] $Location = "Cert:\CurrentUser\My", [SecureString] $Password, [PSCredential] $UserCredential ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $importScript = { $cert = Import-PfxCertificate -FilePath $Using:FilePath -CertStoreLocation $Using:Location -Password:$Using:Password -Exportable return $cert } $cert = Start-Job -ScriptBlock $importScript -Credential:$UserCredential | Receive-Job -Wait -ErrorAction Stop return $cert } function Export-SttiPfxCertificate{ [CmdletBinding()] Param( [Parameter(Mandatory)] [string] $FilePath, [Parameter(Mandatory, ValueFromPipeline)] [X509Certificate] $Certificate, [SecureString] $Password, [PSCredential] $UserCredential, [switch] $RemoveFromStore, [ValidateSet("TripleDES_SHA1", "AES256_SHA256")] [string] $CryptoAlgorithmOption = "TripleDES_SHA1", [string] $CertificatePurpose ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug } Process{ if (!$Password){ $Password = Read-Password -Prompt "Please enter or paste a password for the exported $CertificatePurpose certificate" -ErrorAction Stop } $exportScript = { $cryptoAlgorithmParameterAvailable = (Get-Command Export-PfxCertificate).Parameters.ContainsKey("CryptoAlgorithmOption") # It is necessary to test if the parameter CryptoAlgorithmOption is available since it was added in Windows 1809 if ($cryptoAlgorithmParameterAvailable){ $Using:Certificate | Export-PfxCertificate -FilePath $Using:FilePath -CryptoAlgorithmOption:$Using:CryptoAlgorithmOption -Password:$Using:Password -ErrorAction Stop > $null } else{ $Using:Certificate | Export-PfxCertificate -FilePath $Using:FilePath -Password:$Using:Password -ErrorAction Stop > $null } if ($Using:RemoveFromStore){ $Using:Certificate | Remove-Item } } Start-Job -ScriptBlock $exportScript -Credential:$UserCredential | Receive-Job -Wait -ErrorAction Stop return Get-Item $FilePath } } function Get-SttiDatabaseEncryptionCertificateLocation { [OutputType([string])] [CmdletBinding()] Param( [Parameter(Mandatory)] [string] $Thumbprint ) $key = Get-ChildItem -Path "Cert:CurrentUser\My" | Where-Object Thumbprint -eq $Thumbprint if ($null -ne $key){ Write-SttiLog "Found database encryption key in Cert:CurrentUser\My" -Level Verbose return "CurrentUser" } else{ $key = Get-ChildItem -Path "Cert:LocalMachine\My" | Where-Object Thumbprint -eq $Thumbprint if ($null -ne $key){ Write-SttiLog "Found database encryption key in Cert:LocalMachine\My" -Level Verbose return "LocalMachine" } else { Write-SttiLog "No database encryption key was found with thumbprint $Thumbprint" -Level Error return $null } } } function Install-SttiDatabaseEncryptionCertificate{ [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [PSCredential] $ServiceCredential ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $targetLocation = "cert:\CurrentUser\My" $subject = "Stti $($Instance.Name) Database Encryption" $notAfter = (Get-Date).AddYears(50) $newCertThumbprint = Start-Job -ScriptBlock { Param([string] $location, [string] $subject, [DateTime] $notAfter) $cert = New-SelfSignedCertificate -Subject $subject -FriendlyName $subject -CertStoreLocation $location -KeyExportPolicy Exportable -Type DocumentEncryptionCert -KeyUsage KeyEncipherment -KeySpec KeyExchange -KeyLength 2048 -NotAfter $notAfter -ErrorAction Stop return $cert.Thumbprint } -Credential $ServiceCredential -ArgumentList $targetLocation, $subject, $notAfter | Receive-Job -Wait -ErrorAction Stop Set-SttiInstanceConfig -Instance $Instance -EncryptionCertificateThumbprint $newCertThumbprint -ErrorAction Stop > $null Write-SttiLog "A new database encryption certificate with thumbprint $newCertThumbprint has been created. Do not forget to backup the certificate and store it in a safe location. Therefore Export-SttiDatabaseEncryptionCertificate should be used." -Level Warn } catch{ Write-SttiLog $_ -Level Error } } function Export-SttiDatabaseEncryptionCertificate{ [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [PSCredential] $ServiceCredential, [ValidateSet("TripleDES_SHA1", "AES256_SHA256")] [string] $CryptoAlgorithmOption = "TripleDES_SHA1", [securestring] $DatabaseEncryptionKeyPassword = (Read-Password -Prompt "Please enter or paste a password for the exported database encryption key certificate") ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $location = "cert:\CurrentUser\My" $certPath = "$($location)\$($Instance.EncryptionCertificateThumbprint)" $exportPath = [System.IO.Path]::Combine("$(Get-SttiInstancePath -Instance $Instance)\", "data\", "Stti_$($Instance.Name)_DatabaseEncryption.pfx") Start-Job -ScriptBlock { if (!(Test-Path -Path $Using:certPath)){ throw "$certPath does not exist" } $cryptoAlgorithmParameterAvailable = (Get-Command Export-PfxCertificate).Parameters.ContainsKey("CryptoAlgorithmOption") # It is necessary to test if the parameter CryptoAlgorithmOption is available since it was added in Windows 1809 if ($cryptoAlgorithmParameterAvailable){ Get-Item -Path $Using:certPath | Export-PfxCertificate -FilePath $Using:exportPath -CryptoAlgorithmOption:$Using:CryptoAlgorithmOption -Password $Using:DatabaseEncryptionKeyPassword -ErrorAction Stop > $null } else{ Get-Item -Path $Using:certPath | Export-PfxCertificate -FilePath $Using:exportPath -Password $Using:DatabaseEncryptionKeyPassword -ErrorAction Stop > $null } } -Credential $ServiceCredential | Receive-Job -Wait -ErrorAction Stop Write-SttiLog "Database encryption certificate with thumbprint $($Instance.EncryptionCertificateThumbprint) was exported to $exportPath. Backup file to a secure location and remember password. Then delete the file." -Level Info } catch{ Write-SttiLog $_ -Level Error } } function Install-SttiInitialUserCertificate{ [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $targetLocation = "cert:\CurrentUser\My" $alreadyExists = Test-Path -Path "$($targetLocation)\$($initialUserCertificateThumbprint)" if (!$alreadyExists){ Write-SttiLog "Initial user certificate with thumbprint $initialUserCertificateThumbprint does not exist in $targetLocation." -Level Verbose $initialUserCertPath = [System.IO.Path]::Combine("$(Get-SttiInstancePath -Instance $Instance)\bin\", "InitialUser.pfx") Import-PfxCertificate -FilePath $initialUserCertPath -CertStoreLocation $targetLocation -Password $initialUserCertificatePassword -ErrorAction Stop > $null Write-SttiLog "Imported initial user certificate with thumbprint $initialUserCertificateThumbprint to $targetLocation." -Level Verbose } else{ Write-SttiLog "Initial user certificate with thumbprint $initialUserCertificateThumbprint already exists in $targetLocation." -Level Verbose } Write-SttiLog "A new STTI instance always has a standard initial user which compromises security. Do not forget to switch to new user account. Therefore Switch-SttiInitialUser should be used." -Level Warn } catch{ Write-SttiLog $_ -Level Error } } function Switch-SttiInitialUser{ [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [bool] $NewRootCertificate = $true, [bool] $NewUserCertificate = $true, [string] $CertificateThumbprint = $initialUserCertificateThumbprint, [securestring] $RootCertificatePassword, [ValidateSet("TripleDES_SHA1", "AES256_SHA256")] [string] $CryptoAlgorithmOption = "TripleDES_SHA1" ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug if ($NewRootCertificate){ $subject = "Stti $($Instance.Name) Root User" $rootCert = New-SttiUserCertificate -Subject $subject -FriendlyName $subject -NotAfter ([datetime]::Now.AddYears(50)) -ErrorAction Stop try{ $exportPath = [System.IO.Path]::Combine("$(Get-SttiInstancePath -Instance $Instance)\", "data\", "Stti_$($Instance.Name)_RootUser.pfx") $rootCert | Export-SttiPfxCertificate -FilePath $exportPath -Password:$RootCertificatePassword -RemoveFromStore -CryptoAlgorithmOption:$CryptoAlgorithmOption -CertificatePurpose 'Root-user private key' -ErrorAction Stop > $null New-SttiUser -Instance:$Instance -DisplayName "$subject $($rootCert.Thumbprint)" -UserCertificateThumbprint $rootCert.Thumbprint -Roles SecurityAdmin -AllClients -CertificateThumbprint:$CertificateThumbprint -ErrorAction Stop > $null Write-SttiLog "Root user certificate with thumbprint $($rootCert.Thumbprint) was exported to $exportpath. Backup file to a secure location and remember password. Then delete the file." -Level Info Write-SttiLog "Root user was added to stti with securityadmin role" -Level Info } catch{ $rootCert | Remove-Item -ErrorAction Continue Write-SttiLog "Error in path NewRootCertificate. The following exception occured: $_" -Level Error -ErrorAction Stop } } if ($NewUserCertificate){ $subject = "Stti $($Instance.Name) User $env:UserName" $userCert = New-SttiUserCertificate -Subject $subject -FriendlyName $subject -ErrorAction Stop try{ New-SttiUser -Instance:$Instance -DisplayName "$subject $($userCert.Thumbprint)" -UserCertificateThumbprint $userCert.Thumbprint -Roles SecurityAdmin, OperationsAdmin, DataAdmin -AllClients -CertificateThumbprint:$CertificateThumbprint -ErrorAction Stop > $null Write-SttiLog "Current user was added to stti with securityadmin, operationsadmin roles" -Level Info } catch{ $userCert | Remove-Item -ErrorAction Continue Write-SttiLog "Error in path NewUserCertificate. The following exception occured: $_" -Level Error -ErrorAction Stop } } try{ $initialUser = Get-SttiUser -Instance:$Instance -UserId $initialUserId -CertificateThumbprint:$CertificateThumbprint -ErrorAction Stop if ($initialUser.IsActive){ $initialUser | Disable-SttiUser -Instance:$Instance -CertificateThumbprint:$CertificateThumbprint -ErrorAction Stop > $null Write-SttiLog "Initial installation user was disabled. Please use the new certificate thumbprint." -Level Info } } catch{ Write-SttiLog "Could not disable initial user in STTI. The following exception occured: $_" -Level Error -ErrorAction Stop } $instanceDefaults = (Get-SttiDefaultValues).ForInstance($Instance) $defaultCertificateThumbprint = $null -ne $instanceDefaults ? $instanceDefaults.CertificateThumbprint : $null if ([string]::IsNullOrWhiteSpace($defaultCertificateThumbprint) -and $userCert){ try{ Set-SttiDefaultValues -InstanceName $Instance.Name -CertificateThumbprint $userCert.Thumbprint -ErrorAction Stop > $null Write-SttiLog "The new user certificate thumbprint was set as default." -Level Info } catch{ Write-SttiLog "Could not set the new user certificate thumbprint as default. The following exception occured: $_" -Level Error -ErrorAction Continue } } } function Test-SttiSslCertificateForRole { [OutputType([bool])] [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [pscredential] $ServiceCredential, [SttiInstanceRoles] $InstanceRole ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug # check presence of certificate if ($InstanceRole -contains [SttiInstanceRoles]::Worker) {$store = $Instance.WorkerSslCertificateStore; $subject = $Instance.WorkerSslCertificateSubject} elseif ($InstanceRole -contains [SttiInstanceRoles]::Web) {$store = $Instance.WebSslCertificateStore; $subject = $Instance.WebSslCertificateSubject} else {throw "Invalid instance role $InstanceRole"} switch (($null -ne $store ? $store.ToLowerInvariant() : $null)){ "CurrentUser" { $location = "Cert:\CurrentUser\My" } "LocalMachine" { $location = "Cert:\LocalMachine\My" } default {throw "Invalid certificate store $store"} } $checkCertificateScript = { Param( [string] $subject, [string] $location, [string] $store, [string] $serviceUsername ) # find certificates with subject try{ $certsWithSubject = Get-ChildItem -Path $location | Where-Object {$_.DnsNameList -Contains $subject -and ($_.EnhancedKeyUsageList | Where-Object ObjectId -like "1.3.6.1.5.5.7.3.1").Count -gt 0} if ($null -eq $certsWithSubject){ throw "Could not find ssl certificate with subject ""$subject"" in $location" } else{ $certsWithSubject | Foreach-Object { Write-Verbose "Found certificate with thumbprint $($_.Thumbprint)" } } } catch{ Write-Error "Could not find a matching ssl certificate: $($_.Exception)" -ErrorAction Stop return $false } $foundValidCert = $false foreach ($cert in $certsWithSubject){ # check usage and date try{ $cert | Test-Certificate -Policy SSL -EKU "1.3.6.1.5.5.7.3.1" -AllowUntrustedRoot -ErrorAction Stop -WarningAction SilentlyContinue -WarningVariable testWarnings > $null if ($testWarnings) {$testWarnings | Write-Warning } } catch{ Write-Warning "SSL certificate with thumbprint $($cert.Thumbprint) is not valid: $($_.Exception)" continue } # check presence of private key try{ $certKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert) if ($null -eq $certKey){ throw "Could not load private key for the certificate" } } catch{ Write-Warning "Could not load private key for the certificate with thumbprint $($cert.Thumbprint): $($_.Exception)" continue } # grant read access for service user if certificate ist in local machine store if ($store -like "LocalMachine"){ try{ $keyFileName = $certKey.key.UniqueName $keyPath = "$env:ALLUSERSPROFILE\Microsoft\Crypto\RSA\MachineKeys\$keyFileName" $keyPermissions = Get-Acl -Path $keyPath if ($null -eq ($keyPermissions.Access | Where-Object {$_.IdentityReference -like $serviceUsername -and $_.AccessControlType -eq "Allow" -and $_.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::Read })){ Write-Verbose "Add read access to private key for $serviceUsername to certificate with thumbprint $($cert.Thumbprint)" $keyPermissions.AddAccessRule(([System.Security.Accesscontrol.FileSystemAccessRule]::new($serviceUsername,"Read","Allow"))) Set-Acl -Path $keyPath -AclObject $keyPermissions } } catch{ Write-Warning "Could not ensure read access for $($ServiceCredential.UserName) to the certificate with thumbprint $($cert.Thumbprint): $($_.Exception)" continue } } Write-Verbose "Found valid certificate with thumbprint $($cert.Thumbprint)" $foundValidCert = $true } return $foundValidCert } try{ if ($store -eq "LocalMachine"){ ($foundValidCert = Invoke-Command -ScriptBlock $checkCertificateScript -ArgumentList $subject,$location,$store,$ServiceCredential.UserName -ErrorAction Stop 6>$null) >$null 2>&1 3>&1 4>&1 5>&1 | Write-SttiLog -Level Warn } elseif ($store -eq "CurrentUser"){ ($foundValidCert = Start-Job -ScriptBlock $checkCertificateScript -ArgumentList $subject,$location,$store,$ServiceCredential.UserName -Credential $ServiceCredential -ErrorAction Stop | Receive-Job -Wait -ErrorAction Stop 6>$null) >$null 2>&1 3>&1 4>&1 5>&1 | Write-SttiLog -Level Warn } } catch{ Write-SttiLog "Could not execute certificate checking: $($_.Exception)" -Level Error -ErrorAction Stop } if (!$foundValidCert){ Write-SttiLog "Could not find a valid SSL certificate" -Level Error -ErrorAction Stop } return $foundValidCert } #endregion Certificate management #region Instance Management function Get-SttiInstanceConfig{ [OutputType([SttiInstanceConfig])] [CmdletBinding()] Param( [string] $Name ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $instanceNames = $PSBoundParameters.ContainsKey("Name") ? @($Name) : (Get-ChildItem -Path "$([System.IO.Path]::Combine((Get-SttiBasePath), "instances\*"))" | Select-Object -ExpandProperty Name | Where-Object { (Test-Path (Get-SttiInstanceConfigFileName -Name $_)) }) foreach ($name in $instanceNames) { $instanceConfigFile = Get-SttiInstanceConfigFileName -Name $Name if (-not (Test-Path $instanceConfigFile)){ Write-SttiLog "The instance configuration $instanceConfigFile does not exists" -Level Error continue } $config = [SttiInstanceConfig](Get-Content $instanceConfigFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop) $config.Path = Get-SttiInstancePath -Name $Name # Migration - Add default values for new settings if (!$config.WorkerSslCertificateStore){ $config.WorkerSslCertificateStore = "CurrentUser" } if (!$config.WebSslCertificateStore){ $config.WebSslCertificateStore = "CurrentUser" } if (!$config.WriteLogFiles){ $config.WriteLogFiles = $false } Write-Output $config } } #Get-SttiInstanceConfig -Name "dev" function New-SttiInstanceConfig{ [OutputType([SttiInstanceConfig])] [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [string] $Name, [Parameter(Mandatory=$true)] [string] $SqlServer, [Parameter()] [string] $Database = "STTI_$($Name.ToUpperInvariant())", [Parameter()] [SttiInstanceRoles[]] $Roles = @([SttiInstanceRoles]::Worker, [SttiInstanceRoles]::Web), [Parameter()] [switch] $StoreDbInInstanceDataPath = $false, [Parameter(Mandatory)] [string] $WorkerSslCertificateSubject, [ValidateSet("LocalMachine", "CurrentUser")] [string] $WorkerSslCertificateStore = "CurrentUser", [Parameter(Mandatory)] [uint] $WorkerPort, [Parameter(Mandatory)] [string] $WebSslCertificateSubject, [ValidateSet("LocalMachine", "CurrentUser")] [string] $WebSslCertificateStore = "CurrentUser", [Parameter(Mandatory)] [uint] $WebPort, [Parameter()] [string] $WorkerHostname = "localhost", [string] $EncryptionCertificateThumbprint, [Parameter()] [AllowNull()] [ValidateScript({ if ([string]::IsNullOrWhiteSpace($_) -or $_ -match "^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_]*)?$") { $true } else{ throw "Value must be an url like http://127.0.0.1:3128" }})] [string] $HttpProxyAddress, [Parameter()] [bool] $WriteLogFiles = $false ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $Name = $Name.ToUpperInvariant() $instancePath = Get-SttiInstancePath $Name if (-not (Test-Path $instancePath)){ New-Item $instancePath -ItemType Directory -ErrorAction Stop | Out-Null } $instanceConfigFile = Get-SttiInstanceConfigFileName $Name if ((Test-Path $instanceConfigFile)){ throw "The instance configuration $instanceConfigFile already exists" } $config = [SttiInstanceConfig]::new() $config.Name = $Name $config.SqlServer = $SqlServer $config.Database = $Database $config.Roles = $Roles $config.Path = $instancePath $config.StoreDbInInstanceDataPath = $StoreDbInInstanceDataPath $config.WorkerSslCertificateSubject = $WorkerSslCertificateSubject $config.WorkerSslCertificateStore = $WorkerSslCertificateStore $config.WorkerPort = $WorkerPort $config.WebSslCertificateSubject = $WebSslCertificateSubject $config.WebSslCertificateStore = $WebSslCertificateStore $config.WebPort = $WebPort $config.WorkerHostname = $WorkerHostname $config.EncryptionCertificateThumbprint = $EncryptionCertificateThumbprint $config.EncryptionMasterKeyName = "STTI_$EncryptionCertificateThumbprint" $config.HttpProxyAddress = $HttpProxyAddress $config.WriteLogFiles = $WriteLogFiles $config | ConvertTo-Json -ErrorAction Stop | Out-File $instanceConfigFile -Encoding utf8 -ErrorAction Stop return $config } #New-SttiInstanceConfig -Name "DEV" -SqlServer localdb -Database SttiDev -Roles Worker, Web function Set-SttiInstanceConfig{ [OutputType([SttiInstanceConfig])] [CmdletBinding()] Param( [Parameter(Mandatory, ParameterSetName="InstanceName")] [string] $Name, [Parameter(Mandatory, ParameterSetName="Instance", ValueFromPipeline)] [SttiInstanceConfig] $Instance, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [string] $SqlServer, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [string] $Database, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [SttiInstanceRoles[]] $Roles, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [switch] $StoreDbInInstanceDataPath = $false, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [string] $WorkerSslCertificateSubject, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [ValidateSet("LocalMachine", "CurrentUser")] [string] $WorkerSslCertificateStore, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [uint] $WorkerPort, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [string] $WebSslCertificateSubject, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [ValidateSet("LocalMachine", "CurrentUser")] [string] $WebSslCertificateStore, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [uint] $WebPort, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [string] $WorkerHostname, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [string] $EncryptionCertificateThumbprint, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [AllowNull()] [ValidateScript({ if ([string]::IsNullOrWhiteSpace($_) -or $_ -match "^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_]*)?$") { $true } else{ throw "Value must be an url like http://127.0.0.1:3128" }})] [string] $HttpProxyAddress, [Parameter()] [bool] $WriteLogFiles = $false ) Process{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug if ($null -ne $Instance){ $Name = $Instance.Name } else{ $Instance = Get-SttiInstanceConfig ($Name) -ErrorAction Stop } $actualConfig = Get-SttiInstanceConfig ($Name) -ErrorAction Stop $Instance.SqlServer = ($PSBoundParameters.ContainsKey("SqlServer") ? $SqlServer : $actualConfig.SqlServer) $Instance.Database = ($PSBoundParameters.ContainsKey("Database") ? $Database : $actualConfig.Database) $Instance.Roles = ($PSBoundParameters.ContainsKey("Roles") ? $Roles : $actualConfig.Roles) $Instance.StoreDbInInstanceDataPath = ($PSBoundParameters.ContainsKey("StoreDbInInstanceDataPath") ? $StoreDbInInstanceDataPath : $actualConfig.StoreDbInInstanceDataPath) $Instance.WorkerSslCertificateSubject = ($PSBoundParameters.ContainsKey("WorkerSslCertificateSubject") ? $WorkerSslCertificateSubject : $actualConfig.WorkerSslCertificateSubject) $Instance.WorkerSslCertificateStore = ($PSBoundParameters.ContainsKey("WorkerSslCertificateStore") ? $WorkerSslCertificateStore : $actualConfig.WorkerSslCertificateStore) $Instance.WorkerPort = ($PSBoundParameters.ContainsKey("WorkerPort") ? $WorkerPort : $actualConfig.WorkerPort) $Instance.WebSslCertificateSubject = ($PSBoundParameters.ContainsKey("WebSslCertificateSubject") ? $WebSslCertificateSubject : $actualConfig.WebSslCertificateSubject) $Instance.WebSslCertificateStore = ($PSBoundParameters.ContainsKey("WebSslCertificateStore") ? $WebSslCertificateStore : $actualConfig.WebSslCertificateStore) $Instance.WebPort = ($PSBoundParameters.ContainsKey("WebPort") ? $WebPort : $actualConfig.WebPort) $Instance.WorkerHostname = ($PSBoundParameters.ContainsKey("WorkerHostname") ? $WorkerHostname : $actualConfig.WorkerHostname) if ($PSBoundParameters.ContainsKey("EncryptionCertificateThumbprint")){ if (![string]::IsNullOrEmpty($actualConfig.EncryptionCertificateThumbprint) -and $EncryptionCertificateThumbprint -ne $actualConfig.EncryptionCertificateThumbprint -and (Get-SttiInstanceInstallStatus -Instance $Instance).Installed){ Write-SttiLog "The EncryptionCertificateThumbprint has already been set and cannot be changed. Use Rotate-SttiEncryptionMasterKey" -Level Error -ErrorAction Stop } $Instance.EncryptionCertificateThumbprint = $EncryptionCertificateThumbprint $Instance.EncryptionMasterKeyName = "STTI_$EncryptionCertificateThumbprint" } else{ $Instance.EncryptionCertificateThumbprint = $actualConfig.EncryptionCertificateThumbprint $Instance.EncryptionMasterKeyName = $actualConfig.EncryptionMasterKeyName } $Instance.HttpProxyAddress = ($PSBoundParameters.ContainsKey("HttpProxyAddress") ? $HttpProxyAddress : $actualConfig.HttpProxyAddress) $Instance.WriteLogFiles = ($PSBoundParameters.ContainsKey("WriteLogFiles") ? $WriteLogFiles : $actualConfig.WriteLogFiles) $instanceConfigFile = Get-SttiInstanceConfigFileName $Name $Instance | ConvertTo-Json -ErrorAction Stop | Out-File $instanceConfigFile -Encoding utf8 -ErrorAction Stop $PSCmdlet.WriteObject($Instance) # Renew session and default Values if ($script:shellSession.Instance -and $script:shellSession.Instance.Name -ieq $Instance.Name){ Set-SttiSessionValues -Instance $Instance } } } #Set-SttiInstanceConfig -InstanceName dev -Database "Test" -Roles Web, Worker #Get-SttiInstanceConfig dev | Set-SttiInstanceConfig -Database "Test" -Roles Web function Set-SttiSessionValues { [CmdletBinding()] Param( [Parameter(Mandatory, ParameterSetName="InstanceName")] [string] $InstanceName, [Parameter(Position=1, ParameterSetName="Instance")] [SttiInstanceConfig] $Instance, [Parameter(ParameterSetName="Instance")] [Parameter(ParameterSetName="InstanceName")] [int] $ApiCallTimeoutSec ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug if ($PSBoundParameters.ContainsKey("InstanceName")){ $Instance = Get-SttiInstanceConfig -Name $InstanceName -ErrorAction Stop $script:shellSession.Instance = $Instance } elseif ($PSBoundParameters.ContainsKey("Instance")){ $script:shellSession.Instance = $Instance } if ($PSBoundParameters.ContainsKey("ApiCallTimeoutSec")){ $script:shellSession.ApiCallTimeoutSec = $ApiCallTimeoutSec } Restore-SttiDefaultParameterValues } function Test-SttiInstanceBinding{ [OutputType([bool])] [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug foreach ($otherInstance in (Get-SttiInstanceConfig | Where-Object Name -ne $Instance.Name)) { if ($Instance.WorkerPort -eq $otherInstance.WorkerPort){ Write-SttiLog "$($otherInstance.Name) is also configured for port $($Instance.WorkerPort)" -Level ((Get-SttiInstanceInstallStatus -Instance $otherInstance).Installed -eq $true ? "Error" : "Warn") return $false } } return $true } function Get-SttiInstancePath{ Param( [Parameter(Mandatory, Position, ParameterSetName="InstanceName")] [string] $Name, [Parameter(Mandatory, ParameterSetName="Instance")] [SttiInstanceConfig] $Instance ) if ($null -ne $Instance){ $Name = $Instance.Name } return [System.IO.Path]::Combine((Get-SttiBasePath), "Instances\", $Name) } function Get-SttiInstanceConfigFileName{ Param( [Parameter(Mandatory)] [string] $Name ) return [System.IO.Path]::Combine((Get-SttiInstancePath $Name), "config.json") } function Get-SttiInstanceInstallStatusFilename{ Param( [Parameter(Mandatory)] [string] $Name ) return [System.IO.Path]::Combine((Get-SttiInstancePath $Name), "install.json") } function Get-SttiInstallLogFilename{ Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [DateTime] $Timestamp ) if ($Timestamp -and $null -ne $Timestamp){ return [System.IO.Path]::Combine((Get-SttiBasePath), "install-$($Instance.Name)-$(("{0:s}" -f $Timestamp) -replace ":", "-").log") } else{ return [System.IO.Path]::Combine((Get-SttiBasePath), "install-$($Instance.Name).log") } } function Get-SttiBasePath{ [OutputType([string])] Param() $basePath = $env:SttiBasePath ?? $script:defaultBasePath if (-not (Test-Path $basePath)){ Write-SttiLog "STTI base path $basePath does not exist" -Level Warn } return $basePath } function Set-SttiBasePath{ Param( [Parameter(Mandatory)] [string] $BasePath ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug [System.Environment]::SetEnvironmentVariable("SttiBasePath", $BasePath, [System.EnvironmentVariableTarget]::Machine) $env:SttiBasePath = $BasePath Write-SttiLog "STTI base path was set to $BasePath." -Level Warn if (-not (Test-Path $BasePath)){ Write-SttiLog "STTI base path $BasePath does not exist. It will be created." -Level Warn New-Item $BasePath -ItemType Directory -ErrorAction Stop | Out-Null } $PackagePath = [System.IO.Path]::Combine($BasePath, "Packages") if (-not (Test-Path $PackagePath)){ New-Item $PackagePath -ItemType Directory -ErrorAction Stop | Out-Null } } function Get-SttiInstanceStatus{ [OutputType([SttiInstanceStatus])] [CmdletBinding()] Param( [Parameter(Mandatory, ParameterSetName="InstanceName")] [string] $Name, [Parameter(Mandatory, ParameterSetName="Instance", ValueFromPipeline)] [SttiInstanceConfig] $Instance, [Parameter(ParameterSetName="InstanceName")] [Parameter(ParameterSetName="Instance")] [X509Certificate] $Certificate ) Process { Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $status = [SttiInstanceStatus]::new() if ($null -eq $Instance){ $Instance = Get-SttiInstanceConfig -Name $Name -ErrorAction Stop if ($null -eq $Instance){ Write-SttiLog "No instance config found" -Level Verbose return $status } } # Check if instance exists $status.InstanceExists = Test-Path (Get-SttiInstancePath -Name $Instance.Name) # Check install status $status.InstallStatus = Get-SttiInstanceInstallStatus -Instance $Instance # Check if database exists try { $sqlDb = Get-SqlDatabase @sqlVersionSpecificParameters -Name $Instance.Database -ServerInstance $Instance.SqlServer -ErrorAction Ignore } catch { Write-SttiLog "Could not access database $($Instance.Database) on $($Instance.SqlServer): $($_.Exception)." -Level Warn } $status.DatabaseExists = ($null -ne $sqlDb) # Check if worker service is running $workerService = Get-Service -Name (Get-SttiServiceName -Instance $Instance -Role Worker) -ErrorAction SilentlyContinue $status.WorkerRoleRunning = ($null -ne $workerService -and $workerService.Status -eq "Running") # Check if webui service is running $webService = Get-Service -Name (Get-SttiServiceName -Instance $Instance -Role Web) -ErrorAction SilentlyContinue $status.WebRoleRunning = ($null -ne $webService -and $webService.Status -eq "Running") $PSCmdlet.WriteObject($status) } } #Get-SttiInstanceStatus -Name dev function Start-SttiInstance { [CmdletBinding()] Param( [Parameter(Mandatory, ValueFromPipeline)] [SttiInstanceConfig] $Instance, [X509Certificate] $Certificate ) Process { Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $workerServiceName = (Get-SttiServiceName -Instance $Instance -Role Worker) Get-Service -Name $workerServiceName -ErrorAction SilentlyContinue | Where-Object Status -eq Stopped | Start-Service -ErrorAction Stop } catch{ Write-SttiLog $_ -Level Error } try{ $webServiceName = (Get-SttiServiceName -Instance $Instance -Role Web) Get-Service -Name $webServiceName -ErrorAction SilentlyContinue | Where-Object Status -eq Stopped | Start-Service -ErrorAction Stop } catch{ Write-SttiLog $_ -Level Error } } } function Stop-SttiInstance { [CmdletBinding()] Param( [Parameter(Mandatory, ValueFromPipeline)] [SttiInstanceConfig] $Instance, [X509Certificate] $Certificate ) Process { Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $workerServiceName = (Get-SttiServiceName -Instance $Instance -Role Worker) $workerService = Get-Service -Name $workerServiceName -ErrorAction SilentlyContinue | Where-Object Status -eq Running if ($null -ne $workerService){ $processId = Get-CimInstance Win32_Service -Verbose:$false | Where-Object { $_.Name -eq $workerService.Name } | Select-Object -ExpandProperty ProcessId Stop-Service $workerService Get-Process -Id $processId -ErrorAction SilentlyContinue | Wait-Process -TimeoutSec 120 -ErrorAction Stop } Start-Sleep -Seconds 0.5 } catch{ Write-SttiLog $_ -Level Error } try{ $webServiceName = (Get-SttiServiceName -Instance $Instance -Role Web) $webService = Get-Service -Name $webServiceName -ErrorAction SilentlyContinue | Where-Object Status -eq Running if ($null -ne $webService){ $processId = Get-CimInstance Win32_Service -Verbose:$false | Where-Object { $_.Name -eq $webService.Name } | Select-Object -ExpandProperty ProcessId Stop-Service $webService Get-Process -Id $processId -ErrorAction SilentlyContinue | Wait-Process -TimeoutSec 120 -ErrorAction Stop } Start-Sleep -Seconds 0.5 } catch{ Write-SttiLog $_ -Level Error } } } function Restart-SttiInstance { [CmdletBinding()] Param( [Parameter(Mandatory, ValueFromPipeline)] [SttiInstanceConfig] $Instance, [X509Certificate] $Certificate ) Process{ Stop-SttiInstance -Instance:$Instance -Certificate:$Certificate Start-SttiInstance -Instance:$Instance -Certificate:$Certificate } } #endregion Instance Management #region Database Management function New-SttiDatabase{ [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $dbPath = [System.IO.Path]::Combine((Get-SttiInstancePath -Name $Instance.Name), "data\db\") try{ $command = " DECLARE @DefaultDataPath varchar(max) SET @DefaultDataPath = (SELECT CONVERT(varchar(max), SERVERPROPERTY('INSTANCEDEFAULTDATAPATH'))) DECLARE @DefaultLogPath varchar(max) SET @DefaultLogPath = (SELECT CONVERT(varchar(max), SERVERPROPERTY('INSTANCEDEFAULTLOGPATH'))) DECLARE @DataPath varchar(max) SET @DataPath = $($Instance.StoreDbInInstanceDataPath ? "'$dbPath'" : "@DefaultDataPath") DECLARE @LogPath varchar(max) SET @LogPath = $($Instance.StoreDbInInstanceDataPath ? "'$dbPath'" : "@DefaultLogPath") EXECUTE(' CREATE DATABASE [$($Instance.Database)] ON PRIMARY ( NAME = N''STTI'', FILENAME = N''' + @DataPath + '$($Instance.Database).mdf'' , SIZE = 262144KB , FILEGROWTH = 65536KB ) LOG ON ( NAME = N''STTI_log'', FILENAME = N''' + @LogPath + '$($Instance.Database).ldf'' , SIZE = 65536KB , FILEGROWTH = 65536KB ) COLLATE Latin1_General_100_CI_AS'); GO ALTER DATABASE [$($Instance.Database)] SET COMPATIBILITY_LEVEL = 130 ALTER DATABASE [$($Instance.Database)] SET ANSI_NULL_DEFAULT OFF ALTER DATABASE [$($Instance.Database)] SET ANSI_NULLS OFF ALTER DATABASE [$($Instance.Database)] SET ANSI_PADDING OFF ALTER DATABASE [$($Instance.Database)] SET ANSI_WARNINGS OFF ALTER DATABASE [$($Instance.Database)] SET ARITHABORT OFF ALTER DATABASE [$($Instance.Database)] SET AUTO_CLOSE OFF ALTER DATABASE [$($Instance.Database)] SET AUTO_SHRINK OFF ALTER DATABASE [$($Instance.Database)] SET AUTO_CREATE_STATISTICS ON(INCREMENTAL = OFF) ALTER DATABASE [$($Instance.Database)] SET AUTO_UPDATE_STATISTICS ON ALTER DATABASE [$($Instance.Database)] SET CURSOR_CLOSE_ON_COMMIT OFF ALTER DATABASE [$($Instance.Database)] SET CONCAT_NULL_YIELDS_NULL OFF ALTER DATABASE [$($Instance.Database)] SET NUMERIC_ROUNDABORT OFF ALTER DATABASE [$($Instance.Database)] SET QUOTED_IDENTIFIER OFF ALTER DATABASE [$($Instance.Database)] SET RECURSIVE_TRIGGERS OFF ALTER DATABASE [$($Instance.Database)] SET DISABLE_BROKER ALTER DATABASE [$($Instance.Database)] SET ALLOW_SNAPSHOT_ISOLATION ON ALTER DATABASE [$($Instance.Database)] SET READ_COMMITTED_SNAPSHOT ON ALTER DATABASE [$($Instance.Database)] SET RECOVERY FULL ALTER DATABASE [$($Instance.Database)] SET PAGE_VERIFY CHECKSUM GO" Invoke-Sqlcmd @sqlVersionSpecificParameters -ServerInstance $Instance.SqlServer -Database "master" -Query $command -ErrorAction Stop } catch{ Write-SttiLog $_ -Level Error } } #Get-SttiInstanceConfig dev | New-SttiDatabase -EncryptionMasterKey $null function Update-SttiDatabaseUser{ [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [string] $ServiceUsername, [switch] $SkipServerChanges ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $command = (!$SkipServerChanges ? " IF NOT EXISTS (SELECT name FROM master.sys.server_principals WHERE name = '$($ServiceUsername)') BEGIN CREATE LOGIN [$($ServiceUsername)] FROM WINDOWS WITH DEFAULT_DATABASE=[master], DEFAULT_LANGUAGE=[us_english] END GO ": "") ` + "USE [$($Instance.Database)] GO DECLARE @uid INT SELECT @uid = principal_id from sys.database_principals WHERE sid = SUSER_SID('$($ServiceUsername)') IF @uid IS NULL BEGIN CREATE USER [$($ServiceUsername)] FOR LOGIN [$($ServiceUsername)] ALTER ROLE [db_owner] ADD MEMBER [$($ServiceUsername)] END ELSE IF NOT EXISTS (SELECT * FROM sys.database_role_members WHERE role_principal_id = DATABASE_PRINCIPAL_ID('db_owner') AND member_principal_id = @uid) BEGIN ALTER ROLE [db_owner] ADD MEMBER [$($ServiceUsername)] END GO" Invoke-Sqlcmd @sqlVersionSpecificParameters -ServerInstance $Instance.SqlServer -Database "master" -Query $command -ErrorAction Stop } catch{ Write-SttiLog $_ -Level Error } } #Get-SttiInstanceConfig dev | New-SttiDatabase -EncryptionMasterKey $null function Add-SttiDatabaseEncryption{ [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter()] [PSCredential] $ServiceCredential = $null ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $scriptBlock = { param( $Instance, $EncryptColumnEncryptionKeyFunc ) Import-Module SqlServer # search for the key $key = Get-ChildItem -Path "Cert:CurrentUser\My" | Where-Object Thumbprint -eq $Instance.EncryptionCertificateThumbprint if ($null -ne $key){ Write-Verbose "Found database encryption key in Cert:CurrentUser\My" $certificateStoreLocation = "CurrentUser" } else{ $key = Get-ChildItem -Path "Cert:LocalMachine\My" | Where-Object Thumbprint -eq $Instance.EncryptionCertificateThumbprint if ($null -ne $key){ Write-Verbose "Found database encryption key in Cert:LocalMachine\My" $certificateStoreLocation = "LocalMachine" } else { throw "No database encryption key was found with thumbprint $($Instance.EncryptionCertificateThumbprint)" } } # construct script for creating a new column master key $masterKeyCertificatePath = "$certificateStoreLocation/My/$($Instance.EncryptionCertificateThumbprint)" $masterKeyCertificate = $key $newMasterKeyScript = "CREATE COLUMN MASTER KEY [$($Instance.EncryptionMasterKeyName)] WITH ( KEY_STORE_PROVIDER_NAME = 'MSSQL_CERTIFICATE_STORE', KEY_PATH = '$masterKeyCertificatePath' ); GO " # create a new random encryption key $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() try{ $encryptionKey = [byte[]]::new(32) $rng.GetBytes($encryptionKey) } finally{ $rng.Dispose() } $encryptionKeyName = "CEK" # encrypt the key if ($EncryptColumnEncryptionKeyFunc -is [string]){ $EncryptColumnEncryptionKeyFunc = [scriptblock]::Create($EncryptColumnEncryptionKeyFunc) } $encryptedKey = (& $EncryptColumnEncryptionKeyFunc -Certificate $masterKeyCertificate -MasterKeyPath $masterKeyCertificatePath -ColumnEncryptionKey $encryptionKey) # seralize to format that sql server wants $encryptedKeySerialized = "0x" + [BitConverter]::ToString($encryptedKey).Replace("-", "") # construct script for creating a new column encryption key $newEncryptionKeyScript = "CREATE COLUMN ENCRYPTION KEY [$encryptionKeyName] WITH VALUES ( COLUMN_MASTER_KEY = [$($Instance.EncryptionMasterKeyName)], ALGORITHM = 'RSA_OAEP', ENCRYPTED_VALUE = $encryptedKeySerialized ); GO " $db = Get-SqlDatabase @Using:sqlVersionSpecificParameters -Name $Instance.Database -ServerInstance $Instance.SqlServer -ErrorAction Stop $masterKeyExists = ($null -ne (Get-SqlColumnMasterKey @Using:sqlVersionSpecificParameters -Name $Instance.EncryptionMasterKeyName -InputObject $db -ErrorAction SilentlyContinue)) $encryptionKeyExists = ($null -ne (Get-SqlColumnEncryptionKey @Using:sqlVersionSpecificParameters -Name "CEK" -InputObject $db -ErrorAction SilentlyContinue)) $script = " BEGIN TRANSACTION $(!$masterKeyExists ? $newMasterKeyScript : '') $(!$encryptionKeyExists ? $newEncryptionKeyScript : '') COMMIT TRANSACTION " Invoke-SqlCmd @Using:sqlVersionSpecificParameters -ServerInstance $Instance.SqlServer -Database $Instance.Database -Query $script -ErrorAction Stop } $argumentList = @($Instance, $encryptColumnEncryptionKeyFunc) # Workaround, since start-job with credentials cannot be called in remoting session if ($null -eq $ServiceCredential -or ($env:useCurrentUser -and $env:useCurrentUser -eq $true)){ Start-Job -ScriptBlock $scriptBlock -ArgumentList $argumentList | Receive-Job -Wait -ErrorAction Stop } else{ Start-Job -Credential $ServiceCredential -ScriptBlock $scriptBlock -ArgumentList $argumentList | Receive-Job -Wait -ErrorAction Stop } } catch{ Write-SttiLog $_ -Level Error } } $encryptColumnEncryptionKeyFunc = { Param( [X509Certificate] $Certificate, [string] $MasterKeyPath, [byte[]] $ColumnEncryptionKey ) # Construct the encryptedColumnEncryptionKey # Format is # version + keyPathLength + ciphertextLength + ciphertext + keyPath + signature # We currently only support one version $version = [byte[]] @( 0x01 ) # Get the Unicode encoded bytes of cultureinvariant lower case masterKeyPath $masterKeyPathBytes = [System.Text.UnicodeEncoding]::Unicode.GetBytes($masterKeyPath.ToLowerInvariant()) $keyPathLength = [System.BitConverter]::GetBytes([Int16]$masterKeyPathBytes.Length) # Encrypt the plain text $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($Certificate) $cipherText = $rsa.Encrypt($columnEncryptionKey, [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA1) $cipherTextLength = [System.BitConverter]::GetBytes([Int16]$cipherText.Length) # Compute hash # SHA-2-256(version + keyPathLength + ciphertextLength + keyPath + ciphertext) $sha256 = [System.Security.Cryptography.SHA256CryptoServiceProvider]::new() try { $sha256.TransformBlock($version, 0, $version.Length, $version, 0) > $null $sha256.TransformBlock($keyPathLength, 0, $keyPathLength.Length, $keyPathLength, 0) > $null $sha256.TransformBlock($cipherTextLength, 0, $cipherTextLength.Length, $cipherTextLength, 0) > $null $sha256.TransformBlock($masterKeyPathBytes, 0, $masterKeyPathBytes.Length, $masterKeyPathBytes, 0) > $null $sha256.TransformFinalBlock($cipherText, 0, $cipherText.Length) > $null $hash = $sha256.Hash } finally{ $sha256.Dispose() } # Sign the hash $rsaPrivate = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate) $rsaFormatter = [System.Security.Cryptography.RSAPKCS1SignatureFormatter]::new($rsaPrivate) $rsaFormatter.SetHashAlgorithm('SHA256') $signedHash = $rsaFormatter.CreateSignature($hash) # Construct the encrypted column encryption key # EncryptedColumnEncryptionKey = version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature $encryptedColumnEncryptionKeyLength = $version.Length + $cipherTextLength.Length + $keyPathLength.Length + $cipherText.Length + $masterKeyPathBytes.Length + $signedHash.Length $encryptedColumnEncryptionKey = [byte[]]::new($encryptedColumnEncryptionKeyLength) # Copy version byte $currentIndex = 0; [System.Buffer]::BlockCopy($version, 0, $encryptedColumnEncryptionKey, $currentIndex, $version.Length) > $null $currentIndex += $version.Length # Copy key path length [System.Buffer]::BlockCopy($keyPathLength, 0, $encryptedColumnEncryptionKey, $currentIndex, $keyPathLength.Length) > $null $currentIndex += $keyPathLength.Length # Copy ciphertext length [System.Buffer]::BlockCopy($cipherTextLength, 0, $encryptedColumnEncryptionKey, $currentIndex, $cipherTextLength.Length) > $null $currentIndex += $cipherTextLength.Length # Copy key path [System.Buffer]::BlockCopy($masterKeyPathBytes, 0, $encryptedColumnEncryptionKey, $currentIndex, $masterKeyPathBytes.Length) > $null $currentIndex += $masterKeyPathBytes.Length # Copy ciphertext [System.Buffer]::BlockCopy($cipherText, 0, $encryptedColumnEncryptionKey, $currentIndex, $cipherText.Length) > $null $currentIndex += $cipherText.Length # copy the signature [System.Buffer]::BlockCopy($signedHash, 0, $encryptedColumnEncryptionKey, $currentIndex, $signedHash.Length) > $null return $encryptedColumnEncryptionKey } function Test-SttiDatabaseConnection { [OutputType([bool])] [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [pscredential] $ServiceCredential, [switch] $SkipPermissionCheck ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $scriptBlock = { Invoke-Sqlcmd @Using:sqlVersionSpecificParameters -ServerInstance $Using:Instance.SqlServer -Database master -Query "SELECT 1" -ErrorAction Stop > $null } Start-Job -ScriptBlock $scriptBlock -Credential $ServiceCredential | Receive-Job -Wait -ErrorAction Stop } catch{ Write-SttiLog "Could not connect to Sql Server instance $($Instance.SqlServer): $($_.Exception)" -Level Error -ErrorAction Stop return $false } #TODO: checks if version and settings of server are correct if (!$SkipPermissionCheck){ try{ $query = "SELECT permission_name FROM fn_my_permissions (NULL, 'SERVER') WHERE permission_Name IN ('CREATE ANY DATABASE', 'ALTER ANY LOGIN')" $result = Invoke-Sqlcmd @sqlVersionSpecificParameters -ServerInstance $Instance.SqlServer -Database master -Query $query -ErrorAction Stop } catch{ Write-SttiLog "Could not query server permissions: $($_.Exception)" -Level Error -ErrorAction Stop return $false } $createDatabasePermission = $null -ne ($result | Where-Object permission_name -Like "CREATE ANY DATABASE") $createLoginPermission = $null -ne ($result | Where-Object permission_name -Like "ALTER ANY LOGIN") if (!$createDatabasePermission || !$createLoginPermission){ Write-SttiLog "Not enough permissions in Sql Server for installation. CREATE ANY DATABASE=$createDatabasePermission. ALTER ANY LOGIN=$createLoginPermission" -Level Error -ErrorAction Stop return $false } } return $true } function Test-SttiDatabase { [OutputType([bool])] [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [switch] $UseExistingDatabase, [pscredential] $ServiceCredential, [switch] $SkipPermissionCheck ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ $scriptBlock = { $query = "SELECT name FROM sys.databases WHERE name LIKE '$($Using:Instance.Database)'" return Invoke-Sqlcmd @Using:sqlVersionSpecificParameters -ServerInstance $Using:Instance.SqlServer -Database master -Query $query -ErrorAction Stop } $databases = Start-Job -ScriptBlock $scriptBlock -Credential $ServiceCredential | Receive-Job -Wait -ErrorAction Stop } catch{ Write-SttiLog "Could not enumerate Sql databases: $($_.Exception)" -Level Error -ErrorAction Stop return $false } $databaseExists = $null -ne ($databases | Where-Object name -Like $Instance.Database) if (!$UseExistingDatabase) { if ($databaseExists){ Write-SttiLog "Database $($Instance.Database) already exists. Specify parameter -UseExistingDatabase if you want to use the existing database." -Level Error -ErrorAction Stop return $false } } else{ if (!$databaseExists){ Write-SttiLog "Database $($Instance.Database) does not exist." -Level Error -ErrorAction Stop return $false } #TODO: checks if settings of database are correct if (!$SkipPermissionCheck){ try{ $query = "SELECT permission_name FROM fn_my_permissions (NULL, 'DATABASE') WHERE permission_Name IN ('ALTER ANY ROLE', 'ALTER ANY USER')" $result = Invoke-Sqlcmd @sqlVersionSpecificParameters -ServerInstance $Instance.SqlServer -Database $Instance.Database -Query $query -ErrorAction Stop } catch{ Write-SttiLog "Could not query database permissions: $($_.Exception)" -Level Error -ErrorAction Stop return $false } $alterRolePermission = $null -ne ($result | Where-Object permission_name -Like "ALTER ANY ROLE") if (!$alterRolePermission){ Write-SttiLog "Not enough permissions in Sql Server database $($Instance.Database) for installation. ALTER ANY ROLE=$alterRolePermission." -Level Error -ErrorAction Stop return $false } } } return $true } function Invoke-SttiDatabaseMigration{ [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [PSCredential] $ServiceCredential ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ if ($Instance.Roles.Contains([SttiInstanceRoles]::Worker)){ try{ $workerPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Worker") $workerBinaryPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Worker\Halvotec.Stti.Worker.exe") $outPath = [System.IO.Path]::Combine($Instance.Path, "data\logs\migratedb-out.txt") $errorPath = [System.IO.Path]::Combine($Instance.Path, "data\logs\migratedb-err.txt") #Workaround for running in remoting session where no different credentials can be used if ($env:useCurrentUser -and $env:useCurrentUser -eq $true){ ## Workaround because of Bug in PS 7.4. ## See https://github.com/PowerShell/PowerShell/issues/5421 ## Rollback when PS 7.4.1 is released ##$process = Start-Process -FilePath $workerBinaryPath -ArgumentList "migratedb" -WorkingDirectory $workerPath -Wait -PassThru -RedirectStandardOutput $outPath -RedirectStandardError $errorPath -WindowStyle Hidden -ErrorAction Stop $process = Start-Process -FilePath $workerBinaryPath -ArgumentList "migratedb" -WorkingDirectory $workerPath -PassThru -RedirectStandardOutput $outPath -RedirectStandardError $errorPath -WindowStyle Hidden -ErrorAction Stop $dummy = $process.Handle $process | Wait-Process } else{ ## Workaround because of Bug in PS 7.4. ## See https://github.com/PowerShell/PowerShell/issues/5421 ## Rollback when PS 7.4.1 is released ##$process = Start-Process -FilePath $workerBinaryPath -ArgumentList "migratedb" -WorkingDirectory $workerPath -Credential $ServiceCredential -Wait -PassThru -RedirectStandardOutput $outPath -RedirectStandardError $errorPath -WindowStyle Hidden -ErrorAction Stop $process = Start-Process -FilePath $workerBinaryPath -ArgumentList "migratedb" -WorkingDirectory $workerPath -Credential $ServiceCredential -PassThru -RedirectStandardOutput $outPath -RedirectStandardError $errorPath -WindowStyle Hidden -ErrorAction Stop $dummy = $process.Handle $process | Wait-Process } $exitCode = $process.ExitCode Get-Content $outPath -ErrorAction SilentlyContinue | Write-SttiLog -Level Verbose Get-Content $errorPath -ErrorAction SilentlyContinue | Write-SttiLog -Level Warn if ($exitCode -ne 0){ Write-SttiLog "Halvotec.Stti.Worker.exe exited with code $exitCode" -Level Error } } finally{ Remove-Item $outPath -ErrorAction SilentlyContinue Remove-Item $errorPath -ErrorAction SilentlyContinue } } } catch{ Write-SttiLog $_ -Level Error } } function Test-SttiDatabaseEncryption{ [OutputType([bool])] [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance # [Parameter(Mandatory)] # [PSCredential] $ServiceCredential ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ return $true } catch{ Write-SttiLog $_ -Level Error return $false } } function Get-SttiDatabaseConnectionString{ [OutputType([string])] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance ) return "Server=$($Instance.SqlServer);Database=$($Instance.Database);Integrated Security=true;Column Encryption Setting=enabled;ApplicationIntent=ReadWrite;Language=English;Encrypt=False" } function Set-SttiSqlVersionSpecificParameters{ $sqlEncryptValue = "Optional" $sqlEncryptParameterAvailable = (Get-Command Invoke-Sqlcmd).Parameters.ContainsKey("Encrypt") if ($sqlEncryptParameterAvailable -eq $true){ $sqlVersionSpecificParameters.Add("Encrypt", $sqlEncryptValue) } } #endregion Database Management #region Package Management $deploymentFilePattern = "^SttiApp.(?<version>\d+\.\d+\.\d+(?:\.\d+)?)\.nupkg$" function Get-SttiPackage { [OutputType([SttiPackage])] [CmdletBinding()] Param( [ValidatePattern("^\d+\.\d+\.\d+(?:\.\d+)?$|^latest$")] [string] $VersionNumber = "latest" ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $deploymentFiles = Get-ChildItem "$([System.IO.Path]::Combine((Get-SttiBasePath), "Packages", "*.nupkg"))" $packages = $deploymentFiles | Where-Object -Property Name -Match $deploymentFilePattern | ForEach-Object {([SttiPackage]::new([System.Version]::Parse($Matches.version), $_.FullName, $true))} if ($VersionNumber -eq "latest"){ $package = $packages | Sort-Object -Property Version -Descending | Select-Object -First 1 } else { $version = [System.Version]::Parse($VersionNumber) $package = $packages | Where-Object -Property Version -eq $version } if ($null -eq $package){ throw "no package found" } return $package } # Get-SttiPackage -VersionNumber "latest" function Set-SttiDefaultValues{ [CmdletBinding(DefaultParameterSetName="InstanceName")] Param( [Parameter(ParameterSetName="Instance")] [SttiInstanceConfig] $Instance, [Parameter(ParameterSetName="InstanceName")] [string] $InstanceName, [Parameter(ParameterSetName="Instance")] [Parameter(ParameterSetName="InstanceName")] [PSCredential] $CustomerCredential, [Parameter(ParameterSetName="Instance")] [Parameter(ParameterSetName="InstanceName")] [PSCredential] $ServiceCredential, [Parameter(ParameterSetName="Instance")] [Parameter(ParameterSetName="InstanceName")] [string] $CertificateThumbprint, [Parameter(ParameterSetName="Instance")] [Parameter(ParameterSetName="InstanceName")] [string] $PackageSource, [Parameter(ParameterSetName="Instance")] [Parameter(ParameterSetName="InstanceName")] [string] $UseLocalServiceUser ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug if ($PSBoundParameters.ContainsKey("Instance")){ $InstanceName = $Instance.Name.ToUpperInvariant() } else{ if ($InstanceName){ $InstanceName = $InstanceName.ToUpperInvariant() } } # Restore $defaults = Get-SttiDefaultValues # Set new Values if ($PSBoundParameters.ContainsKey("CustomerCredential")){ $defaults.CustomerCredential = $CustomerCredential } if ($PSBoundParameters.ContainsKey("PackageSource")){ $defaults.PackageSource = $PackageSource } if ($PSBoundParameters.ContainsKey("ServiceCredential")){ if (!$InstanceName) { throw "For parameter ServiceCredential also parameter Instance or Instancename must be specified" } $defaults.ForInstance($InstanceName).ServiceCredential = $ServiceCredential } if ($PSBoundParameters.ContainsKey("CertificateThumbprint")){ if (!$InstanceName) { throw "For parameter CertificateThumbprint also parameter Instance or Instancename must be specified" } $defaults.ForInstance($InstanceName).CertificateThumbprint = $CertificateThumbprint } if ($PSBoundParameters.ContainsKey("UseLocalServiceUser")){ if (!$InstanceName) { throw "For parameter UseLocalServiceUser also parameter Instance or Instancename must be specified" } $defaults.ForInstance($InstanceName).UseLocalServiceUser = $UseLocalServiceUser } #Serialize $defaultsFile = [System.IO.Path]::Combine((Get-SttiUserHomePath), "defaults.json") $defaultsData = @{ InstanceDefaults = @{} } foreach ($key in ($defaults | Get-Member -Type Properties | Select-Object -ExpandProperty Name)){ if ($key -eq "InstanceDefaults"){ $instanceDefaultsTable = $defaults.$Key foreach ($in in $instanceDefaultsTable.Keys){ $instanceDefaults = $defaults.ForInstance($in) $instanceDefaultsData = $defaultsData.InstanceDefaults[$in] = @{} foreach ($ikey in ($instanceDefaults | Get-Member -Type Properties | Select-Object -ExpandProperty Name)){ if ($ikey -like "*Credential"){ $instanceDefaultsData[$ikey] = Convert-CredentialToHashtable -Credential $instanceDefaults.$ikey } else{ $instanceDefaultsData[$ikey] = $instanceDefaults.$ikey } } } } elseif ($key -like "*Credential"){ $defaultsData[$key] = Convert-CredentialToHashtable -Credential $defaults.$key } else{ $defaultsData[$key] = $defaults.$key } } $defaultsData | ConvertTo-Json -Depth 10 -ErrorAction Stop | Out-File $defaultsFile -Encoding utf8 -ErrorAction Stop Restore-SttiDefaultParameterValues } #Set-SttiDefaults function Get-SttiDefaultValues{ [OutputType([SttiDefaults])] [CmdletBinding()] Param( ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $defaults = [SttiDefaults]::new() $defaults.InstanceDefaults = @{} $defaultsFile = [System.IO.Path]::Combine((Get-SttiUserHomePath), "defaults.json") if (!(Test-Path $defaultsFile)){ return $defaults } $defaultsData = Get-Content $defaultsFile -Raw -Encoding utf8 | ConvertFrom-Json -AsHashtable foreach ($key in $defaultsData.Keys){ if ($key -eq "InstanceDefaults"){ $instanceDefaultsTable = $defaultsData[$key] foreach ($in in $instanceDefaultsTable.Keys){ $instanceDefaultsData = $instanceDefaultsTable[$in] $instanceDefaults = $defaults.ForInstance($in) foreach ($ikey in $instanceDefaultsData.Keys){ if ($ikey -like "*Credential"){ $instanceDefaults.$ikey = Convert-HashtableToCredential -Data $instanceDefaultsData[$ikey] } else{ $instanceDefaults.$ikey = $instanceDefaultsData[$ikey] } } } } elseif ($key -like "*Credential"){ $defaults.$key = Convert-HashtableToCredential -Data $defaultsData[$key] } else{ $defaults.$key = $defaultsData[$key] } } return $defaults } #Get-SttiDefaults function Restore-SttiDefaultParameterValues{ [CmdletBinding()] Param( ) $defaults = Get-SttiDefaultValues $params = @{} foreach ($prop in ($defaults | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)){ if ($prop -eq "InstanceDefaults"){ $instanceDefaults = [SttiInstanceDefaults]::new() if ($script:shellSession.Instance){ $instanceDefaults = $defaults.ForInstance($script:shellSession.Instance) } foreach ($iprop in ($instanceDefaults | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)){ $value = $instanceDefaults.$iprop $params.Add($iprop, $value) } } else{ $value = $defaults.$prop $params.Add($prop, $value) } } foreach ($prop in ($script:shellSession | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)){ $value = $script:shellSession.$prop $params.Add($prop, $value) } $params.Add("NoProxy", $true) foreach ($param in $params.Keys){ $paramNames = switch ($param) { "ApiCallTimeoutSec" { "Invoke-RestMethod:TimeoutSec", "Invoke-WebRequest:TimeoutSec" } "NoProxy" { "Invoke-RestMethod:NoProxy", "Invoke-WebRequest:NoProxy" } default { [string[]]"*-Stti*:$param" } } $paramValue = $params[$param] foreach ($paramName in $paramNames){ if ($paramValue){ $Global:PSDefaultParameterValues[$paramName] = $paramValue } else{ $Global:PSDefaultParameterValues.Remove($paramName) } } } } function Get-SttiUserHomePath{ [OutputType([string])] Param() $sttiUserHomePath = [System.IO.Path]::Combine([Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData), "Stti") if (!(Test-Path $sttiUserHomePath)){ New-Item $sttiUserHomePath -ItemType Directory -Force > $null } return $sttiUserHomePath } function Convert-CredentialToHashtable{ [OutputType([hashtable])] param( [pscredential]$Credential ) if ($Credential){ return @{ UserName = $Credential.UserName Password = ($Credential.Password | ConvertFrom-SecureString) } } else { return $null } } function Convert-HashtableToCredential{ [OutputType([pscredential])] param( [hashtable]$Data ) if ($Data){ return [System.Management.Automation.PsCredential]::new($Data.UserName, ($Data.Password | ConvertTo-SecureString)) } else { return $null } } function Find-SttiPackage{ [OutputType([SttiPackage[]])] [CmdletBinding()] Param( [ValidatePattern("^\d+\.\d+\.\d+(?:\.\d+)?$|^latest$")] [string] $VersionNumber, [string] $PackageSource = $script:defaultPackageSource, [Parameter(Mandatory)] [pscredential] $CustomerCredential ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug Write-SttiLog "Getting feed resources" -Level Verbose $resources = (Invoke-RestMethod -Uri $PackageSource -Credential $customerCredential -ErrorAction Stop).resources $searchEndpoint = ($resources | Where-Object "@type" -like "SearchQueryService")."@id" if ($null -eq $searchEndpoint){ $searchEndpoint = ($resources | Where-Object "@type" -like "SearchQueryService/*" | Select-Object -First 1)."@id" } if ($null -eq $searchEndpoint){ Write-SttiLog "Could not find a v3 search endpoint in the feed" -Level Error } $packageContentEndpoint = ($resources | Where-Object "@type" -like "PackageBaseAddress/3.0.0")."@id" if ($null -eq $packageContentEndpoint){ Write-SttiLog "Could not find a v3 content endpoint in the feed" -Level Error } Write-SttiLog "Searching package" -Level Verbose $packageData = (Invoke-RestMethod -Uri "$($searchEndpoint)?q=SttiApp" -Credential $customerCredential -ErrorAction Stop).data $packages = $packageData.versions | ForEach-Object {([SttiPackage]::new([System.Version]::Parse($_.version), "$packageContentEndpoint/$($packageData.id.ToLower())/$($_.version.ToLower())/$($packageData.id.ToLower()).$($_.version.ToLower()).nupkg", $false ))} if (!$PSBoundParameters.ContainsKey("VersionNumber")){ return $packages } elseif ($VersionNumber -like "latest") { return $packages | Where-Object Version -eq ([System.Version]::Parse($packageData.version)) } else{ return $packages | Where-Object Version -eq ([System.Version]::Parse($VersionNumber)) } } #Find-SttiPackage latest function Import-SttiPackage{ [OutputType([SttiPackage])] [CmdletBinding()] Param( [Parameter(Mandatory, ValueFromPipeline)] [SttiPackage] $Package, [Parameter(Mandatory)] [pscredential] $CustomerCredential ) Process{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug Test-UserHasAdminRole -ErrorAction Stop if ($Package.IsLocal){ Write-SttiLog "Package must not be local" } $fileName = [System.Uri]::new($Package.Path).Segments | Select-Object -Last 1 $packagePath = [System.IO.Path]::Combine((Get-SttiBasePath), "Packages", $fileName) Write-SttiLog "Downloading package" -Level Verbose Invoke-RestMethod -Uri $Package.Path -Credential $customerCredential -OutFile $packagePath -ErrorAction Stop Get-SttiPackage -Version $Package.Version } } #Find-SttiPackage "latest" -Verbose | Import-SttiPackage -Verbose #endregion Package Management #region Windows Service Rights management Add-Type @' using System; using System.Runtime.InteropServices; public enum LSA_AccessPolicy : long { // Other values omitted for clarity POLICY_ALL_ACCESS = 0x00001FFFL } [StructLayout(LayoutKind.Sequential)] public struct LSA_UNICODE_STRING { public UInt16 Length; public UInt16 MaximumLength; public IntPtr Buffer; } [StructLayout(LayoutKind.Sequential)] public struct LSA_OBJECT_ATTRIBUTES { public UInt32 Length; public IntPtr RootDirectory; public LSA_UNICODE_STRING ObjectName; public UInt32 Attributes; public IntPtr SecurityDescriptor; public IntPtr SecurityQualityOfService; } public static partial class AdvAPI32 { [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] public static extern uint LsaOpenPolicy( ref LSA_UNICODE_STRING SystemName, ref LSA_OBJECT_ATTRIBUTES ObjectAttributes, uint DesiredAccess, out IntPtr PolicyHandle); [DllImport("advapi32.dll")] public static extern Int32 LsaClose(IntPtr ObjectHandle); [DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)] public static extern uint LsaAddAccountRights( IntPtr PolicyHandle, byte[] AccountSid, LSA_UNICODE_STRING[] UserRights, uint CountOfRights); } '@ function Get-LsaPolicyHandle() { $system = New-Object LSA_UNICODE_STRING $attrib = New-Object LSA_OBJECT_ATTRIBUTES -Property @{ Length = 0 RootDirectory = [System.IntPtr]::Zero Attributes = 0 SecurityDescriptor = [System.IntPtr]::Zero SecurityQualityOfService = [System.IntPtr]::Zero }; $handle = [System.IntPtr]::Zero $hr = [AdvAPI32]::LsaOpenPolicy([ref] $system, [ref]$attrib, [LSA_AccessPolicy]::POLICY_ALL_ACCESS, [ref]$handle) if (($hr -ne 0) -or ($handle -eq [System.IntPtr]::Zero)) { Write-Error "Failed to open Local Security Authority policy. Error code: $hr" } else { $handle } } function New-Right([string]$rightName){ $unicodeCharSize = 2 New-Object LSA_UNICODE_STRING -Property @{ Buffer = [System.Runtime.InteropServices.Marshal]::StringToHGlobalUni($rightName) Length = $rightName.Length * $unicodeCharSize MaximumLength = ($rightName.Length + 1) * $unicodeCharSize } } function Grant-Rights([System.IntPtr]$policyHandle, [byte[]]$sid, [LSA_UNICODE_STRING[]]$rights) { $result = [AdvAPI32]::LsaAddAccountRights($policyHandle, $sid, $rights, 1) if ($result -ne 0) { Write-Error "Failed to grant right. Error code $result" } } function Grant-LogonAsServiceRight { [CmdletBinding()] Param( [string] $username ) $sid = ((New-Object System.Security.Principal.NTAccount($username)).Translate([System.Security.Principal.SecurityIdentifier])) $sidBytes = [byte[]]::new($sid.BinaryLength) $sid.GetBinaryForm($sidBytes, 0) $logonAsServiceRightName = "SeServiceLogonRight" try { $policy = Get-LsaPolicyHandle $right = New-Right $logonAsServiceRightName Grant-Rights $policy $sidBytes @($right) } finally { if($null -ne $policy){ [AdvAPI32]::LsaClose($policy) | Out-Null } } } #endregion Windows Service Rights management #region Logging function Start-SttiInstallLogFile{ [CmdletBinding()] Param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [switch] $Append = $false, [LogLevels] $Level = [LogLevels]::Verbose ) $Script:logFile = Get-SttiInstallLogFilename -Instance $Instance -Timestamp (Get-Date) $Script:archiveLogFile = Get-SttiInstallLogFilename -Instance $Instance $Script:logLevel = $Level Out-File $logFile -Append:$Append Write-SttiLog "Start installation logfile $($Script:logFile)" -Level Info Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug } function Stop-SttiInstallLogFile{ [CmdletBinding()] Param( ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug Write-SttiLog "Stop installation logfile $($Script:logFile)" -Level Info Get-Content -Path $Script:logFile -Raw | Out-File $Script:archiveLogFile -Append -ErrorAction Continue $Script:logFile = $null $Script:archiveLogFile = $null $Script:logLevel = $null } function Write-SttiInstallLogHeader{ [CmdletBinding()] Param( ) Write-SttiLog "ModuleInfo:" -Level Verbose Get-Module | Where-Object Name -in @("Stti", "SqlServer") | Select-Object -Property Name, Version, PreRelease, ModuleType | Format-Table | Out-String | Write-SttiLog -Level Verbose Write-SttiLog "HostInfo:" -Level Verbose Get-Host | Write-SttiLog -Level Verbose Write-SttiLog "ComputerInfo:" -Level Verbose Get-ComputerInfo | Write-SttiLog -Level Verbose Write-SttiLog "----------------------------------------------------------------" -Level Info -Extraline } function Write-SttiInstallLogFooter{ [CmdletBinding()] Param( ) Write-SttiLog "----------------------------------------------------------------" -Level Info } function Write-SttiLog{ [CmdletBinding()] Param( [switch] $Extraline, [LogLevels] $Level = [LogLevels]::Info, [Parameter(ValueFromPipeline, Position=0)] [object[]] $InputObject ) Process{ if ($null -ne $_){ $i = $_ } elseif ($null -ne $InputObject) { $i = $InputObject } else { throw "InputObject has no value" } # Write to log if existing if ($Script:logFile -and $null -ne $Script:logFile ` -and $Level -ge ($Script:logLevel ?? [LogLevels]::Verbose)) { $i | ForEach-Object{ if ($_ -is [string]) { "$(Get-Date) $($Level.ToString().ToUpperInvariant()) $_" } else { "$(Get-Date) $($Level.ToString().ToUpperInvariant()) $($_ | Format-List | Out-String)" }} | Out-File -FilePath $Script:logFile -Append } switch ($Level){ "Error" { if ($i -is [System.Management.Automation.ErrorRecord]){ Write-Error -ErrorRecord $i } elseif ($i -is [System.Exception]){ Write-Error -Exception $i } else{ Write-Error ($i | ForEach-Object{ if ($_ -is [string]) {$_} else { $_ | Format-List | Out-String }}) } } "Warn" { Write-Warning ($i | ForEach-Object{ if ($_ -is [string]) {$_} else { $_ | Format-List | Out-String }}) } "Info" { Write-Host $i } "Verbose" { Write-Verbose ($i | ForEach-Object{ if ($_ -is [string]) {$_} else { $_ | Format-List | Out-String }}) } "Debug" { Write-Debug ($i | ForEach-Object{ if ($_ -is [string]) {$_} else { $_ | Format-List | Out-String }}) } } } End{ if ($Extraline -eq $true){ if ($Script:logFile -and $null -ne $Script:logFile) { "" | Out-File -FilePath $Script:logFile -Append } switch ($Level){ "Error" { #Write-Error "" } "Warn" { Write-Warning "" } "Info" { Write-Output "" } "Verbose" { Write-Verbose "" } "Debug" { Write-Debug "" } } } } } #"Hallo" | Write-SttiLog -FilePath "Test.txt" function Write-SttiInstanceStatus{ [CmdletBinding()] param( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance ) $instanceStatus = Get-SttiInstanceStatus -Instance $Instance -ErrorAction Stop Write-SttiLog "Current instance status" -Level Verbose $instanceStatus | Write-SttiLog -Level Verbose } #endregion Logging #region Helper function Read-Password { [OutputType([SecureString])] Param( [string] $Prompt ) $pw = $null $validValue = $false while (!$validValue){ $pw1 = [SecureString](Read-Host -Prompt $Prompt -AsSecureString -ErrorAction Stop) $pw2 = [SecureString](Read-Host -Prompt "Please repeat the password" -AsSecureString -ErrorAction Stop) $pw1Text = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pw1)) $pw2Text = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pw2)) $validValue = $pw1Text.CompareTo($pw2Text) -eq 0 if (!$validValue){ Write-Host "The password do not match" } else { $pw = $pw1 } } return $pw } function Test-Credential { [OutputType([bool])] [CmdletBinding()] Param ( [Parameter(Mandatory)] [pscredential] $credential ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $user = $credential.UserName $password = $credential.GetNetworkCredential().Password # Check that all components are present if (!($user) -or !($password)) { Write-SttiLog "Please specify both username and password" -Level Error return $false } # Check format if ($user -match "^[^\\]+\\[^\\]+$"){ $components = $user -split "\\" $domain = $components[0] #$username = $components[1] if ($domain -eq "."){ Write-SttiLog "Don't use '.' as domain or computername." -Level Error return $false } } else{ Write-SttiLog "Username must be specified in format DOMAIN\USERNAME. For local accounts use COMPUTERNAME\USERNAME." -Level Error return $false } $isValid = $false try{ # Workaround, since start-job with credentials cannot be called in remoting session if ($null -eq $ServiceCredential -or ($env:useCurrentUser -and $env:useCurrentUser -eq $true)){ $isValid = $true } else{ $isValid = Start-Job -ScriptBlock { return $true } -Credential $Credential -ErrorAction Stop| Receive-Job -Wait -ErrorAction Stop } return $isValid } catch{ Write-SttiLog "Could not validate the credentials. An error occurred: $($_.Exception)" -Level Error -ErrorAction Stop } if (-not ($isValid)){ Write-SttiLog "Could not validate the credentials. Username or password seems to be invalid." -Level Error -ErrorAction Stop return $isValid } } function Test-Parameter{ Param( [Parameter(Position=1)] $Value, [Parameter(Mandatory, Position=2)] [string]$Name, [ValidateNotNullOrEmpty] [string]$CheckExpression = '$null -ne $Value', [ValidateNotNullOrEmpty] [string]$Message = "Parameter $Name is null" ) if (!(Invoke-Expression $CheckExpression)){ throw $Message } } function Retry-Command { [CmdletBinding()] param ( [parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [scriptblock] $ScriptBlock, [int] $RetryCount = 3, [int] $TimeoutInSecs = 30, [string] $SuccessMessage = "Command executed successfuly!", [string] $FailureMessage = "Failed to execute the command" ) process { $Attempt = 1 $Flag = $true do { try { $PreviousPreference = $ErrorActionPreference $ErrorActionPreference = 'Stop' Invoke-Command -ScriptBlock $ScriptBlock -OutVariable Result $ErrorActionPreference = $PreviousPreference # flow control will execute the next line only if the command in the scriptblock executed without any errors # if an error is thrown, flow control will go to the 'catch' block Write-SttiLog "$SuccessMessage" -Level Verbose $Flag = $false return $Result } catch { if ($Attempt -gt $RetryCount) { Write-SttiLog "$FailureMessage! Total retry attempts: $RetryCount" -Level Error $Flag = $false throw } else { Write-SttiLog "[$Attempt/$RetryCount] $FailureMessage. Retrying in $TimeoutInSecs seconds..." -Level Warn Start-Sleep -Seconds $TimeoutInSecs $Attempt = $Attempt + 1 } } } While ($Flag) } } #endregion Helper #region Deployment function Deploy-Stti { [CmdletBinding()] [OutputType([SttiInstanceStatus])] Param( [string] $InstanceName, [PSCredential] $ServiceCredential, [string] $Version="latest", [string] $SqlServer, [string] $Database="STTI_$($InstanceName.ToUpperInvariant())", [string] $AdminCertificateThumbprint, [string] $EncryptionCertificateThumbprint, [bool] $StoreDbInInstanceDataPath = $true, [string] $WorkerSslCertificateSubject, [uint] $WorkerPort, [string] $WebSslCertificateSubject, [uint] $WebPort, [string] $WorkerHostname, [switch] $Uninstall=$false, [switch] $Force=$false, [SttiInstanceRoles[]] $Roles = @([SttiInstanceRoles]::Worker, [SttiInstanceRoles]::Web) ) Test-UserHasAdminRole -ErrorAction Stop # Create instance stub $instance = Get-SttiInstanceConfig -Name $InstanceName -ErrorAction SilentlyContinue if ($null -eq $instance) { $instance = New-SttiInstanceConfig -Name $InstanceName -SqlServer $SqlServer -Database $Database -Roles $Roles -StoreDbInInstanceDataPath:$StoreDbInInstanceDataPath -WorkerSslCertificateSubject $WorkerSslCertificateSubject -WorkerPort $WorkerPort -WebPort $WebPort -WebSslCertificateSubject $WebSslCertificateSubject -WorkerHostname $WorkerHostname -EncryptionCertificateThumbprint $EncryptionCertificateThumbprint } else { $instance = Set-SttiInstanceConfig -Instance $instance -SqlServer $SqlServer -Database $Database -Roles $Roles -StoreDbInInstanceDataPath:$StoreDbInInstanceDataPath -WorkerSslCertificateSubject $WorkerSslCertificateSubject -WorkerPort $WorkerPort -WebPort $WebPort -WebSslCertificateSubject $WebSslCertificateSubject -WorkerHostname $WorkerHostname } $status = Get-SttiInstanceStatus -Instance $instance -Certificate $adminCert if ($Uninstall -eq $false){ $package = Get-SttiPackage $Version if ($status.InstallStatus.Installed -eq $false){ # Get credentials for service user $svcCred = $ServiceCredential ?? (Get-Credential -Message "Please enter the credentials for the service user") Install-SttiInstance -Instance $instance -Package $package -ServiceCredential $svcCred -Force:$Force } elseif ($status.InstallStatus.Version -ne $package.Version -or $Force){ $svcCred = $ServiceCredential ?? (Get-Credential -Message "Please enter the credentials for the service user") Update-SttiInstance -Instance $instance -Package $package -ServiceCredential $svcCred -Force:$Force -CertificateThumbprint $AdminCertificateThumbprint } else{ #Nothing to do Write-Information "Version $($package.Version) was already installed" } } else { if ($status.InstallStatus.Installed -eq $true -or $Force){ Uninstall-SttiInstance -Instance $instance -Force:$Force # -Certificate $adminCert } else { #Nothing to do Write-Information "The instance was already uninstalled" } } Get-SttiInstanceStatus -Instance $instance -Certificate $adminCert } #endregion Deployment #region Firewall rules function Add-SttiFirewallRules{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ if ($Instance.Roles.Contains([SttiInstanceRoles]::Worker)){ if ($null -eq (Get-NetFirewallRule -Name "Stti-Worker-$($Instance.Name)" -ErrorAction SilentlyContinue)){ Write-SttiLog "Add firewall rule for worker role" -Level Verbose $binaryPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Worker\Halvotec.Stti.Worker.exe") New-NetFirewallRule –Name "Stti-Worker-$($Instance.Name)" -DisplayName "Stti Worker Role($($Instance.Name))" -Program $binaryPath -Direction Inbound -Action Allow -Protocol TCP -Profile Domain -Group "Stti" -ErrorAction Stop > $null } else { Write-SttiLog "Firewall rule for worker role already exists" -Level Verbose } } if ($Instance.Roles.Contains([SttiInstanceRoles]::Web)){ if ($null -eq (Get-NetFirewallRule -Name "Stti-Web-$($Instance.Name)" -ErrorAction SilentlyContinue)){ Write-SttiLog "Add firewall rule for web role" -Level Verbose $binaryPath = [System.IO.Path]::Combine($Instance.Path, "bin\Halvotec.Stti.Web\Halvotec.Stti.Web.exe") New-NetFirewallRule –Name "Stti-Web-$($Instance.Name)" -DisplayName "Stti Web Role($($Instance.Name))" -Program $binaryPath -Direction Inbound -Action Allow -Protocol TCP -Profile Domain -Group "Stti" -ErrorAction Stop > $null } else { Write-SttiLog "Firewall rule for web role already exists" -Level Verbose } } } catch{ Write-SttiLog $_ -Level Error } } function Remove-SttiFirewallRules{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug try{ if ($null -ne (Get-NetFirewallRule -Name "Stti-Web-$($Instance.Name)" -ErrorAction SilentlyContinue)){ Write-SttiLog "Remove firewall rule for web role" -Level Verbose Remove-NetFirewallRule -Name "Stti-Web-$($Instance.Name)" -ErrorAction Stop } else { Write-SttiLog "Firewall rule for web role was not existing" -Level Verbose } if ($null -ne (Get-NetFirewallRule -Name "Stti-Worker-$($Instance.Name)" -ErrorAction SilentlyContinue)){ Write-SttiLog "Remove firewall rule for worker role" -Level Verbose Remove-NetFirewallRule -Name "Stti-Worker-$($Instance.Name)" -ErrorAction Stop } } catch{ Write-SttiLog $_ -Level Error } } #endregion Firewall rules #region Config function Get-SttiApiUrl{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance ) return "https://$($Instance.WorkerHostname):$($Instance.WorkerPort)/api" } function Get-SttiSystemSettings{ [OutputType([SttiSystemSettings])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance try{ try { $config = Invoke-RestMethod "$apiUrl/config/system" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiSystemSettings]::new($config.data) } catch [Microsoft.PowerShell.Commands.HttpResponseException]{ if ($_.Exception.Response.StatusCode -ne 'BadRequest'){ throw } # Use old API, if the new API is not available $config = Invoke-RestMethod "$apiUrl/config" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiSystemSettings]::new($config.data.system) } } catch{ Write-SttiLog $_ -Level Error } } function Set-SttiSystemSettings{ [CmdletBinding()] param ( [Parameter(Mandatory, ParameterSetName="SettingUpdates")] [Parameter(Mandatory, ParameterSetName="AsParameters")] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ParameterSetName="SettingUpdates")] $SettingUpdates, [Parameter(ParameterSetName="AsParameters")] [string] $InstanceName, [Parameter(ParameterSetName="AsParameters")] [string] $DataDirectory, [Parameter(ParameterSetName="AsParameters")] [bool] $UseProxy, [Parameter(ParameterSetName="AsParameters")] [string] $ProxyAddress, [Parameter(Mandatory, ParameterSetName="SettingUpdates")] [Parameter(Mandatory, ParameterSetName="AsParameters")] [string] $CertificateThumbprint ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance if (!$PSBoundParameters.ContainsKey("SettingUpdates")){ $SettingUpdates = @{} if ($PSBoundParameters.ContainsKey("InstanceName")){ $SettingUpdates.instance = $InstanceName } if ($PSBoundParameters.ContainsKey("DataDirectory")){ $SettingUpdates.dataDirectory = $DataDirectory } if ($PSBoundParameters.ContainsKey("UseProxy")){ $SettingUpdates.useProxy = $UseProxy } if ($PSBoundParameters.ContainsKey("ProxyAddress")){ $SettingUpdates.proxyAddress = $ProxyAddress } } try{ $result = Invoke-RestMethod "$apiUrl/config/system" -Method Put -Body (ConvertTo-Json $SettingUpdates) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiSystemSettings]::new($result.data.system) } catch{ Write-SttiLog $_ -Level Error } } function Get-SttiModule{ [OutputType([SttiModule])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [string] $ModuleId, [string] $Name, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance try{ $result = Invoke-RestMethod "$apiUrl/config/modules" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop $modules = $result.data $filteredModules = $modules | Where-Object { (!$PSBoundParameters.ContainsKey("ModuleId") -or $_.id -like $ModuleId) -and (!$PSBoundParameters.ContainsKey("Name") -or $_.name -like $Name) } return [SttiModule]::FromArray($filteredModules) } catch{ Write-SttiLog $_ -Level Error } } function Set-SttiModuleEnvironment{ [OutputType([SttiModuleSettings])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ModuleId, [Parameter(Mandatory)] [string] $Environment, [Parameter(Mandatory)] [string] $CertificateThumbprint, [string] $ClientId ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $result = Invoke-RestMethod "$apiUrl/config$($PSBoundParameters.ContainsKey('ClientId') ? ""/clients/$ClientId"" : """")/modules/$ModuleId/env/$Environment" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiModuleSettings]::new($result.data, $Environment) } catch{ Write-SttiLog $_ -Level Error } } } function Get-SttiModuleSettings{ [OutputType([SttiModuleSettings])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ModuleId, [string] $Environment, [Parameter(Mandatory)] [string] $CertificateThumbprint, [string] $ClientId ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $result = Invoke-RestMethod "$apiUrl/config$($PSBoundParameters.ContainsKey('ClientId') ? ""/clients/$ClientId"" : """")/modules/$ModuleId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiModuleSettings]::new($result.data, $Environment) } catch{ Write-SttiLog $_ -Level Error } } } function Set-SttiModuleSettings{ [OutputType([SttiModuleSettings])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ModuleId, [Parameter(Mandatory)] [PSCustomObject] $SettingUpdates, [string] $Environment, [Parameter(Mandatory)] [string] $CertificateThumbprint, [string] $ClientId ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $result = Invoke-RestMethod "$apiUrl/config$($PSBoundParameters.ContainsKey('ClientId') ? ""/clients/$ClientId"" : """")/modules/$ModuleId$($PSBoundParameters.ContainsKey("Environment") ? ""/env/$Environment"" : """")/settings" -Method Put -Body (ConvertTo-Json $SettingUpdates) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiModuleSettings]::new($result.data, $Environment) } catch{ Write-SttiLog $_ -Level Error } } } function Get-SttiModuleSecrets{ [OutputType([SttiModuleSecrets])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ModuleId, [string] $Environment, [Parameter(Mandatory)] [string] $CertificateThumbprint, [string] $ClientId ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $result = Invoke-RestMethod "$apiUrl/config$($PSBoundParameters.ContainsKey('ClientId') ? ""/clients/$ClientId"" : """")/modules/$ModuleId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiModuleSecrets]::new($result.data, $Environment) } catch{ Write-SttiLog $_ -Level Error } } } function New-SttiModuleTokenSecret{ [OutputType([SttiModuleSecrets])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ModuleId, [Parameter(Mandatory)] [string] $Token, [datetime] $ValidFrom, [datetime] $ValidThru, [string] $Environment, [ValidateSet('AccessToken', 'RefreshToken')] [string] $Usage = 'AccessToken', [Parameter(Mandatory)] [string] $CertificateThumbprint, [string] $ClientId ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ $Secret = @{ type = "TokenSecretInfo" token = $Token usage = $Usage } if ($PSBoundParameters.ContainsKey("ValidFrom")){ $Secret.validFrom = $ValidFrom } if ($PSBoundParameters.ContainsKey("ValidThru")){ $Secret.validThrough = $ValidThru } $Data = @{ moduleId = "" secret = $Secret } try{ $result = Invoke-RestMethod "$apiUrl/config$($PSBoundParameters.ContainsKey('ClientId') ? ""/clients/$ClientId"" : """")/modules/$ModuleId$($PSBoundParameters.ContainsKey("Environment") ? ""/env/$Environment"" : """")/secrets" -Method Post -Body (ConvertTo-Json $Data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiModuleSecrets]::new($result.data, $Environment) } catch{ Write-SttiLog $_ -Level Error } } } function Remove-SttiModuleSecret{ [CmdletBinding()] [OutputType([SttiModuleSecrets])] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ModuleId, [Parameter(Mandatory)] [string] $SecretId, [string] $Environment, [Parameter(Mandatory)] [string] $CertificateThumbprint, [string] $ClientId ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $result = Invoke-RestMethod "$apiUrl/config$($PSBoundParameters.ContainsKey('ClientId') ? ""/clients/$ClientId"" : """")/modules/$ModuleId$($PSBoundParameters.ContainsKey("Environment") ? ""/env/$Environment"" : """")/secrets/$SecretId" -Method Delete -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiModuleSecrets]::FromArray($result.data, $Environment) } catch{ Write-SttiLog $_ -Level Error } } } #endregion Config #region Clients function Get-SttiClient{ [OutputType([SttiClient[]])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [string] $ClientId, [string] $DisplayName, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance try{ $response = Invoke-RestMethod "$apiUrl/security/clients" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop $clients = $response.data $filteredClients = $clients | Where-Object { (!$PSBoundParameters.ContainsKey("ClientId") -or $_.id -like $ClientId) -and (!$PSBoundParameters.ContainsKey("DisplayName") -or $_.displayName -like $DisplayName) } return [SttiClient]::FromArray($filteredClients) } catch{ Write-SttiLog $_ -Level Error } } function Get-SttiClientSettings{ [OutputType([SttiClientSettings])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ClientId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiClientSettings]::new($response.data) } catch{ Write-SttiLog $_ -Level Error } } } function New-SttiClient{ [OutputType([SttiClient])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [string] $DisplayName, [string] $ClientCertificateThumbprint, [SttiClientCommunicationType] $Communication = [SttiClientCommunicationType]::Api, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance $Data = @{ displayName = $DisplayName encryptionKey = @{ algorithm = "Aes256" } } if ($PSBoundParameters.ContainsKey("ClientCertificateThumbprint")){ $Data.certificateCredential = @{ thumbprint = $ClientCertificateThumbprint description = "Set on $(Get-Date -Format "s")" } } $Data.communication = "$($Communication)" try{ $response = Invoke-RestMethod "$apiUrl/security/clients" -Method Post -Body (ConvertTo-Json $Data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiClient]::new($response.data) } catch{ Write-SttiLog $_ -Level Error } } function Set-SttiClientSettings{ [OutputType([SttiClientSettings])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ClientId, [string] $DisplayName, [SttiClientCommunicationType] $Communication, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ $Data = @{ } $hasModifications = $false if ($PSBoundParameters.ContainsKey("DisplayName")){ $Data.displayName = $DisplayName $hasModifications = $true } if ($PSBoundParameters.ContainsKey("Communication")){ $Data.communication = "$($Communication)" $hasModifications = $true } if (!$hasModifications){ throw "Parameters do not result in any modification" } try{ $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId" -Method Put -Body (ConvertTo-Json $Data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiClientSettings]::new($response.data) } catch{ Write-SttiLog $_ -Level Error } } } function Get-SttiClientCredential{ [OutputType([SttiCredential])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ClientId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiCredential]::FromArray($response.data.credentials) } catch{ Write-SttiLog $_ -Level Error } } } function Add-SttiClientCertificateCredential{ [OutputType([SttiCredential])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ClientId, [Parameter(Mandatory)] [string] $ClientCertificateThumbprint, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ $Data = @{ } $Data.certificateCredential = @{ thumbprint = $ClientCertificateThumbprint description = "Set on $(Get-Date -Format "s")" } try{ $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/credentials" -Method Post -Body (ConvertTo-Json $Data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiCredential]::new(($response.data.credentials | Select-Object -Last 1)) } catch{ Write-SttiLog $_ -Level Error } } } function Remove-SttiClientCredential{ [OutputType([SttiCredential])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ClientId, [Parameter(Mandatory)] [string] $CredentialId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/credentials/$CredentialId" -Method Delete -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiCredential]::FromArray($response.data.credentials) } catch{ Write-SttiLog $_ -Level Error } } } function Enable-SttiClient{ [OutputType([SttiClientSettings])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ClientId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/activate" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiClientSettings]::new($response.data) } catch{ Write-SttiLog $_ -Level Error } } } function Disable-SttiClient{ [OutputType([SttiClientSettings])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ClientId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/deactivate" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiClientSettings]::new($response.data) } catch{ Write-SttiLog $_ -Level Error } } } function Get-SttiClientEncryptionKey{ [OutputType([SttiEncryptionKey])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ClientId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiEncryptionKey]::FromArray($response.data.encryption.keys) } catch{ Write-SttiLog $_ -Level Error } } } function New-SttiClientEncryptionKey{ [OutputType([SttiEncryptionKey])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ClientId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ $data = @{ key = @{ algorithm = "Aes256" } } try{ $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/encryption/symmetrickeys" -Method Post -Body (ConvertTo-Json $data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiEncryptionKey]::new(($response.data.encryption.keys | Select-Object -Last 1)) } catch{ Write-SttiLog $_ -Level Error } } } function Enable-SttiClientEncryptionKey{ [OutputType([SttiEncryptionKey])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ClientId, [Parameter(Mandatory)] [string] $KeyId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/encryption/symmetrickeys/$KeyId/activate" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiEncryptionKey]::new(($response.data.encryption.keys | Where-Object { $_.keyId -like $KeyId })) } catch{ Write-SttiLog $_ -Level Error } } } function Disable-SttiClientEncryptionKey{ [OutputType([SttiEncryptionKey])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ClientId, [Parameter(Mandatory)] [string] $KeyId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/encryption/symmetrickeys/$KeyId/deactivate" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiEncryptionKey]::new(($response.data.encryption.keys | Where-Object { $_.keyId -like $KeyId })) } catch{ Write-SttiLog $_ -Level Error } } } function Remove-SttiClientEncryptionKey{ [OutputType([SttiEncryptionKey])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $ClientId, [Parameter(Mandatory)] [string] $KeyId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/clients/$ClientId/encryption/symmetrickeys/$KeyId" -Method Delete -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiEncryptionKey]::FromArray($response.data.encryption.keys) } catch{ Write-SttiLog $_ -Level Error } } } #endregion Clients #region Users function Get-SttiUser{ [OutputType([SttiUser])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [string] $UserId, [string] $DisplayName, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance try{ $response = Invoke-RestMethod "$apiUrl/security/users" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop $users = $response.data $filteredUsers = $users | Where-Object { (!$PSBoundParameters.ContainsKey("UserId") -or $_.id -like $UserId) -and (!$PSBoundParameters.ContainsKey("DisplayName") -or $_.displayName -like $DisplayName) } return [SttiUser]::FromArray($filteredUsers) } catch{ Write-SttiLog $_ -Level Error } } function Convert-ObjectToRoles{ [OutputType([SttiUserRoles[]])] Param( [PSCustomObject] $rolesObj ) $roles = $rolesObj | ConvertTo-Json -Depth 10 | ConvertFrom-Json -Depth 10 -AsHashtable $result = [System.Collections.ArrayList]::new() if ($roles.operationsAdminRole -eq $true) { [void]$result.Add([SttiUserRoles]::OperationsAdmin) } if ($roles.dataAdminRole -eq $true) { [void]$result.Add([SttiUserRoles]::DataAdmin) } if ($roles.securityAdminRole -eq $true) { [void]$result.Add([SttiUserRoles]::SecurityAdmin) } if ($roles.healthWatcherRole -eq $true) { [void]$result.Add([SttiUserRoles]::HealthWatcher) } if ($roles.submissionAppUserRole -eq $true) { [void]$result.Add([SttiUserRoles]::SubmissionAppUser) } return $result } function Convert-RolesToObject{ [OutputType([pscustomobject])] Param( [SttiUserRoles[]] $roles, $objectToUse ) if ($objectToUse){ $result = $objectToUse } else{ $result = @{} } $result.operationsAdminRole = $Roles.Contains([SttiUserRoles]::OperationsAdmin) $result.securityAdminRole = $Roles.Contains([SttiUserRoles]::SecurityAdmin) $result.dataAdminRole = $Roles.Contains([SttiUserRoles]::DataAdmin) $result.healthWatcherRole = $Roles.Contains([SttiUserRoles]::HealthWatcher) $result.submissionAppUserRole = $Roles.Contains([SttiUserRoles]::SubmissionAppUser) if (!$objectToUse){ return $result } } function New-SttiUser{ [OutputType([SttiUser])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [string] $DisplayName, [Parameter(Mandatory, ParameterSetName='CertificateCredentialsSpecificClients')] [Parameter(Mandatory, ParameterSetName='CertificateCredentialsAllClients')] [string] $UserCertificateThumbprint, [Parameter(Mandatory, ParameterSetName='UsernamePasswordCredentialsSpecificClients')] [Parameter(Mandatory, ParameterSetName='UsernamePasswordCredentialsAllClients')] [pscredential] $UsernamePasswordCredential, [Parameter(Mandatory)] [SttiUserRoles[]] $Roles, [Parameter(Mandatory, ParameterSetName='CertificateCredentialsSpecificClients')] [Parameter(Mandatory, ParameterSetName='UsernamePasswordCredentialsSpecificClients')] [string] $ClientIds, [Parameter(Mandatory, ParameterSetName='CertificateCredentialsAllClients')] [Parameter(Mandatory, ParameterSetName='UsernamePasswordCredentialsAllClients')] [switch] $AllClients, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance $data = @{ displayName = $DisplayName } if ($PSBoundParameters.ContainsKey("ClientIds")){ $data.clientid = $ClientIds } if ($PSBoundParameters.ContainsKey("UserCertificateThumbprint")){ $data.certificateCredential = @{ thumbprint = $UserCertificateThumbprint description = "Set on $(Get-Date -Format "s")" } } if ($PSBoundParameters.ContainsKey("UsernamePasswordCredential")){ $data.credential = @{ type = 'SttiPasswordCredential' username = $UsernamePasswordCredential.UserName password = ($UsernamePasswordCredential.Password | ConvertFrom-SecureString -AsPlainText) } } if ($PSBoundParameters.ContainsKey("Roles")){ Convert-RolesToObject -Roles $Roles -ObjectToUse $data } try{ $response = Invoke-RestMethod "$apiUrl/security/users" -Method Post -Body (ConvertTo-Json $data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiUser]::new($response.data) } catch{ Write-SttiLog $_ -Level Error } } function Get-SttiUserSettings{ [OutputType([SttiUserSettings])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $UserId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/users/$UserId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiUserSettings]::new($response.data) } catch{ Write-SttiLog $_ -Level Error } } } function Set-SttiUserSettings{ [OutputType([SttiUserSettings])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $UserId, [string] $DisplayName, [SttiUserRoles[]] $Roles, [Parameter(ParameterSetName='SpecificClients')] [string] $ClientIds, [Parameter(ParameterSetName='AllClients')] [switch] $AllClients, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ $Data = @{ } $hasModifications = $false if ($PSBoundParameters.ContainsKey("DisplayName")){ $Data.displayName = $DisplayName $hasModifications = $true } if ($PSBoundParameters.ContainsKey("Roles")){ Convert-RolesToObject -Roles $Roles -ObjectToUse $Data $hasModifications = $true } if ($PSBoundParameters.ContainsKey("ClientIds")){ $data.clientid = $ClientIds $data.clientidspecified = $true $hasModifications = $true } elseif ($PSBoundParameters.ContainsKey("AllClients")){ $data.clientid = $null $data.clientidspecified = $true $hasModifications = $true } if (!$hasModifications){ throw "Parameters do not result in any modification" } try{ $response = Invoke-RestMethod "$apiUrl/security/users/$UserId" -Method Put -Body (ConvertTo-Json $Data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiUserSettings]::new($response.data) } catch{ Write-SttiLog $_ -Level Error } } } function Get-SttiUserCredential{ [OutputType([SttiCredential])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $UserId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/users/$UserId" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiCredential]::FromArray($response.data.credentials) } catch{ Write-SttiLog $_ -Level Error } } } # Obsolete. Replaced by Add-SttiUserCredential function Add-SttiUserCertificateCredential{ [OutputType([SttiCredential])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $UserId, [Parameter(Mandatory)] [string] $UserCertificateThumbprint, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug Write-Warning "Add-SttiUserCertificateCredential is deprecated. Add-SttiUserCredential should be used instead." } Process{ Add-SttiUserCredential -Instance:$Instance -UserId:$UserId -CertificateThumbprint:$CertificateThumbprint -CertificateThumbprintCredential:$UserCertificateThumbprint } } function Add-SttiUserCredential{ [OutputType([SttiCredential])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $UserId, [Parameter(Mandatory, ParameterSetName='CertificateCredentials')] [string] $CertificateThumbprintCredential, [Parameter(Mandatory, ParameterSetName='UsernamePasswordCredentials')] [pscredential] $UsernamePasswordCredential, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ $Data = @{ } if ($PSBoundParameters.ContainsKey("CertificateThumbprintCredential")){ $Data.certificateCredential = @{ thumbprint = $CertificateThumbprintCredential description = "Set on $(Get-Date -Format "s")" } } if ($PSBoundParameters.ContainsKey("UsernamePasswordCredential")){ $Data.credential = @{ type = 'SttiPasswordCredential' username = $UsernamePasswordCredential.UserName password = ($UsernamePasswordCredential.Password | ConvertFrom-SecureString -AsPlainText) } } try{ $response = Invoke-RestMethod "$apiUrl/security/users/$UserId/credentials" -Method Post -Body (ConvertTo-Json $Data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiCredential]::FromArray(($response.data.credentials | Select-Object -Last 1)) } catch{ Write-SttiLog $_ -Level Error } } } function Remove-SttiUserCredential{ [OutputType([SttiCredential])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $UserId, [Parameter(Mandatory)] [string] $CredentialId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/users/$UserId/credentials/$CredentialId" -Method Delete -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiCredential]::FromArray($response.data.credentials) } catch{ Write-SttiLog $_ -Level Error } } } function Enable-SttiUser{ [OutputType([SttiUserSettings])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $UserId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/users/$UserId/activate" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiUserSettings]::new($response.data) } catch{ Write-SttiLog $_ -Level Error } } } function Disable-SttiUser{ [OutputType([SttiUserSettings])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $UserId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/users/$UserId/deactivate" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiUserSettings]::new($response.data) } catch{ Write-SttiLog $_ -Level Error } } } #endregion Users #region Secrets function New-SttiAsymmetricKeySecret{ [OutputType([SttiSecret])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [string] $PrivateKey, [Parameter()] [string] $Certificate, [Parameter(Mandatory)] [PSCustomObject] $RelatedTo, [Parameter(Mandatory)] [datetime] $ValidThru, [Parameter()] [string] $Algorithm, [Parameter()] [int] $KeySize, [Parameter(Mandatory)] [SttiSecretStore] $SecretStore, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance $secret = @{} $secret.type = "AsymmetricKeySecret" $secret.privateKey = $PrivateKey if ($PSBoundParameters.ContainsKey("Certificate")){ $secret.certificate = $Certificate } if ($PSBoundParameters.ContainsKey("Algorithm")){ $secret.algorithm = $Algorithm } if ($PSBoundParameters.ContainsKey("KeySize")){ $secret.keySize = $KeySize } if ($PSBoundParameters.ContainsKey("ValidThru")){ $secret.validThrough = $ValidThru } if ($PSBoundParameters.ContainsKey("RelatedTo")){ $secret.relatedTo = $RelatedTo } $data = @{ secret = $secret } try{ $response = Invoke-RestMethod "$apiUrl/security/secrets/$SecretStore" -Method Post -Body (ConvertTo-Json $data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiSecret]::new($response.data, $SecretStore) } catch{ Write-SttiLog $_ -Level Error } } function Remove-SttiSecret{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string] $SecretId, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [SttiSecretStore] $SecretStore, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/secrets/$SecretStore/$SecretId" -Method Delete -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiCredential]::FromArray($response.data.credentials) } catch{ Write-SttiLog $_ -Level Error } } } function Get-SttiSecret{ [OutputType([SttiSecret])] [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [SttiSecretStore] $SecretStore, [Parameter()] [string] $SecretId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/security/secrets/$SecretStore/$($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('SecretId') ? "$SecretId" : '')" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return [SttiSecret]::FromArray($response.data, $SecretStore) } catch{ Write-SttiLog $_ -Level Error } } } #endregion Secrets #region Diagnostics function Get-SttiLogEntry{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [datetime] $From, [datetime] $To, [SttiAppLogLevel] $MinimumLevel, [string] $ClientId, [string] $FlowId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance $filter = [System.Collections.Generic.List[string]]::new() if ($PSBoundParameters.ContainsKey("MinimumLevel")){ $filter.Add("MinimumLogLevel=$MinimumLevel") } if ($PSBoundParameters.ContainsKey("ClientId")){ $filter.Add("ClientId=$ClientId") } if ($PSBoundParameters.ContainsKey("FlowId")){ $filter.Add("FlowId=$FlowId") } if ($PSBoundParameters.ContainsKey("From")){ $filter.Add("From=" + [System.Uri]::EscapeDataString("{0:o}" -f $From)) } if ($PSBoundParameters.ContainsKey("To")){ $filter.Add("To=" + [System.Uri]::EscapeDataString("{0:o}" -f $To)) } if ($filter.Count -gt 0){ $params = [string]::Join('&', $filter) } try{ $response = Invoke-RestMethod "$apiUrl/diag/logs$($params ? '?' + $params : '')" -Body (ConvertTo-Json $data) -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return $response.entries } catch{ Write-SttiLog $_ -Level Error } } #endregion Diagnostics #region FlowData function Get-SttiFlow{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance try{ $response = Invoke-RestMethod "$apiUrl/stti/flows" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return $response.Data.items } catch{ Write-SttiLog $_ -Level Error } } function Get-SttiFlowRequestMessage{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [string] $FlowId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/stti/flows/$FlowId/request/message" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return $response.SttiApiResponseOfFlowRequestMessageInfo.Data.Message.OuterXml } catch{ Write-SttiLog $_ -Level Error } } } function Get-SttiFlowTrace{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [string] $FlowId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/stti/flows/$FlowId/trace" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return $response.data.entries } catch{ Write-SttiLog $_ -Level Error } } } function Get-SttiFlowResponses{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [string] $FlowId, [switch] $IncludeContent, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/stti/flows/$FlowId/responses" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop $flowResponses = $response.data if ($IncludeContent){ $flowResponses | ForEach-Object { $responseId = $_.id $contentResponse = Invoke-RestMethod "$apiUrl/stti/flows/$FlowId/responses/$responseId" -Headers @{ "Accept"="application/json" } -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return $contentResponse } } else{ return $flowResponses } } catch{ Write-SttiLog $_ -Level Error } } } function Get-SttiFlowCurrentResponse{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [string] $FlowId, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $response = Invoke-RestMethod "$apiUrl/stti/flows/$FlowId/responses/current/message" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return $response.output.InnerXml } catch{ Write-SttiLog $_ -Level Error } } } # Anstelle von Stop wollte ich Cancel aber das ist laut Microsoft kein erlaubtes Verb und dadurch meckert er an jeder möglichen Stelle.... function Stop-SttiFlows{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [string] $CertificateThumbprint, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [string[]] $FlowIds ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $responses = @() foreach ($FlowId in $FlowIds) { $route = "$apiUrl/stti/flows/$FlowId/request/cancel" $response = Invoke-RestMethod $route -Method Put -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop $responses += $response.Data } return $responses } catch{ Write-SttiLog $_ -Level Error } } } #endregion FlowData #region ExternalData function Remove-SttiExternalData{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [string] $DataType, [Parameter(Mandatory)] [string] $FieldName, [Parameter(Mandatory)] [string[]] $FieldValues, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ try{ $route = "$apiUrl/database/externaldata/$DataType/$FieldName" if ($FieldValues.Count -eq 1) { $route = $route + "/$($FieldValues[0])" } else { $fieldValuesString = [string]::Join(",", $FieldValues) $route = $route + "?fieldvalues=$fieldValuesString" } $response = Invoke-RestMethod $route -Method Delete -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return $response.Data } catch{ Write-SttiLog $_ -Level Error } } } function Remove-SttiChEstvID{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [string[]] $EstvIDs, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug } Process{ try{ return Remove-SttiExternalData -Instance $Instance -DataType CH_ExternalId -FieldName ExternalValue -FieldValues $EstvIDs -CertificateThumbprint $CertificateThumbprint } catch{ Write-SttiLog $_ -Level Error } } } function Remove-SttiNlBoData{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory)] [string[]] $DataKeys, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug } Process{ try{ return Remove-SttiExternalData -Instance $Instance -DataType NL_BO_Data -FieldName DataKey -FieldValues $DataKeys -CertificateThumbprint $CertificateThumbprint } catch{ Write-SttiLog $_ -Level Error } } } #endregion ExternalData #region Testing function Invoke-SttiMockFlowStep{ [CmdletBinding()] param ( [Parameter(Mandatory)] [SttiInstanceConfig] $Instance, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [string] $FlowId, [Parameter(Mandatory)] [bool] $Success, [Parameter()] [string] $InfoMessage, [Parameter(Mandatory)] [string] $CertificateThumbprint ) Begin{ Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand)" -Level Verbose Write-SttiLog "$($PSCmdlet.MyInvocation.MyCommand) was called with parameters $($PSCmdlet.MyInvocation.BoundParameters | ConvertTo-Json -Compress)" -Level Debug $apiUrl = Get-SttiApiUrl -Instance $Instance } Process{ $paramValues = [System.Collections.Generic.List[string]]::new() if ($PSBoundParameters.ContainsKey("Success")){ $paramValues.Add("success=$Success") } if ($PSBoundParameters.ContainsKey("InfoMessage")){ $paramValues.Add("infoMessage=$InfoMessage") } if ($paramValues.Count -gt 0){ $params = [string]::Join('&', $paramValues) } try{ $result = Invoke-RestMethod "$apiUrl/stti/flows/$FlowId/mockStep$($params ? '?' + $params : '')" -Method Put -ContentType "application/json" -CertificateThumbprint $CertificateThumbprint -ErrorAction Stop return $result } catch{ Write-SttiLog $_ -Level Error } } } #endregion Testing # Test required Powershell version Test-SttiPsVersion # Test consistency of powershell modules Test-SttiModuleConsistency # Ensure Stti BasePath if (!$env:SttiBasePath){ Set-SttiBasePath $defaultBasePath } # Ensure Shell Session $shellSession = [SttiShellSession]::new() $shellSession.ApiCallTimeoutSec = 60 # Restore default parameter Values from user defaults Restore-SttiDefaultParameterValues # Set Parameters dependent on the sqlserver module version Set-SttiSqlVersionSpecificParameters |