Carbon.SmbShare.psm1
|
using namespace System.Security.AccessControl using namespace System.Security.Principal # Copyright WebMD Health Services # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License #Requires -Version 5.1 Set-StrictMode -Version 'Latest' # Functions should use $script:moduleDirPath as the relative root from which to find things. A published module has its # function appended to this file, while a module in development has its functions in the Functions directory. $script:moduleDirPath = $PSScriptRoot $modulesDirPath = Join-Path -Path $script:moduleDirPath -ChildPath 'M' -Resolve # Import the .psm1 because PowerShell is limited to 10 nested scopes and importing the directory/.psd1 adds a scope. Import-Module -Name (Join-Path -Path $modulesDirPath -ChildPath 'Carbon.Accounts\Carbon.Accounts.psm1' -Resolve) ` -Function @('Resolve-CPrincipalName') ` -Verbose:$false Import-Module -Name (Join-Path -Path $modulesDirPath -ChildPath 'Carbon.FileSystem\Carbon.FileSystem.psm1' -Resolve) ` -Function @('Install-CDirectory') ` -Verbose:$false # Store each of your module's functions in its own file in the Functions directory. On the build server, your module's # functions will be appended to this file, so only dot-source files that exist on the file system. This allows # developers to work on a module without having to build it first. Grab all the functions that are in their own files. $functionsPath = Join-Path -Path $script:moduleDirPath -ChildPath 'Functions\*.ps1' if( (Test-Path -Path $functionsPath) ) { foreach( $functionPath in (Get-Item $functionsPath) ) { . $functionPath.FullName } } function Install-CSmbShare { <# .SYNOPSIS Installs a file/SMB share. .DESCRIPTION The `Install-CSmbShare` function installs an SMB file share. If the share doesn't exist, it is created. If a share exists, its description and permissions are updated in place. If its path is changing, the share is deleted and re-created. If the path to the share doesn't exist, it is created. Use the `FullAccess`, `ChangeAccess`, and `ReadAccess` parameters to grant full, change, and read permissions on the share. Each parameter takes a list of user/group names. Only the users and groups in the `FullAccess`, `ChangeAccess`, and `ReadAccess` parameters will be given access. Any accounts that have access that aren't in one of the specified lists will have their permissions removed, including the default `Everyone` read access rule. Make sure to pass `Everyone` to the `ReadAccess` parameter if you want everyone to have access! Permissions don't apply to the file system, only to the share. Use the Carbon.FileSystem module's `Grant-CNtfsPermission` function to grant file system permissions or use the SmbShare module's `Set-SmbPathAcl`. .LINK Test-CSmbShare .LINK Uninstall-CSmbShare .EXAMPLE Install-Share -Name TopSecretDocuments -Path C:\TopSecret -Description 'Share for our top secret documents.' -ReadAccess "Everyone" -FullAccess "Analysts" Shares the C:\TopSecret directory as `TopSecretDocuments` and grants `Everyone` read access and `Analysts` full control. #> [CmdletBinding(SupportsShouldProcess)] param( # The share's name. [Parameter(Mandatory)] [String] $Name, # The path to the share. Is created if it doesn't exist. [Parameter(Mandatory)] [String] $Path, # A description of the share. [String] $Description = '', # The user/group names who should be granted full access to the share. Only principals in this list will be # given full access. All other principals will be removed. [String[]] $FullAccess = @(), # The user/group names who should be granted change access to the share. Only principals in this list will be # given change access. All other principals will be removed. [String[]] $ChangeAccess = @(), # The user/group names who should be granted read access to the share. Only principals in this list will be # given read access. All other principals will be removed. # # The `Everyone` group will *not* be granted permission by default. They must be in this list to be given # access. [String[]] $ReadAccess = @() ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState if ([wildcardpattern]::ContainsWildcardCharacters($Name)) { $msg = "Failed to create SMB file share ""${Name}"" because the share name contains wildcard characters." Write-Error -Message $msg -ErrorAction $ErrorActionPreference return } $shareInfoWrittenTo = @{} function Write-Message { param( [String] $Message, [switch] $Verbose ) $writeCmd = 'Write-Information' if ($Verbose) { $writeCmd = 'Write-Verbose' } if (-not $shareInfoWrittenTo.ContainsKey($writeCmd)) { & $writeCmd "SMB File Share ${Name}" $shareInfoWrittenTo[$writeCmd] = $true } & $writeCmd " ${Message}" } function Format-AccessRight { param( [Parameter(Mandatory, ValueFromPipeline)] [ValidateSet('Full', 'Change', 'Read')] [String] $InputObject ) process { return '{0,-6}' -f $InputObject } } # Make sure path ends with \. You can only share directories, after all. $Path = Join-Path -Path $Path -ChildPath '\' if (-not (Test-Path -Path $Path)) { Install-CDirectory -Path $Path } $share = Test-CSmbShare -Name $Name -PassThru # Only way to update a share's path is to delete and re-create the share. if ($share -and (Join-Path -Path $share.Path -ChildPath '\') -ne $Path) { Uninstall-CSmbShare -Name $Name $share = $null } $whatIfTarget = "SMB file share '${Name}'" # If no permissions were specified, grant read access to everyone. if (-not $share) { if (-not $PSCmdlet.ShouldProcess($whatIfTarget, "create")) { return } Write-Message "created at ""${Path}""." foreach ($principal in $FullAccess) { Write-Message "+ $('Full' | Format-AccessRight) ${principal}" } foreach ($principal in $ChangeAccess) { Write-Message "+ $('Change' | Format-AccessRight) ${principal}" } foreach ($principal in $ReadAccess) { Write-Message "+ $('Read' | Format-AccessRight) ${principal}" } $newSmbShareArgs = @{} if ($FullAccess) { $newSmbShareArgs['FullAccess'] = $FullAccess } if ($ChangeAccess) { $newSmbShareArgs['ChangeAccess'] = $ChangeAccess } if ($ReadAccess) { $newSmbShareArgs['ReadAccess'] = $ReadAccess } $share = New-SmbShare -Name $Name -Path $Path -Description $Description @newSmbShareArgs if (-not $share) { return } } $Name = $share.Name if ($share.Description -ne $Description) { Write-Message "Description - $($share.Description)" Write-Message " + ${Description}" if ($PSCmdlet.ShouldProcess($whatIfTarget, "update description")) { Set-SmbShare -Name $Name -Description $Description -Force | Out-Null } } # Resolve all the principals. $FullAccess = $FullAccess | ForEach-Object { Resolve-CPrincipalName -Name $_ } $ChangeAccess = $ChangeAccess | ForEach-Object { Resolve-CPrincipalName -Name $_ } $ReadAccess = $ReadAccess | ForEach-Object { Resolve-CPrincipalName -Name $_ } $aces = Get-SmbShareAccess -Name $Name | Where-Object 'AccessControlType' -eq 'Allow' # Remove extra permissions. foreach ($ace in $aces) { $acePrincipalName = Resolve-CPrincipalName -Name $ace.AccountName $ace | Add-Member -MemberType NoteProperty -Name 'PrincipalName' -Value $acePrincipalName -Force if (-not $acePrincipalName) { $acePrincipalName = $ace.AccountName } if (-not $acePrincipalName -or ` ($ace.AccessRight -eq 'Read' -and $ReadAccess -notcontains $acePrincipalName) -or ` ($ace.AccessRight -eq 'Change' -and $ChangeAccess -notcontains $acePrincipalName) -or ` ($ace.AccessRight -eq 'Full' -and $FullAccess -notcontains $acePrincipalName)) { if (-not $acePrincipalName) { $acePrincipalName = $ace.AccountName } if ($PSCmdlet.ShouldProcess($whatIfTarget, "revoke '$($ace.AccountName)' $($ace.AccessRight) access")) { Write-Message "- $($ace.AccessRight | Format-AccessRight) ${acePrincipalName}" Revoke-SmbShareAccess -Name $Name -AccountName $ace.AccountName -Force | Out-Null } } } function Grant-Access { param( [String[]] $PrincipalName, [ValidateSet('Full', 'Change', 'Read')] [String] $AccessRight ) foreach ($_principalName in $PrincipalName) { if ($aces | Where-Object 'PrincipalName' -EQ $_principalName | Where-Object 'AccessRight' -EQ $AccessRight) { Write-Message "${_principalName} ${AccessRight}" -Verbose continue } if ($PSCmdlet.ShouldProcess($whatIfTarget, "grant '$($_principalName)' ${AccessRight} access")) { Write-Message "+ $($AccessRight | Format-AccessRight) ${_principalName}" Grant-SmbShareAccess -Name $Name -AccountName $_principalName -AccessRight $AccessRight -Force | Out-Null } } } Grant-Access -PrincipalName $FullAccess -AccessRight 'Full' Grant-Access -PrincipalName $ChangeAccess -AccessRight 'Change' Grant-Access -PrincipalName $ReadAccess -AccessRight 'Read' } function Test-CSmbShare { <# .SYNOPSIS Tests if an SMB file share exists on the local computer. .DESCRIPTION The `Test-CSmbShare` function uses `Get-SmbShare` from PowerShell's built-in SmbShare module to check if a file share exists on the local computer. If the share exists, `Test-CSmbShare` returns `$true`. Otherwise, it returns `$false`. Use the `-PassThru` switch to return the share object if it exists. .LINK Install-CSmbShare .LINK Uninstall-CSmbShare .EXAMPLE Test-CSmbShare -Name 'CarbonShare' Demonstrates how to test of a file share exists. #> [CmdletBinding()] param( # The name of a specific share to check. [Parameter(Mandatory)] [String] $Name, [switch] $PassThru ) Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $share = Get-SmbShare -Name $Name -ErrorAction Ignore $shareExists = $null -ne $share if ($PassThru) { if ($shareExists) { return $share } return $null } return $shareExists } function Uninstall-CSmbShare { <# .SYNOPSIS Uninstalls/removes a file share from the local computer. .DESCRIPTION The `Uninstall-CSmbShare` function uses `Remove-SmbShare` to uninstall/remove a file share from the local computer, if it exists. Pass the name of the share to delete to the `Name` parameter (or pipe the name or share objects to the function). If the file share exists, it is deleted. If it doesn't exist, nothing hapens. .LINK Install-CSmbShare .LINK Test-CSmbShare .EXAMPLE Uninstall-CSmbShare -Name 'CarbonShare' Demonstrates how to uninstall/remove a share from the local computer. If the share does not exist, `Uninstall-CSmbShare` silently does nothing (i.e. it doesn't write an error). #> [CmdletBinding(SupportsShouldProcess)] param( # The name of a specific share to uninstall/delete. Wildcards accepted. [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] [String] $Name ) process { Set-StrictMode -Version 'Latest' Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState $shares = Test-CSmbShare -Name $Name -PassThru if (-not $shares) { return } foreach ($share in $shares) { if ($PSCmdlet.ShouldProcess("SMB file share '$($share.Name)'", "remove")) { Write-Information "Removing SMB file share ""$($share.Name)"" at ""$($share.Path)""." $share | Remove-SmbShare -Force -ErrorAction $ErrorActionPreference -WhatIf:$WhatIfPreference } } } } function Use-CallerPreference { <# .SYNOPSIS Sets the PowerShell preference variables in a module's function based on the callers preferences. .DESCRIPTION Script module functions do not automatically inherit their caller's variables, including preferences set by common parameters. This means if you call a script with switches like `-Verbose` or `-WhatIf`, those that parameter don't get passed into any function that belongs to a module. When used in a module function, `Use-CallerPreference` will grab the value of these common parameters used by the function's caller: * ErrorAction * Debug * Confirm * InformationAction * Verbose * WarningAction * WhatIf This function should be used in a module's function to grab the caller's preference variables so the caller doesn't have to explicitly pass common parameters to the module function. This function is adapted from the [`Get-CallerPreference` function written by David Wyatt](https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d). There is currently a [bug in PowerShell](https://connect.microsoft.com/PowerShell/Feedback/Details/763621) that causes an error when `ErrorAction` is implicitly set to `Ignore`. If you use this function, you'll need to add explicit `-ErrorAction $ErrorActionPreference` to every `Write-Error` call. Please vote up this issue so it can get fixed. .LINK about_Preference_Variables .LINK about_CommonParameters .LINK https://gallery.technet.microsoft.com/scriptcenter/Inherit-Preference-82343b9d .LINK http://powershell.org/wp/2014/01/13/getting-your-script-module-functions-to-inherit-preference-variables-from-the-caller/ .EXAMPLE Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Demonstrates how to set the caller's common parameter preference variables in a module function. #> [CmdletBinding()] param ( [Parameter(Mandatory)] #[Management.Automation.PSScriptCmdlet] # The module function's `$PSCmdlet` object. Requires the function be decorated with the `[CmdletBinding()]` # attribute. $Cmdlet, [Parameter(Mandatory)] # The module function's `$ExecutionContext.SessionState` object. Requires the function be decorated with the # `[CmdletBinding()]` attribute. # # Used to set variables in its callers' scope, even if that caller is in a different script module. [Management.Automation.SessionState]$SessionState ) Set-StrictMode -Version 'Latest' # List of preference variables taken from the about_Preference_Variables and their common parameter name (taken # from about_CommonParameters). $commonPreferences = @{ 'ErrorActionPreference' = 'ErrorAction'; 'DebugPreference' = 'Debug'; 'ConfirmPreference' = 'Confirm'; 'InformationPreference' = 'InformationAction'; 'VerbosePreference' = 'Verbose'; 'WarningPreference' = 'WarningAction'; 'WhatIfPreference' = 'WhatIf'; } foreach( $prefName in $commonPreferences.Keys ) { $parameterName = $commonPreferences[$prefName] # Don't do anything if the parameter was passed in. if( $Cmdlet.MyInvocation.BoundParameters.ContainsKey($parameterName) ) { continue } $variable = $Cmdlet.SessionState.PSVariable.Get($prefName) # Don't do anything if caller didn't use a common parameter. if( -not $variable ) { continue } if( $SessionState -eq $ExecutionContext.SessionState ) { Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false } else { $SessionState.PSVariable.Set($variable.Name, $variable.Value) } } } |