Skip to content

Instantly share code, notes, and snippets.

@jdthorpe
Last active August 29, 2023 20:48
Show Gist options
  • Save jdthorpe/3263196aa0f623a84d94d7bff3d8d961 to your computer and use it in GitHub Desktop.
Save jdthorpe/3263196aa0f623a84d94d7bff3d8d961 to your computer and use it in GitHub Desktop.
Automating Microsoft 365 administration with the m365 CLI

Microsoft M365 Account Management

This guide provides a brief introduction to administering Microsoft 365 using the m365 CLI targeted at new PowerShell users. Many Microsoft 365 administrative tasks can be easily automated with PowerShell and the Microsoft 365 command line interface (cli), such as creating and deleting user accounts, granting and revoking licenses and many others!

The m365 CLI for Microsoft 365 is a Microsoft 365 Platform Community (PnP) project. Microsoft 365 Platform Community is a virtual team consisting of Microsoft employees and community members focused on helping the community make the best use of Microsoft products. A detailed guide to the m365 cli can be found in the official docs.

Getting Started

This guide uses PowerShell which is a cross platform (Windows, Linux, OSX) general purpose scripting language. A few PowerShell basics are introduced in the accompanying powershell-basics.md file.

If PowersShell is not available to you, the m365 cli can be used to similar effect in other shells like CMD, bash, zsh, etc. Some tips for those terminals are in the accompanying scripting-with-csv.md file.

Machine Setup

The m365 CLI uses NodeJS, which can be installed from nodejs.org. You can verify that node has been installed by calling node --version at the command line, which should print a version number to the console (e.g. v16.15.1). (Aside: in this document I use the terms "terminal" or "console" to refer to the powershell terminal)

Once NodeJS has been installed, the m365 cli tool can be installed by calling npm i -g @pnp/cli-microsoft365 in the PowerShell terminal.

Logging In

There are several ways to log in to the m365 cli tool, and the easiest (and default) methods is called the Device Code Flow. Typing m365 login in the powershell will prompt you to use a login into via aka.ms/devicelogin using a one time code that will be included in the prompt. The first time you login, you will be prompted to grant permissions to the PnP Management Shell. These permissions are required for the app to act on your behalf as you interact with the m365 cli.

More details and alternatives to the device-code-flow can be found in the official docs.

`m365`` CLI Command Syntax

Detailed documentation on the (many!) commands can be found here. In brief,

  • m365 commands are grouped into areas such as Azure AD (aad), teams, docs, outlook, graph, etc.

  • areas my have topics, such as the messages topic within outlook.

  • topics may have one or more verbs, such as add, list, get, and remove

  • areas, topics and verbs are combined when issuing a command. For example, messages in your outlook inbox can be listed with:

    m365 outlook message list --folderName inbox

Most commands have arguments that are specific to that command that provide additional context for the command, such as such as --folderName inbox which previous example. The arguments for each command are listed and explained in the docs.

Finally, there are a handful of options that are common to all commands that modify the value(s) returned by the command. The two most important common arguments are:

  • --output ( or-o) which determines the format of the output (such as json, csv, text). json and csv is obviously useful for programmatic usa of the outputs, and the text option is great for human readability.

  • --query which applies a JMESPath query to the command's return value in order to extract specific outputs. This is used in the m365 aad license list example below to include the SKU, Sku Name, and licenses available in the command output.

Creating users: aad user add

To create a set of users, we'll start by creating a text file called new-user-params.txt with the following content:

FirstName,LastName,alias
CLI-Test,User1,clitestuser1
CLI-Test,User2,clitestuser2
CLI-Test,User3,clitestuser3

Some things to note about this CSV file:

  • The headers are valid PowerShell variable names, i.e. they start with a letter and contain only letters and numbers.
  • The strings (like John) do not have quotes and the values do not contain commas. (Note that PowerShell can use CSV files with quoted strings, though other terminals mentioned in the accompanying scripting-with-csv.md file below cannot)
  • There are no spaces before or after the commas

Regarding the following code:

  • For convenience we won't require the user to create a new password on first log-in (--forceChangePasswordNextSignIn false)
  • To illustrate gathering outputs, we will not provide a password, but instead allow aad to create a random password which will be gathered the output file new-users.csv
  • In order to apply licenses, users must be assigned a country code. A complete list of country codes can be found here
# the name of the tenant (used as part of the username)
$tenant="mytesttenant"

# Country code
$country_code="US"

# Create the users
$user_details = Import-CSV -Path new-user-params.csv | ForEach-Object {
    m365 aad user add `
        --displayName "$($_.FirstName) $($_.LastName)" `
        --firstName $_.FirstName `
        --lastName $_.LastName `
        --userName "$($_.alias)@$tenant.onmicrosoft.com" `
        --accountEnabled true `
        --forceChangePasswordNextSignIn false `
        --usageLocation $country_code `
        --forceChangePasswordNextSignInWithMfa false `
        | convertFrom-Json
}

# store the user details (including the auto-generated passwords) in a CSV file
$user_details | Export-CSV -Path new-user-details.csv

# Store the User Principal Names (UPNs) in a text file
$user_details | Foreach-Object { $_.userPrincipalName } | out-file -Path userPrincipalNames.txt

This code will create a csv file (new-user-details.csv) with the following content:

"id","businessPhones","displayName","givenName","jobTitle","mail","mobilePhone","officeLocation","preferredLanguage","surname","userPrincipalName","password"
"584de8da-3ceb-44e4-986b-02fd87fb899e","System.Object[]","CLI-Test User1","CLI-Test",,,,,,"User1","clitestuser1@mytesttenant.onmicrosoft.com","124skY87z9@7890"
"b5c3206f-6e9e-4255-a079-d5cdfc016744","System.Object[]","CLI-Test User1","CLI-Test",,,,,,"User1","clitestuser2@mytesttenant.onmicrosoft.com","1234LiVgYum7890"
"2309e050-6afd-47c4-b293-a48ec8d005f1","System.Object[]","CLI-Test User1","CLI-Test",,,,,,"User1","clitestuser3@mytesttenant.onmicrosoft.com","1234Joqx&9b7890"

and a text file (userPrincipalNames.txt) with the following content:

jdttestuser1@mytesttenant.onmicrosoft.com
jdttestuser2@mytesttenant.onmicrosoft.com
jdttestuser3@mytesttenant.onmicrosoft.com

Updating user accounts: m365 aad user set

The following code updates the user's country code and removes the requirement to change the password on next login:

$country_code="US"

# Read the file line-by-line.      | Run the code block {...} for each line in the file
Get-Content userPrincipalNames.txt | foreach-Object {
    m365 aad user set `
        --userPrincipalName $_ `
        --usageLocation $country_code `
        --forceChangePasswordNextSignIn false
}

List Available licenses: m365 aad license list

To get details of the licenses that can be granted to users we can use the following command:

m365 aad license list -o text --query "[].{skuId:skuId,sku:skuPartNumber,licensesUsed:consumedUnits,totalLicenses:prepaidUnits.enabled}"

which produces the following output:

skuId                                 sku                         licensesUsed  totalLicenses
------------------------------------  --------------------------  ------------  -------------
b05e124f-c7cc-45a0-a6aa-8cf78c946968  EMSPREMIUM                  3             250
f30db892-07e9-47e9-837c-80727f46fd3d  FLOW_FREE                   7             10000
3227bcb2-8448-4f81-b3c2-8c2074e15a2a  Microsoft_Viva_Sales        3             200
84a661c4-e949-4bd2-a560-ed7766fcaf2b  AAD_PREMIUM_P2              3             300
359ea3e6-8130-4a57-9f8f-ad897a0342f1  DYN365_CUSTOMER_VOICE_BASE  3             3
06ebc4ee-1bb5-47dd-8120-11324bc54e06  SPE_E5                      262           300

Note that the GUID in the skuId field is used in later commands to grant or revoke licenses in the code snippets below.

Assign Licenses to Users: m365 aad user license add

In this example, the Flow Free and AAD_Premium licenses are granted to each user:

# comma separated list of the skuId's of the licenses to assign
$licenses = "84a661c4-e949-4bd2-a560-ed7766fcaf2b,f30db892-07e9-47e9-837c-80727f46fd3d"

Get-Content userPrincipalNames.txt | foreach-Object {
    m365 aad user license add --userName $_ --ids $licenses
}

Revoke Licenses from Users: m365 aad user license remove

In this example, the Flow Free and AAD_Premium licenses are revoked from each user:

# comma separated list of the skuId's of the licenses to assign
$licenses = "84a661c4-e949-4bd2-a560-ed7766fcaf2b,f30db892-07e9-47e9-837c-80727f46fd3d"

Get-Content userPrincipalNames.txt | foreach-Object {
    m365 aad user license remove --userName $_ --ids $licenses --confirm
}

Note that without the --confirm option, the CLI will prompt you to confirm each license revocation.

Delete users: m364 aad user remove.

To clean up after yourself, you may wish to delete users we created earlier like so:

Get-Content userPrincipalNames.txt | Foreach-Object {
    m365 aad user remove  --userName $_ --confirm
}

PowerShell Basics

Reading CSV and Text files

The code snippets throughout this tutorial use PowerShell which comes with two useful functions for iterating over lines of a file, which will be necessary when applying commands to a batch or user accounts.

  • The Get-Content iterates through lines in a file. Very useful when using a file that contains one user Id per line.
  • The Import-CSV iterates through rows of a CSV file. Very useful when using a file that contains several details for each user, such as first name, last name, alias, and password.

The outputs from these commands can be piped (|) to the ForEach-Object loop, and in the body of the loop the current object is assigned to the $_ variable. For example, this snippet prints users's first names in people1.csv to the console:

Import-CSV -Path people1.csv | ForEach-Object { write-host $_.FirstName }

and this snippet prints the list of UPNs to the console:

Get-Content -Path userPrincipalNames.txt | ForEach-Object { write-host $_ }

Gathering outputs from a loop

PowerShell has a convenient quirk which is that the last variable returned in a loop is gathered in an array, and that array becomes the return value of the loop itself. (Wierd, right?). For example, in the following code snippet, the first name of each user is gathered in an array and stored in the variable $first_names:

$first_names = Import-CSV -Path people1.csv | ForEach-Object { $_.FirstName }

This can be used to gather the results of a series of commands, such as the details for new users created:

$new_users = Import-CSV -Path people1.csv | ForEach-Object {
    m365 aad user add --firstName $_.FirstName ... | convertFrom-Json
}

Note that the default output from m365 commands is a JSON formatted String. The built in convertFrom-Json command can be used to transformed this string in to a powershell Object, which can be handy for later use in the script.

Writing to CSV files

The Export-CSV command writes an array of objects to a CSV file. For example, the list of new users can be written to file like so:

$new_users | Export-CSV -Path new-user-details.csv

Writing to Text files

The Out-File command writes an array of objects to a Text file. For example, the list of new users can be written to file like so:

$new_users | Foreach-Object { $_.userPrincipalName }| Out-File -Path new-user-details.csv

String manipulation

String manipulation is a useful and easy in powershell. Variable names in PowerShell must start with the $ character, and when a variable name appears in a string literal, the value of the variable is included in the new string value. For example, the following code snippet prints Hello world to the console:

$val = "world"
Write-Host "Hello $val"

Similarly, the return value of an expression can be included in a string using the $( some-expression ) in a string literal. For example, is a $user variable includes a firstName attribute, it can be substituted into the string like so:

Write-Host "User's first name: $( $user.firstName )"

Scripting with CSV files

Many scripts are intended to issue a command with many times with many sets of values. For example, we may wish to create users in m365 and for each new user, we will need to provide several attributes like first name, last name, alias, and password.

Powershell: Import-CSV

PowerShell's Import CSV is a unique tool for dealing with CSV files, especially those with headers. For exmaple, if you have a file called people1.csv with the following contents:

FirstName,LastName,alias,password
John,Doe,johndoe,r10h328k
Jane,Doe,janedoe,a7h49xkb
Someone,Else,someelse,v83kf02k

You can iterate over the records in that file simply with:

Import-CSV -Path people1.csv | ForEach-Object {
   m365 aad user create  `
    --firstName $_.FirstName `
    --lastName $_.LastName
}

Note that neurotic users (such as myself) will want to ensure that the headers were spelled correctly and values are not missing with assertions like

Import-CSV -Path people1.csv | ForEach-Object {
    if (([string]::IsNullOrEmpty($_.LastName))) {
        write-host "First name is missing!!!" # or skip or exit
    }
}

CSV Files without headers

More generally, iterating over lines of a file, and splitting strings on a delimiter can be used to great effect when processing (simply formatted) CSV files. (or files with any other simple delimiter)

For example, if we have a CSV file called people2.csv like so

John,Doe,johndoe,r10h328k
Jane,Doe,janedoe,a7h49xkb
Someone,Else,someelse,v83kf02k

where (1) there is no header row and (2) that there are no quotes around the strings or spaces before / after the commas, the values in each row can be used to parameterize commands in many shells.

Powershell

In PowerShell, the Get-Content will stream the lines of as an array of strings, and the -split command can be used to split the lines on the commas.

# read the file           | iterate over the lines in the file
Get-Content something.csv | Foreach-Object {
    # split the current line ($_) on the comma
    $FirstName, $LastName, $UserAlias, $UserPassword = -split $)

    # do things here...
}

Obviously this requires that the values in the CSV file do not contain commas (which is sometimes but certianly NOT alwasy true).

Bash / ZSH

In bash, we can read the contents of a file using cat, and then split the file into lines with read, and split the lines into arrays using read -a (or read -A in the zsh terminal, popular on OSX):

#read the file | split it into lines
cat people.txt | while read line
do
    # Split the line ($line) into an array of values ($values)
    IFS=',' read -r -A values <<< "$line" # note that bash uses `-a` whereas zsh (OSX) uses `-A`

    # Extract the individual values from the array.  Note the "1" based indexing!

    firstname="{values[1]}"
    lastname="{values[2]}"

    # do things here...
done

CMD (batch)

This SO post describes how to do the same in CMD (batch) terminal

@bgedelman
Copy link

Nice samples!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment