1

I have made a script that has to loop through thousands of AD user home directories one by one, basically doing the following steps for each one:

  • Take ownership of the folder
  • Add an access rule for Domain Admin group
  • Return the ownership of the folder
  • Loop through all child folders and files, enabling inheritance and removing all explicit permissions

After excessive testing and problem solving the script works perfectly, except for 1 problem that has left me banging my head against a wall.

The script successfully loops about 50-150 folders (very random) and then results in the following error: "the trust relationship between the primary domain and the trusted domain failed"

I built an additional loop that will retry 30 times (every 30 seconds) when this error occurs. However this does not help as the trust relationship remains lost for as long as the script runs.

The most interesting part is, that once I run the script again, (starting from the problem-folder) the folder is processed without further error. The script never gets stuck on the same folder again. But then this happens again, say 50 folders later.

This is a HUGE inconvenience as I will need to process at least 15,000 user folders and I will always need to compile a new list of "folders left to process", when 1 fails.

Here is the basic code functionality, where I've taken out all the unnecessary error handling and retry-looping for better readability:

foreach ($folder in $homeFoldersFound) {
    $accessControl = Get-Acl -LiteralPath $folder.FullName -ErrorAction Stop

    #Current owner
    $folderOwner = $accessControl.Owner

    #Take ownership for the user running the script
    $accessControl.SetOwner([System.Security.Principal.NTAccount]$currentUser)

    #Access rule to add
    $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($groupToAdd,"FullControl","ContainerInherit,ObjectInherit", "None", "Allow")
    $accessControl.AddAccessRule($accessRule)

    #Purge current explicit permissions
    $accessControl.SetAccessRuleProtection($true, $false)

    #Apply ownership and access rules
    set-acl -AclObject $accessControl -LiteralPath $folder.FullName -ErrorAction Stop | Out-Null


    #Return the previous ownership and apply
    $accessControl.SetOwner([System.Security.Principal.NTAccount]$folderOwner)
    $accessControl.SetAccessRuleProtection($false, $false)
    set-acl -AclObject $accessControl -LiteralPath $folderItem -ErrorAction Stop | Out-Null


    #Loop through child items, enable inheritance & remove explicit permissions
    foreach ($item in (Get-ChildItem -LiteralPath $folder.FullName -Recurse -ErrorAction Stop)) {
        #More code
    }
}

Again, there shouldn't really be anything wrong with the code, as the error happens so randomly and passes when running the script again. Any ideas on what might cause this / how to work around it?

All help is appreciated!

Errno
  • 187
  • 1
  • 1
  • 11
  • Are you running the script on the machine that have the folders with the local path? e.g. D:\sharefoldername\users\ ? I had the same problem when I was querying the rights of each folder in my file server and I I didn't had a problem when I was running it from the fileserver (2012 R2). If I want to run it from my machine I run a get-childitem -path directory export those results into multiple txt files and then run my permission script with each text file as an input. Hope you understand what I am saying. – StefTheo Jul 13 '18 at 07:22
  • Is `$homeFoldersFound` referring to a general domain name or a specific domain server?. If you referring to a general domain name, I guess that the server you talking to might suddenly change and not yet been replicated. – iRon Jul 13 '18 at 07:43
  • `$userFolder = "\\AD.l\path\to\folders"` `$homeFoldersFound = (Get-ChildItem -LiteralPath $userFolder -Directory -Force)` It's a DFS-file system and I cannot access the folders from a local machine. As you can see, I need to use the share path starting with '\\' – Errno Jul 13 '18 at 07:57
  • @iRon are you suggesting that instead of using "\\domain.local\path\folders\" I should use, for instance, "\\domainDC3\path\folders\" ? – Errno Jul 13 '18 at 09:18
  • Yes, that is exactly what I would try. – iRon Jul 13 '18 at 09:26
  • I tried using paths through several different servers but it looks like the same error keeps occurring every time. – Errno Jul 13 '18 at 11:21
  • Have you tried executing the commands locally? Utilizing `Invoke-Command`? Are the home folders all on a share/DFS? – Maximilian Burszley Jul 13 '18 at 13:20
  • @TheIncorrigible1 This is what I wanted to try, however I've been told it's not possible to access the local machine of the drive system. They are all in the same share path – Errno Jul 16 '18 at 05:48
  • FYI: There's a workaround for this here: https://powershell.org/forums/topic/set-acl-on-resource-cross-trusted-forest/. I had a similar issue and this resolved part of it (specifically, my call to the identity reference's `Translate([System.Security.Principal.SecurityIdentifier])`). It didn't help me for the `AddAccessRule` part, but seems to have helped the original requester.... – JohnLBevan Aug 21 '19 at 14:45
  • Update: The solution on that post does work... it hadn't been working for me because my SID was becoming corrupted (a side effect of this issue https://stackoverflow.com/questions/57572937/property-passed-to-invoke-command-changes-type-from-idictionary-to-hashtable), so another function was attempting to recalculate it). – JohnLBevan Aug 21 '19 at 16:31

1 Answers1

0

When you call AddAccessRule, if the identity reference is of type System.Security.Principal.SecuriyIdentifier then you don't hit this problem. The issue seems to occur on conversion from NTAccount to SecurityIdentifier; be this by you calling $ntAccount.Translate([System.Security.Principal.SecuriyIdentifier]), or leaving this for AddAccessRule to do in the background upon receiving an identity reference of any type other than SecurityIdentifier.

The good news is that translating from a SID held as a string to type SecurityIdentifier doesn't have this issue; so a simple cast is sufficient; e.g. [System.Security.Principal.SecurityIdentifier]'S-1-1-0'.

To get the SID without using the Translate option, you may be able to pull it from AD ((Get-AdUser 'myUsername').SID if you have the AD module installed, HexSIDToDec(([ADSI]("WinNT://$myDomain/$myUsername,user")).objectSID) if not).

Alternatively, Dave Wyatt gives a great solution for getting the SID of a user via the Windows API in his Get-Sid function. His code copied below:

function Get-Sid
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $Account,

        [Parameter(Mandatory = $false, Position = 1)]
        [System.String]
        $Domain = $null
    )

    Add-Type -TypeDefinition @'
        using System;
        using System.Runtime.InteropServices;
        using System.Text;

        public enum SID_NAME_USE 
        {
            SidTypeUser = 1,
            SidTypeGroup,
            SidTypeDomain,
            SidTypeAlias,
            SidTypeWellKnownGroup,
            SidTypeDeletedAccount,
            SidTypeInvalid,
            SidTypeUnknown,
            SidTypeComputer
        }

        public class NativeMethods
        {
            [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError = true)]
            public static extern bool LookupAccountName (
                string lpSystemName,
                string lpAccountName,
                [MarshalAs(UnmanagedType.LPArray)] byte[] Sid,
                ref uint cbSid,
                StringBuilder ReferencedDomainName,
                ref uint cchReferencedDomainName,
                out SID_NAME_USE peUse);
        }
'@

    $NO_ERROR = 0
    $ERROR_INSUFFICIENT_BUFFER = 122
    $ERROR_INVALID_FLAGS = 1004

    $sidBytes = $null
    $sidByteCount = 0
    $referencedDomainName = New-Object System.Text.StringBuilder
    $referencedDomainNameCharCount = [System.UInt32]$referencedDomainName.Capacity
    [SID_NAME_USE]$sidNameUse = [SID_NAME_USE]::SidTypeUnknown

    $errorCode = $NO_ERROR

    if (-not [NativeMethods]::LookupAccountName($Domain, $Account, $sidBytes, [ref]$sidByteCount, $referencedDomainName, [ref] $referencedDomainNameCharCount, [ref] $sidNameUse))
    {
        $errorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
        if ($errorCode -eq $ERROR_INSUFFICIENT_BUFFER -or $errorCode -eq $ERROR_INVALID_FLAGS)
        {
            $sidBytes = New-Object Byte[]($sidByteCount)
            $null = $referencedDomainName.EnsureCapacity([int]$referencedDomainNameCharCount)
            $errorCode = $NO_ERROR

            if (-not [NativeMethods]::LookupAccountName($Domain, $Account, $sidBytes, [ref]$sidByteCount, $referencedDomainName, [ref] $referencedDomainNameCharCount, [ref] $sidNameUse))
            {
                $errorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
            }
        }
    }
    else
    {
        $displayAccount = ""

        if (-not [string]::IsNullOrEmpty($Domain))
        {
            $displayAccount += "$Domain\"
        }

        $displayAccount += $Account

        throw "Account '$displayAccount' could not be translated to a SID."
    }

    if ($errorCode -eq $NO_ERROR)
    {
        $sid = New-Object System.Security.Principal.SecurityIdentifier($sidBytes,0)
        Write-Output $sid
    }
    else
    {
        throw (New-Object System.ComponentModel.Win32Exception($errorCode))
    }
}

Get-Sid -Domain 'DOMAIN' -Account 'GroupName'

NB: In my ADSI example, I used a HexSIDToDec function to convert from a byte array to a SID string. Code for that can be found here / copied below.

Function HexSIDToDec($HexSID)
{
    # Convert into normal array of bytes.
    $strSID = "S-" + $HexSID[0]
    $arrSID = $strSID.Split(" ")
    $Max = $arrSID.Count
    $DecSID = $arrSID[0] + "-" + $arrSID[1] + "-" + $arrSID[8]
    If ($Max -eq 11)
    {
      Return $DecSID
    }
    $Temp1 = [Int64]$arrSID[12] + (256 * ([Int64]$arrSID[13] + (256 * ([Int64]$arrSID[14] + (256 * ([Int64]$arrSID[15]))))))
    $DecSID = $DecSID + "-" + $($Temp1)
    If ($Max -eq 15)
    {
      Return $DecSID
    }
    $Temp2 = [Int64]$arrSID[16] + (256 * ([Int64]$arrSID[17] + (256 * ([Int64]$arrSID[18] + (256 * ([Int64]$arrSID[19]))))))
    $DecSID = $DecSID + "-" + $($Temp2)
    $Temp3 = [Int64]$arrSID[20] + (256 * ([Int64]$arrSID[21] + (256 * ([Int64]$arrSID[22] + (256 * ([Int64]$arrSID[23]))))))
    $DecSID = $DecSID + "-" + $($Temp3)
    If ($Max -lt 24)
    {
      Return $DecSID
    }
    $Temp4 = [Int64]$arrSID[24] + (256 * ([Int64]$arrSID[25] + (256 * ([Int64]$arrSID[26] + (256 * ([Int64]$arrSID[27]))))))
    $DecSID = $DecSID + "-" + $($Temp4)
    Return $DecSID
}

I've copied these code samples verbatim from their sources.

JohnLBevan
  • 22,735
  • 13
  • 96
  • 178