Add Microsoft 365 user sync with roles, profile fields and deprovisioning
Some checks are pending
CI / PHP lint (7.4) (pull_request) Waiting to run
CI / PHP lint (8.0) (pull_request) Waiting to run
CI / PHP lint (8.1) (pull_request) Waiting to run
CI / PHP lint (8.2) (pull_request) Waiting to run
CI / PHP lint (8.3) (pull_request) Waiting to run
CI / PHP lint (8.4) (pull_request) Waiting to run
CI / WordPress Coding Standards (pull_request) Waiting to run
CI / WordPress.org Plugin Check (pull_request) Waiting to run
Some checks are pending
CI / PHP lint (7.4) (pull_request) Waiting to run
CI / PHP lint (8.0) (pull_request) Waiting to run
CI / PHP lint (8.1) (pull_request) Waiting to run
CI / PHP lint (8.2) (pull_request) Waiting to run
CI / PHP lint (8.3) (pull_request) Waiting to run
CI / PHP lint (8.4) (pull_request) Waiting to run
CI / WordPress Coding Standards (pull_request) Waiting to run
CI / WordPress.org Plugin Check (pull_request) Waiting to run
New "User sync" tab that imports Microsoft 365 / Entra ID users as WordPress accounts and keeps them up to date: - Scope: whole tenant or the (nested) members of selected groups, guests optional, e-mail domain allow-list respected. Existing accounts are linked by e-mail address. - Roles: selectable default role plus a group -> role mapping (in addition to or instead of the default role, first match wins). Roles of pre-existing accounts are only managed on request. - Profile: selectable Graph attributes (names, job title, department, phones, address, language, ...) and the profile photo as avatar. - Deprovisioning: accounts disabled or deleted in Microsoft 365 (or removed from the sync groups) are deactivated or deleted; accounts deactivated by the sync are reactivated automatically. Deactivated accounts lose every sign-in path and all sessions. - Safeguards: dry run, safety stop above 20 % (min. 5) deprovisioning, abort on any Graph error, "deleted" only on a 404 for the object ID, protected pre-existing administrators and own account, content reassignment required for deletion, run lock. - Runs manually, via WP-Cron or `wp m365-login sync [--dry-run]`. - Users screen column with deactivate/reactivate row actions and a read-only Microsoft 365 section on the profile screen. The Graph client gains paging, retry on throttling and user, group member and photo endpoints. The group picker is now reusable. Version 1.1.0, German translations (du/Sie), docs and audit addendum. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1708bae91a
commit
4edf20bc45
20 changed files with 5532 additions and 982 deletions
134
README.md
134
README.md
|
|
@ -31,6 +31,7 @@
|
|||
- [Gruppen-Beschränkung](#gruppen-beschränkung)
|
||||
- [Nur-Button-Modus & Fallback](#nur-button-modus--fallback)
|
||||
- [Eigene Login-Seite](#eigene-login-seite)
|
||||
- [Benutzer-Sync](#benutzer-sync)
|
||||
- [Sicherheitskonzept](#sicherheitskonzept)
|
||||
- [Shortcode & Hooks](#shortcode--hooks)
|
||||
- [Fehlerbehebung](#fehlerbehebung)
|
||||
|
|
@ -46,7 +47,8 @@
|
|||
| | |
|
||||
| --- | --- |
|
||||
| 🔑 **Login per Microsoft** | Ein Klick auf der Anmeldeseite, Anmeldung bei Microsoft, zurück in WordPress – fertig. |
|
||||
| 📧 **Zuordnung über die E-Mail-Adresse** | Es werden **keine Benutzer angelegt**. Nur wer schon ein WordPress-Konto mit derselben E-Mail hat, kommt rein. |
|
||||
| 📧 **Zuordnung über die E-Mail-Adresse** | Der Login legt **keine Benutzer an**. Nur wer schon ein WordPress-Konto mit derselben E-Mail hat, kommt rein. |
|
||||
| 🔄 **Benutzer-Sync (optional)** | Importiert Microsoft-365-Benutzer als WordPress-Konten – mit Standardrolle, zusätzlichen Rollen per Gruppen-Zuordnung, wählbaren Profilfeldern und Profilbild. In Microsoft 365 deaktivierte oder gelöschte Konten werden in WordPress deaktiviert oder gelöscht. |
|
||||
| 🎨 **Gestaltbarer Button** | Text, Icon (Microsoft-Logo oder eigenes Bild), Farben, Hover-Farbe, Rahmen, Eckenradius, Position – mit Live-Vorschau und Presets. |
|
||||
| 👥 **Entra-Gruppen** | Optional nur Mitglieder ausgewählter Gruppen zulassen. Gruppen werden direkt im Backend gesucht und ausgewählt. |
|
||||
| 🚪 **Nur-Button-Modus** | Passwortfelder ausblenden und Passwort-Logins sperren – mit geheimem Fallback-Link als Notausgang. |
|
||||
|
|
@ -170,7 +172,19 @@ Benutzer liefert Microsoft keinen `groups`-Claim mehr („Overage“); dann grei
|
|||
|
||||
</details>
|
||||
|
||||
Wichtig: Jeder Benutzer, der sich per Microsoft anmelden soll, braucht in WordPress **dieselbe E-Mail-Adresse** wie in Microsoft 365.
|
||||
<details>
|
||||
<summary><strong>Zusätzlich für den Benutzer-Sync</strong></summary>
|
||||
|
||||
| Berechtigung (Anwendung, mit Administratorzustimmung) | Wofür |
|
||||
| --- | --- |
|
||||
| `User.Read.All` | Benutzer, Kontostatus, Profilfelder und Profilbilder lesen |
|
||||
| `GroupMember.Read.All` | Nur nötig, wenn Sync-Gruppen oder Rollen-Zuordnungen verwendet werden |
|
||||
|
||||
Außerdem muss im Tab *Verbindung* die **Tenant-GUID** eingetragen sein (nicht `organizations`/`common`).
|
||||
|
||||
</details>
|
||||
|
||||
Wichtig: Jeder Benutzer, der sich per Microsoft anmelden soll, braucht in WordPress **dieselbe E-Mail-Adresse** wie in Microsoft 365 – von Hand angelegt oder vom [Benutzer-Sync](#benutzer-sync) importiert.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -272,6 +286,65 @@ Das Plugin funktioniert auch, wenn die Anmeldung nicht über `wp-login.php` läu
|
|||
|
||||
Plugins, die `wp-login.php` umbenennen (z. B. WPS Hide Login), sind kompatibel, weil das Plugin durchgehend `wp_login_url()` verwendet.
|
||||
|
||||
### Benutzer-Sync
|
||||
|
||||
Tab *Benutzer-Sync*. Legt WordPress-Konten für Microsoft-365-Benutzer an und hält sie aktuell – manuell per Knopfdruck,
|
||||
automatisch per WP-Cron (stündlich, zweimal täglich, täglich) oder per WP-CLI.
|
||||
|
||||
**Welche Benutzer?** Ohne Auswahl alle Mitglieder des Tenants; optional nur Mitglieder bestimmter Gruppen
|
||||
(verschachtelte Mitgliedschaften zählen). Gäste (B2B) nur auf Wunsch. Die Domain-Allowlist aus dem Tab *Sicherheit* gilt auch hier.
|
||||
|
||||
**Was passiert pro Benutzer?**
|
||||
|
||||
| Situation | Ergebnis |
|
||||
| --- | --- |
|
||||
| Kein WordPress-Konto vorhanden | Konto wird angelegt: Benutzername aus der E-Mail, Zufallspasswort, **keine E-Mail an den Benutzer**, Standardrolle + zugeordnete Rollen. Die Anmeldung läuft über den Microsoft-Button. |
|
||||
| Konto mit derselben E-Mail existiert schon | Wird mit der Microsoft-Objekt-ID verknüpft, Profilfelder werden aktualisiert. Rollen bleiben unangetastet, außer *„Auch die Rollen von Konten verwalten, die schon vor dem Sync existierten“* ist aktiv. |
|
||||
| Bereits verknüpft | E-Mail-Adresse, Profilfelder, Profilbild und (bei importierten Konten) Rollen werden aktualisiert. |
|
||||
| In Microsoft 365 **deaktiviert** | Wahlweise nichts tun, WordPress-Konto **deaktivieren** oder **löschen**. |
|
||||
| In Microsoft 365 **gelöscht** | Wahlweise nichts tun, deaktivieren oder löschen. |
|
||||
| Nicht mehr in den Sync-Gruppen | Wahlweise nichts tun, deaktivieren oder löschen. |
|
||||
| Wieder aktiv in Microsoft 365 | Vom Sync deaktivierte Konten werden automatisch reaktiviert (von Hand deaktivierte nicht). |
|
||||
|
||||
**Rollen.** Jeder importierte Benutzer bekommt die *Standardrolle*. Darunter lassen sich Microsoft-365-Gruppen per Suche
|
||||
auswählen und je einer WordPress-Rolle zuordnen (z. B. „Redaktion“ → Redakteur). Zwei Modi:
|
||||
|
||||
- *Zusätzlich zur Standardrolle* – der Benutzer hat danach mehrere Rollen.
|
||||
- *Anstelle der Standardrolle* – die erste passende Gruppe der Liste gewinnt (Reihenfolge per ↑).
|
||||
|
||||
Verlässt jemand eine Gruppe, wird die Rolle beim nächsten Lauf entfernt. Die Rollen importierter Konten verwaltet der Sync
|
||||
vollständig – manuelle Änderungen werden überschrieben.
|
||||
|
||||
**Profilfelder.** Frei wählbar: Anzeigename, Vor- und Nachname, Profilbild, Position, Abteilung, Firma, Büro, Personalnummer,
|
||||
Telefon (geschäftlich/mobil), Adresse, Sprache. Namen landen in den normalen WordPress-Feldern, alles andere in User-Meta mit
|
||||
dem Präfix `m365_` (z. B. `m365_department`) und wird auf der Profilseite angezeigt. Das **Profilbild** wird nach
|
||||
`wp-content/uploads/m365-login-avatars/` geladen (Dateiname mit gesalzenem Hash statt Objekt-ID) und ersetzt überall den
|
||||
Gravatar; es wird etwa einmal täglich pro Benutzer geprüft. Achtung: Avatare sind öffentlich sichtbar, wo WordPress sie anzeigt.
|
||||
|
||||
**Deaktivierte Konten** können sich überhaupt nicht mehr anmelden – weder per Microsoft noch per Passwort,
|
||||
Anwendungspasswort oder bestehender Session (alle Sessions werden beendet). In der Benutzerliste zeigt die Spalte
|
||||
*Microsoft 365* den Status; per Zeilenaktion lassen sich Konten auch von Hand deaktivieren und reaktivieren.
|
||||
|
||||
**Löschen** braucht einen Benutzer, der die Beiträge übernimmt. Ohne Auswahl wird stattdessen deaktiviert – es gehen nie Inhalte verloren.
|
||||
|
||||
**Schutzmechanismen**
|
||||
|
||||
- **Testlauf:** zeigt vollständig, was angelegt, geändert, deaktiviert oder gelöscht würde – ohne etwas zu ändern.
|
||||
- **Sicherheitsstopp:** Würde ein Lauf mehr als 20 % der verknüpften Konten (mindestens 5) deaktivieren oder löschen,
|
||||
passiert gar nichts (Filter `m365_login_sync_deprovision_limit`).
|
||||
- **Fehler = Abbruch:** Schlägt eine Graph-Anfrage fehl, bricht der Lauf ab, bevor irgendein Konto deaktiviert wird.
|
||||
„Gelöscht“ gilt ein Konto nur, wenn Graph für genau diese Objekt-ID `404` liefert.
|
||||
- **Geschützte Konten:** Administratoren, die schon vor dem Sync existierten, und das eigene Konto werden nie
|
||||
deaktiviert, gelöscht oder umgestuft (Filter `m365_login_sync_protect_user`).
|
||||
- **Sperre gegen Parallelläufe**, Protokoll der letzten Ausführung im Backend.
|
||||
|
||||
**WP-CLI** – empfehlenswert für große Verzeichnisse oder exakte Zeiten per System-Cron:
|
||||
|
||||
```bash
|
||||
wp m365-login sync --dry-run # Testlauf
|
||||
wp m365-login sync # echter Lauf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sicherheitskonzept
|
||||
|
|
@ -284,7 +357,9 @@ Plugins, die `wp-login.php` umbenennen (z. B. WPS Hide Login), sind kompatibel,
|
|||
| Gefälschte Tokens | Signaturprüfung gegen Microsofts JWKS (`RS256` only; `alg=none`/HMAC werden abgelehnt), Schlüssel-Rollover wird automatisch nachgeladen. `iss`, `aud`, `tid`, `exp`, `nbf`, `iat` werden geprüft. |
|
||||
| Fremde Tenants | Bei konfigurierter Tenant-GUID **Tenant-Pinning**; sonst Issuer-Konsistenz mit `tid`. |
|
||||
| Kontoübernahme per E-Mail-Recycling | **Bindung an die Objekt-ID** (`oid`) beim ersten Login. |
|
||||
| Unbefugte Konten | Kein Provisioning, optionale Domain-Allowlist, optionale Gruppen-Beschränkung (fail closed). |
|
||||
| Unbefugte Konten | Der Login legt keine Konten an; optionale Domain-Allowlist, optionale Gruppen-Beschränkung (fail closed). Konten entstehen nur durch den explizit gestarteten bzw. aktivierten Benutzer-Sync. |
|
||||
| Ausgeschiedene Mitarbeitende | Benutzer-Sync deaktiviert oder löscht Konten, die in Microsoft 365 deaktiviert/gelöscht wurden; deaktivierte Konten verlieren sofort alle Sessions und jeden Anmeldeweg. |
|
||||
| Massen-Deprovisionierung durch Fehlkonfiguration | Testlauf, Sicherheitsstopp (> 20 % / min. 5), Abbruch bei jedem Graph-Fehler, „gelöscht“ nur bei `404` für die konkrete Objekt-ID, geschützte Administratoren. |
|
||||
| Secret-Diebstahl aus der Datenbank | AES-256-GCM, Schlüssel per HKDF aus `AUTH_KEY`/`SECURE_AUTH_KEY`; ohne `wp-config.php` ist der Datensatz wertlos. Gilt für Client Secret und privaten Zertifikatsschlüssel. |
|
||||
| Secret-Abfluss im Transport | Zertifikatsmodus: es wird nie ein Geheimnis übertragen, nur eine 5 Minuten gültige, signierte Client Assertion (RFC 7523). |
|
||||
| Kontoübernahme im Multi-Tenant-Modus | `email`-Claim fremder Tenants wird ignoriert (nur UPN mit verifizierter Domain oder `xms_edov`). |
|
||||
|
|
@ -346,6 +421,38 @@ add_filter( 'm365_login_block_password_login', function ( $block, WP_User $user
|
|||
add_filter( 'm365_login_redirect_uri', fn( $uri ) => 'https://www.example.com/m365-login/callback' );
|
||||
```
|
||||
|
||||
**Benutzer-Sync**
|
||||
|
||||
```php
|
||||
// Weiteres Graph-Attribut anbieten (landet in User-Meta "m365_cost_center")
|
||||
add_filter( 'm365_login_sync_attributes', function ( $attributes ) {
|
||||
$attributes['costCenter'] = array( 'label' => 'Kostenstelle', 'target' => 'm365_cost_center' );
|
||||
return $attributes;
|
||||
} );
|
||||
|
||||
// Rollen pro Person anpassen (erste Rolle = Hauptrolle)
|
||||
add_filter( 'm365_login_sync_roles', function ( array $roles, $oid ) {
|
||||
return $roles;
|
||||
}, 10, 2 );
|
||||
|
||||
// Daten für neu angelegte Konten (Argumente für wp_insert_user)
|
||||
add_filter( 'm365_login_sync_new_user_data', function ( array $data, array $person ) {
|
||||
$data['user_login'] = strtolower( $person['userPrincipalName'] );
|
||||
return $data;
|
||||
}, 10, 2 );
|
||||
|
||||
// Weitere Konten vom Sync ausnehmen
|
||||
add_filter( 'm365_login_sync_protect_user', function ( $protected, WP_User $user ) {
|
||||
return $protected || in_array( 'shop_manager', $user->roles, true );
|
||||
}, 10, 2 );
|
||||
|
||||
// Sicherheitsstopp anheben (Standard: 20 % der verknüpften Konten, mindestens 5)
|
||||
add_filter( 'm365_login_sync_deprovision_limit', fn( $limit, $linked ) => max( 10, $linked * 0.5 ), 10, 2 );
|
||||
|
||||
// Weitere: m365_login_sync_email, m365_login_sync_photo_limit, m365_login_sync_photo_interval
|
||||
// Actions: m365_login_sync_user_created, m365_login_sync_finished, m365_login_user_disabled, m365_login_user_enabled
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehebung
|
||||
|
|
@ -361,6 +468,15 @@ add_filter( 'm365_login_redirect_uri', fn( $uri ) => 'https://www.example.com/m3
|
|||
| *This WordPress account is linked to a different Microsoft account.* | Objekt-ID weicht ab. Wenn gewollt (neues Microsoft-Konto): User-Meta `_m365_login_oid` beim Benutzer löschen. |
|
||||
| *… not a member of a group that is allowed …* | Benutzer ist in keiner der ausgewählten Gruppen. |
|
||||
| *Your group membership could not be verified.* | Graph nicht erreichbar oder Berechtigung fehlt (`User.Read.All`) – oder `groups`-Claim aktivieren. |
|
||||
| *This account has been deactivated.* | Das Konto wurde vom Benutzer-Sync oder von Hand deaktiviert. *Benutzer → Zeilenaktion „Reaktivieren“* – ist die Person in Microsoft 365 noch deaktiviert, deaktiviert der nächste Sync sie wieder. |
|
||||
|
||||
| Meldung im Sync-Protokoll | Ursache & Lösung |
|
||||
| --- | --- |
|
||||
| *Microsoft Graph refused the request …* | Anwendungsberechtigung `User.Read.All` (und bei Gruppen `GroupMember.Read.All`) fehlt oder keine Administratorzustimmung. |
|
||||
| *The user sync needs a pinned tenant ID …* | Im Tab *Verbindung* die Tenant-GUID statt `organizations`/`common` eintragen. |
|
||||
| *Safety stop: … accounts would be deactivated or deleted …* | Sync-Gruppen oder Tenant prüfen, Testlauf ansehen; bei gewollter Massenänderung das Limit per Filter anheben. |
|
||||
| *… is protected … and was not changed.* | Bestehender Administrator oder eigenes Konto – bewusst ausgenommen. |
|
||||
| Zeitüberschreitung beim Klick auf *Jetzt synchronisieren* | Der Lauf geht serverseitig weiter; Seite später neu laden. Für große Verzeichnisse `wp m365-login sync` verwenden. |
|
||||
|
||||
Logging: Mit `WP_DEBUG` und `WP_DEBUG_LOG` schreibt das Plugin Fehlerdetails mit Präfix `[M365 Login]` nach `wp-content/debug.log`. Es werden nie Tokens oder Secrets geloggt.
|
||||
|
||||
|
|
@ -376,7 +492,8 @@ includes/
|
|||
class-m365-login-crypto.php AES-256-GCM für das Client Secret
|
||||
class-m365-login-jwt.php RS256-Verifikation, JWKS → PEM
|
||||
class-m365-login-auth.php OAuth-Flow, Callback, Benutzerzuordnung, Nur-Button-Modus
|
||||
class-m365-login-graph.php Client-Credentials-Token, Gruppensuche, checkMemberGroups
|
||||
class-m365-login-graph.php Client-Credentials-Token, Paging, Benutzer/Gruppen/Fotos, checkMemberGroups
|
||||
class-m365-login-sync.php Benutzer-Sync, Rollen, Profilfelder/-bilder, Deaktivierung, WP-CLI
|
||||
class-m365-login-button.php Ausgabe auf wp-login.php, Shortcode
|
||||
class-m365-login-admin.php Einstellungsseite, AJAX
|
||||
assets/ CSS/JS für Login-Seite und Backend (unminifiziert)
|
||||
|
|
@ -412,7 +529,7 @@ Das Plugin bringt alles mit, was das Review-Team verlangt: `readme.txt` mit *Ext
|
|||
|
||||
<details>
|
||||
<summary><strong>Kann ich Benutzer automatisch anlegen lassen?</strong></summary>
|
||||
Nein, bewusst nicht. Der Admin entscheidet, wer ein Konto hat. Wer Auto-Provisioning braucht, kann es über den Hook <code>m365_login_allow_user</code> nicht nachrüsten – das wäre ein anderes Sicherheitsmodell.
|
||||
Ja, mit dem <a href="#benutzer-sync">Benutzer-Sync</a>: Er importiert alle (oder ausgewählte) Microsoft-365-Benutzer vorab als WordPress-Konten. Der Login selbst legt weiterhin nie Konten an – wer nicht importiert oder von Hand angelegt wurde, kommt nicht rein.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
|
@ -427,7 +544,12 @@ Ja. Einstellungen gelten pro Site; der Benutzer muss Mitglied der Site (oder Sup
|
|||
|
||||
<details>
|
||||
<summary><strong>Was passiert beim Deinstallieren?</strong></summary>
|
||||
Einstellungen, Caches (Transients) und die pro Benutzer gespeicherte Objekt-ID werden entfernt – auch in Multisite.
|
||||
Einstellungen, Caches (Transients), Sync-Protokoll, Cron-Termin, gespeicherte Profilbilder und die pro Benutzer gespeicherten Plugin-Daten (Objekt-ID, Deaktivierungs-Status) werden entfernt – auch in Multisite. Importierte Konten und übernommene Profilfelder (<code>m365_*</code>) bleiben erhalten. Deaktivierte Konten sind danach wieder aktiv; wer sie sperren will, sollte sie vorher löschen.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Benutzer-Sync und Multisite?</strong></summary>
|
||||
Der Sync arbeitet pro Site: Neue Konten werden zur aktuellen Site hinzugefügt, „Löschen“ entfernt das Konto nur aus dieser Site. Die Deaktivierung gilt netzwerkweit, weil sie am Benutzer hängt.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue