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 ''
}