DCManagement.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\DCManagement.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName DCManagement.Import.DoDotSource -Fallback $false if ($DCManagement_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 DCManagement.Import.IndividualFiles -Fallback $false if ($DCManagement_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 'DCManagement' -Language 'en-US' function Assert-ADConnection { <# .SYNOPSIS Ensures connection to AD is possible before performing actions. .DESCRIPTION Ensures connection to AD is possible before performing actions. Should be the first things all commands connecting to AD should call. Do this before invoking callbacks, as the configuration change becomes pointless if the forest is unavailable to begin with, .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .PARAMETER Cmdlet The $PSCmdlet variable of the calling command. Used to safely terminate the calling command in case of failure. .EXAMPLE PS C:\> Assert-ADConnection @parameters -Cmdlet $PSCmdlet Kills the calling command if AD is not available. #> [CmdletBinding()] Param ( [PSFComputer] $Server, [PSCredential] $Credential, [Parameter(Mandatory = $true)] [System.Management.Automation.PSCmdlet] $Cmdlet ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential } process { # A domain being unable to retrieve its own object can really only happen if the service is down try { $null = Get-ADDomain @parameters -ErrorAction Stop } catch { Write-PSFMessage -Level Warning -String 'Assert-ADConnection.Failed' -StringValues $Server -Tag 'failed' -ErrorRecord $_ $Cmdlet.ThrowTerminatingError($_) } } } function Assert-Configuration { <# .SYNOPSIS Ensures a set of configuration settings has been provided for the specified setting type. .DESCRIPTION Ensures a set of configuration settings has been provided for the specified setting type. This maps to the configuration variables defined in variables.ps1 Note: Not ALL variables defined in that file should be mapped, only those storing individual configuration settings! .PARAMETER Type The setting type to assert. .PARAMETER Cmdlet The $PSCmdlet variable of the calling command. Used to safely terminate the calling command in case of failure. .EXAMPLE PS C:\> Assert-Configuration -Type Users Asserts, that users have already been specified. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [string] $Type, [Parameter(Mandatory = $true)] [System.Management.Automation.PSCmdlet] $Cmdlet ) process { if ((Get-Variable -Name $Type -Scope Script -ValueOnly -ErrorAction Ignore).Count -gt 0) { return } Write-PSFMessage -Level Warning -String 'Assert-Configuration.NotConfigured' -StringValues $Type -FunctionName $Cmdlet.CommandRuntime $exception = New-Object System.Data.DataException("No configuration data provided for: $Type") $errorID = 'NotConfigured' $category = [System.Management.Automation.ErrorCategory]::NotSpecified $recordObject = New-Object System.Management.Automation.ErrorRecord($exception, $errorID, $category, $Type) $cmdlet.ThrowTerminatingError($recordObject) } } function Compare-Property { <# .SYNOPSIS Helper function simplifying the changes processing. .DESCRIPTION Helper function simplifying the changes processing. .PARAMETER Property The property to use for comparison. .PARAMETER Configuration The object that was used to define the desired state. .PARAMETER ADObject The AD Object containing the actual state. .PARAMETER Changes An arraylist where changes get added to. The content of -Property will be added if the comparison fails. .PARAMETER Resolve Whether the value on the configured object's property should be string-resolved. .PARAMETER ADProperty The property on the ad object to use for the comparison. If this parameter is not specified, it uses the value from -Property. .EXAMPLE PS C:\> Compare-Property -Property Description -Configuration $ouDefinition -ADObject $adObject -Changes $changes -Resolve Compares the description on the configuration object (after resolving it) with the one on the ADObject and adds to $changes if they are inequal. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Property, [Parameter(Mandatory = $true)] [object] $Configuration, [Parameter(Mandatory = $true)] [object] $ADObject, [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [System.Collections.ArrayList] $Changes, [switch] $Resolve, [string] $ADProperty ) begin { if (-not $ADProperty) { $ADProperty = $Property } } process { $propValue = $Configuration.$Property if ($Resolve) { $propValue = $propValue | Resolve-String } if (($propValue -is [System.Collections.ICollection]) -and ($ADObject.$ADProperty -is [System.Collections.ICollection])) { if (Compare-Object $propValue $ADObject.$ADProperty) { $null = $Changes.Add($Property) } } elseif ($propValue -ne $ADObject.$ADProperty) { $null = $Changes.Add($Property) } } } function Get-DomainController { <# .SYNOPSIS Returns a list of domain controllers with their respective FSMO state. .DESCRIPTION Returns a list of domain controllers with their respective FSMO state. .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .EXAMPLE PS C:\> Get-DomainController List all DCs of the current domain with their respective FSMO membership #> [CmdletBinding()] Param ( [PSFComputer] $Server, [pscredential] $Credential ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential } process { $forest = Get-ADForest @parameters $domain = Get-ADDomain @parameters $fsmo = $forest.DomainNamingMaster, $forest.SchemaMaster, $domain.PDCEmulator, $domain.InfrastructureMaster, $domain.RIDMaster $domainControllers = Get-ADComputer @parameters -LdapFilter '(primaryGroupID=516)' foreach ($controller in $domainControllers) { [PSCustomObject]@{ Name = $controller.DNSHostName IsFSMO = $controller.DNSHostName -in $fsmo IsPDCEmulator = $domain.PDCEmulator -eq $controller.DNSHostName IsDomainNamingMaster = $forest.DomainNamingMaster -eq $controller.DNSHostName IsSchemaMaster = $forest.SchemaMaster -eq $controller.DNSHostName IsInfrastructureMaster = $domain.InfrastructureMaster -eq $controller.DNSHostName IsRIDMaster = $domain.RIDMaster -eq $controller.DNSHostName } } } } function Grant-ShareAccess { <# .SYNOPSIS Grants access to a network share. .DESCRIPTION Grants access to a network share. User must be specified as a SID. This command uses PowerShell remoting to access the target computer. .PARAMETER ComputerName The name of the server to operate against. .PARAMETER Credential The credentials to use for authentication. .PARAMETER Name The name of the share to modifiy. .PARAMETER Identity The SID of the user to grant permissions to. .PARAMETER AccessRight The rights of the user that has permissions granted. .EXAMPLE PS C:\> Grant-ShareAccess @parameters -Name Legal -Identity S-1-5-21-584015949-955715703-1113067636-1105 -AccessRight Full Grants the specified user full access right to the share "Legal" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWMICmdlet", "")] [CmdletBinding()] param ( [PSFComputer] $ComputerName, [PSCredential] $Credential, [string] $Name, [string] $Identity, [ValidateSet('Full', 'Change', 'Read')] [string] $AccessRight ) begin { #region Permission Grant Scriptblock $scriptblock = { param ( [Hashtable] $Data ) function Write-Result { [CmdletBinding()] param ( [switch] $Failed, [string] $State, [string] $Message ) [pscustomobject]@{ Success = (-not $Failed) State = $State Message = $Message } } $accessHash = @{ Full = 2032127 Change = 1245631 Read = 1179817 } $sid = [System.Security.Principal.SecurityIdentifier]$Data.Identity [byte[]]$sidBytes = New-Object System.Byte[]($sid.BinaryLength) $sid.GetBinaryForm($sidBytes, 0) $trustee = (New-Object System.Management.ManagementClass('Win32_Trustee')).CreateInstance() $trustee.SID = $sidBytes $trustee.SidLength = $sid.BinaryLength $trustee.SIDString = $sid.Value $aceObject = (New-Object System.Management.ManagementClass('Win32_ACE')).CreateInstance() $aceObject.AceFlags = 0 $aceObject.AceType = 0 $aceObject.AccessMask = $accessHash[$Data.AccessRight] $aceObject.Trustee = $trustee try { $securitySettings = Get-WmiObject -Query ('SELECT * FROM Win32_LogicalShareSecuritySetting WHERE Name = "{0}"' -f $Data.Name) -ErrorAction Stop } catch { return Write-Result -Failed -State WMIAccess -Message $_ } $securityDescriptor = $securitySettings.GetSecurityDescriptor().Descriptor [System.Management.ManagementBaseObject[]]$accessControlList = $securityDescriptor.DACL if (-not $accessControlList) { $accessControlList = New-Object System.Management.ManagementBaseObject[](1) } else { [array]::Resize([ref]$accessControlList, ($accessControlList.Length + 1)) } $accessControlList[-1] = $aceObject $securityDescriptor.DACL = $accessControlList $result = $securitySettings.SetSecurityDescriptor($securityDescriptor) if ($result.ReturnValue -ne 0) { Write-Result -Failed -State 'FailedApply' -Message "Failed to apply with WMI code $($result.ReturnValue)" } else { Write-Result -State Success -Message 'Permissions successfully applied' } } #endregion Permission Grant Scriptblock $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include ComputerName, Credential } process { $data = $PSBoundParameters | ConvertTo-PSFHashtable -Include Name, Identity, AccessRight try { $results = Invoke-PSFCommand @parameters -ScriptBlock $scriptblock -ErrorAction Stop -ArgumentList $data } catch { Stop-PSFFunction -String 'Grant-ShareAccess.WinRM.Failed' -StringValues $Identity, $Name, $ComputerName -EnableException $true -ErrorRecord $_ -Target $ComputerName -Cmdlet $PSCmdlet } if (-not $results.Success) { Stop-PSFFunction -String 'Grant-ShareAccess.Execution.Failed' -StringValues $Identity, $Name, $ComputerName, $results.Status, $results.Message -EnableException $true -ErrorRecord $_ -Target $ComputerName -Cmdlet $PSCmdlet } } } function New-Password { <# .SYNOPSIS Generate a new, complex password. .DESCRIPTION Generate a new, complex password. .PARAMETER Length The length of the password calculated. Defaults to 32 .PARAMETER AsSecureString Returns the password as secure string. .EXAMPLE PS C:\> New-Password Generates a new 32v character password. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] [CmdletBinding()] Param ( [int] $Length = 32, [switch] $AsSecureString ) begin { $characters = @{ 0 = @('A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z') 1 = @('a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z') 2 = @(0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9) 3 = @('#','$','%','&',"'",'(',')','*','+',',','-','.','/',':',';','<','=','>','?','@') 4 = @('A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z') 5 = @('a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z') 6 = @(0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9) 7 = @('#','$','%','&',"'",'(',')','*','+',',','-','.','/',':',';','<','=','>','?','@') } } process { $letters = foreach ($number in (1..$Length)) { $characters[(($number % 4) + (1..4 | Get-Random))] | Get-Random } if ($AsSecureString) { $letters -join "" | ConvertTo-SecureString -AsPlainText -Force } else { $letters -join "" } } } function New-TestResult { <# .SYNOPSIS Generates a new test result object. .DESCRIPTION Generates a new test result object. Helper function that slims down the Test- commands. .PARAMETER ObjectType What kind of object is being processed (e.g.: User, OrganizationalUnit, Group, ...) .PARAMETER Type What kind of change needs to be performed .PARAMETER Identity Identity of the change item .PARAMETER Changed What properties - if any - need to be changed .PARAMETER Server The server the test was performed against .PARAMETER Configuration The configuration object containing the desired state. .PARAMETER ADObject The AD Object(s) containing the actual state. .EXAMPLE PS C:\> New-TestResult -ObjectType User -Type Changed -Identity $resolvedDN -Changed Description -Server $Server -Configuration $userDefinition -ADObject $adObject Creates a new test result object using the specified information. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $ObjectType, [Parameter(Mandatory = $true)] [string] $Type, [Parameter(Mandatory = $true)] [string] $Identity, [object[]] $Changed, [Parameter(Mandatory = $true)] [AllowNull()] [PSFComputer] $Server, $Configuration, $ADObject ) process { $object = [PSCustomObject]@{ PSTypeName = "DCManagement.$ObjectType.TestResult" Type = $Type ObjectType = $ObjectType Identity = $Identity Changed = $Changed Server = $Server Configuration = $Configuration ADObject = $ADObject } Add-Member -InputObject $object -MemberType ScriptMethod -Name ToString -Value { $this.Identity } -Force $object } } function Resolve-ParameterValue { <# .SYNOPSIS Resolves parameter values, defaulting to configured values. .DESCRIPTION Resolves parameter values, defaulting to configured values. .PARAMETER InputObject The object passed by the user. .PARAMETER FullName The name of the configuration. .EXAMPLE PS C:\> Resolve-ParameterValue -FullName 'DCManagement.Defaults.NoDNS' -InputObject $NoDNS Resolves the configuration for NoDNS: - If it was specified by the user, use $NoDNS variable value - If it was not, use the 'DCManagement.Defaults.NoDNS' configuration setting #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [AllowNull()] [PSObject] $InputObject, [Parameter(Mandatory = $true)] [string] $FullName ) process { if ($null -ne $InputObject -and '' -ne $InputObject -and $InputObject -isnot [switch]) { return $InputObject } if ($InputObject -is [switch] -and $InputObject.IsPresent) { return $InputObject } Get-PSFConfigValue -FullName $FullName } } function Revoke-ShareAccess { <# .SYNOPSIS Removes a specific share permission from the specified share. .DESCRIPTION Removes a specific share permission from the specified share. Requires user SID and permission match. This command uses PowerShell remoting to access the target computer. .PARAMETER ComputerName The name of the server to operate against. .PARAMETER Credential The credentials to use for authentication. .PARAMETER Name The name of the share to modifiy. .PARAMETER Identity The SID of the user to revoke permissions for. .PARAMETER AccessRight The rights of the user that has permissions revoked. .EXAMPLE PS C:\> Revoke-ShareAccess @parameters -Name Legal -Identity S-1-5-21-584015949-955715703-1113067636-1105 -AccessRight Full Revokes the specified user's full access right to the share "Legal" #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWMICmdlet", "")] [CmdletBinding()] Param ( [PSFComputer] $ComputerName, [PSCredential] $Credential, [string] $Name, [string] $Identity, [ValidateSet('Full', 'Change', 'Read')] [string] $AccessRight ) begin { #region Permission Revocation Scriptblock $scriptblock = { param ( [Hashtable] $Data ) function Write-Result { [CmdletBinding()] param ( [switch] $Failed, [string] $State, [string] $Message ) [pscustomobject]@{ Success = (-not $Failed) State = $State Message = $Message } } $accessHash = @{ Full = 2032127 Change = 1245631 Read = 1179817 } try { $securitySettings = Get-WmiObject -Query ('SELECT * FROM Win32_LogicalShareSecuritySetting WHERE Name = "{0}"' -f $Data.Name) -ErrorAction Stop } catch { return Write-Result -Failed -State WMIAccess -Message $_ } $securityDescriptor = $securitySettings.GetSecurityDescriptor().Descriptor $securityDescriptor.DACL = [System.Management.ManagementBaseObject[]]($securityDescriptor.DACL | Where-Object { -not ( $_.Trustee.SIDString -eq $Data.Identity -and $_.AccessMask -eq $accessHash[$Data.AccessRight] ) }) $result = $securitySettings.SetSecurityDescriptor($securityDescriptor) if ($result.ReturnValue -ne 0) { Write-Result -Failed -State 'FailedApply' -Message "Failed to apply with WMI code $($result.ReturnValue)" } else { Write-Result -State Success -Message 'Permissions successfully revoked' } } #endregion Permission Revocation Scriptblock $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include ComputerName, Credential } process { $data = $PSBoundParameters | ConvertTo-PSFHashtable -Include Name, Identity, AccessRight try { $results = Invoke-PSFCommand @parameters -ScriptBlock $scriptblock -ErrorAction Stop -ArgumentList $data } catch { Stop-PSFFunction -String 'Revoke-ShareAccess.WinRM.Failed' -StringValues $Identity, $Name, $ComputerName -EnableException $true -ErrorRecord $_ -Target $ComputerName -Cmdlet $PSCmdlet } if (-not $results.Success) { Stop-PSFFunction -String 'Revoke-ShareAccess.Execution.Failed' -StringValues $Identity, $Name, $ComputerName, $results.Status, $results.Message -EnableException $true -ErrorRecord $_ -Target $ComputerName -Cmdlet $PSCmdlet } } } function Get-DCAccessRule { <# .SYNOPSIS Returns the list of registered filesystem access rules. .DESCRIPTION Returns the list of registered filesystem access rules. .PARAMETER Path Filter by the path it is assigned to. Defaults to: '*' .PARAMETER Identity Filter by the Identity granted permissions to. Default to: '*' .EXAMPLE PS C:\> Get-DCAccessRule Returns the list of all registered filesystem access rules. #> [CmdletBinding()] Param ( [string] $Path = '*', [string] $Identity = '*' ) process { ($script:fileSystemAccessRules.Values.Values | Where-Object Path -Like $Path | Where-Object Identity -Like $Identity) } } function Invoke-DCAccessRule { <# .SYNOPSIS Applies the desired state for filesystem permissions on paths on relevant DCs. .DESCRIPTION Applies the desired state for filesystem permissions on paths on relevant DCs. Use Register-DCAccessRule to define the desired state. Use Test-DCAccessRule to test, what should be changed. By default, all pending access rule changes will be applied, specify the explicit test results you want to process to override this. .PARAMETER InputObject The specific test results produced by Test-DCAccessRule to apply. If you do not specify this parameter, ALL pending changes will be performed! .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .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. .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-DCAccessRule -Server corp.contoso.com Brings all DCs of the corp.contoso.com domain into their desired state as far as filesystem Access Rules are concerned. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseUsingScopeModifierInNewRunspaces", "")] [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(ValueFromPipeline = $true)] $InputObject, [PSFComputer] $Server, [PSCredential] $Credential, [switch] $EnableException ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $parameters['Debug'] = $false Assert-ADConnection @parameters -Cmdlet $PSCmdlet Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet Assert-Configuration -Type fileSystemAccessRules -Cmdlet $PSCmdlet Set-DCDomainContext @parameters $psCred = $PSBoundParameters | ConvertTo-PSFHashtable -Include Credential $psSessions = @{ } #region Functions function Add-AccessRule { [CmdletBinding()] param ( $Session, [string] $Path, $AccessRule ) $result = Invoke-Command -Session $Session -ScriptBlock { $referenceRule = $using:AccessRule try { $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( ([System.Security.Principal.SecurityIdentifier]$referenceRule.IdentityReference.ToString()), $referenceRule.FileSystemRights, $referenceRule.InheritanceFlags, $referenceRule.PropagationFlags, $referenceRule.AccessControlType ) $acl = Get-Acl -Path $using:Path -ErrorAction Stop $acl.AddAccessRule($rule) $acl | Set-Acl -Path $using:Path -ErrorAction Stop -Confirm:$false [PSCustomObject]@{ Success = $true Path = $using:Path Rule = $referenceRule Error = $null } } catch { [PSCustomObject]@{ Success = $false Path = $using:Path Rule = $referenceRule Error = $_ } } } if (-not $result.Success) { throw "Error: $($result.Error)" } } function Remove-AccessRule { [CmdletBinding()] param ( $Session, [string] $Path, $AccessRule ) $result = Invoke-Command -Session $Session -ScriptBlock { function Convert-UintToInt([uint32]$Number) { [System.BitConverter]::ToInt32(([System.BitConverter]::GetBytes($Number)), 0) } try { $referenceRule = $using:AccessRule $acl = Get-Acl -Path $using:Path -ErrorAction Stop foreach ($rule in $acl.Access) { if ($rule.IsInherited) { continue } if ($rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).ToString() -ne $referenceRule.IdentityReference.ToString()) { continue } if ([int]$rule.FileSystemRights -ne (Convert-UintToInt -Number $referenceRule.FileSystemRightsNumeric)) { continue } if ($rule.InheritanceFlags -ne $referenceRule.InheritanceFlags) { continue } if ($rule.PropagationFlags -ne $referenceRule.PropagationFlags) { continue } if ($rule.AccessControlType -ne $referenceRule.AccessControlType) { continue } $null = $acl.RemoveAccessRule($rule) } $acl | Set-Acl -Path $using:Path -ErrorAction Stop -Confirm:$false [PSCustomObject]@{ Success = $true Path = $using:Path Rule = $referenceRule Error = $null } } catch { [PSCustomObject]@{ Success = $false Path = $using:Path Rule = $using:AccessRule Error = $_ } } } if (-not $result.Success) { throw "Error: $($result.Error)" } } #endregion Functions } process { if (-not $InputObject) { $InputObject = Test-DCAccessRule @parameters } foreach ($testItem in ($InputObject | Sort-Object Type -Descending)) # Delete before Add { # Catch invalid input - can only process test results if ($testItem.PSObject.TypeNames -notcontains 'DCManagement.FSAccessRule.TestResult') { Stop-PSFFunction -String 'General.Invalid.Input' -StringValues 'Test-DCAccessRule', $testItem -Target $testItem -Continue -EnableException $EnableException } if (-not $psSessions[$testItem.Server]) { try { $psSessions[$testItem.Server] = New-PSSession -ComputerName $testItem.Server @psCred -ErrorAction Stop } catch { Stop-PSFFunction -String 'Invoke-DCAccessRule.Access.Error' -StringValues $testItem.Server -Target $testItem -Continue -EnableException $EnableException -ErrorRecord $_ } } $psSession = $psSessions[$testItem.Server] switch ($testItem.Type) { #region Add 'Add' { $change = @($testItem.Changed)[0] Invoke-PSFProtectedCommand -ActionString 'Invoke-DCAccessRule.AccessRule.Add' -ActionStringValues $change.DisplayName, $change.FileSystemRights, $change.AccessControlType -Target $testItem -ScriptBlock { Add-AccessRule -Session $psSession -Path $testItem.Identity -AccessRule $change } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue } #endregion Add #region Remove 'Remove' { $change = @($testItem.Changed)[0] Invoke-PSFProtectedCommand -ActionString 'Invoke-DCAccessRule.AccessRule.Remove' -ActionStringValues $change.DisplayName, $change.FileSystemRights, $change.AccessControlType -Target $testItem -ScriptBlock { Remove-AccessRule -Session $psSession -Path $testItem.Identity -AccessRule $change } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue } #endregion Remove } } } end { $psSessions.Values | Remove-PSSession -Confirm:$false -ErrorAction Ignore } } function Register-DCAccessRule { <# .SYNOPSIS Registers an access rule for FileSystem paths on a domain controller. .DESCRIPTION Registers an access rule for FileSystem paths on a domain controller. .PARAMETER Path The path to the filesystem object to grant permissions on. Supports string resolution. .PARAMETER Identity What identity / principal to grant access. Supports string resolution. .PARAMETER Rights What file system right to grant. .PARAMETER Type Whether this is an allow or a deny rule. Defaults to Allow. .PARAMETER Inheritance Who and how are access rules inherited. Defaults to 'ContainerInherit, ObjectInherit', meaning everything beneath the path inherits as well. .PARAMETER Propagation How access rules are being propagated. Defaults to "None", the windows default behavior. .PARAMETER Empty This path should have no explicit ACE defined. .PARAMETER AccessMode How filesystem access rules are processed. Supports three configurations: - Constrained: The default access mode, will remove any excess access rules. - Additive: Ignore any access rules already on the path, even if not configured - Defined: Ignore any access rules already on the path, even if not configured UNLESS the identity on those rules has an access level defined for it. .PARAMETER ServerRole What domain controller to apply this to: - All: All DCs in the enterprise - FSMO: Only DCs that have any FSMO role - PDC: Only the PDCEmulator .PARAMETER ContextName The name of the context defining the setting. This allows determining the configuration set that provided this setting. Used by the ADMF, available to any other configuration management solution. .EXAMPLE PS C:\> Get-Content .\accessrules.json | ConvertFrom-Json | Write-Output | Register-DCAccessRule Reads all access rule definitions from json and imports the definitions. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Path, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ACE')] [string] $Identity, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ACE')] [System.Security.AccessControl.FileSystemRights] $Rights, [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ACE')] [System.Security.AccessControl.AccessControlType] $Type = 'Allow', [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ACE')] [System.Security.AccessControl.InheritanceFlags] $Inheritance = 'ContainerInherit, ObjectInherit', [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ACE')] [System.Security.AccessControl.PropagationFlags] $Propagation = 'None', [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Empty')] [bool] $Empty, [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ACE')] [ValidateSet('Constrained', 'Additive', 'Defined')] [string] $AccessMode = 'Constrained', [Parameter(ValueFromPipelineByPropertyName = $true)] [ValidateSet('All', 'FSMO', 'PDC')] [string] $ServerRole = 'All', [string] $ContextName = '<Undefined>' ) process { if (-not $script:fileSystemAccessRules[$Path]) { $script:fileSystemAccessRules[$Path] = @{ } } $script:fileSystemAccessRules[$Path]["$($Identity)þ$($ServerRole)þ$($Rights)þ$($Type)þ$($Inheritance)þ$($Propagation)"] = [pscustomobject]@{ PSTypeName = 'DCManagement.AccessRule' Path = $Path Identity = $Identity Rights = $Rights Type = $Type Inheritance = $Inheritance Propagation = $Propagation AccessMode = $AccessMode ServerRole = $ServerRole Empty = $Empty ContextName = $ContextName } } } function Test-DCAccessRule { <# .SYNOPSIS Tests all DCs, whether their NTFS filesystem Access Rules are configured as designed. .DESCRIPTION Tests all DCs, whether their NTFS filesystem Access Rules are configured as designed. This test ONLY considers paths, that are configured. In opposite to the DomainManagement AccessRule Component there is no system that considers part of the DCs filesystem as "under management". .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .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:\> Test-DCAccessRule -Server corp.contoso.com Tests, whether the filesystem Access Rules on all DCs of the corp.contoso.com domain are configured as designed. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseUsingScopeModifierInNewRunspaces", "")] [CmdletBinding()] param ( [PSFComputer] $Server, [PSCredential] $Credential, [switch] $EnableException ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $parameters['Debug'] = $false Assert-ADConnection @parameters -Cmdlet $PSCmdlet Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet Assert-Configuration -Type fileSystemAccessRules -Cmdlet $PSCmdlet Set-DCDomainContext @parameters $domainControllers = Get-DomainController @parameters $psCred = $PSBoundParameters | ConvertTo-PSFHashtable -Include Credential #region Utility Functions function ConvertFrom-AccessRuleDefinition { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $InputObject, [hashtable] $Parameters ) process { $resolvedPath = Resolve-String -Text $InputObject.Path -ArgumentList $Parameters if ($InputObject.Empty) { [PSCustomObject]@{ Path = $resolvedPath Identity = $null Principal = $null AccessRule = $null Configuration = $InputObject IdentityError = $false ServerRole = $InputObject.ServerRole AccessMode = 'Constrained' Empty = $true } return } $resolvedIdentity = Resolve-String -Text $InputObject.Identity -ArgumentList $Parameters $identityError = $false try { $resolvedPrincipal = Resolve-Principal @Parameters -Name $resolvedIdentity -OutputType SID -ErrorAction Stop } catch { $identityError = $true $resolvedPrincipal = [System.Security.Principal.NTAccount]$resolvedIdentity } $rule = [System.Security.AccessControl.FileSystemAccessRule]::new($resolvedPrincipal, $InputObject.Rights, $InputObject.Inheritance, $InputObject.Propagation, $InputObject.Type) Add-Member -InputObject $rule -MemberType NoteProperty -Name DisplayName -Value $resolvedIdentity [PSCustomObject]@{ Path = $resolvedPath Identity = $resolvedIdentity Principal = $resolvedPrincipal AccessRule = $rule Configuration = $InputObject IdentityError = $identityError ServerRole = $InputObject.ServerRole AccessMode = $InputObject.AccessMode Empty = $false } } } function Get-RemoteAccessRule { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] param ( $Session, [string] $Path ) $rules = Invoke-Command -Session $Session -ScriptBlock { $acl = Get-Acl -Path $using:path foreach ($rule in $acl.Access) { if ($rule.IsInherited) { continue } [PSCustomObject]@{ PSTypeName = 'Remote.FileSystemAccessRule' DisplayName = $rule.IdentityReference.ToString() FileSystemRights = $rule.FileSystemRights FileSystemRightsNumeric = [int]$rule.FileSystemRights AccessControlType = $rule.AccessControlType IdentityReference = $rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) InheritanceFlags = $rule.InheritanceFlags PropagationFlags = $rule.PropagationFlags OriginalRights = $rule.FileSystemRights } } } # The default object had display issues when displayed in the "Change" property foreach ($rule in $rules) { $rule.FileSystemRights = Convert-AccessRight -Right $rule.FileSystemRightsNumeric $rule } } function New-Change { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( $RuleObject ) Add-Member -InputObject $RuleObject -MemberType ScriptMethod -Name ToString -Force -Value { if ($this.DisplayName) { return $this.DisplayName } return $this.IdentityReference } -PassThru } function Test-AccessRule { [CmdletBinding()] param ( $RuleObject, $Reference ) foreach ($entry in $Reference) { if ($entry.FileSystemRights -ne $RuleObject.FileSystemRights) { continue } if ($entry.AccessControlType -ne $RuleObject.AccessControlType) { continue } if ($entry.IdentityReference.ToString() -ne $RuleObject.IdentityReference.ToString()) { continue } if ($entry.InheritanceFlags -ne $RuleObject.InheritanceFlags) { continue } if ($entry.PropagationFlags -ne $RuleObject.PropagationFlags) { continue } return $true } return $false } function Convert-AccessRight { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseUsingScopeModifierInNewRunspaces", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] [CmdletBinding()] param ( [int] $Right ) $bytes = [System.BitConverter]::GetBytes($Right) $uint = [System.BitConverter]::ToUInt32($bytes, 0) $definitiveRight = [DCManagement.FileSystemPermission]$uint # https://docs.microsoft.com/en-us/windows/win32/fileio/file-security-and-access-rights # https://docs.microsoft.com/en-us/windows/win32/secauthz/standard-access-rights $genericRightsMap = @{ All = [DCManagement.FileSystemPermission]::FullControl Execute = ([DCManagement.FileSystemPermission]'ExecuteFile, ReadAttributes, ReadPermissions, Synchronize') Read = ([DCManagement.FileSystemPermission]'ReadAttributes, ReadData, ReadExtendedAttributes, ReadPermissions, Synchronize') Write = ([DCManagement.FileSystemPermission]'AppendData, WriteAttributes, WriteData, WriteExtendedAttributes, ReadPermissions, Synchronize') } if ($definitiveRight -band [DCManagement.FileSystemPermission]::GenericAll) { return [System.Security.AccessControl.FileSystemRights]::FullControl } if ($definitiveRight -band [DCManagement.FileSystemPermission]::GenericExecute) { $definitiveRight = $definitiveRight -bxor [DCManagement.FileSystemPermission]::GenericExecute -bor $genericRightsMap.Execute } if ($definitiveRight -band [DCManagement.FileSystemPermission]::GenericRead) { $definitiveRight = $definitiveRight -bxor [DCManagement.FileSystemPermission]::GenericRead -bor $genericRightsMap.Read } if ($definitiveRight -band [DCManagement.FileSystemPermission]::GenericWrite) { $definitiveRight = $definitiveRight -bxor [DCManagement.FileSystemPermission]::GenericWrite -bor $genericRightsMap.Write } [System.Security.AccessControl.FileSystemRights]$definitiveRight.Value__ } #endregion Utility Functions } process { foreach ($domainController in $domainControllers) { $results = @{ ObjectType = 'FSAccessRule' Server = $domainController.Name } Write-PSFMessage -String 'Test-DCAccessRule.Processing' -StringValues $domainController.Name -Target $domainController.Name try { $psSession = New-PSSession -ComputerName $domainController.Name @psCred -ErrorAction Stop } catch { Stop-PSFFunction -String 'Test-DCAccessRule.PSSession.Failed' -StringValues $domainController.Name -EnableException $EnableException -Cmdlet $PSCmdlet -Continue -Target $domainController.Name -ErrorRecord $_ } $accessConfigurations = Get-DCAccessRule | Where-Object { $_.ServerRole -eq 'ALL' -or ($_.ServerRole -eq 'FSMO' -and $domainController.IsFSMO) -or ($_.ServerRole -eq 'PDC' -and $domainController.IsPDCEmulator) } | ConvertFrom-AccessRuleDefinition -Parameters $parameters $groupedByPath = $accessConfigurations | Group-Object -Property Path foreach ($path in $groupedByPath) { Write-PSFMessage -String 'Test-DCAccessRule.Processing.Path' -StringValues $domainController.Name, $path.Name -Target $domainController.Name $pathExists = Invoke-Command -Session $psSession -ScriptBlock { Test-Path -Path $using:path.Name } if (-not $pathExists) { foreach ($entry in $path.Group) { New-TestResult @results -Type NoPath -Configuration $entry -Identity $path.Name -Changed (New-Change -RuleObject $desiredRule.AccessRule) } Stop-PSFFunction -String 'Test-DCAccessRule.Path.ExistsNot' -StringValues $domainController.Name, $path.Name -EnableException $EnableException -Cmdlet $PSCmdlet -Continue -Target $domainController.Name } $existingRules = Get-RemoteAccessRule -Session $psSession -Path $path.Name #region Empty Mode: No explicit ACE should exist if ($path.Group | Where-Object Empty) { foreach ($rule in $existingRules) { New-TestResult @results -Type Remove -Configuration $path.Group -ADObject $existingRules -Identity $path.Name -Changed (New-Change -RuleObject $rule) } continue } #endregion Empty Mode: No explicit ACE should exist $effectiveMode = 'Additive' if ($path.Group | Where-Object AccessMode -EQ 'Defined') { $effectiveMode = 'Defined' } if ($path.Group | Where-Object AccessMode -EQ 'Constrained') { $effectiveMode = 'Constrained' } if ($path.Group | Where-Object IdentityError) { # Interrupt if Constrained and resolution error if ($effectiveMode -eq 'Constrained') { $errorCfg = $path.Group | Where-Object IdentityError Stop-PSFFunction -String 'Test-DCAccessRule.Identity.Error' -StringValues $domainController.Name, $path.Name, ($errorCfg.Identity -join ",") -EnableException $EnableException -Cmdlet $PSCmdlet -Continue -Target $domainController.Name } else { Write-PSFMessage -Level Warning -String 'Test-DCAccessRule.Identity.Error' -StringValues $domainController.Name, $path.Name, ($errorCfg.Identity -join ",") } } $effectiveDesiredState = $path.Group | Where-Object IdentityError -NE $true #region Compare desired state with existing state foreach ($desiredRule in $effectiveDesiredState) { if (Test-AccessRule -RuleObject $desiredRule.AccessRule -Reference $existingRules) { continue } New-TestResult @results -Type Add -Configuration $desiredRule -ADObject $existingRules -Identity $path.Name -Changed (New-Change -RuleObject $desiredRule.AccessRule) } if ($effectiveMode -eq 'Additive') { continue } foreach ($existingRule in $existingRules) { if ($effectiveMode -eq 'Defined' -and "$($existingRule.IdentityReference.ToString())" -notin ($effectiveDesiredState.AccessRule.IdentityReference | ForEach-Object ToString)) { continue } if (Test-AccessRule -RuleObject $existingRule -Reference $effectiveDesiredState.AccessRule) { continue } New-TestResult @results -Type Remove -Configuration $effectiveDesiredState -ADObject $existingRule -Identity $path.Name -Changed (New-Change -RuleObject $existingRule) } #endregion Compare desired state with existing state } Remove-PSSession -Session $psSession -ErrorAction Ignore -Confirm:$false } } } function Unregister-DCAccessRule { <# .SYNOPSIS Removes an access rule from the list of registered access rules. .DESCRIPTION Removes an access rule from the list of registered access rules. .PARAMETER Path The path to the filesystem resource being managed. .PARAMETER Identity The identity (user, group, etc.) whose permissions ar being removed from the list of intended permissions. .PARAMETER ServerRole The processing mode the rule was assigned to. .PARAMETER Rights The rights assigned. .PARAMETER Type Allow or Deny rule? .PARAMETER Inheritance Who gets to inherit? .PARAMETER Propagation How does it propagate? .EXAMPLE PS C:\> Get-DCaccessRule | Unregister-DCAccessRule Clears all configured access rules. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Path, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Identity, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $ServerRole, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Rights, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Type, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Inheritance, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Propagation ) process { if (-not $script:fileSystemAccessRules[$Path]) { return } $script:fileSystemAccessRules[$Path].Remove("$($Identity)þ$($ServerRole)þ$($Rights)þ$($Type)þ$($Inheritance)þ$($Propagation)") if (-not $script:fileSystemAccessRules[$Path]) { $script:fileSystemAccessRules.Remove($Path) } } } function Install-DCChildDomain { <# .SYNOPSIS Installs a child domain. .DESCRIPTION Installs a child domain. .PARAMETER ComputerName The server to promote to a DC hosting a new subdomain. .PARAMETER Credential The credentials to use for connecting to the DC-to-be. .PARAMETER DomainName The name of the domain to install. Note: Only specify the first DNS element, not the full fqdn of the domain. (The component usually representing the Netbios Name) .PARAMETER ParentDomainName The FQDN of the parent domain. .PARAMETER NetBiosName The NetBios name of the domain. Will use the DomainName if not specified. .PARAMETER SafeModeAdministratorPassword The SafeModeAdministratorPassword specified during domain creation. If not specified, a random password will be chosen. The password is part of the return values. .PARAMETER EnterpriseAdminCredential The Credentials of an Enterprise administrator. Will prompt for credentials if not specified. .PARAMETER NoDNS Disables installation and configuration of the DNS role as part of the installation. .PARAMETER NoReboot Prevents reboot of the server after installation. Note: Generally a reboot is required before proceeding, disabling this will lead to having to manually reboot the computer. .PARAMETER LogPath The path where the NTDS logs should be stored. .PARAMETER SysvolPath The path where SYSVOL should be stored. .PARAMETER DatabasePath The path where the NTDS database is being stored. .PARAMETER NoResultCache Disables caching of the command's return object. By default, this command will cache the return object as a global variable. .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. .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:\> Install-DCChildDomain -ComputerName 10.1.2.3 -Credential $cred -DomainName corp -ParentDomainName contoso.com Will install the childdomain corp.contoso.com under the domain contoso.com on the server 10.1.2.3. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding(SupportsShouldProcess = $true)] Param ( [PSFComputer] $ComputerName = 'localhost', [PSCredential] $Credential, [Parameter(Mandatory = $true)] [PsfValidatePattern('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9])$', ErrorString = 'DCManagement.Validate.Child.DomainName')] [string] $DomainName, [Parameter(Mandatory = $true)] [PsfValidatePattern('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9])){1,}$', ErrorString = 'DCManagement.Validate.Parent.DnsDomainName')] [string] $ParentDomainName, [string] $NetBiosName, [securestring] $SafeModeAdministratorPassword = (New-Password -Length 32 -AsSecureString), [PSCredential] $EnterpriseAdminCredential = (Get-Credential -Message "Enter credentials for Enterprise Administrator to create child domain"), [switch] $NoDNS, [switch] $NoReboot, [string] $LogPath, [string] $SysvolPath, [string] $DatabasePath, [switch] $NoResultCache, [switch] $EnableException ) begin { $parameters = @{ Server = $ComputerName; IsDCInstall = $true } if ($Credential) { $parameters['Credential'] = $Credential } Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet $NoDNS = Resolve-ParameterValue -FullName 'DCManagement.Defaults.NoDNS' -InputObject $NoDNS $NoReboot = Resolve-ParameterValue -FullName 'DCManagement.Defaults.NoReboot' -InputObject $NoReboot $LogPath = Resolve-ParameterValue -FullName 'DCManagement.Defaults.LogPath' -InputObject $LogPath $SysvolPath = Resolve-ParameterValue -FullName 'DCManagement.Defaults.SysvolPath' -InputObject $SysvolPath $DatabasePath = Resolve-ParameterValue -FullName 'DCManagement.Defaults.DatabasePath' -InputObject $DatabasePath #region Scriptblock $scriptBlock = { param ($Configuration) function New-Result { [CmdletBinding()] param ( [ValidateSet('Success', 'Error')] [string] $Status = 'Success', [string] $Message, $ErrorRecord, $Data ) [PSCustomObject]@{ Status = $Status Success = $Status -eq 'Success' Message = $Message Error = $ErrorRecord Data = $Data SafeModeAdminPassword = $null } } # Check whether domain member $computerSystem = Get-CimInstance win32_ComputerSystem if ($computerSystem.PartOfDomain) { New-Result -Status Error -Message "Computer $env:COMPUTERNAME is part of AD domain: $($computerSystem.Domain)" return } $parameters = @{ NewDomainName = $Configuration.NewDomainName NewDomainNetBiosName = $Configuration.NewDomainNetBiosName ParentDomainName = $Configuration.ParentDomainName Credential = $Configuration.EnterpriseAdminCredential DomainMode = 'Win2012R2' DatabasePath = $Configuration.DatabasePath LogPath = $Configuration.LogPath SysvolPath = $Configuration.SysvolPath InstallDNS = $Configuration.InstallDNS SafeModeAdministratorPassword = $Configuration.SafeModeAdministratorPassword NoRebootOnCompletion = $Configuration.NoRebootOnCompletion } # Test Installation $testResult = Test-ADDSDomainInstallation @parameters -WarningAction SilentlyContinue if ($testResult.Status -eq "Error") { New-Result -Status Error -Message "Failed validating Domain Installation: $($testResult.Message)" -Data $testResult return } # Execute Installation try { $resultData = Install-ADDSDomain @parameters -ErrorAction Stop -Confirm:$false -WarningAction SilentlyContinue if ($resultData.Status -eq "Error") { New-Result -Status Error -Message "Failed installing domain: $($resultData.Message)" -Data $resultData return } New-Result -Status 'Success' -Message "Domain $($Configuration.NewDomainName) successfully installed" -Data $resultData return } catch { New-Result -Status Error -Message "Error executing domain deployment: $_" -ErrorRecord $_ return } } #endregion Scriptblock } process { if (-not $NetBiosName) { $NetBiosName = $DomainName } $configuration = [PSCustomObject]@{ NewDomainName = $DomainName NewDomainNetBiosName = $NetBiosName ParentDomainName = $ParentDomainName EnterpriseAdminCredential = $EnterpriseAdminCredential InstallDNS = (-not $NoDNS) LogPath = $LogPath SysvolPath = $SysvolPath DatabasePath = $DatabasePath NoRebootOnCompletion = $NoReboot SafeModeAdministratorPassword = $SafeModeAdministratorPassword } Invoke-PSFProtectedCommand -ActionString 'Install-DCChildDomain.Installing' -Target $DomainName -ScriptBlock { $result = Invoke-PSFCommand -ComputerName $ComputerName -Credential $Credential -ScriptBlock $scriptBlock -ErrorAction Stop -ArgumentList $configuration $result.SafeModeAdminPassword = $SafeModeAdministratorPassword $result = $result | Select-PSFObject -KeepInputObject -ScriptProperty @{ Password = { [PSCredential]::new("Foo", $this.SafeModeAdminPassword).GetNetworkCredential().Password } } -ShowProperty Success, Message if (-not $NoResultCache) { $global:DomainCreationResult = $result } $result } -EnableException $EnableException -PSCmdlet $PSCmdlet if (Test-PSFFunctionInterrupt) { return } if (-not $NoResultCache) { Write-PSFMessage -Level Host -String 'Install-DCChildDomain.Results' -StringValues $DomainName } } } function Install-DCDomainController { <# .SYNOPSIS Adds a new domain controller to an existing domain. .DESCRIPTION Adds a new domain controller to an existing domain. The target computer cannot already be part of the domain. .PARAMETER ComputerName The target to promote to domain controller. Accepts and reuses an already established PowerShell Remoting Session. .PARAMETER Credential Credentials to use for authenticating to the computer account being promoted. .PARAMETER DomainName The fully qualified dns name of the domain to join the DC to. .PARAMETER DomainCredential Credentials to use when authenticating to the domain. .PARAMETER SafeModeAdministratorPassword The password to use as SafeModeAdministratorPassword. Autogenerates and reports a new password if not specified. .PARAMETER NoDNS Disable deploying a DNS service with the new domain controller. .PARAMETER NoReboot Prevent reboot after finishing deployment .PARAMETER LogPath The path where the DC will store the logs. .PARAMETER SysvolPath The path where the DC will store sysvol. .PARAMETER DatabasePath The path where the DC will store NTDS Database. .PARAMETER NoResultCache Disables caching the result object of the operation. By default, this command will cache the result of the installation (including the SafeModeAdministratorPassword), to reduce the risk of user error. .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. .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:\> Install-DCDomainController -Computer dc2.contoso.com -Credential $localCred -DomainName 'contoso.com' -DomainCredential $domCred Joins the server dc2.contoso.com into the contoso.com domain, as a promoted domain controller using the specified credentials. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding(SupportsShouldProcess = $true)] Param ( [PSFComputer] $ComputerName = 'localhost', [PSCredential] $Credential, [Parameter(Mandatory = $true)] [PsfValidatePattern('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9])){1,}$', ErrorString = 'DCManagement.Validate.ForestRoot.DnsDomainName')] [string] $DomainName, [PSCredential] $DomainCredential = (Get-Credential -Message 'Specify domain admin credentials needed to authorize the promotion to domain controller'), [securestring] $SafeModeAdministratorPassword = (New-Password -Length 32 -AsSecureString), [switch] $NoDNS, [switch] $NoReboot, [string] $LogPath, [string] $SysvolPath, [string] $DatabasePath, [switch] $NoResultCache, [switch] $EnableException ) begin { $parameters = @{ Server = $ComputerName; IsDCInstall = $true } if ($Credential) { $parameters['Credential'] = $Credential } Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet $NoDNS = Resolve-ParameterValue -FullName 'DCManagement.Defaults.NoDNS' -InputObject $NoDNS $NoReboot = Resolve-ParameterValue -FullName 'DCManagement.Defaults.NoReboot' -InputObject $NoReboot $LogPath = Resolve-ParameterValue -FullName 'DCManagement.Defaults.LogPath' -InputObject $LogPath $SysvolPath = Resolve-ParameterValue -FullName 'DCManagement.Defaults.SysvolPath' -InputObject $SysvolPath $DatabasePath = Resolve-ParameterValue -FullName 'DCManagement.Defaults.DatabasePath' -InputObject $DatabasePath #region Main Scriptblock $scriptBlock = { param ( $Configuration ) function New-Result { [CmdletBinding()] param ( [ValidateSet('Success', 'Error')] [string] $Status = 'Success', [string] $Message, $ErrorRecord, $Data ) [PSCustomObject]@{ Status = $Status Success = $Status -eq 'Success' Message = $Message Error = $ErrorRecord Data = $Data SafeModeAdminPassword = $null } } # Check whether domain member $computerSystem = Get-CimInstance win32_ComputerSystem if ($computerSystem.PartOfDomain) { New-Result -Status Error -Message "Computer $env:COMPUTERNAME is part of AD domain: $($computerSystem.Domain)" return } $null = Install-WindowsFeature -Name AD-Domain-Services -IncludeManagementTools $parameters = @{ DomainName = $Configuration.DomainName Credential = $Configuration.DomainCredential DatabasePath = $Configuration.DatabasePath LogPath = $Configuration.LogPath SysvolPath = $Configuration.SysvolPath InstallDNS = $Configuration.InstallDNS SafeModeAdministratorPassword = $Configuration.SafeModeAdministratorPassword NoRebootOnCompletion = $Configuration.NoRebootOnCompletion } # Test Installation $testResult = Test-ADDSDomainControllerInstallation @parameters -WarningAction SilentlyContinue if ($testResult.Status -eq "Error") { New-Result -Status Error -Message "Failed validating Domain Controller Installation: $($testResult.Message)" -Data $testResult return } # Execute Installation try { $resultData = Install-ADDSDomainController @parameters -ErrorAction Stop -Confirm:$false -WarningAction SilentlyContinue if ($resultData.Status -eq "Error") { New-Result -Status Error -Message "Failed installing Domain Controller: $($resultData.Message)" -Data $resultData return } New-Result -Status 'Success' -Message "Domain $($Configuration.DomainName) successfully installed" -Data $resultData return } catch { New-Result -Status Error -Message "Error executing Domain Controller deployment: $_" -ErrorRecord $_ return } } #endregion Main Scriptblock } process { if (-not $NetBiosName) { $NetBiosName = $DnsName -split "\." | Select-Object -First 1 } $configuration = [PSCustomObject]@{ DomainName = $DomainName DomainCredential = $DomainCredential SafeModeAdministratorPassword = $SafeModeAdministratorPassword InstallDNS = (-not $NoDNS) LogPath = $LogPath SysvolPath = $SysvolPath DatabasePath = $DatabasePath NoRebootOnCompletion = $NoReboot } Invoke-PSFProtectedCommand -ActionString 'Install-DCDomainController.Installing' -ActionStringValues $DomainName -Target $DnsName -ScriptBlock { $result = Invoke-PSFCommand -ComputerName $ComputerName -Credential $Credential -ScriptBlock $scriptBlock -ErrorAction Stop -ArgumentList $configuration $result.SafeModeAdminPassword = $SafeModeAdministratorPassword $result = $result | Select-PSFObject -KeepInputObject -ScriptProperty @{ Password = { [PSCredential]::new("Foo", $this.SafeModeAdminPassword).GetNetworkCredential().Password } } -ShowProperty Success, Message if (-not $NoResultCache) { $global:DCCreationResult = $result } $result } -EnableException $EnableException -PSCmdlet $PSCmdlet if (Test-PSFFunctionInterrupt) { return } if (-not $NoResultCache) { Write-PSFMessage -Level Host -String 'Install-DCDomainController.Results' -StringValues $DnsName } } } function Install-DCRootDomain { <# .SYNOPSIS Deploys a new forest / root domain. .DESCRIPTION Deploys a new forest / root domain. .PARAMETER ComputerName The computer on which to install it. Uses WinRM / PowerShell remoting if not local execution. .PARAMETER Credential The credentials to use for this operation. .PARAMETER DnsName The name of the new domain & forest. .PARAMETER NetBiosName The netbios name of the new domain. If not specified, it will automatically use the first element of the DNS name .PARAMETER SafeModeAdministratorPassword The password to use as SafeModeAdministratorPassword. Autogenerates and reports a new password if not specified. .PARAMETER NoDNS Disable deploying a DNS service with the new forest. .PARAMETER NoReboot Prevent reboot after finishing deployment .PARAMETER LogPath The path where the DC will store the logs. .PARAMETER SysvolPath The path where the DC will store sysvol. .PARAMETER DatabasePath The path where the DC will store NTDS Database. .PARAMETER NoResultCache Disables caching the result object of the operation. By default, this command will cache the result of the installation (including the SafeModeAdministratorPassword), to reduce the risk of user error. .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. .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:\> Install-DCRootDomain -DnsName 'contoso.com' Creates the forest "contoso.com" while promoting the computer as DC. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding(SupportsShouldProcess = $true)] Param ( [PSFComputer] $ComputerName = 'localhost', [PSCredential] $Credential, [Parameter(Mandatory = $true)] [PsfValidatePattern('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-_]{0,61}[a-zA-Z0-9])){1,}$', ErrorString = 'DCManagement.Validate.ForestRoot.DnsDomainName')] [string] $DnsName, [string] $NetBiosName, [securestring] $SafeModeAdministratorPassword = (New-Password -Length 32 -AsSecureString), [switch] $NoDNS, [switch] $NoReboot, [string] $LogPath, [string] $SysvolPath, [string] $DatabasePath, [switch] $NoResultCache, [switch] $EnableException ) begin { $parameters = @{ Server = $ComputerName; IsDCInstall = $true } if ($Credential) { $parameters['Credential'] = $Credential} Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet $NoDNS = Resolve-ParameterValue -FullName 'DCManagement.Defaults.NoDNS' -InputObject $NoDNS $NoReboot = Resolve-ParameterValue -FullName 'DCManagement.Defaults.NoReboot' -InputObject $NoReboot $LogPath = Resolve-ParameterValue -FullName 'DCManagement.Defaults.LogPath' -InputObject $LogPath $SysvolPath = Resolve-ParameterValue -FullName 'DCManagement.Defaults.SysvolPath' -InputObject $SysvolPath $DatabasePath = Resolve-ParameterValue -FullName 'DCManagement.Defaults.DatabasePath' -InputObject $DatabasePath #region Main Scriptblock $scriptBlock = { param ( $Configuration ) function New-Result { [CmdletBinding()] param ( [ValidateSet('Success', 'Error')] [string] $Status = 'Success', [string] $Message, $ErrorRecord, $Data ) [PSCustomObject]@{ Status = $Status Success = $Status -eq 'Success' Message = $Message Error = $ErrorRecord Data = $Data SafeModeAdminPassword = $null } } # Check whether domain member $computerSystem = Get-CimInstance win32_ComputerSystem if ($computerSystem.PartOfDomain) { New-Result -Status Error -Message "Computer $env:COMPUTERNAME is part of AD domain: $($computerSystem.Domain)" return } $parameters = @{ DomainName = $Configuration.DnsName DomainMode = 'Win2012R2' DomainNetbiosName = $Configuration.NetBiosName ForestMode = 'Win2012R2' DatabasePath = $Configuration.DatabasePath LogPath = $Configuration.LogPath SysvolPath = $Configuration.SysvolPath InstallDNS = $Configuration.InstallDNS SafeModeAdministratorPassword = $Configuration.SafeModeAdministratorPassword NoRebootOnCompletion = $Configuration.NoRebootOnCompletion } # Test Installation $testResult = Test-ADDSForestInstallation @parameters -WarningAction SilentlyContinue if ($testResult.Status -eq "Error") { New-Result -Status Error -Message "Failed validating Forest Installation: $($testResult.Message)" -Data $testResult return } # Execute Installation try { $resultData = Install-ADDSForest @parameters -ErrorAction Stop -Confirm:$false -WarningAction SilentlyContinue if ($resultData.Status -eq "Error") { New-Result -Status Error -Message "Failed installing Forest: $($resultData.Message)" -Data $resultData return } New-Result -Status 'Success' -Message "Domain $($Configuration.DnsName) successfully installed" -Data $resultData return } catch { New-Result -Status Error -Message "Error executing forest deployment: $_" -ErrorRecord $_ return } } #endregion Main Scriptblock } process { if (-not $NetBiosName) { $NetBiosName = $DnsName -split "\." | Select-Object -First 1 } $configuration = [PSCustomObject]@{ DnsName = $DnsName NetBiosName = $NetBiosName SafeModeAdministratorPassword = $SafeModeAdministratorPassword InstallDNS = (-not $NoDNS) LogPath = $LogPath SysvolPath = $SysvolPath DatabasePath = $DatabasePath NoRebootOnCompletion = $NoReboot } Invoke-PSFProtectedCommand -ActionString 'Install-DCRootDomain.Installing' -Target $DnsName -ScriptBlock { $result = Invoke-PSFCommand -ComputerName $ComputerName -Credential $Credential -ScriptBlock $scriptBlock -ErrorAction Stop -ArgumentList $configuration $result.SafeModeAdminPassword = $SafeModeAdministratorPassword $result = $result | Select-PSFObject -KeepInputObject -ScriptProperty @{ Password = { [PSCredential]::new("Foo", $this.SafeModeAdminPassword).GetNetworkCredential().Password } } -ShowProperty Success, Message if (-not $NoResultCache) { $global:ForestCreationResult = $result } $result } -EnableException $EnableException -PSCmdlet $PSCmdlet if (Test-PSFFunctionInterrupt) { return } if (-not $NoResultCache) { Write-PSFMessage -Level Host -String 'Install-DCRootDomain.Results' -StringValues $DnsName } } } function Get-DCShare { <# .SYNOPSIS Returns the list of registered shares. .DESCRIPTION Returns the list of registered shares. .PARAMETER Name Filter the returned share definitions by name. Defaults to '*' .EXAMPLE PS C:\> Get-DCShare Returns the list of registered shares. #> [CmdletBinding()] Param ( [string] $Name = '*' ) process { $script:shares.Values | Where-Object Name -like $Name } } function Invoke-DCShare { <# .SYNOPSIS Brings all network shares on all DCs in the domain into the desired state. .DESCRIPTION Brings all network shares on all DCs in the domain into the desired state. Use Register-DCShare (or an ADMF Context) to define the desired state. .PARAMETER InputObject Individual test results to process. Only accepts the output of Test-DCShare. .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .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. .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-DCShare -Server contoso.com Tests all DCs in contoso.com, validating that their network shares are as configured, correcting any deviations. #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(ValueFromPipeline = $true)] $InputObject, [PSFComputer] $Server, [PSCredential] $Credential, [switch] $EnableException ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $parameters['Debug'] = $false Assert-ADConnection @parameters -Cmdlet $PSCmdlet Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet Assert-Configuration -Type Shares -Cmdlet $PSCmdlet Set-DCDomainContext @parameters $cimCred = $PSBoundParameters | ConvertTo-PSFHashtable -Include Credential $cimSessions = @{ } } process { if (-not $InputObject) { $InputObject = Test-DCShare @parameters } foreach ($testItem in $InputObject) { # Catch invalid input - can only process test results if ($testItem.PSObject.TypeNames -notcontains 'DCManagement.Share.TestResult') { Stop-PSFFunction -String 'General.Invalid.Input' -StringValues 'Test-DCShare', $testItem -Target $testItem -Continue -EnableException $EnableException } if (-not $cimSessions[$testItem.Server]) { try { $cimSessions[$testItem.Server] = New-CimSession -ComputerName $testItem.Server @cimCred -ErrorAction Stop } catch { Stop-PSFFunction -String 'Invoke-DCShare.Access.Error' -StringValues $testItem.Server -Target $testItem -Continue -EnableException $EnableException -ErrorRecord $_ } } $cimSession = $cimSessions[$testItem.Server] switch ($testItem.Type) { #region New Share 'New' { $newShareParam = @{ CimSession = $cimSession Name = $testItem.Configuration.Name Path = $testItem.Configuration.Path ErrorAction = 'Stop' WhatIf = $false Confirm = $false } if ($testItem.Configuration.Description) { $newShareParam.Description = $testItem.Configuration.Description } $grantParam = @{ ComputerName = $testItem.Server Name = $testItem.Configuration.Name } $grantParam += $cimCred Invoke-PSFProtectedCommand -ActionString 'Invoke-DCShare.Share.Create' -ActionStringValues $testItem.Identity -Target $testItem -ScriptBlock { $null = New-SmbShare @newShareParam foreach ($identity in $testItem.Configuration.FullAccess) { Grant-ShareAccess @grantParam -Identity $identity -AccessRight Full } foreach ($identity in $testItem.Configuration.WriteAccess) { Grant-ShareAccess @grantParam -Identity $identity -AccessRight Change } foreach ($identity in $testItem.Configuration.ReadAccess) { Grant-ShareAccess @grantParam -Identity $identity -AccessRight Read } } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue } #endregion New Share #region Update 'Update' { if ($testItem.Changed -contains 'Path') { $newShareParam = @{ CimSession = $cimSession Name = $testItem.Configuration.Name Path = $testItem.Configuration.Path ErrorAction = 'Stop' WhatIf = $false Confirm = $false } if ($testItem.Configuration.Description) { $newShareParam.Description = $testItem.Configuration.Description } $grantParam = @{ ComputerName = $testItem.Server Name = $testItem.Configuration.Name } $grantParam += $cimCred Invoke-PSFProtectedCommand -ActionString 'Invoke-DCShare.Share.Migrate' -ActionStringValues $testItem.Identity -Target $testItem -ScriptBlock { $null = Remove-SmbShare -Name $testItem.Configuration.Name -CimSession $cimSession $null = New-SmbShare @newShareParam foreach ($identity in $testItem.Configuration.FullAccess) { Grant-ShareAccess @grantParam -Identity $identity -AccessRight Full } foreach ($identity in $testItem.Configuration.WriteAccess) { Grant-ShareAccess @grantParam -Identity $identity -AccessRight Change } foreach ($identity in $testItem.Configuration.ReadAccess) { Grant-ShareAccess @grantParam -Identity $identity -AccessRight Read } } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue } else { $setShareParam = @{ CimSession = $cimSession Name = $testItem.Configuration.Name ErrorAction = 'Stop' WhatIf = $false Confirm = $false } if ($testItem.Configuration.Description) { $setShareParam.Description = $testItem.Configuration.Description } Invoke-PSFProtectedCommand -ActionString 'Invoke-DCShare.Share.Update' -ActionStringValues $testItem.Identity -Target $testItem -ScriptBlock { Set-SmbShare @setShareParam } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue } } #endregion Update #region Update Access Rules 'AccessUpdate' { foreach ($accessEntry in $testItem.Changed) { Invoke-PSFProtectedCommand -ActionString 'Invoke-DCShare.Share.UpdateAccess' -ActionStringValues $testItem.Identity, $accessEntry.Action, $accessEntry.Identity, $accessEntry.AccessRight -Target $testItem -ScriptBlock { if ($accessEntry.Action -eq 'Add') { Grant-ShareAccess -ComputerName $testItem.Server @cimCred -Name $testItem.Configuration.Name -Identity $accessEntry.Identity -AccessRight $accessEntry.AccessRight } else { Revoke-ShareAccess -ComputerName $testItem.Server @cimCred -Name $testItem.Configuration.Name -Identity $accessEntry.Identity -AccessRight $accessEntry.AccessRight } } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue } } #endregion Update Access Rules #region Delete Share 'Delete' { Invoke-PSFProtectedCommand -ActionString 'Invoke-DCShare.Share.Delete' -ActionStringValues $testItem.Identity -Target $testItem -ScriptBlock { $null = Remove-SmbShare -Name $testItem.ADObject.Name -CimSession $cimSession -WhatIf:$false -Confirm:$false } -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue } #endregion Delete Share } } } end { $cimSessions.Values | Remove-CimSession -ErrorAction Ignore -Confirm:$false -WhatIf:$false } } function Register-DCShare { <# .SYNOPSIS Registers an SMB share that should exist on DCs. .DESCRIPTION Registers an SMB share that should exist on DCs. .PARAMETER Name The name of the share. Supports string resolution. .PARAMETER Path The path the share points to. Supports string resolution. .PARAMETER Description The description of the share. Supports string resolution. .PARAMETER FullAccess The principals to grant full access to. Supports string resolution. .PARAMETER WriteAccess The principals to grant write access to. Supports string resolution. .PARAMETER ReadAccess The principals to grant read access to. Supports string resolution. .PARAMETER AccessMode How share access rules are processed. Supports three configurations: - Constrained: The default access mode, will remove any excess access rules. - Additive: Ignore any access rules already on the share, even if not configured - Defined: Ignore any access rules already on the share, even if not configured UNLESS the identity on those rules has an access level defined for it. .PARAMETER ServerRole What domain controller to apply this to: - All: All DCs in the enterprise - FSMO: Only DCs that have any FSMO role - PDC: Only the PDCEmulator .PARAMETER ContextName The name of the context defining the setting. This allows determining the configuration set that provided this setting. Used by the ADMF, available to any other configuration management solution. .EXAMPLE PS C:\> Get-Content .\shares.json | ConvertFrom-Json | Write-Output | Register-DCShare Reads all share definitions from json and imports the definitions. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Name, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Path, [Parameter(ValueFromPipelineByPropertyName = $true)] [AllowEmptyCollection()] [string] $Description, [Parameter(ValueFromPipelineByPropertyName = $true)] [AllowEmptyCollection()] [string[]] $FullAccess, [Parameter(ValueFromPipelineByPropertyName = $true)] [AllowEmptyCollection()] [string[]] $WriteAccess, [Parameter(ValueFromPipelineByPropertyName = $true)] [AllowEmptyCollection()] [string[]] $ReadAccess, [Parameter(ValueFromPipelineByPropertyName = $true)] [ValidateSet('Constrained', 'Additive', 'Defined')] [string] $AccessMode = 'Constrained', [Parameter(ValueFromPipelineByPropertyName = $true)] [ValidateSet('All', 'FSMO', 'PDC')] [string] $ServerRole = 'All', [string] $ContextName = '<Undefined>' ) process { $script:shares[$Name] = [PSCustomObject]@{ PSTypeName = 'DCManagement.Share' Name = $Name Path = $Path Description = $Description FullAccess = $FullAccess WriteAccess = $WriteAccess ReadAccess = $ReadAccess AccessMode = $AccessMode ServerRole = $ServerRole ContextName = $ContextName } } } function Test-DCShare { <# .SYNOPSIS Tests all DCs in the target domain for share compliance. .DESCRIPTION Tests all DCs in the target domain, by comparing existing shares with the list of defined shares. Use Register-DCShare (or an ADMF Context) to define shares. .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .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:\> Test-DCShare -Server contoso.com Tests all DCs in the domain contoso.com, whether their shares are as configured. #> [CmdletBinding()] Param ( [PSFComputer] $Server, [PSCredential] $Credential, [switch] $EnableException ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $parameters['Debug'] = $false Assert-ADConnection @parameters -Cmdlet $PSCmdlet Invoke-PSFCallback -Data $parameters -EnableException $true -PSCmdlet $PSCmdlet Assert-Configuration -Type Shares -Cmdlet $PSCmdlet Set-DCDomainContext @parameters $domainControllers = Get-DomainController @parameters $cimCred = $PSBoundParameters | ConvertTo-PSFHashtable -Include Credential #region Utility Functions function ConvertFrom-ShareConfiguration { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $ShareConfiguration, [hashtable] $Parameters ) process { $cfgHash = $ShareConfiguration | ConvertTo-PSFHashtable $cfgHash.Name = $cfgHash.Name | Resolve-String -ArgumentList $Parameters $cfgHash.Path = $cfgHash.Path | Resolve-String -ArgumentList $Parameters $cfgHash.Description = $cfgHash.Description | Resolve-String -ArgumentList $Parameters $cfgHash.AccessIdentityIntegrity = $true $cfgHash.FullAccess = foreach ($entry in $cfgHash.FullAccess) { try { ($entry | Resolve-String -ArgumentList $Parameters | Resolve-Principal @Parameters -OutputType SID -ErrorAction Stop) -as [string] } catch { Write-PSFMessage -Level Warning -String 'Test-DCShare.Identity.Resolution.Failed' -StringValues $entry -Target $ShareConfiguration $cfgHash.AccessIdentityIntegrity = $false } } $cfgHash.WriteAccess = foreach ($entry in $cfgHash.WriteAccess) { try { ($entry | Resolve-String -ArgumentList $Parameters | Resolve-Principal @Parameters -OutputType SID -ErrorAction Stop) -as [string] } catch { Write-PSFMessage -Level Warning -String 'Test-DCShare.Identity.Resolution.Failed' -StringValues $entry -Target $ShareConfiguration $cfgHash.AccessIdentityIntegrity = $false } } $cfgHash.ReadAccess = foreach ($entry in $cfgHash.ReadAccess) { try { ($entry | Resolve-String -ArgumentList $Parameters | Resolve-Principal @Parameters -OutputType SID -ErrorAction Stop) -as [string] } catch { Write-PSFMessage -Level Warning -String 'Test-DCShare.Identity.Resolution.Failed' -StringValues $entry -Target $ShareConfiguration $cfgHash.AccessIdentityIntegrity = $false } } [pscustomobject]$cfgHash } } function Compare-ShareAccess { [CmdletBinding()] param ( $Configuration, $ShareAccess, [hashtable] $Parameters ) $access = @{ Full = @() Change = @() Read = @() } foreach ($accessItem in $ShareAccess) { $access["$($accessItem.AccessRight)"] += ($accessItem.AccountName | Resolve-Principal @Parameters -OutputType SID) -as [string] } #region Compare Defined with current state foreach ($entity in $Configuration.FullAccess) { if ($entity -notin $access.Full) { New-AccessChange -AccessRight Full -Identity $entity -Action Add } } foreach ($entity in $Configuration.WriteAccess) { if ($entity -notin $access.Change) { New-AccessChange -AccessRight Change -Identity $entity -Action Add } } foreach ($entity in $Configuration.ReadAccess) { if ($entity -notin $access.Read) { New-AccessChange -AccessRight Read -Identity $entity -Action Add } } #endregion Compare Defined with current state # If we will not remove any rights, no point inspecting the existing access rights if ($Configuration.AccessMode -eq 'Additive') { return } #region Compare current with defined state #region Full Access foreach ($entity in $access.Full) { if ($entity -notin $Configuration.FullAccess) { if ($Configuration.AccessMode -eq 'Constrained') { New-AccessChange -AccessRight Full -Identity $entity -Action Remove continue } if ($entity -notin $Configuration.WriteAccess -and $entity -notin $Configuration.ReadAccess) { continue } New-AccessChange -AccessRight Full -Identity $entity -Action Remove } } #endregion Full Access #region Write Access foreach ($entity in $access.Change) { if ($entity -notin $Configuration.WriteAccess) { if ($Configuration.AccessMode -eq 'Constrained') { New-AccessChange -AccessRight Change -Identity $entity -Action Remove continue } if ($entity -notin $Configuration.FullAccess -and $entity -notin $Configuration.ReadAccess) { continue } New-AccessChange -AccessRight Change -Identity $entity -Action Remove } } #endregion Write Access #region Read Access foreach ($entity in $access.Read) { if ($entity -notin $Configuration.ReadAccess) { if ($Configuration.AccessMode -eq 'Constrained') { New-AccessChange -AccessRight Read -Identity $entity -Action Remove continue } if ($entity -notin $Configuration.FullAccess -and $entity -notin $Configuration.WriteAccess) { continue } New-AccessChange -AccessRight Read -Identity $entity -Action Remove } } #endregion Read Access #endregion Compare current with defined state } function New-AccessChange { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [ValidateSet('Full','Change','Read')] [string] $AccessRight, [string] $Identity, [ValidateSet('Add','Remove')] [string] $Action ) [PSCustomObject]@{ PSTypeName = 'DCManagement.Share.AccessChange' Action = $Action AccessRight = $AccessRight Identity = $Identity } } #endregion Utility Functions } process { foreach ($domainController in $domainControllers) { $results = @{ ObjectType = 'Share' Server = $domainController.Name } Write-PSFMessage -String 'Test-DCShare.Processing' -StringValues $domainController.Name try { $cimSession = New-CimSession -ComputerName $domainController.Name @cimCred -ErrorAction Stop } catch { Stop-PSFFunction -String 'Test-DCShare.CimSession.Failed' -StringValues $domainController.Name -EnableException $EnableException -Cmdlet $PSCmdlet -Continue -Target $domainController.Name -ErrorRecord $_ } $shareConfigurations = Get-DCShare | Where-Object { $_.ServerRole -eq 'ALL' -or ($_.ServerRole -eq 'FSMO' -and $domainController.IsFSMO) -or ($_.ServerRole -eq 'PDC' -and $domainController.IsPDCEmulator) } | ConvertFrom-ShareConfiguration -Parameters $parameters $shares = Get-SmbShare -CimSession $cimSession -IncludeHidden | Add-Member -MemberType NoteProperty -Name ComputerName -Value $domainController.Name -Force -PassThru foreach ($share in $shares) { $sResults = $results.Clone() $sResults.Identity = '\\{0}\{1}' -f $share.ComputerName, $share.Name $sResults.ADObject = $share #region Share exists, but is not defined if ($share.Name -notin $shareConfigurations.Name) { # The special builtin shares will not trigger delete actions if ($share.Special) { continue } if ($share.Name -in 'NETLOGON','SYSVOL') { continue } New-Testresult @sResults -Type Delete continue } #endregion Share exists, but is not defined $configuration = $shareConfigurations | Where-Object Name -EQ $share.Name $sResults.Configuration = $configuration #region Handle Property Settings [System.Collections.ArrayList]$changes = @() Compare-Property -Configuration $configuration -ADObject $share -Changes $changes -Property Path Compare-Property -Configuration $configuration -ADObject $share -Changes $changes -Property Description if ($changes) { New-Testresult @sResults -Type Update -Changed $changes.ToArray() } #endregion Handle Property Settings #region Delegation if (-not $configuration.AccessIdentityIntegrity) { Write-PSFMessage -Level Warning -String 'Test-DCShare.Access.IntegrityError' -StringValues $share.Name, $domainController.Name -Tag panic, error, fail continue } $access = Get-SmbShareAccess -CimSession $cimSession -Name $share.Name $delta = Compare-ShareAccess -Configuration $configuration -ShareAccess $access -Parameters $parameters if ($delta) { New-Testresult @sResults -Type AccessUpdate -Changed $delta } #endregion Delegation } foreach ($cfgShare in $shareConfigurations) { if ($cfgShare.Name -in $shares.Name) { continue } New-TestResult @results -Type New -Identity "\\$($domainController.Name)\$($cfgShare.Name)" -Configuration $cfgShare } Remove-CimSession -CimSession $cimSession -ErrorAction Ignore -WhatIf:$false -Confirm:$false } } } function Unregister-DCShare { <# .SYNOPSIS Removes a specific share from the list of registered shares. .DESCRIPTION Removes a specific share from the list of registered shares. .PARAMETER Name The exact name of the share to unregister. .EXAMPLE PS C:\> Get-DCShare | Unregister-DCShare Clears all registered shares. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Name ) process { foreach ($nameString in $Name) { $script:shares.Remove($nameString) } } } function Clear-DCConfiguration { <# .SYNOPSIS Resets all DC specific configuration settings. .DESCRIPTION Resets all DC specific configuration settings. .EXAMPLE PS C:\> Clear-DCConfiguration Resets all DC specific configuration settings. #> [CmdletBinding()] Param ( ) process { . "$script:ModuleRoot\internal\scripts\variables.ps1" } } function Set-DCDomainContext { <# .SYNOPSIS Updates the domain settings for string replacement. .DESCRIPTION Updates the domain settings for string replacement. .PARAMETER Server The server / domain to work with. .PARAMETER Credential The credentials to use for this operation. .EXAMPLE PS C:\> Set-DCDomainContext @parameters Updates the current domain context #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [PSFComputer] $Server, [PSCredential] $Credential ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential $parameters['Debug'] = $false } process { $domainObject = Get-ADDomain @parameters $forestObject = Get-ADForest @parameters if ($forestObject.RootDomain -eq $domainObject.DNSRoot) { $forestRootDomain = $domainObject $forestRootSID = $forestRootDomain.DomainSID.Value } else { try { $cred = $PSBoundParameters | ConvertTo-PSFHashtable -Include Credential $forestRootDomain = Get-ADDomain @cred -Server $forestObject.RootDomain -ErrorAction Stop $forestRootSID = $forestRootDomain.DomainSID.Value } catch { $forestRootDomain = [PSCustomObject]@{ Name = $forestObject.RootDomain.Split(".", 2)[0] DNSRoot = $forestObject.RootDomain DistinguishedName = 'DC={0}' -f ($forestObject.RootDomain.Split(".") -join ",DC=") } $forestRootSID = (Get-ADObject @parameters -SearchBase "CN=System,$($domainObject.DistinguishedName)" -SearchScope OneLevel -LDAPFilter "(&(objectClass=trustedDomain)(trustPartner=$($forestObject.RootDomain)))" -Properties securityIdentifier).securityIdentifier.Value } } Register-StringMapping -Name '%DomainName%' -Value $domainObject.Name Register-StringMapping -Name '%DomainNetBIOSName%' -Value $domainObject.NetbiosName Register-StringMapping -Name '%DomainFqdn%' -Value $domainObject.DNSRoot Register-StringMapping -Name '%DomainDN%' -Value $domainObject.DistinguishedName Register-StringMapping -Name '%DomainSID%' -Value $domainObject.DomainSID.Value Register-StringMapping -Name '%RootDomainName%' -Value $forestRootDomain.Name Register-StringMapping -Name '%RootDomainFqdn%' -Value $forestRootDomain.DNSRoot Register-StringMapping -Name '%RootDomainDN%' -Value $forestRootDomain.DistinguishedName Register-StringMapping -Name '%RootDomainSID%' -Value $forestRootSID Register-StringMapping -Name '%ForestFqdn%' -Value $forestObject.Name } } <# 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 'DCManagement' -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 'DCManagement' -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 'DCManagement' -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 'DCManagement' -Name 'Defaults.NoDNS' -Value $false -Validation bool -Initialize -Description 'Default value for "NoDNS" parameter when creating a new forest' Set-PSFConfig -Module 'DCManagement' -Name 'Defaults.NoReboot' -Value $false -Validation bool -Initialize -Description 'Default value for "NoReboot" parameter when creating a new forest' Set-PSFConfig -Module 'DCManagement' -Name 'Defaults.LogPath' -Value 'C:\Windows\NTDS' -Validation string -Initialize -Description 'Default value for "LogPath" parameter when creating a new forest' Set-PSFConfig -Module 'DCManagement' -Name 'Defaults.SysvolPath' -Value 'C:\Windows\SYSVOL' -Validation string -Initialize -Description 'Default value for "SysvolPath" parameter when creating a new forest' Set-PSFConfig -Module 'DCManagement' -Name 'Defaults.DatabasePath' -Value 'C:\Windows\NTDS' -Validation string -Initialize -Description 'Default value for "DatabasePath" parameter when creating a new forest' <# 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 'DCManagement.ScriptBlockName' -Scriptblock { } #> <# # Example: Register-PSFTeppScriptblock -Name "DCManagement.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } #> <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name DCManagement.alcohol #> New-PSFLicense -Product 'DCManagement' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2019-11-14") -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. "@ $PSDefaultParameterValues['Resolve-String:ModuleName'] = 'ADMF.Core' $PSDefaultParameterValues['Register-StringMapping:ModuleName'] = 'ADMF.Core' $PSDefaultParameterValues['Clear-StringMapping:ModuleName'] = 'ADMF.Core' $PSDefaultParameterValues['Unregister-StringMapping:ModuleName'] = 'ADMF.Core' Register-PSFCallback -Name DCManagement.ConfigurationReset -ModuleName ADMF.Core -CommandName Clear-AdcConfiguration -ScriptBlock { Clear-DCConfiguration } # Stores Share configuration $script:shares = @{ } # Stores File System Access Rule configuration $script:fileSystemAccessRules = @{ } #endregion Load compiled code |