diff --git a/ADMF.Core/ADMF.Core.psd1 b/ADMF.Core/ADMF.Core.psd1 index e94aff6..af24dea 100644 --- a/ADMF.Core/ADMF.Core.psd1 +++ b/ADMF.Core/ADMF.Core.psd1 @@ -3,7 +3,7 @@ RootModule = 'ADMF.Core.psm1' # Version number of this module. - ModuleVersion = '1.2.12' + ModuleVersion = '1.4.20' # ID used to uniquely identify this module GUID = '11e2d894-33d7-4020-a65e-f13c2f1893aa' @@ -21,13 +21,14 @@ Description = 'Central Tooling used across available across all ADMF Project modules' # Minimum version of the Windows PowerShell engine required by this module - PowerShellVersion = '5.0' + PowerShellVersion = '5.1' # Modules that must be imported into the global environment prior to importing # this module RequiredModules = @( @{ ModuleName = 'PSFramework'; ModuleVersion = '1.13.416' } + # @{ ModuleName = 'Principal'; ModuleVersion = '1.0.0' } # @{ ModuleName = 'ResolveString'; ModuleVersion = '1.0.0' } ) @@ -43,25 +44,65 @@ # Functions to export from this module FunctionsToExport = @( 'Clear-AdcConfiguration' + 'Compare-AdcAccessRules' 'Compare-AdcProperty' + 'Convert-AdcAccessRuleIdentity' + 'Convert-AdcPrincipal' + 'Convert-AdcSchemaGuid' + 'ConvertFrom-AdcAccessRuleConfiguration' + 'Find-AdcObjectCategoryItem' + 'Get-AdcAccessRuleMode' + 'Get-AdcCategoryBasedRules' + 'Get-AdcDomain' + 'Get-AdcDomainCredential' 'Get-AdcExchangeVersion' + 'Get-AdcObjectCategory' + 'Get-AdcObjectDefaultPermission' + 'Get-AdcPrincipal' + 'Get-AdcPrivilegedGroupSet' 'Get-AdcRemotingConfig' 'New-AdcChange' 'New-AdcPSSession' + 'Register-AdcAccessRuleMode' + 'Register-AdcBuiltInSID' + 'Register-AdcObjectCategory' + 'Register-AdcPrivilegedGroupSet' + 'Resolve-AdcPrivilegedGroupSet' 'Remove-AdcRemotingConfig' + 'Reset-AdcDomainCredential' + 'Resolve-AdcAccessRuleMode' + 'Resolve-AdcAceIdentity' + 'Resolve-AdcObjectCategory' + 'Set-AdcDomainCredential' 'Set-AdcRemotingConfig' 'Sync-AdcObject' + 'Test-AdcAccessRuleEquality' + 'Unregister-AdcAccessRuleMode' + 'Unregister-AdcObjectCategory' 'Write-AdcChangeLog' ) # Cmdlets to export from this module - # CmdletsToExport = '' + CmdletsToExport = @() # Variables to export from this module # VariablesToExport = '' # Aliases to export from this module - # AliasesToExport = '' + AliasesToExport = @( + 'Get-DMDomainCredential' + 'Get-DMObjectDefaultPermission' + 'Reset-DMDomainCredential' + 'Set-DMDomainCredential' + + 'Find-DMObjectCategoryItem' + 'Get-DMObjectCategory' + 'Register-DMObjectCategory' + 'Resolve-DMObjectCategory' + 'Unregister-DMObjectCategory' + + 'Register-DMBuiltInSID' + ) # List of all modules packaged with this module # ModuleList = @() diff --git a/ADMF.Core/changelog.md b/ADMF.Core/changelog.md index ef54cde..874189f 100644 --- a/ADMF.Core/changelog.md +++ b/ADMF.Core/changelog.md @@ -1,5 +1,16 @@ # Changelog +## 1.4.20 (2026-06-19) + +- New: Tool: Privileged Group Set - support tool for ACL, allowing multiple acceptable owners for a resource +- Upd: Component AccessRuleMode - moved over from DomainManagement +- Upd: Component AccessRules - partially moved over from DomainManagement +- Upd: Component DomainCredentials - moved over from DomainManagement +- Upd: Component ObjectCategories - moved over from DomainManagement +- Upd: Tools - moved over most of the Identity resolution tools from DomainManagement +- Upd: Get-AdcObjectDefaultPermission - moved over from DomainManagement and Get-DMObjectDefaultPermission +- Upd: New-AdcPSSession - added `Server` alias for parameter `ComputerName`, to allow using the same connection hashtable for AD and PSRemoting commands. + ## 1.2.12 (2025-10-31) - New: Remoting config handling, to allow specifying the settings used during invocation. diff --git a/ADMF.Core/en-us/strings.psd1 b/ADMF.Core/en-us/strings.psd1 index fcdcb47..856ed32 100644 --- a/ADMF.Core/en-us/strings.psd1 +++ b/ADMF.Core/en-us/strings.psd1 @@ -1,18 +1,51 @@ # This is where the strings go, that are written by # Write-PSFMessage, Stop-PSFFunction or the PSFramework validation scriptblocks @{ - 'Get-LdapObject.SearchError' = 'Failed to execute ldap request.' # - 'Get-LdapObject.Searchfilter' = 'Searching with filter: {0}' # $LdapFilter - 'Get-LdapObject.SearchRoot' = 'Searching {0} in {1}' # $SearchScope, $searcher.SearchRoot.Path + 'Compare-AdcAccessRule.Error.BadIdentity' = 'Failed to resolve Identity "{1}" configured for "{0}"' # $ADObject, $configuredRule.IdentityReference - 'New-AdcPSSession.Connecting' = 'Connecting via PS Remoting to {0}' # $ComputerName - 'New-AdcPSSession.Connecting.Config' = 'Remoting Config used for {0} | Target: {1} | Options: {2} | SSH: {3} | Parameters: {4}' # $ComputerName, $config.Target, $config.HasOptions, $config.UseSSH, @($config.Parameters).Count - 'Sync-AdcObject.ConnectError' = 'Failed to connect to {0}' # $errorObject.TargetObject + 'Convert-AdcPrincipal.Processing' = 'Converting principal: {0}' # $Name + 'Convert-AdcPrincipal.Processing.InputNT' = 'Input detected as NT: {0}' # $Name + 'Convert-AdcPrincipal.Processing.InputSID' = 'Input detected as SID: {0}' # $Name + 'Convert-AdcPrincipal.Processing.NT.LdapFilter' = 'Resolving NT identity via AD using the following filter: {0}' # "(samAccountName=$namePart)" + 'Convert-AdcPrincipal.Processing.NTDetails' = 'Resolved NT identity: Domain = {0} | Name = {1}' # $domainPart, $namePart - 'Sync-LdapObject.DestinationAccessError' = 'Failed to connect to destination server {0} | {1}' # $Target, $_ - 'Sync-LdapObject.PerformingReplication' = 'Performing replication from {0} to {1}' # $Server, $Target - 'Sync-LdapObject.SourceAccessError' = 'Failed to connect to source server {0} | {1}' # $Server, $_ + 'ConvertFrom-AdcAccessRuleConfiguration.Identity.ResolutionError' = 'Failed to convert identity "{0}" on "{1}. This generally means a configuration error, especially when referencing the parent as identity.' # $ruleObject.IdentityReference, $ADObject + + 'Find-AdcObjectCategoryItem.ADError' = 'Error retrieving AD Objects for object category {0}' # $Name + 'Find-AdcObjectCategoryItem.Category.NotFound' = 'No such object category defined: {0}' # $Name + + 'Get-AdcCategoryBasedRules.Identity.ResolutionError' = 'Error resolving Access Rule Identity {0} for a rule configured for the Object Category {1}' # $ruleObject.IdentityReference, $resolvedCategory.Name + + 'Get-AdcPrincipal.Resolution.Failed' = 'Failed to resolve principal: SID {0} | Name {1} | ObjectClass {2} | Domain {3}' # $Sid, $Name, $ObjectClass, $Domain + 'Get-AdcPrincipal.Resolution.FailedWithTarget' = 'Failed to resolve principal: SID {0} | Name {1} | ObjectClass {2} | Domain {3} | Target {4}' # $Sid, $Name, $ObjectClass, $Domain, $Target + + 'Get-LdapObject.SearchError' = 'Failed to execute ldap request.' # + 'Get-LdapObject.Searchfilter' = 'Searching with filter: {0}' # $LdapFilter + 'Get-LdapObject.SearchRoot' = 'Searching {0} in {1}' # $SearchScope, $searcher.SearchRoot.Path + + 'Get-PermissionGuidMapping.Processing' = 'Processing Permission Guids for domain: {0} (This may take a while)' # $identity + + 'Get-SchemaGuidMapping.Processing' = 'Processing Schema Guids for domain: {0} (This may take a while)' # $identity + + 'New-AdcPSSession.Connecting' = 'Connecting via PS Remoting to {0}' # $ComputerName + 'New-AdcPSSession.Connecting.Config' = 'Remoting Config used for {0} | Target: {1} | Options: {2} | SSH: {3} | Parameters: {4}' # $ComputerName, $config.Target, $config.HasOptions, $config.UseSSH, @($config.Parameters).Count + + 'Resolve-AdcAccessRuleMode.PathResolution.Failed' = 'Unable to resolve path: {0}' # $mode.Path + + 'Resolve-AdcAceIdentity.ParentObject.NoSecurityPrincipal' = 'Error resolving the parent object of {0} for the purpose of access rule or owner assignment. While {1} (of type {2}) can be found, it does not have a SID and cannot be assigned any permissions' # $ADObject, $parentObject.Name, $parentObject.ObjectClass + + 'Resolve-AdcPrivilegedGroupSet.Error.NoDefaultSid' = 'Error resolving the Privileged Group Set "{0}" agaiunst Domain "{1}": No default SID was included in the resolution result!' # $Type, $domain.DnsRoot + 'Resolve-AdcPrivilegedGroupSet.Error.NoSids' = 'Error resolving the Privileged Group Set "{0}" agaiunst Domain "{1}": No SIDs were included in the resolution result!' # $Type, $domain.DnsRoot + 'Resolve-AdcPrivilegedGroupSet.Error.UnknownPGS' = 'Error resolving the Privileged Group Set "{0}". No PGS of that name was found! Check your configuration and make sure no typos were made. PGS known: {1}' # $Type, ($script:privilegedGroupSets.Keys -join ", ") + 'Resolve-AdcPrivilegedGroupSet.Resolving' = 'Resolving the identities that are part of the Privileged Group Set "{0}" in domain "{1}"' # $Type, $domain.DnsRoot + + + 'Sync-AdcObject.ConnectError' = 'Failed to connect to {0}' # $errorObject.TargetObject + + 'Sync-LdapObject.DestinationAccessError' = 'Failed to connect to destination server {0} | {1}' # $Target, $_ + 'Sync-LdapObject.PerformingReplication' = 'Performing replication from {0} to {1}' # $Server, $Target + 'Sync-LdapObject.SourceAccessError' = 'Failed to connect to source server {0} | {1}' # $Server, $_ - 'Write-AdcChangeLog.ChangeEntry' = 'Updating {0} from {1} to {2} on {3}' # $change.Property, $change.Old, $change.New, $change.Identity + 'Write-AdcChangeLog.ChangeEntry' = 'Updating {0} from {1} to {2} on {3}' # $change.Property, $change.Old, $change.New, $change.Identity } \ No newline at end of file diff --git a/ADMF.Core/functions/AccessRuleMode/Get-AdcAccessRuleMode.ps1 b/ADMF.Core/functions/AccessRuleMode/Get-AdcAccessRuleMode.ps1 new file mode 100644 index 0000000..fa24e1c --- /dev/null +++ b/ADMF.Core/functions/AccessRuleMode/Get-AdcAccessRuleMode.ps1 @@ -0,0 +1,33 @@ +function Get-AdcAccessRuleMode { + <# + .SYNOPSIS + Retrieve registered AccessRule processing modes. + + .DESCRIPTION + Retrieve registered AccessRule processing modes. + These are used to define, how AccessRules will be processed. + + .PARAMETER Path + Filter by the path the AccessRule processing mode applies to. + + .PARAMETER ObjectCategory + Filter by the object category the AccessRule processing mode applies to. + + .EXAMPLE + PS C:\> Get-AdcAccessRuleMode + + List all registered AccessRule processing modes. + #> + [CmdletBinding()] + param ( + [string] + $Path = '*', + + [string] + $ObjectCategory = '*' + ) + + process { + $script:accessRuleMode.Values | Where-Object Path -Like $Path | Where-Object ObjectCategory -Like $ObjectCategory + } +} diff --git a/ADMF.Core/functions/AccessRuleMode/Register-AdcAccessRuleMode.ps1 b/ADMF.Core/functions/AccessRuleMode/Register-AdcAccessRuleMode.ps1 new file mode 100644 index 0000000..5a27c5d --- /dev/null +++ b/ADMF.Core/functions/AccessRuleMode/Register-AdcAccessRuleMode.ps1 @@ -0,0 +1,73 @@ +function Register-AdcAccessRuleMode { + <# + .SYNOPSIS + Register the processing mode for access rules on a specified object. + + .DESCRIPTION + Register the processing mode for access rules on a specified object. + This is used by the AccessRule Component exclusively. + + .PARAMETER Path + The path to the AD object to govern. + This should be a distinguishedname. + This path uses name resolution. + For example %DomainDN% will be replaced with the DN of the target domain itself (and should probably be part of everyy single path). + + .PARAMETER PathMode + Whether to only target a specific path or the target path and all items beneath it. + + .PARAMETER ObjectCategory + Instead of a path, define a category to apply the processing mode to. + Categories are defined using Register-DMObjectCategory. + This allows you to apply processing mode to a category of objects, rather than a specific path. + With this you could apply a processing mode to all domain controller objects, for example. + + .PARAMETER Mode + Determines, how the AccessRules are applied on the target object: + - Constrained: All non-defined AccessRules will be removed. + - Defined: Only non-defined AccessRules with identities for which a configuration exists on the object will be deleted. + - Additive: Non-defined AccessRules on the targeted object will be ignored. + By default, with no AccessRuleMode defined, all objects are considered to be in Constrained mode. + + .EXAMPLE + PS C:\> Register-AdcAccessRuleMode -Path 'OU=Company,%DomainDN%' -PathMode SubTree -Mode Additive + + Configures the specified OU and all items beneath it to be in additive mode. + Defined AccessRules will be applied if missing, but previously existing rules remain untouched. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Path')] + [string] + $Path, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Path')] + [ValidateSet('SingleItem', 'SubTree')] + [string] + $PathMode = 'SingleItem', + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Category')] + [string] + $ObjectCategory, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [ValidateSet('Constrained', 'Defined', 'Additive')] + [string] + $Mode + ) + + process { + $identity = 'Path:{0}:{1}' -f $PathMode, $Path + if ($ObjectCategory) { $identity = 'Category:{0}' -f $ObjectCategory } + + $script:accessRuleMode[$identity] = [PSCustomObject]@{ + PSTypeName = 'ADMF.Core.AccessRuleMode' + Identity = $identity + Type = $PSCmdlet.ParameterSetName + Path = $Path + PathMode = $PathMode + ObjectCategory = $ObjectCategory + Mode = $Mode + } + } +} diff --git a/ADMF.Core/functions/AccessRuleMode/Resolve-AdcAccessRuleMode.ps1 b/ADMF.Core/functions/AccessRuleMode/Resolve-AdcAccessRuleMode.ps1 new file mode 100644 index 0000000..5aa6a92 --- /dev/null +++ b/ADMF.Core/functions/AccessRuleMode/Resolve-AdcAccessRuleMode.ps1 @@ -0,0 +1,81 @@ +function Resolve-AdcAccessRuleMode { + <# + .SYNOPSIS + Resolves the AccessRule processing mode that applies to the specified ADObject. + + .DESCRIPTION + Resolves the AccessRule processing mode that applies to the specified ADObject. + + .PARAMETER ADObject + The AD Object for which to resolve the AccessRule processing mode. + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .EXAMPLE + PS C:\> Resolve-AdcAccessRuleMode @parameters -ADObject $adObject + + Resolves the AccessRule processing mode that applies to the specified ADObject. + #> + [OutputType([string])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + $ADObject, + + [PSFComputer] + $Server, + + [PSCredential] + $Credential + ) + + begin { + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + } + process { + if ($script:accessRuleMode.Count -lt 1) { return 'Constrained' } + + $relevantCategories = @() + if ($script:accessRuleMode.Values.ObjectCategory) { + $relevantCategories = Resolve-AdcObjectCategory -ADObject $ADObject @parameters + } + + $applicableModes = :main foreach ($mode in $script:accessRuleMode.Values) { + if ($mode.Path) { + try { $resolvedPath = $mode.Path | Resolve-String } # @parameters + catch { + Write-PSFMessage -Level Warning -String 'Resolve-AdcAccessRuleMode.PathResolution.Failed' -StringValues $mode.Path -ErrorRecord $_ + $resolvedPath = $mode.Path | Resolve-String + } + switch ($mode.PathMode) { + 'SingleItem' { + if ($ADObject.DistinguishedName -eq $resolvedPath) { $mode } + continue main + } + 'SubTree' { + if ($ADObject.DistinguishedName -like "*$resolvedPath") { $mode } + continue main + } + } + } + if ($mode.ObjectCategory -and ($mode.ObjectCategory -in $relevantCategories.Name)) { + $mode + } + } + + if ($primaryMode = $applicableModes | Where-Object { $_.Type -eq 'Path' -and $_.PathMode -eq 'SingleItem' }) { + return $primaryMode.Mode + } + if ($secondaryMode = $applicableModes | Where-Object Type -EQ 'Category' | Select-Object -First 1) { + return $secondaryMode.Mode + } + if ($tertiaryMode = $applicableModes | Where-Object { $_.Type -eq 'Path' -and $_.PathMode -eq 'SubTree' } | Sort-Object { $_.Path.Length } -Descending | Select-Object -First 1) { + return $tertiaryMode.Mode + } + return 'Constrained' + } +} diff --git a/ADMF.Core/functions/AccessRuleMode/Unregister-AdcAccessRuleMode.ps1 b/ADMF.Core/functions/AccessRuleMode/Unregister-AdcAccessRuleMode.ps1 new file mode 100644 index 0000000..1d3084a --- /dev/null +++ b/ADMF.Core/functions/AccessRuleMode/Unregister-AdcAccessRuleMode.ps1 @@ -0,0 +1,50 @@ +function Unregister-AdcAccessRuleMode { + <# + .SYNOPSIS + Removes previously registered AccessRule processing modes. + + .DESCRIPTION + Removes previously registered AccessRule processing modes. + Prioritizes Identity over Path over ObjectCategory. + + .PARAMETER Identity + The Identity of the AccessRule processing mode to remove. + + .PARAMETER Path + The Path of the AccessRule processing mode to remove. + + .PARAMETER ObjectCagegory + The ObjectCategory of the AccessRule processing mode to remove. + + .EXAMPLE + PS C:\> Get-AdcAccessRuleMode | Unregister-AdcAccessRuleMode + + Clears all registered AccessRule processing modes. + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [AllowEmptyString()] + [AllowNull()] + [string] + $Identity, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [AllowEmptyString()] + [AllowNull()] + [string] + $Path, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [AllowEmptyString()] + [AllowNull()] + [string] + $ObjectCagegory + ) + + process { + if ($Identity) { $script:accessRuleMode.Remove($Identity) } + elseif ($Path) { $script:accessRuleMode.Remove("Path:$Path") } + elseif ($ObjectCagegory) { $script:accessRuleMode.Remove("Category:$ObjectCategory") } + } +} diff --git a/ADMF.Core/functions/AccessRules/Compare-AdcAccessRules.ps1 b/ADMF.Core/functions/AccessRules/Compare-AdcAccessRules.ps1 new file mode 100644 index 0000000..8d585bd --- /dev/null +++ b/ADMF.Core/functions/AccessRules/Compare-AdcAccessRules.ps1 @@ -0,0 +1,210 @@ +function Compare-AdcAccessRules { + <# + .SYNOPSIS + Compare actual access rules to the system default and configured rules. + + .DESCRIPTION + Compare actual access rules to the system default and configured rules. + + .PARAMETER ADRules + The Access Rules actually deployed on the AD Object + + .PARAMETER ConfiguredRules + The Access Rules configured for the AD Object + + .PARAMETER DefaultRules + The default Access Rules for objects of this type + + .PARAMETER ADObject + The AD Object for which Access Rules are being compared + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .EXAMPLE + PS C:\> Compare-AdcAccessRule @parameters -ADRules ($adAclObject.Access | Convert-AdcAccessRuleIdentity @parameters) -ConfiguredRules $desiredPermissions -DefaultRules $defaultPermissions -ADObject $adObject + + Compare actual access rules on the specified AD Object to the system default and configured rules. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] + [CmdletBinding()] + param ( + [AllowEmptyCollection()] + $ADRules, + + [AllowEmptyCollection()] + $ConfiguredRules, + + [AllowEmptyCollection()] + $DefaultRules, + + $ADObject, + + [PSFComputer] + $Server, + + [PSCredential] + $Credential + ) + + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + + # Resolve the mode under which it will be evaluated. Either 'Additive' or 'Constrained' + $processingMode = Resolve-AdcAccessRuleMode @parameters -ADObject $adObject + + function Write-Result { + [CmdletBinding()] + param ( + [ValidateSet('Create', 'Delete', 'FixConfig', 'Restore')] + [Parameter(Mandatory = $true)] + $Type, + + $Identity, + + [AllowNull()] + $ADObject, + + [AllowNull()] + $Configuration, + + [string] + $DistinguishedName + ) + + $item = [PSCustomObject]@{ + PSTypeName = 'Admf.Core.AccessRule.Change' + Type = $Type + ACT = $ADObject.AccessControlType + Identity = $Identity + Rights = $ADObject.ActiveDirectoryRights + DistinguishedName = $DistinguishedName + ADObject = $ADObject + Configuration = $Configuration + } + if (-not $ADObject) { + $item.ACT = $Configuration.AccessControlType + $item.Rights = $Configuration.ActiveDirectoryRights + } + Add-Member -InputObject $item -MemberType ScriptMethod ToString -Value { '{0}: {1}' -f $this.Type, $this.Identity } -Force -PassThru + } + + $defaultRulesPresent = [System.Collections.ArrayList]::new() + $relevantADRules = :outer foreach ($adRule in $ADRules) { + if ($adRule.OriginalRule.IsInherited) { continue } + #region Skip OUs' "Protect from Accidential Deletion" ACE + if (($adRule.AccessControlType -eq 'Deny') -and ($ADObject.ObjectClass -in 'organizationalUnit', 'container')) { + if ($adRule.IdentityReference -eq 'everyone') { continue } + $eSid = [System.Security.Principal.SecurityIdentifier]'S-1-1-0' + $eName = $eSid.Translate([System.Security.Principal.NTAccount]) + if ($adRule.IdentityReference -eq $eName) { continue } + if ($adRule.IdentityReference -eq $eSid) { continue } + } + #endregion Skip OUs' "Protect from Accidential Deletion" ACE + + foreach ($defaultRule in $DefaultRules) { + if (Test-AdcAccessRuleEquality -Parameters $parameters -Rule1 $adRule -Rule2 $defaultRule) { + $null = $defaultRulesPresent.Add($defaultRule) + continue outer + } + } + $adRule + } + + #region Foreach non-default AD Rule: Check whether configured and delete if not so + :outer foreach ($relevantADRule in $relevantADRules) { + foreach ($configuredRule in $ConfiguredRules) { + if (Test-AdcAccessRuleEquality -Parameters $parameters -Rule1 $relevantADRule -Rule2 $configuredRule) { + # If explicitly defined for deletion, do so + if ('False' -eq $configuredRule.Present) { + Write-Result -Type Delete -Identity $relevantADRule.IdentityReference -ADObject $relevantADRule -DistinguishedName $ADObject + } + continue outer + } + } + + # Don't generate delete changes + if ($processingMode -eq 'Additive') { continue } + # Don't generate delete changes, unless we have configured a permission level for the affected identity + if ($processingMode -eq 'Defined') { + if (-not ($relevantADRule.IdentityReference | Compare-Identity -Parameters $parameters -ReferenceIdentity $ConfiguredRules.IdentityReference -IncludeEqual -ExcludeDifferent)) { + continue + } + } + + Write-Result -Type Delete -Identity $relevantADRule.IdentityReference -ADObject $relevantADRule -DistinguishedName $ADObject + } + #endregion Foreach non-default AD Rule: Check whether configured and delete if not so + + #region Foreach present Default-AD-Rule, check whether there is a Present: False rule for it + foreach ($defaultRulePresent in $defaultRulesPresent) { + foreach ($configuredRule in $ConfiguredRules) { + if ('False' -ne $configuredRule.Present) { continue } + + if (Test-AdcAccessRuleEquality -Parameters $parameters -Rule1 $defaultRulePresent -Rule2 $configuredRule) { + Write-Result -Type Delete -Identity $defaultRulePresent.IdentityReference -ADObject $defaultRulePresent -DistinguishedName $ADObject + } + } + } + #endregion Foreach present Default-AD-Rule, check whether there is a Present: False rule for it + + #region Foreach configured rule: Check whether it exists as defined or make it so + :outer foreach ($configuredRule in $ConfiguredRules) { + if (-not (Test-Identity -Identity $configuredRule.IdentityReference -Parameters $parameters)) { + Write-PSFMessage -Level Warning -String 'Compare-AdcAccessRule.Error.BadIdentity' -StringValues $ADObject, $configuredRule.IdentityReference -Target $ADObject + if (Get-PSFConfigValue -FullName 'ADMF.Core.AccessRule.IdentityErrorChange') { + Write-Result -Type IdentityError -Identity $configuredRule.IdentityReference -Configuration $configuredRule -DistinguishedName $ADObject + } + continue + } + + # Do not generate Create rules for any rule not configured for creation + if ('True' -ne $configuredRule.Present) { continue } + + foreach ($defaultRule in $DefaultRules) { + if ($configuredRule.NoFixConfig) { break } + if (Test-AdcAccessRuleEquality -Parameters $parameters -Rule1 $defaultRule -Rule2 $configuredRule) { + Write-Result -Type FixConfig -Identity $defaultRule.IdentityReference -ADObject $defaultRule -Configuration $configuredRule -DistinguishedName $ADObject + continue outer + } + } + + foreach ($relevantADRule in $relevantADRules) { + if (Test-AdcAccessRuleEquality -Parameters $parameters -Rule1 $relevantADRule -Rule2 $configuredRule) { + continue outer + } + } + + Write-Result -Type Create -Identity $configuredRule.IdentityReference -Configuration $configuredRule -DistinguishedName $ADObject + } + #endregion Foreach configured rule: Check whether it exists as defined or make it so + + #region Foreach non-existent default rule: Create unless configured otherwise + $domainControllersOUFilter = '*{0}' -f ('OU=Domain Controllers,%DomainDN%' | Resolve-String) + :outer foreach ($defaultRule in $DefaultRules | Where-Object { $_ -notin $defaultRulesPresent.ToArray() }) { + if (-not $DefaultRule) { continue } + + # In Additive Mode, do not restore to schema default + if ($processingMode -eq 'Additive') { break } + + # Do not apply restore to Domain Controllers OU, as it is already deployed intentionally diverging from the OU defaults + if ($ADObject -like $domainControllersOUFilter) { break } + + # Skip 'CREATOR OWNER' Rules, as those should never be restored. + # When creating an AD object that has this group as default permissions, it will instead + # Translate those to the identity creating the object + if ('S-1-3-0' -eq $defaultRule.SID) { continue } + + foreach ($configuredRule in $ConfiguredRules) { + if (Test-AdcAccessRuleEquality -Parameters $parameters -Rule1 $defaultRule -Rule2 $configuredRule) { + # If we explicitly don't want the rule: Skip and do NOT create a restoration action + if ('True' -ne $configuredRule.Present) { continue outer } + } + } + + Write-Result -Type Restore -Identity $defaultRule.IdentityReference -Configuration $defaultRule -DistinguishedName $ADObject + } + #endregion Foreach non-existent default rule: Create unless configured otherwise +} \ No newline at end of file diff --git a/ADMF.Core/functions/AccessRules/Convert-AdcAccessRuleIdentity.ps1 b/ADMF.Core/functions/AccessRules/Convert-AdcAccessRuleIdentity.ps1 new file mode 100644 index 0000000..53b828b --- /dev/null +++ b/ADMF.Core/functions/AccessRules/Convert-AdcAccessRuleIdentity.ps1 @@ -0,0 +1,75 @@ +function Convert-AdcAccessRuleIdentity { + <# + .SYNOPSIS + Converts the identity on the specified access rule to NT Account. + + .DESCRIPTION + Converts the identity on the specified access rule to NT Account. + + .PARAMETER InputObject + The Access Rules for which to convert the Identity. + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .PARAMETER Target + The target AD object this access rule applies to. + Used for logging only. + + .EXAMPLE + PS C:\> $adAclObject.Access | Convert-AdcAccessRuleIdentity @parameters + + Converts the identity on all Access Rules in $adAclObject to NT Account. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')] + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [System.DirectoryServices.ActiveDirectoryAccessRule[]] + $InputObject, + + [PSFComputer] + $Server, + + [PSCredential] + $Credential, + + [string] + $Target + ) + begin { + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + $domainObject = Get-AdcDomain @parameters + } + process { + :main foreach ($accessRule in $InputObject) { + if ($accessRule.IdentityReference -is [System.Security.Principal.NTAccount]) { + Add-Member -InputObject $accessRule -MemberType NoteProperty -Name OriginalRule -Value $accessRule -PassThru + continue main + } + + if (-not $accessRule.IdentityReference.AccountDomainSid) { + try { $identity = Get-AdcPrincipal @parameters -Sid $accessRule.IdentityReference -Domain $domainObject.DNSRoot -OutputType NTAccount -Target $Target } + catch { + # Empty Catch is OK here, warning happens in command + } + } + else { + try { $identity = Get-AdcPrincipal @parameters -Sid $accessRule.IdentityReference -Domain $accessRule.IdentityReference -OutputType NTAccount -Target $Target } + catch { + # Empty Catch is OK here, warning happens in command + } + } + if (-not $identity) { + $identity = $accessRule.IdentityReference + } + + $newRule = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($identity, $accessRule.ActiveDirectoryRights, $accessRule.AccessControlType, $accessRule.ObjectType, $accessRule.InheritanceType, $accessRule.InheritedObjectType) + # Include original object as property in order to facilitate removal if needed. + Add-Member -InputObject $newRule -MemberType NoteProperty -Name OriginalRule -Value $accessRule -PassThru + } + } +} \ No newline at end of file diff --git a/ADMF.Core/functions/AccessRules/ConvertFrom-AdcAccessRuleConfiguration.ps1 b/ADMF.Core/functions/AccessRules/ConvertFrom-AdcAccessRuleConfiguration.ps1 new file mode 100644 index 0000000..9b9366b --- /dev/null +++ b/ADMF.Core/functions/AccessRules/ConvertFrom-AdcAccessRuleConfiguration.ps1 @@ -0,0 +1,104 @@ +function ConvertFrom-AdcAccessRuleConfiguration { + <# + .SYNOPSIS + Resolves AccessRule configuration objects. + + .DESCRIPTION + Resolves AccessRule configuration objects. + For use by Access Rule configurations of both DomainManagement and ForestManagement. + + .PARAMETER Rule + The rule configuration to convert. + + .PARAMETER ADObject + The AD Object the rule applies to. + + .PARAMETER IncludeCategory + Whether to also return all Object-Category-based rules that apply to the object. + + .PARAMETER CategoryRules + Access rules defined via Object Categories + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .EXAMPLE + PS C:\> $script:accessRules[$key] | ConvertFrom-AdcAccessRuleConfiguration @parameters -ADObject $adObject -IncludeCategory + + Resolve all configured access rules stored in $script:accessRules[$key], including any applicable rules assigned via Object Category + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + $Rule, + + [Parameter(Mandatory = $true)] + $ADObject, + + [switch] + $IncludeCategory, + + [hashtable] + $CategoryRules, + + [PSFComputer] + $Server, + + [PSCredential] + $Credential + ) + begin { + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + $convertCmdName = { Convert-AdcSchemaGuid @parameters -OutType Name }.GetSteppablePipeline() + $convertCmdName.Begin($true) + $convertCmdGuid = { Convert-AdcSchemaGuid @parameters -OutType Guid }.GetSteppablePipeline() + $convertCmdGuid.Begin($true) + + $explicitRules = [System.Collections.ArrayList]@() + } + process { + foreach ($ruleObject in $Rule) { + $objectTypeGuid = $convertCmdGuid.Process($ruleObject.ObjectType)[0] + $objectTypeName = $convertCmdName.Process($ruleObject.ObjectType)[0] + $inheritedObjectTypeGuid = $convertCmdGuid.Process($ruleObject.InheritedObjectType)[0] + $inheritedObjectTypeName = $convertCmdName.Process($ruleObject.InheritedObjectType)[0] + + try { $identity = Resolve-AdcAceIdentity @parameters -IdentityReference $ruleObject.IdentityReference -ADObject $ADObject } + catch { + if ('True' -ne $ruleObject.Present) { continue } + Stop-PSFFunction -String 'ConvertFrom-AdcAccessRuleConfiguration.Identity.ResolutionError' -StringValues $ruleObject.IdentityReference, $ADObject -Target $ruleObject -ErrorRecord $_ -Continue + } + + $rule = [PSCustomObject]@{ + PSTypeName = 'ADMF.Core.AccessRule.Converted' + IdentityReference = $identity + AccessControlType = $ruleObject.AccessControlType + ActiveDirectoryRights = $ruleObject.ActiveDirectoryRights + InheritanceFlags = $ruleObject.InheritanceFlags + InheritanceType = $ruleObject.InheritanceType + InheritedObjectType = $inheritedObjectTypeGuid + InheritedObjectTypeName = $inheritedObjectTypeName + ObjectFlags = $ruleObject.ObjectFlags + ObjectType = $objectTypeGuid + ObjectTypeName = $objectTypeName + PropagationFlags = $ruleObject.PropagationFlags + Present = $ruleObject.Present + } + $null = $explicitRules.Add($rule) + $rule + } + } + end { + #region Inject Category-Based rules + if ($IncludeCategory) { + Get-AdcCategoryBasedRules -ADObject $ADObject @parameters -ConvertNameCommand $convertCmdName -ConvertGuidCommand $convertCmdGuid -CategoryRules $CategoryRules -ExplicitRules $explicitRules.ToArray() + } + #endregion Inject Category-Based rules + + $convertCmdName.End() + $convertCmdGuid.End() + } +} \ No newline at end of file diff --git a/ADMF.Core/functions/AccessRules/Get-AdcCategoryBasedRules.ps1 b/ADMF.Core/functions/AccessRules/Get-AdcCategoryBasedRules.ps1 new file mode 100644 index 0000000..595a230 --- /dev/null +++ b/ADMF.Core/functions/AccessRules/Get-AdcCategoryBasedRules.ps1 @@ -0,0 +1,111 @@ +function Get-AdcCategoryBasedRules { + <# + .SYNOPSIS + Returns all access rules applicable to an ad object via category rules. + + .DESCRIPTION + Returns all access rules applicable to an ad object via category rules. + + .PARAMETER ADObject + The AD Object for which to resolve access rules by category. + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .PARAMETER ConvertNameCommand + A steppable pipeline wrapping Convert-AdcSchemaGuid converting to name. + + .PARAMETER ConvertGuidCommand + A steppable pipeline wrapping Convert-AdcSchemaGuid converting to guid. + + .PARAMETER CategoryRules + All access rules defined via Object Categories. + These are compared against the list of ObjectCategories that apply to the object and then - if applicable - converted to access rules. + + .PARAMETER ExplicitRules + Explicitly assigned rules by configuration via Path. + Path-based assignment overrides category-based assignment. + + .EXAMPLE + PS C:\> Get-AdcCategoryBasedRules -ADObject $foundADObject @parameters -ConvertNameCommand $convertCmdName -ConvertGuidCommand $convertCmdGuid + + Returns all access rules applicable to $foundADObject via category rules. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + $ADObject, + + [PSFComputer] + $Server, + + [PSCredential] + $Credential, + + $ConvertNameCommand, + + $ConvertGuidCommand, + + [hashtable] + $CategoryRules, + + $ExplicitRules + ) + + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include ADObject, Server, Credential + + $resolvedCategories = Resolve-AdcObjectCategory @parameters + $processedRules = foreach ($resolvedCategory in $resolvedCategories) { + :byRule foreach ($ruleObject in $CategoryRules[$resolvedCategory.Name]) { + $objectTypeGuid = $ConvertGuidCommand.Process($ruleObject.ObjectType)[0] + $objectTypeName = $ConvertNameCommand.Process($ruleObject.ObjectType)[0] + $inheritedObjectTypeGuid = $ConvertGuidCommand.Process($ruleObject.InheritedObjectType)[0] + $inheritedObjectTypeName = $ConvertNameCommand.Process($ruleObject.InheritedObjectType)[0] + + try { $identity = Resolve-AdcAceIdentity @parameters -IdentityReference $ruleObject.IdentityReference } + catch { Stop-PSFFunction -String 'Get-AdcCategoryBasedRules.Identity.ResolutionError' -StringValues $ruleObject.IdentityReference, $resolvedCategory.Name -Target $ruleObject -ErrorRecord $_ -Continue } + + $categoryRule = [PSCustomObject]@{ + PSTypeName = 'DomainManagement.AccessRule.Converted' + IdentityReference = $identity + AccessControlType = $ruleObject.AccessControlType + ActiveDirectoryRights = $ruleObject.ActiveDirectoryRights + InheritanceFlags = $ruleObject.InheritanceFlags + InheritanceType = $ruleObject.InheritanceType + InheritedObjectType = $inheritedObjectTypeGuid + InheritedObjectTypeName = $inheritedObjectTypeName + ObjectFlags = $ruleObject.ObjectFlags + ObjectType = $objectTypeGuid + ObjectTypeName = $objectTypeName + PropagationFlags = $ruleObject.PropagationFlags + Present = $ruleObject.Present + } + # Path-based rules take precedence, when they attempt to do the exact same thing. + # Mostly so an explicit "Present = $false" can be applied to override a far-reaching category + foreach ($rule in $ExplicitRules) { + if (Test-AdcAccessRuleEquality -Rule1 $rule -Rule2 $categoryRule -Parameters $parameters) { + continue byRule + } + } + $categoryRule + } + } + + # When two category-rules clash, the Present = $false rule wins + $nonPresent = $processedRules | Where-Object Present -EQ 'False' + $present = $processedRules | Where-Object Present -NE 'False' + + :main foreach ($rule in $present) { + foreach ($denyRule in $nonPresent) { + if (Test-AdcAccessRuleEquality -Rule1 $rule -Rule2 $denyRule -Parameters $parameters) { + continue main + } + } + $rule + } + $nonPresent +} \ No newline at end of file diff --git a/ADMF.Core/functions/AccessRules/Get-AdcObjectDefaultPermission.ps1 b/ADMF.Core/functions/AccessRules/Get-AdcObjectDefaultPermission.ps1 new file mode 100644 index 0000000..cf1be34 --- /dev/null +++ b/ADMF.Core/functions/AccessRules/Get-AdcObjectDefaultPermission.ps1 @@ -0,0 +1,94 @@ +function Get-AdcObjectDefaultPermission { + <# + .SYNOPSIS + Gathers the default object permissions in AD. + + .DESCRIPTION + Gathers the default object permissions in AD. + Uses PowerShell remoting against the SchemaMaster to determine the default permissions, as local identity resolution is not reliable. + + .PARAMETER ObjectClass + The object class to look up. + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .EXAMPLE + PS C:\> Get-AdcObjectDefaultPermission -ObjectClass user + + Returns the default permissions for a user. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '')] + [Alias('Get-DMObjectDefaultPermission')] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $ObjectClass, + + [PSFComputer] + $Server = '', + + [PSCredential] + $Credential + ) + + begin { + if (-not $script:schemaObjectDefaultPermission) { + $script:schemaObjectDefaultPermission = @{ } + } + + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + + #region Scriptblock that gathers information on default permission + $gatherScript = { + $parameters = @{ Server = $env:COMPUTERNAME } + $rootDSE = Get-ADRootDSE @parameters + $classes = Get-ADObject @parameters -SearchBase $rootDSE.schemaNamingContext -LDAPFilter '(objectCategory=classSchema)' -Properties defaultSecurityDescriptor, lDAPDisplayName + foreach ($class in $classes) { + $acl = [System.DirectoryServices.ActiveDirectorySecurity]::new() + $acl.SetSecurityDescriptorSddlForm($class.defaultSecurityDescriptor) + + $access = foreach ($accessRule in $acl.Access) { + try { Add-Member -InputObject $accessRule -MemberType NoteProperty -Name SID -Value $accessRule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) } + catch { + # Do nothing, don't want the property if no SID is to be had + } + $accessRule + } + + # Workaround to prevent serialization issue. The $null of a loop return converts into an empty PSCustomObject, rather than actually being null, when serialized and deserialized. + if (-not $access) { $access = $null } + + [PSCustomObject]@{ + Class = $class.lDAPDisplayName + Access = $access + } + } + } + #endregion Scriptblock that gathers information on default permission + } + process { + if ($script:schemaObjectDefaultPermission["$Server"]) { + return $script:schemaObjectDefaultPermission["$Server"].$ObjectClass + } + + #region Process Gathering logic + if ($Server -ne '') { + $parameters['ComputerName'] = $parameters.Server + $parameters.Remove("Server") + } + + try { $data = Invoke-PSFCommand @parameters -ScriptBlock $gatherScript -ErrorAction Stop } + catch { throw } + $script:schemaObjectDefaultPermission["$Server"] = @{ } + foreach ($datum in $data) { + $script:schemaObjectDefaultPermission["$Server"][$datum.Class] = $datum.Access + } + $script:schemaObjectDefaultPermission["$Server"].$ObjectClass + #endregion Process Gathering logic + } +} \ No newline at end of file diff --git a/ADMF.Core/functions/AccessRules/Resolve-AdcAceIdentity.ps1 b/ADMF.Core/functions/AccessRules/Resolve-AdcAceIdentity.ps1 new file mode 100644 index 0000000..cd08dbe --- /dev/null +++ b/ADMF.Core/functions/AccessRules/Resolve-AdcAceIdentity.ps1 @@ -0,0 +1,73 @@ +function Resolve-AdcAceIdentity { + <# + .SYNOPSIS + Resolve an Identity Reference with special rules. + + .DESCRIPTION + Resolve an Identity Reference with special rules. + Resolves to a SID (preferred) or NT Account (Fallback). + + Special Rules: + resolves to the parent object in AD + + This is a helper tool to resolve Identities on Access Rules applied to (or ointended for) AD objects only. + + .PARAMETER IdentityReference + The Identity to resolve. + + .PARAMETER ADObject + The AD Object from which the access rules has been read where the Identity is being resolved. + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .EXAMPLE + PS C:\> Resolve-AdcAceIdentity -Identity $name -ADObject %adObject @parameters + + Resolve the Identity in $name + #> + [OutputType([System.Security.Principal.NTAccount])] + [CmdletBinding()] + param ( + [string] + $IdentityReference, + + $ADObject, + + [PSFComputer] + $Server, + + [PSCredential] + $Credential + ) + + #region Parent Resolution + if ($IdentityReference -eq '') { + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + $domainObject = Get-AdcDomain @parameters + $parentPath = ($ADObject.DistinguishedName -split ",", 2)[1] + $parentObject = Get-ADObject @parameters -Identity $parentPath -Properties SamAccountName, Name, ObjectSID + if (-not $parentObject.ObjectSID) { + Stop-PSFFunction -String 'Resolve-AdcAceIdentity.ParentObject.NoSecurityPrincipal' -StringValues $ADObject, $parentObject.Name, $parentObject.ObjectClass -EnableException $true -Cmdlet $PSCmdlet + } + if ($parentObject.SamAccountName) { return [System.Security.Principal.NTAccount]('{0}\{1}' -f $domainObject.Name, $parentObject.SamAccountName) } + else { return [System.Security.Principal.NTAccount]('{0}\{1}' -f $domainObject.Name, $parentObject.Name) } + } + #endregion Parent Resolution + + #region Default Resolution + $identity = Resolve-String -Text $IdentityReference + if ($identity -as [System.Security.Principal.SecurityIdentifier]) { + $identity = $identity -as [System.Security.Principal.SecurityIdentifier] + } + else { + $identity = $identity -as [System.Security.Principal.NTAccount] + } + if ($null -eq $identity) { $identity = (Resolve-String -Text $IdentityReference) -as [System.Security.Principal.NTAccount] } + + $identity + #endregion Default Resolution +} \ No newline at end of file diff --git a/ADMF.Core/functions/AccessRules/Test-AdcAccessRuleEquality.ps1 b/ADMF.Core/functions/AccessRules/Test-AdcAccessRuleEquality.ps1 new file mode 100644 index 0000000..f0e8c93 --- /dev/null +++ b/ADMF.Core/functions/AccessRules/Test-AdcAccessRuleEquality.ps1 @@ -0,0 +1,59 @@ +function Test-AdcAccessRuleEquality { + <# + .SYNOPSIS + Compares two access rules with each other. + + .DESCRIPTION + Compares two access rules with each other. + + .PARAMETER Rule1 + The first rule to compare + + .PARAMETER Rule2 + The second rule to compare + + .PARAMETER Parameters + Hashtable containing server and credential informations. + + .EXAMPLE + PS C:\> Test-AdcAccessRuleEquality -Rule1 $rule -Rule2 $rule2 + + Compares $rule with $rule2 + #> + [OutputType([System.Boolean])] + [CmdletBinding()] + param ( + $Rule1, + $Rule2, + $Parameters + ) + + function Get-SID { + [CmdletBinding()] + param ( + $Rule, + $Parameters + ) + + if ($Rule.SID) { return $Rule.SID } + if ($Rule.IdentityReference -is [System.Security.Principal.SecurityIdentifier]) { return $Rule.IdentityReference } + + # NTAccount + Resolve-Principal -Name (Convert-BuiltInToSID -Identity $Rule.IdentityReference) -OutputType SID @Parameters -ErrorAction Stop -WarningAction SilentlyContinue + } + + if ($Rule1.ActiveDirectoryRights -ne $Rule2.ActiveDirectoryRights) { return $false } + if ($Rule1.InheritanceType -ne $Rule2.InheritanceType) { return $false } + if ($Rule1.ObjectType -ne $Rule2.ObjectType) { return $false } + if ($Rule1.InheritedObjectType -ne $Rule2.InheritedObjectType) { return $false } + if ($Rule1.AccessControlType -ne $Rule2.AccessControlType) { return $false } + if ("$(Convert-BuiltInToSID -Identity $Rule1.IdentityReference)" -ne "$(Convert-BuiltInToSID -Identity $Rule2.IdentityReference)") { + try { + $oneSID = Get-SID -Rule $Rule1 -Parameters $Parameters + $twoSID = Get-SID -Rule $Rule2 -Parameters $Parameters + if ("$oneSID" -ne "$twoSID") { return $false } + } + catch { return $false } + } + return $true +} \ No newline at end of file diff --git a/ADMF.Core/functions/DomainCredentials/Get-AdcDomainCredential.ps1 b/ADMF.Core/functions/DomainCredentials/Get-AdcDomainCredential.ps1 new file mode 100644 index 0000000..8184cb0 --- /dev/null +++ b/ADMF.Core/functions/DomainCredentials/Get-AdcDomainCredential.ps1 @@ -0,0 +1,33 @@ +function Get-AdcDomainCredential { + <# + .SYNOPSIS + Retrieve credentials stored for accessing the targeted domain. + + .DESCRIPTION + Retrieve credentials stored for accessing the targeted domain. + Returns nothing when no credentials were stored. + This is NOT used by the main commands, but internally for retrieving data regarding foreign principals in one-way trusts. + Generally, these credentials should never have more than reading access to the target domain. + + .PARAMETER Domain + The domain to retrieve credentials for. + Does NOT accept wildcards. + + .EXAMPLE + PS C:\> Get-AdcDomainCredential -Domain contoso.com + + Returns the credentials for accessing contoso.com, as long as those have previously been stored. + #> + [Alias('Get-DMDomainCredential')] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Domain + ) + + process { + if (-not $script:domainCredentialCache) { return } + $script:domainCredentialCache[$Domain] + } +} diff --git a/ADMF.Core/functions/DomainCredentials/Reset-AdcDomainCredential.ps1 b/ADMF.Core/functions/DomainCredentials/Reset-AdcDomainCredential.ps1 new file mode 100644 index 0000000..fca9e25 --- /dev/null +++ b/ADMF.Core/functions/DomainCredentials/Reset-AdcDomainCredential.ps1 @@ -0,0 +1,73 @@ +function Reset-AdcDomainCredential { + <# + .SYNOPSIS + Resets cached credentials for contacting domains. + + .DESCRIPTION + Resets cached credentials for contacting domains. + Use this command when invalidating credentials you used. + For example in ADMF the credential provider: + If you create one that uses a temporary account, then delete it when done, you need to reset the cache when connecting with your default credentials. + + .PARAMETER Credential + Clear all cache entries using this credential object. + + .PARAMETER Domain + Clear the cached credentials for the target domain. + + .PARAMETER UserName + Clear all cached credentials using this username. + + .PARAMETER All + Clear ALL cached credentials + + .EXAMPLE + PS C:\> Reset-AdcDomainCredential -All + + Clear all cached credentials +#> + [Alias('Reset-DMDomainCredential')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Credential')] + [PSCredential] + $Credential, + + [Parameter(Mandatory = $true, ParameterSetName = 'Domain')] + [string] + $Domain, + + [Parameter(Mandatory = $true, ParameterSetName = 'Name')] + [string] + $UserName, + + [Parameter(Mandatory = $true, ParameterSetName = 'All')] + [switch] + $All + ) + + process { + switch ($PSCmdlet.ParameterSetName) { + 'Credential' { + [string[]]$keys = $script:domainCredentialCache.Keys + foreach ($key in $keys) { + if ($script:domainCredentialCache[$key] -eq $Credential) { $script:domainCredentialCache.Remove($key) } + } + } + 'Domain' { + $script:domainCredentialCache.Remove($Domain) + } + 'Name' { + [string[]]$keys = $script:domainCredentialCache.Keys + foreach ($key in $keys) { + if ($script:domainCredentialCache[$key].UserName -eq $UserName) { $script:domainCredentialCache.Remove($key) } + } + } + 'All' { + $script:domainCredentialCache = @{ } + } + } + } +} \ No newline at end of file diff --git a/ADMF.Core/functions/DomainCredentials/Set-AdcDomainCredential.ps1 b/ADMF.Core/functions/DomainCredentials/Set-AdcDomainCredential.ps1 new file mode 100644 index 0000000..d3b18a6 --- /dev/null +++ b/ADMF.Core/functions/DomainCredentials/Set-AdcDomainCredential.ps1 @@ -0,0 +1,43 @@ +function Set-AdcDomainCredential { + <# + .SYNOPSIS + Stores credentials stored for accessing the targeted domain. + + .DESCRIPTION + Stores credentials stored for accessing the targeted domain. + This is NOT used by the main commands, but internally for retrieving data regarding foreign principals in one-way trusts. + Generally, these credentials should never have more than reading access to the target domain. + + .PARAMETER Domain + The domain to store credentials for. + Does NOT accept wildcards. + + .PARAMETER Credential + The credentials to store. + + .EXAMPLE + PS C:\> Set-AdcDomainCredential -Domain contoso.com -Credential $cred + + Stores the credentials for accessing contoso.com. + #> + [Alias('Set-DMDomainCredential')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Domain, + + [Parameter(Mandatory = $true)] + [PSCredential] + $Credential + ) + + process { + if (-not $script:domainCredentialCache) { + $script:domainCredentialCache = @{ } + } + + $script:domainCredentialCache[$Domain] = $Credential + } +} diff --git a/ADMF.Core/functions/Other/Convert-AdcPrincipal.ps1 b/ADMF.Core/functions/Other/Convert-AdcPrincipal.ps1 new file mode 100644 index 0000000..909f877 --- /dev/null +++ b/ADMF.Core/functions/Other/Convert-AdcPrincipal.ps1 @@ -0,0 +1,101 @@ +function Convert-AdcPrincipal { + <# + .SYNOPSIS + Converts a principal to either SID or NTAccount format. + + .DESCRIPTION + Converts a principal to either SID or NTAccount format. + It caches all resolutions, uses Convert-BuiltInToSID to resolve default builtin account names, + uses Get-Domain to resolve foreign domain SIDs and names. + + Basically, it is a best effort attempt to resolve principals in a useful manner. + + .PARAMETER Name + The name of the entity to convert. + + .PARAMETER OutputType + Whether to return an NTAccount or SID. + Defaults to SID + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .EXAMPLE + PS C:\> Convert-AdcPrincipal @parameters -Name contoso\administrator + + Tries to convert the user contoso\administrator into a SID + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string] + $Name, + + [ValidateSet('SID', 'NTAccount')] + [string] + $OutputType = 'SID', + + [PSFComputer] + $Server, + + [PSCredential] + $Credential + ) + + begin { + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + } + process { + Write-PSFMessage -Level Debug -String 'Convert-AdcPrincipal.Processing' -StringValues $Name + + # Terminate if already cached + if ($OutputType -eq 'SID' -and $script:cache_PrincipalToSID[$Name]) { return $script:cache_PrincipalToSID[$Name] } + if ($OutputType -eq 'NTAccount' -and $script:cache_PrincipalToNT[$Name]) { return $script:cache_PrincipalToNT[$Name] } + + $builtInIdentity = Convert-BuiltInToSID -Identity $Name + if ($builtInIdentity -ne $Name) { return $builtInIdentity } + + #region Processing Input SID + if ($Name -as [System.Security.Principal.SecurityIdentifier]) { + Write-PSFMessage -Level Debug -String 'Convert-AdcPrincipal.Processing.InputSID' -StringValues $Name + if ($OutputType -eq 'SID') { + $script:cache_PrincipalToSID[$Name] = $Name -as [System.Security.Principal.SecurityIdentifier] + return $script:cache_PrincipalToSID[$Name] + } + + $script:cache_PrincipalToNT[$Name] = Get-AdcPrincipal @parameters -Sid $Name -Domain $Name -OutputType NTAccount + return $script:cache_PrincipalToNT[$Name] + } + #endregion Processing Input SID + + Write-PSFMessage -Level Debug -String 'Convert-AdcPrincipal.Processing.InputNT' -StringValues $Name + $ntAccount = $Name -as [System.Security.Principal.NTAccount] + if ($OutputType -eq 'NTAccount') { + $script:cache_PrincipalToNT[$Name] = $ntAccount + return $script:cache_PrincipalToNT[$Name] + } + + try { + $script:cache_PrincipalToSID[$Name] = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]) + return $script:cache_PrincipalToSID[$Name] + } + catch { + $domainPart, $namePart = $ntAccount.Value.Split("\", 2) + $domain = Resolve-Domain @parameters -Name $domainPart -OutputType ADObject + Write-PSFMessage -Level Debug -String 'Convert-AdcPrincipal.Processing.NTDetails' -StringValues $domainPart, $namePart + + $param = @{ + Server = $domain.DNSRoot + } + $cred = Get-AdcDomainCredential -Domain $domain.DNSRoot + if ($cred) { $param['Credential'] = $cred } + Write-PSFMessage -Level Debug -String 'Convert-AdcPrincipal.Processing.NT.LdapFilter' -StringValues "(samAccountName=$namePart)" + $adObject = Get-ADObject @param -LDAPFilter "(samAccountName=$namePart)" -Properties ObjectSID + $script:cache_PrincipalToSID[$Name] = $adObject.ObjectSID + $adObject.ObjectSID + } + } +} \ No newline at end of file diff --git a/ADMF.Core/functions/Other/Convert-AdcSchemaGuid.ps1 b/ADMF.Core/functions/Other/Convert-AdcSchemaGuid.ps1 new file mode 100644 index 0000000..a3016d2 --- /dev/null +++ b/ADMF.Core/functions/Other/Convert-AdcSchemaGuid.ps1 @@ -0,0 +1,105 @@ +function Convert-AdcSchemaGuid { + <# + .SYNOPSIS + Converts names to guid and guids to name as defined in the active directory schema. + + .DESCRIPTION + Converts names to guid and guids to name as defined in the active directory schema. + Can handle both attributes as well as rights. + Uses mapping data generated from active directory. + + .PARAMETER Name + The name to convert. Can be both string or guid. + + .PARAMETER OutType + The data tape to emit: + - Name: Humanly readable name + - Guid: Guid object + - GuidString: Guid as a string + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .EXAMPLE + PS C:\> Convert-AdcSchemaGuid -Name Public-Information -OutType GuidString + + Converts the right "Public-Information" into its guid representation (guid returned as a string type) + #> + [CmdletBinding()] + param ( + [Parameter(ValueFromPipeline = $true)] + [Alias('Guid')] + [string[]] + $Name, + + [ValidateSet('Name', 'Guid', 'GuidString')] + [string] + $OutType = 'Guid', + + [PSFComputer] + $Server, + + [PSCredential] + $Credential + ) + + begin { + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + $parameters['Debug'] = $false + + $guidToName = Get-SchemaGuidMapping @parameters + $nameToGuid = Get-SchemaGuidMapping @parameters -NameToGuid + $guidToRight = Get-PermissionGuidMapping @parameters + $rightToGuid = Get-PermissionGuidMapping @parameters -NameToGuid + } + process { + :main foreach ($nameString in $Name) { + switch ($OutType) { + 'Name' { + if ($nameString -as [Guid]) { + if ($guidToName[$nameString]) { + $guidToName[$nameString] + continue main + } + if ($guidToRight[$nameString]) { + $guidToRight[$nameString] + continue main + } + } + else { $nameString } + } + 'Guid' { + if ($nameString -as [Guid]) { + $nameString -as [Guid] + continue main + } + if ($nameToGuid[$nameString]) { + $nameToGuid[$nameString] -as [guid] + continue main + } + if ($rightToGuid[$nameString]) { + $rightToGuid[$nameString] -as [guid] + continue main + } + } + 'GuidString' { + if ($nameString -as [Guid]) { + $nameString + continue main + } + if ($nameToGuid[$nameString]) { + $nameToGuid[$nameString] + continue main + } + if ($rightToGuid[$nameString]) { + $rightToGuid[$nameString] + continue main + } + } + } + } + } +} diff --git a/ADMF.Core/functions/Other/Get-AdcDomain.ps1 b/ADMF.Core/functions/Other/Get-AdcDomain.ps1 new file mode 100644 index 0000000..e4e3670 --- /dev/null +++ b/ADMF.Core/functions/Other/Get-AdcDomain.ps1 @@ -0,0 +1,48 @@ +function Get-AdcDomain { + <# + .SYNOPSIS + Returns the direct domain object accessible via the server/credential parameter connection. + + .DESCRIPTION + Returns the direct domain object accessible via the server/credential parameter connection. + Caches data for subsequent calls. + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .EXAMPLE + PS C:\> Get-AdcDomain @parameters + + Returns the domain associated with the specified connection information + #> + [CmdletBinding()] + param ( + [PSFComputer] + [Alias('ComputerName')] + $Server = '', + + [PSCredential] + $Credential + ) + + begin { + # Note: Module Scope variable solely maintained in this file + # Scriptscope for data persistence only + if (-not ($script:directDomainObjectCache)) { + $script:directDomainObjectCache = @{ } + } + } + process { + if ($script:directDomainObjectCache["$Server"]) { + return $script:directDomainObjectCache["$Server"] + } + + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + $adObject = Get-ADDomain @parameters + $script:directDomainObjectCache["$Server"] = $adObject + $adObject + } +} diff --git a/ADMF.Core/functions/Other/Get-AdcPrincipal.ps1 b/ADMF.Core/functions/Other/Get-AdcPrincipal.ps1 new file mode 100644 index 0000000..eabed66 --- /dev/null +++ b/ADMF.Core/functions/Other/Get-AdcPrincipal.ps1 @@ -0,0 +1,143 @@ +function Get-AdcPrincipal { + <# + .SYNOPSIS + Returns a principal's resolved AD object if able to. + + .DESCRIPTION + Returns a principal's resolved AD object if able to. + Will throw an exception if the AD connection fails. + Will return nothing if the target domain does not contain the specified principal. + Uses the credentials provided by Set-AdcDomainCredential if available. + + Results will be cached automatically, subsequent callls returning the cached results. + + .PARAMETER Sid + The SID of the principal to search. + + .PARAMETER Name + The name of the principal to search for. + + .PARAMETER ObjectClass + The objectClass of the principal to search for. + + .PARAMETER Domain + The domain in which to look for the principal. + + .PARAMETER OutputType + The format in which the output is being returned. + - ADObject: Returns the full AD object with full information from AD + - NTAccount: Returns a simple NT Account notation. + + .PARAMETER Refresh + Do not use cached data, reload fresh data. + + .PARAMETER Target + The target AD object this access rule applies to. + Used for logging only. + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .EXAMPLE + PS C:\> Get-AdcPrincipal -Sid $adObject.ObjectSID -Domain $redForestDomainFQDN + + Tries to return the principal from the specified domain based on the SID offered. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "")] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'SID')] + [string] + $Sid, + + [Parameter(Mandatory = $true, ParameterSetName = 'Name')] + [string] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Name')] + [string] + $ObjectClass, + + [Parameter(Mandatory = $true)] + [string] + $Domain, + + [ValidateSet('ADObject', 'NTAccount')] + [string] + $OutputType = 'ADObject', + + [switch] + $Refresh, + + [AllowEmptyString()] + [string] + $Target, + + [PSFComputer] + $Server, + + [PSCredential] + $Credential + ) + + begin { + $parametersAD = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + } + process { + $identity = $Sid + if (-not $Sid) { $identity = "$($Domain)þ$($objectClass)þ$($Name)" } + + if ($script:resolvedPrincipals[$identity] -and -not $Refresh) { + switch ($OutputType) { + 'ADObject' { return $script:resolvedPrincipals[$identity] } + 'NTAccount' { + if ($script:resolvedPrincipals[$identity].objectSID.AccountDomainSid) { return [System.Security.Principal.NTAccount]"$(Resolve-Domain @parametersAD -Name $script:resolvedPrincipals[$identity].objectSID.AccountDomainSid -OutputType NetBIOSName)\$($script:resolvedPrincipals[$identity].SamAccountName)" } + else { return [System.Security.Principal.NTAccount]"BUILTIN\$($script:resolvedPrincipals[$identity].SamAccountName)" } + } + } + } + + try { + $domainObject = Resolve-Domain @parametersAD -Name $Domain -OutputType ADObject + $parameters = @{ + Server = $domainObject.DNSRoot + } + $domainName = $domainObject.DNSRoot + } + catch { + $parameters = @{ + Server = $Domain + } + $domainName = $Domain + } + if ($credentials = Get-AdcDomainCredential -Domain $domainName) { $parameters['Credential'] = $credentials } + + $filter = "(objectSID=$Sid)" + if (-not $Sid) { $filter = "(&(objectClass=$ObjectClass)(|(name=$Name)(samAccountName=$Name)(distinguishedName=$Name)))" } + + try { $adObject = Get-ADObject @parameters -LDAPFilter $filter -ErrorAction Stop -Properties * | Select-Object -First 1 } + catch { + try { $adObject = Get-ADObject @parametersAD -LDAPFilter $filter -ErrorAction Stop -Properties * | Select-Object -First 1 } + catch { } + if (-not $adObject) { + if ($Target) { Write-PSFMessage -Level Warning -String 'Get-AdcPrincipal.Resolution.FailedWithTarget' -StringValues $Sid, $Name, $ObjectClass, $Domain, $Target -Target $PSBoundParameters } + else { Write-PSFMessage -Level Warning -String 'Get-AdcPrincipal.Resolution.Failed' -StringValues $Sid, $Name, $ObjectClass, $Domain -Target $PSBoundParameters } + throw + } + } + if ($adObject) { + $script:resolvedPrincipals[$identity] = $adObject + switch ($OutputType) { + 'ADObject' { return $adObject } + 'NTAccount' { + if ($adObject.objectSID.AccountDomainSid) { return [System.Security.Principal.NTAccount]"$(Resolve-Domain @parametersAD -Name $adObject.objectSID.AccountDomainSid -OutputType NetBIOSName)\$($adObject.SamAccountName)" } + else { [System.Security.Principal.NTAccount]"BUILTIN\$($adObject.SamAccountName)" } + } + } + } + } +} \ No newline at end of file diff --git a/ADMF.Core/functions/Other/Get-AdcPrivilegedGroupSet.ps1 b/ADMF.Core/functions/Other/Get-AdcPrivilegedGroupSet.ps1 new file mode 100644 index 0000000..678eaef --- /dev/null +++ b/ADMF.Core/functions/Other/Get-AdcPrivilegedGroupSet.ps1 @@ -0,0 +1,29 @@ +function Get-AdcPrivilegedGroupSet { + <# + .SYNOPSIS + List the registered Privileged Group Sets. + + .DESCRIPTION + List the registered Privileged Group Sets. + They can be used to assign ownership to a group of identities, any one of which will be considered valid. + + For more details, see Register-AdcPrivilegedGroupSet. + + .PARAMETER Name + The name of the PGS to look for. + Defaults to: * + + .EXAMPLE + PS C:\> Get-AdcPrivilegedGroupSet + + List all registered Privileged Group Sets. + #> + [CmdletBinding()] + param ( + [string] + $Name = '*' + ) + process { + $($script:privilegedGroupSetCalculators.Values | Where-Object Name -Like $Name) + } +} \ No newline at end of file diff --git a/ADMF.Core/functions/Other/Register-AdcBuiltInSID.ps1 b/ADMF.Core/functions/Other/Register-AdcBuiltInSID.ps1 new file mode 100644 index 0000000..fb184b2 --- /dev/null +++ b/ADMF.Core/functions/Other/Register-AdcBuiltInSID.ps1 @@ -0,0 +1,38 @@ +function Register-AdcBuiltInSID { + <# + .SYNOPSIS + Register a name that points at a well-known SID. + + .DESCRIPTION + Register a name that points at a well-known SID. + This is used to reliably be able to compare access rules where built-in SIDs fail (e.g. for Sub-Domains). + This functionality is exposed, in order to be able to resolve these identities, irrespective of name resolution and localization. + + .PARAMETER Name + The name of the builtin entity to map. + + .PARAMETER SID + The SID associated with the builtin entity. + + .EXAMPLE + PS C:\> Register-AdcBuiltInSID -Name 'BUILTIN\Incoming Forest Trust Builders' -SID 'S-1-5-32-557' + + Maps the group 'BUILTIN\Incoming Forest Trust Builders' to the SID 'S-1-5-32-557' + Note: This mapping is pre-defined in the module and needs not be applied + #> + [Alias('Register-DMBuiltInSID')] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)] + [System.Security.Principal.SecurityIdentifier] + $SID + ) + + process { + $script:builtInSidMapping[$Name] = $SID + } +} diff --git a/ADMF.Core/functions/Other/Register-AdcPrivilegedGroupSet.ps1 b/ADMF.Core/functions/Other/Register-AdcPrivilegedGroupSet.ps1 new file mode 100644 index 0000000..b439197 --- /dev/null +++ b/ADMF.Core/functions/Other/Register-AdcPrivilegedGroupSet.ps1 @@ -0,0 +1,63 @@ +function Register-AdcPrivilegedGroupSet { + <# + .SYNOPSIS + Register a Privileged Group Set for use during ownership assignments. + + .DESCRIPTION + Register a Privileged Group Set for use during ownership assignments. + They can be used to assign a group of identities as owners - any one of which is a viable owner for the assignment. + They also provide a default SID in case none of them are assigned. + + The data is calculated per-domain, cached on the first application. + Cache is cleared when resetting configuration (e.g. loading a new context combination). + + The SIDs resolved can be from one or multiple domains. + + To test your Privileged Group Set use "Resolve-AdcPrivilegedGroupSet". + To assign it as an owner, wrap its name in two underscores on each side when assigning it as owner in the config: + If the Privileged Group Set is named "DomainAdmins", assign it as owner by setting the owner to "__DomainAdmins__". + + The scriptblock doing the calculation receives two parameters: + - Parameters : The server/credential pair used for the current execution. + - Domain : The domain object targeted - as returned by Get-ADDomain + + The scriptblock should return a hashtable or custom object that has two entries/properties: + - SIDs : The list of SIDs that are matched. Must be at least one valid sid. + - DefaultSID : The SID to use for the change if the target is none of the permitted SIDs. This SID should ALSO be included in "SIDs". + + .PARAMETER Name + The name of the Privileged Group Set. + + .PARAMETER Description + A description explaining the Privileged Group Set. + + .PARAMETER Code + The code that calculates the data on demand. + See the Description section for details on what parameters it receives and what output it should produce. + + .EXAMPLE + PS C:\> Register-AdcPrivilegedGroupSet -Name PKIServer -Description $description -Code $code + + Registers the Privileged Group Set "PKI Server" with the provided code. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [string] + $Description, + + [Scriptblock] + $Code + ) + process { + $script:privilegedGroupSetCalculators[$Name] = [PSCustomObject]@{ + PSTypeName = 'ADMF.Core.PrivilegedGroupSet' + Name = $Name + Description = $Description + Code = $Code + } + } +} \ No newline at end of file diff --git a/ADMF.Core/functions/Other/Resolve-AdcPrivilegedGroupSet.ps1 b/ADMF.Core/functions/Other/Resolve-AdcPrivilegedGroupSet.ps1 new file mode 100644 index 0000000..bf659d5 --- /dev/null +++ b/ADMF.Core/functions/Other/Resolve-AdcPrivilegedGroupSet.ps1 @@ -0,0 +1,79 @@ +function Resolve-AdcPrivilegedGroupSet { + <# + .SYNOPSIS + Resolves a Privileged Group Set for a specified domain. + + .DESCRIPTION + Resolves a Privileged Group Set for a specified domain. + Results are cached for any given domain the first time, subsequent requests return cached data. + Cache is cleared when resetting configuration. + + For more details on designing a Privileged Group Set, see the documentation on Register-AdcPrivilegedGroupSet. + + .PARAMETER Type + What Type / Name to resolve the PGS for. + + .PARAMETER Server + The server to resolve the PGS for. + Alaways resolves to the domain the server is member of, when specifying a specific hostname. + + .PARAMETER Credential + The Credentials to use for the operation + + .EXAMPLE + PS C:\> Resolve-AdcPrivilegedGroupSet -Type DomainAdminsEx -Server contoso.com + + Resolves the DomainAdminsEx PGS for the forest containing contoso.com. + (The builtin DomainAdminsEx PGS resolves to all Domain Admins of all member Domains as well as the Enterprise Admins) + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] + [OutputType([hashtable])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Type, + + [Parameter(Mandatory = $true)] + [string] + $Server, + + [PSCredential] + $Credential + ) + process { + if ($script:privilegedGroupSets.Keys -notcontains $Type) { + Stop-PSFFunction -String 'Resolve-AdcPrivilegedGroupSet.Error.UnknownPGS' -StringValues $Type, ($script:privilegedGroupSets.Keys -join ", ") -EnableException $true -Cmdlet $PSCmdlet -Category InvalidArgument + } + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + + $domain = Get-AdcDomain @parameters + if ($script:privilegedGroupSets[$Type][$domain.DnsRoot]) { + return $script:privilegedGroupSets[$Type][$domain.DnsRoot] + } + + Invoke-PSFProtectedCommand -ActionString 'Resolve-AdcPrivilegedGroupSet.Resolving' -ActionStringValues $Type, $domain.DnsRoot -Target $domain.DnsRoot -ScriptBlock { + $data = & $script:privilegedGroupSetCalculators[$Type].Code $parameters $domain + } -EnableException $true -PSCmdlet $PSCmdlet + + $sids = foreach ($sid in $data.SIDs) { + if ($sid -as [System.Security.Principal.SecurityIdentifier]) { + "$sid" + } + } + if (-not $sids) { + Stop-PSFFunction -String 'Resolve-AdcPrivilegedGroupSet.Error.NoSids' -StringValues $Type, $domain.DnsRoot -EnableException $true -Cmdlet $PSCmdlet -Category ObjectNotFound -Target $data + } + if (-not ($data.DefaultSID -as [System.Security.Principal.SecurityIdentifier])) { + Stop-PSFFunction -String 'Resolve-AdcPrivilegedGroupSet.Error.NoDefaultSid' -StringValues $Type, $domain.DnsRoot -EnableException $true -Cmdlet $PSCmdlet -Category ObjectNotFound -Target $data + } + + $newData = @{ + SIDs = $sids + DefaultSID = $data.DefaultSID -as [string] + } + + $script:privilegedGroupSets[$Type][$domain.DnsRoot] = $newData + $newData + } +} \ No newline at end of file diff --git a/ADMF.Core/functions/Remoting/New-AdcPSSession.ps1 b/ADMF.Core/functions/Remoting/New-AdcPSSession.ps1 index 381efbf..98cb5b3 100644 --- a/ADMF.Core/functions/Remoting/New-AdcPSSession.ps1 +++ b/ADMF.Core/functions/Remoting/New-AdcPSSession.ps1 @@ -23,6 +23,7 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")] [CmdletBinding()] param ( + [Alias('Server')] [PSFComputer] $ComputerName, diff --git a/ADMF.Core/functions/System/Clear-AdcConfiguration.ps1 b/ADMF.Core/functions/System/Clear-AdcConfiguration.ps1 index 600e3b4..e52b962 100644 --- a/ADMF.Core/functions/System/Clear-AdcConfiguration.ps1 +++ b/ADMF.Core/functions/System/Clear-AdcConfiguration.ps1 @@ -1,6 +1,5 @@ -function Clear-AdcConfiguration -{ -<# +function Clear-AdcConfiguration { + <# .SYNOPSIS Resets all configuration settings part of ADMF processing. @@ -24,15 +23,15 @@ Resets all configuration settings part of ADMF processing. #> [CmdletBinding()] - Param ( + param ( [switch] $EnableException ) - process - { + process { Invoke-PSFCallback -PSCmdlet $PSCmdlet -EnableException $EnableException & "$script:ModuleRoot\internal\scripts\variables.ps1" Clear-StringMapping + Clear-PrincipalCache } } diff --git a/ADMF.Core/functions/System/Compare-AdcProperty.ps1 b/ADMF.Core/functions/System/Compare-AdcProperty.ps1 index b6221a8..daa7fe0 100644 --- a/ADMF.Core/functions/System/Compare-AdcProperty.ps1 +++ b/ADMF.Core/functions/System/Compare-AdcProperty.ps1 @@ -1,6 +1,5 @@ -function Compare-AdcProperty -{ -<# +function Compare-AdcProperty { + <# .SYNOPSIS Helper function simplifying the changes processing of Test-* commands. @@ -27,9 +26,6 @@ The property on the ad object to use for the comparison. If this parameter is not specified, it uses the value from -Property. - .PARAMETER Parameters - AD Parameters to pass through for Resolve-String. - .PARAMETER AsString Compare properties as string. Will convert all $null values to "". @@ -54,7 +50,7 @@ #> [CmdletBinding()] - Param ( + param ( [Parameter(Mandatory = $true)] [string] $Property, @@ -78,9 +74,6 @@ [string] $ADProperty, - [hashtable] - $Parameters = @{ }, - [switch] $AsString, @@ -97,21 +90,21 @@ $Type = 'Unknown' ) - begin - { + begin { if (-not $ADProperty) { $ADProperty = $Property } } - process - { + process { if ($IfExists -and $Configuration.PSObject.Properties.Name -notcontains $Property) { return } $param = @{ Property = $Property Identity = $ADObject.DistinguishedName - Type = $Type + Type = $Type } $propValue = $Configuration.$Property - if ($Resolve) { $propValue = $propValue | Resolve-String @parameters } + if ($Resolve) { + $propValue = $propValue | Resolve-String # @parameters + } if (($propValue -is [System.Collections.ICollection]) -and ($ADObject.$ADProperty -is [System.Collections.ICollection])) { if (Compare-Object $propValue $ADObject.$ADProperty -CaseSensitive:$CaseSensitive) { diff --git a/ADMF.Core/functions/objectcategories/Find-AdcObjectCategoryItem.ps1 b/ADMF.Core/functions/objectcategories/Find-AdcObjectCategoryItem.ps1 new file mode 100644 index 0000000..1924324 --- /dev/null +++ b/ADMF.Core/functions/objectcategories/Find-AdcObjectCategoryItem.ps1 @@ -0,0 +1,82 @@ +function Find-AdcObjectCategoryItem { + <# + .SYNOPSIS + Searches for items that are part of an object category. + + .DESCRIPTION + Searches for items that are part of an object category. + Caution: A combination of inefficient filters and large scope can lead to significant performance delays in large environments! + + .PARAMETER Name + The name of the object category to search items for. + + .PARAMETER Property + Properties to include when retrieving matching items. + Ensure the property is legal for all potential matches. + + .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:\> Find-AdcObjectCategoryItem -Name 'CAServer' + + Find all objects that are part of the CAServer category. +#> + [Alias('Find-DMObjectCategoryItem')] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [string[]] + $Property, + + [PSFComputer] + $Server, + + [PSCredential] + $Credential, + + [switch] + $EnableException + ) + + begin { + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + $parameters['Debug'] = $false + } + process { + $category = $script:objectCategories[$Name] + if (-not $category) { + Stop-PSFFunction -String 'Find-AdcObjectCategoryItem.Category.NotFound' -StringValues $Name -EnableException $EnableException -Cmdlet $PSCmdlet + return + } + + $searchBase = Resolve-String -Text $category.SearchBase # @parameters + if ($category.LdapFilter) { + $filter = '(&(objectClass={0})({1}))' -f $category.ObjectClass, (Resolve-String -Text $category.LdapFilter <# @parameters #>) + if ($Property) { $parameters.Properties = $Property } + try { Get-ADObject @parameters -LDAPFilter $filter -SearchBase $searchBase -SearchScope $category.SearchScope -ErrorAction Stop } + catch { + Stop-PSFFunction -String 'Find-AdcObjectCategoryItem.ADError' -StringValues $Name -EnableException $EnableException -Cmdlet $PSCmdlet + return + } + } + else { + if ($Property) { $parameters.Properties = $Property } + try { Get-ADObject @parameters -Filter $category.Filter -SearchBase $searchBase -SearchScope $category.SearchScope -ErrorAction Stop } + catch { + Stop-PSFFunction -String 'Find-AdcObjectCategoryItem.ADError' -StringValues $Name -EnableException $EnableException -Cmdlet $PSCmdlet + return + } + } + } +} \ No newline at end of file diff --git a/ADMF.Core/functions/objectcategories/Get-AdcObjectCategory.ps1 b/ADMF.Core/functions/objectcategories/Get-AdcObjectCategory.ps1 new file mode 100644 index 0000000..e5cd956 --- /dev/null +++ b/ADMF.Core/functions/objectcategories/Get-AdcObjectCategory.ps1 @@ -0,0 +1,28 @@ +function Get-AdcObjectCategory { + <# + .SYNOPSIS + Returns registered object category objects. + + .DESCRIPTION + Returns registered object category objects. + See description on Register-DMObjectCategory for details on object categories in general. + + .PARAMETER Name + The name to filter by.4 + + .EXAMPLE + PS C:\> Get-AdcObjectCategory + + Returns all registered object categories. + #> + [Alias('Get-DMObjectCategory')] + [CmdletBinding()] + param ( + [string] + $Name = '*' + ) + + process { + ($script:objectCategories.Values | Where-Object Name -Like $Name) + } +} diff --git a/ADMF.Core/functions/objectcategories/Register-AdcObjectCategory.ps1 b/ADMF.Core/functions/objectcategories/Register-AdcObjectCategory.ps1 new file mode 100644 index 0000000..40c7d88 --- /dev/null +++ b/ADMF.Core/functions/objectcategories/Register-AdcObjectCategory.ps1 @@ -0,0 +1,112 @@ +function Register-AdcObjectCategory { + <# + .SYNOPSIS + Registers a new object category. + + .DESCRIPTION + Registers a new object category. + Object categories are a way to apply settings to a type of object based on a ruleset / filterset. + For example, by registering an object category "Domain Controllers" (with appropriate filters / conditions), + it becomes possible to define access rules that apply to all domain controllers, but not all computers. + + Note: Not all setting types support categories yet. + + .PARAMETER Name + The name of the category. Must be unique. + Will NOT be resolved. + + .PARAMETER ObjectClass + The ObjectClass of the object. + This is the AD attribute of the object. + Each object category can only apply to one class of object, in order to protect system performance. + + .PARAMETER Property + The properties needed for this category. + This attribute is used to optimize object reetrieval in case of multiple categories applying to the same class of object. + + .PARAMETER TestScript + Scriptblock used to determine, whether the input object is part of the category. + Receives the AD object with the requested attributes as input object / argument. + + .PARAMETER Filter + A filter used to find all objects in AD that match this category. + + .PARAMETER LdapFilter + An LDAP filter used to find all objects in AD that match this category. + + .PARAMETER SearchBase + The path under which to look for objects of this category. + Defaults to domain wide. + Supports string resolution. + + .PARAMETER SearchScope + How deep to search for objects of this category under the chosen searchbase. + Supported Values: + - Subtree: All items under the searchbase. (default) + - OneLevel: All items directly under the searchbase. + - Base: Only the searchbase itself is inspected. + + .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:\> Register-AdcObjectCategory -Name DomainController -ObjectClass computer -Property PrimaryGroupID -TestScript { $args[0].PrimaryGroupID -eq 516 } -LDAPFilter '(&(objectCategory=computer)(primaryGroupID=516))' + + Registers an object category applying to all domain controller's computer object in AD. +#> + [Alias('Register-DMObjectCategory')] + [CmdletBinding(DefaultParameterSetName = 'Filter')] + param ( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [string] + $ObjectClass, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [string[]] + $Property, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] + [scriptblock] + $TestScript, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Filter')] + [string] + $Filter, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'LdapFilter')] + [string] + $LdapFilter, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string] + $SearchBase = '%DomainDN%', + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [ValidateSet('Subtree', 'OneLevel', 'Base')] + [string] + $SearchScope = 'Subtree', + + [string] + $ContextName = '' + ) + + process { + $script:objectCategories[$Name] = [PSCustomObject]@{ + Name = $Name + ObjectClass = $ObjectClass + Property = $Property + TestScript = $TestScript + Filter = $Filter + LdapFilter = $LdapFilter + SearchBase = $SearchBase + SearchScope = $SearchScope + ContextName = $ContextName + } + } +} diff --git a/ADMF.Core/functions/objectcategories/Resolve-AdcObjectCategory.ps1 b/ADMF.Core/functions/objectcategories/Resolve-AdcObjectCategory.ps1 new file mode 100644 index 0000000..31b28f8 --- /dev/null +++ b/ADMF.Core/functions/objectcategories/Resolve-AdcObjectCategory.ps1 @@ -0,0 +1,65 @@ +function Resolve-AdcObjectCategory { + <# + .SYNOPSIS + Resolves what object categories apply to a given AD Object. + + .DESCRIPTION + Resolves what object categories apply to a given AD Object. + + .PARAMETER ADObject + The AD Object for which to resolve the object categories. + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .EXAMPLE + PS C:\> Resolve-AdcObjectCategory @parameters -ADObject $adobject + + Resolves the object categories that apply to $adobject + #> + [Alias('Resolve-DMObjectCategory')] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + $ADObject, + + [PSFComputer] + $Server, + + [PSCredential] + $Credential + ) + + begin { + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + $parameters['Debug'] = $false + } + process { + if ($script:objectCategories.Values.ObjectClass -notcontains $ADObject.ObjectClass) { + return + } + + $filteredObjectCategories = $script:objectCategories.Values | Where-Object ObjectClass -EQ $ADobject.ObjectClass + $propertyNames = $filteredObjectCategories.Property | Select-Object -Unique + $adObjectReloaded = Get-Adobject @parameters -Identity $ADObject.DistinguishedName -Properties $propertyNames + :main foreach ($filteredObjectCategory in $filteredObjectCategories) { + #region Consider Searchbase + $resolvedBase = Resolve-String -Text $filteredObjectCategory.SearchBase # @parameters + switch ($filteredObjectCategory.SearchScope) { + 'Base' { if ($adObjectReloaded.DistinguishedName -ne $resolvedBase) { continue main } } + 'OneLevel' { + if ($adObjectReloaded.DistinguishedName -notlike "*,$resolvedBase") { continue main } + if (($adObjectReloaded.DistinguishedName -split ",").Count -ne (($resolvedBase -split ",").Count + 1)) { continue main } + } + 'Subtree' { if ($adObjectReloaded.DistinguishedName -notlike "*,$resolvedBase") { continue main } } + } + #endregion Consider Searchbase + if ($filteredObjectCategory.Testscript.Invoke($adObjectReloaded)) { + $filteredObjectCategory + } + } + } +} diff --git a/ADMF.Core/functions/objectcategories/Unregister-AdcObjectCategory.ps1 b/ADMF.Core/functions/objectcategories/Unregister-AdcObjectCategory.ps1 new file mode 100644 index 0000000..ee57fdb --- /dev/null +++ b/ADMF.Core/functions/objectcategories/Unregister-AdcObjectCategory.ps1 @@ -0,0 +1,31 @@ +function Unregister-AdcObjectCategory { + <# + .SYNOPSIS + Removes an object category from the list of registered object categories. + + .DESCRIPTION + Removes an object category from the list of registered object categories. + See description on Register-DMObjectCategory for details on object categories in general. + + .PARAMETER Name + The exact name of the object category to unregister. + + .EXAMPLE + PS C:\> Get-AdcObjectCategory | Unregister-AdcObjectCategory + + Clears all registered object categories. + #> + [Alias('Unregister-DMObjectCategory')] + [CmdletBinding()] + param ( + [parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] + [string[]] + $Name + ) + + process { + foreach ($nameItem in $Name) { + $script:objectCategories.Remove($nameItem) + } + } +} diff --git a/ADMF.Core/internal/configurations/configuration.ps1 b/ADMF.Core/internal/configurations/configuration.ps1 index a08f896..dac2d43 100644 --- a/ADMF.Core/internal/configurations/configuration.ps1 +++ b/ADMF.Core/internal/configurations/configuration.ps1 @@ -12,4 +12,5 @@ Set-PSFConfig -Module 'ADMF.Core' -Name 'Example.Setting' -Value 10 -Initialize #> Set-PSFConfig -Module 'ADMF.Core' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." -Set-PSFConfig -Module 'ADMF.Core' -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." \ No newline at end of file +Set-PSFConfig -Module 'ADMF.Core' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." +Set-PSFConfig -Module 'ADMF.Core' -Name 'AccessRule.IdentityErrorChange' -Value $false -Initialize -Validation 'bool' -Description "When comparing access rules for changes to apply, should a change entry be generated for configuration entries whose IdentityReference cannot be resolved?" \ No newline at end of file diff --git a/ADMF.Core/internal/functions/Compare-Identity.ps1 b/ADMF.Core/internal/functions/Compare-Identity.ps1 new file mode 100644 index 0000000..0fecef9 --- /dev/null +++ b/ADMF.Core/internal/functions/Compare-Identity.ps1 @@ -0,0 +1,126 @@ +function Compare-Identity { + <# + .SYNOPSIS + Compares two sets of identity references, similar to Compare-Object. + + .DESCRIPTION + Compares two sets of identity references, similar to Compare-Object. + Only real difference: Performs identity resolution and compares at the SID level. + + .PARAMETER ReferenceIdentity + One set of identities to compare. + + .PARAMETER DifferenceIdentity + The other set of identities to compare. + + .PARAMETER Parameters + AD connection parameters. + Offer a hashtable containing server or credentials in any combination. + + .PARAMETER IncludeEqual + Return identities that occur in both sets. + + .PARAMETER ExcludeDifferent + Do not return identities that only occur in one set + + .EXAMPLE + PS C:\> $relevantADRule.IdentityReference | Compare-Identity -Parameters $parameters -ReferenceIdentity $ConfiguredRules.IdentityReference -IncludeEqual -ExcludeDifferent + + Compares all identities between the accessrule already existing on the AD object and the ones defined for it. + Only returns existing Active Directory-existing rules if there also is at least one configured rule for its identity. + #> + [CmdletBinding()] + param ( + $ReferenceIdentity, + + [Parameter(ValueFromPipeline = $true)] + $DifferenceIdentity, + + [Hashtable] + $Parameters = @{ }, + + [switch] + $IncludeEqual, + + [switch] + $ExcludeDifferent + ) + + begin { + #region Utility Functions + function ConvertTo-SID { + [CmdletBinding()] + param ( + $IdentityReference, + + [Hashtable] + $Parameters + ) + + $resolved = Convert-BuiltInToSID -Identity $IdentityReference + if ($resolved -is [System.Security.Principal.SecurityIdentifier]) { return $resolved } + + # NTAccount + try { Resolve-Principal -Name $resolved -OutputType SID @Parameters } + catch { $resolved } + } + #endregion Utility Functions + + $referenceItems = foreach ($identity in $ReferenceIdentity) { + $sid = ConvertTo-SID -IdentityReference $identity -Parameters $Parameters + [PSCustomObject]@{ + Type = "Reference" + Original = $identity + SID = $sid + SIDString = "$sid" + } + } + + [System.Collections.ArrayList]$differenceItems = @() + } + process { + foreach ($item in $DifferenceIdentity) { + $sid = ConvertTo-SID -IdentityReference $item -Parameters $Parameters + $result = [PSCustomObject]@{ + Type = "Difference" + Original = $identity + SID = $sid + SIDString = "$sid" + } + $null = $differenceItems.Add($result) + } + } + end { + if (-not $ExcludeDifferent) { + foreach ($differenceItem in $differenceItems) { + if ($differenceItem.SIDString -in $referenceItems.SIDString) { continue } + + [PSCustomObject]@{ + Identity = $differenceItem.Original + SID = $differenceItem.SID + Direction = '<=' + } + } + foreach ($referenceItem in $referenceItems) { + if ($referenceItem.SIDString -in $differenceItems.SIDString) { continue } + + [PSCustomObject]@{ + Identity = $referenceItem.Original + SID = $referenceItem.SID + Direction = '=>' + } + } + } + if ($IncludeEqual) { + foreach ($differenceItem in $differenceItems) { + if ($differenceItem.SIDString -notin $referenceItems.SIDString) { continue } + + [PSCustomObject]@{ + Identity = $differenceItem.Original + SID = $differenceItem.SID + Direction = '==' + } + } + } + } +} \ No newline at end of file diff --git a/ADMF.Core/internal/functions/Convert-BuiltInToSID.ps1 b/ADMF.Core/internal/functions/Convert-BuiltInToSID.ps1 new file mode 100644 index 0000000..a9c2086 --- /dev/null +++ b/ADMF.Core/internal/functions/Convert-BuiltInToSID.ps1 @@ -0,0 +1,31 @@ +function Convert-BuiltInToSID +{ + <# + .SYNOPSIS + Converts pre-configured built in accounts into SID form. + + .DESCRIPTION + Converts pre-configured built in accounts into SID form. + These must be registered using Register-DMBuiltInSID. + Returns all identity references that are not a BuiltIn account that was registered. + + .PARAMETER Identity + The identity reference to translate. + + .EXAMPLE + Convert-BuiltInToSID -Identity $Rule1.IdentityReference + + Converts to IdentityReference of $Rule1 if necessary + #> + [CmdletBinding()] + Param ( + $Identity + ) + + process + { + if ($Identity -as [System.Security.Principal.SecurityIdentifier]) { return ($Identity -as [System.Security.Principal.SecurityIdentifier]) } + if ($script:builtInSidMapping["$Identity"]) { return $script:builtInSidMapping["$Identity"] } + $Identity + } +} \ No newline at end of file diff --git a/ADMF.Core/internal/functions/Get-PermissionGuidMapping.ps1 b/ADMF.Core/internal/functions/Get-PermissionGuidMapping.ps1 new file mode 100644 index 0000000..6228644 --- /dev/null +++ b/ADMF.Core/internal/functions/Get-PermissionGuidMapping.ps1 @@ -0,0 +1,74 @@ +function Get-PermissionGuidMapping +{ + <# + .SYNOPSIS + Retrieve a hashtable mapping permission guids to their respective name. + + .DESCRIPTION + Retrieve a hashtable mapping permission guids to their respective name. + This is retrieved from the target forest on first request, then cached for subsequent calls. + The cache is specific to the targeted server and maintained as long as the process runs. + + .PARAMETER NameToGuid + Rather than returning a hashtable mapping guid to name, return a hashtable mapping name to guid. + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .EXAMPLE + PS C:\> Get-PermissionGuidMapping -Server contoso.com + + Returns a hashtable mapping guids to rights from the contoso.com forest. + #> + [CmdletBinding()] + Param ( + [switch] + $NameToGuid, + + [PSFComputer] + $Server = 'default', + + [PSCredential] + $Credential + ) + + begin + { + # Script scope variables declared and maintained in this file only + if (-not $script:schemaGuidToRightMapping) { + $script:schemaGuidToRightMapping = @{ } + } + if (-not $script:schemaRightToGuidMapping) { + $script:schemaRightToGuidMapping = @{ } + } + } + process + { + [string]$identity = $Server + if ($script:schemaGuidToRightMapping[$identity]) { + if ($NameToGuid) { return $script:schemaRightToGuidMapping[$identity] } + else { return $script:schemaGuidToRightMapping[$identity] } + } + Write-PSFMessage -Level Host -String 'Get-PermissionGuidMapping.Processing' -StringValues $identity + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + $parameters['Debug'] = $false + + $configurationNC = (Get-ADRootDSE @parameters).configurationNamingContext + $objects = Get-ADObject @parameters -SearchBase "CN=Extended-Rights,$configurationNC" -Properties Name,rightsGUID -LDAPFilter '(objectCategory=controlAccessRight)' # Exclude the schema object itself + $processed = $objects | Select-PSFObject Name, 'rightsGUID to Guid as ID' | Select-PSFObject Name, 'ID to string' + + if (-not $processed) { return } + $script:schemaGuidToRightMapping[$identity] = @{ "$([guid]::Empty)" = '' } + $script:schemaRightToGuidMapping[$identity] = @{ '' = "$([guid]::Empty)" } + + foreach ($processedItem in $processed) { + $script:schemaGuidToRightMapping[$identity][$processedItem.ID] = $processedItem.Name + $script:schemaRightToGuidMapping[$identity][$processedItem.Name] = $processedItem.ID + } + if ($NameToGuid) { return $script:schemaRightToGuidMapping[$identity] } + else { return $script:schemaGuidToRightMapping[$identity] } + } +} diff --git a/ADMF.Core/internal/functions/Get-SchemaGuidMapping.ps1 b/ADMF.Core/internal/functions/Get-SchemaGuidMapping.ps1 new file mode 100644 index 0000000..ba7b6fd --- /dev/null +++ b/ADMF.Core/internal/functions/Get-SchemaGuidMapping.ps1 @@ -0,0 +1,68 @@ +function Get-SchemaGuidMapping +{ + <# + .SYNOPSIS + Returns a hashtable mapping schema guids to the name of an attribute / class. + + .DESCRIPTION + Returns a hashtable mapping schema guids to the name of an attribute / class. + This hashtable is being generated (and cached) on a per-Server basis. + + .PARAMETER NameToGuid + Return a hashtable mapping name to guid, rather than one mapping guid to name. + + .PARAMETER Server + The server / domain to work with. + + .PARAMETER Credential + The credentials to use for this operation. + + .EXAMPLE + PS C:\> Get-SchemaGuidMapping @parameters + + Returns a hashtable mapping Guid of attributes or classes to their humanly readable name. + #> + [CmdletBinding()] + Param ( + [switch] + $NameToGuid, + + [PSFComputer] + $Server, + + [PSCredential] + $Credential + ) + + process + { + [string]$identity = '' + if ($Server) { $identity = $Server } + + if (Test-PSFTaskEngineCache -Module DomainManagement -Name "SchemaGuidCache.$Identity") { + if ($NameToGuid) { return (Get-PSFTaskEngineCache -Module DomainManagement -Name "SchemaGuidCache.$Identity").NameToGuid } + else { return (Get-PSFTaskEngineCache -Module DomainManagement -Name "SchemaGuidCache.$Identity").GuidToName } + } + + Write-PSFMessage -Level Host -String 'Get-SchemaGuidMapping.Processing' -StringValues $identity + $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Server, Credential + $parameters['Debug'] = $false + + $schemaNC = (Get-ADRootDSE @parameters).schemaNamingContext + $objects = Get-ADObject @parameters -SearchBase $schemaNC -Properties Name,SchemaIDGuid -LDAPFilter '(schemaIDGUID=*)' # Exclude the schema object itself + $processed = $objects | Select-PSFObject Name, 'SchemaIDGuid to Guid as ID' | Select-PSFObject Name, 'ID to string' + + if (-not $processed) { return } + $data = [PSCustomObject]@{ + NameToGuid = @{ '' = "$([guid]::Empty)" } + GuidToName = @{ "$([guid]::Empty)" = '' } + } + foreach ($processedItem in $processed) { + $data.GuidToName[$processedItem.ID] = $processedItem.Name + $data.NameToGuid[$processedItem.Name] = $processedItem.ID + } + Set-PSFTaskEngineCache -Module DomainManagement -Name "SchemaGuidCache.$Identity" -Value $data + if ($NameToGuid) { return $data.NameToGuid } + else { return $data.GuidToName } + } +} diff --git a/ADMF.Core/internal/functions/Test-Identity.ps1 b/ADMF.Core/internal/functions/Test-Identity.ps1 new file mode 100644 index 0000000..ecc8e7e --- /dev/null +++ b/ADMF.Core/internal/functions/Test-Identity.ps1 @@ -0,0 +1,39 @@ +function Test-Identity { + <# + .SYNOPSIS + Verifies, whether the provided identity reference exists. + + .DESCRIPTION + Verifies, whether the provided identity reference exists. + Used in the AccessRule component helper tools. + + .PARAMETER Identity + The Identity to resolve. + + .PARAMETER Parameters + AD connection parameters. + Offer a hashtable containing server or credentials in any combination. + + .EXAMPLE + PS C:\> Test-Identity -Identity Max.Mustermann -Parameters $param + + Verifies whether the account with SamAccountName "Max.Mustermann" exists + #> + [OutputType([bool])] + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Identity, + + [hashtable] + $Parameters = @{} + ) + process { + try { + $null = Resolve-Principal -Name (Convert-BuiltInToSID -Identity $Identity) -OutputType SID @Parameters -ErrorAction Stop -WarningAction SilentlyContinue + $true + } + catch { $false } + } +} \ No newline at end of file diff --git a/ADMF.Core/internal/scripts/postimport.ps1 b/ADMF.Core/internal/scripts/postimport.ps1 index bad3260..8f423e1 100644 --- a/ADMF.Core/internal/scripts/postimport.ps1 +++ b/ADMF.Core/internal/scripts/postimport.ps1 @@ -29,5 +29,8 @@ $moduleRoot = Split-Path (Split-Path $PSScriptRoot) # Load Exchange Data "$moduleRoot\internal\scripts\exchange.ps1" +# Load Default Privileged Group Sets +"$moduleRoot\internal\scripts\privilegedGroupSets.ps1" + # Load License "$moduleRoot\internal\scripts\license.ps1" \ No newline at end of file diff --git a/ADMF.Core/internal/scripts/privilegedGroupSets.ps1 b/ADMF.Core/internal/scripts/privilegedGroupSets.ps1 new file mode 100644 index 0000000..d197db9 --- /dev/null +++ b/ADMF.Core/internal/scripts/privilegedGroupSets.ps1 @@ -0,0 +1,39 @@ +$param = @{ + Name = 'DomainAdmins' + Description = 'Any domain admin group in the Forest. Defaults to the Domain Admins of the Forest Root Domain.' + Code = { + param ( + $Parameters, + $Domain + ) + + $forest = Get-ADForest @Parameters + $domains = $forest | ForEach-Object Domains | Get-ADDomain @Parameters + $rootDomain = $domains | Where-Object DnsRoot -EQ $forest.RootDomain + @{ + SIDs = $domains.DomainSID | Format-String '{0}-512' + DefaultSID = '{0}-512' -f $rootDomain.DomainSID + } + } +} +Register-AdcPrivilegedGroupSet @param + +$param = @{ + Name = 'DomainAdminsEx' + Description = 'Any domain admin group in the Forest or the Enterprise Admins group of the Forest Root Domain. Defaults to the Enterprise Admins of the Forest Root Domain.' + Code = { + param ( + $Parameters, + $Domain + ) + + $forest = Get-ADForest @Parameters + $domains = $forest | ForEach-Object Domains | Get-ADDomain @Parameters + $rootDomain = $domains | Where-Object DnsRoot -EQ $forest.RootDomain + @{ + SIDs = @($domains.DomainSID | Format-String '{0}-512') + "$($rootDomain.DomainSID)-519" + DefaultSID = '{0}-519' -f $rootDomain.DomainSID + } + } +} +Register-AdcPrivilegedGroupSet @param \ No newline at end of file diff --git a/ADMF.Core/internal/scripts/variables.ps1 b/ADMF.Core/internal/scripts/variables.ps1 index 3c73fc6..5614f19 100644 --- a/ADMF.Core/internal/scripts/variables.ps1 +++ b/ADMF.Core/internal/scripts/variables.ps1 @@ -1 +1,27 @@ -# Variables that should be reset on configuration reload \ No newline at end of file +# Variables that should be reset on configuration reload + +#----------------------------------------------------------------------------# +# Configuration # +#----------------------------------------------------------------------------# + +# Configured Access Rule processing Modes +$script:accessRuleMode = @{ } + +# Configured Object Categories +$script:objectCategories = @{ } + +#----------------------------------------------------------------------------# +# Cached Data # +#----------------------------------------------------------------------------# + +# More principal caching, used by Convert-AdcPrincipal. Mapping to SID or NT Account +$script:cache_PrincipalToSID = @{ } +$script:cache_PrincipalToNT = @{ } + +# Cached security principals, used by Get-AdcPrincipal. Mapping to AD Objects +$script:resolvedPrincipals = @{ } + +$script:privilegedGroupSets = @{ + DomainAdmins = @{} + DomainAdminsEx = @{} +} \ No newline at end of file diff --git a/ADMF.Core/internal/scripts/variables_persistent.ps1 b/ADMF.Core/internal/scripts/variables_persistent.ps1 index efd1039..b871981 100644 --- a/ADMF.Core/internal/scripts/variables_persistent.ps1 +++ b/ADMF.Core/internal/scripts/variables_persistent.ps1 @@ -1,4 +1,40 @@ # Configuration Entries for remoting $script:PSRemotingConfig = @{ -} \ No newline at end of file +} + +$script:schemaObjectDefaultPermission = @{ } + +$script:builtInSidMapping = @{ + # English + 'BUILTIN\Administrators' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-544' + 'BUILTIN\Account Operators' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-548' + 'BUILTIN\Server Operators' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-549' + 'BUILTIN\Print Operators' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-550' + 'BUILTIN\Pre-Windows 2000 Compatible Access' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-554' + 'BUILTIN\Incoming Forest Trust Builders' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-557' + 'BUILTIN\Windows Authorization Access Group' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-560' + 'BUILTIN\Terminal Server License Servers' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-561' + 'BUILTIN\Certificate Service DCOM Access' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-574' + 'BUILTIN\RDS Remote Access Servers' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-575' + 'BUILTIN\RDS Endpoint Servers' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-576' + 'BUILTIN\RDS Management Servers' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-577' + 'BUILTIN\Storage Replica Administrators' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-582' + + # Deutsch + 'BUILTIN\Administratoren' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-544' + 'BUILTIN\Konten-Operatoren' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-548' + 'BUILTIN\Server-Operatoren' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-549' + 'BUILTIN\Druck-Operatoren' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-550' + 'BUILTIN\Prä-Windows 2000 kompatibler Zugriff' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-554' + 'BUILTIN\Erstellungen eingehender Gesamtstrukturvertrauensstellung' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-557' + 'BUILTIN\Windows-Autorisierungszugriffsgruppe' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-560' + 'BUILTIN\Terminalserver-Lizenzserver' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-561' + 'BUILTIN\Zertifikatdienst-DCOM-Zugriff' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-574' + # 'BUILTIN\RDS Remote Access Servers' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-575' + # 'BUILTIN\RDS Endpoint Servers' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-576' + # 'BUILTIN\RDS Management Servers' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-577' + # 'BUILTIN\Storage Replica Administrators' = [System.Security.Principal.SecurityIdentifier]'S-1-5-32-582' +} + +$script:privilegedGroupSetCalculators = @{} \ No newline at end of file diff --git a/ADMF.Core/xml/ADMF.Core.Format.ps1xml b/ADMF.Core/xml/ADMF.Core.Format.ps1xml index 5fa312d..4b5839c 100644 --- a/ADMF.Core/xml/ADMF.Core.Format.ps1xml +++ b/ADMF.Core/xml/ADMF.Core.Format.ps1xml @@ -1,6 +1,89 @@  + + + ADMF.Core.AccessRule.Change + + ADMF.Core.AccessRule.Change + + + + + + + + + 80 + + + + + + + + Type + + + ACT + + + Identity + + + Rights + + + DistinguishedName + + + + + + + + + + ADMF.Core.AccessRuleMode + + ADMF.Core.AccessRuleMode + + + + + + + + + + + + + + + + Type + + + Mode + + + PathMode + + + + + if ($_.Path) { $_.Path } + else { $_.ObjectCategory } + + + + + + + + + ADMF.Core.ExchangeVersion