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 requesting
  • Connect-MgGraph - Establishes a connection to Microsoft Graph API
    • -Scopes parameter defines what permissions we're requesting
    • -TenantId specifies 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

    • -BodyParameter accepts a hashtable of properties for the new group
    • DisplayName sets the name shown in the admin center
    • MailEnabled = $false specifies this won't have an email address (security group vs. Microsoft 365 Group)
    • MailNickname is required even for security groups (used for internal identification)
    • SecurityEnabled = $true makes 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
    • -BodyParameter accepts a hashtable of properties for the new AU
    • DisplayName sets the visible name
    • Description provides 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
    • -BodyParameter accepts a hashtable with all user properties
    • DisplayName is the user's name shown in the directory
    • UserPrincipalName is the user's login name/email address
    • PasswordProfile is a nested hashtable containing:
      • Password - the initial password for the account
      • ForceChangePasswordNextSignIn - whether the user must change password on first login
    • AccountEnabled determines if the account is active immediately
    • MailNickname is used to generate email addresses
    • UsageLocation is 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:

    • -in operator checks if the SKU part number matches any in our array
    • -lt compares 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 SKU

  • Set-MgUserLicense - Assigns licenses to a user

    • -UserId specifies which user is receiving the license
    • -BodyParameter requires a specific format:
      • AddLicenses is an array of license SKUs to add (with nested hashtables)
      • RemoveLicenses is 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.id property points to the API URL of the object being referenced
    • This format is required by the Graph API to establish relationships
  • New-MgGroupMemberByRef - Adds a member to a group

    • -GroupId specifies which group to modify
    • -BodyParameter contains 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

    • -AdministrativeUnitId specifies which AU to modify
    • -BodyParameter contains 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 property

  • if ([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

    • -Filter uses 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:

# This command disables EVERY user in your organization instantly Get-MgUser | ForEach-Object { Update-MgUser -UserId $_.Id -AccountEnabled $false }

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:

# This removes ALL licenses from a user and may delete data Set-MgUserLicense -UserId $userId -BodyParameter @{ AddLicenses = @() RemoveLicenses = @("all licenses here") }

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:

# Never store credentials like this in production $password = "SuperSecretPassword123"

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:

# ⚠ Assigns premium licenses to everyone — this could get expensive! Get-MgUser | ForEach-Object { Set-MgUserLicense -UserId $_.Id -AddLicenses @{ SkuId = "premium-license-id" } }

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:

# This grants excessive permissions — be cautious! New-MgRoleAssignment -PrincipalId $userId -RoleDefinitionId "Global Admin ID"

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:

# This may fail silently without error handling try { New-MgUser -BodyParameter $params } catch { # This ensures you know something went wrong Write-Error "User creation failed: $_" }

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.