After countless hours navigating through Microsoft 365's admin portal, clicking endlessly through menus and submenus, I finally had enough. There had to be a better way to manage users, groups, and permissions. Enter PowerShell—the command-line interface that promised to revolutionize how I managed my Microsoft 365 tenant. This lab experience transformed me from a point-and-click administrator into a scripting enthusiast.
The PowerShell Advantage: Why Bother Learning Commands?
Before diving into the scripts, you might wonder: "Why should I learn PowerShell when the admin portal works just fine?"
The answer became clear within minutes of running my first successful command. PowerShell automation delivers:
Exponential Time Savings – What took me 15 minutes in the portal takes 15 seconds in PowerShell. After the initial learning curve, you'll recover that investment quickly.
Error-Proof Operations – Humans make mistakes when clicking through dozens of fields. Scripts execute the exact same way, every time.
Delegation Power – I can now package my administrative tasks into scripts that less privileged admins can execute without needing full tenant access.
The Bulk Advantage – Creating 50 users takes essentially the same effort as creating one. Try that in the portal!
Future-Proof Skills – As Microsoft continues its cloud transformation, PowerShell proficiency becomes increasingly valuable across their entire ecosystem.
Command Arsenal: My PowerShell Journey
The Authentication Gateway
My journey began with establishing a connection to Microsoft Graph:
# Request specific permissions
$scopes = @(
"AdministrativeUnit.ReadWrite.All",
"User.ReadWrite.All",
"Group.ReadWrite.All",
"Directory.ReadWrite.All"
)
# Connect interactively to the tenant
Connect-MgGraph -Scopes $scopes -TenantId "Your-Tenant-ID.onmicrosoft.com"
Command breakdown:
$scopes = @()- Creates an array of permission scopes we're requestingConnect-MgGraph- Establishes a connection to Microsoft Graph API-Scopesparameter defines what permissions we're requesting-TenantIdspecifies which Microsoft 365 tenant we're connecting to- This command opens a browser window where you authenticate with your credentials
After running this command, PowerShell has an authenticated session with Microsoft Graph, allowing it to perform operations based on the requested permissions.
Building Organizational Structures
First, I needed to create a security group and an administrative unit:
# Create a new security group
$finance = New-MgGroup -BodyParameter @{
DisplayName = "Finance Team Members"
MailEnabled = $false
MailNickname = "FinanceTeamMembers"
SecurityEnabled = $true
}
# Create an administrative boundary (AU)
$financeAU = New-MgDirectoryAdministrativeUnit -BodyParameter @{
DisplayName = "Finance AU"
Description = "Administrative boundary for the Finance Department"
}
Command breakdown:
New-MgGroup- Creates a new group in Microsoft 365-BodyParameteraccepts a hashtable of properties for the new groupDisplayNamesets the name shown in the admin centerMailEnabled = $falsespecifies this won't have an email address (security group vs. Microsoft 365 Group)MailNicknameis required even for security groups (used for internal identification)SecurityEnabled = $truemakes this a security group that can be used for permissions
New-MgDirectoryAdministrativeUnit- Creates an Administrative Unit (AU)- AUs are containers that let you delegate administrative rights to specific subsets of users/resources
-BodyParameteraccepts a hashtable of properties for the new AUDisplayNamesets the visible nameDescriptionprovides additional context about the AU's purpose
Both commands return objects representing the newly created items, which I store in variables for later use.
Crafting Digital Identities
With structures in place, I created individual users:
$tristan = New-MgUser -BodyParameter @{
DisplayName = "Tristan Smith"
UserPrincipalName = "Tristan.Smith@Your-Tenant-ID.onmicrosoft.com"
PasswordProfile = @{
Password = "InitialPwd!-5182989"
ForceChangePasswordNextSignIn = $false
}
AccountEnabled = $true
MailNickname = "Tristan.Smith"
UsageLocation = "US" # Required for licensing!
}
Command breakdown:
New-MgUser- Creates a new user account in Microsoft 365-BodyParameteraccepts a hashtable with all user propertiesDisplayNameis the user's name shown in the directoryUserPrincipalNameis the user's login name/email addressPasswordProfileis a nested hashtable containing:Password- the initial password for the accountForceChangePasswordNextSignIn- whether the user must change password on first login
AccountEnableddetermines if the account is active immediatelyMailNicknameis used to generate email addressesUsageLocationis critical for licensing - you cannot assign licenses without this
The command returns the newly created user object, which I store in the $tristan variable to access its properties (particularly the ID) later.
Automating Entitlements
Next came licensing, which required a dynamic approach to find available licenses:
# Find an available license
$license = Get-MgSubscribedSku | Where-Object {
$_.SkuPartNumber -in @("SPB", "SPE_E5") -and
$_.ConsumedUnits -lt $_.PrepaidUnits.Enabled
} | Select-Object -First 1
# Assign the found license
Set-MgUserLicense -UserId $tristan.Id -BodyParameter @{
AddLicenses = @( @{ SkuId = $license.SkuId } )
RemoveLicenses = @()
}
Command breakdown:
Get-MgSubscribedSku- Retrieves all license SKUs (Stock Keeping Units) available in your tenant- Returns details about each license type including how many are available vs. used
- The
|(pipe) sends this list to the next command
Where-Object- Filters the list of licenses based on conditions:-inoperator checks if the SKU part number matches any in our array-ltcompares consumed licenses with total licenses to ensure some are available- The
|pipes this filtered list to the next command
Select-Object -First 1- Takes just the first matching license SKUSet-MgUserLicense- Assigns licenses to a user-UserIdspecifies which user is receiving the license-BodyParameterrequires a specific format:AddLicensesis an array of license SKUs to add (with nested hashtables)RemoveLicensesis an array of license SKUs to remove (empty in this case)
This approach automatically picks an available license from several possibilities, making the script resilient to different tenant configurations.
Weaving the Web of Permissions
With users created, I established relationships between users, groups, and administrative units:
.
# Create a reference object for the user
$ref = @{ "@odata.id" = "[https://graph.microsoft.com/v1.0/users/$($tristan.Id](https://graph.microsoft.com/v1.0/users/$($tristan.Id))" }
# Add user as a member and owner
New-MgGroupMemberByRef -GroupId $finance.Id -BodyParameter $ref
New-MgGroupOwnerByRef -GroupId $finance.Id -BodyParameter $ref
# Add user to the AU
New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId $financeAU.Id -BodyParameter $ref
Command breakdown:
$ref = @{ "@odata.id" = "..." }- Creates a reference object using Microsoft Graph API format- The
@odata.idproperty points to the API URL of the object being referenced - This format is required by the Graph API to establish relationships
- The
New-MgGroupMemberByRef- Adds a member to a group-GroupIdspecifies which group to modify-BodyParametercontains the reference to the user being added as a member- Members can access resources shared with the group
New-MgGroupOwnerByRef- Adds an owner to a group- Similar to the member command but creates ownership relationship
- Owners can manage group settings, add/remove members, etc.
- The difference between member and owner is significant for delegation
New-MgDirectoryAdministrativeUnitMemberByRef- Adds a member to an Administrative Unit-AdministrativeUnitIdspecifies which AU to modify-BodyParametercontains the reference to the user being added- This places the user within the management boundary of the AU
These "ByRef" commands all use the same pattern of establishing relationships between objects using their Graph API references.
Mass Creation: The CSV Power Play
Finally, I tackled bulk user creation using a CSV file:
The true power of automation is realized when creating users in bulk from a CSV file.
foreach ($user in (Import-Csv "C:\Users\Admin\Documents\BulkUsers.csv")) {
$newUser = New-MgUser -BodyParameter @{
DisplayName = $user.displayname
UserPrincipalName = $user.principal
PasswordProfile = @{
Password = $user.password
ForceChangePasswordNextSignIn = $false
}
AccountEnabled = [System.Convert]::ToBoolean($user.enabled)
MailNickname = $user.mailnickname
UsageLocation = "US"
}
if ([System.Convert]::ToBoolean($user.licensed)) {
Set-MgUserLicense -UserId $newUser.Id -BodyParameter @{
AddLicenses = @( @{ SkuId = $license.SkuId } )
RemoveLicenses = @()
}
}
if ($user.group) {
$group = Get-MgGroup -Filter "displayName eq '$($user.group)'"
New-MgGroupMemberByRef -GroupId $group.Id -BodyParameter @{
"@odata.id" = "https://graph.microsoft.com/v1.0/users/$($newUser.Id)"
}
}
}
Command breakdown:
Import-Csv- Reads a CSV file and creates objects for each row- Each column becomes a property you can access (e.g., $user.displayname)
foreach- Iterates through each row in the CSV file[System.Convert]::ToBoolean()- Converts string values from CSV ("true"/"false") to actual boolean values- This is necessary because CSV imports everything as strings
New-MgUser- Same as before, but using CSV data for each propertyif ([System.Convert]::ToBoolean($user.licensed))- Conditional license assignment- Only users with "true" in the licensed column get licenses
Get-MgGroup -Filter "displayName eq '...'"- Finds a group by its display name-Filteruses OData syntax to query Microsoft Graph- This allows finding objects by their properties rather than IDs
if ($user.group)- Conditional group assignment- Only users with a value in the group column get added to groups
This script shows the true power of PowerShell automation—processing multiple users with different settings from a single data source with minimal code.
The Double-Edged Sword of PowerShell: Potential Dangers You Must Know
PowerShell is a fantastic tool. It boosts your efficiency, automates tedious tasks, and helps you manage your environment like a pro. But with great power comes great responsibility. A simple mistake can lead to data loss, security breaches, or costly errors. In this blog post, we'll explore some common risks when using PowerShell — and how you can avoid them.
Mass Destruction Risk: One Command, Thousands Affected
The admin portal usually processes one user at a time. PowerShell doesn’t care. It can execute changes across hundreds or thousands of accounts in seconds — sometimes without you realizing the scale.
Example:
Safety Tips:
✔ Test scripts in a non-production environment first
✔ Use the -WhatIf parameter to preview changes
✔ Add confirmation prompts for bulk actions
✔ Start with small, filtered groups before running on all users
Irreversible Actions: No "Undo" Button Here
Some actions can’t be rolled back. Once you remove licenses or change settings, you might lose data permanently.
Example:
Safety Tips:
✔ Always export current settings before making changes
✔ Document what you're updating or removing
✔ Check your organization's data retention and recovery policies
Credential Security Risks: Keep Secrets Safe
Hardcoding passwords or credentials in scripts is a major security risk. If your script leaks, attackers can gain full access.
Example:
Safety Tips:
✔ Store secrets securely using Azure Key Vault or similar solutions
✔ Use certificate-based authentication instead of passwords
✔ Avoid keeping plain-text credentials in your scripts or shared drives
License & Cost Implications: Don’t Overspend
A script can assign expensive licenses to users in bulk without oversight — leading to unnecessary costs.
Example:
Safety Tips:
✔ Implement approval workflows before assigning licenses
✔ Regularly monitor license usage
✔ Use conditional logic to assign licenses only when required
Security Boundary Violations: Avoid Over-Permissioning
PowerShell can override security groups and boundaries if you're not careful, potentially giving users more permissions than they need.
Example:
Safety Tips:
✔ Follow least-privilege principles — only assign necessary permissions
✔ Use Privileged Identity Management (PIM) for sensitive roles
✔ Audit role assignments regularly to ensure compliance
Silent Failures: Errors May Go Unnoticed
Unlike admin portals that clearly show errors, PowerShell might fail quietly unless you explicitly check for issues.
Example:
Safety Tips:
✔ Always include error handling in your scripts
✔ Verify operations succeeded after execution
✔ Log all actions to trace and troubleshoot problems
Troubleshooting Trenches: Battle Scars and Victories
Like any worthwhile endeavor, my PowerShell journey came with challenges:
The Credential Conundrum: My first attempt used -Credential with Connect-MgGraph, which fails spectacularly for delegated permissions. The solution was to embrace interactive authentication.
The Missing Location Mystery: I couldn't figure out why license assignment kept failing until I discovered the UsageLocation property is mandatory before licensing.
The Boolean Bamboozle: CSV imports treat everything as strings, including "true" and "false" values. The fix was using [System.Convert]::ToBoolean() to properly convert these values.
The Parameter Puzzle: PowerShell commands can be picky about parameter formats. Learning when to use -Body versus -BodyParameter, and how to structure nested hashtables, was an exercise in patience.
The Relationship Riddle: Creating connections between objects requires specific API URL formats with @odata.id. It's not intuitive, but it's consistent once you understand the pattern.
From Script Novice to Automation Advocate: My Takeaways
This lab journey transformed how I view Microsoft 365 administration:
PowerShell Is an Investment, Not an Expense: The time spent learning these commands pays dividends with every administrative task.
Think in Objects and Relationships: Microsoft 365 is fundamentally about entities (users, groups) and their connections. PowerShell makes this model explicit and manipulable.
Automation Creates Consistency: With scripts, I know exactly what will happen every time—no more wondering if I configured something correctly.
Documentation Through Code: My scripts now serve as living documentation of my tenant configuration decisions.
Scale Without Struggle: Tasks that would be prohibitively time-consuming in the portal become trivial with automation.
The Microsoft 365 admin portal is fine for occasional tasks, but for serious administrators, PowerShell isn't optional—it's essential. This lab didn't just teach me commands; it changed my entire approach to administration.
What started as a requirement to complete an assignment has become my default way of working. Why click through ten screens when I can type one command? That's the PowerShell promise, and it delivers.