Microsoft Graph PowerShell : 12 commandes pour les utilisateurs et les licences

Inventoriez les utilisateurs, distinguez licences directes et héritées, analysez les erreurs et préparez deux changements pilotes avec permissions et retour arrière.

01

12 recettes utiles, pas un audit complet

Une licence visible n’explique pas toujours comment elle est arrivée sur un compte ni pourquoi un service refuse de démarrer. Ces 12 recettes PowerShell relient utilisateurs, SKU, plans de service et groupes. Les dix premières lisent des données; les deux dernières illustrent des changements individuels protégés par un interrupteur désactivé.

Exécutez-les dans l’ordre dans une session dédiée. Tous les identifiants sont fictifs. Ce diagnostic ciblé ne mesure ni l’usage réel des services ni l’ensemble de la sécurité Microsoft 365 et ne remplace pas une évaluation complète.

02

Préparer PowerShell, l’identité et les permissions

Utilisez les modules stables Microsoft Graph avec les API v1.0, pas les anciens modules AzureAD/MSOnline. PowerShell 7 est recommandé. Installez les sous-modules dans une étape approuvée, figez les versions validées et consignez celles chargées; une mise à jour automatique n’est pas un prérequis de collecte.

Le scénario est interactif et délégué : compte administratif nominatif, MFA et bon locataire. Les scopes OAuth, le consentement administrateur et les rôles Entra sont des contrôles distincts. Commencez par les lectures nécessaires. Directory Readers est notamment pris en charge pour les lectures de SKU et de détails de licence; certaines opérations demandent des rôles spécifiques.

Préparation — connexion, pas de changement utilisateur
# PowerShell 7; approved, tested module versions
Import-Module Microsoft.Graph.Authentication
Import-Module Microsoft.Graph.Users
Import-Module Microsoft.Graph.Users.Actions
Import-Module Microsoft.Graph.Groups
Import-Module Microsoft.Graph.Identity.DirectoryManagement
Get-Module Microsoft.Graph.* | Select-Object Name,Version
$tenantId = '33333333-3333-3333-3333-333333333333' # fictional tenant
Connect-MgGraph -TenantId $tenantId -Scopes 'User.Read.All','LicenseAssignment.Read.All' -ContextScope Process
Get-MgContext | Select-Object TenantId,Account,AuthType,Scopes
# Only for checks 8–10, reconnect with approved group read scopes:
# Connect-MgGraph -TenantId $tenantId -Scopes 'User.Read.All','LicenseAssignment.Read.All','Group.Read.All','GroupMember.Read.All' -ContextScope Process
03

1. Inventorier les utilisateurs avec les propriétés utiles

Permission déléguée : User.Read.All.

-All parcourt les pages; -Property demande les champs non retournés par défaut. Un locataire volumineux exige une collecte planifiée et des reprises sur limitation de débit. L’inventaire est un instantané, pas un journal d’activité.

Lecture seule
$users = @(Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,AccountEnabled,UsageLocation,UserType,AssignedLicenses,LicenseAssignmentStates -ErrorAction Stop)
$users | Select-Object Id,DisplayName,UserPrincipalName,AccountEnabled,UsageLocation,UserType
04

2. Examiner un compte pilote précis

Permission déléguée : User.Read.All.

Privilégiez l’Object ID pour éviter les ambiguïtés après renommage. Confirmez l’UPN et le propriétaire avant un changement. Une propriété vide non demandée avec -Property ne prouve pas qu’elle est vide dans Entra.

Lecture seule
$userId = '11111111-1111-1111-1111-111111111111' # fictional OBJECT ID
$user = Get-MgUser -UserId $userId -Property Id,DisplayName,UserPrincipalName,AccountEnabled,UsageLocation,AssignedLicenses,LicenseAssignmentStates -ErrorAction Stop
$user | Select-Object Id,DisplayName,UserPrincipalName,AccountEnabled,UsageLocation,AssignedLicenses
05

3. Repérer les comptes désactivés encore licenciés

Permission déléguée : User.Read.All.

Cette sélection locale réutilise la collecte 1. Un compte désactivé peut conserver une licence pour des données, une conservation ou un transfert métier. Faites valider le besoin; ne retirez rien à partir de ce filtre seul.

Lecture seule
$users | Where-Object { $_.AccountEnabled -eq $false -and @($_.AssignedLicenses).Count -gt 0 } |
  Select-Object Id,UserPrincipalName,UsageLocation,
    @{Name='SkuIds';Expression={($_.AssignedLicenses.SkuId -join ';')}}
06

4. Lister les SKU souscrits et leurs compteurs

Permission déléguée : LicenseAssignment.Read.All.

Utilisez SkuId pour les opérations et SkuPartNumber pour le repérage; les noms commerciaux se vérifient dans la table Microsoft. Les compteurs ne remplacent pas les conditions contractuelles ni une réservation de capacité. Conservez le statut de la souscription.

Lecture seule
$skus = @(Get-MgSubscribedSku -All -ErrorAction Stop)
$skus | Select-Object SkuId,SkuPartNumber,CapabilityStatus,ConsumedUnits,
  @{Name='EnabledUnits';Expression={$_.PrepaidUnits.Enabled}}
07

5. Lire les détails des licences et plans d’un utilisateur

Permission déléguée : LicenseAssignment.Read.All.

Inspectez ProvisioningStatus de chaque plan. Une licence présente n’implique pas que chaque service soit disponible. Cet endpoint est utilisé ici en mode délégué; ne transposez pas sans validation à une identité applicative. Le rôle Directory Readers est l’un des rôles pris en charge pour cette lecture.

Lecture seule
Get-MgUserLicenseDetail -UserId $userId -All -ErrorAction Stop |
  Select-Object SkuId,SkuPartNumber,ServicePlans |
  ConvertTo-Json -Depth 10
08

6. Distinguer attribution directe et groupe

Permission déléguée : User.Read.All.

AssignedByGroup nul indique une attribution directe; sinon, résolvez l’ID du groupe. Un même SKU peut avoir plusieurs chemins d’attribution. AssignedLicenses seul ne suffit pas à identifier la source ou la politique qui doit être corrigée.

Lecture seule
$user.LicenseAssignmentStates |
  Select-Object SkuId,AssignedByGroup,State,Error,DisabledPlans,LastUpdatedDateTime
09

7. Isoler les erreurs d’attribution

Permission déléguée : User.Read.All.

Corrélez état et code : capacité, dépendance de plan, offre incompatible ou usageLocation peuvent être en cause. Ne relancez pas en boucle une attribution sans corriger la cause. L’absence d’erreur dans un instantané ne prouve pas la disponibilité de l’application.

Lecture seule
foreach ($u in $users) {
  foreach ($a in $u.LicenseAssignmentStates) {
    if ($a.State -in @('Error','ActiveWithError') -or
        ($a.Error -and $a.Error -ne 'None')) {
      [pscustomobject]@{
        UserId=$u.Id; UPN=$u.UserPrincipalName; SkuId=$a.SkuId
        GroupId=$a.AssignedByGroup; State=$a.State; Error=$a.Error
      }
    }
  }
}
10

8. Inventorier les groupes portant des licences

Permission déléguée : Group.Read.All.

Le groupe est parfois la bonne unité de remédiation. Une modification peut toucher tous ses membres. Vérifiez règles dynamiques, plans désactivés et licences exigées pour l’attribution par groupe et les groupes dynamiques; elles ne se déduisent pas de la seule réponse Graph.

Lecture seule
$groups = @(Get-MgGroup -All -Property Id,DisplayName,GroupTypes,AssignedLicenses,MembershipRule,MembershipRuleProcessingState -ErrorAction Stop)
$groups | Where-Object { @($_.AssignedLicenses).Count -gt 0 } |
  Select-Object Id,DisplayName,GroupTypes,AssignedLicenses,MembershipRule,MembershipRuleProcessingState |
  ConvertTo-Json -Depth 10
11

9. Lister les membres directs d’un groupe pilote

Permission déléguée : GroupMember.Read.All.

Le résultat peut contenir plusieurs types d’objets. Il s’agit de membres directs; ne supposez pas une expansion transitive ou l’héritage des licences via groupes imbriqués. Les appartenances masquées exigent des permissions supplémentaires et une justification.

Lecture seule
$groupId = '22222222-2222-2222-2222-222222222222' # fictional group ID
Get-MgGroupMember -GroupId $groupId -All -ErrorAction Stop |
  Select-Object Id,@{Name='Type';Expression={$_.AdditionalProperties['@odata.type']}}
12

10. Lister les appartenances directes du compte pilote

Permission déléguée : User.Read.All.

Ces objets peuvent inclure groupes et rôles d’annuaire. Résolvez uniquement les ID utiles avec la permission adaptée; des détails limités ne signifient pas une absence d’objet. Comparez les groupes aux sources du contrôle 6 avant de retirer une attribution directe.

Lecture seule
Get-MgUserMemberOf -UserId $userId -All -ErrorAction Stop |
  Select-Object Id,@{Name='Type';Expression={$_.AdditionalProperties['@odata.type']}}
13

Avant les changements : réduire le rayon d’impact

Obtenez l’accord du propriétaire métier, confirmez les droits de licence et choisissez un utilisateur pilote non privilégié. Exportez dans un emplacement restreint l’Object ID, la localisation, les SKU, DisabledPlans et chaque source d’attribution. Les exports contiennent des données personnelles : accès limité et durée de conservation définie.

La lecture de Graph ne crée pas un droit d’usage produit. Vérifiez les conditions des offres et des licences par groupe, particulièrement les groupes dynamiques. Les licences héritées sont traitées de façon asynchrone; les groupes imbriqués ne distribuent pas leurs licences aux membres indirects. N’élargissez pas les permissions pour contourner une erreur 403.

14

11. Corriger UsageLocation sur le pilote

Changement explicite : User.ReadWrite.All et un rôle autorisé à modifier cet utilisateur, par exemple User Administrator pour ce pilote non administrateur. UsageLocation est un code ISO à deux lettres, pas une adresse ni une préférence linguistique. Confirmez le pays réellement applicable; CA est seulement un exemple.

L’exemple refuse une valeur initialement absente : la propriété n’est pas nullable, donc remettre null n’est pas un retour arrière valide. La variable false fait uniquement afficher la proposition. Préservez la valeur précédente hors session avant d’autoriser l’écriture.

Modification pilote — désactivée par défaut
# CHANGE: existing location correction on a non-admin pilot
# Separate approved write session; refresh check 2 first
Connect-MgGraph -TenantId $tenantId -Scopes 'User.ReadWrite.All' -ContextScope Process
$before = Get-MgUser -UserId $userId -Property Id,UserPrincipalName,UsageLocation -ErrorAction Stop
if ([string]::IsNullOrWhiteSpace($before.UsageLocation)) {
  throw 'Missing previous location: define an initial-population recovery plan first.'
}
$previousLocation = $before.UsageLocation
$applyLocation = $false
[pscustomobject]@{User=$before.UserPrincipalName; Before=$previousLocation; After='CA'}
if ($applyLocation) {
  Update-MgUser -UserId $userId -UsageLocation 'CA' -ErrorAction Stop
  Get-MgUser -UserId $userId -Property Id,UserPrincipalName,UsageLocation -ErrorAction Stop
}
# Approved rollback: Update-MgUser -UserId $userId -UsageLocation $previousLocation
15

12. Ajouter une nouvelle licence directe au pilote

Changement explicite : LicenseAssignment.ReadWrite.All, plus User.Read.All pour la relecture, et rôle License Administrator autorisé. Choisissez un vrai SkuId de la collecte 4, vérifiez capacité, dépendances, pays et besoin métier. Le garde-fou refuse un SKU déjà présent, quelle que soit sa source.

DisabledPlans vide active tous les plans disponibles de cette attribution; faites approuver la sélection avant de passer le commutateur à true. RemoveLicenses vide ne retire aucune licence. Un succès HTTP ne garantit pas la fin du provisionnement : relisez 5, 6 et 7 et testez le service attendu.

Nouvelle attribution pilote — désactivée par défaut
# CHANGE: new direct assignment, never a bulk removal
Connect-MgGraph -TenantId $tenantId -Scopes 'User.Read.All','LicenseAssignment.ReadWrite.All' -ContextScope Process
$skuId = [guid]'44444444-4444-4444-4444-444444444444' # replace from check 4
$baseline = Get-MgUser -UserId $userId -Property Id,UserPrincipalName,UsageLocation,AssignedLicenses,LicenseAssignmentStates -ErrorAction Stop
if ([string]::IsNullOrWhiteSpace($baseline.UsageLocation)) { throw 'Confirm usage location first.' }
if (@($baseline.AssignedLicenses | Where-Object { $_.SkuId -eq $skuId }).Count -gt 0) {
  throw 'SKU already assigned: inspect source and plans; do not overwrite.'
}
$applyLicense = $false
$approvedDisabledPlans = @() # explicitly enables all available plans: review first
[pscustomobject]@{User=$baseline.UserPrincipalName; SkuId=$skuId; DisabledPlans=$approvedDisabledPlans}
if ($applyLicense) {
  Set-MgUserLicense -UserId $userId -AddLicenses @(@{
    SkuId=$skuId; DisabledPlans=$approvedDisabledPlans
  }) -RemoveLicenses @() -ErrorAction Stop
}
16

Valider, dépanner et revenir en arrière

  • Recollectez le pilote après le délai de traitement. Comparez les sources et les plans au relevé initial, puis validez ouverture de session et service avec son propriétaire.
  • 403 : distinguez consentement, scope, rôle et périmètre administratif. 404 : validez Object ID et locataire. 429 : respectez Retry-After et espacez les collectes; ne lancez pas une boucle agressive.
  • Pour une erreur de licence, examinez le détail : capacité, dépendances de plans et disponibilité géographique. Consignez résultat et heure opérationnelle sans exposer les données dans un ticket public.
  • Retour arrière localisation : restaurez la valeur enregistrée si elle reste correcte. Retour arrière licence : seule la nouvelle attribution directe créée par ce pilote peut être envisagée pour retrait, après validation métier et conservation des données. Aucun retrait automatique n’est fourni.
  • Une attribution héritée se gère à sa source; changer un groupe peut toucher tous ses membres. Réattribuer une licence ne restaure pas des données déjà supprimées.
  • Terminez par Disconnect-MgGraph. Cela ferme la session, sans révoquer le consentement OAuth déjà accordé. Révisez séparément les autorisations inutiles.
17

Passer d’une collecte à un plan d’action

Un résultat utile tient en quatre colonnes : observation, impact, propriétaire, prochaine validation. Par exemple, une erreur de capacité sur un pilote devient une vérification de l’offre et du besoin, pas une suppression de licences ailleurs. Pour standardiser les droits, les groupes et le cycle de vie des comptes, un accompagnement Microsoft 365 permet de cadrer les changements au-delà de ces commandes.