Quantcast
Channel: Windows Stuff That Your Dreams Dreamed Of » Import
Viewing all articles
Browse latest Browse all 2

Importing Share Info and Permissions with Powershell and a CSV file (cont’d)

$
0
0

In my last post (here) I talked about MoW’s import script (here) and the customizations that I wanted to make. If you need some brushing up on windows share permissions, I recommend using my post on it as a reference as you go through this. If you just want to see the script, scroll down to the bottom of the post.

For my own import, I stuck with some of MoW’s ideas as the backbone but I needed to write several functions in order to verify that things are working smoothly as the shares are being imported. My script would have to be able to import several thousand shares, so I wanted to safeguard against any possible errors in the CSV file. Also, there are numerous scenarios that could happen while importing other than simply a share needs created, for instance:

  • The share could already exist
  • The share could exist with the wrong permissions
  • The CSV file could have been tampered with or exported incorrectly
  • The file path of the share may not exist

Therefore, I needed some framework for each share to run through as it traverses my import script in order to account for all possible scenarios. On top of this, the script had to be able to import several thousand shares in a relatively short amount of time and therefore speed considerations were also very important.

As I said previously, I organized these ‘scenarios’ into functions that each ‘share’ (not the actual share object, the the object from the CSV containing the share info) will traverse in order to decide what to do with that share. Here is a visual representation of the function flow:

Share Import Function Flow

This is a broad view of what is being accomplished, there are some other functions as well.

At first, my script works similar to MoW’s (as discussed in my last post). The shares are imported with import-CSV from a file made with ExportShareInfo.ps1. He then uses a ‘select each unique share,’ cycling through each with ‘for each unique share, look for other shares with the same name.’ This is done to account for shares that only need created once, but have multiple permissions. I ran into serious speed issues with this part of the script because my file server has so many shares. For each unique share, powershell had to search for shares with the same name in a list of thousands of shares, and then do this thousands of times (1 for each share). So, I decided it would be better to traverse the array in order, comparing each share to the last. If the share is the same as the last share, it is added to an array ‘$arrCSVShareInfo.’ If the current share is a new share the last share is created (with $arrCSVShareInfo as the share’s info), and then a new array is initialized which contains the new share info. The shares in the CSV are probably already in alphabetical order, but I sort them by name before beginning to ensure that shares with the same name with be ‘back to back’ in the list.

So, each share that has all its share info stored in $arrCSVShare info get passed along the function flow as diagrammed above. If there is a problem with anything, the share is failed and added to a hash table ‘problem shares’ with an error description. Here is what each function does:

1.)VerifyAndFix-ShareInfo

This function is a ‘parent’ function to many of the other functions, it passes the share info array to other functions, and then interprets the return values to determine if a share has passed or failed. This is also an advanced function. For info on advanced functions, look here. Basically, I wanted to have this function act like a cmdlet so I could use the ErrorVariable parameter. That way, if a share fails at any step, I can throw an error which will be stored in the error variable wherever I call the function (assuming i give the -EV parameter). The Cmdlet Binding of advanced functions gives this ability.

2.)Check-CleanInput 

Also an advanced function, this functions checks the info in $arrCSVShareInfo and verifies that it is legitimate. There are 8 categories of info for each share that is stored in the CSV, the script checks for validity in all except for ‘description.’ Here is how it checks each part of the share info:

  • Domain: Check if the domain in the CSV is the same as the domain the script is running on. Could add other domains too.
  • Username: Calls functions that check if the user name or group common name exists.
  • File path: uses a regular expression to check if the path is windows valid
  • Share name: uses a regular expression to check the the name is windows valid
  • Access mask: used to let only certain AM’s through (such as read, change, full control)
  • Ace flags: make sure ace flags is valid (0 through 31)
  • Ace type: make sure ace type is valid (0 or 1)

If any of these items fail, an error is thrown with the error message and passed down through the -errorVariable parameter, and the share will not be created. Otherwise, we move on to the next function.

3.)Check-DoesSharePathExist

We want to see if the folder exists on the server. If it doesn’t, a function is called (create-folder) that will create the folder at that destination. If the creation fails, the share is failed.

4.)Get-SharePath

The share name is used to see if a share path is returned. What we really want is whether the share exists or not. If the name has a share path already, than the share already exists.

5.) Create-ShareFromCSVInfo

For shares that meet all specifications and do not already exist, we call this function. This function create new security objects (ACE, Trustee), and fills them with the share info. They are added to a security descriptor object and the win32_share.create method is invoked with the new SD and the necessary share info as parameters. If the return value is ’0,’ we have created the share successfully. If the return value is something else, creation failed and we add the share to the $problemShares hash table.

6.)Check-SharePermissions

For shares that already exist, check and see if the permissions in the CSV match the permissions that are on the server. Look at the security descriptor of the existing share. There may be more than 1 ACE in the CSV, so we compare each CSV entry ($arrCSVShareInfo) with each ACE in the existing SD. If all ACE’s on existing share match the CSV info, than we can pass this share without doing anything else (since it already exists, and is correct). Otherwise, we pass the share into Fix-SharePermissions.

7.) Fix-SharePermissions

This function works similarly to create-sharefromCSVinfo. We make new security objects, filling them with the CSV info. After adding them to a new security descriptor, we invoke the win32_Share ‘setShareInfo’ method. BEWARE, I chose to have the CSV info as the overriding factor for permissions, so all shares that don’t match the CSV will be changed. If you have an old export, you may be changing security info that you don’t want to change. You need to create a new CSV with an export script often (if you have many shares that change often), or remove this function from the script if you don’t wish the CSV info to override existing share info on the server. Once again, if the return value is ’0′ the share passes, otherwise the SetShareInfo method failed and the share is added to $problemShares.

You can see by my function flow map that after traversing these functions, a particular share will have ‘passed’ (been created, fixed, or already existed correctly) or ‘failed’ (added to $problemShares with error message). Then, the script will move on to the next share in the CSV file until all the shares have been dealt with.

Here is the full script. Being a longer script, I highly recommend clicking the button in the top right corner to paste to your clipboard and then look at it in a text editor with PowerShell syntax highlighting. Remember to change the $fileServer and CSV file path to your own name/location!!

# ImportShareInfo.ps1
# This script will Import the Shares from a CSV file
# made by ExportShareInfo.ps1 complete with securityInfo

###### CHANGE FILESERVER TO SERVER SCRIPT IS RUNNING ON ######
$FileServer = "YOUR FILE SERVER HERE"

#Import the CSV file
write-host "Importing the CSV Info"
#CHANGE TO FILEPATH OF CSV
$ShareList = Import-Csv 'YOUR CSV FILE PATH HERE'
write-host "   Complete" -ForegroundColor green
write-host "Sorting share list"
#Sort the Shares
$ShareList = $ShareList | sort-object {$_.name}
write-host "   Complete" -ForegroundColor green

# Advanced function: acts like a cmdlet so the ErrorVariable parameter can be used when its called.
# That way, we can throw an error message, trap it from outputting on host, yet
# store it in EV so we can add the error to the "ProblemShares" hash table
Function Check-CleanInput {
	[CmdletBinding()]
	param($arrCSVShareInfo)
	$blnCleanInput = $null
	$blnCleanInput = $true

	# used below to check domain, user
	$strThisDomainName = [adsi]''; $strThisDomainName = $strThisDomainName.get("Name")

	  $arrCSVShareInfo | % {

			# Domain
			If ($blnCleanInput -eq $true) {
				If ((($_.domain -match $strThisDomainName) -ne $true) -and (($_.domain -match "builtin") -ne $true)){
					Trap {Continue;}
					Throw "The domain name in the CSV file makes the script uncomfortable"
					$blnCleanInput = $false
				}
			}

			# User checks for username and group names- may need to add things specific to your server
			If ($blnCleanInput -eq $true) {
				If ($_.Domain -eq $strThisDomainName) {
					$blnDoesUserExist = Check-DoesUserExist ($_.User)
					If ($blnDoesUserExist -eq $false) {
						#If the user in the CSV isn't a user, check if its a group.
						$blnDoesGroupExist = Check-DoesGroupExist($_.User)
						If ($blnDoesGroupExist -eq $false) {
							Trap {Continue;}
							Throw "The user in the CSV doesn't exist"
							$blnCleanInput = $false
						}
					}
				}
			}

			# Path... RegEx from http://regexlib.com/REDetails.aspx?regexp_id=2285
			If ($blnCleanInput -eq $true) {
				$blnPathValid = ($_.Path -match "^((\\\\[a-zA-Z0-9-]+\\[a-zA-Z0-9`~!@#$%^&(){}'._-]+([ ]+[a-zA-Z0-9`~!@#$%^&(){}'._-]+)*)|([a-zA-Z]:))(\\[^ \\/:*?""<>|]+([ ]+[^ \\/:*?""<>|]+)*)*\\?$")
				If ($blnPathValid -eq $false) {
					Trap {Continue;}
					Throw "The share path in the CSV is not a valid Windows share path"
					$blnCleanInput = $false
				}
			}

			# Share Name... RegEx from http://regexlib.com/REDetails.aspx?regexp_id=1145, but modified it to match share name (original was for user name)
			# Share name can't contain :   "/\[]:|<>+=;,?*  can contain: ~`!@#$%^&()_-{}".
			If ($blnCleanInput -eq $true) {
				$blnShareNameValid = ($_.Name -match "(^[A-Za-z0-9~`!@#$%_\^\&amp;\-\.\ \(\)\{\}]{1,80})$")
				If ($blnShareNameValid -eq $false) {
					Trap {Continue;}
					Throw "The share name in the CSV is not a valid Windows share name"
					$blnCleanInput = $false
				}
			}

			# AccessMask: Read, Change, Full Control are the only access masks currently supported. If there are any accounts with weird permissions,
			# they wont be created (but will appear in "Problem Shares"). Obviously you can add whatever AM's you want here
			If ($blnCleanInput -eq $true) {
				If (`
					#ADD OR REMOVE ACCESS MASKS BASED ON YOUR PREFERENCES
				   ($_.AccessMask -ne "1179817") -and ($_.AccessMask -ne "1245631") -and ($_.AccessMask -ne "2032127")) {
				 	 Trap {Continue;}
				 	 Throw $_.AccessMask + " is an unsupported access mask"
					 $blnCleanInput = $false
				 }
			}

			# Ace Type: 0 is allow, 1 is deny
			If ($blnCleanInput -eq $true) {
				[int]$AceType = $_.AceType
				If (($AceType -ne 0) -and ($AceType -ne 1)) {
				Trap {Continue;}
				Throw "Ace type must be 0 or 1"
				$blnCleanInput = $false
				}
			}

			#Ace Flags is a bit mask of values 1, 2, 4, 8, 16 and therefore any combo is between 1 and 31
			#values are here: http://msdn.microsoft.com/en-us/library/aa392711(v=VS.85).aspx
			If ($blnCleanInput -eq $true) {
				[int]$AceFlags = $_.AceFlags
				If (($AceFlags -lt 0) -or ($AceFlags -gt 31)) {
					Trap {Continue;}
					Throw "Ace flags must be between 0 and 31"
					$blnCleanInput = $false
				}
			}
		}

	Return $blnCleanInput
}

Function Check-DoesSharePathExist($sharePath) {
	$pathExists = $null
	$pathExists = Test-Path $SharePath
	If ($PathExists -ne $true) {
		return $false
	}
	Else {
		Return $true
	}
}

Function Check-DoesGroupExist($groupCN)	{
	#grab all groups with GID's
	$searchRoot = [ADSI]''
	$searcher = new-object System.DirectoryServices.DirectorySearcher($searchRoot)
	$searcher.filter = "(&(objectClass=group)(CN=" + $groupCN + "))"
	$searchResults = $searcher.findall()

	If($searchResults.count -lt 1)
		{$results = $false}
	Else
		{$results = $true}

	$searchResults.Dispose()
	$searcher.Dispose()
	$searchResults = $null
	$searcher = $null

	Return $results
}

Function Check-DoesUserExist($sAMAccountName)	{

	$searchRoot = [ADSI]''
	$searcher = new-object System.DirectoryServices.DirectorySearcher($searchRoot)
	$searcher.filter = "(&(objectClass=person)(sAMAccountName=" + $sAMAccountName + "))"
	$searchResults = $searcher.findall()

	If($searchResults.count -lt 1)
		{$results =  $false}
	Else
		{$results = $true}

	$searchResults.Dispose()
	$searcher.Dispose()
	$searchResults = $null
	$searcher = $null

	Return $results
}

Function Create-Folder($strFolder)	{
	$results = $null
	$results = $false

	If($strFolder -eq $null -or $strFolder -eq "" -or $strFolder -eq $false)
		{$results = $false}
	Else
		{
			If((Test-Path $strFolder) -eq $true)
				{$results = $true}
			Else
				{New-Item $strFolder -itemType Directory}

			If((Test-Path $strFolder) -eq $true)
				{$results = $true}
			Else
				{$results = $false}
		}

	Return $results
}

Function Get-SharePath($shareName)	{
	Trap{continue;}
	$strWMI = $null
	$strWMI = "\\" + $fileserver + "\root\cimv2:win32_share.name='" + $shareName + "'"
	$sharePath = $null
	$sharePath = ([wmi]$strWMI).path
	Return $sharePath
}

Function Check-DoesShareExist($shareName)	{
		$shareExists = $null
		$sharePath = $null
		$sharePath = Get-SharePath $shareName
		If($sharePath -eq $false -or $sharePath -eq $null)
			{$shareExists = $false}
		Else
			{$shareExists = $true}
		Return $shareExists
}

Function Check-SharePermissions($ShareName, $arrCSVShareInfo) {
	$strWMI = "\\" + $fileServer + "\root\cimv2:win32_LogicalShareSecuritySetting.Name='" + $ShareName + "'"
	$objWMI_ShareSec = [wmi]$strWMI
  $objWMI_SD = $objWMI_ShareSec.invokeMethod('GetSecurityDescriptor',$null,$null)
  $numACEsMatched = 0
  $ExistingShareDACL = $objWMI_SD.Descriptor.DACL
  $DACLlength = $ExistingShareDACL.length
  #Only compare CSV to current DACL if they are the same length
  If ($arrCSVShareInfo.length -eq $DACLlength) {
  	$ExistingShareDACL | % {
  		For($i = 0;$i -lt $arrCSVShareInfo.length;$i++) {
  			#If aone of the ACE's is the same as 1 of the CSV entries in all categories, add 1 to the ACEsMatched
  			If(`
  			($_.AccessMask -eq $arrCSVShareInfo[$i].AccessMask)`
  			-and ($_.Trustee.Name -eq $arrCSVShareInfo[$i].User)`
  			-and ($_.Trustee.Domain -eq $arrCSVShareInfo[$i].Domain)`
  			-and ($_.AceFlags -eq $arrCSVShareInfo[$i].AceFlags)`
  			-and ($_.AceType -eq $arrCSVShareInfo[$i].AceType)) {

  				$numACEsMatched++
		  	}
			}
		}
	}

	If ($numACEsMatched -eq $DACLlength) {
		Return $true
	}
	Else {
		Return $false
	}
}

Function Fix-SharePermissions ($ShareName, $arrCSVShareInfo) {
	#Create security objects
	$objWMI_NewSD = ([WMIClass] ("\\" + $FileServer + "\root\CIMv2:Win32_SecurityDescriptor")).CreateInstance()
	$objWMI_NewSD.DACL = @()
	$objWMI_NewEmptyACE = ([WMIClass] ("\\" + $FileServer + "\root\CIMv2:Win32_ACE")).CreateInstance()
	$objWMI_NewTrustee = ([WMIClass] ("\\" + $FileServer + "\root\CIMv2:Win32_Trustee")).CreateInstance()

	$arrCSVShareInfo | % {
		#fill with CSV permissions
		$objWMI_NewCompleteACE = (Add-PermissionsToACE $_ $objWMI_NewTrustee $objWMI_NewEmptyACE )
		$objWMI_NewSD.DACL += $objWMI_NewCompleteACE.PsObject.BaseObject
	}

	$strWMI = "\\" + $fileServer + "\root\cimv2:win32_Share.Name='" + $ShareName + "'"
	$objWMI_ThisShare = [wmi]$strWMI
	$inParams = $objWMI_ThisShare.GetMethodParameters("SetShareInfo")
	$inParams["Access"] = $objWMI_NewSD.PsObject.BaseObject
	#attach new SD to share
	$objWMI_ThisShare.InvokeMethod("SetShareInfo",$inParams,$null)

	#Check if permissions are now right
	$blnDoPermsMatch = Check-SharePermissions $ShareName $arrCSVShareInfo

	Return $blnDoPermsMatch
}

Function VerifyAndFix-ShareInfo {
	[CmdletBinding()]
	param($name, $path, $arrCSVShareInfo)
	$blnerrorsdetected = $null
	$blnErrorsDetected = $false
	$strErrorMsg = $null

	$blnCleanInput = Check-CleanInput $arrCSVShareInfo -EV err -EA SilentlyContinue
	If ($blnCleanInput -eq $false) {
		$blnErrorsDetected = $true
		$strErrorMsg = $err
		trap {Continue;}
		throw $strErrorMsg
		return $blnErrorsDetected
		break
	}

	$blnPathExist = Check-DoesSharePathExist $path
	If ($blnPathExist -eq $false) {
		$blnPathExist = Create-Folder $Path
		If ($blnPathExist -eq $false) {
		 	$blnErrorsDetected = $true
		 	$strErrorMsg = "The file path does not exist and could not be created"
		 	trap {Continue;}
		 	throw $strErrorMsg
			return $blnErrorsDetected
			break
		}
	}

	# If Share Path exists, share exists
	$blnShareExist = Check-DoesShareExist $name
	If($blnShareExist -ne $false -and $blnShareExist -ne $null) {
		$blnDoPermsMatch = Check-SharePermissions $name $arrCSVShareInfo
		If ($blnDoPermsMatch -eq $false) {
			$blnDoPermsMatch = Fix-SharePermissions $name $arrCSVShareInfo
			If ($blnDoPermsMatch -eq $false) {
				$blnErrorsDetected = $true
				$strErrorMsg = "The share exists, but the permissions do not match the CSV"
		 		trap {Continue;}
		 		throw $strErrorMsg
				return $blnErrorsDetected
				break
			}
		}
	}

	return $blnErrorsDetected
}

Function Add-PermissionsToACE($arrCSVShareInfo, [System.Management.ManagementObject]$objWMI_Trustee, [System.Management.ManagementObject]$objWMI_ACE) {

	# Add properties to trustee
  $objWMI_Trustee.Domain = $arrCSVShareInfo.Domain
  $objWMI_Trustee.Name = $arrCSVShareInfo.Name

  # Get SID, convert to binary. Reference: http://mow001.blogspot.com/2005/10/getting-and-using-securityprincipal.html
  $strSID = ((new-object System.Security.Principal.NTAccount ($arrCSVShareInfo.domain, $arrCSVShareInfo.user)).translate([System.Security.Principal.SecurityIdentifier]))
  [byte[]]$binSID = ,0 * $strSID.BinaryLength
  $strSID.GetBinaryForm($binSID,0)
  $objWMI_Trustee.SID = $binSID

  # Add properties to ACE
  $objWMI_ACE.AccessMask = $arrCSVShareInfo.AccessMask
  $objWMI_ACE.AceType = $arrCSVShareInfo.AceType
  $objWMI_ACE.AceFlags = $arrCSVShareInfo.AceFlags
  $objWMI_ACE.Trustee = $objWMI_Trustee.PsObject.BaseObject

  Return $objWMI_ACE
}

Function Create-ShareFromCSVInfo($Name, $Path, $Description , [System.Management.ManagementObject]$SecurityDescriptor) {
	$results = $null

	$objWMI_Share= [WMIClass]("\\" + $FileServer + "\root\CIMv2:Win32_Share" )
  $InParams = $objWMI_Share.GetMethodParameters('Create')

  # Fill parameters
  $InParams["Access"] = $SecurityDescriptor.PsObject.BaseObject
  $InParams["Description"] = $Description
  $InParams["Name"] = $Name
  #$InParams["Password"] = [string]
  $InParams["Path"] = $Path
  $InParams["Type"] = "0"

  $R = $objWMI_Share.InvokeMethod('Create', $InParams, $Null)
  If ($R.ReturnValue -ne "0") {
  	#0 means success, anything else failed
  	$results = $false
  }
  Else {
  	$results = $true
  }
  Return $results
}

########### MAIN ###########

#This hash table will hold names of shares with errors.

$ProblemShares = @{}

#	The sharelist is sorted, so we are basically looking at the share name and seeing if it is the same as the last one.
# If it is, we add it to $arrCSVShareInfo. If it isn't, we pass $arrCSVShareInfo through the necessary functions to
# verify and create it, and then we make a new $arrCSVShareInfo with the new share name (the next share) when finished.

$counter = $null
$arrCSVShareInfo = @()
For($counter = 0;$counter -le $sharelist.length;$counter++) {
	If (($sharelist[$counter].name -eq $sharelist[$counter-1].name) -or ($counter -eq 0)) {
		$arrCSVShareInfo += $sharelist[$counter]
	}

	ElseIf ((($sharelist[$counter].name -ne $sharelist[$counter-1].name) -and ($counter -ne 0)) -or ($counter -eq $sharelist.length)) {
		$name = $sharelist[$counter-1].name
		$path = $sharelist[$counter-1].path
		$description = $sharelist[$counter-1].description
		write-host "Processing :" $arrCSVShareInfo[0].name

		# Pass each share through verification process, failed shares have error message stored in $err
		$blnErrorsDetected = VerifyAndFix-ShareInfo $Name $Path $arrCSVShareInfo -EV err -EA SilentlyContinue

		#Check if share exists, only want to create it if it does not exist
		$blnShareExists = $null
		$blnShareExists = Check-DoesShareExist $name

		#Only want to create it if there were no errors
		If ($blnErrorsDetected -eq $true) {
			trap {continue;}
			$ProblemShares.Add($name, $err)
		}
		ElseIf ($blnShareExists -eq $false) {

			#Create Security Objects
			$objWMI_SD = ([WMIClass] ("\\" + $FileServer + "\root\CIMv2:Win32_SecurityDescriptor")).CreateInstance()
			$objWMI_SD.DACL = @()
			$objWMI_EmptyACE = ([WMIClass] ("\\" + $FileServer + "\root\CIMv2:Win32_ACE")).CreateInstance()
			$objWMI_Trustee = ([WMIClass] ("\\" + $FileServer + "\root\CIMv2:Win32_Trustee")).CreateInstance()

			$arrCSVShareInfo | % {
			$objWMI_CompleteACE = (Add-PermissionsToACE $_ $objWMI_Trustee $objWMI_EmptyACE )
			$objWMI_SD.DACL += $objWMI_CompleteACE.PsObject.BaseObject
			}

			$blnShareCreated = Create-ShareFromCSVinfo $Name $Path $Description $objWMI_SD
			If ($blnShareCreated -eq $false) {
				$ProblemShares.Add($name, "Share Creation Failed")
			}
		}
		#Make a new array for the next share
		$arrCSVShareInfo = @()
		$arrCSVShareInfo += $sharelist[$counter]
	}
}
Write-Host "Import Complete" -ForegroundColor green
If ($problemShares.count -ge 1) {
	Write-Host "These Shares Failed to Import:"
	#Write the failed shares to the screen, complete with detailed error messages
	$ProblemShares
}



Viewing all articles
Browse latest Browse all 2

Latest Images

Trending Articles





Latest Images