Working with Access Rules in PowerShell

Yesterday I posted a function to create a summary report of ACL information using Windows PowerShell. I posted this in response to a question in the Ask Don and Jeff forum at PowerShell.com. I received an appreciative followup. The next step for this IT Pro it seems is to get a detailed list of the user based access control entries. Here is some of my response.

What you are experiencing is both the pleasure and pain of PowerShell. You can get to some amazing information, but sometimes it is buried deeply and takes a little work to unwind. Assuming you have my function loaded in your shell, try this on a small test folder.


dir c:\work -recurse | Where {$_.PSIsContainer} |
get-aclinfo | Where {$_.UserAcl -gt 0} |
ForEach {
$path=$_.Path
$_ | select -expand accessrules |
where {$_.identityreference -notmatch "BUILTIN|NT AUTHORITY|EVERYONE|CREATOR OWNER"} |
Select @{Name="Path";Expression={$Path}},IdentityReference,FileSystemRights
}

The first part command, DIR, gets goes through C:\Work recursively. These objects are piped to Where-Object which only keeps containers, i.e. folders. These folder objects are then piped to my Get-ACLInfo function. Its results are then piped to another Where-Object to filter out anything that doesn’t have a UserACL value greater than 0.

Now it gets a little trickier. I want to display both the file path and get at the underlying, nested access rules. So I’ll pipe each of my aclinfo objects to ForEach-Object. The first thing I do is save the path property from the incoming object. Then I pipe the object to Select-Object, expanding the Accessrules property. Remember, this is a collection of accessrule objects.

These in turn are filtered again to weed out the system accounts. You could also modify the filter to match say on a domain name or specific username. Finally, the filtered results are piped to Select-Object which shows the username, their rights, and a custom property that uses the saved Path variable.

Here’s what the end result looks like:


Path IdentityReference FileSystemRights
---- ----------------- ----------------
C:\work\foo SERENITY\fooby FullControl
C:\work\foo\test SERENITY\fooby FullControl
C:\work\foo\test1 SERENITY\fooby FullControl
C:\work\foo\test2 SERENITY\fooby FullControl
C:\work\foo\test1\foo SERENITY\fooby FullControl
C:\work\foo\test1\foo2 SERENITY\fooby FullControl
C:\work\foo\test1\foo3 SERENITY\fooby FullControl
C:\work\foo\test2\bar SERENITY\fooby FullControl
C:\work\foo\test2\bar2 SERENITY\fooby FullControl

In reality though, you could probably skip my function altogether since all you want are the underlying access rules. Here’s a variation that uses Get-ACL.


dir c:\work -recurse | Where {$_.PSIsContainer} | get-acl |
ForEach {
[regex]$regex="\w:\\\S+"
$path=$regex.match($_.Path).Value
$_ | select -expand access |
where {$_.identityreference -notmatch "BUILTIN|NT AUTHORITY|EVERYONE|CREATOR OWNER"} |
Select @{Name="Path";Expression={$Path}},IdentityReference,FileSystemRights
}

The logic is essentially the same except I threw in my regex code to make the folder path easier to read. Otherwise you get a path value like Microsoft.PowerShell.Core\FileSystem::C:\scripts\. I’ll admit this is a bit much to get your head around, especially for people still starting out in PowerShell. But I hope my logical explanation helps.

Get ACL Information with PowerShell

I got a question in the “Ask Don and Jeff” forum on PowerShell.com that intrigued me. The issue was working with the results of the Get-ACL cmdlet. The resulting object includes a property called Access which is a collection of access rule objects. Assuming you are using this with the file system, these are System.Security.AccessControl.FileSystemAccessRule objects that look like this:

FileSystemRights : ReadAndExecute, Synchronize
AccessControlType : Allow
IdentityReference : BUILTIN\Users
IsInherited : False
InheritanceFlags : ContainerInherit, ObjectInherit
PropagationFlags : None

If I’m understanding the original problem, the poster wanted to identify folders that had a single non-system entry. That is, a folder where someone added a single entry. It doesn’t matter who. So this got me to thinking about a tool that would look at a folder ACL and report on how many access rules were found and then break that count down by system and user. I figured that any rule where the identity reference name included “Builtin”, “NT Authority”, “Everyone” or “Creator Owner” would be considered a system rule. Anything else would be considered a user rule.

In the console, I could run a command like this:


PS S:\> get-acl c:\work | select -expand access | where {$_.identityreference -notmatch "BUILTIN|NT AUTHORITY|EVERYONE|CREATOR OWNER"}

FileSystemRights : FullControl
AccessControlType : Allow
IdentityReference : SERENITY\Jeff
IsInherited : False
InheritanceFlags : None
PropagationFlags : None

Because I ran this interactively, I know what folder has this potential issue. So the next step is to turn this into a tool that will write ACL summary information to the pipeline. Here’s my function, and then I’ll explain a few things. The download version includes comment based help.


Function Get-ACLInfo {

[cmdletbinding()]

Param(
[Parameter(Position=0,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
[ValidateScript({Test-Path $_})]
[Alias('PSPath','Fullname')]
[string[]]$Path="."
)

Begin {
Write-Verbose "Starting $($myinvocation.mycommand)"

#create a format file on the fly
[email protected]"




JDH.ACLInfo

JDH.ACLInfo




50



8


9


7






Path


Owner


TotalACL


SystemACL


UserACL








"@
#create a temp file
$tmpfile=[system.io.path]::GetTempFileName()
#add the necessary file extension
$tmpfile+=".ps1xml"

#pipe the xml text to the temp file
Write-Verbose "Creating $tmpfile"
$xml | Out-File -FilePath $tmpfile

<# update format data. I'm setting error action to SilentlyContinue because everytime you run the function it creates a new temp file but Update-FormatData tries to reload all the format files it knows about in the current session, which includes previous versions of this file which have already been deleted. #>
Write-Verbose "Updating format data"
Update-FormatData -AppendPath $tmpfile -ErrorAction SilentlyContinue

} #Begin

Process {
Foreach ($folder in $path) {
Write-Verbose "Getting ACL for $folder"
#get the folder ACL
$acl=Get-ACL -Path $path

#a regex to get a file path
[regex]$regex="\w:\\\S+"

#get full path from ACL object
$folderpath=$regex.match($acl.path).Value

#get Access rules
$access=$acl.Access

#get builtin and system ACLS
$sysACL=$access | where {$_.identityreference -match "BUILTIN|NT AUTHORITY|EVERYONE|CREATOR OWNER"}

#get non builtin and system ACLS
$nonSysACL=$access | where {$_.identityreference -notmatch "BUILTIN|NT AUTHORITY|EVERYONE|CREATOR OWNER"}

#grab some properties and add them to a hash table.
[email protected]{
Path=$folderpath
Owner=$acl.Owner
TotalACL=$access.count
SystemACL=($sysACL | measure-object).Count
UserACL=($nonSysACL | measure-object).Count
AccessRules=$access
}

#write a new object to the pipeline
$obj=New-object -TypeName PSObject -Property $hash
#add a type name for the format file
$obj.PSObject.TypeNames.Insert(0,'JDH.ACLInfo')
$obj

} #foreach

} #Process

End {
#delete the temp file if it still exists
if (Test-Path $tmpfile) {
Write-Verbose "Deleting $tmpfile"
Remove-Item -Path $tmpFile
}
Write-Verbose "Ending $($myinvocation.mycommand)"
} #end

} #end function

When I run the function here’s what the result looks like:


PS S:\> get-aclinfo | select *

SystemACL : 7
Owner : SERENITY\Jeff
UserACL : 0
AccessRules : {System.Security.AccessControl.FileSystemAccessRule, System.Security.AccessControl.Fi
leSystemAccessRule, System.Security.AccessControl.FileSystemAccessRule, System.Securi
ty.AccessControl.FileSystemAccessRule...}
Path : C:\scripts\
TotalACL : 7

The function takes a path parameter and passes that to Get-ACL.


$acl=Get-ACL -Path $path

I will be using some of the properties of this object in the custom object I’ll eventually write to the pipeline. One thing I want is the full path. Unfortunately, I need to parse that out of the path property. I decided to use a regular expression.


#a regex to get a file path
[regex]$regex="\w:\\\S+"

#get full path from ACL object
$folderpath=$regex.match($acl.path).Value

Next, I need to count the access rule entries and determine which are system and which are user.


#get Access rules
$access=$acl.Access

#get builtin and system ACLS
$sysACL=$access | where {$_.identityreference -match "BUILTIN|NT AUTHORITY|EVERYONE|CREATOR OWNER"}

#get non builtin and system ACLS
$nonSysACL=$access | where {$_.identityreference -notmatch "BUILTIN|NT AUTHORITY|EVERYONE|CREATOR OWNER"}

I like creating new objects with hash tables, which will get even easier in PowerShell 3.0.


#grab some properties and add them to a hash table.
[email protected]{
Path=$folderpath
Owner=$acl.Owner
TotalACL=$access.count
SystemACL=($sysACL | measure-object).Count
UserACL=($nonSysACL | measure-object).Count
AccessRules=$access
}

#write a new object to the pipeline
$obj=New-object -TypeName PSObject -Property $hash

The object shows counts of the different ACL “types” and also includes a property with the full access rules should you want to look at them in more detail. But now for the “scoop of ice cream on the side” part of this function.

In PowerShell 2.0, hash tables are unordered meaning there’s no guarantee what order your properties will be display. Plus, you may want to have more control over how PowerShell formats the resulting objects. The way we handle this is by creating a format file and loading it into the shell with Update-FormatData. I’m not going to go into the mechanics of custom format files here. I know the topic is covered in the Windows PowerShell 2.0: TFM book, and the Month of Lunches books among others.

Now, if I had created a module out of this function I could have packaged it with a separate format ps1xml file. But I had a thought of trying to use a format file “on the fly”. In the Begin script block, I have the XML that would normally go into the format.ps1xml file. I create a temp file and add the xml to it.


#create a temp file
$tmpfile=[system.io.path]::GetTempFileName()
#add the necessary file extension
$tmpfile+=".ps1xml"

#pipe the xml text to the temp file
Write-Verbose "Creating $tmpfile"
$xml | Out-File -FilePath $tmpfile

The format file apparently needs to have .ps1xml file extension so I have to update the file name. Once I have this file, I call Update-FormatData to load it.


Update-FormatData -AppendPath $tmpfile -ErrorAction SilentlyContinue

Normally, I’m not a big fan of turning off errors, but in this case I need to. When you run Update-FormatData, PowerShell reloads all the format files that it knows in this session. The first time I run the function, the temp file is created and loaded. But in the End script block, I delete it.


if (Test-Path $tmpfile) {
Write-Verbose "Deleting $tmpfile"
Remove-Item -Path $tmpFile
}

The next time I run the function, I recreate the format file. But Update-FormatData also looks for the previous temp files which have since been deleted, which normally raises an exception. I’m not saying this approach of “on the fly formatting” is perfect but so far it works for me in these situations. The formatting file takes my custom object and creates a table with all properties except the access rules.

My format file allows for plenty of space for the file path. But you can always tighten things up.

If I want to work with the underlying access rules, I still can.

Download Get-ACLInfo and let me know what you think.

Creating ACL Reports

I saw a tweet this morning that was a PowerShell one-liner for capturing folder permissions to a text file. There’s nothing wrong with it but it’s hard to be truly productive in 140 characters so I thought I would take the idea and run with it a little bit. Here are some ways you might want to extend the concept. Continue reading