Public/Mount-FileShare.ps1
|
function Mount-FileShare { <# .SYNOPSIS Mounts Azure File Shares defined in a drive-letter map using identity-based authentication. .DESCRIPTION Iterates over the ShareDriveMap hashtable and mounts each Azure File Share using 'net use' with the provided credential. If a mount fails, the error is logged as a warning and the function continues with remaining shares. Keys can be a plain share name (e.g. 'files') or a share/subfolder path (e.g. 'staff/%USERNAME%'). When a key contains '/', the part before the first '/' is the share name and everything after is appended as a subfolder. The token %USERNAME% is expanded to the username portion of the Credential UPN. When the -Reset switch is used, the function will: 1. Disconnect all currently mapped drives that target the storage account. 2. Purge any cached Windows credentials for the storage account. 3. Open the Microsoft password change page in the default browser. 4. Wait for the user to confirm the password has been changed. 5. Prompt for updated credentials. 6. Remap all drives according to the ShareDriveMap. .PARAMETER StorageAccountName Name of the Azure Storage Account hosting the file shares. .PARAMETER Credential PSCredential containing the user's UPN (user@domain.com) and password. When -Reset is specified this parameter is ignored; the function will prompt for fresh credentials after the password change. .PARAMETER ShareDriveMap Hashtable mapping share names (or share/subfolder paths) to drive letters. Required for normal operation. When -Reset is specified and this parameter is omitted, the function auto-discovers the current drive map by inspecting existing net-use mappings that target the storage account. .PARAMETER Reset Switch that triggers a full credential-reset flow: disconnect shares, purge cached credentials, guide the user through a password change, then remap drives with the new credentials. If -ShareDriveMap is not supplied the existing mappings are captured first and reused after the password change. .EXAMPLE $cred = Get-Credential user@contoso.com Mount-FileShare -Credential $cred -StorageAccountName 'mystorageaccount' -ShareDriveMap @{ profiles = 'P'; data = 'D' } .EXAMPLE Mount-FileShare -Credential $cred -StorageAccountName 'mystorageaccount' -ShareDriveMap @{ profiles = 'P'; 'staff/%USERNAME%' = 'H' } .EXAMPLE Mount-FileShare -StorageAccountName 'mystorageaccount' -ShareDriveMap @{ profiles = 'P'; data = 'D' } -Reset .EXAMPLE # Reset using the currently mounted drives as the map Mount-FileShare -StorageAccountName 'mystorageaccount' -Reset #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$StorageAccountName, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential, [Parameter(Mandatory = $false)] [hashtable]$ShareDriveMap, [switch]$Reset ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # ── Constants ──────────────────────────────────────────────────────────── $FileShareRoot = "\\$StorageAccountName.file.core.windows.net" $PasswordChangeUrl = 'https://account.activedirectory.windowsazure.com/ChangePassword.aspx' # ── Reset flow ─────────────────────────────────────────────────────────── if ($Reset) { Write-Host '' Write-Host '── Reset Mode ─────────────────────────────────────────────────' -ForegroundColor Yellow # 0. If no ShareDriveMap was provided, build one from current net-use mappings if (-not $ShareDriveMap) { Write-Host ' No ShareDriveMap provided – discovering existing mappings ...' -ForegroundColor Yellow $ShareDriveMap = @{} $discoveryOutput = Invoke-NetUse -Arguments @() foreach ($line in $discoveryOutput) { # Match lines like: OK Z: \\stor.file.core.windows.net\share\sub if ($line -match '^\s*(?:OK|Disconnected|Unavailable)\s+([A-Z]):?\s+(\\\\[^\s]+)' -and $Matches[2] -like "*$StorageAccountName.file.core.windows.net*") { $discoveredDrive = $Matches[1] # e.g. 'Z' $discoveredUnc = $Matches[2] # e.g. \\stor.file.core.windows.net\share\sub # Strip the \\<fqdn>\ prefix to get share[\sub\folder] $relativePath = $discoveredUnc -replace [regex]::Escape("$FileShareRoot\"), '' # Convert backslashes to forward slashes for the map-key format $mapKeyDiscovered = $relativePath -replace '\\', '/' $ShareDriveMap[$mapKeyDiscovered] = $discoveredDrive Write-Host " Found ${discoveredDrive}: -> $mapKeyDiscovered" -ForegroundColor Yellow } } if ($ShareDriveMap.Count -eq 0) { Write-Error "No existing file-share mappings found for $FileShareRoot. Please provide -ShareDriveMap explicitly." return } Write-Host " Discovered $($ShareDriveMap.Count) mapping(s)." -ForegroundColor Yellow Write-Host '' } # 1. Disconnect all drives that point at this storage account Write-Host ' Disconnecting existing file-share mappings ...' -ForegroundColor Yellow foreach ($mapKey in $ShareDriveMap.Keys) { $driveLetter = "$($ShareDriveMap[$mapKey].TrimEnd(':')):" if (Test-Path -Path "${driveLetter}\") { Write-Host " Removing $driveLetter ..." -ForegroundColor Yellow $null = Invoke-NetUse -Arguments @($driveLetter, '/delete', '/yes') } } # Also remove any other mappings targeting this storage account $existingMappings = Invoke-NetUse -Arguments @() foreach ($line in $existingMappings) { if ($line -match '^\s*(OK|Disconnected|Unavailable)\s+([A-Z]:)\s+(\\\\[^\s]+)' -and $Matches[3] -like "*$StorageAccountName.file.core.windows.net*") { $staleDrive = $Matches[2] Write-Host " Removing additional mapping $staleDrive ..." -ForegroundColor Yellow $null = Invoke-NetUse -Arguments @($staleDrive, '/delete', '/yes') } } # 2. Purge cached Windows credentials for the storage account Write-Host ' Purging cached credentials ...' -ForegroundColor Yellow $credTargets = @( "$StorageAccountName.file.core.windows.net" "*$StorageAccountName.file.core.windows.net*" "\\$StorageAccountName.file.core.windows.net" ) foreach ($target in $credTargets) { $null = Invoke-CmdKey -Arguments @("/delete:$target") } # Also enumerate and remove any cmdkey entries that match the storage FQDN $cmdkeyList = Invoke-CmdKey -Arguments @('/list') | Out-String $fqdn = "$StorageAccountName.file.core.windows.net" foreach ($line in ($cmdkeyList -split "`n")) { if ($line -match 'Target:\s*(.+)' -and $Matches[1].Trim() -like "*$fqdn*") { $storedTarget = $Matches[1].Trim() Write-Host " Removing stored credential: $storedTarget" -ForegroundColor Yellow $null = Invoke-CmdKey -Arguments @("/delete:$storedTarget") } } # 3. Open the Microsoft password change page Write-Host '' Write-Host ' Opening Microsoft password change page in your browser ...' -ForegroundColor Cyan Write-Host " URL: $PasswordChangeUrl" -ForegroundColor DarkGray Start-Process $PasswordChangeUrl # 4. Wait for user confirmation Write-Host '' Write-Host ' Please change your password in the browser window that just opened.' -ForegroundColor Yellow Write-Host ' Once you have successfully changed your password, return here.' -ForegroundColor Yellow Write-Host '' Read-Host ' Press ENTER when you have finished changing your password' # 5. Prompt for updated credentials Write-Host '' Write-Host ' Please enter your NEW credentials (UPN and new password).' -ForegroundColor Cyan $Credential = Get-Credential -Message 'Enter your UPN (user@domain.com) and NEW password' if (-not $Credential) { Write-Error 'No credentials provided. Aborting reset.' return } Write-Host '' Write-Host ' Credential reset complete. Proceeding to remap drives ...' -ForegroundColor Green Write-Host '' } elseif (-not $Credential) { Write-Error 'The -Credential parameter is required when -Reset is not specified.' return } if (-not $ShareDriveMap -or $ShareDriveMap.Count -eq 0) { Write-Error 'The -ShareDriveMap parameter is required when -Reset is not specified (or no mappings were discovered).' return } $credUsername = ($Credential.UserName -split '@', 2)[0] $upn = $Credential.UserName $pwd = $Credential.GetNetworkCredential().Password # ── Build mount operations ─────────────────────────────────────────────── $mountOps = foreach ($mapKey in $ShareDriveMap.Keys) { $parts = $mapKey -split '/', 2 $rootShare = $parts[0] $subFolder = if ($parts.Count -gt 1) { $parts[1] -replace '%USERNAME%', $credUsername } else { $null } [PSCustomObject]@{ MapKey = $mapKey RootShare = $rootShare SubFolder = $subFolder DriveLetter = $ShareDriveMap[$mapKey].TrimEnd(':') } } # ── Display planned mounts ─────────────────────────────────────────────── Write-Host '' Write-Host '── Drive Map ──────────────────────────────────────────────────' -ForegroundColor Cyan foreach ($op in $mountOps) { $label = if ($op.SubFolder) { "$($op.RootShare)/$($op.SubFolder)" } else { $op.RootShare } Write-Host " $($op.DriveLetter): -> $label" -ForegroundColor Cyan } Write-Host '' # ── Mount each share ───────────────────────────────────────────────────── $successCount = 0 $failCount = 0 foreach ($op in $mountOps) { $uncPath = if ($op.SubFolder) { "$FileShareRoot\$($op.RootShare)\$($op.SubFolder)" } else { "$FileShareRoot\$($op.RootShare)" } $drive = "$($op.DriveLetter):" if (-not $PSCmdlet.ShouldProcess("$uncPath -> $drive", 'Mount file share')) { continue } # Remove stale mapping if (Test-Path -Path "${drive}\") { Write-Verbose "Removing existing mapping on $drive ..." $null = Invoke-NetUse -Arguments @($drive, '/delete', '/yes') } Write-Host " Mounting $uncPath -> $drive" -ForegroundColor Green $result = Invoke-NetUse -Arguments @($drive, $uncPath, "/user:$upn", $pwd, '/persistent:yes') if ($LASTEXITCODE -ne 0) { Write-Warning " FAILED to mount '$($op.MapKey)' ($drive): $result" $failCount++ } else { Write-Host " OK" -ForegroundColor Green $successCount++ } } # ── Summary ────────────────────────────────────────────────────────────── Write-Host '' Write-Host '── Summary ────────────────────────────────────────────────────' -ForegroundColor Cyan Write-Host " Mounted : $successCount" -ForegroundColor Green if ($failCount -gt 0) { Write-Host " Failed : $failCount (see warnings above)" -ForegroundColor Yellow } Write-Host '' } |