Export external users with CLI for Microsoft 365

Export external users with CLI for Microsoft 365 header image

Another post after blogging about Migration report with CLI for Microsoft 365. Same scenario, however this time I had to extract all external users from the environment. Having that overview we could reach out and see if they still needed access. Having insights into who accesses what information will help you in determining the best migrations strategy. As migration tooling often don’t migrate external users for you. So following a sample script to report all external users.

CLI for Microsoft 365

For getting a simple list of all external users you have the option to use m365 spo externaluser list. This returns a list of all external users, but does not return the site they are a member of. Instead it returns those users and show some additional info. So I took my previous sample on iterating to all sites from the samples on the CLI for Microsoft 365 docs, and picked the spo user list command to retrieve all users. Passing a query option allows you to filter the dataset. The returned JSON for a user looks as follows:

{
      "Id": 14,
      "IsHiddenInUI": false,
      "LoginName": "i:0#.f|membership|user_domain.nl#ext#@tenant.onmicrosoft.com",
      "Title": "Albert-Jan Schot",
      "PrincipalType": 1,
      "Email": "[email protected]",
      "Expiration": "",
      "IsEmailAuthenticationGuestUser": false,
      "IsShareByEmailGuestUser": true,
      "IsSiteAdmin": false,
      "UserId": null,
      "UserPrincipalName": "user_domain.nl#ext#@tenant.onmicrosoft.com"
},

That means you can filter on either the IsShareByEmailGuestUser property or the Login name as that contains the #ext. Since the IsShareByEmailGuestUser could be false if a user is already present in the tenant I decided to stick with the #ext filter. By adding a value[?contains(LoginName,'#ext#')] filter you can filter on the value object (minor bug in the CLI, some commands return their info wrapped in a value object). The contains part makes sure that search in the full string, and the LoginName makes sure we only query that property. Based on all those external users, we can also retrieve additional data using the m365 spo externaluser list. If we can find the user the account is still present in the Azure Active Directory, if the account is missing it will return no information, so you could also consider removing the account. The full scripts below.

$fileExportPath = "<PUTYOURPATHHERE.csv>"

$m365Status = m365 status

if ($m365Status -eq "Logged Out") {
  # Connection to Microsoft 365
  m365 login
}

$results = @()
Write-host "Retrieving all sites and check external users..."
$allSPOSites = m365 spo site classic list -o json | ConvertFrom-Json
$siteCount = $allSPOSites.Count

Write-Host "Processing $siteCount sites..."
#Loop through each site
$siteCounter = 0

foreach ($site in $allSPOSites) {
  $siteCounter++
  Write-Host "Processing $($site.Url)... ($siteCounter/$siteCount)"

  Write-host "Retrieving all external users ..."

  $users = m365 spo user list --webUrl $site.Url --output json --query "value[?contains(LoginName,'#ext#')]" | ConvertFrom-Json

  foreach ($user in $users) {
    $externalUserObject = m365 spo externaluser list --siteUrl $site.url -o json --query "[?AcceptedAs == '$($user.Email)']" | ConvertFrom-Json

    $results += [pscustomobject][ordered]@{
      UserPrincipalName = $user.UserPrincipalName
      Email             = $user.Email
      InvitedAs         = $externalUserObject.InvitedAs
      WhenCreated       = $externalUserObject.WhenCreated
      InvitedBy         = $externalUserObject.InvitedBy
      Url               = $site.Url
    }
  }
}

Write-Host "Exporting file to $fileExportPath..."
$results | Export-Csv -Path $fileExportPath -NoTypeInformation
Write-Host "Completed."

I am happy that with the previous script and this one even was quicker to get up and running. I have to admit though that the JMESPath stuff was a bit complexer than I anticipated. And I still have to get some bash in there as well 💻.

Loading comments…