diff --git a/PS-Tools/PS-Tools.psd1 b/PS-Tools/PS-Tools.psd1 index 0cad2df..9e8623b 100644 --- a/PS-Tools/PS-Tools.psd1 +++ b/PS-Tools/PS-Tools.psd1 @@ -12,7 +12,7 @@ RootModule = 'PS-Tools.psm1' # Version number of this module. -ModuleVersion = '1.0.1' +ModuleVersion = '1.1.0' # Supported PSEditions # CompatiblePSEditions = @() @@ -92,14 +92,17 @@ FunctionsToExport = @( 'Copy-SnapshotToVHD', 'Get-TimeStamp', 'Set-ResourceGroupTags', - 'Write-InformationPlus' + 'Write-InformationPlus', + 'Set-PSToolsConfig' ) # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() # Variables to export from this module -VariablesToExport = @() +VariablesToExport = @( + 'PSToolsConfig' +) # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. AliasesToExport = @() diff --git a/PS-Tools/PS-Tools.psm1 b/PS-Tools/PS-Tools.psm1 index 73f8042..8d8ae7d 100644 --- a/PS-Tools/PS-Tools.psm1 +++ b/PS-Tools/PS-Tools.psm1 @@ -16,9 +16,16 @@ foreach ($Function in $Functions) { } } +try { + $PSToolsConfig = Get-Content -Path 'C:\ProgramData\PS-Tools\config.json' -ErrorAction 'Stop' | ConvertFrom-Json + Write-Verbose "Configuration loaded from C:\ProgramDate\PS-Tools\config.json" +} +catch { + Write-Warning "PS-Tools configuration file not found." + Write-Warning "Run Set-PSToolsConfig to create one." +} # Export only the functions using PowerShell standard verb-noun naming. # Be sure to list each exported functions in the FunctionsToExport field of the module manifest file. # This improves performance of command discovery in PowerShell. -Export-ModuleMember -Function *-* - +Export-ModuleMember -Function $Functions.BaseName -Variable 'PSToolsConfig' \ No newline at end of file diff --git a/PS-Tools/PSToolsConfigReqs.ps1 b/PS-Tools/PSToolsConfigReqs.ps1 new file mode 100644 index 0000000..15c3401 --- /dev/null +++ b/PS-Tools/PSToolsConfigReqs.ps1 @@ -0,0 +1,20 @@ +$ReferenceSettings = @{ + 'New-SA' = @( + 'SandboxServiceAccountOU', + 'SandboxServiceAccountGroup', + 'DevServiceAccountOU', + 'DevServiceAccountGroup', + 'TestServiceAccountOU', + 'TestServiceAccountGroup', + 'TrainingServiceAccountOU', + 'TrainingServiceAccountGroup', + 'UATServiceAccountOU', + 'UATServiceAccountGroup', + 'ProdServiceAccountOU', + 'ProdServiceAccountGroup', + 'NoMFAGroup' + ) + 'New-User' = @( + 'UserOU' + ) +} \ No newline at end of file diff --git a/PS-Tools/Private/Test-PSToolsConfig.ps1 b/PS-Tools/Private/Test-PSToolsConfig.ps1 new file mode 100644 index 0000000..2aea8ec --- /dev/null +++ b/PS-Tools/Private/Test-PSToolsConfig.ps1 @@ -0,0 +1,44 @@ +function Test-PSToolsConfig { + [CmdletBinding()] + + param ( + [Parameter(Mandatory)] + [string] + $Domain + + ) + + # Get the name of the calling function + # [0] is the current function + # [1] is the calling function + $FunctionName = (Get-PSCallStack)[1].Command + Write-Verbose "Testing for function $FunctionName" + + # Check to see if the PSToolsConfig variable exists + if ($null -eq $PSToolsConfig) { + Write-Error 'PSToolsConfig has not been set, run Set-PSToolsConfig' + break + } + + # use the function name to check for required values. + if ($FunctionName -eq 'New-SA') { + # Load Reference Settings + $PSToolsConfigReqsPath = "$((Get-Item $PSScriptRoot).Parent)\PSToolsConfigReqs.ps1" + Write-Verbose "Loading config from $PSToolsConfigReqsPath" + . $PSToolsConfigReqsPath + + $MissingSetting = $false + foreach ($Setting in $ReferenceSettings."$FunctionName") { + Write-Verbose "Checking for $Setting" + if ($null -eq $PSToolsConfig."$Domain".$Setting) { + Write-Warning "Missing setting $Setting for $Domain" + $MissingSetting = $true + } + } + if ($MissingSetting) { + Write-Error "Missing settings required for New-SA, run Set-PSToolsConfig -Domain $Domain -Function $FunctionName" + } + + } + +} \ No newline at end of file diff --git a/PS-Tools/Public/New-SA.ps1 b/PS-Tools/Public/New-SA.ps1 index 9302b45..b0407b1 100644 --- a/PS-Tools/Public/New-SA.ps1 +++ b/PS-Tools/Public/New-SA.ps1 @@ -1,302 +1,299 @@ function New-SA { - [CmdletBinding( - DefaultParameterSetName = 'Prompt', - SupportsShouldProcess = $True - )] - - param ( - - [Parameter( - ParameterSetName = 'CmdLine', - Mandatory = $true + [CmdletBinding( + DefaultParameterSetName = 'Prompt', + SupportsShouldProcess = $True )] - [ValidatePattern("^SA-(SBX|DEV|TST|TRN|UAT|PRD)([A-Z]{3}|SQL[0-9]{2})-\w{2,10}$")] - [String] $UserName, - - [Parameter( - ParameterSetName = 'CmdLine', - Mandatory = $true - )] - [String] $Description, - - [Parameter( - ParameterSetName = 'CmdLine', - Mandatory = $true - )] - [ValidateSet("PRD","UAT","TRN","TST","DEV","SBX")] - [String] $Environment, - - [Parameter( - ParameterSetName = 'CmdLine' - )] - [Switch] $AzureSync, - - [Parameter( - ParameterSetName = 'Prompt' - )] - [String] $Prompt - - ) - - begin { - $ErrorActionPreference = 'Stop' - - if ($PSBoundParameters['Debug']) { - $DebugPreference = 'Continue' - } - - # Set the domain controller - try { - # The [0] gets the first DC returned and makes it a string vs an AD object - $DomainController = (Get-ADDomainController -Service PrimaryDC -Discover).HostName[0] - if ([string]::IsNullOrEmpty($DomainController)) { - throw 'No Domain Controller found!!' - } - Write-Verbose "Using domain controller $DomainController" - } - catch { - Format-Error -e $_ - } - - # Array to hold accounts to be created - $Accounts = @() - - } - - process { - function Initialize-AccountDetails { - param ( - [string] $Env - ) - - if ($AzureSync) { - $BaseOU = 'OU=CloudSync,DC=Domain,DC=com' - } - else { - $BaseOU = 'OU=Service Accounts,DC=Domain,DC=com' - } - - switch ($Env) { - {'P','PRD' -contains $_} { - $Script:UserOU = "OU=Production,$BaseOU" - $Script:AcctEnv = "PRD" - $Script:SAGroup = "Prod-ServiceAccounts" - } - {'U','UAT' -contains $_} { - $Script:UserOU = "OU=UAT,$BaseOU" - $Script:AcctEnv = "UAT" - $Script:SAGroup = "NonProd-ServiceAccounts" - } - {'T','TRN' -contains $_} { - $Script:UserOU = "OU=Training,$BaseOU" - $Script:AcctEnv = "TRN" - $Script:SAGroup = "NonProd-ServiceAccounts" - } - {'Q','TST' -contains $_} { - $Script:UserOU = "OU=QA-Test,$BaseOU" - $Script:AcctEnv = "TST" - $Script:SAGroup = "NonProd-ServiceAccounts" + param ( + [string] + $Domain = ((Get-ADDomain).DNSRoot), + + [Parameter( + ParameterSetName = 'CmdLine', + Mandatory = $true + )] + [ValidatePattern("^SA-(SBX|DEV|TST|TRN|UAT|PRD)([A-Z]{3}|SQL[0-9]{2})-\w{2,10}$")] + [String] $UserName, + + [Parameter( + ParameterSetName = 'CmdLine', + Mandatory = $true + )] + [String] $Description, + + [Parameter( + ParameterSetName = 'CmdLine', + Mandatory = $true + )] + [ValidateSet("PRD", "UAT", "TRN", "TST", "DEV", "SBX")] + [String] $Environment, + + [Parameter( + ParameterSetName = 'CmdLine' + )] + [Switch] $AzureSync, + + [Parameter( + ParameterSetName = 'Prompt' + )] + [String] $Prompt + + ) + + begin { + $ErrorActionPreference = 'Stop' + + # Check for PSToolsConfig variable required settings + Test-PSToolsConfig -Domain $Domain + + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' } - {'D','DEV' -contains $_} { - $Script:UserOU = "OU=Development,$BaseOU" - $Script:AcctEnv = "DEV" - $Script:SAGroup = "NonProd-ServiceAccounts" + + # Set the domain controller + try { + # The [0] gets the first DC returned and makes it a string vs an AD object + $DomainController = (Get-ADDomainController -Service PrimaryDC -Discover).HostName[0] + if ([string]::IsNullOrEmpty($DomainController)) { + throw 'No Domain Controller found!!' + } + Write-Verbose "Using domain controller $DomainController" } - {'S','SBX' -contains $_} { - $Script:UserOU = "OU=Sandbox,$BaseOU" - $Script:AcctEnv = "SBX" - $Script:SAGroup = "NonProd-ServiceAccounts" + catch { + Format-Error -e $_ } - } # End Switch - } # End Function Initialize-AccountDetails - if ($PSCmdlet.ParameterSetName -eq 'Prompt') { - Clear-Host - # Walk user through account questions - # Clear Variables - Clear-Variable Description,UserOU,AcctEnv,SAGroup -ErrorAction SilentlyContinue + # Array to hold accounts to be created + $Accounts = @() + + } - # Select SA Environment - Write-InformationPlus "Prod = P UAT = U Training = T Test\QA = Q Develoment = D Sandbox = S" - $EnvResponse = Get-Input -Prompt "What environment is this account for:" -ValidResponses @('P','U','T','Q','D','S') + process { + function Initialize-AccountDetails { + param ( + [string] $Env + ) + + switch ($Env) { + { 'P', 'PRD' -contains $_ } { + $Script:UserOU = $PSToolsConfig."$Domain".ProdServiceAccountOU + $Script:AcctEnv = "PRD" + $Script:SAGroup = $PSToolsConfig."$Domain".ProdServiceAccountGroup + } + { 'U', 'UAT' -contains $_ } { + $Script:UserOU = $PSToolsConfig."$Domain".UATServiceAccountOU + $Script:AcctEnv = "UAT" + $Script:SAGroup = $PSToolsConfig."$Domain".UATServiceAccountGroup + } + { 'T', 'TRN' -contains $_ } { + $Script:UserOU = $PSToolsConfig."$Domain".TrainingServiceAccountOU + $Script:AcctEnv = "TRN" + $Script:SAGroup = $PSToolsConfig."$Domain".TrainingServiceAccountGroup + } + { 'Q', 'TST' -contains $_ } { + $Script:UserOU = $PSToolsConfig."$Domain".TestServiceAccountOU + $Script:AcctEnv = "TST" + $Script:SAGroup = $PSToolsConfig."$Domain".TestServiceAccountGroup + } + { 'D', 'DEV' -contains $_ } { + $Script:UserOU = $PSToolsConfig."$Domain".DevServiceAccountOU + $Script:AcctEnv = "DEV" + $Script:SAGroup = $PSToolsConfig."$Domain".DevServiceAccountGroup + } + { 'S', 'SBX' -contains $_ } { + $Script:UserOU = $PSToolsConfig."$Domain".SandboxServiceAccountOU + $Script:AcctEnv = "SBX" + $Script:SAGroup = $PSToolsConfig."$Domain".SandboxServiceAccountGroup + } + } # End Switch + + } # End Function Initialize-AccountDetails + + if ($PSCmdlet.ParameterSetName -eq 'Prompt') { + Clear-Host + # Walk user through account questions + # Clear Variables + Clear-Variable Description, UserOU, AcctEnv, SAGroup -ErrorAction SilentlyContinue + + # Select SA Environment + Write-InformationPlus "Prod = P UAT = U Training = T Test\QA = Q Develoment = D Sandbox = S" + $EnvResponse = Get-Input -Prompt "What environment is this account for:" -ValidResponses @('P', 'U', 'T', 'Q', 'D', 'S') - Initialize-AccountDetails -Env $EnvResponse - - # IsSQL Account - $SQLResponse = Get-Input -Prompt 'Is the a SQL Server service account?' -Default 'N' -ValidResponses @('Y','N') - - if ($SQLResponse -eq 'Y') { - - $Instance = Get-Input -Prompt 'Enter the SQL Instance Number' -ValidResponses (1..99) - # Convert instance to an int and format as a two character string - $Instance = "{0:D2}" -f [int]$Instance - - Write-InformationPlus "`nChoose one or more of the following SQL account types:" - Write-InformationPlus "A - SQL Server Database Engine" - Write-InformationPlus "B - SQL Server Agent" - Write-InformationPlus "C - SQL Server Reporting Services" - Write-InformationPlus "D - SQL Server Analysis Services" - Write-InformationPlus "E - SQL Server Integration Services`n" - $SQLAcctTypes = Get-Input -Prompt 'Choose one or more of the SQL account types' -ValidResponses ('A','B','C','D','E') -MultipleChoice - - Write-InformationPlus "`n`n" - $CSResponse = Get-Input -Prompt "Will the account(s) be synchronized with Azure" -ValidResponses ('true','false') -Default 'false' - - $AzureSync = [System.Convert]::ToBoolean($CSResponse) - - foreach ($Selection in $SQLAcctTypes) { - Switch ($Selection) { - 'A' { - $SQLType = "SQLSrvr" - $Description = "SQL Server service account for SQL Instance $Instance" - } - 'B' { - $SQLType = "SQLAgent" - $Description = "SQL Agent service account for SQL Instance $Instance" - } - 'C' { - $SQLType = "SSRS" - $Description = "SQL Service Reporting Services service account for SQL Instance $Instance" - } - 'D' { - $SQLType = "SSAS" - $Description = "SQL Server Analysis Services service account for SQL Instance $Instance" - } - 'E' { - $SQLType = "SSIS" - $Description = "SQL Server Integration Services service account for SQL Instance $Instance" - } - } - - $UserName = "SA-$($Script:AcctEnv)SQL$Instance-$SQLType" - - $UserProperties = @{ - UserName = $UserName - Description = $Description - UserOU = $Script:UserOU - SAGroup = $Script:SAGroup - AzureSync = $AzureSync - } - $Accounts += New-Object PSObject -Property $UserProperties - } # End foreach SQLAcctTypes - - } # End SQL Section - else { - Write-InformationPlus "Please use the Service Account Naming Standard:" - Write-InformationPlus "SA--`n" - Write-InformationPlus "Examples:" - Write-InformationPlus "SA-DEVAPP-WFEAppPool" - Write-InformationPlus "SA-PRDAPP-Monitoring`n" - Write-InformationPlus "The should be relatable and between 2-10 characters" - Write-InformationPlus "You may not exceed 20 total characters`n" - $UserName = Get-Input -Prompt "Enter a username for the service account" -Match "^SA-(SBX|DEV|TST|TRN|UAT|PRD)([A-Z]{3})-\w{2,10}$" -MatchHint "Examples: SA-DEVAPP-AppPool, SA-PRDAPP-AzDevOps" - - Write-InformationPlus "`n`n" - Write-InformationPlus "A description is required for all service acoounts." - Write-InformationPlus "Please use the following format:" - Write-InformationPlus " - - `n" - Write-InformationPlus "Examples:" - Write-InformationPlus "ServerName - Scheduled Task - Runs FTP job" - Write-InformationPlus "ServerName - IIS App Pool - Runs the example.com site" - $Description = Get-Input -Prompt "Enter a description for the account" -Required - - Write-InformationPlus "`n`n" - $CSResponse = Get-Input -Prompt "Will this account be synchronized with Azure" -ValidResponses ('true','false') -Default 'false' - - $AzureSync = [System.Convert]::ToBoolean($CSResponse) - - $UserProperties = @{ - UserName = $UserName - Description = $Description - UserOU = $Script:UserOU - SAGroup = $Script:SAGroup - AzureSync = $AzureSync - } - $Accounts += New-Object PSObject -Property $UserProperties - } # End Non-SQL Section - - } # End if Prompt - Handles Prompted Accounts - else { - # Handles Command Line Account - Initialize-AccountDetails -Env $Environment - - $UserProperties = @{ - UserName = $UserName - Description =$Description - UserOU = $Script:UserOU - SAGroup = $Script:SAGroup - AzureSync = $AzureSync - } - $Accounts += New-Object PSObject -Property $UserProperties - } # End Else - Handles Command Line Account - - # Data validation - Write-InformationPlus "`nThe following account will be created:" - Write-Output ($Accounts | Format-Table UserName, Description, AzureSync -AutoSize) - - $null = Get-Input -Prompt 'Enter YES to continue or press Ctrl-C to exit' -ValidResponses 'Yes' - - # Create Accounts - foreach ($Account in $Accounts) { - Write-Verbose "Username:$($Account.UserName)" - Write-Verbose "Path :$($Account.UserOU)" - Write-Verbose "SAGroup :$($Account.SAGroup)" - $Password = New-Password - $UserProperties = @{ - Name = $Account.UserName - GivenName = 'SA' - # The Surname is set to the rest of the Username after 'SA-' - Surname = $Account.UserName.Split('-',2)[1] - Description =$Account.Description - SamAccountName = $Account.UserName - UserPrincipalName = "$($Account.UserName)@example.com" - EmailAddress = "$($Account.UserName)@example.com" - Path = $Account.UserOU - AccountPassword = (ConvertTo-SecureString -String $Password -AsPlainText -Force) - Enable = $true - PasswordNeverExpires = $true - CannotChangePassword = $true - Server = $DomainController - } - try { - New-ADUser @UserProperties - # Set Groups - if ($PSCmdlet.ShouldProcess($Account.UserName, "Add to service account groups")) { - Add-ADGroupMember -Server $DomainController -Identity 'BatchServiceLogon' -Members $Account.UserName - Add-ADGroupMember -Server $DomainController -Identity $Account.SAGroup -Members $Account.UserName - if ($Account.AzureSync) { - Add-ADGroupMember -Server $DomainController -Identity 'NoMFA' -Members $Account.UserName - } - } - # Wait for group membership confirmation - $Script:SAGroupObj = Get-ADGroup -Server $DomainController -Identity $Account.SAGroup -Properties @("primaryGroupToken") - # Get-ADUser doesn't have a -whatif parameter - if ($PSCmdlet.ShouldProcess($Account.UserName, "Retrieve user and replace primary group token")) { - Get-ADUser -Server $DomainController -Identity $Account.UserName | Set-ADUser -Server $DomainController -Replace @{primaryGroupID=$Script:SAGroupObj.primaryGroupToken} - } - # Wait for the primary group to change - if ($PSCmdlet.ShouldProcess($Account.UserName, "Remove account from Domain Users group")) { - Remove-ADGroupMember -Server $DomainController -Identity 'Domain Users' -Members $Account.UserName -Confirm:$false - } - # Output results - Write-InformationPlus "Service Account $UserName created, the password is $Password" - if ($Account.AzureSync) { - Write-InformationPlus "IMPORTANT REQUIREMENT FOR AZURE SYNCED ACCOUNTS:" - Write-InformationPlus "1. You must login as the account once to finish the Okta setup. Please record the security question in the Secret Server notes." - Write-InformationPlus "2. The NoMFA option only works on the internal network. Connections coming from the internet (Azure) will need special consideration." - } - } - catch { - Format-Error -e $_ -Message "Failed to create user $($Account.UserName), aborting process!" - } - } # End Account Creation - foreach Account + Initialize-AccountDetails -Env $EnvResponse + + # IsSQL Account + $SQLResponse = Get-Input -Prompt 'Is the a SQL Server service account?' -Default 'N' -ValidResponses @('Y', 'N') + + if ($SQLResponse -eq 'Y') { + + $Instance = Get-Input -Prompt 'Enter the SQL Instance Number' -ValidResponses (1..99) + # Convert instance to an int and format as a two character string + $Instance = "{0:D2}" -f [int]$Instance + + Write-InformationPlus "`nChoose one or more of the following SQL account types:" + Write-InformationPlus "A - SQL Server Database Engine" + Write-InformationPlus "B - SQL Server Agent" + Write-InformationPlus "C - SQL Server Reporting Services" + Write-InformationPlus "D - SQL Server Analysis Services" + Write-InformationPlus "E - SQL Server Integration Services`n" + $SQLAcctTypes = Get-Input -Prompt 'Choose one or more of the SQL account types' -ValidResponses ('A', 'B', 'C', 'D', 'E') -MultipleChoice + + Write-InformationPlus "`n`n" + $CSResponse = Get-Input -Prompt "Will the account(s) be synchronized with Azure" -ValidResponses ('true', 'false') -Default 'false' + + $AzureSync = [System.Convert]::ToBoolean($CSResponse) + + foreach ($Selection in $SQLAcctTypes) { + Switch ($Selection) { + 'A' { + $SQLType = "SQLSrvr" + $Description = "SQL Server service account for SQL Instance $Instance" + } + 'B' { + $SQLType = "SQLAgent" + $Description = "SQL Agent service account for SQL Instance $Instance" + } + 'C' { + $SQLType = "SSRS" + $Description = "SQL Service Reporting Services service account for SQL Instance $Instance" + } + 'D' { + $SQLType = "SSAS" + $Description = "SQL Server Analysis Services service account for SQL Instance $Instance" + } + 'E' { + $SQLType = "SSIS" + $Description = "SQL Server Integration Services service account for SQL Instance $Instance" + } + } + + $UserName = "SA-$($Script:AcctEnv)SQL$Instance-$SQLType" + + $UserProperties = @{ + UserName = $UserName + Description = $Description + UserOU = $Script:UserOU + SAGroup = $Script:SAGroup + AzureSync = $AzureSync + } + $Accounts += New-Object PSObject -Property $UserProperties + } # End foreach SQLAcctTypes + + } # End SQL Section + else { + Write-InformationPlus "Please use the Service Account Naming Standard:" + Write-InformationPlus "SA--`n" + Write-InformationPlus "Examples:" + Write-InformationPlus "SA-DEVAPP-WFEAppPool" + Write-InformationPlus "SA-PRDAPP-Monitoring`n" + Write-InformationPlus "The should be relatable and between 2-10 characters" + Write-InformationPlus "You may not exceed 20 total characters`n" + $UserName = Get-Input -Prompt "Enter a username for the service account" -Match "^SA-(SBX|DEV|TST|TRN|UAT|PRD)([A-Z]{3})-\w{2,10}$" -MatchHint "Examples: SA-DEVAPP-AppPool, SA-PRDAPP-AzDevOps" + + Write-InformationPlus "`n`n" + Write-InformationPlus "A description is required for all service acoounts." + Write-InformationPlus "Please use the following format:" + Write-InformationPlus " - - `n" + Write-InformationPlus "Examples:" + Write-InformationPlus "ServerName - Scheduled Task - Runs FTP job" + Write-InformationPlus "ServerName - IIS App Pool - Runs the example.com site" + $Description = Get-Input -Prompt "Enter a description for the account" -Required + + Write-InformationPlus "`n`n" + $CSResponse = Get-Input -Prompt "Will this account be synchronized with Azure" -ValidResponses ('true', 'false') -Default 'false' + + $AzureSync = [System.Convert]::ToBoolean($CSResponse) + + $UserProperties = @{ + UserName = $UserName + Description = $Description + UserOU = $Script:UserOU + SAGroup = $Script:SAGroup + AzureSync = $AzureSync + } + $Accounts += New-Object PSObject -Property $UserProperties + } # End Non-SQL Section + + } # End if Prompt - Handles Prompted Accounts + else { + # Handles Command Line Account + Initialize-AccountDetails -Env $Environment + + $UserProperties = @{ + UserName = $UserName + Description = $Description + UserOU = $Script:UserOU + SAGroup = $Script:SAGroup + AzureSync = $AzureSync + } + $Accounts += New-Object PSObject -Property $UserProperties + } # End Else - Handles Command Line Account + + # Data validation + Write-InformationPlus "`nThe following account will be created:" + Write-Output ($Accounts | Format-Table UserName, Description, AzureSync -AutoSize) + + $null = Get-Input -Prompt 'Enter YES to continue or press Ctrl-C to exit' -ValidResponses 'Yes' + + # Create Accounts + foreach ($Account in $Accounts) { + Write-Verbose "Username:$($Account.UserName)" + Write-Verbose "Path :$($Account.UserOU)" + Write-Verbose "SAGroup :$($Account.SAGroup)" + $Password = New-Password + $UserProperties = @{ + Name = $Account.UserName + GivenName = 'SA' + # The Surname is set to the rest of the Username after 'SA-' + Surname = $Account.UserName.Split('-', 2)[1] + Description = $Account.Description + SamAccountName = $Account.UserName + UserPrincipalName = "$($Account.UserName)@$Domain" + EmailAddress = "$($Account.UserName)@$Domain" + Path = $Account.UserOU + AccountPassword = (ConvertTo-SecureString -String $Password -AsPlainText -Force) + Enable = $true + PasswordNeverExpires = $true + CannotChangePassword = $true + Server = $DomainController + } + try { + New-ADUser @UserProperties + # Set Groups + if ($PSCmdlet.ShouldProcess($Account.UserName, "Add to service account groups")) { + Add-ADGroupMember -Server $DomainController -Identity $Account.SAGroup -Members $Account.UserName + if ($Account.AzureSync) { + Add-ADGroupMember -Server $DomainController -Identity $PSToolsConfig."$Domain".NoMFAGroup -Members $Account.UserName + } + } + # Wait for group membership confirmation + $Script:SAGroupObj = Get-ADGroup -Server $DomainController -Identity $Account.SAGroup -Properties @("primaryGroupToken") + # Get-ADUser doesn't have a -whatif parameter + if ($PSCmdlet.ShouldProcess($Account.UserName, "Retrieve user and replace primary group token")) { + Get-ADUser -Server $DomainController -Identity $Account.UserName | Set-ADUser -Server $DomainController -Replace @{primaryGroupID = $Script:SAGroupObj.primaryGroupToken } + } + # Wait for the primary group to change + if ($PSCmdlet.ShouldProcess($Account.UserName, "Remove account from Domain Users group")) { + Remove-ADGroupMember -Server $DomainController -Identity 'Domain Users' -Members $Account.UserName -Confirm:$false + } + # Output results + Write-InformationPlus "Service Account $UserName created, the password is $Password" + if ($Account.AzureSync) { + Write-InformationPlus "IMPORTANT REQUIREMENT FOR AZURE SYNCED ACCOUNTS:" + Write-InformationPlus "1. You must login as the account once to finish the Okta setup. Please record the security question in the Secret Server notes." + Write-InformationPlus "2. The NoMFA option only works on the internal network. Connections coming from the internet (Azure) will need special consideration." + } + } + catch { + Format-Error -e $_ -Message "Failed to create user $($Account.UserName), aborting process!" + } + } # End Account Creation - foreach Account - } # End Proccess + } # End Proccess - <# + <# .SYNOPSIS .PARAMETER diff --git a/PS-Tools/Public/New-User.ps1 b/PS-Tools/Public/New-User.ps1 index b657028..0207a60 100644 --- a/PS-Tools/Public/New-User.ps1 +++ b/PS-Tools/Public/New-User.ps1 @@ -117,6 +117,19 @@ function New-User { $InformationPreference = 'Ignore' } + # Set the domain controller + try { + # The [0] gets the first DC returned and makes it a string vs an AD object + $DomainController = (Get-ADDomainController -Service PrimaryDC -Discover).HostName[0] + if ([string]::IsNullOrEmpty($DomainController)) { + throw 'No Domain Controller found!!' + } + Write-Verbose "Using domain controller $DomainController" + } + catch { + Format-Error -e $_ + } + Write-Debug $PSCmdlet.ParameterSetName } @@ -366,6 +379,7 @@ function New-User { Path = 'ou=Users,dc=domain,dc=com' Enabled = $true ChangePasswordAtLogon = $true + Server = $DomainController } [array]$User.ADGroups += 'MFA', 'AADP2', 'SSPR' diff --git a/PS-Tools/Public/Set-AzAppGwConfig.ps1 b/PS-Tools/Public/Set-AzAppGwConfig.ps1 index 1fbd59c..144bdaa 100644 --- a/PS-Tools/Public/Set-AzAppGwConfig.ps1 +++ b/PS-Tools/Public/Set-AzAppGwConfig.ps1 @@ -53,6 +53,10 @@ function Set-AzAppGWConfig { [string]$AuthCertPath, + # This is used with the AppGw v2 and does not require a specific + # Cert for backend pool verification when using E2E TLS + [switch]$TrustedCA, + [string]$HttpListenerPort = "80", [string]$HttpsListenerPort = "443" @@ -70,8 +74,6 @@ function Set-AzAppGWConfig { process { - Write-InformationPlus '' - # Capitalize the first letter of the environment # This is for Az Dev Ops pipelines where the env_name var has to be lower case # Because it is used in Azure names that do not accept capitals @@ -221,26 +223,31 @@ function Set-AzAppGWConfig { Format-Error -e $_ -Message 'Failed to process the SSL certificate' } try { - Write-InformationPlus "Processing Auth Certificate..." -NoNewLine - $AuthCert = Get-AzApplicationGatewayAuthenticationCertificate -Name $AuthCertName -ApplicationGateway $AppGW -ErrorAction SilentlyContinue - if ($null -eq $AuthCert -and [boolean]$AuthCertPath) { - Write-InformationPlus "`n Adding Auth Certificate..." -NoNewLine - Add-AzApplicationGatewayAuthenticationCertificate -Name $AuthCertName -CertificateFile $AuthCertPath -ApplicationGateway $AppGW | Out-Null - $AuthCert = Get-AzApplicationGatewayAuthenticationCertificate -Name $AuthCertName -ApplicationGateway $AppGW - Write-InformationPlus "Done!" -ForegroundColor Green - } - elseif ([boolean]$AuthCert -and [boolean]$AuthCertPath) { - Write-InformationPlus "`n Updating Auth Certificate..." -NoNewLine - Set-AzApplicationGatewayAuthenticationCertificate -Name $AuthCertName -CertificateFile $AuthCertPath -ApplicationGateway $AppGW | Out-Null - $AuthCert = Get-AzApplicationGatewayAuthenticationCertificate -Name $AuthCertName -ApplicationGateway $AppGW - Write-InformationPlus "Done!" -ForegroundColor Green - } - elseif ($null -eq $AuthCert -and -not [boolean]$AuthCertPath) { - Write-InformationPlus "Error!" -ForegroundColor Red - throw "No Auth Certificate found and no path provided!" + if (-not $TrustedCA) { + Write-InformationPlus "Processing Auth Certificate..." -NoNewLine + $AuthCert = Get-AzApplicationGatewayAuthenticationCertificate -Name $AuthCertName -ApplicationGateway $AppGW -ErrorAction SilentlyContinue + if ($null -eq $AuthCert -and [boolean]$AuthCertPath) { + Write-InformationPlus "`n Adding Auth Certificate..." -NoNewLine + Add-AzApplicationGatewayAuthenticationCertificate -Name $AuthCertName -CertificateFile $AuthCertPath -ApplicationGateway $AppGW | Out-Null + $AuthCert = Get-AzApplicationGatewayAuthenticationCertificate -Name $AuthCertName -ApplicationGateway $AppGW + Write-InformationPlus "Done!" -ForegroundColor Green + } + elseif ([boolean]$AuthCert -and [boolean]$AuthCertPath) { + Write-InformationPlus "`n Updating Auth Certificate..." -NoNewLine + Set-AzApplicationGatewayAuthenticationCertificate -Name $AuthCertName -CertificateFile $AuthCertPath -ApplicationGateway $AppGW | Out-Null + $AuthCert = Get-AzApplicationGatewayAuthenticationCertificate -Name $AuthCertName -ApplicationGateway $AppGW + Write-InformationPlus "Done!" -ForegroundColor Green + } + elseif ($null -eq $AuthCert -and -not [boolean]$AuthCertPath) { + Write-InformationPlus "Error!" -ForegroundColor Red + throw "No Auth Certificate found and no path provided!" + } + else { + Write-InformationPlus "Done!" -ForegroundColor Green + } } else { - Write-InformationPlus "Done!" -ForegroundColor Green + Write-InformationPlus "Using Trusted Certificate Authority." } } catch { @@ -356,16 +363,29 @@ function Set-AzAppGWConfig { } } if ($HTTPS) { + $BEHttpProperties = @{ + Name = $BEHttpsCfgName + Protocol = 'Https' + Port = $BEHttpsCfgPort + CookieBasedAffinity = $CookieBasedAffinity + AffinityCookieName = $AffinityCookieName + RequestTimeout = 240 + Probe = $HttpsProbe + ApplicationGateway = $AppGW + } + if (-not $TrustedCA) { + AuthenticationCertificates = $AuthCert + } $BEHttpsCfg = Get-AzApplicationGatewayBackendHttpSetting -Name $BEHttpsCfgName -ApplicationGateway $AppGW -ErrorAction SilentlyContinue if ($null -eq $BEHttpsCfg) { Write-InformationPlus "`n Adding HTTPS Config ..." -NoNewLine - Add-AzApplicationGatewayBackendHttpSetting -Name $BEHttpsCfgName -Protocol Https -Port $BEHttpsCfgPort -CookieBasedAffinity $CookieBasedAffinity -AffinityCookieName $AffinityCookieName -RequestTimeout 240 -AuthenticationCertificates $AuthCert -Probe $HttpsProbe -ApplicationGateway $AppGW | Out-Null + Add-AzApplicationGatewayBackendHttpSetting @BEHttpProperties | Out-Null $BEHttpsCfg = Get-AzApplicationGatewayBackendHttpSetting -Name $BEHttpsCfgName -ApplicationGateway $AppGW Write-InformationPlus "Done!" -ForegroundColor Green } else { Write-InformationPlus "`n Updating HTTPS Config ..." -NoNewLine - Set-AzApplicationGatewayBackendHttpSetting -Name $BEHttpsCfgName -Protocol Https -Port $BEHttpsCfgPort -CookieBasedAffinity $CookieBasedAffinity -AffinityCookieName $AffinityCookieName -RequestTimeout 240 -AuthenticationCertificates $AuthCert -Probe $HttpsProbe -ApplicationGateway $AppGW | Out-Null + Set-AzApplicationGatewayBackendHttpSetting @BEHttpProperties | Out-Null $BEHttpsCfg = Get-AzApplicationGatewayBackendHttpSetting -Name $BEHttpsCfgName -ApplicationGateway $AppGW Write-InformationPlus "Done!" -ForegroundColor Green } diff --git a/PS-Tools/Public/Set-PSToolsConfig.ps1 b/PS-Tools/Public/Set-PSToolsConfig.ps1 new file mode 100644 index 0000000..18c65ed --- /dev/null +++ b/PS-Tools/Public/Set-PSToolsConfig.ps1 @@ -0,0 +1,123 @@ +function Set-PSToolsConfig { + [CmdletBinding( + SupportsShouldProcess = $true, + DefaultParameterSetName = 'Manual' + )] + + param ( + [string] + $Domain = ((Get-ADDomain).DNSRoot), + + [Parameter( + Mandatory = $true, + ParameterSetName = 'Manual' + )] + [ValidateSet('New-SA')] + [string] + $FunctionName, + + [Parameter( + Mandatory = $true, + ParameterSetName = 'Auto' + )] + [hashtable] + $ConfigValues + ) + + # Reload Configuration from File to make sure it is in the correct form + try { + $Global:PSToolsConfig = Get-Content -Path 'C:\ProgramData\PS-Tools\config.json' -ErrorAction 'Stop' | ConvertFrom-Json -AsHashtable + Write-Verbose "Current Configuration loaded." + } + catch { + $Global:PSToolsConfig = @{} + Write-Verbose "No Configuration Present." + } + + # Check for Domain + if ($Global:PSToolsConfig."$Domain") { + Write-Verbose "Domain exists in configuration." + } + else { + $Global:PSToolsConfig.Add($Domain,@{}) + Write-Verbose "Domain added to configuration." + } + + # Shortcut to domain config + $Config = $Global:PSToolsConfig."$Domain" + + # Load reference settings object + $PSToolsConfigReqsPath = "$((Get-Item $PSScriptRoot).Parent)\PSToolsConfigReqs.ps1" + Write-Verbose "Loading config from $PSToolsConfigReqsPath" + . $PSToolsConfigReqsPath + + # Filter settings to ask for based on function name + if ($PSBoundParameters.Keys -contains 'FunctionName') { + Write-Verbose "Processing settings for function $FunctionName" + $Settings = $ReferenceSettings."$FunctionName" + } + else { + $Settings = $ReferenceSettings.GetEnumerator() | ForEach-Object { $_.Value } + } + + # Get valid organizational units + $OUs = (Get-ADOrganizationalUnit -Filter * -Credential $cred -Server dalprddom01.freemanco.com).DistinguishedName + Write-Verbose "$($OUs.Count) Organizational Units retrieved." + + # Get valid group names + $Groups = (Get-ADGroup -Filter * -Credential $cred -Server dalprddom01.freemanco.com).Name + Write-Verbose "$($Groups.Count) AD Groups retrieved." + + # Can we make suggestions on incorrect OU or Group Names? + if (Get-InstalledModule -Name Communary.PASM) { + $Fuzzy = $true + } + else { + $Fuzzy = $false + Write-InformationPlus -Message 'Install module Communary.PASM to enable fuzzy match checking on your AD entries.' + } + + # Ask for a value for each setting + foreach ($Setting in $Settings) { + $Validated = $false + $Default = $Config."$Setting" + do { + if ($Setting -match "^.*OU$") { + $ValidationType = 'Organizational Unit' + $ValidationSet = $OUs + } + elseif ($Setting -match "^.*Group$") { + $ValidationType = 'AD Group' + $ValidationSet = $Groups + } + + $Value = Get-Input -Prompt "$Setting" -Default $Default + + if ($ValidationSet -notcontains $Value) { + Write-InformationPlus -Message "$Value is not a valid $ValidationType." -ForegroundColor Yellow + if ($Fuzzy) { + $FuzzyMatch = ($ValidationSet | Select-FuzzyString -Search $Value | Sort-Object -Property Score -Descending | Select-Object -First 1).Result + Write-InformationPlus -Message "Did you mean:`n$FuzzyMatch" + $Default = $FuzzyMatch + } + } + else { + $Validated = $true + } + } until ($Validated) + + if ($Config.Keys -notcontains $Setting) { + $Config.Add($Setting, $Value) + } + else { + $Config."$Setting" = $Value + } + + } # End add settings loop + + if (-not (Test-Path -Path 'C:\ProgramData\PS-Tools\')) { + New-Item -ItemType Directory -Path 'C:\ProgramData\PS-Tools\' + } + + $Global:PSToolsConfig | ConvertTo-Json | Set-Content -Path 'C:\ProgramData\PS-Tools\config.json' +} \ No newline at end of file diff --git a/PS-Tools/tests/New-User.tests.ps1 b/PS-Tools/tests/New-User.tests.ps1 index a0f2993..73fed0e 100644 --- a/PS-Tools/tests/New-User.tests.ps1 +++ b/PS-Tools/tests/New-User.tests.ps1 @@ -10,6 +10,10 @@ BeforeAll { Function Connect-AzureADTenant {} Mock Connect-AzureADTenant { $true } + # Mock needs to return an array like the real command would + Function Get-ADDomainController {} + Mock Get-ADDomainController { New-Object -TypeName psobject -Property @{Hostname = @('some-dc.domain.local')} } + # Mock needs to return $true because the result is used as a condition Function New-ADUser {} #[CmdletBinding(SupportsShouldProcess = $True)]param([object]$object)} Mock New-ADUser { $true } diff --git a/README.md b/README.md index efa68ed..368ff59 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,17 @@ PS-Tools is a collection of tools to simplify the administrative processes. * [Pending Improvements](#Pending-Improvements) ## Change Log +* v1.1.0 - 2020-7-13 + * New Cmdlets + * Set-PSToolsConfig + * Created JSON config file to hold values required for other cmdlets. Solves issue around editing cmdlets for each environments + * Test-PSToolsConfig (Internal) + * Used internally to validate the required settings exist for a specific cmdlet. + * Updated Cmdlets + * New-SA + * Adapted for PSToolsConfig values + * Set-AzAppGwConfig + * Added TrustedCA switch to support Application Gateway v2 and the ability to use a Trusted CA instead of an uploaded certificate. * v1.0.1 - 2020-6-29 * New Cmdlets * Write-InformationPlus diff --git a/test.ps1 b/test.ps1 index 2abc54e..e558e8c 100644 --- a/test.ps1 +++ b/test.ps1 @@ -24,7 +24,8 @@ Write-InformationPlus "Pester Version: $($Pester.Version)`n" $Config = [PesterConfiguration]::Default $Config.Output.Verbosity = $Output -$config.Run.PassThru = $true +$Config.Run.PassThru = $true +$Config.Run.Path = "$PSScriptRoot\PS-Tools\tests" if (-not $Local) { $Config.TestResult.Enabled = $true