Export external users with CLI for Microsoft 365
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 💻.