Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions ForestManagement/ForestManagement.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
RootModule = 'ForestManagement.psm1'

# Version number of this module.
ModuleVersion = '1.5.82'
ModuleVersion = '1.6.85'

# ID used to uniquely identify this module
GUID = '7de4379d-17c8-48d3-bd6d-93279aef64bb'
Expand Down Expand Up @@ -47,8 +47,11 @@
# Functions to export from this module
FunctionsToExport = @(
'Clear-FMConfiguration'
'Get-FMAccessRule'
'Get-FMAcl'
'Get-FMCallback'
'Get-FMCertificate'
'Get-FMContentMode'
'Get-FMExchangeSchema'
'Get-FMForestLevel'
'Get-FMNTAuthStore'
Expand All @@ -58,6 +61,8 @@
'Get-FMSite'
'Get-FMSiteLink'
'Get-FMSubnet'
'Invoke-FMAccessRule'
'Invoke-FMAcl'
'Invoke-FMCertificate'
'Invoke-FMExchangeSchema'
'Invoke-FMForestLevel'
Expand All @@ -69,6 +74,8 @@
'Invoke-FMSite'
'Invoke-FMSiteLink'
'Invoke-FMSubnet'
'Register-FMAccessRule'
'Register-FMAcl'
'Register-FMCallback'
'Register-FMCertificate'
'Register-FMExchangeSchema'
Expand All @@ -81,6 +88,9 @@
'Register-FMSite'
'Register-FMSiteLink'
'Register-FMSubnet'
'Set-FMContentMode'
'Test-FMAccessRule'
'Test-FMAcl'
'Test-FMCertificate'
'Test-FMExchangeSchema'
'Test-FMForestLevel'
Expand All @@ -92,6 +102,8 @@
'Test-FMSite'
'Test-FMSiteLink'
'Test-FMSubnet'
'Unregister-FMAccessRule'
'Unregister-FMAcl'
'Unregister-FMCallback'
'Unregister-FMCertificate'
'Unregister-FMExchangeSchema'
Expand All @@ -106,13 +118,13 @@
)

# Cmdlets to export from this module
# CmdletsToExport = ''
CmdletsToExport = @()

# Variables to export from this module
# VariablesToExport = ''

# Aliases to export from this module
# AliasesToExport = ''
AliasesToExport = @()

# List of all modules packaged with this module
# ModuleList = @()
Expand Down
6 changes: 6 additions & 0 deletions ForestManagement/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.6.85 (2026-06-19)

- New: Component AccessRule - Managed access rules across Configuration and Schema naming context
- New: Component ACL - Manage owners and inheritance across Configuration and Schema naming context
- New: Component ContentMode - define management scope across Configuration and Schema naming context

## 1.5.82 (2025-10-31)

- Upd: General - Use the shared managed remoting feature, allowing configuring session options.
Expand Down
35 changes: 34 additions & 1 deletion ForestManagement/en-us/strings.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,25 @@
'Invoke-Callback.Invoking' = 'Executing callback: {0}' # $callback.Name
'Invoke-Callback.Invoking.Failed' = 'Error executing callback: {0}' # $callback.Name
'Invoke-Callback.Invoking.Success' = 'Successfully executed callback: {0}' # $callback.Name


'Invoke-FMAcl.MissingADObject' = 'The target object could not be found: {0}' # $testItem.Identity
'Invoke-FMAcl.NoAccess' = 'Failed to access Acl on {0}' # $testItem.Identity
'Invoke-FMAcl.OwnerNotResolved' = 'Was unable to resolve the current owner ({1}) of {0}' # $testItem.Identity, $testItem.ADObject.GetOwner([System.Security.Principal.SecurityIdentifier])
'Invoke-FMAcl.ShouldManage' = 'The ADObject {0} has no defined ACL state and should either be configured or removed' # $testItem.Identity
'Invoke-FMAcl.UpdatingInheritance' = 'Updating inheritance - Inheritance Disabled: {0}' # $testItem.Configuration.NoInheritance
'Invoke-FMAcl.UpdatingOwner' = 'Granting ownership to {0}' # ($testItem.Configuration.Owner | Resolve-String)

'Invoke-FMAccessRule.Access.Failed' = 'Failed to access ACL on {0}' # $testItem.Identity
'Invoke-FMAccessRule.AccessRule.Create' = 'Adding access rule for {0}, granting {1} ({2})' # $changeEntry.Configuration.IdentityReference, $changeEntry.Configuration.ActiveDirectoryRights, $changeEntry.Configuration.AccessControlType
'Invoke-FMAccessRule.AccessRule.Creation.Failed' = 'Failed to create accessrule at {0} for {1}' # $testItem.Identity, $changeEntry.Configuration.IdentityReference
'Invoke-FMAccessRule.AccessRule.Remove' = 'Removing access rule for {0}, granting {1} ({2}) from {3}' # $changeEntry.ADObject.IdentityReference, $changeEntry.ADObject.ActiveDirectoryRights, $changeEntry.ADObject.AccessControlType, $changeEntry.DistinguishedName
'Invoke-FMAccessRule.AccessRule.Remove.Error.Consistency' = 'Failed to remove access rule from {0}! This may be due to a consistency error. Investigate the object, it may be resolvable via the dsa GUI in the security tab' # $testItem.Identity
'Invoke-FMAccessRule.AccessRule.Remove.Failed' = 'Failed to removing access rule for {0}, granting {1} ({2}) from {3} for unknown reasons (sorry). If this persists, consider enabling the alternative deletion mode through the "Domainmanagement.AccessRules.Remove.Option2" configuration setting.' # $changeEntry.ADObject.IdentityReference, $changeEntry.ADObject.ActiveDirectoryRights, $changeEntry.ADObject.AccessControlType, $changeEntry.DistinguishedName
'Invoke-FMAccessRule.AccessRule.Restore' = 'Restoring access rule from schema default for {0}, granting {1} ({2})' # $changeEntry.Configuration.IdentityReference, $changeEntry.Configuration.ActiveDirectoryRights, $changeEntry.Configuration.AccessControlType
'Invoke-FMAccessRule.ADObject.Missing' = 'Cannot process access rules, due to missing AD object: {0}. Please ensure the domain object is created before trying to apply rules to it!' # $testItem.Identity
'Invoke-FMAccessRule.Processing.Execute' = 'Applying {0} out of {1} intended access rule changes' # ($testItem.Changed.Count - $failedCount), $testItem.Changed.Count
'Invoke-FMAccessRule.Processing.Rules' = 'Processing {1} access rule changes on {0}' # $testItem.Identity, $testItem.Changed.Count

'Invoke-FMCertificate.Add' = 'Adding {1} certificate: {0}' # $testResult.Configuration.Certificate.Subject, $testResult.Configuration.Type
'Invoke-FMCertificate.Invalid.Input' = 'Invalid input - not a valid testresult object returned by Test-FMCertificate: {0}' # $testResult
'Invoke-FMCertificate.Remove' = 'Removing certificate {0} from {1}' # $testResult.ADObject.Subject, $testResult.ADObject.ADObject
Expand Down Expand Up @@ -105,7 +123,22 @@
'Remove-SchemaAdminCredential.TemporaryAccount.Remove' = 'Removing temporary schema admin account {0}' # $script:temporarySchemaUpdateUser.Name
'Remove-SchemaAdminCredential.TemporaryAccount.Remove.Failed' = 'Failed to remove temporary schema admin account {0}' # $script:temporarySchemaUpdateUser.Name

'Resolve-ContentSearchBase.Exclude.NotFound' = 'Failed to find excluded ou/container: {0}' # $item.Name
'Resolve-ContentSearchBase.Include.NotFound' = 'Failed to find included ou/container: {0}' # $item.Name
'Resolve-ContentSearchBase.Searchbase.Found' = 'Resolved searchbase in {2}: {0} | {1}' # $searchBase.SearchScope, $searchBase.SearchBase, $script:domainContext.Fqdn

'Resolve-SchemaAttribute.Update.SystemOnlyError' = 'Cannot update {0} to {1} on {2}. The attribute property is system protected and can only ever be defined when creating a new attribute! This cannot be undone and only replacing the attribute with a new attribute will allow you to resolve the issue.' # $attributeName, $attributes.$attributeName, $ADObject

'Set-FMContentMode.Error.UnknownExcludedComponent' = 'Error excluding a Component from the Forest Content Mode. Unexpected Component: {0}. Ensure the Component specified not only exists, but also supports being excluded from Forest Content Mode.' # $pair.Key

'Test-FMAccessRule.DefaultPermission.Failed' = 'Failed to retrieve default permissions from Schema when connecting to {0}' # $Server
'Test-FMAccessRule.NoAccess' = 'Failed to access {0}' # $resolvedPath
'Test-FMAccessRule.Parallel.Error' = 'Failed to process {0}' # $fail.ADObject

'Test-FMAcl.ADObjectNotFound' = 'The target object could not be found: {0}' # $resolvedPath
'Test-FMAcl.NoAccess' = 'Failed to access Acl on {0}' # $resolvedPath
'Test-FMAcl.ResolveOwner' = 'Resolving acceptable owners via Privileged Group Set {0} for object {1}' # $Category.Owner.Trim('_'),$ADObject.DistinguishedName

'Test-FMSchema.Connect.Failed' = 'Failed to connect to {0}' # $Server

'Test-FMSchemaDefaultPermission.Class.IdentityUncertain' = 'Unable to resolve all identities for the default permissions to apply to {0}. This objectclass will be skipped instead.' # $Configuration[0].ClassName
Expand Down
32 changes: 32 additions & 0 deletions ForestManagement/functions/AccessRule/Get-FMAccessRule.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
function Get-FMAccessRule
{
<#
.SYNOPSIS
Returns the list of configured access rules.

.DESCRIPTION
Returns the list of configured access rules.
These access rules define the desired state where delegation in a forest's configuration & schema naming context is concerned.
This is consumed by Test-FMAccessRule, see the help on that command for more details.

.PARAMETER Identity
The Identity to filter by.
This allows swiftly filtering by who is being granted permission.

.EXAMPLE
PS C:\> Get-FMAccessRule

Returns a list of all registered accessrules
#>
[CmdletBinding()]
Param (
[string]
$Identity = '*'
)

process
{
$($script:accessRules.Values | Where-Object IdentityReference -like $Identity)
$($script:accessCategoryRules.Values | Where-Object IdentityReference -like $Identity)
}
}
166 changes: 166 additions & 0 deletions ForestManagement/functions/AccessRule/Invoke-FMAccessRule.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
function Invoke-FMAccessRule {
<#
.SYNOPSIS
Applies the desired state of accessrule configuration.

.DESCRIPTION
Applies the desired state of accessrule configuration.
Define the desired state with Register-FMAccessRule.
Test the desired state with Test-FMAccessRule.

.PARAMETER InputObject
Test results provided by the associated test command.
Only the provided changes will be executed, unless none were specified, in which ALL pending changes will be executed.

.PARAMETER Server
The server / domain to work with.

.PARAMETER Credential
The credentials to use for this operation.

.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.

.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:\> Invoke-FMAccessRule -Server contoso.com

Applies the desired access rule configuration to the contoso.com forest.
#>
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
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-Callback @parameters -Cmdlet $PSCmdlet
Assert-Configuration -Type accessRules -Cmdlet $PSCmdlet
Set-FMDomainContext @parameters

$alternativeRemoval = Get-PSFConfigValue -FullName 'ForestManagement.AccessRules.Remove.Option2' -Fallback $false
}
process {
if (-not $InputObject) {
$InputObject = Test-FMAccessRule @parameters
}

foreach ($testItem in $InputObject) {
# Catch invalid input - can only process test results
if ($testItem.PSObject.TypeNames -notcontains 'ForestManagement.AccessRule.TestResult') {
Stop-PSFFunction -String 'General.Invalid.Input' -StringValues 'Test-FMAccessRule', $testItem -Target $testItem -Continue -EnableException $EnableException
}

switch ($testItem.Type) {
'Update' {
Write-PSFMessage -Level Debug -String 'Invoke-FMAccessRule.Processing.Rules' -StringValues $testItem.Identity, $testItem.Changed.Count -Target $testItem

try { $aclObject = Get-AdsAcl @parameters -Path $testItem.Identity -EnableException }
catch { Stop-PSFFunction -String 'Invoke-FMAccessRule.Access.Failed' -StringValues $testItem.Identity -EnableException $EnableException -Target $testItem -Continue -ErrorRecord $_ }
$failedCount = 0
foreach ($changeEntry in $testItem.Changed) {
#region Remove Access Rules
if ($changeEntry.Type -eq 'Delete') {
Write-PSFMessage -Level InternalComment -String 'Invoke-FMAccessRule.AccessRule.Remove' -StringValues $changeEntry.ADObject.IdentityReference, $changeEntry.ADObject.ActiveDirectoryRights, $changeEntry.ADObject.AccessControlType, $changeEntry.DistinguishedName -Target $changeEntry
try { $aclObject.RemoveAccessRuleSpecific($changeEntry.ADObject.OriginalRule) }
catch {
Write-PSFMessage -Level Warning -String 'Invoke-FMAccessRule.AccessRule.Remove.Error.Consistency' -StringValues $testItem.Identity -Target $changeEntry
continue
}
Remove-RedundantAce -AccessControlList $aclObject -IdentityReference $changeEntry.ADObject.OriginalRule.IdentityReference

$stillThere = $false
foreach ($rule in $aclObject.GetAccessRules($true, $false, [System.Security.Principal.NTAccount])) {
if (Test-AdcAccessRuleEquality -Parameters $parameters -Rule1 $rule -Rule2 $changeEntry.ADObject.OriginalRule) {
$stillThere = $true
$failedCount = $failedCount + 1
break
}
}

if ($stillThere -and $alternativeRemoval) {
$null = $aclObject.RemoveAccessRule($changeEntry.ADObject.OriginalRule)
Remove-RedundantAce -AccessControlList $aclObject -IdentityReference $changeEntry.ADObject.OriginalRule.IdentityReference

$stillThere = $false
foreach ($rule in $aclObject.GetAccessRules($true, $false, [System.Security.Principal.NTAccount])) {
if (Test-AdcAccessRuleEquality -Parameters $parameters -Rule1 $rule -Rule2 $changeEntry.ADObject.OriginalRule) {
$stillThere = $true
$failedCount = $failedCount + 1
break
}
}
}

if ($stillThere) {
Write-PSFMessage -Level Warning -String 'Invoke-FMAccessRule.AccessRule.Remove.Failed' -StringValues $changeEntry.ADObject.IdentityReference, $changeEntry.ADObject.ActiveDirectoryRights, $changeEntry.ADObject.AccessControlType, $changeEntry.DistinguishedName -Target $changeEntry -Debug:$false
}
continue
}
#endregion Remove Access Rules

#region Add Access Rules
if ($changeEntry.Type -eq 'Create') {
Write-PSFMessage -Level InternalComment -String 'Invoke-FMAccessRule.AccessRule.Create' -StringValues $changeEntry.Configuration.IdentityReference, $changeEntry.Configuration.ActiveDirectoryRights, $changeEntry.Configuration.AccessControlType -Target $changeEntry
try {
if (-not $changeEntry.Configuration.ObjectType) { throw "Unknown ObjectType! Unable to translate $($changeEntry.Configuration.ObjectTypeName). Validate the configuration and ensure pending schema updates (e.g. Exchange, Skype, etc.) have been applied." }
if (-not $changeEntry.Configuration.InheritedObjectType) { throw "Unknown InheritedObjectType! Unable to translate $($changeEntry.Configuration.InheritedObjectTypeName). Validate the configuration and ensure pending schema updates (e.g. Exchange, Skype, etc.) have been applied." }
$accessRule = [System.DirectoryServices.ActiveDirectoryAccessRule]::new((Convert-AdcPrincipal @parameters -Name $changeEntry.Configuration.IdentityReference), $changeEntry.Configuration.ActiveDirectoryRights, $changeEntry.Configuration.AccessControlType, $changeEntry.Configuration.ObjectType, $changeEntry.Configuration.InheritanceType, $changeEntry.Configuration.InheritedObjectType)
}
catch {
$failedCount = $failedCount + 1
Stop-PSFFunction -String 'Invoke-FMAccessRule.AccessRule.Creation.Failed' -StringValues $testItem.Identity, $changeEntry.Configuration.IdentityReference -EnableException $EnableException -Target $changeEntry -Continue -ErrorRecord $_
}
$null = $aclObject.AddAccessRule($accessRule)
#TODO: Validation and remediation of success. Adding can succeed but not do anything, when accessrules are redundant. Potentially flag it for full replacement?
continue
}
#endregion Add Access Rules

#region Restore Default Access Rules
if ($changeEntry.Type -eq 'Restore') {
Write-PSFMessage -Level InternalComment -String 'Invoke-FMAccessRule.AccessRule.Restore' -StringValues $changeEntry.Configuration.IdentityReference, $changeEntry.Configuration.ActiveDirectoryRights, $changeEntry.Configuration.AccessControlType -Target $changeEntry
try {
if (-not $changeEntry.Configuration.ObjectType) { throw "Unknown ObjectType! Unable to translate $($changeEntry.Configuration.ObjectTypeName). Validate the configuration and ensure pending schema updates (e.g. Exchange, Skype, etc.) have been applied." }
if (-not $changeEntry.Configuration.InheritedObjectType) { throw "Unknown InheritedObjectType! Unable to translate $($changeEntry.Configuration.InheritedObjectTypeName). Validate the configuration and ensure pending schema updates (e.g. Exchange, Skype, etc.) have been applied." }
$accessRule = [System.DirectoryServices.ActiveDirectoryAccessRule]::new((Convert-AdcPrincipal @parameters -Name $changeEntry.Configuration.IdentityReference), $changeEntry.Configuration.ActiveDirectoryRights, $changeEntry.Configuration.AccessControlType, $changeEntry.Configuration.ObjectType, $changeEntry.Configuration.InheritanceType, $changeEntry.Configuration.InheritedObjectType)
}
catch {
$failedCount = $failedCount + 1
Stop-PSFFunction -String 'Invoke-FMAccessRule.AccessRule.Creation.Failed' -StringValues $testItem.Identity, $changeEntry.Configuration.IdentityReference -EnableException $EnableException -Target $changeEntry -Continue -ErrorRecord $_
}
$null = $aclObject.AddAccessRule($accessRule)
#TODO: Validation and remediation of success. Adding can succeed but not do anything, when accessrules are redundant. Potentially flag it for full replacement?
continue
}
#endregion Restore Default Access Rules
}
Invoke-PSFProtectedCommand -ActionString 'Invoke-FMAccessRule.Processing.Execute' -ActionStringValues ($testItem.Changed.Count - $failedCount), $testItem.Changed.Count -Target $testItem -ScriptBlock {
Set-AdsAcl @parameters -Path $testItem.Identity -AclObject $aclObject -EnableException -Confirm:$false
} -EnableException $EnableException -PSCmdlet $PSCmdlet -Continue
}
'MissingADObject' {
Write-PSFMessage -Level Warning -String 'Invoke-FMAccessRule.ADObject.Missing' -StringValues $testItem.Identity -Target $testItem -Debug:$false
}
}
}
}
}
Loading