MS365 Tenant Manager
Microsoft 365 enterprise administration — user management, security policies, SSO/SAML setup, compliance, and tenant optimization.
What this skill does
Streamline Microsoft 365 administration by automating user setup, security policies, and organization configuration. You can generate ready-to-use instructions for bulk onboarding, enforcing login safeguards, and auditing compliance settings. Use this when launching a new enterprise environment or managing ongoing security and user management tasks.
name: “ms365-tenant-manager” description: Microsoft 365 tenant administration for Global Administrators. Automate M365 tenant setup, Office 365 admin tasks, Azure AD user management, Exchange Online configuration, Teams administration, and security policies. Generate PowerShell scripts for bulk operations, Conditional Access policies, license management, and compliance reporting. Use for M365 tenant manager, Office 365 admin, Azure AD users, Global Administrator, tenant configuration, or Microsoft 365 automation.
Microsoft 365 Tenant Manager
Expert guidance and automation for Microsoft 365 Global Administrators managing tenant setup, user lifecycle, security policies, and organizational optimization.
Quick Start
Run a Security Audit
Connect-MgGraph -Scopes "Directory.Read.All","Policy.Read.All","AuditLog.Read.All"
Get-MgSubscribedSku | Select-Object SkuPartNumber, ConsumedUnits, @{N="Total";E={$_.PrepaidUnits.Enabled}}
Get-MgPolicyAuthorizationPolicy | Select-Object AllowInvitesFrom, DefaultUserRolePermissions
Bulk Provision Users from CSV
# CSV columns: DisplayName, UserPrincipalName, Department, LicenseSku
Import-Csv .\new_users.csv | ForEach-Object {
$passwordProfile = @{ Password = (New-Guid).ToString().Substring(0,16) + "!"; ForceChangePasswordNextSignIn = $true }
New-MgUser -DisplayName $_.DisplayName -UserPrincipalName $_.UserPrincipalName `
-Department $_.Department -AccountEnabled -PasswordProfile $passwordProfile
}
Create a Conditional Access Policy (MFA for Admins)
$adminRoles = (Get-MgDirectoryRole | Where-Object { $_.DisplayName -match "Admin" }).Id
$policy = @{
DisplayName = "Require MFA for Admins"
State = "enabledForReportingButNotEnforced" # Start in report-only mode
Conditions = @{ Users = @{ IncludeRoles = $adminRoles } }
GrantControls = @{ Operator = "OR"; BuiltInControls = @("mfa") }
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $policy
Workflows
Workflow 1: New Tenant Setup
Step 1: Generate Setup Checklist
Confirm prerequisites before provisioning:
- Global Admin account created and secured with MFA
- Custom domain purchased and accessible for DNS edits
- License SKUs confirmed (E3 vs E5 feature requirements noted)
Step 2: Configure and Verify DNS Records
# After adding the domain in the M365 admin center, verify propagation before proceeding
$domain = "company.com"
Resolve-DnsName -Name "_msdcs.$domain" -Type NS -ErrorAction SilentlyContinue
# Also run from a shell prompt:
# nslookup -type=MX company.com
# nslookup -type=TXT company.com # confirm SPF record
Wait for DNS propagation (up to 48 h) before bulk user creation.
Step 3: Apply Security Baseline
# Disable legacy authentication (blocks Basic Auth protocols)
$policy = @{
DisplayName = "Block Legacy Authentication"
State = "enabled"
Conditions = @{ ClientAppTypes = @("exchangeActiveSync","other") }
GrantControls = @{ Operator = "OR"; BuiltInControls = @("block") }
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $policy
# Enable unified audit log
Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $true
Step 4: Provision Users
$licenseSku = (Get-MgSubscribedSku | Where-Object { $_.SkuPartNumber -eq "ENTERPRISEPACK" }).SkuId
Import-Csv .\employees.csv | ForEach-Object {
try {
$user = New-MgUser -DisplayName $_.DisplayName -UserPrincipalName $_.UserPrincipalName `
-AccountEnabled -PasswordProfile @{ Password = (New-Guid).ToString().Substring(0,12)+"!"; ForceChangePasswordNextSignIn = $true }
Set-MgUserLicense -UserId $user.Id -AddLicenses @(@{ SkuId = $licenseSku }) -RemoveLicenses @()
Write-Host "Provisioned: $($_.UserPrincipalName)"
} catch {
Write-Warning "Failed $($_.UserPrincipalName): $_"
}
}
Validation: Spot-check 3–5 accounts in the M365 admin portal; confirm licenses show “Active.”
Workflow 2: Security Hardening
Step 1: Run Security Audit
Connect-MgGraph -Scopes "Directory.Read.All","Policy.Read.All","AuditLog.Read.All","Reports.Read.All"
# Export Conditional Access policy inventory
Get-MgIdentityConditionalAccessPolicy | Select-Object DisplayName, State |
Export-Csv .\ca_policies.csv -NoTypeInformation
# Find accounts without MFA registered
$report = Get-MgReportAuthenticationMethodUserRegistrationDetail
$report | Where-Object { -not $_.IsMfaRegistered } |
Select-Object UserPrincipalName, IsMfaRegistered |
Export-Csv .\no_mfa_users.csv -NoTypeInformation
Write-Host "Audit complete. Review ca_policies.csv and no_mfa_users.csv."
Step 2: Create MFA Policy (report-only first)
$policy = @{
DisplayName = "Require MFA All Users"
State = "enabledForReportingButNotEnforced"
Conditions = @{ Users = @{ IncludeUsers = @("All") } }
GrantControls = @{ Operator = "OR"; BuiltInControls = @("mfa") }
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $policy
Validation: After 48 h, review Sign-in logs in Entra ID; confirm expected users would be challenged, then change State to "enabled".
Step 3: Review Secure Score
# Retrieve current Secure Score and top improvement actions
Get-MgSecuritySecureScore -Top 1 | Select-Object CurrentScore, MaxScore, ActiveUserCount
Get-MgSecuritySecureScoreControlProfile | Sort-Object -Property ActionType |
Select-Object Title, ImplementationStatus, MaxScore | Format-Table -AutoSize
Workflow 3: User Offboarding
Step 1: Block Sign-in and Revoke Sessions
$upn = "[email protected]"
$user = Get-MgUser -Filter "userPrincipalName eq '$upn'"
# Block sign-in immediately
Update-MgUser -UserId $user.Id -AccountEnabled:$false
# Revoke all active tokens
Invoke-MgInvalidateAllUserRefreshToken -UserId $user.Id
Write-Host "Sign-in blocked and sessions revoked for $upn"
Step 2: Preview with -WhatIf (license removal)
# Identify assigned licenses
$licenses = (Get-MgUserLicenseDetail -UserId $user.Id).SkuId
# Dry-run: print what would be removed
$licenses | ForEach-Object { Write-Host "[WhatIf] Would remove SKU: $_" }
Step 3: Execute Offboarding
# Remove licenses
Set-MgUserLicense -UserId $user.Id -AddLicenses @() -RemoveLicenses $licenses
# Convert mailbox to shared (requires ExchangeOnlineManagement module)
Set-Mailbox -Identity $upn -Type Shared
# Remove from all groups
Get-MgUserMemberOf -UserId $user.Id | ForEach-Object {
try { Remove-MgGroupMemberByRef -GroupId $_.Id -DirectoryObjectId $user.Id } catch {}
}
Write-Host "Offboarding complete for $upn"
Validation: Confirm in the M365 admin portal that the account shows “Blocked,” has no active licenses, and the mailbox type is “Shared.”
Best Practices
Tenant Setup
- Enable MFA before adding users
- Configure named locations for Conditional Access
- Use separate admin accounts with PIM
- Verify custom domains (and DNS propagation) before bulk user creation
- Apply Microsoft Secure Score recommendations
Security Operations
- Start Conditional Access policies in report-only mode
- Review Sign-in logs for 48 h before enforcing a new policy
- Never hardcode credentials in scripts — use Azure Key Vault or
Get-Credential - Enable unified audit logging for all operations
- Conduct quarterly security reviews and Secure Score check-ins
PowerShell Automation
- Prefer Microsoft Graph (
Microsoft.Graphmodule) over legacy MSOnline - Include
try/catchblocks for error handling - Implement
Write-Host/Write-Warninglogging for audit trails - Use
-WhatIfor dry-run output before bulk destructive operations - Test in a non-production tenant first
Reference Guides
references/powershell-templates.md
- Ready-to-use script templates
- Conditional Access policy examples
- Bulk user provisioning scripts
- Security audit scripts
references/security-policies.md
- Conditional Access configuration
- MFA enforcement strategies
- DLP and retention policies
- Security baseline settings
references/troubleshooting.md
- Common error resolutions
- PowerShell module issues
- Permission troubleshooting
- DNS propagation problems
Limitations
| Constraint | Impact |
|---|---|
| Global Admin required | Full tenant setup needs highest privilege |
| API rate limits | Bulk operations may be throttled |
| License dependencies | E3/E5 required for advanced features |
| Hybrid scenarios | On-premises AD needs additional configuration |
| PowerShell prerequisites | Microsoft.Graph module required |
Required PowerShell Modules
Install-Module Microsoft.Graph -Scope CurrentUser
Install-Module ExchangeOnlineManagement -Scope CurrentUser
Install-Module MicrosoftTeams -Scope CurrentUser
Required Permissions
- Global Administrator — Full tenant setup
- User Administrator — User management
- Security Administrator — Security policies
- Exchange Administrator — Mailbox management
{
"setup_checklist": {
"total_phases": 5,
"estimated_time": "3.5 hours",
"phases": [
{
"phase": 1,
"name": "Initial Tenant Configuration",
"priority": "critical",
"task_count": 3,
"estimated_time": "30 minutes"
},
{
"phase": 2,
"name": "Custom Domain Configuration",
"priority": "critical",
"task_count": 4,
"estimated_time": "45 minutes"
},
{
"phase": 3,
"name": "Security Baseline Configuration",
"priority": "critical",
"task_count": 5,
"estimated_time": "60 minutes"
},
{
"phase": 4,
"name": "Service Configuration",
"priority": "high",
"task_count": 4,
"estimated_time": "90 minutes"
},
{
"phase": 5,
"name": "Compliance Configuration",
"priority": "high",
"task_count": 1,
"estimated_time": "45 minutes"
}
]
},
"dns_records": {
"mx_records": 1,
"txt_records": 2,
"cname_records": 6,
"srv_records": 2,
"total_records": 11
},
"powershell_scripts_generated": [
"Initial_Tenant_Setup.ps1",
"Configure_DNS_Records.txt",
"Enable_Security_Baseline.ps1"
],
"license_recommendations": {
"E5": {
"count": 5,
"monthly_cost": 285.00,
"users": "Executives and IT admins"
},
"E3": {
"count": 15,
"monthly_cost": 540.00,
"users": "Finance, Legal, HR departments"
},
"Business_Standard": {
"count": 50,
"monthly_cost": 625.00,
"users": "Standard office workers"
},
"Business_Basic": {
"count": 5,
"monthly_cost": 30.00,
"users": "Part-time staff"
},
"total_monthly_cost": 1480.00,
"total_annual_cost": 17760.00
},
"next_steps": [
"Review and verify DNS records",
"Test MFA enrollment process",
"Create security groups for departments",
"Begin user provisioning",
"Schedule security review meeting"
]
}
PowerShell Script Templates
Ready-to-use PowerShell scripts for Microsoft 365 administration with error handling and best practices.
Table of Contents
- Prerequisites
- Security Audit Script
- Conditional Access Policy
- Bulk User Provisioning
- User Offboarding
- License Management
- DNS Records Configuration
Prerequisites
Install required modules before running scripts:
# Install Microsoft Graph module (recommended)
Install-Module Microsoft.Graph -Scope CurrentUser -Force
# Install Exchange Online module
Install-Module ExchangeOnlineManagement -Scope CurrentUser -Force
# Install Teams module
Install-Module MicrosoftTeams -Scope CurrentUser -Force
# Verify installations
Get-InstalledModule Microsoft.Graph, ExchangeOnlineManagement, MicrosoftTeamsSecurity Audit Script
Comprehensive security audit for MFA status, admin accounts, inactive users, and permissions.
<#
.SYNOPSIS
Microsoft 365 Security Audit Report
.DESCRIPTION
Performs comprehensive security audit and generates CSV reports.
Checks: MFA status, admin accounts, inactive users, guest access, licenses
.OUTPUTS
CSV reports in SecurityAudit_[timestamp] directory
#>
#Requires -Modules Microsoft.Graph, ExchangeOnlineManagement
param(
[int]$InactiveDays = 90,
[string]$OutputPath = "."
)
# Connect to services
Connect-MgGraph -Scopes "Directory.Read.All", "User.Read.All", "AuditLog.Read.All"
Connect-ExchangeOnline
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$reportPath = Join-Path $OutputPath "SecurityAudit_$timestamp"
New-Item -ItemType Directory -Path $reportPath -Force | Out-Null
Write-Host "Starting Security Audit..." -ForegroundColor Cyan
# 1. MFA Status Check
Write-Host "[1/5] Checking MFA status..." -ForegroundColor Yellow
$users = Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,AccountEnabled
$mfaReport = @()
foreach ($user in $users) {
$authMethods = Get-MgUserAuthenticationMethod -UserId $user.Id -ErrorAction SilentlyContinue
$hasMFA = ($authMethods | Where-Object { $_.AdditionalProperties.'@odata.type' -ne '#microsoft.graph.passwordAuthenticationMethod' }).Count -gt 0
$mfaReport += [PSCustomObject]@{
UserPrincipalName = $user.UserPrincipalName
DisplayName = $user.DisplayName
AccountEnabled = $user.AccountEnabled
MFAEnabled = $hasMFA
AuthMethodsCount = $authMethods.Count
}
}
$mfaReport | Export-Csv -Path "$reportPath/MFA_Status.csv" -NoTypeInformation
$usersWithoutMFA = ($mfaReport | Where-Object { -not $_.MFAEnabled -and $_.AccountEnabled }).Count
Write-Host " Users without MFA: $usersWithoutMFA" -ForegroundColor $(if($usersWithoutMFA -gt 0){'Red'}else{'Green'})
# 2. Admin Roles Audit
Write-Host "[2/5] Auditing admin roles..." -ForegroundColor Yellow
$adminRoles = Get-MgDirectoryRole -All
$adminReport = @()
foreach ($role in $adminRoles) {
$members = Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id -All
foreach ($member in $members) {
$memberUser = Get-MgUser -UserId $member.Id -ErrorAction SilentlyContinue
if ($memberUser) {
$adminReport += [PSCustomObject]@{
UserPrincipalName = $memberUser.UserPrincipalName
DisplayName = $memberUser.DisplayName
Role = $role.DisplayName
AccountEnabled = $memberUser.AccountEnabled
}
}
}
}
$adminReport | Export-Csv -Path "$reportPath/Admin_Roles.csv" -NoTypeInformation
Write-Host " Admin assignments: $($adminReport.Count)" -ForegroundColor Cyan
# 3. Inactive Users
Write-Host "[3/5] Finding inactive users ($InactiveDays+ days)..." -ForegroundColor Yellow
$inactiveDate = (Get-Date).AddDays(-$InactiveDays)
$inactiveUsers = Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,SignInActivity,AccountEnabled |
Where-Object {
$_.AccountEnabled -and
$_.SignInActivity.LastSignInDateTime -and
$_.SignInActivity.LastSignInDateTime -lt $inactiveDate
} |
Select-Object UserPrincipalName, DisplayName,
@{N='LastSignIn';E={$_.SignInActivity.LastSignInDateTime}},
@{N='DaysSinceSignIn';E={((Get-Date) - $_.SignInActivity.LastSignInDateTime).Days}}
$inactiveUsers | Export-Csv -Path "$reportPath/Inactive_Users.csv" -NoTypeInformation
Write-Host " Inactive users: $($inactiveUsers.Count)" -ForegroundColor $(if($inactiveUsers.Count -gt 0){'Yellow'}else{'Green'})
# 4. Guest Users
Write-Host "[4/5] Reviewing guest access..." -ForegroundColor Yellow
$guestUsers = Get-MgUser -Filter "userType eq 'Guest'" -All -Property UserPrincipalName,DisplayName,AccountEnabled,CreatedDateTime
$guestUsers | Select-Object UserPrincipalName, DisplayName, AccountEnabled, CreatedDateTime |
Export-Csv -Path "$reportPath/Guest_Users.csv" -NoTypeInformation
Write-Host " Guest users: $($guestUsers.Count)" -ForegroundColor Cyan
# 5. License Usage
Write-Host "[5/5] Analyzing licenses..." -ForegroundColor Yellow
$licenses = Get-MgSubscribedSku -All
$licenseReport = foreach ($lic in $licenses) {
[PSCustomObject]@{
ProductName = $lic.SkuPartNumber
TotalLicenses = $lic.PrepaidUnits.Enabled
AssignedLicenses = $lic.ConsumedUnits
AvailableLicenses = $lic.PrepaidUnits.Enabled - $lic.ConsumedUnits
Utilization = [math]::Round(($lic.ConsumedUnits / [math]::Max($lic.PrepaidUnits.Enabled, 1)) * 100, 1)
}
}
$licenseReport | Export-Csv -Path "$reportPath/License_Usage.csv" -NoTypeInformation
Write-Host " License SKUs: $($licenses.Count)" -ForegroundColor Cyan
# Summary
Write-Host "`n=== Security Audit Summary ===" -ForegroundColor Green
Write-Host "Total Users: $($users.Count)"
Write-Host "Users without MFA: $usersWithoutMFA $(if($usersWithoutMFA -gt 0){'[ACTION REQUIRED]'})"
Write-Host "Inactive Users: $($inactiveUsers.Count)"
Write-Host "Guest Users: $($guestUsers.Count)"
Write-Host "Admin Assignments: $($adminReport.Count)"
Write-Host "`nReports saved to: $reportPath" -ForegroundColor Green
# Disconnect
Disconnect-MgGraph
Disconnect-ExchangeOnline -Confirm:$falseConditional Access Policy
Create Conditional Access policy requiring MFA for administrators.
<#
.SYNOPSIS
Create Conditional Access Policy for MFA
.DESCRIPTION
Creates a Conditional Access policy requiring MFA.
Policy is created in report-only mode for safe testing.
.PARAMETER PolicyName
Name for the policy
.PARAMETER IncludeAllUsers
Apply to all users (default: false, admins only)
#>
#Requires -Modules Microsoft.Graph
param(
[string]$PolicyName = "Require MFA for Administrators",
[switch]$IncludeAllUsers,
[switch]$Enforce
)
Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess", "Directory.Read.All"
# Get admin role IDs
$adminRoles = @(
"62e90394-69f5-4237-9190-012177145e10" # Global Administrator
"194ae4cb-b126-40b2-bd5b-6091b380977d" # Security Administrator
"f28a1f50-f6e7-4571-818b-6a12f2af6b6c" # SharePoint Administrator
"29232cdf-9323-42fd-ade2-1d097af3e4de" # Exchange Administrator
"fe930be7-5e62-47db-91af-98c3a49a38b1" # User Administrator
)
# Build conditions
$conditions = @{
Users = @{
IncludeRoles = if ($IncludeAllUsers) { $null } else { $adminRoles }
IncludeUsers = if ($IncludeAllUsers) { @("All") } else { $null }
ExcludeUsers = @("GuestsOrExternalUsers")
}
Applications = @{
IncludeApplications = @("All")
}
ClientAppTypes = @("browser", "mobileAppsAndDesktopClients")
}
# Remove null entries
if ($IncludeAllUsers) {
$conditions.Users.Remove("IncludeRoles")
} else {
$conditions.Users.Remove("IncludeUsers")
}
$grantControls = @{
BuiltInControls = @("mfa")
Operator = "OR"
}
$state = if ($Enforce) { "enabled" } else { "enabledForReportingButNotEnforced" }
$policyParams = @{
DisplayName = $PolicyName
State = $state
Conditions = $conditions
GrantControls = $grantControls
}
try {
$policy = New-MgIdentityConditionalAccessPolicy -BodyParameter $policyParams
Write-Host "Policy created successfully" -ForegroundColor Green
Write-Host " Name: $($policy.DisplayName)"
Write-Host " ID: $($policy.Id)"
Write-Host " State: $state"
if (-not $Enforce) {
Write-Host "`nPolicy is in REPORT-ONLY mode." -ForegroundColor Yellow
Write-Host "Monitor sign-in logs before enforcing."
Write-Host "To enforce: Update policy state to 'enabled' in Azure AD portal"
}
} catch {
Write-Host "Error creating policy: $_" -ForegroundColor Red
}
Disconnect-MgGraphBulk User Provisioning
Create users from CSV with license assignment.
<#
.SYNOPSIS
Bulk User Provisioning from CSV
.DESCRIPTION
Creates users from CSV file with automatic license assignment.
.PARAMETER CsvPath
Path to CSV file with columns: DisplayName, UserPrincipalName, Department, JobTitle
.PARAMETER LicenseSku
License SKU to assign (e.g., ENTERPRISEPACK for E3)
.PARAMETER Password
Initial password (auto-generated if not provided)
#>
#Requires -Modules Microsoft.Graph
param(
[Parameter(Mandatory)]
[string]$CsvPath,
[string]$LicenseSku = "ENTERPRISEPACK",
[string]$Password,
[switch]$WhatIf
)
Connect-MgGraph -Scopes "User.ReadWrite.All", "Directory.ReadWrite.All"
# Validate CSV
if (-not (Test-Path $CsvPath)) {
Write-Host "CSV file not found: $CsvPath" -ForegroundColor Red
exit 1
}
$users = Import-Csv $CsvPath
Write-Host "Found $($users.Count) users in CSV" -ForegroundColor Cyan
# Get license SKU ID
$license = Get-MgSubscribedSku -All | Where-Object { $_.SkuPartNumber -eq $LicenseSku }
if (-not $license) {
Write-Host "License SKU not found: $LicenseSku" -ForegroundColor Red
Write-Host "Available SKUs:"
Get-MgSubscribedSku -All | ForEach-Object { Write-Host " $($_.SkuPartNumber)" }
exit 1
}
$results = @()
$successCount = 0
$errorCount = 0
foreach ($user in $users) {
$upn = $user.UserPrincipalName
if ($WhatIf) {
Write-Host "[WhatIf] Would create: $upn" -ForegroundColor Yellow
continue
}
# Generate password if not provided
$userPassword = if ($Password) { $Password } else {
-join ((65..90) + (97..122) + (48..57) + (33,35,36,37) | Get-Random -Count 16 | ForEach-Object { [char]$_ })
}
$userParams = @{
DisplayName = $user.DisplayName
UserPrincipalName = $upn
MailNickname = $upn.Split("@")[0]
AccountEnabled = $true
Department = $user.Department
JobTitle = $user.JobTitle
UsageLocation = "US" # Required for license assignment
PasswordProfile = @{
Password = $userPassword
ForceChangePasswordNextSignIn = $true
ForceChangePasswordNextSignInWithMfa = $true
}
}
try {
# Create user
$newUser = New-MgUser -BodyParameter $userParams
Write-Host "Created: $upn" -ForegroundColor Green
# Assign license
$licenseParams = @{
AddLicenses = @(@{ SkuId = $license.SkuId })
RemoveLicenses = @()
}
Set-MgUserLicense -UserId $newUser.Id -BodyParameter $licenseParams
Write-Host " License assigned: $LicenseSku" -ForegroundColor Cyan
$successCount++
$results += [PSCustomObject]@{
UserPrincipalName = $upn
Status = "Success"
Password = $userPassword
Message = "Created and licensed"
}
} catch {
Write-Host "Error for $upn : $_" -ForegroundColor Red
$errorCount++
$results += [PSCustomObject]@{
UserPrincipalName = $upn
Status = "Failed"
Password = ""
Message = $_.Exception.Message
}
}
}
# Export results
if (-not $WhatIf) {
$resultsPath = "UserProvisioning_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
$results | Export-Csv -Path $resultsPath -NoTypeInformation
Write-Host "`nResults saved to: $resultsPath" -ForegroundColor Green
Write-Host "Success: $successCount | Errors: $errorCount"
}
Disconnect-MgGraphCSV Format:
DisplayName,UserPrincipalName,Department,JobTitle
John Smith,[email protected],Engineering,Developer
Jane Doe,[email protected],Marketing,ManagerUser Offboarding
Secure user offboarding with mailbox conversion and access removal.
<#
.SYNOPSIS
Secure User Offboarding
.DESCRIPTION
Performs secure offboarding: disables account, revokes sessions,
converts mailbox to shared, removes licenses, sets forwarding.
.PARAMETER UserPrincipalName
UPN of user to offboard
.PARAMETER ForwardTo
Email to forward messages to (optional)
.PARAMETER RetainMailbox
Keep mailbox as shared (default: true)
#>
#Requires -Modules Microsoft.Graph, ExchangeOnlineManagement
param(
[Parameter(Mandatory)]
[string]$UserPrincipalName,
[string]$ForwardTo,
[switch]$RetainMailbox = $true,
[switch]$WhatIf
)
Connect-MgGraph -Scopes "User.ReadWrite.All", "Directory.ReadWrite.All"
Connect-ExchangeOnline
Write-Host "Starting offboarding for: $UserPrincipalName" -ForegroundColor Cyan
$user = Get-MgUser -UserId $UserPrincipalName -ErrorAction SilentlyContinue
if (-not $user) {
Write-Host "User not found: $UserPrincipalName" -ForegroundColor Red
exit 1
}
$actions = @()
# 1. Disable account
if (-not $WhatIf) {
Update-MgUser -UserId $user.Id -AccountEnabled:$false
}
$actions += "Disabled account"
Write-Host "[1/6] Account disabled" -ForegroundColor Green
# 2. Revoke all sessions
if (-not $WhatIf) {
Revoke-MgUserSignInSession -UserId $user.Id
}
$actions += "Revoked all sessions"
Write-Host "[2/6] Sessions revoked" -ForegroundColor Green
# 3. Reset password
$newPassword = -join ((65..90) + (97..122) + (48..57) | Get-Random -Count 32 | ForEach-Object { [char]$_ })
if (-not $WhatIf) {
$passwordProfile = @{
Password = $newPassword
ForceChangePasswordNextSignIn = $true
}
Update-MgUser -UserId $user.Id -PasswordProfile $passwordProfile
}
$actions += "Reset password"
Write-Host "[3/6] Password reset" -ForegroundColor Green
# 4. Remove from groups
$groups = Get-MgUserMemberOf -UserId $user.Id -All
$groupCount = 0
foreach ($group in $groups) {
if ($group.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.group') {
if (-not $WhatIf) {
Remove-MgGroupMemberByRef -GroupId $group.Id -DirectoryObjectId $user.Id -ErrorAction SilentlyContinue
}
$groupCount++
}
}
$actions += "Removed from $groupCount groups"
Write-Host "[4/6] Removed from $groupCount groups" -ForegroundColor Green
# 5. Convert mailbox to shared (if retaining)
if ($RetainMailbox) {
if (-not $WhatIf) {
Set-Mailbox -Identity $UserPrincipalName -Type Shared
}
$actions += "Converted mailbox to shared"
Write-Host "[5/6] Mailbox converted to shared" -ForegroundColor Green
# Set forwarding if specified
if ($ForwardTo) {
if (-not $WhatIf) {
Set-Mailbox -Identity $UserPrincipalName -ForwardingAddress $ForwardTo
}
$actions += "Mail forwarding set to $ForwardTo"
Write-Host " Forwarding to: $ForwardTo" -ForegroundColor Cyan
}
} else {
Write-Host "[5/6] Mailbox retention skipped" -ForegroundColor Yellow
}
# 6. Remove licenses
$licenses = Get-MgUserLicenseDetail -UserId $user.Id
if ($licenses -and -not $WhatIf) {
$licenseParams = @{
AddLicenses = @()
RemoveLicenses = $licenses.SkuId
}
Set-MgUserLicense -UserId $user.Id -BodyParameter $licenseParams
}
$actions += "Removed $($licenses.Count) licenses"
Write-Host "[6/6] Removed $($licenses.Count) licenses" -ForegroundColor Green
# Summary
Write-Host "`n=== Offboarding Complete ===" -ForegroundColor Green
Write-Host "User: $UserPrincipalName"
Write-Host "Actions taken:"
$actions | ForEach-Object { Write-Host " - $_" }
if ($WhatIf) {
Write-Host "`n[WhatIf] No changes were made" -ForegroundColor Yellow
}
Disconnect-MgGraph
Disconnect-ExchangeOnline -Confirm:$falseLicense Management
Analyze license usage and optimize allocation.
<#
.SYNOPSIS
License Usage Analysis and Optimization
.DESCRIPTION
Analyzes current license usage and identifies optimization opportunities.
#>
#Requires -Modules Microsoft.Graph
Connect-MgGraph -Scopes "Directory.Read.All", "User.Read.All"
Write-Host "Analyzing License Usage..." -ForegroundColor Cyan
$licenses = Get-MgSubscribedSku -All
$report = foreach ($lic in $licenses) {
$available = $lic.PrepaidUnits.Enabled - $lic.ConsumedUnits
$utilization = [math]::Round(($lic.ConsumedUnits / [math]::Max($lic.PrepaidUnits.Enabled, 1)) * 100, 1)
[PSCustomObject]@{
ProductName = $lic.SkuPartNumber
Total = $lic.PrepaidUnits.Enabled
Assigned = $lic.ConsumedUnits
Available = $available
Utilization = "$utilization%"
Status = if ($utilization -gt 90) { "Critical" }
elseif ($utilization -gt 75) { "Warning" }
elseif ($utilization -lt 50) { "Underutilized" }
else { "Healthy" }
}
}
$report | Format-Table -AutoSize
# Find users with unused licenses
Write-Host "`nChecking for inactive licensed users..." -ForegroundColor Yellow
$inactiveDate = (Get-Date).AddDays(-90)
$inactiveLicensed = Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,SignInActivity,AssignedLicenses |
Where-Object {
$_.AssignedLicenses.Count -gt 0 -and
$_.SignInActivity.LastSignInDateTime -and
$_.SignInActivity.LastSignInDateTime -lt $inactiveDate
} |
Select-Object DisplayName, UserPrincipalName,
@{N='LastSignIn';E={$_.SignInActivity.LastSignInDateTime}},
@{N='LicenseCount';E={$_.AssignedLicenses.Count}}
if ($inactiveLicensed) {
Write-Host "Found $($inactiveLicensed.Count) inactive users with licenses:" -ForegroundColor Yellow
$inactiveLicensed | Format-Table -AutoSize
} else {
Write-Host "No inactive licensed users found" -ForegroundColor Green
}
# Export
$report | Export-Csv -Path "LicenseAnalysis_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
Disconnect-MgGraphDNS Records Configuration
Generate DNS records for custom domain setup.
<#
.SYNOPSIS
Generate DNS Records for Microsoft 365
.DESCRIPTION
Outputs required DNS records for custom domain verification and services.
.PARAMETER Domain
Custom domain name
#>
param(
[Parameter(Mandatory)]
[string]$Domain
)
Write-Host "DNS Records for: $Domain" -ForegroundColor Cyan
Write-Host "=" * 60
Write-Host "`n### MX Record (Email)" -ForegroundColor Yellow
Write-Host "Type: MX"
Write-Host "Host: @"
Write-Host "Points to: $Domain.mail.protection.outlook.com"
Write-Host "Priority: 0"
Write-Host "`n### SPF Record (Email Authentication)" -ForegroundColor Yellow
Write-Host "Type: TXT"
Write-Host "Host: @"
Write-Host "Value: v=spf1 include:spf.protection.outlook.com -all"
Write-Host "`n### Autodiscover (Outlook Configuration)" -ForegroundColor Yellow
Write-Host "Type: CNAME"
Write-Host "Host: autodiscover"
Write-Host "Points to: autodiscover.outlook.com"
Write-Host "`n### DKIM Records (Email Signing)" -ForegroundColor Yellow
$domainKey = $Domain.Replace(".", "-")
Write-Host "Type: CNAME"
Write-Host "Host: selector1._domainkey"
Write-Host "Points to: selector1-$domainKey._domainkey.{tenant}.onmicrosoft.com"
Write-Host ""
Write-Host "Type: CNAME"
Write-Host "Host: selector2._domainkey"
Write-Host "Points to: selector2-$domainKey._domainkey.{tenant}.onmicrosoft.com"
Write-Host "`n### DMARC Record (Email Policy)" -ForegroundColor Yellow
Write-Host "Type: TXT"
Write-Host "Host: _dmarc"
Write-Host "Value: v=DMARC1; p=quarantine; rua=mailto:dmarc@$Domain"
Write-Host "`n### Teams/Skype Records" -ForegroundColor Yellow
Write-Host "Type: CNAME"
Write-Host "Host: sip"
Write-Host "Points to: sipdir.online.lync.com"
Write-Host ""
Write-Host "Type: CNAME"
Write-Host "Host: lyncdiscover"
Write-Host "Points to: webdir.online.lync.com"
Write-Host ""
Write-Host "Type: SRV"
Write-Host "Service: _sip._tls"
Write-Host "Port: 443"
Write-Host "Target: sipdir.online.lync.com"
Write-Host ""
Write-Host "Type: SRV"
Write-Host "Service: _sipfederationtls._tcp"
Write-Host "Port: 5061"
Write-Host "Target: sipfed.online.lync.com"
Write-Host "`n### MDM Enrollment (Intune)" -ForegroundColor Yellow
Write-Host "Type: CNAME"
Write-Host "Host: enterpriseregistration"
Write-Host "Points to: enterpriseregistration.windows.net"
Write-Host ""
Write-Host "Type: CNAME"
Write-Host "Host: enterpriseenrollment"
Write-Host "Points to: enterpriseenrollment.manage.microsoft.com"
Write-Host "`n" + "=" * 60 -ForegroundColor Cyan
Write-Host "Verify DNS propagation: nslookup -type=mx $Domain"
Write-Host "Note: DNS changes may take 24-48 hours to propagate" Security Policies Reference
Comprehensive security configuration guide for Microsoft 365 tenants covering Conditional Access, MFA, DLP, and security baselines.
Table of Contents
- Conditional Access Policies
- Multi-Factor Authentication
- Data Loss Prevention
- Security Baselines
- Admin Role Security
- Guest Access Controls
Conditional Access Policies
Policy Architecture
| Policy Type | Target Users | Applications | Grant Control |
|---|---|---|---|
| Admin MFA | Admin roles | All apps | Require MFA |
| User MFA | All users | All apps | Require MFA |
| Device Compliance | All users | Office 365 | Compliant device |
| Location-Based | All users | All apps | Block non-trusted |
| Legacy Auth Block | All users | All apps | Block |
Recommended Policies
1. Require MFA for Administrators
Scope: Global Admin, Security Admin, Exchange Admin, SharePoint Admin, User Admin
Settings:
- Include: Directory roles (admin roles)
- Exclude: Emergency access accounts
- Grant: Require MFA
- Session: Sign-in frequency 4 hours
2. Require MFA for All Users
Scope: All users
Settings:
- Include: All users
- Exclude: Emergency access accounts, service accounts
- Conditions: All cloud apps
- Grant: Require MFA
- Session: Persistent browser session disabled
3. Block Legacy Authentication
Scope: All users
Settings:
- Include: All users
- Conditions: Exchange ActiveSync, Other clients
- Grant: Block access
Why: Legacy protocols (POP, IMAP, SMTP AUTH) cannot enforce MFA.
4. Require Compliant Devices
Scope: All users accessing sensitive data
Settings:
- Include: All users
- Applications: Office 365, SharePoint, Exchange
- Grant: Require device compliance OR Hybrid Azure AD joined
- Platforms: Windows, macOS, iOS, Android
5. Block Access from Untrusted Locations
Scope: High-risk operations
Settings:
- Include: All users
- Applications: Azure Management, Microsoft Graph
- Conditions: Exclude named locations (corporate IPs)
- Grant: Block access
Named Locations Configuration
| Location Name | Type | IP Ranges |
|---|---|---|
| Corporate HQ | IP ranges | 203.0.113.0/24 |
| VPN Exit Points | IP ranges | 198.51.100.0/24 |
| Trusted Countries | Countries | US, CA, GB |
| Blocked Countries | Countries | (high-risk regions) |
Policy Deployment Strategy
Report-Only Mode (Week 1-2)
- Enable policies in report-only
- Monitor sign-in logs for impact
- Identify false positives
Pilot Group (Week 3-4)
- Enable for IT staff first
- Address issues before broad rollout
- Document exceptions needed
Gradual Rollout (Week 5-8)
- Enable by department
- Provide user communication
- Monitor help desk tickets
Full Enforcement
- Enable for all users
- Maintain exception process
- Quarterly policy review
Multi-Factor Authentication
MFA Methods (Strength Ranking)
| Method | Security Level | User Experience |
|---|---|---|
| FIDO2 Security Keys | Highest | Excellent |
| Windows Hello | Highest | Excellent |
| Microsoft Authenticator (Passwordless) | High | Good |
| Microsoft Authenticator (Push) | High | Good |
| OATH Hardware Token | High | Fair |
| SMS/Voice | Medium | Good |
| Email OTP | Low | Fair |
Recommended Configuration
For Administrators:
- Require phishing-resistant MFA (FIDO2, Windows Hello)
- Disable SMS/Voice as backup
- Enforce re-authentication every 4 hours
For Standard Users:
- Require Microsoft Authenticator
- Allow SMS as backup (temporary)
- Session lifetime: 90 days with risk-based re-auth
For External/Guest Users:
- Require MFA from home tenant
- Fall back to email OTP if needed
MFA Registration Campaign
Phase 1: Communication (Week 1)
- Announce MFA requirement
- Provide registration instructions
- Set deadline for registration
Phase 2: Registration (Week 2-3)
- Open registration portal
- IT support available
- Track registration progress
Phase 3: Enforcement (Week 4)
- Enable MFA requirement
- Grace period for stragglers
- Block unregistered after deadlineData Loss Prevention
Sensitive Information Types
| Category | Examples | Action |
|---|---|---|
| Financial | Credit card, Bank account | Block external sharing |
| PII | SSN, Passport, Driver's license | Require justification |
| Health | Medical records, Insurance | Block and notify |
| Credentials | Passwords, API keys | Block all sharing |
DLP Policy Templates
Financial Data Protection
Scope: Exchange, SharePoint, OneDrive, Teams
Rules:
- Credit card numbers (Luhn validated)
- Bank account numbers
- SWIFT codes
Actions:
- Block external sharing
- Encrypt email to external recipients
- Notify compliance team
PII Protection
Scope: All Microsoft 365 locations
Rules:
- Social Security Numbers
- Passport numbers
- Driver's license numbers
Actions:
- Warn user before sharing
- Require business justification
- Log all incidents
Healthcare (HIPAA)
Scope: Exchange, SharePoint, Teams
Rules:
- Medical record numbers
- Health insurance IDs
- Drug names with patient info
Actions:
- Block external sharing
- Apply encryption
- Retain for 7 years
DLP Deployment
Audit Mode First
- Enable policies in test mode
- Review matched content
- Tune false positives
User Tips
- Enable policy tips in apps
- Educate before enforcing
- Provide override option with justification
Enforcement
- Block high-risk content
- Warn for medium-risk
- Log everything
Security Baselines
Microsoft Secure Score Targets
| Category | Target Score | Key Actions |
|---|---|---|
| Identity | 80%+ | MFA, Conditional Access, PIM |
| Data | 70%+ | DLP, Sensitivity labels, Encryption |
| Device | 75%+ | Compliance policies, Defender |
| Apps | 70%+ | OAuth app review, Admin consent |
Priority Security Settings
Identity (Do First)
- Enable Security Defaults OR Conditional Access
- Require MFA for all admins
- Block legacy authentication
- Enable self-service password reset
- Configure password protection (banned passwords)
Data Protection
- Enable sensitivity labels
- Configure DLP policies
- Enable audit logging
- Set retention policies
- Configure information barriers (if needed)
Device Security
- Require device compliance
- Enable Microsoft Defender for Endpoint
- Configure BitLocker requirements
- Set application protection policies
- Enable Windows Autopilot
Application Security
- Review OAuth app permissions
- Configure admin consent workflow
- Block risky OAuth apps
- Enable app governance
- Configure MCAS policies
Admin Role Security
Privileged Identity Management (PIM)
Configuration:
- Require approval for Global Admin activation
- Maximum activation: 8 hours
- Require MFA at activation
- Require justification
- Send notification to security team
Role Assignment Best Practices
| Role | Assignment Type | Approval Required |
|---|---|---|
| Global Admin | Eligible only | Yes |
| Security Admin | Eligible only | Yes |
| User Admin | Eligible | No |
| Help Desk Admin | Permanent (limited) | No |
Emergency Access Accounts
Configuration:
- 2 cloud-only accounts
- Excluded from ALL Conditional Access
- No MFA (break-glass scenario)
- Monitored via alerts
- Passwords in secure vault
- Test quarterly
Naming: [email protected]
Guest Access Controls
Guest Invitation Settings
| Setting | Recommended Value |
|---|---|
| Guest invite restrictions | Admins and users in guest inviter role |
| Enable guest self-service sign-up | No |
| Enable email one-time passcode | Yes |
| Collaboration restrictions | Allow invitations only to specified domains |
Guest Access Review
Frequency: Quarterly
Scope:
- All guest users
- Group memberships
- Application access
Actions:
- Remove inactive guests (90+ days)
- Revoke unnecessary permissions
- Require re-certification
B2B Collaboration Settings
Allowed Domains:
- Partners:
partner1.com,partner2.com - Block all others for sensitive resources
Guest Permissions:
- Limited directory browsing
- Cannot enumerate users
- Cannot invite other guests
Troubleshooting Guide
Common issues and solutions for Microsoft 365 tenant administration.
Table of Contents
- Authentication Errors
- PowerShell Module Issues
- Permission Problems
- License Assignment Failures
- DNS and Domain Issues
- Conditional Access Lockouts
- Mailbox Issues
Authentication Errors
"AADSTS50076: MFA Required"
Cause: User requires MFA but hasn't completed it.
Solutions:
- Complete MFA registration at https://aka.ms/mfasetup
- Use interactive authentication:
Connect-MgGraph -Scopes "User.Read.All" -UseDeviceAuthentication - Check Conditional Access policies excluding the user
"AADSTS65001: User hasn't consented"
Cause: Application requires permissions user hasn't granted.
Solutions:
- Grant admin consent in Azure AD portal
- Use admin account for initial consent:
Connect-MgGraph -Scopes "User.ReadWrite.All" -ContextScope Process - Add application to enterprise applications with pre-consent
"AADSTS700016: Application not found"
Cause: App registration missing or incorrect tenant.
Solutions:
- Verify app ID in Azure AD > App registrations
- Check multi-tenant setting if cross-tenant
- Re-register application if needed
"Access Denied" Despite Admin Role
Causes:
- PIM role not activated
- Role assignment pending
- Conditional Access blocking
Solutions:
- Activate PIM role:
- Go to Azure AD > Privileged Identity Management
- Activate required role
- Wait 5-10 minutes for role propagation
- Check Conditional Access policies in report-only mode
PowerShell Module Issues
Module Not Found
Error: The term 'Connect-MgGraph' is not recognized
Solutions:
# Install module
Install-Module Microsoft.Graph -Scope CurrentUser -Force
# If already installed, import explicitly
Import-Module Microsoft.Graph
# Check installation
Get-InstalledModule Microsoft.GraphModule Version Conflicts
Error: Assembly with same name already loaded
Solutions:
# Remove all versions
Get-Module Microsoft.Graph* | Remove-Module -Force
# Clear cache
Remove-Item "$env:USERPROFILE\.local\share\powershell\*" -Recurse -Force
# Reinstall
Install-Module Microsoft.Graph -Force -AllowClobberExchange Online Connection Failures
Error: Connecting to remote server failed
Solutions:
# Use modern authentication
Connect-ExchangeOnline -UserPrincipalName [email protected]
# If MFA issues, use device code
Connect-ExchangeOnline -Device
# Check WinRM service
Get-Service WinRM | Start-ServiceGraph API Throttling
Error: 429 Too Many Requests
Solutions:
- Implement retry logic:
$retryCount = 0 $maxRetries = 3 do { try { $result = Get-MgUser -All break } catch { if ($_.Exception.Response.StatusCode -eq 429) { $retryAfter = $_.Exception.Response.Headers['Retry-After'] Start-Sleep -Seconds ([int]$retryAfter + 5) $retryCount++ } else { throw } } } while ($retryCount -lt $maxRetries) - Reduce batch sizes
- Use delta queries for incremental updates
Permission Problems
Insufficient Privileges for User Creation
Error: Insufficient privileges to complete the operation
Required Permissions:
- User Administrator role
- OR User.ReadWrite.All Graph permission
Solutions:
- Verify role assignment:
Get-MgDirectoryRoleMember -DirectoryRoleId (Get-MgDirectoryRole -Filter "displayName eq 'User Administrator'").Id - Request role assignment or PIM activation
- Use service principal with appropriate permissions
Cannot Modify Another Admin
Error: Cannot update privileged user
Cause: Attempting to modify user with equal or higher privileges.
Solutions:
- Use account with higher privilege level
- Global Admin required to modify other Global Admins
- Remove target's admin role first (if appropriate)
Application Permission vs Delegated
Issue: Script works interactively but fails in automation
Solution: Use application permissions for automation:
# Application authentication (daemon/service)
$clientId = "app-id"
$tenantId = "tenant-id"
$clientSecret = ConvertTo-SecureString "secret" -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential($clientId, $clientSecret)
Connect-MgGraph -ClientSecretCredential $credential -TenantId $tenantIdLicense Assignment Failures
"Usage location must be specified"
Error: License assignment failed because UsageLocation is not set
Solution:
# Set usage location before license assignment
Update-MgUser -UserId [email protected] -UsageLocation "US"
# Then assign license
$license = @{
AddLicenses = @(@{SkuId = "sku-id"})
RemoveLicenses = @()
}
Set-MgUserLicense -UserId [email protected] -BodyParameter $license"No available licenses"
Error: License quota exceeded
Solutions:
- Check available licenses:
Get-MgSubscribedSku | Select-Object SkuPartNumber, @{N='Available';E={$_.PrepaidUnits.Enabled - $_.ConsumedUnits}} - Remove licenses from inactive users
- Purchase additional licenses
Conflicting Service Plans
Error: Conflicting service plans
Cause: User has license with overlapping services.
Solution:
# Check current licenses
Get-MgUserLicenseDetail -UserId [email protected] |
Select-Object SkuPartNumber, @{N='Plans';E={$_.ServicePlans.ServicePlanName}}
# Remove conflicting license first
$remove = @{
AddLicenses = @()
RemoveLicenses = @("conflicting-sku-id")
}
Set-MgUserLicense -UserId [email protected] -BodyParameter $remove
# Then add new licenseDNS and Domain Issues
Domain Verification Failing
Error: Domain verification record not found
Solutions:
- Verify TXT record:
nslookup -type=TXT domain.com - Check for typos in record value
- Wait 24-48 hours for propagation
- Try alternate verification (MX record)
MX Record Not Resolving
Error: Mail flow disrupted
Diagnostic:
nslookup -type=MX domain.com
# Should return: domain.com.mail.protection.outlook.comSolutions:
- Verify MX record points to
domain.com.mail.protection.outlook.com - Priority should be 0 or lowest number
- Remove conflicting MX records
SPF Record Issues
Error: SPF validation failed
Correct SPF:
v=spf1 include:spf.protection.outlook.com -allCommon Mistakes:
- Multiple SPF records (only one allowed)
- Missing
-allor using~all - Too many DNS lookups (max 10)
Check:
nslookup -type=TXT domain.com | findstr spfConditional Access Lockouts
Locked Out by MFA Policy
Symptoms: Cannot sign in, MFA loop
Immediate Actions:
- Use emergency access account
- Sign in from trusted location/device
- Contact admin to temporarily exclude user
Resolution:
# Add user to CA exclusion group
$group = Get-MgGroup -Filter "displayName eq 'CA-Excluded-Users'"
New-MgGroupMember -GroupId $group.Id -DirectoryObjectId (Get-MgUser -UserId [email protected]).IdPolicy Conflicts
Symptoms: Unexpected blocks, inconsistent behavior
Diagnostic:
- Check sign-in logs: Azure AD > Sign-in logs
- Filter by user, check "Conditional Access" tab
- Review which policies applied/failed
Resolution:
- Review all policies in report-only mode
- Check for conflicting conditions
- Ensure proper policy ordering
Break-Glass Procedure
When to use: Complete admin lockout
Steps:
- Sign in with emergency access account
- Go to Azure AD > Security > Conditional Access
- Set all policies to "Report-only"
- Diagnose and fix root cause
- Re-enable policies gradually
Mailbox Issues
Mailbox Not Provisioning
Error: Mailbox doesn't exist
Causes:
- License not assigned
- License assignment pending
- User created without Exchange license
Solutions:
- Verify license:
Get-MgUserLicenseDetail -UserId [email protected] - Wait 5-10 minutes after license assignment
- Force mailbox provisioning:
# Reassign license Set-MgUserLicense -UserId [email protected] -BodyParameter @{ RemoveLicenses = @("sku-id") AddLicenses = @() } Start-Sleep -Seconds 60 Set-MgUserLicense -UserId [email protected] -BodyParameter @{ AddLicenses = @(@{SkuId = "sku-id"}) RemoveLicenses = @() }
Mailbox Size Limit
Error: Mailbox quota exceeded
Solutions:
# Check current quota
Get-Mailbox [email protected] | Select-Object ProhibitSendQuota, ProhibitSendReceiveQuota
# Increase quota (if license allows)
Set-Mailbox [email protected] -ProhibitSendQuota 99GB -ProhibitSendReceiveQuota 100GB
# Or enable archive
Enable-Mailbox [email protected] -ArchiveMail Flow Issues
Diagnostic:
# Test mail flow
Test-Mailflow -TargetEmailAddress [email protected]
# Check mail flow rules
Get-TransportRule | Where-Object {$_.State -eq 'Enabled'} | Select-Object Name, Priority, Conditions
# Check connectors
Get-InboundConnector
Get-OutboundConnectorCommon Fixes:
- Check transport rules for blocks
- Verify connector configuration
- Check ATP/spam policies
- Review quarantine for false positives
{
"task": "initial_tenant_setup",
"tenant_config": {
"company_name": "Acme Corporation",
"domain_name": "acme.com",
"user_count": 75,
"industry": "technology",
"compliance_requirements": ["GDPR"],
"licenses": {
"E5": 5,
"E3": 15,
"Business_Standard": 50,
"Business_Basic": 5
}
},
"admin_details": {
"primary_admin_email": "[email protected]",
"timezone": "Pacific Standard Time",
"country": "US"
}
}
"""
PowerShell script generator for Microsoft 365 administration tasks.
Creates ready-to-use scripts with error handling and best practices.
"""
from typing import Dict, List, Any, Optional
class PowerShellScriptGenerator:
"""Generate PowerShell scripts for common Microsoft 365 admin tasks."""
def __init__(self, tenant_domain: str):
"""
Initialize generator with tenant domain.
Args:
tenant_domain: Primary domain of the Microsoft 365 tenant
"""
self.tenant_domain = tenant_domain
def generate_conditional_access_policy_script(self, policy_config: Dict[str, Any]) -> str:
"""
Generate script to create Conditional Access policy.
Args:
policy_config: Policy configuration parameters
Returns:
PowerShell script
"""
policy_name = policy_config.get('name', 'MFA Policy')
require_mfa = policy_config.get('require_mfa', True)
include_users = policy_config.get('include_users', 'All')
exclude_users = policy_config.get('exclude_users', [])
script = f"""<#
.SYNOPSIS
Create Conditional Access Policy: {policy_name}
.DESCRIPTION
Creates a Conditional Access policy with specified settings.
Policy will be created in report-only mode for testing.
#>
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Policy.ReadWrite.ConditionalAccess"
# Define policy parameters
$policyName = "{policy_name}"
# Create Conditional Access Policy
$conditions = @{{
Users = @{{
IncludeUsers = @("{include_users}")
"""
if exclude_users:
exclude_list = '", "'.join(exclude_users)
script += f""" ExcludeUsers = @("{exclude_list}")
"""
script += """ }
Applications = @{
IncludeApplications = @("All")
}
Locations = @{
IncludeLocations = @("All")
}
}
$grantControls = @{
"""
if require_mfa:
script += """ BuiltInControls = @("mfa")
Operator = "OR"
"""
script += """}
$policy = @{
DisplayName = $policyName
State = "enabledForReportingButNotEnforced" # Start in report-only mode
Conditions = $conditions
GrantControls = $grantControls
}
try {
$newPolicy = New-MgIdentityConditionalAccessPolicy -BodyParameter $policy
Write-Host "✓ Conditional Access policy created: $($newPolicy.DisplayName)" -ForegroundColor Green
Write-Host " Policy ID: $($newPolicy.Id)" -ForegroundColor Cyan
Write-Host " State: Report-only (test before enforcing)" -ForegroundColor Yellow
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Cyan
Write-Host "1. Review policy in Azure AD > Security > Conditional Access"
Write-Host "2. Monitor sign-in logs for impact assessment"
Write-Host "3. When ready, change state to 'enabled' to enforce"
} catch {
Write-Host "✗ Error creating policy: $_" -ForegroundColor Red
}
Disconnect-MgGraph
"""
return script
def generate_security_audit_script(self) -> str:
"""
Generate comprehensive security audit script.
Returns:
PowerShell script for security assessment
"""
script = """<#
.SYNOPSIS
Microsoft 365 Security Audit Report
.DESCRIPTION
Performs comprehensive security audit and generates detailed report.
Checks: MFA status, admin accounts, inactive users, permissions, licenses
.OUTPUTS
CSV reports with security findings
#>
# Connect to services
Connect-MgGraph -Scopes "Directory.Read.All", "User.Read.All", "AuditLog.Read.All"
Connect-ExchangeOnline
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$reportPath = "SecurityAudit_$timestamp"
New-Item -ItemType Directory -Path $reportPath -Force | Out-Null
Write-Host "Starting Security Audit..." -ForegroundColor Cyan
Write-Host ""
# 1. Check MFA Status
Write-Host "[1/7] Checking MFA status for all users..." -ForegroundColor Yellow
$mfaReport = @()
$users = Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,AccountEnabled
foreach ($user in $users) {
$authMethods = Get-MgUserAuthenticationMethod -UserId $user.Id
$hasMFA = $authMethods.Count -gt 1 # More than just password
$mfaReport += [PSCustomObject]@{
UserPrincipalName = $user.UserPrincipalName
DisplayName = $user.DisplayName
AccountEnabled = $user.AccountEnabled
MFAEnabled = $hasMFA
AuthMethodsCount = $authMethods.Count
}
}
$mfaReport | Export-Csv -Path "$reportPath/MFA_Status.csv" -NoTypeInformation
$usersWithoutMFA = ($mfaReport | Where-Object { $_.MFAEnabled -eq $false -and $_.AccountEnabled -eq $true }).Count
Write-Host " Users without MFA: $usersWithoutMFA" -ForegroundColor $(if($usersWithoutMFA -gt 0){'Red'}else{'Green'})
# 2. Check Admin Accounts
Write-Host "[2/7] Auditing admin role assignments..." -ForegroundColor Yellow
$adminRoles = Get-MgDirectoryRole -All
$adminReport = @()
foreach ($role in $adminRoles) {
$members = Get-MgDirectoryRoleMember -DirectoryRoleId $role.Id
foreach ($member in $members) {
$user = Get-MgUser -UserId $member.Id -ErrorAction SilentlyContinue
if ($user) {
$adminReport += [PSCustomObject]@{
UserPrincipalName = $user.UserPrincipalName
DisplayName = $user.DisplayName
Role = $role.DisplayName
AccountEnabled = $user.AccountEnabled
}
}
}
}
$adminReport | Export-Csv -Path "$reportPath/Admin_Roles.csv" -NoTypeInformation
Write-Host " Total admin assignments: $($adminReport.Count)" -ForegroundColor Cyan
# 3. Check Inactive Users
Write-Host "[3/7] Identifying inactive users (90+ days)..." -ForegroundColor Yellow
$inactiveDate = (Get-Date).AddDays(-90)
$inactiveUsers = @()
foreach ($user in $users) {
$signIns = Get-MgAuditLogSignIn -Filter "userId eq '$($user.Id)'" -Top 1
$lastSignIn = if ($signIns) { $signIns[0].CreatedDateTime } else { $null }
if ($lastSignIn -and $lastSignIn -lt $inactiveDate -and $user.AccountEnabled) {
$inactiveUsers += [PSCustomObject]@{
UserPrincipalName = $user.UserPrincipalName
DisplayName = $user.DisplayName
LastSignIn = $lastSignIn
DaysSinceSignIn = ((Get-Date) - $lastSignIn).Days
}
}
}
$inactiveUsers | Export-Csv -Path "$reportPath/Inactive_Users.csv" -NoTypeInformation
Write-Host " Inactive users found: $($inactiveUsers.Count)" -ForegroundColor $(if($inactiveUsers.Count -gt 0){'Yellow'}else{'Green'})
# 4. Check Guest Users
Write-Host "[4/7] Reviewing guest user access..." -ForegroundColor Yellow
$guestUsers = Get-MgUser -Filter "userType eq 'Guest'" -All
$guestReport = $guestUsers | Select-Object UserPrincipalName, DisplayName, AccountEnabled, CreatedDateTime
$guestReport | Export-Csv -Path "$reportPath/Guest_Users.csv" -NoTypeInformation
Write-Host " Guest users: $($guestUsers.Count)" -ForegroundColor Cyan
# 5. Check License Usage
Write-Host "[5/7] Analyzing license allocation..." -ForegroundColor Yellow
$licenses = Get-MgSubscribedSku
$licenseReport = @()
foreach ($license in $licenses) {
$licenseReport += [PSCustomObject]@{
ProductName = $license.SkuPartNumber
TotalLicenses = $license.PrepaidUnits.Enabled
AssignedLicenses = $license.ConsumedUnits
AvailableLicenses = $license.PrepaidUnits.Enabled - $license.ConsumedUnits
UtilizationPercent = [math]::Round(($license.ConsumedUnits / $license.PrepaidUnits.Enabled) * 100, 2)
}
}
$licenseReport | Export-Csv -Path "$reportPath/License_Usage.csv" -NoTypeInformation
Write-Host " License SKUs analyzed: $($licenses.Count)" -ForegroundColor Cyan
# 6. Check Mailbox Permissions
Write-Host "[6/7] Auditing mailbox delegations..." -ForegroundColor Yellow
$mailboxes = Get-Mailbox -ResultSize Unlimited
$delegationReport = @()
foreach ($mailbox in $mailboxes) {
$permissions = Get-MailboxPermission -Identity $mailbox.Identity |
Where-Object { $_.User -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false }
foreach ($perm in $permissions) {
$delegationReport += [PSCustomObject]@{
Mailbox = $mailbox.UserPrincipalName
DelegatedTo = $perm.User
AccessRights = $perm.AccessRights -join ", "
}
}
}
$delegationReport | Export-Csv -Path "$reportPath/Mailbox_Delegations.csv" -NoTypeInformation
Write-Host " Delegated mailboxes: $($delegationReport.Count)" -ForegroundColor Cyan
# 7. Check Conditional Access Policies
Write-Host "[7/7] Reviewing Conditional Access policies..." -ForegroundColor Yellow
$caPolicies = Get-MgIdentityConditionalAccessPolicy
$caReport = $caPolicies | Select-Object DisplayName, State, CreatedDateTime,
@{N='IncludeUsers';E={$_.Conditions.Users.IncludeUsers -join '; '}},
@{N='RequiresMFA';E={$_.GrantControls.BuiltInControls -contains 'mfa'}}
$caReport | Export-Csv -Path "$reportPath/ConditionalAccess_Policies.csv" -NoTypeInformation
Write-Host " Conditional Access policies: $($caPolicies.Count)" -ForegroundColor Cyan
# Generate Summary Report
Write-Host ""
Write-Host "=== Security Audit Summary ===" -ForegroundColor Green
Write-Host ""
Write-Host "Users:" -ForegroundColor Cyan
Write-Host " Total Users: $($users.Count)"
Write-Host " Users without MFA: $usersWithoutMFA $(if($usersWithoutMFA -gt 0){'⚠️'}else{'✓'})"
Write-Host " Inactive Users (90+ days): $($inactiveUsers.Count) $(if($inactiveUsers.Count -gt 0){'⚠️'}else{'✓'})"
Write-Host " Guest Users: $($guestUsers.Count)"
Write-Host ""
Write-Host "Administration:" -ForegroundColor Cyan
Write-Host " Admin Role Assignments: $($adminReport.Count)"
Write-Host " Conditional Access Policies: $($caPolicies.Count)"
Write-Host ""
Write-Host "Licenses:" -ForegroundColor Cyan
foreach ($lic in $licenseReport) {
Write-Host " $($lic.ProductName): $($lic.AssignedLicenses)/$($lic.TotalLicenses) ($($lic.UtilizationPercent)%)"
}
Write-Host ""
Write-Host "Reports saved to: $reportPath" -ForegroundColor Green
Write-Host ""
Write-Host "Recommended Actions:" -ForegroundColor Yellow
if ($usersWithoutMFA -gt 0) {
Write-Host " 1. Enable MFA for users without MFA"
}
if ($inactiveUsers.Count -gt 0) {
Write-Host " 2. Review and disable inactive user accounts"
}
if ($guestUsers.Count -gt 10) {
Write-Host " 3. Review guest user access and remove unnecessary guests"
}
# Disconnect
Disconnect-MgGraph
Disconnect-ExchangeOnline -Confirm:$false
"""
return script
def generate_bulk_license_assignment_script(self, users_csv_path: str, license_sku: str) -> str:
"""
Generate script for bulk license assignment from CSV.
Args:
users_csv_path: Path to CSV with user emails
license_sku: License SKU to assign
Returns:
PowerShell script
"""
script = f"""<#
.SYNOPSIS
Bulk License Assignment from CSV
.DESCRIPTION
Assigns {license_sku} license to users listed in CSV file.
CSV must have 'UserPrincipalName' column.
.PARAMETER CsvPath
Path to CSV file with user list
#>
param(
[Parameter(Mandatory=$true)]
[string]$CsvPath = "{users_csv_path}"
)
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "User.ReadWrite.All", "Directory.ReadWrite.All"
# Get license SKU ID
$targetSku = "{license_sku}"
$licenseSkuId = (Get-MgSubscribedSku -All | Where-Object {{$_.SkuPartNumber -eq $targetSku}}).SkuId
if (-not $licenseSkuId) {{
Write-Host "✗ License SKU not found: $targetSku" -ForegroundColor Red
exit
}}
Write-Host "License SKU found: $targetSku" -ForegroundColor Green
Write-Host "SKU ID: $licenseSkuId" -ForegroundColor Cyan
Write-Host ""
# Import users from CSV
$users = Import-Csv -Path $CsvPath
if (-not $users) {{
Write-Host "✗ No users found in CSV file" -ForegroundColor Red
exit
}}
Write-Host "Found $($users.Count) users in CSV" -ForegroundColor Cyan
Write-Host ""
# Process each user
$successCount = 0
$errorCount = 0
$results = @()
foreach ($user in $users) {{
$userEmail = $user.UserPrincipalName
try {{
# Get user
$mgUser = Get-MgUser -UserId $userEmail -ErrorAction Stop
# Check if user already has license
$currentLicenses = Get-MgUserLicenseDetail -UserId $mgUser.Id
if ($currentLicenses.SkuId -contains $licenseSkuId) {{
Write-Host " ⊘ $userEmail - Already has license" -ForegroundColor Yellow
$results += [PSCustomObject]@{{
UserPrincipalName = $userEmail
Status = "Skipped"
Message = "Already licensed"
}}
continue
}}
# Assign license
$licenseParams = @{{
AddLicenses = @(
@{{
SkuId = $licenseSkuId
}}
)
}}
Set-MgUserLicense -UserId $mgUser.Id -BodyParameter $licenseParams
Write-Host " ✓ $userEmail - License assigned successfully" -ForegroundColor Green
$successCount++
$results += [PSCustomObject]@{{
UserPrincipalName = $userEmail
Status = "Success"
Message = "License assigned"
}}
}} catch {{
Write-Host " ✗ $userEmail - Error: $_" -ForegroundColor Red
$errorCount++
$results += [PSCustomObject]@{{
UserPrincipalName = $userEmail
Status = "Failed"
Message = $_.Exception.Message
}}
}}
}}
# Export results
$resultsPath = "LicenseAssignment_Results_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
$results | Export-Csv -Path $resultsPath -NoTypeInformation
# Summary
Write-Host ""
Write-Host "=== Summary ===" -ForegroundColor Cyan
Write-Host "Total users processed: $($users.Count)"
Write-Host "Successfully assigned: $successCount" -ForegroundColor Green
Write-Host "Errors: $errorCount" -ForegroundColor $(if($errorCount -gt 0){{'Red'}}else{{'Green'}})
Write-Host ""
Write-Host "Results saved to: $resultsPath" -ForegroundColor Cyan
# Disconnect
Disconnect-MgGraph
"""
return script
"""
Microsoft 365 tenant setup and configuration module.
Generates guidance and scripts for initial tenant configuration.
"""
from typing import Dict, List, Any, Optional
class TenantSetupManager:
"""Manage Microsoft 365 tenant setup and initial configuration."""
def __init__(self, tenant_config: Dict[str, Any]):
"""
Initialize with tenant configuration.
Args:
tenant_config: Dictionary containing tenant details and requirements
"""
self.company_name = tenant_config.get('company_name', '')
self.domain_name = tenant_config.get('domain_name', '')
self.user_count = tenant_config.get('user_count', 0)
self.industry = tenant_config.get('industry', 'general')
self.compliance_requirements = tenant_config.get('compliance_requirements', [])
self.licenses = tenant_config.get('licenses', {})
self.setup_steps = []
def generate_setup_checklist(self) -> List[Dict[str, Any]]:
"""
Generate comprehensive tenant setup checklist.
Returns:
List of setup steps with details and priorities
"""
checklist = []
# Phase 1: Initial Configuration
checklist.append({
'phase': 1,
'name': 'Initial Tenant Configuration',
'priority': 'critical',
'tasks': [
{
'task': 'Sign in to Microsoft 365 Admin Center',
'url': 'https://admin.microsoft.com',
'estimated_time': '5 minutes'
},
{
'task': 'Complete tenant setup wizard',
'details': 'Set organization profile, contact info, and preferences',
'estimated_time': '10 minutes'
},
{
'task': 'Configure company branding',
'details': 'Upload logo, set theme colors, customize sign-in page',
'estimated_time': '15 minutes'
}
]
})
# Phase 2: Domain Setup
checklist.append({
'phase': 2,
'name': 'Custom Domain Configuration',
'priority': 'critical',
'tasks': [
{
'task': 'Add custom domain',
'details': f'Add {self.domain_name} to tenant',
'estimated_time': '5 minutes'
},
{
'task': 'Verify domain ownership',
'details': 'Add TXT record to DNS: MS=msXXXXXXXX',
'estimated_time': '10 minutes (plus DNS propagation)'
},
{
'task': 'Configure DNS records',
'details': 'Add MX, CNAME, TXT records for services',
'estimated_time': '20 minutes'
},
{
'task': 'Set as default domain',
'details': f'Make {self.domain_name} the default for new users',
'estimated_time': '2 minutes'
}
]
})
# Phase 3: Security Baseline
checklist.append({
'phase': 3,
'name': 'Security Baseline Configuration',
'priority': 'critical',
'tasks': [
{
'task': 'Enable Security Defaults or Conditional Access',
'details': 'Enforce MFA and modern authentication',
'estimated_time': '15 minutes'
},
{
'task': 'Configure named locations',
'details': 'Define trusted IP ranges for office locations',
'estimated_time': '10 minutes'
},
{
'task': 'Set up admin accounts',
'details': 'Create separate admin accounts, enable PIM',
'estimated_time': '20 minutes'
},
{
'task': 'Enable audit logging',
'details': 'Turn on unified audit log for compliance',
'estimated_time': '5 minutes'
},
{
'task': 'Configure password policies',
'details': 'Set expiration, complexity, banned passwords',
'estimated_time': '10 minutes'
}
]
})
# Phase 4: Service Provisioning
checklist.append({
'phase': 4,
'name': 'Service Configuration',
'priority': 'high',
'tasks': [
{
'task': 'Configure Exchange Online',
'details': 'Set up mailboxes, mail flow, anti-spam policies',
'estimated_time': '30 minutes'
},
{
'task': 'Set up SharePoint Online',
'details': 'Configure sharing settings, storage limits, site templates',
'estimated_time': '25 minutes'
},
{
'task': 'Enable Microsoft Teams',
'details': 'Configure Teams policies, guest access, meeting settings',
'estimated_time': '20 minutes'
},
{
'task': 'Configure OneDrive for Business',
'details': 'Set storage quotas, sync restrictions, sharing policies',
'estimated_time': '15 minutes'
}
]
})
# Phase 5: Compliance (if required)
if self.compliance_requirements:
compliance_tasks = []
if 'GDPR' in self.compliance_requirements:
compliance_tasks.append({
'task': 'Configure GDPR compliance',
'details': 'Set up data residency, retention policies, DSR workflows',
'estimated_time': '45 minutes'
})
if 'HIPAA' in self.compliance_requirements:
compliance_tasks.append({
'task': 'Enable HIPAA compliance features',
'details': 'Configure encryption, audit logs, access controls',
'estimated_time': '40 minutes'
})
checklist.append({
'phase': 5,
'name': 'Compliance Configuration',
'priority': 'high',
'tasks': compliance_tasks
})
return checklist
def generate_dns_records(self) -> Dict[str, List[Dict[str, str]]]:
"""
Generate required DNS records for Microsoft 365 services.
Returns:
Dictionary of DNS record types and configurations
"""
domain = self.domain_name
return {
'mx_records': [
{
'type': 'MX',
'name': '@',
'value': f'{domain.replace(".", "-")}.mail.protection.outlook.com',
'priority': '0',
'ttl': '3600',
'purpose': 'Email delivery to Exchange Online'
}
],
'txt_records': [
{
'type': 'TXT',
'name': '@',
'value': 'v=spf1 include:spf.protection.outlook.com -all',
'ttl': '3600',
'purpose': 'SPF record for email authentication'
},
{
'type': 'TXT',
'name': '@',
'value': 'MS=msXXXXXXXX',
'ttl': '3600',
'purpose': 'Domain verification (replace XXXXXXXX with actual value)'
}
],
'cname_records': [
{
'type': 'CNAME',
'name': 'autodiscover',
'value': 'autodiscover.outlook.com',
'ttl': '3600',
'purpose': 'Outlook autodiscover for automatic email configuration'
},
{
'type': 'CNAME',
'name': 'selector1._domainkey',
'value': f'selector1-{domain.replace(".", "-")}._domainkey.onmicrosoft.com',
'ttl': '3600',
'purpose': 'DKIM signature for email security'
},
{
'type': 'CNAME',
'name': 'selector2._domainkey',
'value': f'selector2-{domain.replace(".", "-")}._domainkey.onmicrosoft.com',
'ttl': '3600',
'purpose': 'DKIM signature for email security (rotation)'
},
{
'type': 'CNAME',
'name': 'msoid',
'value': 'clientconfig.microsoftonline-p.net',
'ttl': '3600',
'purpose': 'Azure AD authentication'
},
{
'type': 'CNAME',
'name': 'enterpriseregistration',
'value': 'enterpriseregistration.windows.net',
'ttl': '3600',
'purpose': 'Device registration for Azure AD join'
},
{
'type': 'CNAME',
'name': 'enterpriseenrollment',
'value': 'enterpriseenrollment.manage.microsoft.com',
'ttl': '3600',
'purpose': 'Mobile device management (Intune)'
}
],
'srv_records': [
{
'type': 'SRV',
'name': '_sip._tls',
'value': 'sipdir.online.lync.com',
'port': '443',
'priority': '100',
'weight': '1',
'ttl': '3600',
'purpose': 'Skype for Business / Teams federation'
},
{
'type': 'SRV',
'name': '_sipfederationtls._tcp',
'value': 'sipfed.online.lync.com',
'port': '5061',
'priority': '100',
'weight': '1',
'ttl': '3600',
'purpose': 'Teams external federation'
}
]
}
def generate_powershell_setup_script(self) -> str:
"""
Generate PowerShell script for initial tenant configuration.
Returns:
Complete PowerShell script as string
"""
script = f"""<#
.SYNOPSIS
Microsoft 365 Tenant Initial Setup Script
Generated for: {self.company_name}
Domain: {self.domain_name}
.DESCRIPTION
This script performs initial Microsoft 365 tenant configuration.
Run this script with Global Administrator credentials.
.NOTES
Prerequisites:
- Install Microsoft.Graph module: Install-Module Microsoft.Graph -Scope CurrentUser
- Install ExchangeOnlineManagement: Install-Module ExchangeOnlineManagement
- Install MicrosoftTeams: Install-Module MicrosoftTeams
#>
# Connect to Microsoft 365 services
Write-Host "Connecting to Microsoft 365..." -ForegroundColor Cyan
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Organization.ReadWrite.All", "Directory.ReadWrite.All", "Policy.ReadWrite.ConditionalAccess"
# Connect to Exchange Online
Connect-ExchangeOnline
# Connect to Microsoft Teams
Connect-MicrosoftTeams
# Step 1: Configure organization settings
Write-Host "Configuring organization settings..." -ForegroundColor Green
$orgSettings = @{{
DisplayName = "{self.company_name}"
PreferredLanguage = "en-US"
}}
Update-MgOrganization -OrganizationId (Get-MgOrganization).Id -BodyParameter $orgSettings
# Step 2: Enable Security Defaults (or use Conditional Access for advanced)
Write-Host "Enabling Security Defaults (MFA)..." -ForegroundColor Green
# Uncomment to enable Security Defaults:
# Update-MgPolicyIdentitySecurityDefaultEnforcementPolicy -IsEnabled $true
# Step 3: Enable audit logging
Write-Host "Enabling unified audit log..." -ForegroundColor Green
Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $true
# Step 4: Configure Exchange Online settings
Write-Host "Configuring Exchange Online..." -ForegroundColor Green
# Set organization config
Set-OrganizationConfig -DefaultPublicFolderAgeLimit 30
# Configure anti-spam policy
$antiSpamPolicy = @{{
Name = "Default Anti-Spam Policy"
SpamAction = "MoveToJmf" # Move to Junk folder
HighConfidenceSpamAction = "Quarantine"
BulkSpamAction = "MoveToJmf"
EnableEndUserSpamNotifications = $true
}}
# Step 5: Configure SharePoint Online settings
Write-Host "Configuring SharePoint Online..." -ForegroundColor Green
# Note: SharePoint management requires SharePointPnPPowerShellOnline module
# Connect-PnPOnline -Url "https://{self.domain_name.split('.')[0]}-admin.sharepoint.com" -Interactive
# Step 6: Configure Microsoft Teams settings
Write-Host "Configuring Microsoft Teams..." -ForegroundColor Green
# Set Teams messaging policy
$messagingPolicy = @{{
Identity = "Global"
AllowUserChat = $true
AllowUserDeleteMessage = $true
AllowGiphy = $true
GiphyRatingType = "Moderate"
}}
# Step 7: Summary
Write-Host "`nTenant setup complete!" -ForegroundColor Green
Write-Host "Next steps:" -ForegroundColor Cyan
Write-Host "1. Add and verify custom domain: {self.domain_name}"
Write-Host "2. Configure DNS records (see DNS configuration output)"
Write-Host "3. Create user accounts or set up AD Connect for hybrid"
Write-Host "4. Assign licenses to users"
Write-Host "5. Review and configure Conditional Access policies"
Write-Host "6. Complete compliance configuration if required"
# Disconnect from services
Disconnect-MgGraph
Disconnect-ExchangeOnline -Confirm:$false
Disconnect-MicrosoftTeams
"""
return script
def get_license_recommendations(self) -> Dict[str, Any]:
"""
Recommend appropriate Microsoft 365 licenses based on requirements.
Returns:
Dictionary with license recommendations
"""
recommendations = {
'basic_users': {
'license': 'Microsoft 365 Business Basic',
'features': ['Web versions of Office apps', 'Teams', 'OneDrive (1TB)', 'Exchange (50GB)'],
'cost_per_user_month': 6.00,
'recommended_for': 'Frontline workers, part-time staff'
},
'standard_users': {
'license': 'Microsoft 365 Business Standard',
'features': ['Desktop Office apps', 'Teams', 'OneDrive (1TB)', 'Exchange (50GB)', 'SharePoint'],
'cost_per_user_month': 12.50,
'recommended_for': 'Most office workers'
},
'advanced_security': {
'license': 'Microsoft 365 E3',
'features': ['All Business Standard features', 'Advanced security', 'Compliance tools', 'Azure AD P1'],
'cost_per_user_month': 36.00,
'recommended_for': 'Users handling sensitive data, compliance requirements'
},
'executives_admins': {
'license': 'Microsoft 365 E5',
'features': ['All E3 features', 'Advanced threat protection', 'Azure AD P2', 'Advanced compliance'],
'cost_per_user_month': 57.00,
'recommended_for': 'Executives, IT admins, high-risk users'
}
}
# Calculate recommended distribution
total_users = self.user_count
distribution = {
'E5': min(5, int(total_users * 0.05)), # 5% or 5 users, whichever is less
'E3': int(total_users * 0.20) if total_users > 50 else 0, # 20% for larger orgs
'Business_Standard': int(total_users * 0.70), # 70% standard users
'Business_Basic': int(total_users * 0.05) # 5% basic users
}
# Adjust for compliance requirements
if self.compliance_requirements:
distribution['E3'] = distribution['E3'] + distribution['Business_Standard'] // 2
distribution['Business_Standard'] = distribution['Business_Standard'] // 2
estimated_monthly_cost = (
distribution['E5'] * 57.00 +
distribution['E3'] * 36.00 +
distribution['Business_Standard'] * 12.50 +
distribution['Business_Basic'] * 6.00
)
return {
'recommendations': recommendations,
'suggested_distribution': distribution,
'estimated_monthly_cost': round(estimated_monthly_cost, 2),
'estimated_annual_cost': round(estimated_monthly_cost * 12, 2)
}
"""
User lifecycle management module for Microsoft 365.
Handles user creation, modification, license assignment, and deprovisioning.
"""
from typing import Dict, List, Any, Optional
from datetime import datetime
class UserLifecycleManager:
"""Manage Microsoft 365 user lifecycle operations."""
def __init__(self, domain: str):
"""
Initialize with tenant domain.
Args:
domain: Primary domain name for the tenant
"""
self.domain = domain
self.operations_log = []
def generate_user_creation_script(self, users: List[Dict[str, Any]]) -> str:
"""
Generate PowerShell script for bulk user creation.
Args:
users: List of user dictionaries with details
Returns:
PowerShell script for user provisioning
"""
script = """<#
.SYNOPSIS
Bulk User Provisioning Script for Microsoft 365
.DESCRIPTION
Creates multiple users, assigns licenses, and configures mailboxes.
.NOTES
Prerequisites:
- Install-Module Microsoft.Graph -Scope CurrentUser
- Install-Module ExchangeOnlineManagement
#>
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "User.ReadWrite.All", "Directory.ReadWrite.All", "Group.ReadWrite.All"
# Connect to Exchange Online
Connect-ExchangeOnline
# Define users to create
$users = @(
"""
for user in users:
upn = f"{user.get('username', '')}@{self.domain}"
display_name = user.get('display_name', '')
first_name = user.get('first_name', '')
last_name = user.get('last_name', '')
job_title = user.get('job_title', '')
department = user.get('department', '')
license_sku = user.get('license_sku', 'Microsoft_365_Business_Standard')
script += f""" @{{
UserPrincipalName = "{upn}"
DisplayName = "{display_name}"
GivenName = "{first_name}"
Surname = "{last_name}"
JobTitle = "{job_title}"
Department = "{department}"
LicenseSku = "{license_sku}"
UsageLocation = "US"
PasswordProfile = @{{
Password = "ChangeMe@$(Get-Random -Minimum 1000 -Maximum 9999)"
ForceChangePasswordNextSignIn = $true
}}
}}
"""
script += """
)
# Create users
foreach ($user in $users) {
try {
Write-Host "Creating user: $($user.DisplayName)..." -ForegroundColor Cyan
# Create user account
$newUser = New-MgUser -UserPrincipalName $user.UserPrincipalName `
-DisplayName $user.DisplayName `
-GivenName $user.GivenName `
-Surname $user.Surname `
-JobTitle $user.JobTitle `
-Department $user.Department `
-PasswordProfile $user.PasswordProfile `
-UsageLocation $user.UsageLocation `
-AccountEnabled $true `
-MailNickname ($user.UserPrincipalName -split '@')[0]
Write-Host " ✓ User created successfully" -ForegroundColor Green
# Wait for user provisioning
Start-Sleep -Seconds 5
# Assign license
$licenseParams = @{
AddLicenses = @(
@{
SkuId = (Get-MgSubscribedSku -All | Where-Object {$_.SkuPartNumber -eq $user.LicenseSku}).SkuId
}
)
}
Set-MgUserLicense -UserId $newUser.Id -BodyParameter $licenseParams
Write-Host " ✓ License assigned: $($user.LicenseSku)" -ForegroundColor Green
# Log success
$user | Add-Member -NotePropertyName "Status" -NotePropertyValue "Success" -Force
$user | Add-Member -NotePropertyName "CreatedDate" -NotePropertyValue (Get-Date) -Force
} catch {
Write-Host " ✗ Error creating user: $_" -ForegroundColor Red
$user | Add-Member -NotePropertyName "Status" -NotePropertyValue "Failed" -Force
$user | Add-Member -NotePropertyName "Error" -NotePropertyValue $_.Exception.Message -Force
}
}
# Export results
$users | Export-Csv -Path "UserCreation_Results_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv" -NoTypeInformation
# Disconnect
Disconnect-MgGraph
Disconnect-ExchangeOnline -Confirm:$false
Write-Host "`nUser provisioning complete!" -ForegroundColor Green
"""
return script
def generate_user_offboarding_script(self, user_email: str) -> str:
"""
Generate script for secure user offboarding.
Args:
user_email: Email address of user to offboard
Returns:
PowerShell script for offboarding
"""
script = f"""<#
.SYNOPSIS
User Offboarding Script - Secure Deprovisioning
.DESCRIPTION
Securely offboards user: {user_email}
- Revokes access and signs out all sessions
- Converts mailbox to shared (preserves emails)
- Removes licenses
- Archives OneDrive
- Documents all actions
#>
# Connect to services
Connect-MgGraph -Scopes "User.ReadWrite.All", "Directory.ReadWrite.All"
Connect-ExchangeOnline
$userEmail = "{user_email}"
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
Write-Host "Starting offboarding for: $userEmail" -ForegroundColor Cyan
try {{
# Step 1: Get user details
$user = Get-MgUser -UserId $userEmail
Write-Host "✓ User found: $($user.DisplayName)" -ForegroundColor Green
# Step 2: Disable sign-in (immediately revokes access)
Update-MgUser -UserId $user.Id -AccountEnabled $false
Write-Host "✓ Account disabled - user cannot sign in" -ForegroundColor Green
# Step 3: Revoke all active sessions
Revoke-MgUserSignInSession -UserId $user.Id
Write-Host "✓ All active sessions revoked" -ForegroundColor Green
# Step 4: Remove from all groups (except retained groups)
$groups = Get-MgUserMemberOf -UserId $user.Id
foreach ($group in $groups) {{
if ($group.AdditionalProperties["@odata.type"] -eq "#microsoft.graph.group") {{
Remove-MgGroupMemberByRef -GroupId $group.Id -DirectoryObjectId $user.Id
Write-Host " - Removed from group: $($group.AdditionalProperties.displayName)"
}}
}}
Write-Host "✓ Removed from all groups" -ForegroundColor Green
# Step 5: Remove mobile devices
$devices = Get-MgUserRegisteredDevice -UserId $user.Id
foreach ($device in $devices) {{
Remove-MgUserRegisteredDeviceByRef -UserId $user.Id -DirectoryObjectId $device.Id
Write-Host " - Removed device: $($device.AdditionalProperties.displayName)"
}}
Write-Host "✓ All mobile devices removed" -ForegroundColor Green
# Step 6: Convert mailbox to shared (preserves emails, removes license requirement)
Set-Mailbox -Identity $userEmail -Type Shared
Write-Host "✓ Mailbox converted to shared mailbox" -ForegroundColor Green
# Step 7: Set up email forwarding (optional - update recipient as needed)
# Set-Mailbox -Identity $userEmail -ForwardingAddress "manager@{self.domain}"
# Write-Host "✓ Email forwarding configured" -ForegroundColor Green
# Step 8: Set auto-reply
$autoReplyMessage = @"
Thank you for your email. This mailbox is no longer actively monitored as the employee has left the organization.
For assistance, please contact: support@{self.domain}
"@
Set-MailboxAutoReplyConfiguration -Identity $userEmail `
-AutoReplyState Enabled `
-InternalMessage $autoReplyMessage `
-ExternalMessage $autoReplyMessage
Write-Host "✓ Auto-reply configured" -ForegroundColor Green
# Step 9: Remove licenses (wait a bit after mailbox conversion)
Start-Sleep -Seconds 30
$licenses = Get-MgUserLicenseDetail -UserId $user.Id
if ($licenses) {{
$licenseParams = @{{
RemoveLicenses = @($licenses.SkuId)
}}
Set-MgUserLicense -UserId $user.Id -BodyParameter $licenseParams
Write-Host "✓ Licenses removed" -ForegroundColor Green
}}
# Step 10: Hide from GAL (Global Address List)
Set-Mailbox -Identity $userEmail -HiddenFromAddressListsEnabled $true
Write-Host "✓ Hidden from Global Address List" -ForegroundColor Green
# Step 11: Document offboarding
$offboardingReport = @{{
UserEmail = $userEmail
DisplayName = $user.DisplayName
OffboardingDate = Get-Date
MailboxStatus = "Converted to Shared"
LicensesRemoved = $licenses.SkuPartNumber -join ", "
AccountDisabled = $true
SessionsRevoked = $true
}}
$offboardingReport | Export-Csv -Path "Offboarding_${{userEmail}}_$timestamp.csv" -NoTypeInformation
Write-Host "`n✓ Offboarding completed successfully!" -ForegroundColor Green
Write-Host "`nNext steps:" -ForegroundColor Cyan
Write-Host "1. Archive user's OneDrive data (available for 30 days by default)"
Write-Host "2. Review shared mailbox permissions"
Write-Host "3. After 30 days, consider permanently deleting the account if no longer needed"
Write-Host "4. Review and transfer any owned resources (Teams, SharePoint sites, etc.)"
}} catch {{
Write-Host "✗ Error during offboarding: $_" -ForegroundColor Red
}}
# Disconnect
Disconnect-MgGraph
Disconnect-ExchangeOnline -Confirm:$false
"""
return script
def generate_license_assignment_recommendations(self, user_role: str, department: str) -> Dict[str, Any]:
"""
Recommend appropriate license based on user role and department.
Args:
user_role: Job title or role
department: Department name
Returns:
License recommendations with justification
"""
# License decision matrix
if any(keyword in user_role.lower() for keyword in ['ceo', 'cto', 'cfo', 'executive', 'director', 'vp']):
return {
'recommended_license': 'Microsoft 365 E5',
'justification': 'Executive level - requires advanced security, compliance, and full feature set',
'features_needed': [
'Advanced Threat Protection',
'Azure AD P2 with PIM',
'Advanced compliance and eDiscovery',
'Phone System and Audio Conferencing'
],
'monthly_cost': 57.00
}
elif any(keyword in user_role.lower() for keyword in ['admin', 'it', 'security', 'compliance']):
return {
'recommended_license': 'Microsoft 365 E5',
'justification': 'IT/Security role - requires full admin and security capabilities',
'features_needed': [
'Advanced security and compliance tools',
'Azure AD P2',
'Privileged Identity Management',
'Advanced analytics'
],
'monthly_cost': 57.00
}
elif department.lower() in ['legal', 'finance', 'hr', 'accounting']:
return {
'recommended_license': 'Microsoft 365 E3',
'justification': 'Handles sensitive data - requires enhanced security and compliance',
'features_needed': [
'Data Loss Prevention',
'Information Protection',
'Azure AD P1',
'Advanced compliance tools'
],
'monthly_cost': 36.00
}
elif any(keyword in user_role.lower() for keyword in ['manager', 'lead', 'supervisor']):
return {
'recommended_license': 'Microsoft 365 Business Premium',
'justification': 'Management role - needs full productivity suite with security',
'features_needed': [
'Desktop Office apps',
'Advanced security',
'Device management',
'Teams advanced features'
],
'monthly_cost': 22.00
}
elif any(keyword in user_role.lower() for keyword in ['part-time', 'contractor', 'temporary', 'intern']):
return {
'recommended_license': 'Microsoft 365 Business Basic',
'justification': 'Temporary/part-time role - web apps and basic features sufficient',
'features_needed': [
'Web versions of Office apps',
'Teams',
'OneDrive (1TB)',
'Exchange (50GB)'
],
'monthly_cost': 6.00
}
else:
return {
'recommended_license': 'Microsoft 365 Business Standard',
'justification': 'Standard office worker - full productivity suite',
'features_needed': [
'Desktop Office apps',
'Teams',
'OneDrive (1TB)',
'Exchange (50GB)',
'SharePoint'
],
'monthly_cost': 12.50
}
def generate_group_membership_recommendations(self, user: Dict[str, Any]) -> List[str]:
"""
Recommend security and distribution groups based on user attributes.
Args:
user: User dictionary with role, department, location
Returns:
List of recommended group names
"""
recommended_groups = []
# Department-based groups
department = user.get('department', '').lower()
if department:
recommended_groups.append(f"DL-{department.capitalize()}") # Distribution list
recommended_groups.append(f"SG-{department.capitalize()}") # Security group
# Location-based groups
location = user.get('location', '').lower()
if location:
recommended_groups.append(f"SG-Location-{location.capitalize()}")
# Role-based groups
job_title = user.get('job_title', '').lower()
if any(keyword in job_title for keyword in ['manager', 'director', 'vp', 'executive']):
recommended_groups.append("SG-Management")
if any(keyword in job_title for keyword in ['admin', 'administrator']):
recommended_groups.append("SG-ITAdmins")
# Functional groups
if user.get('needs_sharepoint_access'):
recommended_groups.append(f"SG-SharePoint-{department.capitalize()}")
if user.get('needs_project_access'):
recommended_groups.append("SG-ProjectUsers")
return recommended_groups
def validate_user_data(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Validate user data before provisioning.
Args:
user_data: User information dictionary
Returns:
Validation results with errors and warnings
"""
errors = []
warnings = []
# Required fields
required_fields = ['first_name', 'last_name', 'username']
for field in required_fields:
if not user_data.get(field):
errors.append(f"Missing required field: {field}")
# Username validation
username = user_data.get('username', '')
if username:
if ' ' in username:
errors.append("Username cannot contain spaces")
if not username.islower():
warnings.append("Username should be lowercase")
if len(username) < 3:
errors.append("Username must be at least 3 characters")
# Email validation
email = user_data.get('email')
if email and '@' not in email:
errors.append("Invalid email format")
# Display name
if not user_data.get('display_name'):
first = user_data.get('first_name', '')
last = user_data.get('last_name', '')
warnings.append(f"Display name not provided, will use: {first} {last}")
# License validation
if not user_data.get('license_sku'):
warnings.append("No license specified, will need manual assignment")
return {
'is_valid': len(errors) == 0,
'errors': errors,
'warnings': warnings
}
Install this Skill
Skills give your AI agent a consistent, structured approach to this task — better output than a one-off prompt.
npx skills add alirezarezvani/claude-skills --skill engineering-team/ms365-tenant-manager Community skill by @alirezarezvani. Need a walkthrough? See the install guide →
Works with
Prefer no terminal? Download the ZIP and place it manually.
Details
- Category
- Productivity
- License
- MIT
- Author
- @alirezarezvani
- Source
- GitHub →
- Source file
-
show path
engineering-team/ms365-tenant-manager/SKILL.md
People who install this also use
Senior SecOps Engineer
SAST/DAST scanning automation, CVE triage and remediation, GDPR and SOC2 compliance workflows, and security operations from a senior SecOps perspective.
@alirezarezvani
Atlassian Administrator
Administer Jira and Confluence at scale — user management, permission schemes, SSO integration, security configuration, and disaster recovery planning.
@alirezarezvani