Category Archives: Powershell

Ultimate folder redirection for Onedrive, Teams and Sharepoint

In the post-Onedrivemapper era where we have Files On Demand, there is still room for improvement in client side configuration of Onedrive for Business. Onedrive Known Folders isn’t up to par yet, doesn’t support any customization and there are situations where I want to be able to redirect local folders to other places than Onedrive like Teams or Sharepoint.

Therefore I present to you “Invoke-O4BAutoMount“; the ultimate Onedrive/Sharepoint/Teams sync and redirect solution in modern workplace scenario’s, no WebDav, just the NSG Onedrive Client and native Intune Management Extension:

  • Single PS script
  • User does NOT have to be an Admin
  • Per special folder configuration of redirection settings
  • Extra (symlink) redirection for ANY folder (specific appdata subfolders anyone?)
  • Automatically runs at logon and/or at specified intervals
  • Can be unassigned (removing a user from the target group in Intune stops the script from running)
  • Can redirect to subfolders, even nested
  • Handles automatic creation of any folders
  • Supports variables in paths
  • Registers Environment Variables for the new locations if you configure it to
  • Can hide target folders (appdata likes to hide)
  • Multi-user support (Windows Virtual Desktop / Server 2019)

How to use in 8 easy steps:

1. Configure the tenant ID and how often you want to script to run:

2. Then get the ODOpen URL of any libraries you wish to sync:

Use Chrome or Edge, do not use Internet Explorer!

3. paste the URL into the script in the syncUrl section and configure the site and list titles:

User experience: The site and list title will show in explorer like “siteTitle – listTitle”

4. if you only need Onedrive and don’t want to sync any sharepoint/teams libraries, configure like this:

5. Pick which folders you want to redirect, and where you want to redirect them to:

This example redirects the My Documents and Desktop folders to the user’s Onedrive, and the Pictures folder to a subfolder in a teamsite called “AutoMapTestTeam”, the subfolder is automatically created using the user’s email address.

6. Configure any special folders that normally cannot be redirected

This example will redirect the local Skype folderin APPDATA to a user’s Onedrive, thus roaming Skype contacts and profile information to Onedrive

7. Upload to Intune as a PowerShell script:

Make sure you set it to 64 bit AND to run as system instead of the user’s logged on credentials

8. Deploy to your users (you can also deploy to machines, but then you need to set autoRerunMinutes to > 0 and it might not run at logon)

Further recommendations

This script assumes you’ve already Configured Onedrive to automatically log the user in and I strongly recommend to run Onedrive in Per Machine Mode for faster config.

I haven’t extensively tested running this script when multiple users are active on a device at the same time (ie: citrix, wvd, remoteapp); I’d love to hear about your results!

Thanks and inspiration

Michael Mardahl for giving me the ODOPEN and IME hack examples

Aaron Parker for writing on Folder Redirection with PowerShell

Justin Murray for showing me how to impersonate another user

Per Larsen for writing on silent config / admx stuff regarding Onedrive

Olivier Kieselbach for his three deep dives on the Intune Management Extension

Notice

As always, this is a free script and no guarantees are given, use at your own risk.

Troubleshooting

Log locations:

c:\\users\\appdata\\local\\temp\\Invoke-O4BAutoMount.log for the user portion.

C:\\Windows\\Temp\\Invoke-O4BAutoMount.log for the SYSTEM portion

If the script isn’t running at all (no log file), check the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\IntuneManagementExtension\Policies subkeys for error info from the script. Make sure the config section of the script is correct or it won’t run.

Also check Olivier’s deep dives for general troubleshooting of Powershell scripts with Intune

Import a PBIX to PowerBI using PowerShell

Reading up on the PowerBI API to import PBIX files to the PowerBI service and an example on the actual request I decided to write a PowerShell function to import a PowerBI PBIX file to the PowerBI service.

The PowerBI Import API is quite specific and kept giving me 400’s like:

  • “Bad Request” with no details
  • UnknownError pbi.error exceptionCulprit 1 (loved this one)
  • MultiPartMimeStreamFormatException
  • RequestedFileIsEncryptedOrCorrupted

Eventually I figured out how to import my reports directly into PowerBI, so to help you automate importing your reports into workspaces or directly to customers using PowerShell, I’ll share my PowerShell function with you:

GitLab: Import-PBIXToPowerBI.ps1

Devices that lack a bitlocker recovery key in AzureAD

With Intune’s new Bitlocker Encryption Report administrators have an effective way of seeing which of their devices have been encrypted.

But if we want to know if we can actually recover the bitlocker key of a device, we need to know if it was ever uploaded to AzureAD.

Network or local device issues can sometimes prevent the recovery key from reaching AzureAD, resulting in lost data if the device’s disk needs to be recovered for any reason. To hunt down devices that have not escrowed their recovery key to AzureAD, you can use my report function (in PowerShell as always):

GitLab source download link

Programmatically triggering a group licenses refresh for AzureAD

Azure AD allows us to assign licenses to groups, a nifty feature that has made a host of automation scripts dealing with bulk license assignment obsolete.

A problem I’ve encountered is that when you assign users to a group, license assignments are not processed right away, especially if you didn’t have enough licenses when you assigned the user to the group (and added licenses to the tenant later).

Azure AD has a button to trigger an update manually:

But of course, this can also be automated with PowerShell!

function Invoke-AzHAPIReprocessGroupLicenses{
    <#
        .SYNOPSIS
        reprocesses group license assignment

        .NOTES
        Author: Jos Lieben

        .PARAMETER AzureRMToken
        Use Get-azureRMToken to get a token for this parameter

        .PARAMETER groupGUID
        GUID of the group to reprocess licenses of
    
        Requires:
        - Global Administrator Credentials (non-CSP!)
        - AzureRM Module
        - supply result of get-azureRMToken function
    #>
    param(
        [Parameter(Mandatory = $true)]$AzureRMToken,
        [Parameter(Mandatory = $true)]$groupGUID
    )
    $header = @{
        'Authorization' = 'Bearer ' + $AzureRMToken
        'X-Requested-With'= 'XMLHttpRequest'
        'x-ms-client-request-id'= [guid]::NewGuid()
        'x-ms-correlation-id' = [guid]::NewGuid()
    }   

    $url = "https://main.iam.ad.ext.azure.com/api/AccountSkus/Group/$groupGUID/Reprocess"
    Invoke-RestMethod –Uri $url –Headers $header –Method POST -Body $Null -UseBasicParsing -ErrorAction Stop -ContentType "application/json"
}

Source on GIT: https://gitlab.com/Lieben/assortedFunctions/blob/master/invoke-AzHAPIReprocessGroupLicenses.ps1https://gitlab.com/Lieben/assortedFunctions/blob/master/invoke-AzHAPIReprocessGroupLicenses.ps1

Disclaimer: the ‘hidden azure api’ is not officially supported.

Requires output from the Get-AzureRMToken function

Get an Office365 / Azure AD tenant ID from a user’s login name or domain

I often need a tenant ID for a given customer, the usual method to get it is to log in to the Azure portal and find it there. But what if you want to get the tenant ID programmatically? Without actually logging in? And you only know the log in name of a user? Or just one of the customer’s domain names?

Then this’ll help you out!

function get-tenantIdFromLogin(){
    <#
      .SYNOPSIS
      Retrieves an Office 365 / Azure AD tenant ID for a given user login name (email address)
      .EXAMPLE
      $tenantId = get-tenantIdFromLogin -Username you@domain.com
      .PARAMETER Username
      the UPN of a user
      .NOTES
      filename: get-tenantIdFromLogin.ps1
      author: Jos Lieben
      blog: www.lieben.nu
      created: 8/3/2019
    #>
    Param(
        [Parameter(Mandatory=$true)]$Username
    )
    $openIdInfo = Invoke-RestMethod "https://login.windows.net/$($Username.Split("@")[1])/.well-known/openid-configuration" -Method GET
    return $openIdInfo.userinfo_endpoint.Split("/")[3]
}

Obviously, you can also get the tenant ID by just filling out bogus info in front of the user’s login (e.g. bogus@ogd.nl), it’ll still work as only the domain part of the login is really used.

Hope this helps someone 🙂

Git link: https://gitlab.com/Lieben/assortedFunctions/blob/master/get-tenantIdFromLogin.ps1

Deploying an embedded file (FONT) in a Powershell script through Intune MDM

Most solutions that describe how to deploy a font through Intune use an external source to host fonts such as Azure Blob storage.

If you want to KISS (keep it simple, stupid), you don’t want to maintain two different things (your script and externally accessible storage).

The following example shows how to embed a file in a PowerShell script, and then install it into the Fonts folder. This could, of course, be used for other purposes, but don’t forget that the script size limit in Intune is only 200kb.

First, execute the following in a PS window:

$inputFontPath = "C:\fonts\YourFont.ttf"
Compress-Archive $inputFontPath -CompressionLevel Optimal -DestinationPath (Join-Path $Env:TEMP -ChildPath "tempzipfontzipfile.zip") -Force
[Array]$bytes = [io.file]::ReadAllBytes((Join-Path $Env:TEMP -ChildPath "tempzipfontzipfile.zip"))
$b64 = [System.Convert]::ToBase64String($bytes)
$b64 | Set-Clipboard

You have now compressed and base64 encoded YourFont.ttf, and this is loaded in memory (clipboard).

Create a new file, e.g. YourFont.ps1 and add the following:

$b64 = "<SELECT EVERYTHING BETWEEN THESE QUOTES, THEN PRESS CTRL+V>"
$byteContent = [System.Convert]::FromBase64String($b64)
$byteContent | Set-Content (Join-Path $Env:TEMP -ChildPath "tempzipfontzipfile.zip") -Encoding Byte -Force
Expand-Archive -Path (Join-Path $Env:TEMP -ChildPath "tempzipfontzipfile.zip") -DestinationPath (Join-Path $Env:TEMP -ChildPath "tempfontsfolder") -Force

$sa =  new-object -comobject shell.application
$Fonts =  $sa.NameSpace(20)
gci (Join-Path $Env:TEMP -ChildPath "tempfontsfolder") | % {$Fonts.MoveHere($_.FullName)}

Remove-Item (Join-Path $Env:TEMP -ChildPath "tempzipfontzipfile.zip") -Force -ErrorAction SilentlyContinue
Remove-Item (Join-Path $Env:TEMP -ChildPath "tempfontsfolder") -Force -Recurse -ErrorAction SilentlyContinue

On the first line, follow the instructions (e.g. paste your b64-encoded font on that line). Now save and check if the size is below 200kb, then distribute the script to your users. For Fonts, don’t forget to let the script run in system context as administrative permissions are required when installing Fonts.

This method can be used to deploy other payloads as well, happy scripting!

Removing special characters from UTF8 input for use in email addresses or login names

When working with non-US customers, users often have characters in their names like ë, ó, ç and so on. Most of the time, a ‘human process’ converts these to their simple equivalent of e, o and c for use in computerized systems.

When searching for such a mapping of special characters to ‘safe’ characters I had a hard time finding a good list or PowerShell method to automatically convert special characters to standard A-Z characters so I wrote one:

function get-sanitizedUTF8Input{
    Param(
        [String]$inputString
    )
    $replaceTable = @{"ß"="ss";"à"="a";"á"="a";"â"="a";"ã"="a";"ä"="a";"å"="a";"æ"="ae";"ç"="c";"è"="e";"é"="e";"ê"="e";"ë"="e";"ì"="i";"í"="i";"î"="i";"ï"="i";"ð"="d";"ñ"="n";"ò"="o";"ó"="o";"ô"="o";"õ"="o";"ö"="o";"ø"="o";"ù"="u";"ú"="u";"û"="u";"ü"="u";"ý"="y";"þ"="p";"ÿ"="y"}

    foreach($key in $replaceTable.Keys){
        $inputString = $inputString -Replace($key,$replaceTable.$key)
    }
    $inputString = $inputString -replace '[^a-zA-Z0-9]', ''
    return $inputString
}

#example usage:
get-sanitizedUTF8Input -inputString "Jösè"
#result:
Jose

Edit: my colleague Gerbrand alerted me to a post by Grégory Schiro which solves this issue much more elegantly using native .NET functions. My slightly modified version to really ensure nothing non a-zA-Z0-9 gets past the function:

function Remove-DiacriticsAndSpaces
{
    Param(
        [String]$inputString
    )
    $objD = $inputString.Normalize([Text.NormalizationForm]::FormD)
    $sb = New-Object Text.StringBuilder
 
    for ($i = 0; $i -lt $objD.Length; $i++) {
        $c = [Globalization.CharUnicodeInfo]::GetUnicodeCategory($objD[$i])
        if($c -ne [Globalization.UnicodeCategory]::NonSpacingMark) {
          [void]$sb.Append($objD[$i])
        }
      }
    
    $sb = $sb.Normalize([Text.NormalizationForm]::FormC)
    return($sb -replace '[^a-zA-Z0-9]', '')
}
#example usage:
Remove-DiacriticsAndSpaces -inputString "Jösè"
#result:
Jose

And an even easier oneliner I converted to a function by John Seerden:

function Remove-DiacriticsAndSpaces
{
    Param(
        [String]$inputString
    )
    #replace diacritics
    $sb = [Text.Encoding]::ASCII.GetString([Text.Encoding]::GetEncoding("Cyrillic").GetBytes($inputString))

    #remove spaces and anything the above function may have missed
    return($sb -replace '[^a-zA-Z0-9]', '')
}

And the most advanced function I’ve found so far is by 
Daniele Catanesi (PsCustomObject): https://github.com/PsCustomObject/New-StringConversion/blob/master/New-StringConversion.ps1 in which all features of above functions are supported and parameterized.

Reporting on global tenant storage usage and per site storage usage

As my employer is a Microsoft Cloud Service Provider, we want to monitor the total storage available and the total storage used by all of the tenants we manage under CSP, including storage used by Sharepoint and Teams. This called for a script!

per customer total storage usage overview

I slimmed down the resulting script to work for just a single tenant that you can use to generate an XLSX report of which of your sites / teams are nearing their assigned storage quota. You can either build your own alerting around this to raise site quota’s before your users upload too much data, or you can use it to buy additional storage from Microsoft before your tenant reaches the maximum quota 🙂

per site storage overview in excel

As usual, find it on Gitlab!

Finding files in Sharepoint Online or Teams that exceed 218 path length

A well known issue when migrating to Office 365 (Sharepoint, Teams and Onedrive) is path length.

Recently, Microsoft increased the maximum path length in Sharepoint Online from 256 to 400 characters (total length of the URL). This causes issues when you use Office, because Office 2013, 2016 and 2019 do not support paths over 218 characters in length.*

To help you proactively identify files that exceed this limit I wrote a PowerShell script you can run:

  • it can filter based on file type
  • automatically finds and processes all sharepoint sites in your tenant
  • automatically finds and processes all team sites in your tenant
  • it can handle multi-factor authentication

Find get-filesWithLongPathsInOffice365.ps1 on GitLab

It leans heavily on the great work done by the community around OfficePnP, all credits to the community for providing so much quality code for free!

*longer paths may still work, this is not a hard limit

SAP SuccessFactors to Active Directory Sync (updated users)

For a customer that is using SuccessFactors to manage their employees / contractees, I wrote a script that will update the AD accounts of any person that is updated in SuccessFactors.

I expect you’ll have working knowledge on how to configure SF PerformanceManager to export the users you wish to update to a CSV file on the sFTP server SF provides for you.

With that, you should be able to configure the script. It’ll basically map any field you export to any field in Active Directory you wish. In some cases, such as the Manager field, special logic has been added to the script to look up the user’s manager. For other special fields you may have to write your own logic.

If you wish, the script will provide you with a full report in your email, for example:

Get it @ Gitlab directly: https://gitlab.com/Lieben/assortedFunctions/blob/master/update-AdUsersFromSAPSuccessFactorsReport.ps1