PowerShell: SMB Shares Migration

$object='\\fileserver\sharename'
$identity='domain\account'
$permissions='Full'

function importNtfsSecurity{
    try{
        set-executionpolicy unrestricted -force
        # Include the required NTFSSecurity library from the PowerShell Gallery
        if (!(Get-InstalledModule -Name NTFSSecurity -ErrorAction Ignore)) {
            Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
            Install-Module -Name NTFSSecurity -Force
            Import-Module NTFSSecurity -Force
            }
        }
        catch{
            write-host "NTFS Security module is required. Please install it before proceeding."
            }
        }
importNtfsSecurity

Add-NTFSAccess -Path $object -Account $identity -AccessRights $permissions
<# SMB_Share_Migration_0.1.ps1

Purpose:
This script connects to an external domain and copy files onto a Intranet server. Since corresponding accounts on the file permissions list
at the source domain may not exist at the destination domain, it may be necessary to create those objects at the destination domain.
It would then be possible to substitute the external identities with corresponding local ones.

1. Report Generation Section (done)
- Connect to an SMB Share (UNC path) on external domain with provided credentials
- Obtain NTFS permissions of that SMB Share and output a CSV report

2. Generating Accounts and Groups
- Automatically generate group names and user names with on the local Active Directory domain, if those objects do not already exist

3. Trigger mirroring copy operation from external SMB share to internal domain (done)
- Connect to an SMB Share (UNC path) on external domain with provided credentials
- Copy files to local file server

4. Recreate NTFS folder persmissions at the destination (done)
- Receive accounts substitution list as input
- Parse through the destination folders and files to apply the translated permissions list

#>

$sourceDomainUser="domain\username"
$plainTextPassword="plainTextPassword"
$sourceDomainPassword = ConvertTo-SecureString $plainTextPassword -AsPlainText -Force
$sourceCredential = New-Object System.Management.Automation.PSCredential ($sourceDomainUser, $sourceDomainPassword)

################################################### ↓ Copying Operation Section ↓ ###################################################

$arr=@{}
$arr["from"] = @{}; $arr["to"] = @{}
$baseUNC="\\fileserver01.domain.com\sharename";$baseLocalShare="Z:\sharename";
$arr["from"] = @("$baseUNC\somepath"); $arr["to"]=@("$baseLocalShare\somepath")

$dateStamp = Get-Date -Format "yyyy-MM-dd-hhmmss"
$scriptName=$MyInvocation.MyCommand.Path
$scriptPath=Split-Path -Path $scriptName
$logPath="$scriptPath\logs"
if(!(Test-Path $logPath)){New-Item -ItemType Directory -Force -Path $logPath}
$logFile="$logPath\robocopy-log-$dateStamp.txt"
$log="/LOG+:'$logFile'"
$switches="/MIR /R:0 /W:0 /XO /XJD /XJF /FFT /MT:32 /TBD /NP "
$GLOBAL:pathErrors="";
$GLOBAL:stopWatch= [System.Diagnostics.Stopwatch]::StartNew()

function validatePaths{
    param($GLOBAL:object=$arr)
    $objectLength=$object.from.length 
    
    if (selectPsDrive){
        if ($firstAvailableDriveLetter -in (Get-PSDrive).Name){Remove-PSDrive -Name $firstAvailableDriveLetter -Scope Global}
        try{                
            Invoke-Expression "net use '$firstAvailableDriveLetter`:' $baseUNC /user:$sourceDomainUser $plainTextPassword /y"
            }
            catch{
                clearAllMappedDrives;
                Invoke-Expression "net use '$firstAvailableDriveLetter`:' $baseUNC /user:$sourceDomainUser $plainTextPassword /y"
                }
        # Check sources and destinations. Remove any row with a broken UNC or local directory.
        for ($i=0;$i -lt $objectLength; $i++){
            $from=$object.from[$i]
            $to=$object.to[$i]
            if(!(test-path $from -ErrorAction SilentlyContinue) -OR !(test-path $to -ErrorAction SilentlyContinue) ){         
                # Remove row if any path doesn't resolve. Overcome limitation of Powershell's immutable array "fixed size"
                Write-Host "Removing $($object.from[$i]) and $($object.to[$i]) ..."            
                $object.from = $object.from|?{$_ -ne $from}
                $object.to = $object.to|?{$_ -ne $to}
                $objectLength--;
                $i--;
                }
        }

        Invoke-Expression "net use /delete '$firstAvailableDriveLetter`:' /y"
        return $object
    }else{
        write-host "No available drive letters to proceed with drive mapping."
        break;
        }
}

# This function is useful for sanity checks prior to assigning persistent drive letters
function selectPsDrive{
    # Select available drive letter at this moment in time
    $driveLettersExclusion="[CDHKLMPZ]"
    $availableDriveLetters=ls function:[A-Z]: -n|?{!(test-path $_)}|%{$_[0]}|?{!($_ -match $driveLettersExclusion)}
    $GLOBAL:firstAvailableDriveLetter=$availableDriveLetters[0];
    if ($firstAvailableDriveLetter -in (Get-PSdrive).Name){return $False;}else{return $True;}    
    }

function clearAllMappedDrives{
    # The old-school method
    Net Use * /delete /y
    
    # The fancy new Powow Shill method
    # Get a list of currently mapped drives
    $mappedDrives = Get-WMIObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq 4 }

    if ($mappedDrives) { 
        $driveList = $mappedDrives.DeviceID
            Foreach ($drive in $driveList) {
                $driveLetter = $drive -replace ":"
                Remove-SmbMapping -LocalPath $Drive -Force -UpdateProfile
                If ( (Get-PSDrive -Name $driveLetter) 2>$Null ) {
                    Remove-PSDrive -Name $driveLetter -Force
                    }
                }    
            }
}

function copyFilesFromDifferentDomain{
    param(
        [string]$from,
        [string]$to
        )

    if (selectPsDrive){
            <# Troubleshooting: How to remove persistent and hidden PSDrive
            PS C:\Windows\system32> New-PSDrive -Name $firstAvailableDriveLetter -PSProvider FileSystem -Root $from -Persist -Credential $sourceCredential
            New-PSDrive : The local device name has a remembered connection to another network resource
            At line:1 char:1
            + New-PSDrive -Name $firstAvailableDriveLetter -PSProvider FileSystem - ...
            + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                + CategoryInfo          : InvalidOperation: (A:PSDriveInfo) [New-PSDrive], Win32Exception
                + FullyQualifiedErrorId : CouldNotMapNetworkDrive,Microsoft.PowerShell.Commands.NewPSDriveCommand
            
            PS C:\Windows\system32> net use
            New connections will be remembered.
            Status       Local     Remote                    Network

            -------------------------------------------------------------------------------
            OK                     \\FILESHERVER01.KIMCONNECT.COM\IPC$       Microsoft Windows Network
            The command completed successfully.

            PS C:\Windows\system32> net use /delete \\FILESHERVER01.KIMCONNECT.COM\IPC$
            \\FILESHERVER01.KIMCONNECT.COM\IPC$ was deleted successfully.
            
            The incident above has correlated to the "persistent" setting of the New-PSDrive command that was triggered prior to be followed by Remove-PSDrive
            Persitent: New-PSDrive -Name $firstAvailableDriveLetter -PSProvider FileSystem -Root $from -Persistent -Credential $sourceCredential
            
            Failed experimental method
            Create a UNC map to the outside-domain source
            Test: New-PSDrive -Name "mapped_by_$($env:username)" -PSProvider FileSystem -Root "\\FILESHERVER01.KIMCONNECT.COM\Test" -Credential $sourceCredential
            $mapName=firstAvailableDriveLetter;
            if ($mapName -in (Get-PSDrive).Name){Remove-PSDrive -Name $mapName -Scope Global|Out-Null}
            New-PSDrive -Name $mapName -PSProvider FileSystem -Root $from -Credential $sourceCredential
            #>            
            if ($firstAvailableDriveLetter -in (Get-PSDrive).Name){Remove-PSDrive -Name $firstAvailableDriveLetter -Scope Global}
            try{                
                Invoke-Expression "net use '$firstAvailableDriveLetter`:' $from /user:$sourceDomainUser $plainTextPassword /y"
                #New-PSDrive -Name $firstAvailableDriveLetter -PSProvider FileSystem -Root $from -Credential $sourceCredential|Out-Null
                }
                catch{
                    clearAllMappedDrives;
                    Invoke-Expression "net use '$firstAvailableDriveLetter`:' $from /user:$sourceDomainUser $plainTextPassword /y"
                    #New-PSDrive -Name $firstAvailableDriveLetter -PSProvider FileSystem -Root $from -Credential $sourceCredential|Out-Null
                    }
    
            # Validate the source path prior to copying    
            if(test-path $from -ErrorAction SilentlyContinue){
                try{
                    write-host "robocopy '$from' '$to' $switches $log"
                    invoke-expression "robocopy '$from' '$to' $switches $log"
                    }
                    catch{
                        $errorMessage = $_.Exception.Message
                        $failedItem = $_.Exception.ItemName
                        write-Host "$errorMessage $failedItem"
                        continue;
                        }
                }else{
                    write-Host "$from is not accessible."
                    $GLOBAL:pathErrors+="$from`n"
                    }

            # Release UNC map for future reassignments
            # Failed experimental method: Remove-PSDrive -Name $mapName -Scope Global -Force
            # Remove-PSDrive -Name $firstAvailableDriveLetter -Scope Global -Force
            Invoke-Expression "net use /delete '$firstAvailableDriveLetter`:' /y"
            }        
    }

function processCopyOperations{    
    # Validate inputs
    $GLOBAL:arr=validatePaths -object $arr;

    # Write log header
    $initInfo="=============================================Job Started: $dateStamp=============================================`r`n";
    $initInfo+="Powershell version detected: $($PSVersionTable.PSVersion.Major)`.$($PSVersionTable.PSVersion.Minor)`r`n"      
    $initInfo+="Processing the following operations:`n";
    if($arr.from -is [Array]){
        $initInfo+=for ($i=0;$i -lt $arr.from.length; $i++){
            "$($arr.from[$i]) => $($arr.to[$i])`n";
            }
        }else{$initInfo+="$($arr.from) => $($arr.to)`n";}
    $initInfo;
    Add-Content $logFile $initInfo;

    # Process the copying operations depending on whether we have a multi-dimensional array
    if($arr.from -is [Array]){
        # Create paths to Sources and trigger robocopy for each item in the array
        $arrayLength=$arr.from.length
        for ($i=0; $i -lt $arrayLength; $i++){
            $from=$arr.from[$i]
            $to=$arr.to[$i]
            $processDisplay="=============Pass $($i+1) of $arrayLength`: $from => $to=======================================`r";
            Write-Host $processDisplay;
            Add-Content $logFile $processDisplay;        
            copyFilesFromDifferentDomain -from $from -to $to;
            $passMarker="==============Pass $($i+1) of $arrayLength` Completed=======================================`r";
            Add-Content $logFile $passMarker;        
            }
        }else{
            $processDisplay="=======================================Pass 1 of 1: $($arr.from) => $($arr.to)=======================================`r";
            Write-Host $processDisplay;
            Add-Content $logFile $processDisplay; 
            copyFilesFromDifferentDomain -from $arr.from -to $arr.to;
            $passMarker="==============Pass $($i+1) of $arrayLength` Completed=======================================`r";
            Add-Content $logFile $passMarker; 
            }

    if ($pathErrors){Add-Content $logFile "Path errors:`n$pathErrors";}

    # Stop the timer
    $time=[math]::Round($($GLOBAL:stopWatch.Elapsed.TotalHours),2);

    # Write closure to log
    Write-Host "Copying process completed in $time hours."
    Add-Content $logFile $pathErrors
    Add-Content $logFile "`n========================================Copying process completed in $time hours========================================"
}

#processCopyOperations;
################################################### ↑ Copying Operation Section ↑ ###################################################

################################################### ↓ Report Generation Section ↓ ###################################################

$baseUNC="\\fileserver01.domain.com\sharename";$baseLocalShare="Z:\sharename";
$translatedBaseUNC="\fileserver01.domain2.com\sharename";
$sourceSubDomain="domain";
$destinationSubDomain="domain2";
$foldersDepth=0

$dateStamp = Get-Date -Format "yyyy-MM-dd-hhmmss"
$scriptName=$MyInvocation.MyCommand.Path
$scriptPath=Split-Path -Path $scriptName
#$calsReport="$scriptPath\cals-report-$dateStamp.csv"
$calsReport="$scriptPath\ntfs-permissions-list-$dateStamp.csv"

function generateAclReport{
    # Expected output of the report to be in this format
    # "sourceFolder","destinationFolder","originalPrinciple","translatedPrinciple","permissions","inheritedYesNo"
    param(
        [string]$directory=$baseUNC,
        [int]$depth=$foldersDepth,
        [string]$username=$sourceDomainUser,
        [string]$password=$plainTextPassword
        )
    $folderName=Split-Path $directory -Leaf
    
    # This function is useful for sanity checks prior to assigning persistent drive letters
    function selectPsDrive{
        # Select available drive letter at this moment in time
        $driveLettersExclusion="[CDHKLMPZ]"
        $availableDriveLetters=ls function:[A-Z]: -n|?{!(test-path $_)}|%{$_[0]}|?{!($_ -match $driveLettersExclusion)}
        $GLOBAL:secondAvailableDriveLetter=$availableDriveLetters[1];
        if ($secondAvailableDriveLetter -in (Get-PSdrive).Name){return $False;}else{return $True;}    
        }

    function getAccountType{
        param($account)
        $selectPrincipleRegex=".*\\"        
        $principle=$account -replace $selectPrincipleRegex,"";
        if ($principle -notlike "S-1-5-21*"){
            $userType=try{get-aduser $principle}catch{}
                    if ($userType){
            return "user"
            }else{                
                    $groupType=try{get-adgroup $principle}catch{}
                            if ($groupType){
                    return "group"
                    }else{
                            $computerType=try{get-adcomputer $principle}catch{}
                                    if ($computerType){
                            return "computer";
                            }else{
                                    return "unknown";
                                }
                            }
                }
            } else{return "unknown";}
        }

    function getAcl{
        param(
            [string]$sourceBase=$baseUNC,
            [string]$destinationBase=$translatedBaseUNC,
            [string]$sourceDomain=$sourceSubDomain,
            [string]$destinationDomain=$destinationSubDomain,
            [int]$depth=$foldersDepth
            )
        $folders = Get-ChildItem -Directory -Path $sourceBase -Depth $depth -Recurse -Force | ?{ $_.PSIsContainer -and ($_.Mode -ne "-a----") }
        $output = @()
        ForEach ($folder in $folders) {
            $Acl = Get-Acl -Path $folder.FullName
            ForEach ($Access in $Acl.Access) {
                $Properties = [ordered]@{
                        'sourceFolder'=$folder.FullName;                        
                        'destinationFolder'=$folder.FullName -replace "^$([RegEx]::escape($sourceBase))",$destinationBase;
                        'originalPrinciple'=$Access.IdentityReference;
                        'originalPrincipleType'=getAccountType $Access.IdentityReference;
                        'originalGroupMembers'=if(getAccountType $Access.IdentityReference -eq "group"){
                                            $groupMembers=try{get-adgroupmember $principle}catch{}
                                            if ($groupMembers){
                                                $groupMembers|%{"$($_.SamAccountName):$($_.Name):$($_.objectclass)"}
                                                }
                                            }else{""}
                        'translatedPrinciple'=$Access.IdentityReference -replace $sourceDomain,$destinationDomain
                        'permissions'=$Access.FileSystemRights;
                        #'inheritedYesNo'=$Access.IsInherited
                        }
                $output += New-Object -TypeName PSObject -Property $Properties            
                }
            }
        
        # Generate report
        if ($calsReport -notlike "\cals-report*"){
            $output|Export-Csv -Path $calsReport -NoTypeInformation
            }else{
                $output|Export-Csv -Path .\cals-report.csv -NoTypeInformation
                }
        
        # Display something on console
        return $output | Out-GridView #Generate extra window with grid view
        }

    Function importActiveDirectoryModule{
        if (!(Get-Module ActiveDirectory)){
                Import-Module ServerManager -ErrorAction SilentlyContinue;
                [void](Add-WindowsFeature RSAT-AD-PowerShell -Confirm:$false);
                Import-Module ActiveDirectory -ErrorAction SilentlyContinue;
                }
    }

    if (selectPsDrive){
        importActiveDirectoryModule;
        Invoke-Expression "net use '$secondAvailableDriveLetter`:' $directory /user:$username $password /y"
        getAcl;      
        Invoke-Expression "net use /delete '$secondAvailableDriveLetter`:' /y"
        }else{"No Available Drive Letters to Map Remote UNC Path.";}
}

generateAclReport;
Write-Host "Review the reports and press any key to proceed. Ctrl+C to cancel";
pause;

################################################### ↑ Report Generation Section ↑ ###################################################

################################################### ↓ Creating Accounts Section ↓ ###################################################
Function CreateAccounts{
    Function importActiveDirectoryModule{
        if (!(Get-Module ActiveDirectory)){
                Import-Module ServerManager -ErrorAction SilentlyContinue;
                [void](Add-WindowsFeature RSAT-AD-PowerShell -Confirm:$false);
                Import-Module ActiveDirectory -ErrorAction SilentlyContinue;
                }
    }
    importActiveDirectoryModule;
    
    # Import the permissions list
    # Expecting a list with these headers
    # "sourceFolder","destinationFolder","originalPrinciple","originalPrincipleType","originalGroupMembers","translatedPrinciple","permissions"
    Function importPermisisonsList{
        try{
            $GLOBAL:import=Import-Csv -Path $permissionsFile -UseCulture
            }
            catch{
                write-host "Cannot import the file";
                break;
                }
        }

    Function createObjectIfNotExists{
        param(
            $object,
            $objectType,
            $members
            )
        # I need to write this code
        }    

}

################################################### ↑ Creating Accounts Section ↑ ###################################################

################################################### ↓ Applying NTFS Permissions Section ↓ ###########################################

$scriptName=$MyInvocation.MyCommand.Path
$scriptPath=Split-Path -Path $scriptName
$calsReport="$scriptPath\ntfs-permissions-list.csv"
$permissionsFile="$calsReport";
$errorLogPath="$scriptPath\ntfs-permissions-processing-errors.txt"
# $errorLogPath=((Get-Item -Path ".\").FullName+"\ntfs-permissions-processing-errors.txt");
# Expected input of the report to be in this format
# "sourceFolder","destinationFolder","originalPrinciple","translatedPrinciple","permissions","inheritedYesNo"

function setAcl{
    param(
        $permissionsFile,
        $errorLogPath="C:\scripts\ntfs-permissions-processing-errors.txt"
        )

    # Import the permissions list
    Function importPermisisonsList{
        try{
            $GLOBAL:importFile=Import-Csv -Path $permissionsFile -UseCulture
            }
            catch{
                write-host "Cannot import the file";
                break;
                }
        }
    # Import NTFS Security module
    function importNtfsSecurity{
    try{
        # Include the required NTFSSecurity library from the PowerShell Gallery
        if (!(Get-InstalledModule -Name NTFSSecurity -ErrorAction Continue)) {
            Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
            Install-Module -Name NTFSSecurity -Force
            Import-Module NTFSSecurity -Force
            }
        }
        catch{
            write-host "NTFS Security module is required. Please install it before proceeding."
            }
        }

Function applyPermissions{
        param(
            $object,
            $identity,
            $permissions
            )

        try{
            Add-NTFSAccess –Path "$object" –Account $identity –AccessRights $permissions -ErrorAction Stop;
            Write-Host "$permissions for $identity has been set on $object"
            }
        catch{
            $errorMsg = (Get-Date -Format g)+": "+ $_.Exception.Message+".. " + $directory;
            Add-Content $errorLogPath $errorMsg;
            write-host "$errorMsg";
            $GLOBAL:errorFlag=$true;
            continue;
            }        
    }

    Function processList{

        importNtfsSecurity;
        importPermisisonsList;        
        $GLOBAL:errorFlag=$False;

        foreach ($line in $importFile){
            $folder=$line.destinationFolder;
            $principle=$line.translatedPrinciple;
            $permissions=$line.permissions;
            applyPermissions -object $folder -identity $principle -permissions $permissions;
            }
        if ($errorFlag){write-host "We have encountered some errors and a log has been generated at this location '$errorLogPath'."}
        }
    processList;
}
# setAcl -permissionsFile $permissionsFile -errorLogPath $errorLogPath
################################################### ↑ Applying NTFS Permissions Section ↑ ###########################################


Leave a Reply

Your email address will not be published. Required fields are marked *