Solito blocco appunti che potenzialmente mi tornerà ancora utile in futuro, magari però chiarisce le idee e torna comodo anche a te che ci sei atterrato sopra.
Scenario: avevo bisogno di modificare velocemente nomi e descrizioni di una decina circa di gruppi Entra (quindi solo Cloud), e subito dopo crearne di nuovi mantenendo la stessa naming convention ma andando a ritoccare l’ultima parte. Leggi l’articolo, è più facile che spiegarlo qui nell’introduzione!
PowerShell 7, moduli di Microsoft Graph già installati, è un passaggio obbligato.
Connettiti a Microsoft Graph e, se non sai come farlo, sbircia uno dei miei precedenti articoli.
Alla ricerca dei gruppi
La base: tutti i gruppi che mi interessava rinominare hanno nome che comincia per “GitLab”. Per questo motivo, ho potuto lanciare facilmente una ricerca:
Get-MgGroup -Filter "startsWith(displayName,'GitLab')" -All
Questo produce una lista ordinata di gruppi con nome che comincia per GitLab, restituendo anche altre informazioni utili (Id del gruppo, descrizione, ecc.). Ho poi dovuto escludere un singolo gruppo dalla ricerca, questo non sarebbe stato soggetto alle successive modifiche. Il gruppo da escludere è GitLab-Full
, perciò:
$groups = Get-MgGroup -Filter "startswith(displayName,'GitLab')" -ConsistencyLevel eventual -CountVariable groupCount -All $groups | Where-Object { $_.DisplayName -ne 'GitLab-Full' } | Select-Object Id, DisplayName | Sort-Object DisplayName
Basato totalmente sulla prima query che ti ho mostrato, ma in questo caso ho tenuto conto di qualche parametro aggiuntivo e – infine – escluso quello che nel DisplayName compariva proprio come GitLab-Full
.
Rinominare i gruppi
Avevo bisogno di accodare un semplice _DEV
al nome dei gruppi trovati. Per questo motivo, ho potuto sfruttare il più classico dei cicli ForEach-Object, lanciando dapprima una Preview di quello che sarebbe poi accaduto:
$groups = Get-MgGroup -Filter "startswith(displayName,'GitLab')" -ConsistencyLevel eventual -All | Where-Object { $_.DisplayName -ne 'GitLab-Full' } foreach ($grp in $groups) { $newName = $grp.DisplayName + "_DEV" Update-MgGroup -GroupId $grp.Id -DisplayName $newName Write-Host "Renamed $($grp.DisplayName) -> $newName" }
Questo mi ha permesso di vedere cosa sarebbe successo se avessi lanciato realmente il ciclo per rinominare i gruppi. Considerato che era tutto assolutamente come previsto, ho leggermente modificato la query per intervenire sul serio:
$groups = Get-MgGroup -Filter "startswith(displayName,'GitLab')" -ConsistencyLevel eventual -All | Where-Object { $_.DisplayName -ne 'GitLab-Full' } foreach ($grp in $groups) { $newName = $grp.DisplayName + "_DEV" Update-MgGroup -GroupId $grp.Id -DisplayName $newName Write-Host "Renamed $($grp.DisplayName) -> $newName" }
Posso anche tornare indietro?
Beh, sì.
Se per indietro intendi “eliminare quel _DEV
” in coda, allora lo puoi fare semplicemente lanciando una query che andrà a rimuovere quell’ultima parte dal nome di ciascun gruppo:
$groups = Get-MgGroup -Filter "endswith(displayName,'_DEV')" -ConsistencyLevel eventual -All foreach ($grp in $groups) { $originalName = $grp.DisplayName -replace '_DEV$','' Update-MgGroup -GroupId $grp.Id -DisplayName $originalName Write-Host "Renamed back $($grp.DisplayName) -> $originalName" }
Facci caso: la ricerca, stavolta, terrà conto del fatto che andrà a trovare gruppi con nome che termina in _DEV
, processandoli e rinominandoli per eliminare quel suffisso. Per questo motivo, se hai altri gruppi che terminano per _DEV
e non c’entrano nulla con quelli che devi ritoccare, allora NON lanciare la query che ti ho proposto qui sopra.
Dovrai prima adattarla al tuo scenario per fare una ricerca più specifica, magari combinando il “comincia con” (startswith) e “finisce con” (endswith).
E la descrizione?
Giusta osservazione.
Non cambia poi molto dalla query che ti ha permesso di rinominare i gruppi. Stavolta, però, andremo ad accodare un testo particolare (in questo caso “(Ruolo: Developer)“) alla descrizione già esistente:
$groups = Get-MgGroup -Filter "startswith(displayName,'GitLab')" -ConsistencyLevel eventual -All | Where-Object { $_.DisplayName -ne 'GitLab-Full' } foreach ($grp in $groups) { $newDesc = ($grp.Description + " (Ruolo: Developer)").Trim() Update-MgGroup -GroupId $grp.Id -Description $newDesc Write-Host "Updated $($grp.DisplayName) Description -> $newDesc" }
Posso tornare indietro anche stavolta?
Un eventuale rollback di questa modifica si affronta più o meno alla stessa maniera di come hai affrontato quello del rename:
$groups = Get-MgGroup -ConsistencyLevel eventual -All | Where-Object { $_.Description -match '\(Ruolo: Developer\)$' } foreach ($grp in $groups) { $originalDesc = $grp.Description -replace '\s*\(Ruolo: Developer\)$','' Update-MgGroup -GroupId $grp.Id -Description $originalDesc Write-Host "Restored description for $($grp.DisplayName): $originalDesc" }
Forse inutile dirlo ma è bene sottolinearlo nuovamente: la ricerca, stavolta, terrà conto del fatto che andrà a trovare gruppi con descrizione che termina in (Ruolo: Developer). Se hai altri gruppi con descrizione che termina alla stessa maniera e che non c’entrano nulla con quelli che devi ritoccare, allora NON lanciare la query che ti ho proposto qui sopra.
Dovrai prima adattarla al tuo scenario per fare una ricerca più specifica, magari combinando il nome del gruppo tramite un “comincia con” (startswith) e il filtro RegEx che ti permette di individuare chi contiene quel testo a fine descrizione.
Nome del gruppo e descrizione in un solo colpo
Tecnicamente si sarebbe potuto fare tutto insieme sin dall’inizio, sia rinominare il gruppo che modificargli la descrizione. Basta ritoccare un pelo la query che ti ho già proposto un po’ più sopra, quella di rinomina, dicendole di andare anche ad accodare un testo particolare (in questo caso “(Ruolo: Developer)“) alla descrizione già esistente:
$groups = Get-MgGroup -Filter "startswith(displayName,'GitLab')" -ConsistencyLevel eventual -All | Where-Object { $_.DisplayName -ne 'GitLab-Full' } foreach ($grp in $groups) { $newName = $grp.DisplayName + "_DEV" $newDesc = ($grp.Description + " (Ruolo: Developer)").Trim() Update-MgGroup -GroupId $grp.Id -DisplayName $newName -Description $newDesc Write-Host "Updated $($grp.DisplayName) -> $newName | Description -> $newDesc" }
Clonazione dei gruppi
Lavoro più seccante: dovevo clonare i gruppi esistenti (sempre tenendo escluso il full) dando origine a quelli _MAINT
(al posto di _DEV
) adattando anche la descrizione e, già che c’ero, clonando proprietari dei gruppi e membri. Ho dato in pasto tutto a ChatGPT 5 e, dopo aver aggiustato un attimo il tiro sugli errori commessi, eccoti lo script completo che si occupa del lavoro sporco, tenendo conto che si tratta di clonare dei gruppi di sicurezza su Entra:
<# .SYNOPSIS Clone SECURITY groups to new *_MAINT Security groups and set description to include "(Ruolo: Maintainer)" once. Copies owners and members. Dry-run by default. .PREREQUISITES Connect-MgGraph -Scopes "Group.ReadWrite.All","Directory.ReadWrite.All" .PARAMETERS - Prefix: Source groups must start with this displayName prefix (default: "GitLab") - ExcludeNames: Exact source displayNames to skip - DryRun: If true (default), only preview without creating groups #> param( [string] $Prefix = "GitLab", [string[]] $ExcludeNames = @("GitLab-Full"), [switch] $DryRun = $true ) function Escape-ODataLiteral { param([Parameter(Mandatory)][string]$Value) # Escape single quotes for OData string literals return $Value.Replace("'", "''") } function Normalize-Ascii { param([Parameter(Mandatory)][string]$Text) # Strip diacritics -> ASCII $formD = $Text.Normalize([Text.NormalizationForm]::FormD) $sb = New-Object System.Text.StringBuilder foreach ($ch in $formD.ToCharArray()) { if ([Globalization.CharUnicodeInfo]::GetUnicodeCategory($ch) -ne [Globalization.UnicodeCategory]::NonSpacingMark) { [void]$sb.Append($ch) } } return $sb.ToString() } function Sanitize-MailNickname { param([Parameter(Mandatory)][string]$Name) # Lowercase; keep only a-z, 0-9; max length 64 (Graph limit) $ascii = Normalize-Ascii $Name $nick = ($ascii.ToLower() -replace '[^a-z0-9]', '') if ([string]::IsNullOrWhiteSpace($nick)) { $nick = 'group' } if ($nick.Length -gt 64) { $nick = $nick.Substring(0,64) } return $nick } function Get-UniqueMailNickname { param([Parameter(Mandatory)][string]$BaseNickname) $base = (Sanitize-MailNickname $BaseNickname) $candidate = $base $i = 1 while ($true) { $exists = Get-MgGroup -Filter "mailNickname eq '$candidate'" -ConsistencyLevel eventual -All if (-not $exists) { return $candidate } $i++ $suffix = $i.ToString() $candidate = $base if ($candidate.Length -gt (64 - $suffix.Length)) { $candidate = $candidate.Substring(0, 64 - $suffix.Length) } $candidate = $candidate + $suffix } } Write-Host "Fetching SECURITY source groups starting with '$Prefix'..." -ForegroundColor Cyan # Server-side prefix filter, then keep only Security groups locally $sourceGroups = Get-MgGroup ` -Filter "startswith(displayName,'$Prefix')" ` -ConsistencyLevel eventual ` -All ` -Property "id,displayName,description,securityEnabled,mailEnabled" | Where-Object { $_.SecurityEnabled -eq $true -and ($_.MailEnabled -ne $true) -and ($ExcludeNames -notcontains $_.DisplayName) } if (-not $sourceGroups) { Write-Warning "No SECURITY groups found with prefix '$Prefix' (after exclusions). Nothing to do." return } Write-Host ("Found {0} SECURITY source groups" -f $sourceGroups.Count) foreach ($g in $sourceGroups) { # --- Build target DisplayName (normalize suffix first) ----------------- $srcName = $g.DisplayName $baseName = $srcName -replace '_MAINT$','' -replace '_DEV$','' # remove any trailing _MAINT or _DEV $newDisplayName = "{0}_MAINT" -f $baseName # --- Build target Description (idempotent role tag) -------------------- $roleTag = " (Ruolo: Maintainer)" $cleanDesc = "$($g.Description)" # Remove any existing role tags to avoid duplication, then append once $cleanDesc = $cleanDesc -replace '\s*\(Ruolo:\s*Developer\)\s*','' $cleanDesc = $cleanDesc -replace '\s*\(Ruolo:\s*Maintainer\)\s*','' $newDescription = ($cleanDesc.Trim() + $roleTag).Trim() # Idempotency: skip if a target with same DisplayName already exists $escapedTarget = Escape-ODataLiteral $newDisplayName $existing = Get-MgGroup -Filter "displayName eq '$escapedTarget'" -ConsistencyLevel eventual -All if ($existing) { Write-Warning "Target already exists: '$newDisplayName' (skipping)" continue } # Always provide a MailNickname (some SDK/API paths require it even for Security groups) $mailNickBase = "${baseName}_maint" # use normalized baseName here $mailNickname = Get-UniqueMailNickname -BaseNickname $mailNickBase Write-Host "------------------------------------------------------------" Write-Host "SOURCE : $srcName (Security)" Write-Host "TARGET : $newDisplayName" Write-Host "DESC : $newDescription" Write-Host "ALIAS : $mailNickname" if ($DryRun) { Write-Host "[Dry-Run] Would create Security group and copy owners/members." -ForegroundColor Yellow continue } # --- Create Security group -------------------------------------------- try { $createParams = @{ DisplayName = $newDisplayName Description = $newDescription SecurityEnabled = $true MailEnabled = $false MailNickname = $mailNickname } $newGroup = New-MgGroup @createParams if (-not ($newGroup -and $newGroup.Id)) { throw "Creation returned no Id." } Write-Host "Created: $newDisplayName (Id: $($newGroup.Id))" -ForegroundColor Green } catch { Write-Error "Creation failed for '$newDisplayName': $($_.Exception.Message)" continue } # --- Copy owners ------------------------------------------------------- try { $owners = Get-MgGroupOwner -GroupId $g.Id -All if ($owners) { $added = 0 foreach ($o in $owners) { try { New-MgGroupOwnerByRef -GroupId $newGroup.Id -OdataId "https://graph.microsoft.com/v1.0/directoryObjects/$($o.Id)" | Out-Null $added++ } catch { Write-Warning "Owner add failed ($($o.Id)) -> $($newDisplayName): $($_.Exception.Message)" } } Write-Host ("Owners copied: {0}" -f $added) } else { Write-Host "No owners to copy." } } catch { Write-Warning "Owners step failed for '$newDisplayName': $($_.Exception.Message)" } # --- Copy members ------------------------------------------------------ try { $members = Get-MgGroupMember -GroupId $g.Id -All if ($members) { $added = 0 foreach ($m in $members) { try { New-MgGroupMemberByRef -GroupId $newGroup.Id -OdataId "https://graph.microsoft.com/v1.0/directoryObjects/$($m.Id)" | Out-Null $added++ } catch { Write-Warning "Member add failed ($($m.Id)) -> $($newDisplayName): $($_.Exception.Message)" } } Write-Host ("Members copied: {0}" -f $added) } else { Write-Host "No members to copy." } } catch { Write-Warning "Members step failed for '$newDisplayName': $($_.Exception.Message)" } } Write-Host "Done."
Salva lo script sul tuo PC, modificalo come reputi opportuno (perché al suo interno io ho comunque usato i riferimenti ai miei gruppi GitLab) e poi usalo!
Considera che, per comportamento predefinito, partirà in DryRun, perciò ti mostrerà a video cosa farebbe se venisse lanciato in scrittura. Se vuoi fargli fare sul serio il lavoro, allora ricorda di lanciarlo da riga di comando con parametro -DryRun:$false
.
In caso di errori, dubbi, boiate riportate in articolo sai già cosa fare, l’area commenti è a tua totale disposizione :-)
#KeepItSimple