Skip to content

Commit

Permalink
Add Test-ExchangePropertyPermissions script
Browse files Browse the repository at this point in the history
  • Loading branch information
bill-long committed Dec 13, 2023
1 parent a71d357 commit 6e428e0
Show file tree
Hide file tree
Showing 9 changed files with 513 additions and 0 deletions.
1 change: 1 addition & 0 deletions Admin/Test-ExchangePropertyPermissions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

$propertySets = @(
[PSCustomObject]@{
Name = "Exchange-Information"
RightsGuid = [Guid]::Parse("1F298A89-DE98-47b8-B5CD-572AD53D267E")
MemberAttributes = New-Object System.Collections.ArrayList
},
[PSCustomObject]@{
Name = "Exchange-Personal-Information"
RightsGuid = [Guid]::Parse("B1B3A417-EC55-4191-B327-B72E33E38AF2")
MemberAttributes = New-Object System.Collections.ArrayList
},
[PSCustomObject]@{
Name = "Personal-Information"
RightsGuid = [Guid]::Parse("77B5B886-944A-11d1-AEBD-0000F80367C1")
MemberAttributes = New-Object System.Collections.ArrayList
},
[PSCustomObject]@{
Name = "Public-Information"
RightsGuid = [Guid]::Parse("E48D0154-BCF8-11D1-8702-00C04FB96050")
MemberAttributes = New-Object System.Collections.ArrayList
}
)

$rootDSE = [ADSI]("LDAP://RootDSE")
$schemaContainer = [ADSI]("LDAP://" + $rootDSE.schemaNamingContext)

foreach ($propertySet in $propertySets) {
$rightsGuidByteString = ""
$propertySet.RightsGuid.ToByteArray() | ForEach-Object { $rightsGuidByteString += ("\$($_.ToString("X"))") }
$searcher = New-Object System.DirectoryServices.directorySearcher($schemaContainer, "(&(objectClass=attributeSchema)(attributeSecurityGuid=$rightsGuidByteString))")
$searcher.PageSize = 100
$results = $searcher.FindAll()
foreach ($result in $results) {
[void]$propertySet.MemberAttributes.Add($result.Properties["cn"][0])
}
}

$getPropertySetInfoBuilder = New-Object System.Text.StringBuilder
[void]$getPropertySetInfoBuilder.AppendLine("# Copyright (c) Microsoft Corporation.")
[void]$getPropertySetInfoBuilder.AppendLine("# Licensed under the MIT License.")
[void]$getPropertySetInfoBuilder.AppendLine("")
[void]$getPropertySetInfoBuilder.AppendLine("# This is a generated function. Do not manually modify.")
[void]$getPropertySetInfoBuilder.AppendLine("function Get-PropertySetInfo {")
[void]$getPropertySetInfoBuilder.AppendLine(" [CmdletBinding()]")
[void]$getPropertySetInfoBuilder.AppendLine(" [OutputType([System.Object[]])]")
[void]$getPropertySetInfoBuilder.AppendLine(" param ()")
[void]$getPropertySetInfoBuilder.AppendLine("")
[void]$getPropertySetInfoBuilder.AppendLine(" # cSpell:disable")
[void]$getPropertySetInfoBuilder.AppendLine(" `$propertySetInfo = @(")
for ($i = 0; $i -lt $propertySets.Count; $i++) {
$propertySet = $propertySets[$i]
$memberAttributeString = [string]::Join(", ", ($propertySet.MemberAttributes | ForEach-Object { "`"$_`"" }))
[void]$getPropertySetInfoBuilder.AppendLine(" [PSCustomObject]@{")
[void]$getPropertySetInfoBuilder.AppendLine(" Name = `"$($propertySet.Name)`"")
[void]$getPropertySetInfoBuilder.AppendLine(" RightsGuid = [Guid]::Parse(`"$($propertySet.RightsGuid)`")")
[void]$getPropertySetInfoBuilder.AppendLine(" MemberAttributes = $memberAttributeString")
[void]$getPropertySetInfoBuilder.Append(" }")
if ($i + 1 -lt $propertySets.Count) {
[void]$getPropertySetInfoBuilder.AppendLine(",")
}
}
[void]$getPropertySetInfoBuilder.AppendLine(" )")
[void]$getPropertySetInfoBuilder.AppendLine(" # cSpell:enable")
[void]$getPropertySetInfoBuilder.AppendLine(" `$propertySetInfo")
[void]$getPropertySetInfoBuilder.AppendLine("}")
[void]$getPropertySetInfoBuilder.AppendLine("")

Set-Content $PSScriptRoot\Get-PropertySetInfo.ps1 $getPropertySetInfoBuilder.ToString()
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

function Get-ObjectTypeDisplayName {
[CmdletBinding()]
param (
[Parameter()]
[Guid]
$ObjectType
)

$rootDSE = [ADSI]"LDAP://RootDSE"
$extendedRightsContainer = [ADSI]"LDAP://$("CN=Extended-Rights," + $rootDSE.ConfigurationNamingContext)"
$searcher = New-Object System.DirectoryServices.DirectorySearcher($extendedRightsContainer, "(&(rightsGuid=$ObjectType))", "displayName")
$result = $searcher.FindOne()

if ($null -ne $result) {
$result.Properties["displayName"][0]
return
}

$schemaContainer = [ADSI]"LDAP://$("CN=Schema," + $rootDSE.ConfigurationNamingContext)"
$objectTypeBytes = [string]::Join("", ($ObjectType.ToByteArray() | ForEach-Object { ("\" + $_.ToString("X")) }))
$searcher = New-Object System.DirectoryServices.DirectorySearcher($schemaContainer, "(&(schemaIdGuid=$objectTypeBytes))", "lDAPDisplayName")
$result = $searcher.FindOne()
if ($null -ne $result) {
$result.Properties["lDAPDisplayName"][0]
return
}

throw "ObjectType $ObjectType not found"
}
34 changes: 34 additions & 0 deletions Admin/Test-ExchangePropertyPermissions/Get-PropertySetInfo.ps1

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

function Get-TokenGroupsGlobalAndUniversal {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]
$AccountDN
)

$searchRoot = [ADSI]("GC://" + $AccountDN)
$searcher = New-Object System.DirectoryServices.DirectorySearcher($searchRoot, "(objectClass=*)", @("tokenGroupsGlobalAndUniversal"), [System.DirectoryServices.SearchScope]::Base)
$result = $searcher.FindOne()
if ($null -eq $result) {
throw "Account not found: $AccountDN"
}

foreach ($sidBytes in $result.Properties["tokenGroupsGlobalAndUniversal"]) {
$translated = $null
$sid = New-Object System.Security.Principal.SecurityIdentifier($sidBytes, 0)
try {
$translated = $sid.Translate("System.Security.Principal.NTAccount").ToString()
} catch {
try {
$adObject = ([ADSI]("LDAP://<SID=" + $sid.ToString() + ">"))
$translated = $adObject.Properties["samAccountName"][0].ToString()
} catch {
Write-Error ("Failed to translate SID: " + $sid.ToString())
throw
}
}

[PSCustomObject]@{
SID = $sid.ToString()
Name = $translated
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

[CmdletBinding()]
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]
$TargetObjectDN,

[Parameter(Mandatory = $true, Position = 1)]
[string]
$ComputerAccountDN,

[Parameter(Mandatory = $false, Position = 2)]
[string]
$DomainController,

[Parameter(Mandatory = $false, Position = 3)]
[switch]
$SaveReport,

[Parameter(Mandatory = $false, Position = 3)]
[switch]
$OutputDebugInfo
)

begin {
. $PSScriptRoot\..\..\Shared\Out-Columns.ps1
. $PSScriptRoot\..\..\Shared\ActiveDirectoryFunctions\Get-ActiveDirectoryAcl.ps1
. $PSScriptRoot\..\..\Shared\ActiveDirectoryFunctions\Get-ExchangeOtherWellKnownObjects.ps1
. $PSScriptRoot\Get-TokenGroupsGlobalAndUniversal.ps1
. $PSScriptRoot\Get-ObjectTypeDisplayName.ps1
. $PSScriptRoot\Get-PropertySetInfo.ps1
. $PSScriptRoot\Test-ExchangeSchema.ps1

$requiredWellKnownGroupsInToken = "Exchange Trusted Subsystem", "Exchange Servers"

$report = [PSCustomObject]@{
TargetObjectDN = $TargetObjectDN
ComputerAccountDN = $ComputerAccountDN
DomainController = $DomainController
RequiredInToken = @()
Token = $null
ACL = $null
ProblemsFound = @()
}
}

process {
if (-not (Test-ExchangeSchema)) {
Write-Warning "Schema validation failed. Exiting."
return
}

$token = Get-TokenGroupsGlobalAndUniversal -AccountDN $ComputerAccountDN
$report.Token = $token
Write-Host "Token groups: $ComputerAccountDN"
$token | Out-Columns

$wellKnownObjects = Get-ExchangeOtherWellKnownObjects
foreach ($wellKnownName in $requiredWellKnownGroupsInToken) {
$groupDN = ($wellKnownObjects | Where-Object { $_.WellKnownName -eq $wellKnownName }).DistinguishedName
$objectSidBytes = ([ADSI]("LDAP://$groupDN")).Properties["objectSID"][0]
$objectSid = New-Object System.Security.Principal.SecurityIdentifier($objectSidBytes, 0)
$report.RequiredInToken += [PSCustomObject]@{
WellKnownName = $wellKnownName
DistinguishedName = $groupDN
ObjectSid = $objectSid.ToString()
}

$matchFound = $token | Where-Object { $_.SID -eq $objectSid.ToString() }
if ($null -eq $matchFound) {
$report.ProblemsFound += "The group $wellKnownName is not in the token."
}
}

$params = @{
DistinguishedName = $TargetObjectDN
}

if (-not [string]::IsNullOrEmpty($DomainController)) {
$params.DomainController = $DomainController
}

$acl = Get-ActiveDirectoryAcl @params
$objectTypeCache = @{}
$displayAces = @()
for ($i = 0; $i -lt $acl.Access.Count; $i++) {
$ace = $acl.Access[$i]
if ($ace.ObjectType -ne [Guid]::Empty) {
if ($null -ne $objectTypeCache[$ace.ObjectType]) {
$ace | Add-Member -NotePropertyName ObjectTypeDisplay -NotePropertyValue $objectTypeCache[$ace.ObjectType]
} else {
$objectTypeDisplay = Get-ObjectTypeDisplayName -ObjectType $ace.ObjectType
$objectTypeCache[$ace.ObjectType] = $objectTypeDisplay
$ace | Add-Member -NotePropertyName ObjectTypeDisplay -NotePropertyValue $objectTypeDisplay
}
}

if ($ace.InheritedObjectType -ne [Guid]::Empty) {
if ($null -ne $objectTypeCache[$ace.InheritedObjectType]) {
$ace | Add-Member -NotePropertyName InheritedObjectTypeDisplay -NotePropertyValue $objectTypeCache[$ace.InheritedObjectType]
} else {
$objectTypeDisplay = Get-ObjectTypeDisplayName -ObjectType $ace.InheritedObjectType
$objectTypeCache[$ace.InheritedObjectType] = $objectTypeDisplay
$ace | Add-Member -NotePropertyName InheritedObjectTypeDisplay -NotePropertyValue $objectTypeDisplay
}
}

$ace | Add-Member -MemberType NoteProperty -Name "Index" -Value $i
$displayAces += $ace
}

$report.ACL = $displayAces
Write-Host "ACL: $TargetObjectDN"
$displayAces | Where-Object { $_.PropagationFlags -ne "InheritOnly" } | Out-Columns -Properties Index, IdentityReference, AccessControlType, ActiveDirectoryRights, ObjectTypeDisplay, IsInherited

$propertySetInfo = Get-PropertySetInfo
$attributeCount = $propertySetInfo.MemberAttributes.Count
$progressCount = 0
$sw = New-Object System.Diagnostics.Stopwatch
$sw.Start()
$schemaPath = ([ADSI]("LDAP://RootDSE")).Properties["schemaNamingContext"][0]
$identityReferenceCache = @{}
foreach ($propertySet in $propertySetInfo) {
foreach ($attributeName in $propertySet.MemberAttributes) {
$progressCount++
if ($sw.ElapsedMilliseconds -gt 1000) {
$sw.Restart()
Write-Progress -Activity "Checking permissions" -PercentComplete $((($progressCount * 100) / $attributeCount))
}

$attributeSchemaEntry = [ADSI]("LDAP://CN=$attributeName,$schemaPath")
if ($attributeSchemaEntry.Properties["attributeSecurityGuid"].Count -lt 1) {
# This schema validation failure should be extremely rare, but we have seen a few
# cases in lab/dev/test environments, such as when ADSchemaAnalyzer has been used to
# copy schema between forests.
$report.ProblemsFound += "The attribute $attributeName is not in the $($propertySet.Name) property set."
continue
}

$schemaIdGuid = New-Object Guid(, $attributeSchemaEntry.Properties["schemaIDGuid"][0])

# We need to hit a write allow ACE for a SID in the token on one of the following:
# - The rightsGuid from the property set
# - The schemaIdGuid from the attributeSchemaEntry
# We must hit the allow before we hit a deny on the same thing.

$found = $false
$problemAceIndex = $null
for ($i = 0; $i -lt $displayAces.Count; $i++) {
$ace = $displayAces[$i]
if ($ace.PropagationFlags -eq "InheritOnly") {
continue
}

$sidToFind = $null
if ($null -eq $ace.IdentityReference.SID) {
if ($null -ne $identityReferenceCache[$ace.IdentityReference.Value]) {
$sidToFind = $identityReferenceCache[$ace.IdentityReference.Value]
} else {
$sidToFind = $ace.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
$identityReferenceCache[$ace.IdentityReference.Value] = $sidToFind
}
} else {
$sidToFind = $ace.IdentityReference.SID
}

$matchingSid = $token | Where-Object { $_.SID -eq $sidToFind.ToString() }
if ($null -ne $matchingSid) {
# The ACE affects this token.
# Does it affect this property?
if ($ace.ObjectType -eq $propertySet.RightsGuid -or $ace.ObjectType -eq $schemaIdGuid -or $ace.ObjectType -eq [Guid]::Empty) {
if ($ace.ActiveDirectoryRights -contains "WriteProperty" -or $ace.ActiveDirectoryRights -contains "GenericAll") {
if ($ace.AccessControlType -eq "Allow") {
$found = $true
break
} else {
$problemAceIndex = $i
break
}
}
}
}
}

if (-not $found) {
if ($null -ne $problemAceIndex) {
$report.ProblemsFound += "The property $attributeName is denied Write by ACE $problemAceIndex."
} else {
$report.ProblemsFound += "The property $attributeName is not allowed Write by any ACE."
}
}
}
}

if ($report.ProblemsFound.Count -gt 0) {
foreach ($problem in $report.ProblemsFound) {
Write-Warning $problem
}
} else {
Write-Host "No problems found."
}

if ($SaveReport) {
$reportPath = $PSScriptRoot + "\" + "PermissionReport-$([DateTime]::Now.ToString("yyMMddHHmmss")).xml"
$report | Export-Clixml $reportPath
Write-Host "Report saved to $reportPath"
}

if ($OutputDebugInfo) {
$debugInfo = @{
ACL = $acl
DisplayAces = $displayAces
IdentityReferenceCache = $identityReferenceCache
Token = $token
TargetObjectDN = $TargetObjectDN
Report = $report
}

$debugInfoPath = Join-Path $PSScriptRoot "DebugInfo.xml"
$debugInfo | Export-Clixml -Path $debugInfoPath
Write-Host "Debug info saved to $debugInfoPath"
}
}
Loading

0 comments on commit 6e428e0

Please sign in to comment.