Microsoft Entra ID P1-Lizenzen sind für viele KMU und IT-Abteilungen eine Hürde, wenn es nur um ein spezielles Feature geht: Dynamische Gruppen. Während Microsoft für automatisierte Gruppenmitgliedschaften monatliche Gebühren pro Benutzer aufruft, lässt sich dieselbe Logik mit Bordmitteln, etwas PowerShell und Azure Automation abbilden. Das Ziel ist eine „Pseudo-dynamische Gruppe“, die sich selbst pflegt, ohne dass Du für jeden Nutzer extra bezahlen musst.
Native dynamische Gruppen basieren auf Regeln, die im Hintergrund vom Entra-Dienst evaluiert werden. Bei unserem DIY-Ansatz übernehmen wir diese Evaluation selbst. Wir nutzen die Microsoft Graph API, um Benutzer nach bestimmten Kriterien zu filtern und die Gruppenmitgliedschaften in regelmäßigen Intervallen abzugleichen.
Die Architektur der Pseudo-Dynamik
Um eine zuverlässige Automatisierung aufzubauen, benötigst Du ein Azure Automation Konto und eine Managed Identity. Die Verwendung einer Managed Identity ist zwingend erforderlich, da sie den Verzicht auf gespeicherte Client-Secrets oder Passwörter ermöglicht und somit das Risiko von Credential-Leaks minimiert.
Die Managed Identity erhält die Berechtigungen Group.ReadWrite.All und User.Read.All. Sobald das Runbook startet, authentifiziert es sich gegenüber dem Graph, identifiziert die Ziel-User und führt einen Delta-Abgleich durch. Dieser Abgleich stellt sicher, dass nur notwendige Änderungen geschrieben werden, was die API-Drosselung (Throttling) verhindert.
Schritt 1: Die Basis-Gruppe erstellen
Zuerst benötigst Du eine Standard-Microsoft 365 Gruppe (Unified Group). Da wir die Dynamik manuell steuern, muss der GroupTypes auf Unified gesetzt werden, aber ohne das Attribut DynamicMembership.
# Verbindung zum Graph herstellen (Lokal für die Ersteinrichtung)
Connect-MgGraph -Scopes "Group.ReadWrite.All"
$groupParams = @{
Description = "Automatisierte Gruppe für die Marketing-Abteilung"
DisplayName = "Marketing-Team (Auto)"
MailEnabled = $true
SecurityEnabled = $true
MailNickname = "marketing-auto"
GroupTypes = @("Unified")
}
$targetGroup = New-MgGroup @groupParams

Schritt 2: Die Krux mit der Filterung
Ein häufiger Pain Point bei der Nutzung von Get-MgUser ist die eingeschränkte Filterfähigkeit des Graph-Endpunkts. Einfache Abfragen wie jobTitle eq 'Manager' funktionieren einwandfrei, aber komplexe Suchen oder Teilübereinstimmungen werden oft blockiert.
Um dieses Problem zu umgehen, nutzen wir extensionAttributes (oder onPremisesExtensionAttributes). Diese Felder sind extrem wertvoll, wodurch Du spezifische Flags setzen kannst, die nicht mit den offiziellen Jobtiteln kollidieren. Wenn Du beispielsweise alle Projektleiter erfassen willst, aber die Titel zwischen „Senior Projekt Manager“ und „Lead PL“ variieren, setzt Du im lokalen AD oder direkt im Entra ID das extensionAttribute15 auf den Wert ProjectLead.
# Abfrage der User, die dem Kriterium entsprechen
$criteria = "onPremisesExtensionAttributes/extensionAttribute15 eq 'ProjectLead'"
$matchingUsers = Get-MgUser -Filter $criteria -All -Property "Id", "DisplayName"
Schritt 3: Der Delta-Sync-Algorithmus
Einfach alle Mitglieder zu löschen und neu hinzuzufügen, ist ineffizient und führt bei großen Gruppen zu massiven Problemen in Teams oder SharePoint. Wir berechnen stattdessen die Differenz zwischen dem IST-Zustand (aktuelle Mitglieder) und dem SOLL-Zustand (gefilterte User).
Die Graph API erlaubt bei Batch-Operationen maximal 20 Änderungen pro Request. Wir müssen die Liste der hinzuzufügenden User daher in Pakete zerlegen.
# Aktuelle Mitglieder abrufen
$currentMembers = Get-MgGroupMember -GroupId $targetGroup.Id -All | Select-Object -ExpandProperty Id
# Wer muss rein?
$usersToAdd = $matchingUsers.Id | Where-Object { $_ -notin $currentMembers }
# Wer muss raus?
$usersToRemove = $currentMembers | Where-Object { $_ -notin $matchingUsers.Id }
# Bulk-Add (Max 20 pro Batch)
foreach ($batch in (0..([Math]::Ceiling($usersToAdd.Count / 20) - 1))) {
$start = $batch * 20
$end = [Math]::Min($start + 19, $usersToAdd.Count - 1)
$ids = $usersToAdd[$start..$end]
if ($ids) {
$values = $ids | ForEach-Object { "https://graph.microsoft.com/v1.0/directoryObjects/$_" }
$params = @{ "members@odata.bind" = $values }
Update-MgGroup -GroupId $targetGroup.Id -BodyParameter $params
}
}
Schritt 4: Automatisierung im Azure Portal
Damit das Skript zum „Dienst“ wird, lädst Du es als Runbook in Dein Azure Automation Konto hoch.
- Erstelle ein Automation Account (Workflow siehe Bilder unten).
- Aktiviere die „System-assigned Managed Identity“.
- Weise der Identity über die PowerShell die Graph-Berechtigungen zu (dies geht nicht über das UI!).
Um die Berechtigungen zu setzen, nutzt Du folgendes Snippet lokal mit Deinem Admin-Account:
$MI_ID = "Deine-Managed-Identity-Object-ID"
$GraphApp = Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'"
$Role = $GraphApp.AppRoles | Where-Object { $_.Value -eq "Group.ReadWrite.All" }
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $MI_ID `
-PrincipalId $MI_ID `
-ResourceId $GraphApp.Id `
-AppRoleId $Role.Id






- Erstelle ein PowerShell-Runbook ((Workflow siehe Bilder unten – PowerShell Version 7.2).
- Füge den Code ein und ersetze
Connect-MgGraphdurchConnect-MgGraph -Identity.



Fazit und kritische Würdigung
Der Aufbau einer eigenen Dynamik-Engine spart signifikante Lizenzkosten, bringt jedoch operative Verantwortung mit sich. Du bist nun der „Provider“ dieses Features. Wenn das Runbook aufgrund einer API-Änderung fehlschlägt, werden keine Mitglieder mehr aktualisiert.
Aus Sicherheitsaspekten ist dieser Weg hochgradig solide, solange Du das Prinzip der geringsten Rechte (Least Privilege) einhältst. Die Managed Identity sollte niemals Global Admin Rechte erhalten, sondern strikt auf die benötigten Graph-Scopes begrenzt bleiben. Ein weiterer Vorteil gegenüber nativen dynamischen Gruppen ist die Flexibilität: Du kannst komplexe Logiken implementieren, die über einfache Attribut-Matches hinausgehen – zum Beispiel „Alle User, die seit 30 Tagen nicht mehr eingeloggt waren“ oder „Mitglieder basierend auf Daten aus einer externen SQL-Datenbank“.
Performance-technisch stößt dieser Ansatz erst bei sehr großen Tenants (über 10.000 User in einer Gruppe) an Grenzen. Hier müsstest Du auf fortgeschrittene Batching-Methoden oder Azure Functions ausweichen, um die Laufzeitlimits von Azure Automation (3 Stunden pro Job) nicht zu überschreiten. Für die meisten Szenarien ist das stündliche oder tägliche Ausführen eines Runbooks jedoch das ideale Gleichgewicht zwischen Aktualität und Kostenersparnis.
In der Praxis solltest Du immer abwägen: Wenn Dein Unternehmen ohnehin die Sicherheitsfeatures von Entra P1 (wie Conditional Access) benötigt, ist die native dynamische Gruppe vorzuziehen. Wenn es Dir jedoch rein um die Automatisierung von Standard-Verteilerlisten oder Teams-Zutritten geht, ist der hier beschriebene PowerShell-Weg die technisch sauberere und wirtschaftlichere Lösung.
weitere Links
| Quelle | URL |
| Microsoft Learn | https://learn.microsoft.com/en-us/graph/api/user-get |
| Microsoft Learn | https://learn.microsoft.com/en-us/graph/api/group-update |
| GitHub | https://github.com/microsoftgraph/msgraph-sdk-powershell |
| Practical 365 | https://practical365.com/diy-dynamic-microsoft-365-group/ |


Hinterlasse jetzt einen Kommentar