Manage Microsoft 365 with the Microsoft.Graph PowerShell module through GraphWarden

Once connected through the quickstart, Microsoft.Graph cmdlets work against GraphWarden transparently. This page walks through common Microsoft 365 admin scenarios — users, groups, licences, directory audit — and shows how ruleset enforcement surfaces when a cmdlet is denied. Every snippet assumes the connection is already established; see the quickstart for the Connect-MgGraph setup.

Microsoft.Graph v2.x

Prerequisites

- Completed the PowerShell quickstart (see `./quickstart`). The connection must be established via `Connect-MgGraph -AccessToken $SecureToken` before any cmdlet below will route through GraphWarden. - An App Identity configured in GraphWarden App Admin with a ruleset that permits the operations you intend to run. Cmdlets whose underlying Graph calls are blocked by a rule surface as `403 Forbidden` errors.

List users with selected properties

The Get-MgUser cmdlet wraps GET /v1.0/users. -Property maps to $select, -Top maps to $top, and -Filter maps to $filter. All three query shapes pass through the proxy byte-for-byte.

Get-MgUser `
    -Top 10 `
    -Property 'displayName','mail','userPrincipalName','accountEnabled'
    | Format-Table displayName, mail, accountEnabled

If a ruleset Response Filter applies allow_properties or strip_properties to /v1.0/users, the missing keys will not appear in the deserialized objects — a user with mail stripped surfaces as $null.

Create a group

New-MgGroup issues POST /v1.0/groups. The example below creates a security group; adjust MailEnabled, SecurityEnabled, and GroupTypes for other shapes (Microsoft 365 group, distribution list, dynamic membership).

$group = New-MgGroup `
    -DisplayName   'Test security group' `
    -MailNickname  'test-sec-group' `
    -MailEnabled:$false `
    -SecurityEnabled:$true `
    -GroupTypes    @()

Write-Host "Created group: $($group.Id) - $($group.DisplayName)"

Group creation is a common ruleset gate — many policies limit which App Identities can create groups, or require a naming convention enforced by a rule's conditions block. If your request is denied, the response body will name the matched rule.

Assign a licence

License assignment uses Set-MgUserLicense, which issues POST /v1.0/users/{id}/assignLicense. You need the SKU Id of the plan you are assigning — fetch it with Get-MgSubscribedSku before calling assign.

# 1. Find the SKU you want to assign (e.g. Microsoft 365 E3).
$sku = Get-MgSubscribedSku `
    | Where-Object SkuPartNumber -eq 'ENTERPRISEPACK' `
    | Select-Object -First 1

# 2. Assign it to a user by UPN or object id.
Set-MgUserLicense `
    -UserId       'alice@contoso.com' `
    -AddLicenses  @(@{ SkuId = $sku.SkuId }) `
    -RemoveLicenses @()

License state is an audit-heavy area. Even with action: allow, every assign / remove surfaces as an audit event in GraphWarden with the matched rule id, the caller, and the payload summary.

Read audit log entries

Get-MgAuditLogDirectoryAudit returns tenant-level audit activities (admin actions, policy changes). The scope AuditLog.Read.All or Directory.Read.All must be granted on the underlying Azure AD app registration AND permitted by the ruleset — the App Identity needs to be permitted to call /v1.0/auditLogs/directoryAudits in GraphWarden.

Get-MgAuditLogDirectoryAudit `
    -Top    10 `
    -Filter "activityDisplayName eq 'Add user'"
    | Select-Object ActivityDateTime, ActivityDisplayName, InitiatedBy

If the ruleset has not been configured to permit this endpoint, expect 403 Forbidden with a rule id in the response body (see the "Understanding ruleset denials in PowerShell" section below).

Delete a user (permission-gated)

Delete operations are high-impact and are typically denied by default in GraphWarden rulesets. The example below intentionally demonstrates a denied cmdlet — run it against an identity whose ruleset blocks user deletes to see the shape of the error.

`Remove-MgUser` issues `DELETE /v1.0/users/{id}` and is irreversible when permitted. If your ruleset has been configured to allow deletes, running this snippet against a real user account will remove that user. Use a disposable test account or a demo tenant for the denial demonstration.
try {
    Remove-MgUser -UserId 'victim@contoso.com' -ErrorAction Stop
    Write-Host "User removed."
}
catch {
    # Expected: 403 Forbidden when the ruleset denies DELETE on /v1.0/users.
    Write-Warning "Remove-MgUser failed: $($_.Exception.Message)"
    Write-Host  "Response body:" ($_.ErrorDetails.Message)
}

Expected output when the rule blocks the request (exact rule id will vary with your ruleset):

# WARNING: Remove-MgUser failed: Response status code does not indicate success: 403 (Forbidden).
# Response body: {"error":{"code":"Forbidden","message":"Blocked by GraphWarden rule 'deny-user-delete'."}}

Understanding ruleset denials in PowerShell

A blocked cmdlet raises a terminating error whose $_.Exception.Response.StatusCode is 403 and whose response body contains the matched rule id. There are two useful ways to inspect it.

# Option 1 — inspect $Error[0] after a failed cmdlet.
try {
    Get-MgAuditLogSignIn -Top 1 -ErrorAction Stop
}
catch {
    $response = $_.Exception.Response
    Write-Host "Status:      $([int]$response.StatusCode) $($response.StatusCode)"
    Write-Host "Rule body:   $($_.ErrorDetails.Message)"
}

# Option 2 — -ErrorVariable captures the error without terminating the script.
$failures = $null
Get-MgGroup -Top 1 -ErrorAction SilentlyContinue -ErrorVariable failures

if ($failures) {
    foreach ($f in $failures) {
        Write-Host "Failed: $($f.Exception.Message)"
    }
}

The rule id in the response body is the same identifier shown in GraphWarden App Admin's audit log entry for the request; use it to locate the rule, review its match pattern and conditions, and decide whether to widen the rule or adjust the script.

GraphWarden returns Microsoft Graph's native status codes for pass-through responses (2xx, 4xx forwarded from Graph). A `403` that originates at the proxy (ruleset denial) versus a `403` that originates at Graph (scope not granted on the underlying app registration) both surface the same status code; the response body distinguishes them — GraphWarden denials name a rule, Graph denials name a scope.

Troubleshooting

The five failure modes below cover the common surprises when running Microsoft.Graph cmdlets against GraphWarden.

Connect-MgGraph succeeds but every cmdlet returns 401

The proxy JWT that was passed to Connect-MgGraph has expired, or the SDK is not re-reading MICROSOFT_GRAPH_API_ENDPOINT (set after the connection was established). Disconnect with Disconnect-MgGraph, reacquire a fresh proxy JWT, ensure the env var is set, and reconnect.

Cmdlet returns 403 Forbidden with no rule id in the body

The rule body format changes with proxy versions; on older proxies the body may be {"error":{"code":"Forbidden","message":"Access denied."}} with no rule id. Check the audit log in GraphWarden App Admin for the matching request — audit events always include the rule id regardless of response body shape.

Cmdlet hangs on first call

The proxy URL resolves by DNS but the host is unreachable. Test with Test-NetConnection -ComputerName <proxy-host> -Port 443. In on-prem deployments, verify the firewall permits outbound 443 from the scripting host to the proxy.

Invoke-MgGraphRequest ignores the env var

Invoke-MgGraphRequest is a lower-level cmdlet that composes against the endpoint fixed at Connect-MgGraph time. If you switched MICROSOFT_GRAPH_API_ENDPOINT after connecting, Invoke-MgGraphRequest will keep calling the previous endpoint. Disconnect and reconnect to pick up the change.

Some ruleset Response Filters strip the @odata.nextLink property. If pagination appears broken, inspect the raw response with Invoke-MgGraphRequest -Method GET -Uri '/v1.0/users?$top=2' and check whether @odata.nextLink is present. If it was filtered, adjust the rule to keep navigation properties in the allow list.

Last reviewed:

Last reviewed: .