For one of our customers, we are working on restricting permissions of admin accounts by implementing Role Based Access and delegating permissions to Organizational Units (OUs). But one of the first questions was… What are the current permissions, and what should we remove and where? In this blog post, I will show you a way to report on the current permissions so that you can remove them where they shouldn’t be granted 🙂
Requirements
The script should scan all OUs in the Active Directory Domain, but you should also be able to specify a certain OU to start including all child OUs. The output should be stored in a CSV file for easy import in Excel or other tools which could report on the data. Things like inheritance should be reported, and on what type of objects the permissions were given. (For example, when Delegate Control was used for Helpdesk tasks like resetting/unlocking accounts etc.)
Challenges
During the creation and testing of the Get-ActiveDirectoryOupermissions function, I ran into a few issues:
- Some ObjectTypes were not translated to a friendly/more readable name. Luckily someone already wrote a function for that which I used in the script (Downloaded that from here)
- Some Built-In Security Identifiers were also not translated, translated using a Microsoft Docs page as input (here)
Running the script
After running the script, the Get-ActiveDirectoryOUpermissions function is available with two Parameters:
- Output, enter the path to where the CSV file should be stored, e.g c:\temp\OU_ACL.csv
- StartOU, the start OU to scan including child OU’s, format it like ‘OU=Servers,DC=domain,DC=Local’
In the example below, I ran the Function with the -Output c:\temp\permissions.csv Parameter

When the script is done, it will tell you how many permissions were exported and where:

The permission.csv looks like this in Excel:

The script
Below is the script. You should run it as Administrator on a Domain-Joined computer with the ActiveDirectory Module installed or on a Domain Controller 😉
function Get-ActiveDirectoryOUpermissions { param ( [Parameter(Mandatory = $true, HelpMessage = "Enter the path to where the CSV file should be stored, e.g c:\temp\OU_ACL.csv")][string]$Output, [Parameter(Mandatory = $false, HelpMessage = "Start OU to scan including child OU's, format it like 'OU=Servers,DC=domain,DC=Local'")][string]$StartOU ) #Validate output by creating the file, stop if location is inaccessible try { New-Item -Path $Output -ItemType File -Force:$true -ErrorAction Stop | Out-Null Write-Host ("Output to {0} is valid" -f $Output) -ForegroundColor Green } catch { Write-Warning ("The output can't be saved as {0}, is specified path accessible?" -f $Output) return } #Try to detect the Active Directory Domain name before continuing and stop script if it fails try { $domain = (Get-ADDomain -ErrorAction Stop).DNSroot Write-Host ('Domain {0} detected' -f $domain) -ForegroundColor Green } catch { Write-Warning "Could not retrieve Domain Name, is the ActiveDirectory module installed or are you running this from a non-domain-joined device?" return } #Continu if Active Directory Domain was detected and retrieve list of OU's from the whole domain #or from the Ou specified in the StartOU parameter if ($domain) { if ($StartOU) { try { Write-Host ("Retrieving OU's for Domain {0} starting from {1}" -f $domain, $StartOU) -ForegroundColor Green $oulist = Get-ADOrganizationalUnit -SearchBase $StartOU -Filter * -ResultSetSize 10000 -SearchScope Subtree -ErrorAction Stop | Sort-Object DistinguishedName } catch { Write-Warning ("Could not use {0}, check spelling and format it like 'OU=Servers,DC=domain,DC=Local')" -f $startou) return } } else { Write-Host ("Retrieving all OU's for Domain {0}" -f $domain, $StartOU) -ForegroundColor Green $oulist = Get-ADOrganizationalUnit -Filter * -ResultSetSize 10000 -SearchScope Subtree -ErrorAction Stop | Sort-Object DistinguishedName } } #Function for translating ObjectTypes to name #Thanks go out for the blog here https://blog.wobl.it/2016/04/active-directory-guid-to-friendly-name-using-just-powershell/ function Get-NameForGUID { [CmdletBinding()] Param( [guid]$guid ) Begin { $DomainDC = ([ADSI]"").distinguishedName $ExtendedRightGUIDs = "LDAP://cn=Extended-Rights,cn=configuration,$DomainDC" $PropertyGUIDs = "LDAP://cn=schema,cn=configuration,$DomainDC" } Process { If ($guid -eq "00000000-0000-0000-0000-000000000000") { Return "All" } Else { $rightsGuid = $guid $property = "cn" $SearchAdsi = ([ADSISEARCHER]"(rightsGuid=$rightsGuid)") $SearchAdsi.SearchRoot = $ExtendedRightGUIDs $SearchAdsi.SearchScope = "OneLevel" $SearchAdsiRes = $SearchAdsi.FindOne() If ($SearchAdsiRes) { Return $SearchAdsiRes.Properties[$property] } Else { $SchemaGuid = $guid $SchemaByteString = "\" + ((([guid]$SchemaGuid).ToByteArray() | ForEach-Object { $_.ToString("x2") }) -Join "\") $property = "ldapDisplayName" $SearchAdsi = ([ADSISEARCHER]"(schemaIDGUID=$SchemaByteString)") $SearchAdsi.SearchRoot = $PropertyGUIDs $SearchAdsi.SearchScope = "OneLevel" $SearchAdsiRes = $SearchAdsi.FindOne() If ($SearchAdsiRes) { Return $SearchAdsiRes.Properties[$property] } Else { Return $guid.ToString() } } } } } #Custom object for certain Security Identifiers which don't report a friendly name #List is from https://docs.microsoft.com/en-us/windows/security/identity-protection/access-control/security-identifiers $customidentifiers = @{ 'S-1-5-32-544' = 'Administrators' 'S-1-5-32-545' = 'Users' 'S-1-5-32-546' = 'Guests' 'S-1-5-32-547' = 'Power Users' 'S-1-5-32-548' = 'Account Operators' 'S-1-5-32-549' = 'Server Operators' 'S-1-5-32-550' = 'Print Operators' 'S-1-5-32-551' = 'Backup Operators' 'S-1-5-32-552' = 'Replicators' 'S-1-5-32-554' = 'Builtin\Pre-Windows 2000 Compatible Access' 'S-1-5-32-555' = 'Builtin\Remote Desktop Users' 'S-1-5-32-556' = 'Builtin\Network Configuration Operators' 'S-1-5-32-557' = 'Builtin\Incoming Forest Trust Builders' 'S-1-5-32-558' = 'Builtin\Performance Monitor Users' 'S-1-5-32-559' = 'Builtin\Performance Log Users' 'S-1-5-32-560' = 'Builtin\Windows Authorization Access Group' 'S-1-5-32-561' = 'Builtin\Terminal Server License Servers' 'S-1-5-32-562' = 'Builtin\Distributed COM Users' 'S-1-5-32-568' = 'Builtin\IIS_IUSRS' 'S-1-5-32-569' = 'Builtin\Cryptographic Operators' 'S-1-5-32-573' = 'Builtin\Event Log Readers' 'S-1-5-32-574' = 'Builtin\Certificate Service DCOM Access' 'S-1-5-32-575' = 'Builtin\RDS Remote Access Servers' 'S-1-5-32-576' = 'Builtin\RDS Endpoint Servers' 'S-1-5-32-577' = 'Builtin\RDS Management Servers' 'S-1-5-32-578' = 'Builtin\Hyper-V Administrators' 'S-1-5-32-579' = 'Builtin\Access Control Assistance Operators' 'S-1-5-32-580' = 'Builtin\Remote Management Users' } #Create empty variable acltotal, loop through all OU's and save the ACL's to $acltotal $acltotal = foreach ($ou in $oulist) { Write-Host ("Processing {0}" -f $ou.DistinguishedName) -ForegroundColor Green $acls = (Get-Acl -path "AD:$($ou.DistinguishedName)").Access foreach ($acl in $acls) { #If IdentityReference matches item in $customidentifiers, change it to the friendly name #Otherwise just use the IdentityReference found by Get-Acl if ($customidentifiers | Select-string "$($acl.IdentityReference.Value)" -SimpleMatch ) { $IdentityReference = ($customidentifiers | Select-Object -Property $acl.IdentityReference.Value).$($acl.IdentityReference.Value) } else { $IdentityReference = "$($acl.IdentityReference)" } Write-Host ("- Retrieving {0} details for {1}" -f $acl.ActiveDirectoryRights, $IdentityReference) -ForegroundColor Gray [PSCustomObject]@{ OrganizationalUnit = $ou.DistinguishedName Principal = $IdentityReference Rights = $acl.ActiveDirectoryRights AppliesTo = Get-NameForGUID $acl.InheritedObjectType Item = Get-NameForGUID $acl.ObjectType Access = $acl.AccessControlType Inheritance = $acl.InheritanceType InheritanceFrom = $acl.InheritanceFlags } } } #Export results to CSV file if ($acltotal.count -gt 0) { Write-Host ("Exporting {0} results to {1}" -f $acltotal.count, $Output) -ForegroundColor Green $acltotal | Sort-Object OrganizationalUnit, Principal, Rights, AppliesTo, Item, Access, Inheritance, InheritanceFrom | Export-Csv -Path $Output -Encoding UTF8 -Delimiter ';' -NoTypeInformation } }
Download the script(s) from GitHub here
I’ve been using Ashley McGlone’s OUPermissions script for several years. Both that script (which I subsequently modified) and yours overlooks the other containers in Active Directory that administrators may also need to review permissions for.
Yes, containers like Computers or Users are not taken into the report. The script only does OU’s, I could update it in the future to also include containers
Hi Harm,
Based on your script, I’ve made some minor improvments.
– You can choose to export in a .csv or .xlsx file (using ImportExcel Module).
– I’m also use ValidateScript to validate that the path is a leaf path.
https://gist.github.com/Rapidhands/97fc54eec193e323619e77dc35e19bf1
Pingback: Report on Active Directory Container permissions using PowerShell | PowerShell is fun :)
I created a Container permissions script here https://powershellisfun.com/2022/08/22/report-on-active-directory-container-permissions-using-powershell/
Hello Harm,
I am getting following errors when I run the script.
Exception calling “FindOne” with “0” argument(s): “There is no such object on the server.
”
At line:113 char:17
+ $SearchAdsiRes = $SearchAdsi.FindOne()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DirectoryServicesCOMException
Pavel
Is your line 113 this : ‘S-1-5-32-569’ = ‘Builtin\Cryptographic Operators’ ? And does it work when you comment that line?
Hi Harm, thanks for the script! I’m getting the same errors the previous commenter mentioned but at lines 68 and 79 in your loop when executing “$SearchAdsiRes = $SearchAdsi.FindOne()”. Unfortunately it looks like the link to blog.wobl.it is no longer working. Any thoughts/suggestions on these errors? I get it for many by not all groups. Seems to be an issue with the “FindOne()” option.
Retrieving CreateChild, DeleteChild details for BUILTIN\Print Operators
Exception calling “FindOne” with “0” argument(s): “There is no such object on the server.
”
At C:\Users***removed***\Get-ActiveDirectoryOUpermissions.ps1:68 char:21
$SearchAdsiRes = $SearchAdsi.FindOne()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CategoryInfo : NotSpecified: (:) [], MethodInvocationException
FullyQualifiedErrorId : DirectoryServicesCOMException
Exception calling “FindOne” with “0” argument(s): “There is no such object on the server.
”
At C:\Users***removed***\Get-ActiveDirectoryOUpermissions.ps1:79 char:25
+ $SearchAdsiRes = $SearchAdsi.FindOne()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DirectoryServicesCOMException
Thank you!
Mike
Strange, I did run the script on the AD of one of our customers two weeks ago without any issues… I will try again in my test AD to see what’s going on! Could you give me the AD level? 2008R2, 2012R2, 2016? And does this one work? https://powershellisfun.com/2022/08/22/report-on-active-directory-container-permissions-using-powershell/ And another question, do you have the Print Operators group present in your AD?
I just tried the container permission script and get the same error. The container script seems to be consistently generating this only for the “NT AUTHORITY\Authenticated Users” Group. The OU permission script generates the error for many (but not all) groups including both Built-in and self-created AD groups.
Retrieving ExtendedRight details for NT AUTHORITY\Authenticated Users
Exception calling “FindOne” with “0” argument(s): “There is no such object on the server.
”
At C:\Users***removed***\Get-ActiveDirectoryContainerPermissions.ps1:67 char:17
$SearchAdsiRes = $SearchAdsi.FindOne()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
CategoryInfo : NotSpecified: (:) [], MethodInvocationException
FullyQualifiedErrorId : DirectoryServicesCOMException
Exception calling “FindOne” with “0” argument(s): “There is no such object on the server.
”
At C:\Users***removed***\Get-ActiveDirectoryContainerPermissions.ps1:78 char:21
+ $SearchAdsiRes = $SearchAdsi.FindOne()
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DirectoryServicesCOMException
To answer your other questions.
It’s a two domain forest, empty root and resource domain. Both Domains and Forest Functional Level are 2016. Yes, the Built In Print Operators group exists.
So strange, I run it on 2012R2 and 2016/2019 servers.. But on PowerShell 5.1, is that a difference perhaps?
Hello Mike,
I have encountered the same error.
The problem seems to be the root child domain constellation.
In line 54 the script does not resolve the root domain where the schema is located but the child domain and therefore runs into an error.
If you fill the variable $DomainDC with the DN of the root domain manually, the script runs without error.
Greetings
Mike
Ah, makes sense! Thanks for sharing!
Thank you Harm and Mike D! Putting the DN of the Root domain in line 54 took care of the issue. Thanks again!
I’m not sure what the deal is with this script, but it does absolutely nothing for me. I’ve run it on several machines and different versions of powershell and it just jumps to a new prompt with no output whatsoever. If I put in the csv path, that file is not created.
“After running the script, the Get-ActiveDirectoryOUpermissions function is available with two Parameters” So, if you run it in ISE or command-line (. .\Get-ActiveDirectoryOUpermissions.ps1) you can then use it in that PowerShell session. (It’s a Function, not a script…)
You could remove the first and the last line, then it works as a script if that’s more convenient?
Ah! I didn’t realize it needed to be dot loaded. Thanks. Great script.