ADMF.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\ADMF.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName ADMF.Import.DoDotSource -Fallback $false if ($ADMF_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName ADMF.Import.IndividualFiles -Fallback $false if ($ADMF_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath if ($doDotSource) { . $resolvedPath } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1" # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1" # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code <# This file loads the strings documents from the respective language folders. This allows localizing messages and errors. Load psd1 language files for each language you wish to support. Partial translations are acceptable - when missing a current language message, it will fallback to English or another available language. #> Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'ADMF' -Language 'en-US' function Invoke-CallbackMenu { <# .SYNOPSIS Calls a GUI window to pick the contexts for a specific server. .DESCRIPTION Calls a GUI window to pick the contexts for a specific server. This is used when invoking Set-AdmfContext with the (hidden) -Callback parameter. It is designed to be triggered automatically when trying to manage a forest / domain that has not yet had its context defined. Note: This makes it critical to define a context first when doing unattended automation. .PARAMETER Server The server / domain being connected to. Used for documentation purposes, as well as to potentially determine initial checkbox state. .PARAMETER Credential The credentials to use for this operation. .EXAMPLE PS C:\> Invoke-CallbackMenu -Server contoso.com Shows the context selection menu for the domain contoso.com #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Server, [pscredential] $Credential ) begin { #region Utility Functions function New-CheckBox { [CmdletBinding()] param ( $ContextObject, $Parent ) $column = $Parent.Controls.Count % 2 $row = [math]::Truncate(($Parent.Controls.Count / 2)) $checkbox = [System.Windows.Forms.CheckBox]::new() $checkbox.Width = 200 $checkbox.Height = 20 $checkbox.AutoSize = $false $checkbox.Location = [System.Drawing.Point]::new((210 * $column + 15), (25 * $row + 15)) $checkbox.Text = $ContextObject.Name $checkbox.Font = 'Microsoft Sans Serif,10' $null = $Parent.Controls.Add($checkbox) $tooltip = [System.Windows.Forms.ToolTip]::new() $tooltip.ToolTipTitle = $ContextObject.Name $tooltipText = $ContextObject.Description if ($ContextObject.Prerequisites.Count -gt 0) { $tooltipText += "`nPrerequisites: $($ContextObject.Prerequisites -join ', ')" } if ($ContextObject.MutuallyExclusive.Count -gt 0) { $tooltipText += "`nMutually exclusive with: $($ContextObject.MutuallyExclusive -join ', ')" } $tooltip.SetToolTip($checkbox, $tooltipText) $checkbox.Add_CheckedChanged({ Update-Checkbox }) $checkbox } function Update-Checkbox { [CmdletBinding()] param () # Exemption: Accessing superscope variables directly. Forms and their events are screwey enough. foreach ($checkbox in $contextCheckboxes.Values) { $checkbox.Enabled = $true } foreach ($contextObject in $allContexts) { foreach ($prerequisite in $contextObject.Prerequisites) { if (-not $contextCheckboxes[$prerequisite].Checked) { $contextCheckboxes[$contextObject.Name].Enabled = $false $contextCheckboxes[$contextObject.Name].Checked = $false break } } foreach ($exclusion in $contextObject.MutuallyExclusive) { if (-not $contextCheckboxes[$contextObject.Name].Checked) { break } if (-not $contextCheckboxes[$exclusion]) { continue } $contextCheckboxes[$exclusion].Enabled = $false $contextCheckboxes[$exclusion].Checked = $false } } } function New-Form { [OutputType([System.Windows.Forms.Form])] [CmdletBinding()] param () New-Object System.Windows.Forms.Form -Property @{ ClientSize = '500,500' Text = "Context Selection" TopMost = $false AutoSize = $false } } function New-GroupBox { [OutputType([System.Windows.Forms.Groupbox])] [CmdletBinding()] param ( [string] $Text, [int] $Height, $Form ) $newHeight = 10 if ($Form.Controls.Count -gt 0) { $last = $Form.Controls | Sort-Object { $_.Location.Y } -Descending | Select-Object -First 1 $newHeight = 10 + $last.Height + $last.Location.Y } $groupBox = New-Object System.Windows.Forms.Groupbox -Property @{ Height = $Height Width = 480 Text = $Text AutoSize = $false Location = (New-Object System.Drawing.Point(10, $newHeight)) } $Form.Controls.Add($groupBox) $groupBox } function New-Label { [CmdletBinding()] param ( [string] $Text, $Parent ) $label = New-Object system.Windows.Forms.Label -Property @{ Text = $Text AutoSize = $false Font = 'Microsoft Sans Serif,10' Location = (New-Object System.Drawing.Point(10, 15)) Width = 460 TextAlign = 'MiddleCenter' } $Parent.Controls.Add($label) } #endregion Utility Functions #region Form [System.Windows.Forms.Application]::EnableVisualStyles() $form = New-Form $group_Server = New-GroupBox -Text "Selected Domain / Server" -Height 50 -Form $form New-Label -Text $Server -Parent $group_Server #region Contexts $allContexts = Get-AdmfContext $groupedContexts = $allContexts | Group-Object Group $contextCheckboxes = @{ } foreach ($groupedContext in $groupedContexts) { $rows = [math]::Round(($groupedContext.Group.Count / 2), [System.MidpointRounding]::AwayFromZero) $group_Context = New-GroupBox -Text $groupedContext.Name -Height ($rows * 25 + 15) -Form $form foreach ($contextObject in ($groupedContext.Group | Sort-Object Name)) { $contextCheckboxes[$contextObject.Name] = New-CheckBox -ContextObject $contextObject -Parent $group_Context } } $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential if ($parameters.Server -eq '<Default Domain>') { $parameters.Server = $env:USERDNSDOMAIN } foreach ($context in $allContexts | Sort-Object Weight) { $path = Join-Path $context.Path 'contextPromptChecked.ps1' if (Test-Path $path) { try { $result = & $path @parameters if ($result) { $contextCheckboxes[$context.Name].Checked = $true } } catch { Write-PSFMessage -Level Warning -String 'Invoke-CallbackMenu.Context.Checked.Error' -StringValues $context.Name -ErrorRecord $_ } } } Update-Checkbox #endregion Contexts #region Buttons $button_Cancel = New-Object system.Windows.Forms.Button -Property @{ Text = 'Cancel' Width = 60 Height = 30 Anchor = 'right,bottom' Location = (New-Object System.Drawing.Point(426, 460)) Font = 'Microsoft Sans Serif,10' } $form.Controls.Add($button_Cancel) $button_OK = New-Object system.Windows.Forms.Button -Property @{ Text = 'OK' Width = 38 Height = 30 Anchor = 'right,bottom' Location = (New-Object System.Drawing.Point(378, 460)) Font = 'Microsoft Sans Serif,10' } $form.Controls.Add($button_OK) #endregion Buttons #region Other Stuff $okbox = [System.Windows.Forms.CheckBox]::new() $okbox.Visible = $false $form.Controls.Add($okbox) $button_OK.Add_Click({ $okbox.Checked = $true $this.Parent.Close() }) $form.ShowIcon = $false $form.CancelButton = $button_Cancel $form.AcceptButton = $button_OK $last = $form.Controls | Where-Object { $_ -is [System.Windows.Forms.Groupbox] } | Sort-Object { $_.Location.Y } -Descending | Select-Object -First 1 $newHeight = 90 + $last.Height + $last.Location.Y $form.Height = $newHeight #endregion Other Stuff #endregion Form } process { $null = $form.ShowDialog() if (-not $okbox.Checked) { throw "Interrupting: User cancelled operation" } $selectedNames = @(($contextCheckboxes.Values | Where-Object Checked).Text) $allContexts | Where-Object Name -In $selectedNames } } function Invoke-PostCredentialProvider { <# .SYNOPSIS Executes the PostScript action of a credential provider. .DESCRIPTION Executes the PostScript action of a credential provider. .PARAMETER ProviderName Name of the credential provider to use. .PARAMETER Server The original server targeted. .PARAMETER Credential The original credentials specified by the user. .PARAMETER Cmdlet The $PSCmdlet object of the calling command. Used to kill it with maximum prejudice in case of error. .EXAMPLE PS C:\> Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential Performs any post-execution action registered for the $CredentialProvider (if any) #> [CmdletBinding()] param ( [string] $ProviderName, [PSFComputer] $Server, [AllowNull()] [PSCredential] $Credential, [System.Management.Automation.PSCmdlet] $Cmdlet ) if (-not $script:credentialProviders[$ProviderName]) { Stop-PSFFunction -String 'Invoke-PostCredentialProvider.Provider.NotFound' -StringValues $ProviderName -EnableException $true -Cmdlet $Cmdlet } if (-not $script:credentialProviders[$ProviderName].PostScript) { return } $argument = [PSCustomObject]@{ Server = $Server Credential = $Credential } try { $null = $script:credentialProviders[$ProviderName].PostScript.Invoke($argument) } catch { Stop-PSFFunction -String 'Invoke-PostCredentialProvider.Provider.ExecutionError' -StringValues $ProviderName -EnableException $true -ErrorRecord $_ -Cmdlet $Cmdlet } } function Invoke-PreCredentialProvider { <# .SYNOPSIS Resolves credentials to use using the registered credential provider. .DESCRIPTION Resolves credentials to use using the registered credential provider. .PARAMETER ProviderName Name of the credential provider to use. .PARAMETER Server The server to connect to. .PARAMETER Credential The credentials specified by the user. .PARAMETER Parameter The parameter object resolved from the original user input. .PARAMETER Cmdlet The $PSCmdlet object of the calling command. Used to kill it with maximum prejudice in case of error. .EXAMPLE PS C:\> $originalArgument = Invoke-PreCredentialProvider @parameters -ProviderName $CredentialProvider -Parameter $parameters Resolves the credentials to use and automatically injects them into the $parameters hashtable. Also returns the original input for use when invoking the PostScript scriptblock of the provider. #> [CmdletBinding()] param ( [string] $ProviderName, [PSFComputer] $Server, [AllowNull()] [PSCredential] $Credential, [Hashtable] $Parameter, [System.Management.Automation.PSCmdlet] $Cmdlet ) if (-not $script:credentialProviders[$ProviderName]) { Write-PSFMessage -Level Warning -String 'Invoke-PreCredentialProvider.Provider.NotFound' -StringValues $ProviderName Stop-PSFFunction -String 'Invoke-PreCredentialProvider.Provider.NotFound' -StringValues $ProviderName -EnableException $true -Cmdlet $Cmdlet } $argument = [PSCustomObject]@{ Server = $Server Credential = $Credential } try { $results = $script:credentialProviders[$ProviderName].PreScript.Invoke($argument) | Where-Object { $_ -is [PSCredential] } | Select-Object -First 1 } catch { Write-PSFMessage -Level Warning -String 'Invoke-PreCredentialProvider.Provider.ExecutionError' -StringValues $ProviderName -ErrorRecord $_ Stop-PSFFunction -String 'Invoke-PreCredentialProvider.Provider.ExecutionError' -StringValues $ProviderName -EnableException $true -ErrorRecord $_ -Cmdlet $Cmdlet } if ($results) { $Parameter['Credential'] = $results } elseif ($Parameter.ContainsKey('Credential')) { $Parameter.Remove('Credential') } return $argument } function Resolve-DomainController { <# .SYNOPSIS Resolves a domain to a specific domaincontroller. .DESCRIPTION Resolves a domain to a specific domaincontroller. .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .PARAMETER Type The type of DC to resolve to. Governed by the 'ADMF.DCSelectionMode' configuration setting. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Resolve-DomainController @parameters Picks the server to work against. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] [OutputType([string])] [CmdletBinding(SupportsShouldProcess = $true)] param ( [PSFComputer] $Server, [PSCredential] $Credential, [ValidateSet('PDCEmulator', 'Random')] [string] $Type = (Get-PSFConfigValue -FullName 'ADMF.DCSelectionMode' -Fallback 'PDCEmulator') ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $parameters['Debug'] = $false } process { $targetString = $Server if (-not $targetString) { $targetString = $env:USERDNSNAME } $null = Invoke-PSFProtectedCommand -ActionString 'Resolve-DomainController.Connecting' -ActionStringValues $targetString -Target $targetString -ScriptBlock { $domainController = Get-ADDomainController @parameters -ErrorAction Stop } -PSCmdlet $PSCmdlet -EnableException $true -RetryCount 5 -RetryWait 2 -Confirm:$false if ($domainController.HostName -eq $Server) { Write-PSFMessage -Level Host -String 'Resolve-DomainController.Resolved' -StringValues $domainController.HostName return $domainController.HostName } if ($domainController.Name -eq $Server) { Write-PSFMessage -Level Host -String 'Resolve-DomainController.Resolved' -StringValues $domainController.Name return $domainController.Name } switch ($Type) { 'Random' { Write-PSFMessage -Level Host -String 'Resolve-DomainController.Resolved' -StringValues $domainController.HostName return $domainController.HostName } default { $domain = Get-ADDomain @parameters Write-PSFMessage -Level Host -String 'Resolve-DomainController.Resolved' -StringValues $domain.PDCEmulator $domain.PDCEmulator } } } } function Export-AdmfGpo { <# .SYNOPSIS Creates an export of GPO objects for use in the Domain Management module. .DESCRIPTION Creates an export of GPO objects for use in the Domain Management module. Use this command to record new GPO data for the module. .PARAMETER Path The path to which to export the GPOs. .PARAMETER GpoObject The GPO objects to export. Only accepts output of Get-GPO .PARAMETER Domain The domain to export from. .EXAMPLE PS C:\> Get-GPO -All | Where-Object DisplayName -like 'AD-D-SEC-T0*' | Export-AdmfGpo -Path . Exports all GPOs named like 'AD-D-SEC-T0*' to the current path #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] Param ( [PsfValidateScript('ADMF.Validate.Path', ErrorString = 'ADMF.Validate.Path')] [Parameter(Mandatory = $true)] [string] $Path, [PsfValidateScript('ADMF.Validate.Type.Gpo', ErrorString = 'ADMF.Validate.Type.Gpo')] [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $GpoObject, [string] $Domain = $env:USERDNSDOMAIN ) begin { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem $backupCmd = { Backup-GPO -Path $resolvedPath -Domain $Domain } $backupGPO = $backupCmd.GetSteppablePipeline() $backupGPO.Begin($true) [System.Collections.ArrayList]$gpoData = @() $exportID = (New-Guid).ToString() } process { foreach ($gpoItem in $GpoObject) { $exportData = $backupGPO.Process(($gpoItem | Select-PSFObject 'ID as GUID')) $data = [PSCustomObject]@{ DisplayName = $gpoItem.DisplayName Description = $gpoItem.Description ID = "{$($exportData.ID.ToString().ToUpper())}" ExportID = $exportID } $null = $gpoData.Add($data) } } end { $backupGPO.End() $gpoData | ConvertTo-Json | Set-Content "$resolvedPath\exportData.json" # Remove hidden attribute, top prevent issues with copy over WinRM foreach ($fsItem in (Get-ChildItem -Path $resolvedPath -Recurse -Force)) { $fsItem.Attributes = $fsItem.Attributes -band [System.IO.FileAttributes]::Directory } } } function Get-AdmfContext { <# .SYNOPSIS Return available contexts. .DESCRIPTION Return available contexts. By default, only the latest version of any given context will be returned. .PARAMETER Name The name of the context to filter by. .PARAMETER Store The context stores to look in. .PARAMETER All Return all versions of any given context, rather than just the latest version. .PARAMETER Current Displays the currently active contexts. .PARAMETER Importing Return the contexts that are currently being imported. Use this to react from within your context's scriptblocks to any other context that is selected. This parameter only has meaning when used within a context's scriptblocks. .PARAMETER DomainTable Return a list of which target domain has which contexts assigned in cache. .EXAMPLE PS C:\> Get-AdmfContext Returns the latest version of all available contexts. #> [CmdletBinding(DefaultParameterSetName = 'Search')] param ( [Parameter(ParameterSetName = 'Search')] [string] $Name = '*', [Parameter(ParameterSetName = 'Search')] [string] $Store = '*', [Parameter(ParameterSetName = 'Search')] [switch] $All, [Parameter(ParameterSetName = 'Current')] [switch] $Current, [Parameter(ParameterSetName = 'Importing')] [switch] $Importing, [Parameter(ParameterSetName = 'Server')] [switch] $DomainTable ) process { if ($Current) { return $script:loadedContexts } if ($DomainTable) { return $script:assignedContexts.Clone() } if ($Importing) { return (Get-PSFTaskEngineCache -Module ADMF -Name currentlyImportingContexts) } $contextStores = Get-AdmfContextStore -Name $Store $allContextData = foreach ($contextStore in $contextStores) { if (-not (Test-Path $contextStore.Path)) { continue } foreach ($folder in (Get-ChildItem -Path $contextStore.Path -Filter $Name -Directory)) { $versionFolders = Get-ChildItem -Path $folder.FullName -Directory | Where-Object { $_.Name -as [version] } | Sort-Object { [version]$_.Name } -Descending if (-not $All) { $versionFolders = $versionFolders | Select-Object -First 1 } foreach ($versionFolder in $versionFolders) { $resultObject = [pscustomobject]@{ PSTypeName = 'ADMF.Context' Name = $folder.Name Version = ($versionFolder.Name -as [version]) Store = $contextStore.Name Path = $versionFolder.FullName Description = '' Weight = 50 Author = '' Prerequisites = @() MutuallyExclusive = @() Group = 'Default' } if (Test-Path -Path "$($versionFolder.FullName)\context.json") { $contextData = Get-Content -Path "$($versionFolder.FullName)\context.json" | ConvertFrom-Json if ($contextData.Weight -as [int]) { $resultObject.Weight = $contextData.Weight -as [int] } if ($contextData.Description) { $resultObject.Description = $contextData.Description } if ($contextData.Author) { $resultObject.Author = $contextData.Author } if ($contextData.Prerequisites) { $resultObject.Prerequisites = $contextData.Prerequisites } if ($contextData.MutuallyExclusive) { $resultObject.MutuallyExclusive = $contextData.MutuallyExclusive } if ($contextData.Group) { $resultObject.Group = $contextData.Group } } $resultObject } } } if ($All) { return $allContextData } # Only return highest version if -All has not been set # The same context name might be stored in multiple stores $allContextData | Group-Object Name | ForEach-Object { $_.Group | Sort-Object Version -Descending | Select-Object -First 1 | Select-PSFObject -TypeName 'ADMF.Context' } } } function Get-AdmfContextStore { <# .SYNOPSIS Returns the list of available context stores. .DESCRIPTION Returns the list of available context stores. .PARAMETER Name The name to filter by. .EXAMPLE PS C:\> Get-AdmfContextStore Returns all available context stores. #> [CmdletBinding()] param ( [string] $Name = '*' ) process { foreach ($config in (Get-PSFConfig -FullName "ADMF.Context.Store.$Name")) { [PSCustomObject]@{ PSTypeName = 'ADMF.Context.Store' Name = $config.Name -replace '^Context\.Store\.' Path = $config.Value PathExists = (Test-Path $config.Value) } } } } function Invoke-AdmfDC { <# .SYNOPSIS Brings all DCs of the target domain into the desired/defined state. .DESCRIPTION Brings all DCs of the target domain into the desired/defined state. .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .PARAMETER Options Which aspects to actually update. By default, all Components are applied. .PARAMETER CredentialProvider The credential provider to use to resolve the input credentials. See help on Register-AdmfCredentialProvider for details. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Invoke-AdmfDC -Server corp.contoso.com Brings all DCs of the domain corp.contoso.com into the desired/defined state. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param ( [PSFComputer] $Server, [PSCredential] $Credential, [ADMF.UpdateDCOptions[]] $Options = 'Default', [string] $CredentialProvider = 'default' ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $originalArgument = Invoke-PreCredentialProvider @parameters -ProviderName $CredentialProvider -Parameter $parameters -Cmdlet $PSCmdlet try { $dcServer = Resolve-DomainController @parameters -Confirm:$false } catch { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet throw } $parameters.Server = $dcServer Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet Set-AdmfContext @parameters -Interactive -ReUse -EnableException $parameters += $PSBoundParameters | ConvertTo-PSFHashtable -Include WhatIf, Confirm, Verbose, Debug $parameters.Server = $dcServer [ADMF.UpdateDCOptions]$newOptions = $Options } process { try { if ($newOptions -band [ADMF.UpdateDCOptions]::Share) { if (Get-DCShare) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDC.Executing.Invoke' -StringValues 'Shares', $parameters.Server Invoke-DCShare @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDC.Skipping.Test.NoConfiguration' -StringValues 'Shares' } } if ($newOptions -band [ADMF.UpdateDCOptions]::FSAccessRule) { if (Get-DCAccessRule) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDC.Executing.Invoke' -StringValues 'FSAccessRules', $parameters.Server Invoke-DCAccessRule @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDC.Skipping.Test.NoConfiguration' -StringValues 'FSAccessRules' } } } catch { throw } finally { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet } } } function Invoke-AdmfDomain { <# .SYNOPSIS Brings a domain into compliance with the desired state. .DESCRIPTION Brings a domain into compliance with the desired state. It implements a wide variety of settings against the targeed domain, whether it be OUs, groups, users, gpos, acls or many more items. Note on order: - OU Creation and Updating should be done first, but DELETING ous (OUHard) should be one of the last operations performed. - Acl & Access operations should be performed last - Managing group policy yields best results in this order: 1. Create new GPO 2. Create Links, only disabling undesired links 3. Delete unneeded GPO 4. Delete undesired links This is due to the fact that "unneeded GPO" are detected by being linked into managed GPOs. .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .PARAMETER Options The various operations that are supported. By default "default" operations are executed against the targeted domain. - Acl : The basic permission behavior of an object (e.g.: Owner, Inheritance) - GPLink : Manages the linking of group policies. - GPPermission : Managing permissions on group policy objects. - GroupPolicy : Deploying and updating GPOs. - GroupMembership : Assigning group membership - Group : Creating groups - OUSoft : Creating & modifying OUs, but not deleting them - OUHard : Creating, Modifying & Deleting OUs. This exists in order to be able to create new OUs, then move all objects over and only when done deleting undesired OUs. Will NOT delete OUs that contain objects.! - PSO : Implementing Finegrained Password Policies - Object : Custom AD object - User : Managing User objects - GPLinkDisable : Creating GP Links, but only disabling undesired links. This is needed in order to detect undesired GPOs to delete: Those linked when they shouldn't be! - GroupPolicyDelete : Deploy, update and delete Group Policy objects. .PARAMETER CredentialProvider The credential provider to use to resolve the input credentials. See help on Register-AdmfCredentialProvider for details. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Invoke-AdmfDomain Brings the current domain into compliance with the desired state. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] Param ( [PSFComputer] $Server, [PSCredential] $Credential, [ADMF.UpdateDomainOptions[]] $Options = 'Default', [string] $CredentialProvider = 'default' ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $originalArgument = Invoke-PreCredentialProvider @parameters -ProviderName $CredentialProvider -Parameter $parameters -Cmdlet $PSCmdlet try { $dcServer = Resolve-DomainController @parameters -Confirm:$false } catch { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet throw } $parameters.Server = $dcServer Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet Set-AdmfContext @parameters -Interactive -ReUse -EnableException $parameters += $PSBoundParameters | ConvertTo-PSFHashtable -Include WhatIf, Confirm, Verbose, Debug $parameters.Server = $dcServer [ADMF.UpdateDomainOptions]$newOptions = $Options } process { try { if ($newOptions -band [UpdateDomainOptions]::OUSoft) { if (Get-DMOrganizationalUnit) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'OrganizationalUnits - Create & Modify', $parameters.Server Invoke-DMOrganizationalUnit @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'OrganizationalUnits - Create & Modify' } } if ($newOptions -band [UpdateDomainOptions]::Group) { if (Get-DMGroup) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'Groups', $parameters.Server Invoke-DMGroup @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Groups' } } if ($newOptions -band [UpdateDomainOptions]::User) { if (Get-DMUser) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'Users', $parameters.Server Invoke-DMUser @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Users' } } if ($newOptions -band [UpdateDomainOptions]::GroupMembership) { if (Get-DMGroupMembership) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'GroupMembership', $parameters.Server Invoke-DMGroupMembership @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupMembership' } } if ($newOptions -band [UpdateDomainOptions]::PSO) { if (Get-DMPasswordPolicy) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'PasswordPolicies', $parameters.Server Invoke-DMPasswordPolicy @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'PasswordPolicies' } } if ($newOptions -band [UpdateDomainOptions]::GroupPolicy) { if (Get-DMGroupPolicy) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'GroupPolicies - Create & Modify', $parameters.Server Invoke-DMGroupPolicy @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicies - Create & Modify' } } if ($newOptions -band [UpdateDomainOptions]::GPPermission) { if (Get-DMGPPermission) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'GroupPolicyPermissions', $parameters.Server Invoke-DMGPPermission @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicyPermissions' } } if ($newOptions -band [UpdateDomainOptions]::GPLinkDisable) { if (Get-DMGPLink) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'GroupPolicyLinks - Create, Update & Disable unwanted Links', $parameters.Server Invoke-DMGPLink @parameters -Disable } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicyLinks - Create, Update & Disable unwanted Links' } } if ($newOptions -band [UpdateDomainOptions]::GroupPolicyDelete) { if (Get-DMGroupPolicy) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'GroupPolicies - Delete', $parameters.Server Invoke-DMGroupPolicy @parameters -Delete } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicies - Delete' } } if ($newOptions -band [UpdateDomainOptions]::GPLink) { if (Get-DMGPLink) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'GroupPolicyLinks - Delete unwanted Links', $parameters.Server Invoke-DMGPLink @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicyLinks - Delete unwanted Links' } } if ($newOptions -band [UpdateDomainOptions]::OUHard) { if (Get-DMOrganizationalUnit) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'OrganizationalUnits - Delete', $parameters.Server Invoke-DMOrganizationalUnit @parameters -Delete } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'OrganizationalUnits - Delete' } } if ($newOptions -band [UpdateDomainOptions]::Object) { if (Get-DMObject) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'Objects', $parameters.Server Invoke-DMObject @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Objects' } } if ($newOptions -band [UpdateDomainOptions]::Acl) { if (Get-DMAcl) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'Acls', $parameters.Server Invoke-DMAcl @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Acls' } } if ($newOptions -band [UpdateDomainOptions]::AccessRule) { if (Get-DMAccessRule) { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Executing.Invoke' -StringValues 'AccessRules', $parameters.Server Invoke-DMAccessRule @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'AccessRules' } } } catch { throw } finally { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet } } } function Invoke-AdmfForest { <# .SYNOPSIS Applies the currently desired configuration to the targeted forest. .DESCRIPTION Applies the currently desired configuration to the targeted forest. By default, this will only include sites, sitelinks and subnets. To switch to a full application, use the "-Options All" parameter. .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .PARAMETER Options Which aspects to actually update. Defaults to "default" (Sites, SiteLinks & Subnets) Also available: - ServerRelocate (reassigns domain controllers to correct sites, if necessary) - Schema (applies core schema updates) - SchemaLdif (applies product Ldif files, such as SkypeForBusiness) To update everything, use "All". .PARAMETER CredentialProvider The credential provider to use to resolve the input credentials. See help on Register-AdmfCredentialProvider for details. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE PS C:\> Invoke-AdmfForest -Server contoso.com -Options All Applies the full forest configuration to the contoso.com domain. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] Param ( [PSFComputer] $Server, [PSCredential] $Credential, [ADMF.UpdateForestOptions[]] $Options = 'Default', [string] $CredentialProvider = 'default' ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $originalArgument = Invoke-PreCredentialProvider @parameters -ProviderName $CredentialProvider -Parameter $parameters -Cmdlet $PSCmdlet try { $dcServer = Resolve-DomainController @parameters -Confirm:$false } catch { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet throw } $parameters.Server = $dcServer Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet Set-AdmfContext @parameters -Interactive -ReUse -EnableException $parameters += $PSBoundParameters | ConvertTo-PSFHashtable -Include WhatIf, Confirm, Verbose, Debug $parameters.Server = $dcServer [ADMF.UpdateForestOptions]$newOptions = $Options } process { try { if ($newOptions -band [UpdateForestOptions]::Sites) { if (Get-FMSite) { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'Sites', $parameters.Server Invoke-FMSite @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Sites' } } if ($newOptions -band [UpdateForestOptions]::SiteLinks) { if (Get-FMSiteLink) { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'Sitelinks', $parameters.Server Invoke-FMSiteLink @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Sitelinks' } } if ($newOptions -band [UpdateForestOptions]::Subnets) { if (Get-FMSubnet) { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'Subnets', $parameters.Server Invoke-FMSubnet @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Subnets' } } if ($newOptions -band [UpdateForestOptions]::ServerRelocate) { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'Server Site Assignment', $parameters.Server Invoke-FMServer @parameters } if ($newOptions -band [UpdateForestOptions]::Schema) { if (Get-FMSchema) { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'Schema (Custom)', $parameters.Server Invoke-FMSchema @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Schema (Custom)' } } if ($newOptions -band [UpdateForestOptions]::SchemaLdif) { if (Get-FMSchemaLdif) { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'Schema (Ldif)', $parameters.Server Invoke-FMSchemaLdif @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Schema (Ldif)' } } if ($newOptions -band [UpdateForestOptions]::NTAuthStore) { if (Get-FMNTAuthStore) { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Executing.Invoke' -StringValues 'NTAuthStore', $parameters.Server Invoke-FMNTAuthStore @parameters } else { Write-PSFMessage -Level Host -String 'Invoke-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'NTAuthStore' } } } catch { throw } finally { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet } } } function New-AdmfContext { <# .SYNOPSIS Creates a new configuration context for ADMF. .DESCRIPTION Creates a new configuration context for ADMF. Contexts are a set of configuration settings. You can combine multiple contexts at the same time, merging the settings they contain. For more details on how contexts work, see: Get-Help about_ADMF_Context .PARAMETER Name The name of the context to create. .PARAMETER Store The context store to create the context in. Context Stores are registered filesystem locations where ADMF will look for contexts. Defaults to the default store found in %AppData%. .PARAMETER OutPath Create the context in a target path, rather than a registered store. Keep in mind, that this will require the context to be manually moved to a registered location in order for it to become available to use. .PARAMETER Weight The priority of the context. This is used to determine the import order when importing multiple contexts. The higher the value, the later in the import order. Default: 50 .PARAMETER Description Add a description to your context (for documentation purposes only). .PARAMETER Author The author of the context (for documentation purposes only) .PARAMETER Group The group to assign the context to. By default, will be part of the "Default" group. Groups are only relevant fpr the itneractive context selection menu, where they govern the visual display style / grouping. .PARAMETER Prerequisite Contexts the current context depends on / requires. .PARAMETER MutuallyExclusive Contexts that are mutually exclusive with each other. E.g.: Where the user has to select between one of several environments. .PARAMETER DefaultAccessRules A new Active Directory environment comes with more deployed security delegations than defined in the schema. Several containers - especially the BuiltIn container - have a lot of extra access rules. When deploying a restrictive domain content mode, where these objects fall under management, it becomes necessary to also configure these delegations, lest they be removed. Setting this switch will include all the default delegations in your new context. .PARAMETER IncludeTemplate Whether to include example configuration files in the context. These must all be corrected or removed later on, but offer some initial guidance in how a configuration set for a given setting type might look like. .PARAMETER Force This command refuses to replace an existing context by default. Using force, it is a bit more brutish and will kill any previously existing context with the same name in the target store. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> New-AdmfContext -Name 'newContext' Creates a new context named "newContext" .EXAMPLE PS C:\> New-AdmfContext -Name 'Contoso_Baseline' -Store Company -Weight 10 -Author "Sad Joey" -DefaultccessRules -Description "Default baseline for contoso company forests" Creates a new context ... - Named "Contoso_Baseline" - In the context store "Company" - With the weight 10 (very low, causing it to be one of the first to be applied) - By Sad Joey (a great and non-sad person) - that contains the default access rules - has a useful description of what it is for #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName = 'Store')] param ( [Parameter(Mandatory = $true)] [PsfValidatePattern('^[\w\d_\-\.]+$', ErrorString = 'ADMF.Validate.Pattern.ContextName')] [string] $Name, [Parameter(ParameterSetName = 'Store')] [PsfValidateSet(TabCompletion = 'ADMF.Context.Store')] [string] $Store = 'Default', [Parameter(ParameterSetName = 'Path')] [PsfValidateScript('ADMF.Validate.Path.Folder', ErrorString = 'ADMF.Validate.Path.Folder')] [string] $OutPath, [int] $Weight = 50, [string] $Description = "<Insert description-text here>", [string] $Author = "<Insert your name here>", [string] $Group = 'Default', [string[]] $Prerequisite = @(), [string[]] $MutuallyExclusive = @(), [switch] $DefaultAccessRules, [switch] $IncludeTemplate, [switch] $Force, [switch] $EnableException ) begin { if ($OutPath) { $resolvedPath = Resolve-PSFPath -Provider FileSystem -Path $OutPath -SingleItem if (-not $Force -and (Test-Path -Path "$resolvedPath\$Name")) { Stop-PSFFunction -String 'New-AdmfContext.Context.AlreadyExists' -StringValues $resolvedPath, $Name -EnableException $EnableException -Category InvalidArgument -Cmdlet $PSCmdlet return } } else { $storeObject = Get-AdmfContextStore -Name $Store if (-not $Force -and (Test-Path -Path "$($storeObject.Path)\$Name")) { Stop-PSFFunction -String 'New-AdmfContext.Context.AlreadyExists2' -StringValues $Store, $Name -EnableException $EnableException -Category InvalidArgument -Cmdlet $PSCmdlet return } if (-not (Test-Path -Path $storeObject.Path)) { $null = New-Item -Path $storeObject.Path -ItemType Directory -Force } $resolvedPath = Resolve-PSFPath -Provider FileSystem -Path $storeObject.Path -SingleItem } } process { if (Test-PSFFunctionInterrupt) { return } # This can only be $true when -Force was used, as otherwise it would fail in begin if (Test-Path -Path "$resolvedPath\$Name") { Remove-Item -Path "$resolvedPath\$Name" -Recurse -Force } $contextFolder = New-Item -Path $resolvedPath -Name $Name -ItemType Directory $contextVersionFolder = New-Item -Path $contextFolder.FullName -Name '1.0.0' -ItemType Directory if ($IncludeTemplate) { Copy-Item -Path "$script:ModuleRoot\internal\data\contextTemplate\*" -Destination "$($contextVersionFolder.FullName)\" -Recurse } else { Copy-Item -Path "$script:ModuleRoot\internal\data\context\*" -Destination "$($contextVersionFolder.FullName)\" -Recurse } #region Default Access Rules if ($DefaultAccessRules){ Copy-Item -Path "$script:ModuleRoot\internal\data\domainDefaults\accessRules\*.json" -Destination "$($contextVersionFolder.FullName)\domain\accessrules\" Copy-Item -Path "$script:ModuleRoot\internal\data\domainDefaults\objectCategories\*.psd1" -Destination "$($contextVersionFolder.FullName)\domain\objectcategories\" Copy-Item -Path "$script:ModuleRoot\internal\data\domainDefaults\gppermissions\*.json" -Destination "$($contextVersionFolder.FullName)\domain\gppermissions\" Copy-Item -Path "$script:ModuleRoot\internal\data\domainDefaults\gppermissionfilters\*.json" -Destination "$($contextVersionFolder.FullName)\domain\gppermissionfilters\" } #endregion Default Access Rules $contextJson = [pscustomobject]@{ Version = '1.0.0' Weight = $Weight Description = $Description Author = $Author Prerequisites = $Prerequisite MutuallyExclusive = $MutuallyExclusive Group = $Group } $contextJson | ConvertTo-Json | Set-Content -Path "$($contextVersionFolder.FullName)\context.json" Get-AdmfContext -Name $Name -Store $Store } } function New-AdmfContextStore { <# .SYNOPSIS Creates a new Context Store. .DESCRIPTION Creates a new Context Store. Context Stores are locations where configuration contexts are stored and retrieved from. These contexts are stored using the PSFramework configuration system: https://psframework.org/documentation/documents/psframework/configuration.html Making it possible to deploy them using GPO, SCCM or other computer or profile management solutions. .PARAMETER Name The name of the store to create. Must not exist yet. .PARAMETER Path The path where the context is pointing at. Must be an existing folder. .PARAMETER Scope Where to persist the store. by default, this is stored in HKCU, making the store persistently available to the user. For more information on scopes, and what location they corespond with, see: https://psframework.org/documentation/documents/psframework/configuration/persistence-location.html .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> New-AdmfContextStore -Name 'company' -Path '\\contoso\system\ad\contexts' Creates a new context named 'company', pointing at '\\contoso\system\ad\contexts' #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PsfValidateScript('ADMF.Validate.ContextStore.ExistsNot', ErrorString = 'ADMF.Validate.ContextStore.ExistsNot')] [PsfValidatePattern('^[\w\d_\-\.]+$', ErrorString = 'ADMF.Validate.Pattern.ContextStoreName')] [string] $Name, [Parameter(Mandatory = $true)] [PsfValidateScript('ADMF.Validate.Path.Folder', ErrorString = 'ADMF.Validate.Path.Folder')] [string] $Path, [PSFramework.Configuration.ConfigScope] $Scope = "UserDefault", [switch] $EnableException ) process { $resolvedPath = Resolve-PSFPath -Path $Path -Provider FileSystem -SingleItem Set-PSFConfig -FullName "ADMF.Context.Store.$Name" -Value $resolvedPath Register-PSFConfig -FullName "ADMF.Context.Store.$Name" -Scope $Scope -EnableException:$EnableException } } function Register-AdmfCredentialProvider { <# .SYNOPSIS Registers a credential provider used by the ADMF. .DESCRIPTION Registers a credential provider used by the ADMF. Credential providers are used for translating the credentials to use for all actions performed against active directory. For example, the ADMF could be extended to support a password safe solution: When connecting to a target domain, this provider scriptblock would retrieve the required credentials from a password safe solution. A credential provider consists of two scriptblocks: - A PreScript that is executed before running any commands. It must return either a PSCredential object (if applicable) or $null (if default windows credentials should be used instead). - A PostScript that is executed after all component commands have been executed. It need not return anything. Both scriptblocks receive a single input object, with two properties: - Server: The computer / domain targeted - Credential: The credentials originally provided (if any - this may be $null instead!) .PARAMETER Name The name of the credential provider. Each name must be unique, registering a provider using an existing name overwrites the previous provider. The provider "default" exists as part of ADMF and will be used if no other is specified. Overriding it allows you to change the default provider intentionally, but may remove your ability to NOT use any credential transformations, so use with care. .PARAMETER PreScript The script to execute before performing actions, in order to resolve the correct credentials to use. - If it returns a credential object, this object will be used for authenticating all AD operations (including WinRM against domain controllers!). - If it returns nothing / only non-credential objects, instead the default windows identity of the user is used. .PARAMETER PostScript This script is executed after performing all actions. You can use this optional script to perform any cleanup actions if necessary. .EXAMPLE PS C:\> Register-AdmfCredentialProvider -Name AZKeyVault -PreScript $keyVaultScript Registers the scriptblock defined in $keyVaultScript as "AZKeyVault" provider. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [Scriptblock] $PreScript, [Scriptblock] $PostScript ) $script:credentialProviders[$Name] = [PSCustomObject]@{ PSTypeName = 'Admf.CredentialProvider' Name = $Name PreScript = $PreScript PostScript = $PostScript } } function Set-AdmfContext { <# .SYNOPSIS Applies a set of configuration contexts. .DESCRIPTION Applies a set of configuration contexts. This merges the settings from all selected contexts into one configuration set. .PARAMETER Context Name of context or full context object to apply. .PARAMETER Interactive Show an interactive context selection prompt. This is designed for greater convenience when managing many forests. The system automatically uses Set-AdmfContext with this parameter when directly testing or invoking against a new domain without first selecting a context to apply. .PARAMETER ReUse ADMF remembers the last contexts assigned to a specific server/domain. By setting this parameter, it will re-use those contexts, rather than show the prompt again. This parameter is used by the system to prevent prompting automatically on each call. .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .PARAMETER NoDomain If used against a target without a domain, it will skip AD connect and instead use the server name for Context caching purposes. .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS C:\> Set-AdmfContext -Interactive Interactively pick to select the contexts to apply to the user's own domain. .EXAMPLE PS C:\> Set-AdmfContext -Interactive -Server contoso.com Interactively pick to select the contexts to apply to the contoso.com domain. .EXAMPLE PS C:\> Set-AdmfContext -Context Default, Production, Europe -Server eu.contoso.com Configures the contexts Default, Production and Europe to be applied to eu.contoso.com. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName = 'name')] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'name')] [Alias('Name')] [object[]] $Context, [Parameter(ParameterSetName = 'interactive')] [switch] $Interactive, [switch] $ReUse, [PSFComputer] $Server = $env:USERDNSDOMAIN, [System.Management.Automation.PSCredential] $Credential, [Parameter(DontShow = $true)] [switch] $NoDomain, [switch] $EnableException ) begin { #region Utility Functions function Set-Context { [CmdletBinding()] param ( $ContextObject, [string] $Server, [System.Management.Automation.PSCredential] $Credential, [System.Management.Automation.PSCmdlet] $Cmdlet, [bool] $EnableException ) Write-PSFMessage -String 'Set-AdmfContext.Context.Applying' -StringValues $ContextObject.Name -Target $ContextObject $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $stopParam = @{ EnableException = $EnableException Cmdlet = $Cmdlet Target = $ContextObject StepsUpward = 1 } #region PreImport if (Test-Path "$($ContextObject.Path)\preImport.ps1") { try { $null = & "$($ContextObject.Path)\preImport.ps1" @parameters } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.PreImport' -StringValues $ContextObject.Name -ErrorRecord $_ return } } #endregion PreImport #region Forest $forestFields = @{ 'schema' = Get-Command Register-FMSchema 'sitelinks' = Get-Command Register-FMSiteLink 'sites' = Get-Command Register-FMSite 'subnets' = Get-Command Register-FMSubnet } foreach ($key in $forestFields.Keys) { if (-not (Test-Path "$($ContextObject.Path)\forest\$key")) { continue } foreach ($file in (Get-ChildItem "$($ContextObject.Path)\forest\$key\" -Recurse -Filter "*.json")) { Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, $key, $file.FullName try { foreach ($dataSet in (Get-Content $file.FullName | ConvertFrom-Json -ErrorAction Stop | Write-Output | ConvertTo-PSFHashtable -Include $($forestFields[$key].Parameters.Keys))) { if ($forestFields[$key].Parameters.Keys -contains 'ContextName') { $dataSet['ContextName'] = $ContextObject.Name } & $forestFields[$key] @dataSet -ErrorAction Stop } } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, $key, $file.FullName -ErrorRecord $_ return } } } if (Test-Path "$($ContextObject.Path)\forest\schemaldif") { $filesProcessed = @() #region Process Ldif Configuration foreach ($file in (Get-ChildItem "$($ContextObject.Path)\forest\schemaldif\" -Recurse -Filter "*.json")) { $jsonData = Get-Content $file.FullName | ConvertFrom-Json foreach ($jsonEntry in $jsonData) { $targetPath = Join-Path "$($ContextObject.Path)\forest\schemaldif" $jsonEntry.Path if ($filesProcessed -contains $targetPath) { continue } try { $ldifItem = Get-Item -Path $targetPath -ErrorAction Stop -Force } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, 'Schema (ldif)', $file.FullName -ErrorRecord $_ return } $ldifParam = @{ Path = $ldifItem.FullName Name = $ldifItem.BaseName ContextName = $ContextObject.Name } if ($jsonEntry.Name) { $ldifParam.Name = $jsonEntry.Name } if ($jsonEntry.Weight) { $ldifParam['Weight'] = $jsonEntry.Weight } if ($jsonEntry.MissingObjectExemption) { $ldifParam['MissingObjectExemption'] = $jsonEntry.MissingObjectExemption } try { Register-FMSchemaLdif @ldifParam -ErrorAction Stop $filesProcessed += $ldifItem.FullName } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, 'schemaldif', $file.FullName -ErrorRecord $_ return } } } #endregion Process Ldif Configuration #region Process Ldif Files without configuration foreach ($file in (Get-ChildItem "$($ContextObject.Path)\forest\schemaldif\" -Recurse -Filter "*.ldf")) { # Skip files already defined in json if ($filesProcessed -contains $file.FullName) { continue } try { Register-FMSchemaLdif -Name $file.BaseName -Path $file.FullName -ContextName $ContextObject.Name -ErrorAction Stop } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, 'schemaldif', $file.FullName -ErrorRecord $_ return } } #endregion Process Ldif Files without configuration } #region NTAuthStore if (Test-Path "$($ContextObject.Path)\forest\ntAuthStore") { foreach ($file in (Get-ChildItem "$($ContextObject.Path)\forest\ntAuthStore" -Recurse -File)) { switch ($file.Extension) { '.json' { Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, 'NTAuthStore', $file.FullName try { $jsonData = Get-Content -Path $file.FullName | ConvertFrom-Json -ErrorAction Stop if ($jsonData.PSObject.Properties.Name -eq 'Authorative') { Register-FMNTAuthStore -Authorative:$jsonData.Authorative } } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, 'NTAuthStore', $file.FullName -ErrorRecord $_ return } } '.cer' { Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, 'NTAuthStore', $file.FullName try { $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromCertFile($file.FullName) Register-FMNTAuthStore -Certificate $cert } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, 'NTAuthStore', $file.FullName -ErrorRecord $_ return } } } } } #endregion NTAuthStore #endregion Forest #region Domain $domainFields = @{ 'accessrules' = (Get-Command Register-DMAccessRule) 'accessrulemodes' = (Get-Command Register-DMAccessRuleMode) 'acls' = (Get-Command Register-DMAcl) 'builtinsids' = (Get-Command Register-DMBuiltInSID) 'gplinks' = (Get-Command Register-DMGPLink) 'gppermissions' = (Get-Command Register-DMGPPermission) 'gppermissionfilters' = (Get-Command Register-DMGPPermissionFilter) 'gpregistrysettings' = (Get-Command Register-DMGPRegistrySetting) 'groups' = (Get-Command Register-DMGroup) 'groupmemberships' = (Get-Command Register-DMGroupMembership) 'names' = (Get-Command Register-DMNameMapping) 'objects' = (Get-Command Register-DMObject) 'organizationalunits' = (Get-Command Register-DMOrganizationalUnit) 'psos' = (Get-Command Register-DMPasswordPolicy) 'users' = (Get-Command Register-DMUser) } foreach ($key in $domainFields.Keys) { if (-not (Test-Path "$($ContextObject.Path)\domain\$key")) { continue } foreach ($file in (Get-ChildItem "$($ContextObject.Path)\domain\$key\" -Recurse -Filter "*.json")) { Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, $key, $file.FullName try { foreach ($dataSet in (Get-Content $file.FullName | ConvertFrom-Json -ErrorAction Stop | Write-Output | ConvertTo-PSFHashtable -Include $($domainFields[$key].Parameters.Keys))) { if ($domainFields[$key].Parameters.Keys -contains 'ContextName') { $dataSet['ContextName'] = $ContextObject.Name } & $domainFields[$key] @dataSet -ErrorAction Stop } } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.DomainConfig' -StringValues $ContextObject.Name, $key, $file.FullName -ErrorRecord $_ return } } } # Group Policy if (Test-Path "$($ContextObject.Path)\domain\grouppolicies\exportData.json") { $file = Get-Item "$($ContextObject.Path)\domain\grouppolicies\exportData.json" Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, 'Group Policy', $file.FullName try { $dataSet = Get-Content $file.FullName | ConvertFrom-Json -ErrorAction Stop | ConvertTo-PSFHashtable -Include DisplayName, Description, ID, ExportID foreach ($policyEntry in $dataSet) { Register-DMGroupPolicy @policyEntry -Path "$($ContextObject.Path)\domain\grouppolicies\$($policyEntry.ID)" } } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.DomainConfig' -StringValues $ContextObject.Name, 'Group Policy', $file.FullName -ErrorRecord $_ return } } # Object Categories foreach ($file in (Get-ChildItem "$($ContextObject.Path)\domain\objectcategories" -Filter '*.psd1' -ErrorAction Ignore)) { try { $dataSet = Import-PSFPowerShellDataFile -Path $file.FullName $dataSet.TestScript = $dataSet.TestScript.Invoke() | Write-Output # Remove automatic scriptblock nesting Register-DMObjectCategory @dataSet } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.DomainConfig' -StringValues $ContextObject.Name, 'Object Categories', $file.FullName -ErrorRecord $_ return } } # Domain Data foreach ($file in (Get-ChildItem "$($ContextObject.Path)\domain\domaindata" -Filter '*.psd1' -ErrorAction Ignore)) { try { $dataSet = Import-PSFPowerShellDataFile -Path $file.FullName $dataSet.Scriptblock = $dataSet.Scriptblock.Invoke() | Write-Output # Remove automatic scriptblock nesting Register-DMDomainData @dataSet } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.DomainConfig' -StringValues $ContextObject.Name, 'Domain Data', $file.FullName -ErrorRecord $_ return } } # Content Mode if (Test-Path "$($ContextObject.Path)\domain\content_mode.json") { $file = Get-Item "$($ContextObject.Path)\domain\content_mode.json" Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, 'ContentMode', $file.FullName try { $dataSet = Get-Content $file.FullName | ConvertFrom-Json -ErrorAction Stop if ($dataSet.Mode) { Set-DMContentMode -Mode $dataSet.Mode } if ($dataSet.Include) { $includes = @((Get-DMContentMode).Include) foreach ($entry in $dataSet.Include) { $includes += $entry } Set-DMContentMode -Include $includes } if ($dataSet.Exclude) { $excludes = @((Get-DMContentMode).Exclude) foreach ($entry in $dataSet.Exclude) { $excludes += $entry } Set-DMContentMode -Exclude $excludes } if ($dataSet.UserExcludePattern) { $userExcludePatterns = @((Get-DMContentMode).UserExcludePattern) foreach ($entry in $dataSet.UserExcludePattern) { $userExcludePatterns += $entry } Set-DMContentMode -UserExcludePattern $userExcludePatterns } } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.DomainConfig' -StringValues $ContextObject.Name, 'ContentMode', $file.FullName -ErrorRecord $_ return } } #endregion Domain #region DC if (Test-Path "$($ContextObject.Path)\dc\dc_config.json") { try { $dcData = Get-Content "$($ContextObject.Path)\dc\dc_config.json" -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.DCConfig' -StringValues $ContextObject.Name -ErrorRecord $_ return } if ($null -ne $dcData.NoDNS) { Set-PSFConfig -FullName 'DCManagement.Defaults.NoDNS' -Value $dcData.NoDNS } if ($null -ne $dcData.NoReboot) { Set-PSFConfig -FullName 'DCManagement.Defaults.NoReboot' -Value $dcData.NoReboot } if ($dcData.DatabasePath) { Set-PSFConfig -FullName 'DCManagement.Defaults.DatabasePath' -Value $dcData.DatabasePath } if ($dcData.LogPath) { Set-PSFConfig -FullName 'DCManagement.Defaults.LogPath' -Value $dcData.LogPath } if ($dcData.SysvolPath) { Set-PSFConfig -FullName 'DCManagement.Defaults.SysvolPath' -Value $dcData.SysvolPath } } $dcFields = @{ 'shares' = Get-Command Register-DCShare 'fsaccessrules' = Get-Command Register-DCAccessRule } foreach ($key in $dcFields.Keys) { if (-not (Test-Path "$($ContextObject.Path)\dc\$key")) { continue } foreach ($file in (Get-ChildItem "$($ContextObject.Path)\dc\$key\" -Recurse -Filter "*.json")) { Write-PSFMessage -Level Debug -String 'Set-AdmfContext.Context.Loading' -StringValues $ContextObject.Name, $key, $file.FullName try { foreach ($dataSet in (Get-Content $file.FullName | ConvertFrom-Json -ErrorAction Stop | Write-Output | ConvertTo-PSFHashtable -Include $($dcFields[$key].Parameters.Keys))) { if ($dcFields[$key].Parameters.Keys -contains 'ContextName') { $dataSet['ContextName'] = $ContextObject.Name } & $dcFields[$key] @dataSet -ErrorAction Stop } } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.ForestConfig' -StringValues $ContextObject.Name, $key, $file.FullName -ErrorRecord $_ return } } } #endregion DC #region PostImport if (Test-Path "$($ContextObject.Path)\postImport.ps1") { try { $null = & "$($ContextObject.Path)\postImport.ps1" @parameters } catch { Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Stop-PSFFunction @stopParam -String 'Set-AdmfContext.Context.Error.PostImport' -StringValues $ContextObject.Name -ErrorRecord $_ return } } #endregion PostImport } #endregion Utility Functions $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $selectedContexts = @{ } # Common parameters for Stop-PSFFunction $commonParam = @{ EnableException = $EnableException Continue = $true } if ($NoDomain) { $domain = [pscustomobject]@{ DNSRoot = $Server } return # Ends the current block and moves on to process } $adParameters = $parameters.Clone() if (-not $adParameters.Credential) { $adParameters.Remove('Credential') } try { $domain = Get-ADDomain @adParameters -ErrorAction Stop } catch { Stop-PSFFunction -String 'Set-AdmfContext.Domain.AccessError' -StringValues $Server -EnableException $EnableException -ErrorRecord $_ return } } process { if (Test-PSFFunctionInterrupt) { return } #region Explicitly specified contexts foreach ($contextObject in $Context) { if ($contextObject -is [string]) { $foundContext = Get-AdmfContext -Name $contextObject if (-not $foundContext) { Stop-PSFFunction @commonParam -String 'Set-AdmfContext.Context.NotFound' -StringValues $contextObject } if ($foundContext.Count -gt 1) { Stop-PSFFunction @commonParam -String 'Set-AdmfContext.Context.Ambiguous' -StringValues $contextObject, ($foundContext.Name -join ", ") } $selectedContexts[$foundContext.Name] = $foundContext continue } if ($contextObject.PSObject.Typenames -eq 'ADMF.Context') { $selectedContexts[$contextObject.Name] = $contextObject continue } Stop-PSFFunction @commonParam -String 'Set-AdmfContext.Context.InvalidInput' -StringValues $contextObject, $contextObject.GetType().FullName } #endregion Explicitly specified contexts #region Interactively chosen contexts if ($Interactive) { if ($ReUse -and $script:assignedContexts["$($domain.DNSRoot)"]) { foreach ($contextObject in $script:assignedContexts["$($domain.DNSRoot)"]) { $selectedContexts[$contextObject.Name] = $contextObject } return } try { foreach ($contextObject in (Invoke-CallbackMenu @parameters)) { $selectedContexts[$contextObject.Name] = $contextObject } } catch { Stop-PSFFunction -String 'Set-AdmfContext.Interactive.Cancel' -EnableException $EnableException -ErrorRecord $_ return } } #endregion Interactively chosen contexts } end { if (Test-PSFFunctionInterrupt) { return } #region Handle errors in selection $missingPrerequisites = $selectedContexts.Values.Prerequisites | Where-Object { $_ -notin $selectedContexts.Values.Name } if ($missingPrerequisites) { Stop-PSFFunction -String 'Set-AdmfContext.Resolution.MissingPrerequisites' -StringValues ($missingPrerequisites -join ", ") -EnableException $EnableException -Category InvalidData return } $conflictingContexts = $selectedContexts.Values.MutuallyExclusive | Where-Object { $_ -in $selectedContexts.Values.Name } if ($conflictingContexts) { Stop-PSFFunction -String 'Set-AdmfContext.Resolution.ExclusionConflict' -StringValues ($conflictingContexts.Name -join ", ") -EnableException $EnableException -Category InvalidData return } #endregion Handle errors in selection # Do nothing if the currently loaded contexts are equal to the selected ones if ( $script:loadedContexts.Name -and $selectedContexts.Values.Name -and -not (Compare-Object -ReferenceObject $selectedContexts.Values.Name -DifferenceObject $script:loadedContexts.Name) ) { # When switching from one domain to a new one, make sure that the selection is cached, even if it is the same selection. # Otherwise, the second domain will keep reprompting for contexts if (-not $script:assignedContexts["$($domain.DNSRoot)"]) { $script:assignedContexts["$($domain.DNSRoot)"] = $selectedContexts.Values } return } # Kill previous configuration $script:loadedContexts = @() Clear-DCConfiguration Clear-DMConfiguration Clear-FMConfiguration Set-PSFTaskEngineCache -Module ADMF -Name currentlyImportingContexts -Value $selectedContexts.Values foreach ($contextObject in ($selectedContexts.Values | Sort-Object Weight)) { if (Test-PSFFunctionInterrupt) { return } Set-Context @parameters -ContextObject $contextObject -Cmdlet $PSCmdlet -EnableException $EnableException if (Test-PSFFunctionInterrupt) { return } } $script:assignedContexts["$($domain.DNSRoot)"] = $selectedContexts.Values | Sort-Object Weight $script:loadedContexts = @($selectedContexts.Values | Sort-Object Weight) Set-PSFTaskEngineCache -Module ADMF -Name currentlyImportingContexts -Value @() } } function Test-AdmfDC { <# .SYNOPSIS Tests whether all DCs in the target domain are in the desired state. .DESCRIPTION Tests whether all DCs in the target domain are in the desired state. .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .PARAMETER Options What tests to execute. Defaults to all tests. .PARAMETER CredentialProvider The credential provider to use to resolve the input credentials. See help on Register-AdmfCredentialProvider for details. .EXAMPLE PS C:\> Test-AdmfDC Tests the current domain's DCs whether they are compliant with the desired/defined state #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] [CmdletBinding()] param ( [PSFComputer] $Server, [PSCredential] $Credential, [UpdateDCOptions[]] $Options = 'All', [string] $CredentialProvider = 'default' ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $originalArgument = Invoke-PreCredentialProvider @parameters -ProviderName $CredentialProvider -Parameter $parameters -Cmdlet $PSCmdlet try { $parameters.Server = Resolve-DomainController @parameters -ErrorAction Stop -Confirm:$false } catch { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet throw } Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet Set-AdmfContext @parameters -Interactive -ReUse -EnableException [UpdateDCOptions]$newOptions = $Options } process { try { if ($newOptions -band [UpdateDCOptions]::Share) { if (Get-DCShare) { Write-PSFMessage -Level Host -String 'Test-AdmfDC.Executing.Test' -StringValues 'Shares', $parameters.Server Test-DCShare @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDC.Skipping.Test.NoConfiguration' -StringValues 'Shares' } } if ($newOptions -band [UpdateDCOptions]::FSAccessRule) { if (Get-DCAccessRule) { Write-PSFMessage -Level Host -String 'Test-AdmfDC.Executing.Test' -StringValues 'FSAccessRules', $parameters.Server Test-DCAccessRule @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDC.Skipping.Test.NoConfiguration' -StringValues 'FSAccessRules' } } } catch { throw } finally { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet } } } function Test-AdmfDomain { <# .SYNOPSIS Tests a domain for its domain level content and whether it matches the desired state. .DESCRIPTION Tests a domain for its domain level content and whether it matches the desired state. Executes a large battery of tests from the DomainManagement module. The desired state is defined using configuration files, which the module handles for the user. .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .PARAMETER Options Which scan to execute. By default, all tests are run, but it is possibly to selectively choose which to run. .PARAMETER CredentialProvider The credential provider to use to resolve the input credentials. See help on Register-AdmfCredentialProvider for details. .EXAMPLE PS C:\> Test-AdmfDomain -Server corp.fabrikam.com Scans the domain corp.fabrikam.com for compliance with the desired state. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] [CmdletBinding()] Param ( [PSFComputer] $Server, [PSCredential] $Credential, [UpdateDomainOptions[]] $Options = 'All', [string] $CredentialProvider = 'default' ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $originalArgument = Invoke-PreCredentialProvider @parameters -ProviderName $CredentialProvider -Parameter $parameters -Cmdlet $PSCmdlet try { $parameters.Server = Resolve-DomainController @parameters -ErrorAction Stop -Confirm:$false } catch { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet throw } Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet Set-AdmfContext @parameters -Interactive -ReUse -EnableException [UpdateDomainOptions]$newOptions = $Options } process { try { if (($newOptions -band [UpdateDomainOptions]::OUSoft) -or ($newOptions -band [UpdateDomainOptions]::OUHard)) { if (Get-DMOrganizationalUnit) { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'OrganizationalUnits', $parameters.Server Test-DMOrganizationalUnit @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'OrganizationalUnits' } } if ($newOptions -band [UpdateDomainOptions]::Group) { if (Get-DMGroup) { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'Groups', $parameters.Server Test-DMGroup @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Groups' } } if ($newOptions -band [UpdateDomainOptions]::User) { if (Get-DMUser) { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'Users', $parameters.Server Test-DMUser @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Users' } } if ($newOptions -band [UpdateDomainOptions]::GroupMembership) { if (Get-DMGroupMembership) { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'GroupMembership', $parameters.Server Test-DMGroupMembership @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupMembership' } } if ($newOptions -band [UpdateDomainOptions]::Acl) { if (Get-DMAcl) { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'Acls', $parameters.Server Test-DMAcl @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Acls' } } if ($newOptions -band [UpdateDomainOptions]::AccessRule) { if (Get-DMAccessRule) { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'AccessRules', $parameters.Server Test-DMAccessRule @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'AccessRules' } } if ($newOptions -band [UpdateDomainOptions]::PSO) { if (Get-DMPasswordPolicy) { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'PasswordPolicies', $parameters.Server Test-DMPasswordPolicy @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'PasswordPolicies' } } if ($newOptions -band [UpdateDomainOptions]::Object) { if (Get-DMObject) { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'Object', $parameters.Server Test-DMObject @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'Object' } } if (($newOptions -band [UpdateDomainOptions]::GroupPolicy) -or ($newOptions -band [UpdateDomainOptions]::GroupPolicyDelete)) { if (Get-DMGroupPolicy) { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'GroupPolicies', $parameters.Server Test-DMGroupPolicy @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicies' } } if ($newOptions -band [UpdateDomainOptions]::GPPermission) { if (Get-DMGPPermission) { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'GroupPolicyPermissions', $parameters.Server Test-DMGPPermission @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicyPermissions' } } if (($newOptions -band [UpdateDomainOptions]::GPLink) -or ($newOptions -band [UpdateDomainOptions]::GPLinkDisable)) { if (Get-DMGPLink) { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Executing.Test' -StringValues 'GroupPolicyLinks', $parameters.Server Test-DMGPLink @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfDomain.Skipping.Test.NoConfiguration' -StringValues 'GroupPolicyLinks' } } } catch { throw } finally { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet } } } function Test-AdmfForest { <# .SYNOPSIS Tests whether a forest is configured according to baseline configuration .DESCRIPTION Tests whether a forest is configured according to baseline configuration .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .PARAMETER Options What tests to execute. Defaults to all tests. .PARAMETER CredentialProvider The credential provider to use to resolve the input credentials. See help on Register-AdmfCredentialProvider for details. .EXAMPLE PS C:\> Test-AdmfForest Test the current forest for baseline compliance. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] [CmdletBinding()] Param ( [PSFComputer] $Server, [PSCredential] $Credential, [UpdateForestOptions[]] $Options = 'All', [string] $CredentialProvider = 'default' ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $originalArgument = Invoke-PreCredentialProvider @parameters -ProviderName $CredentialProvider -Parameter $parameters -Cmdlet $PSCmdlet try { $parameters.Server = Resolve-DomainController @parameters -ErrorAction Stop -Confirm:$false } catch { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet throw } Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet Set-AdmfContext @parameters -Interactive -ReUse -EnableException [UpdateForestOptions]$newOptions = $Options } process { try { if ($newOptions -band [UpdateForestOptions]::Sites) { if (Get-FMSite) { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'Sites', $parameters.Server Test-FMSite @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Sites' } } if ($newOptions -band [UpdateForestOptions]::SiteLinks) { if (Get-FMSiteLink) { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'Sitelinks', $parameters.Server Test-FMSiteLink @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Sitelinks' } } if ($newOptions -band [UpdateForestOptions]::Subnets) { if (Get-FMSubnet) { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'Subnets', $parameters.Server Test-FMSubnet @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Subnets' } } if ($newOptions -band [UpdateForestOptions]::ServerRelocate) { # Requires no configuration, so no check for configuration existence required Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'Server Site Assignment', $parameters.Server Test-FMServer @parameters } if ($newOptions -band [UpdateForestOptions]::Schema) { if (Get-FMSchema) { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'Schema (Custom)', $parameters.Server Test-FMSchema @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Schema (Custom)' } } if ($newOptions -band [UpdateForestOptions]::SchemaLdif) { if (Get-FMSchemaLdif) { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'Schema (Ldif)', $parameters.Server Test-FMSchemaLdif @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'Schema (Ldif)' } } if ($newOptions -band [UpdateForestOptions]::NTAuthStore) { if (Get-FMNTAuthStore) { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Executing.Test' -StringValues 'NTAuthStore', $parameters.Server Test-FMNTAuthStore @parameters } else { Write-PSFMessage -Level Host -String 'Test-AdmfForest.Skipping.Test.NoConfiguration' -StringValues 'NTAuthStore' } } } catch { throw } finally { Invoke-PostCredentialProvider -ProviderName $CredentialProvider -Server $originalArgument.Server -Credential $originalArgument.Credential -Cmdlet $PSCmdlet } } } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'ADMF' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> Set-PSFConfig -Module 'ADMF' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'ADMF' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." Set-PSFConfig -Module 'ADMF' -Name 'DCSelectionMode' -Value 'PDCEmulator' -Initialize -Validation 'string' -Description 'When executing commands, specifying the domain name will cause the module to resolve to a single DC to work against. This setting governs the algorythm that determines the DC to work against. Either "PDCEmulator" or "Random" are valid choices.' Set-PSFConfig -Module 'ADMF' -Name 'VerboseExecution' -Value $true -Initialize -Validation bool -Handler { if ($args[0]) { $null = New-PSFMessageLevelModifier -Name ADMF_Verbose -Modifier 0 -IncludeModuleName ADMF } else { $null = New-PSFMessageLevelModifier -Name ADMF_Verbose -Modifier 3 -IncludeModuleName ADMF } } -Description 'Enabling this will cause the ADMF module to be more verbose by default' Set-PSFConfig -Module 'ADMF' -Name 'Context.Store.Default' -Value "$(Get-PSFPath -Name AppData)\ADMF\Contexts" -Initialize -Validation string -Description 'The default path in which ADMF will look for configuration contexts. Add additional such paths by declaring additional settings labeled "ADMF.Context.Store.*"' Set-PSFConfig -Module 'ADMF' -Name 'DCInstall.Context.Prompt.Enable' -Value $true -Initialize -Validation 'bool' -Description "Whether the DC installation commands should generate Context selection prompts." <# Stored scriptblocks are available in [PsfValidateScript()] attributes. This makes it easier to centrally provide the same scriptblock multiple times, without having to maintain it in separate locations. It also prevents lengthy validation scriptblocks from making your parameter block hard to read. Set-PSFScriptblock -Name 'ADMF.ScriptBlockName' -Scriptblock { } #> Set-PSFScriptblock -Name 'ADMF.Validate.Type.Gpo' -Scriptblock { foreach ($item in $_) { if (-not ($item -is [Microsoft.GroupPolicy.Gpo])) { return $false } } $true } Set-PSFScriptblock -Name 'ADMF.Validate.Path' -Scriptblock { Test-Path -Path $_ } Set-PSFScriptblock -Name 'ADMF.Validate.Path.Folder' -Scriptblock { $resolvedPath = Resolve-PSFPath -Provider FileSystem -Path $_ -SingleItem Test-Path -Path $resolvedPath -PathType Container } Set-PSFScriptblock -Name 'ADMF.Validate.ContextStore.ExistsNot' -Scriptblock { $_ -notin (Get-AdmfContextStore).Name } <# # Example: Register-PSFTeppScriptblock -Name "ADMF.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } #> Register-PSFTeppScriptblock -Name 'ADMF.Context.Store' -ScriptBlock { (Get-AdmfContextStore).Name } Register-PSFTeppScriptblock -Name 'ADMF.CredentialProvider' -ScriptBlock { $module = Get-Module ADMF if (-not $module) { return } & $module { $script:credentialProviders.Keys } } Register-PSFTeppArgumentCompleter -Command Test-AdmfDomain -Parameter CredentialProvider -Name 'ADMF.CredentialProvider' Register-PSFTeppArgumentCompleter -Command Invoke-AdmfDomain -Parameter CredentialProvider -Name 'ADMF.CredentialProvider' Register-PSFTeppArgumentCompleter -Command Test-AdmfForest -Parameter CredentialProvider -Name 'ADMF.CredentialProvider' Register-PSFTeppArgumentCompleter -Command Invoke-AdmfForest -Parameter CredentialProvider -Name 'ADMF.CredentialProvider' <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name ADMF.alcohol #> Register-PSFTeppArgumentCompleter -Command New-AdmfContext -Parameter Store -Name 'ADMF.Context.Store' New-PSFLicense -Product 'ADMF' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2019-12-20") -Text @" Copyright (c) 2019 Friedrich Weinmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ # The list of currently applied context sets $script:loadedContexts = @() # The list of contexts per domain/server $script:assignedContexts = @{ } # The list of registered credentials providers $script:credentialProviders = @{ } $callbackScript = { [CmdletBinding()] param ( [AllowNull()] $Server, [AllowNull()] $Credential, [AllowNull()] $ForestObject, [AllowNull()] $DomainObject ) $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential if ($parameters.Server -eq '<Default Domain>') { $parameters.Server = $env:USERDNSDOMAIN } Set-AdmfContext @parameters -Interactive -ReUse -EnableException } Register-DMCallback -Name ADMF -ScriptBlock $callbackScript Register-FMCallback -Name ADMF -ScriptBlock $callbackScript $callbackScript2 = { [CmdletBinding()] param ( [Hashtable] $Data ) # If this is a DC Installation command from DC Management and we disabled the prompt in configuration, stop if ($Data.Data.IsDCInstall -and -not (Get-PSFConfigValue -FullName 'ADMF.DCInstall.Context.Prompt.Enable')) { return } $parameters = $Data.Data | ConvertTo-PSFHashtable -Include Server, Credential if ($parameters.Server -eq '<Default Domain>') { $parameters.Server = $env:USERDNSDOMAIN } if (-not $parameters.Server) { $parameters.Server = $env:USERDNSDOMAIN } Set-AdmfContext @parameters -Interactive -ReUse -EnableException -NoDomain:($Data.Data.IsDCInstall -as [bool]) } Register-PSFCallback -Name 'ADMF.ContextPrompt' -ModuleName DCManagement -CommandName '*' -ScriptBlock $callbackScript2 Set-PSFTypeAlias -Mapping @{ 'UpdateDCOptions' = 'ADMF.UpdateDCOptions' 'UpdateDomainOptions' = 'ADMF.UpdateDomainOptions' 'UpdateForestOptions' = 'ADMF.UpdateForestOptions' } Register-AdmfCredentialProvider -Name default -PreScript { param ( $Data ) $Data.Credential } #endregion Load compiled code |