flarum-msteams-webhook/README.md

575 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# flarum-msteams-webhook
Flarum-Erweiterung zur Spiegelung ausgewählter Flarum-Benachrichtigungen in den **Microsoft Teams Activity Feed**.
Die Erweiterung ergänzt Flarum um einen eigenen Benachrichtigungskanal namens **Microsoft Teams**. Benutzer können diesen Kanal in ihren persönlichen Benachrichtigungseinstellungen aktivieren. Neue Aktivitäten beispielsweise Antworten in gefolgten Diskussionen oder Erwähnungen werden anschließend über Microsoft Graph an den Teams Activity Feed des jeweiligen Benutzers gesendet.
Beim Öffnen einer Teams-Benachrichtigung erscheint eine schlanke Zwischenansicht im persönlichen Teams-Tab. Diese enthält einen Link zur zugehörigen Flarum-Diskussion beziehungsweise direkt zum konkreten Beitrag. Das eigentliche Forum muss dadurch nicht in Microsoft Teams eingebettet werden und kann weiterhin mit restriktiven Frame-Sicherheitsheadern geschützt bleiben.
> **Projektstatus:** Interne Erweiterung für Flarum 1.8. Vor einem produktiven Einsatz sollten Konfiguration, Berechtigungen, Logging und Fehlerbehandlung an die jeweilige Umgebung angepasst und getestet werden.
## Funktionsumfang
- eigener Flarum-Benachrichtigungskanal `teams`
- rudimentäres Admin-UI zur Konfiguration der Microsoft-Graph-Anbindung
- Erweiterung der persönlichen Flarum-Benachrichtigungseinstellungen um Microsoft Teams
- asynchroner Versand über einen dedizierten Queue-Job
- Auflösung des Microsoft-Graph-Zielbenutzers über E-Mail-Adresse, UPN oder eine Benutzerpräferenz
- Abruf eines App-only Access Tokens über Microsoft Entra ID
- Versand von Activity-Feed-Benachrichtigungen über Microsoft Graph
- Vorschautext mit auslösendem Benutzer und Diskussionstitel
- Teams Deep Link zu einem persönlichen Teams-Tab
- Übergabe der konkreten Flarum-Ziel-URL über `context.subEntityId`
- Auslesen des Ziels im Teams-Tab über TeamsJS v2 und `context.page.subPageId`
- sichere Zwischenansicht mit validiertem Link zur Diskussion oder zum Beitrag
- Test-Command für einzelne Flarum-Benutzer
## Unterstützte Benachrichtigungstypen
Die Erweiterung enthält Unterstützung beziehungsweise Textvorlagen für folgende Flarum-Typen:
- `newPost` neue Antwort in einer gefolgten Diskussion
- `postMentioned` Erwähnung in einem Beitrag
- `userMentioned` Benutzererwähnung
- `postLiked` eigener Beitrag wurde mit „Gefällt mir“ markiert
- `discussionRenamed` Diskussion wurde umbenannt
- `teamsTest` interne Testbenachrichtigung des CLI-Commands
Welche Typen tatsächlich in den persönlichen Einstellungen angeboten oder standardmäßig aktiviert werden, wird durch die registrierten Flarum-Blueprints und die Frontend-Erweiterung bestimmt.
## Systemvoraussetzungen
- PHP 8.1 oder neuer
- Flarum 1.8
- Composer
- Node.js und npm zum Bauen des Flarum-Frontends
- ein funktionierender Flarum-Queue-Worker
- öffentlich beziehungsweise für Microsoft Teams erreichbare HTTPS-URL
- Microsoft-Entra-App-Registrierung mit den erforderlichen Microsoft-Graph-Berechtigungen
- installierte beziehungsweise im Mandanten veröffentlichte Microsoft-Teams-App
Relevante Composer-Abhängigkeiten:
```json
{
"php": "^8.1",
"flarum/core": "^1.8",
"guzzlehttp/guzzle": "^7.8"
}
```
## Architektur und Ablauf
Der Versand läuft vereinfacht über folgende Komponenten:
```text
Flarum Notification Blueprint
-> TeamsNotificationDriver
-> SendTeamsActivityNotificationJob
-> TeamsActivityNotifier
-> NotificationPayloadFactory
-> Microsoft Graph sendActivityNotification
-> Microsoft Teams Activity Feed
```
### Notification Driver
`TeamsNotificationDriver` ist in `extend.php` als Flarum Notification Driver registriert:
```php
(new Extend\Notification())
->driver('teams', TeamsNotificationDriver::class, []),
```
Der Driver prüft die Benachrichtigungseinstellungen der Empfänger und legt den Versand als `SendTeamsActivityNotificationJob` in der Queue `teams` ab.
### Queue-Job
`SendTeamsActivityNotificationJob` führt den eigentlichen Versand außerhalb des Web-Requests aus. Der Job besitzt Wiederholungs- und Backoff-Einstellungen, damit vorübergehende Graph- oder Netzwerkfehler nicht sofort zum endgültigen Verlust einer Nachricht führen.
Nach Änderungen am PHP-Code müssen lang laufende Queue-Worker neu gestartet werden, da sie andernfalls weiterhin bereits geladene Klassen verwenden können.
### Token Provider
`GraphTokenProvider` ruft über den OAuth-2.0-Client-Credentials-Flow ein App-only Access Token bei Microsoft Entra ID ab. Tenant-ID, Client-ID und Client Secret werden aus den Flarum-Einstellungen gelesen.
Secrets dürfen weder in das Git-Repository eingecheckt noch in Logdateien geschrieben werden.
### Zielauflösung
`TargetResolver` ordnet einen Flarum-Benutzer einem Microsoft-Graph-Benutzer zu. Unterstützt werden abhängig von der Admin-Konfiguration unter anderem:
- Flarum-E-Mail-Adresse
- Microsoft Entra User Principal Name (UPN)
- benutzerdefinierte Flarum-Präferenz
Der aufgelöste Wert wird für den Graph-Endpunkt verwendet:
```text
POST https://graph.microsoft.com/v1.0/users/{target}/teamwork/sendActivityNotification
```
### Payload-Erzeugung
`NotificationPayloadFactory` erstellt den Request-Body für den Microsoft Teams Activity Feed. Der Payload enthält unter anderem:
- `topic.source`
- `topic.value`
- `topic.webUrl`
- `activityType`
- `previewText`
- `templateParameters`
Aktuell wird `systemDefault` als Activity Type verwendet. Dadurch ist kein eigener Activity Type im Teams-Manifest erforderlich.
Für Discussion- und Post-Subjects wird eine passende Ziel-URL erzeugt:
```text
https://forum.example/d/{discussionId}
https://forum.example/d/{discussionId}/{postNumber}
```
Die Ziel-URL wird nicht direkt im Teams-Frame geladen. Stattdessen wird sie im Deep Link als `context.subEntityId` an den persönlichen Teams-Tab übergeben.
## Zwischenansicht im Teams-Tab
Die Teams-App lädt als Personal Tab eine statische Seite, beispielsweise:
```text
https://forum.example/static/flarum-notify-v2.html
```
Die Seite bindet TeamsJS ein und liest den Deep-Link-Kontext aus:
```javascript
await window.microsoftTeams.app.initialize();
const context = await window.microsoftTeams.app.getContext();
const target = context.page.subPageId;
```
Anschließend wird nur dann ein Link angezeigt, wenn:
- die Ziel-URL dieselbe Origin wie die Landing Page besitzt und
- der Pfad mit `/d/` beginnt.
Dadurch kann `subEntityId` nicht als offener Redirect auf fremde Domains missbraucht werden.
Die Seite unterstützt zusätzlich einen direkten Browser-Test über einen Query-Parameter:
```text
https://forum.example/static/flarum-notify-v2.html?target=https%3A%2F%2Fforum.example%2Fd%2F123%2F17
```
## Teams-App-Manifest
Das Teams-Manifest benötigt mindestens einen persönlichen statischen Tab und die Forum-Domain in `validDomains`.
Beispiel:
```json
{
"staticTabs": [
{
"entityId": "YOUR_TEAMS_ENTITY_ID",
"name": "Flarum Notify",
"contentUrl": "https://forum.example/static/flarum-notify-v2.html",
"websiteUrl": "https://forum.example/static/flarum-notify-v2.html",
"scopes": [
"personal"
],
"context": [
"personalTab"
]
}
],
"validDomains": [
"forum.example"
]
}
```
Die folgenden Werte müssen zwischen Manifest und Erweiterung übereinstimmen:
- Teams App ID
- Entity ID des Personal Tabs
- öffentliche Forum-Basis-URL
## Microsoft Entra ID und Graph
Für den App-only-Versand werden eine Microsoft-Entra-App-Registrierung und geeignete Microsoft-Graph-Anwendungsberechtigungen benötigt. Die konkrete Berechtigungskonfiguration hängt vom verwendeten Graph-Endpunkt, dem Mandanten und den internen Sicherheitsrichtlinien ab.
Erforderliche Konfigurationswerte sind typischerweise:
- Tenant ID
- Client ID
- Client Secret
- Teams App ID
- Forum Base URL
- Strategie zur Auflösung des Zielbenutzers
Nach dem Hinzufügen von Application Permissions ist in der Regel eine Administratorzustimmung im Microsoft-Entra-Mandanten erforderlich.
## Flarum-Admin-Einstellungen
Das Admin-Frontend enthält Einstellungen für:
- Aktivierung der Erweiterung
- Tenant ID
- Client ID
- Client Secret
- Forum Base URL
- User Lookup Strategy
- UPN Preference Key
- User Preference Key
- Activity Type
- Teams App ID
- optionale Icon ID
- HTTP Timeout
Beispiel für die Forum Base URL:
```text
https://forum.example
```
Keinen abschließenden Pfad wie `/public`, `/api` oder `/static` eintragen.
## Installation
### 1. Erweiterung bereitstellen
Das Repository muss als Composer-Paket beziehungsweise lokales Flarum-Paket verfügbar sein. Danach im Flarum-Root die Abhängigkeiten beziehungsweise den Autoloader aktualisieren:
```bash
cd /var/www/html/flarum
composer dump-autoload
```
Je nach Installationsart kann zuvor ein `composer require` oder ein Composer-Path-Repository erforderlich sein.
### 2. Admin- und Forum-Frontend bauen
```bash
cd js
npm install
npm run build
```
Für die Entwicklung mit Watch-Modus:
```bash
cd js
npm install
npm run dev
```
### 3. Flarum-Cache leeren
Im Flarum-Root:
```bash
php flarum cache:clear
```
### 4. Queue-Worker neu starten
Bei Supervisor beispielsweise:
```bash
sudo supervisorctl restart all
sudo supervisorctl status
```
In einer produktiven Umgebung sollte möglichst nur die betroffene Worker-Gruppe neu gestartet werden.
## Statische Teams-Dateien
Die Landing Page besteht aus:
```text
flarum-notify-v2.html
flarum-notify-v2.js
```
Bei einer Apache-Konfiguration mit folgendem Alias:
```apache
Alias "/static" "/var/www/html/static"
```
liegen die Dateien beispielsweise hier:
```text
/var/www/html/static/flarum-notify-v2.html
/var/www/html/static/flarum-notify-v2.js
```
## Apache-, CSP- und Frame-Konfiguration
Teams stellt Personal Tabs in einem eingebetteten Browserkontext dar. Ein globales
```http
X-Frame-Options: DENY
```
oder
```http
Content-Security-Policy: frame-ancestors 'none'
```
blockiert deshalb die Landing Page und erzeugt im Teams-Desktop typischerweise eine weiße Fläche.
Das eigentliche Forum kann weiterhin global gegen Framing geschützt bleiben. Nur die statische Landing Page sollte für Microsoft Teams freigegeben werden.
Beispiel für Apache:
```apache
<IfModule mod_headers.c>
<LocationMatch "^/static/flarum-notify-v2\.html$">
Header always unset X-Frame-Options
Header always unset Content-Security-Policy
Header always unset Content-Security-Policy-Report-Only
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://res.cdn.office.net; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'self' https://teams.microsoft.com https://*.teams.microsoft.com https://*.cloud.microsoft; base-uri 'self'; form-action 'self'"
Header always set Cache-Control "no-cache, must-revalidate, max-age=0"
</LocationMatch>
</IfModule>
```
Anschließend:
```bash
sudo apache2ctl configtest
sudo systemctl reload apache2
```
Header prüfen:
```bash
curl -sSI https://forum.example/static/flarum-notify-v2.html
```
Für die Landing Page darf kein restriktiver `X-Frame-Options`-Header mehr ausgeliefert werden. Die CSP muss die benötigten Teams-Hosts als `frame-ancestors` und das TeamsJS-CDN unter `script-src` erlauben.
Das eigentliche Forum insbesondere `/d/...`, Login und Admin-Bereiche muss für diesen Ansatz nicht in einem Frame freigegeben werden.
## Cache-Verhalten in Microsoft Teams
Microsoft Teams kann persönliche Tabs im Desktop-Client im Speicher halten beziehungsweise suspendieren. Änderungen an HTML oder JavaScript werden deshalb nicht immer bei jedem Klick sofort neu geladen.
Empfehlungen:
- Landing-Page-HTML mit `Cache-Control: no-cache, must-revalidate` ausliefern
- JavaScript bei Änderungen versionieren, zum Beispiel:
```html
<script src="/static/flarum-notify-v2.js?v=20260728-1" defer></script>
```
- nach größeren Änderungen Teams vollständig beenden und neu starten
- ein manuelles Löschen des vollständigen Teams-Caches sollte im Normalbetrieb nicht erforderlich sein
## Test-Command
Eine Testbenachrichtigung kann an einen einzelnen Flarum-Benutzer gesendet werden:
```bash
php flarum teams:test-user <flarumUserId>
```
Beispiel:
```bash
php flarum teams:test-user 42
```
Nur das aufgelöste Graph-Ziel anzeigen:
```bash
php flarum teams:test-user 42 --show-target
```
Ziel testweise überschreiben:
```bash
php flarum teams:test-user 42 --target user@example.com
```
Der Test-Blueprint besitzt keinen realen Discussion- oder Post-Subject. Deshalb führt eine reine Testbenachrichtigung üblicherweise nicht zu einem konkreten Beitragslink, sondern zur allgemeinen Zwischenansicht.
## Benutzerkonfiguration
Die Erweiterung ergänzt die persönlichen Flarum-Benachrichtigungseinstellungen um Microsoft Teams. Benutzer können Teams für die angebotenen Benachrichtigungstypen ein- oder ausschalten.
Wenn ein Benutzer keine Teams-Benachrichtigungen erhält, sollten folgende Punkte geprüft werden:
1. Ist Microsoft Teams für den betreffenden Typ in Flarum aktiviert?
2. Kann `TargetResolver` eine E-Mail-Adresse oder einen UPN auflösen?
3. Ist die Teams-App für den Benutzer installiert beziehungsweise verfügbar?
4. Besitzt die Entra-App die benötigten Berechtigungen und Admin Consent?
5. Läuft der Queue-Worker?
6. Enthalten Worker- und Supervisor-Logs Graph-Fehler?
## Fehlersuche
### Weiße Fläche im Teams-Desktop
Response Header prüfen:
```bash
curl -sSI https://forum.example/static/flarum-notify-v2.html
```
Typische Ursachen:
- `X-Frame-Options: DENY`
- `X-Frame-Options: SAMEORIGIN`
- `frame-ancestors 'none'`
- `frame-ancestors 'self'` ohne Teams-Domains
### Seite wird ohne CSS dargestellt
Wenn CSS als `<style>`-Block in der HTML-Datei enthalten ist, benötigt die CSP entweder einen Hash/Nonce oder für diese kleine interne statische Seite beispielsweise:
```text
style-src 'self' 'unsafe-inline'
```
Langfristig kann das CSS in eine separate Datei ausgelagert werden, damit `'unsafe-inline'` nicht erforderlich ist.
### Allgemeiner Link statt konkretem Beitrag
Prüfen, ob die PHP-Datei den Deep-Link-Kontext erzeugt:
```bash
grep -nE 'subEntityId|context|subjectUrl' \
vendor/sbp-jm/flarum-msteams-webhook/src/Support/NotificationPayloadFactory.php
```
Prüfen, ob die Landing Page TeamsJS lädt und `page.subPageId` ausliest:
```bash
grep -nE 'microsoftTeams|subPageId|getContext|initialize' \
/var/www/html/static/flarum-notify-v2.js
```
Nur neu versendete Activity-Feed-Einträge enthalten den aktualisierten Deep Link. Bereits vorhandene Teams-Aktivitäten werden nicht nachträglich geändert.
### Änderungen am PHP-Code werden nicht verwendet
Queue-Worker neu starten:
```bash
php flarum cache:clear
sudo supervisorctl restart all
```
### Queue und Supervisor prüfen
```bash
sudo supervisorctl status
```
Logs abhängig von der lokalen Supervisor-Konfiguration beispielsweise:
```bash
sudo supervisorctl tail \
flarum-teams-worker:flarum-teams-worker_00 \
stderr
```
### HTTP-Aufrufe der Landing Page beobachten
```bash
sudo tail -f /var/log/apache2/access.log |
grep --line-buffered 'flarum-notify-v2'
```
Teams kann den Tab nach dem ersten Laden im Speicher halten. Weitere Klicks müssen daher nicht zwingend neue HTTP-Requests erzeugen.
## Projektstruktur
Wichtige Dateien und Verzeichnisse:
```text
extend.php
composer.json
js/
admin.js
forum.js
src/admin/
src/forum/
locale/
de.yml
en.yml
src/
Console/
TestTeamsUserCommand.php
Job/
SendTeamsActivityNotificationJob.php
Notification/
TeamsNotificationDriver.php
TestActivityBlueprint.php
Service/
GraphTokenProvider.php
TeamsActivityNotifier.php
Support/
NotificationPayloadFactory.php
TargetResolver.php
```
Im Repository können zusätzlich ältere oder experimentelle Klassen wie `TeamsChannel` oder `TeamsDriver` vorhanden sein. Der aktuell in `extend.php` registrierte Einstiegspunkt ist `TeamsNotificationDriver`.
## Sicherheitshinweise
- Client Secrets niemals im Repository speichern.
- Tokens und vollständige Graph-Payloads nur kurzfristig und geschützt protokollieren.
- Die Landing Page darf nur Links zur eigenen Forum-Origin akzeptieren.
- Das eigentliche Forum nicht pauschal für fremde Frames freigeben.
- HTTPS ist verpflichtend.
- Berechtigungen der Entra-App nach dem Least-Privilege-Prinzip vergeben.
- Produktive Secrets regelmäßig rotieren.
- Logdateien auf Benutzerkennungen, E-Mail-Adressen und Diskussionstitel prüfen und angemessen schützen.
## Entwicklung
Frontend-Abhängigkeiten installieren:
```bash
cd js
npm install
```
Produktions-Build:
```bash
npm run build
```
Watch-Modus:
```bash
npm run dev
```
Formatierung:
```bash
npm run format
```
Nach einem Frontend-Build:
```bash
cd /var/www/html/flarum
php flarum cache:clear
```
Nach PHP-Änderungen zusätzlich den Queue-Worker neu starten.
## Lizenz
MIT siehe `composer.json` beziehungsweise eine vorhandene `LICENSE`-Datei.