From 99003f669c81db0c04a01232a7835988fac18af4 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 24 Oct 2024 18:50:20 +0200 Subject: [PATCH 01/27] Update email templates to v1.0.122. (#4308) --- .../5-internal/email-templates-v1.0.122 | 1 + services/brig/deb/opt/brig/template-version | 2 +- .../de/provider/email/activation.html | 2 +- .../de/provider/email/activation.txt | 4 +-- .../de/provider/email/approval-request.html | 2 +- .../de/provider/email/approval-request.txt | 4 +-- .../templates/de/team/email/invitation.html | 2 +- .../templates/de/team/email/invitation.txt | 8 ++--- .../de/team/email/migration-subject.txt | 1 + .../templates/de/team/email/migration.html | 1 + .../templates/de/team/email/migration.txt | 33 +++++++++++++++++++ .../de/team/email/new-member-welcome.html | 2 +- .../de/team/email/new-member-welcome.txt | 2 +- .../templates/de/user/email/activation.html | 2 +- .../templates/de/user/email/activation.txt | 4 +-- .../templates/de/user/email/deletion.html | 2 +- .../brig/templates/de/user/email/deletion.txt | 2 +- .../de/user/email/password-reset.html | 2 +- .../de/user/email/password-reset.txt | 2 +- .../de/user/email/team-activation.html | 2 +- .../de/user/email/team-activation.txt | 4 +-- .../brig/templates/de/user/email/update.html | 2 +- .../brig/templates/de/user/email/update.txt | 4 +-- .../en/provider/email/activation.html | 2 +- .../en/provider/email/activation.txt | 2 +- .../en/provider/email/approval-request.html | 2 +- .../en/provider/email/approval-request.txt | 2 +- .../templates/en/team/email/invitation.html | 2 +- .../templates/en/team/email/invitation.txt | 6 ++-- .../en/team/email/migration-subject.txt | 1 + .../templates/en/team/email/migration.html | 1 + .../templates/en/team/email/migration.txt | 30 +++++++++++++++++ .../en/team/email/new-member-welcome.html | 2 +- .../en/team/email/new-member-welcome.txt | 2 +- .../templates/en/user/email/activation.html | 2 +- .../templates/en/user/email/activation.txt | 2 +- .../templates/en/user/email/deletion.html | 2 +- .../brig/templates/en/user/email/deletion.txt | 2 +- .../en/user/email/password-reset.html | 2 +- .../en/user/email/password-reset.txt | 2 +- .../en/user/email/team-activation.html | 2 +- .../en/user/email/team-activation.txt | 2 +- .../brig/templates/en/user/email/update.html | 2 +- .../brig/templates/en/user/email/update.txt | 2 +- .../templates/et/user/email/activation.html | 2 +- .../templates/et/user/email/activation.txt | 4 +-- .../templates/et/user/email/deletion.html | 2 +- .../brig/templates/et/user/email/deletion.txt | 4 +-- .../et/user/email/password-reset.html | 2 +- .../et/user/email/password-reset.txt | 4 +-- .../et/user/email/team-activation.html | 2 +- .../et/user/email/team-activation.txt | 4 +-- .../brig/templates/et/user/email/update.html | 2 +- .../brig/templates/et/user/email/update.txt | 4 +-- .../templates/fr/user/email/activation.html | 2 +- .../templates/fr/user/email/activation.txt | 4 +-- .../templates/fr/user/email/deletion.html | 2 +- .../brig/templates/fr/user/email/deletion.txt | 4 +-- .../fr/user/email/password-reset.html | 2 +- .../fr/user/email/password-reset.txt | 4 +-- .../fr/user/email/team-activation.html | 2 +- .../fr/user/email/team-activation.txt | 4 +-- .../brig/templates/fr/user/email/update.html | 2 +- .../brig/templates/fr/user/email/update.txt | 4 +-- .../brig/deb/opt/brig/templates/index.html | 2 +- .../templates/it/user/email/activation.html | 2 +- .../templates/it/user/email/activation.txt | 4 +-- .../templates/it/user/email/deletion.html | 2 +- .../brig/templates/it/user/email/deletion.txt | 4 +-- .../it/user/email/password-reset.html | 2 +- .../it/user/email/password-reset.txt | 4 +-- .../it/user/email/team-activation.html | 2 +- .../it/user/email/team-activation.txt | 4 +-- .../brig/templates/it/user/email/update.html | 2 +- .../brig/templates/it/user/email/update.txt | 4 +-- .../templates/ja/user/email/activation.html | 2 +- .../templates/ja/user/email/activation.txt | 3 +- .../templates/ja/user/email/deletion.html | 2 +- .../brig/templates/ja/user/email/deletion.txt | 3 +- .../ja/user/email/password-reset.html | 2 +- .../ja/user/email/password-reset.txt | 3 +- .../ja/user/email/team-activation.html | 2 +- .../ja/user/email/team-activation.txt | 3 +- .../brig/templates/ja/user/email/update.html | 2 +- .../brig/templates/ja/user/email/update.txt | 3 +- .../templates/lt/user/email/activation.html | 2 +- .../templates/lt/user/email/activation.txt | 4 +-- .../templates/lt/user/email/deletion.html | 2 +- .../brig/templates/lt/user/email/deletion.txt | 4 +-- .../lt/user/email/password-reset.html | 2 +- .../lt/user/email/password-reset.txt | 4 +-- .../lt/user/email/team-activation.html | 2 +- .../lt/user/email/team-activation.txt | 4 +-- .../brig/templates/lt/user/email/update.html | 2 +- .../brig/templates/lt/user/email/update.txt | 4 +-- .../templates/pl/user/email/activation.html | 2 +- .../templates/pl/user/email/activation.txt | 4 +-- .../templates/pl/user/email/deletion.html | 2 +- .../brig/templates/pl/user/email/deletion.txt | 4 +-- .../pl/user/email/password-reset.html | 2 +- .../pl/user/email/password-reset.txt | 4 +-- .../pl/user/email/team-activation.html | 2 +- .../pl/user/email/team-activation.txt | 6 ++-- .../brig/templates/pl/user/email/update.html | 2 +- .../brig/templates/pl/user/email/update.txt | 4 +-- .../templates/pt/user/email/activation.html | 2 +- .../templates/pt/user/email/activation.txt | 4 +-- .../templates/pt/user/email/deletion.html | 2 +- .../brig/templates/pt/user/email/deletion.txt | 4 +-- .../pt/user/email/password-reset.html | 2 +- .../pt/user/email/password-reset.txt | 4 +-- .../pt/user/email/team-activation.html | 2 +- .../pt/user/email/team-activation.txt | 6 ++-- .../brig/templates/pt/user/email/update.html | 2 +- .../brig/templates/pt/user/email/update.txt | 4 +-- .../templates/ru/user/email/activation.html | 2 +- .../templates/ru/user/email/activation.txt | 2 +- .../templates/ru/user/email/deletion.html | 2 +- .../brig/templates/ru/user/email/deletion.txt | 4 +-- .../ru/user/email/password-reset.html | 2 +- .../ru/user/email/password-reset.txt | 2 +- .../ru/user/email/team-activation.html | 2 +- .../ru/user/email/team-activation.txt | 4 +-- .../brig/templates/ru/user/email/update.html | 2 +- .../brig/templates/ru/user/email/update.txt | 2 +- .../templates/si/user/email/activation.html | 2 +- .../templates/si/user/email/activation.txt | 4 +-- .../templates/si/user/email/deletion.html | 2 +- .../brig/templates/si/user/email/deletion.txt | 4 +-- .../si/user/email/password-reset.html | 2 +- .../si/user/email/password-reset.txt | 4 +-- .../si/user/email/team-activation.html | 2 +- .../si/user/email/team-activation.txt | 6 ++-- .../brig/templates/si/user/email/update.html | 2 +- .../brig/templates/si/user/email/update.txt | 4 +-- .../templates/tr/user/email/activation.html | 2 +- .../templates/tr/user/email/activation.txt | 4 +-- .../templates/tr/user/email/deletion.html | 2 +- .../brig/templates/tr/user/email/deletion.txt | 4 +-- .../tr/user/email/password-reset.html | 2 +- .../tr/user/email/password-reset.txt | 4 +-- .../tr/user/email/team-activation.html | 2 +- .../tr/user/email/team-activation.txt | 6 ++-- .../brig/templates/tr/user/email/update.html | 2 +- .../brig/templates/tr/user/email/update.txt | 4 +-- services/brig/deb/opt/brig/templates/version | 2 +- .../templates/vi/user/email/activation.html | 2 +- .../templates/vi/user/email/activation.txt | 4 +-- .../templates/vi/user/email/deletion.html | 2 +- .../brig/templates/vi/user/email/deletion.txt | 4 +-- .../vi/user/email/password-reset.html | 2 +- .../vi/user/email/password-reset.txt | 4 +-- .../vi/user/email/team-activation.html | 2 +- .../vi/user/email/team-activation.txt | 6 ++-- .../brig/templates/vi/user/email/update.html | 2 +- .../brig/templates/vi/user/email/update.txt | 4 +-- 156 files changed, 284 insertions(+), 211 deletions(-) create mode 100644 changelog.d/5-internal/email-templates-v1.0.122 create mode 100644 services/brig/deb/opt/brig/templates/de/team/email/migration-subject.txt create mode 100644 services/brig/deb/opt/brig/templates/de/team/email/migration.html create mode 100644 services/brig/deb/opt/brig/templates/de/team/email/migration.txt create mode 100644 services/brig/deb/opt/brig/templates/en/team/email/migration-subject.txt create mode 100644 services/brig/deb/opt/brig/templates/en/team/email/migration.html create mode 100644 services/brig/deb/opt/brig/templates/en/team/email/migration.txt diff --git a/changelog.d/5-internal/email-templates-v1.0.122 b/changelog.d/5-internal/email-templates-v1.0.122 new file mode 100644 index 00000000000..d9bfa9e0a5d --- /dev/null +++ b/changelog.d/5-internal/email-templates-v1.0.122 @@ -0,0 +1 @@ +Updated email templates to v1.0.122 diff --git a/services/brig/deb/opt/brig/template-version b/services/brig/deb/opt/brig/template-version index fea60e70c1a..5c41189b952 100644 --- a/services/brig/deb/opt/brig/template-version +++ b/services/brig/deb/opt/brig/template-version @@ -1 +1 @@ -v1.0.121 +v1.0.122 diff --git a/services/brig/deb/opt/brig/templates/de/provider/email/activation.html b/services/brig/deb/opt/brig/templates/de/provider/email/activation.html index e0f5f48f6a9..940ad39572a 100644 --- a/services/brig/deb/opt/brig/templates/de/provider/email/activation.html +++ b/services/brig/deb/opt/brig/templates/de/provider/email/activation.html @@ -1 +1 @@ -Ihr ${brand_service}-Benutzerkonto

${brand_label_url}

Bestätigen Sie Ihre E-Mail-Adresse

Ihre E-Mail-Adresse ${email} wurde verwendet, um sich als ${brand_service} zu registrieren.

Um die Registrierung abzuschließen, bestätigen Sie bitte Ihre E-Mail-Adresse, indem Sie auf den unteren Button klicken.

Bitte beachten Sie, dass das Service-Provider-Konto nach der Bestätigung der E-Mail-Adresse noch durch uns freigeschaltet werden muss. Dies geschieht üblicherweise innerhalb von 24 Stunden. Sie werden in einer separaten E-Mail über die Freischaltung informiert.

 
Bestätigen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie sich nicht mit dieser E-Mail-Adresse für ein ${brand}-Benutzerkonto registriert haben, können Sie diese Nachricht ignorieren. Wenn Sie den Missbrauch Ihrer E-Mail-Adresse melden möchten, kontaktiere Sie uns bitte.

Bitte antworten Sie nicht auf diese Nachricht.

                                                           
\ No newline at end of file +Ihr ${brand_service}-Benutzerkonto

${brand_label_url}

Bestätigen Sie Ihre E-Mail-Adresse

Ihre E-Mail-Adresse ${email} wurde verwendet, um sich als ${brand_service} zu registrieren.

Um die Registrierung abzuschließen, bestätigen Sie bitte Ihre E-Mail-Adresse, indem Sie auf den unteren Button klicken.

Bitte beachten Sie, dass das Service-Provider-Konto nach der Bestätigung der E-Mail-Adresse noch durch uns freigeschaltet werden muss. Dies geschieht üblicherweise innerhalb von 24 Stunden. Sie werden in einer separaten E-Mail über die Freischaltung informiert.

 
Bestätigen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie sich nicht mit dieser E-Mail-Adresse für ein ${brand}-Benutzerkonto registriert haben, können Sie diese Nachricht ignorieren. Wenn Sie den Missbrauch Ihrer E-Mail-Adresse melden möchten, kontaktiere Sie uns bitte.

Bitte antworten Sie nicht auf diese Nachricht.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/provider/email/activation.txt b/services/brig/deb/opt/brig/templates/de/provider/email/activation.txt index 6447353b46f..8626e814a04 100644 --- a/services/brig/deb/opt/brig/templates/de/provider/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/de/provider/email/activation.txt @@ -14,8 +14,8 @@ E-Mail-Adresse noch durch uns freigeschaltet werden muss. Dies geschieht üblicherweise innerhalb von 24 Stunden. Sie werden in einer separaten E-Mail über die Freischaltung informiert. -Bestätigen [${url}]Falls Sie nicht auf den Button klicken können, kopieren Sie -diesen Link und fügen Sie ihn in Ihren Browser ein: +Bestätigen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren +Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.html b/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.html index c15511e2223..65e903084d2 100644 --- a/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.html +++ b/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.html @@ -1 +1 @@ -Genehmigungsanfrage: ${brand_service}

${brand_label_url}

Genehmigungsanfrage

Ein neuer ${brand_service} ist registriert und wartet auf die Genehmigung. Bitte lesen Sie die unten angegebenen Informationen.

Name: ${name}

E-Mail: ${email}

Website: ${url}

Beschreibung: ${description}

Wenn die Anfrage echt scheint, können Sie den Anbieter genehmigen, indem Sie auf den unteren Button klicken. Sobald genehmigt, kann sich der Anbieter anmelden und mit der Registrierung von Diensten beginnen, die ${brand}-Nutzer ihren Unterhaltungen hinzufügen können.

Falls die Anfrage zweifelhaft scheint, wenden Sie sich bitte an den Anbieter zur Klärung, bevor Sie fortfahren.

 
Genehmigen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Bitte antworten Sie nicht auf diese Nachricht.

                                                           
\ No newline at end of file +Genehmigungsanfrage: ${brand_service}

${brand_label_url}

Genehmigungsanfrage

Ein neuer ${brand_service} ist registriert und wartet auf die Genehmigung. Bitte lesen Sie die unten angegebenen Informationen.

Name: ${name}

E-Mail: ${email}

Website: ${url}

Beschreibung: ${description}

Wenn die Anfrage echt scheint, können Sie den Anbieter genehmigen, indem Sie auf den unteren Button klicken. Sobald genehmigt, kann sich der Anbieter anmelden und mit der Registrierung von Diensten beginnen, die ${brand}-Nutzer ihren Unterhaltungen hinzufügen können.

Falls die Anfrage zweifelhaft scheint, wenden Sie sich bitte an den Anbieter zur Klärung, bevor Sie fortfahren.

 
Genehmigen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Bitte antworten Sie nicht auf diese Nachricht.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.txt b/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.txt index efc6a992d13..5859663943b 100644 --- a/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.txt +++ b/services/brig/deb/opt/brig/templates/de/provider/email/approval-request.txt @@ -22,8 +22,8 @@ Unterhaltungen hinzufügen können. Falls die Anfrage zweifelhaft scheint, wenden Sie sich bitte an den Anbieter zur Klärung, bevor Sie fortfahren. -Genehmigen [${url}]Falls Sie nicht auf den Button klicken können, kopieren Sie -diesen Link und fügen Sie ihn in Ihren Browser ein: +Genehmigen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren +Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/team/email/invitation.html b/services/brig/deb/opt/brig/templates/de/team/email/invitation.html index 7abcafc58aa..56090496f2d 100644 --- a/services/brig/deb/opt/brig/templates/de/team/email/invitation.html +++ b/services/brig/deb/opt/brig/templates/de/team/email/invitation.html @@ -1 +1 @@ -Sie wurden eingeladen, einem ${brand}-Team beizutreten

${brand_label_url}

Einladung zum Team

${inviter} hat Sie auf ${brand} zu einem Team eingeladen. Klicken Sie bitte auf den unteren Button, um die Einladung anzunehmen.

 
Team beitreten
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

Was ist Wire?
Wire ist die sicherste Plattform für Ihre Kommunikation. Wo auch immer Sie sind, arbeiten Sie mit Ihrem Team und externen Partnern zusammen – mittels Nachrichten, Videokonferenzen und Dateiaustausch, alles mit Ende-zu-Ende-Verschlüsselung. Mehr erfahren.

                                                           
\ No newline at end of file +Sie wurden eingeladen, einem ${brand}-Team beizutreten

${brand_label_url}

Einladung zum Team

${inviter} hat Sie auf ${brand} zu einem Team eingeladen. Wählen Sie die folgende Schaltfläche, um die Einladung anzunehmen.

 
Team beitreten
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

Was ist Wire?
Wire ist die sicherste Plattform für Ihre Kommunikation. Wo auch immer Sie sind, arbeiten Sie mit Ihrem Team und externen Partnern zusammen – mittels Nachrichten, Videokonferenzen und Dateiaustausch, alles mit Ende-zu-Ende-Verschlüsselung. Mehr erfahren.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/team/email/invitation.txt b/services/brig/deb/opt/brig/templates/de/team/email/invitation.txt index 1301fc13af1..342ba5b4086 100644 --- a/services/brig/deb/opt/brig/templates/de/team/email/invitation.txt +++ b/services/brig/deb/opt/brig/templates/de/team/email/invitation.txt @@ -3,11 +3,11 @@ ${brand_label_url} [${brand_url}] EINLADUNG ZUM TEAM -${inviter} hat Sie auf ${brand} zu einem Team eingeladen. Klicken Sie bitte auf -den unteren Button, um die Einladung anzunehmen. +${inviter} hat Sie auf ${brand} zu einem Team eingeladen. Wählen Sie die +folgende Schaltfläche, um die Einladung anzunehmen. -Team beitreten [${url}]Falls Sie nicht auf den Button klicken können, kopieren -Sie diesen Link und fügen Sie ihn in Ihren Browser ein: +Team beitreten [${url}]Wenn Sie die Schaltfläche nicht auswählen können, +kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/team/email/migration-subject.txt b/services/brig/deb/opt/brig/templates/de/team/email/migration-subject.txt new file mode 100644 index 00000000000..3bd825679e9 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/de/team/email/migration-subject.txt @@ -0,0 +1 @@ +Sie wurden eingeladen, einem Team auf ${brand} beizutreten \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/team/email/migration.html b/services/brig/deb/opt/brig/templates/de/team/email/migration.html new file mode 100644 index 00000000000..5ea4eef51b2 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/de/team/email/migration.html @@ -0,0 +1 @@ +Sie wurden eingeladen, einem Team auf ${brand} beizutreten

${brand_label_url}

Einladung zum Team

${inviter} hat Sie auf ${brand} zu einem Team eingeladen.

Wenn Sie dem Team beitreten, wird Ihr persönliches Wire Benutzerkonto in ein Team-Konto umgewandelt.

Wählen Sie die folgende Schaltfläche, um mit der Einladung fortzufahren.

 
Team beitreten
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Missbräuchlichen Einladungslink melden

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

Was ist Wire?
Wire ist die sicherste Plattform für Ihre Kommunikation. Wo auch immer Sie sind, arbeiten Sie mit Ihrem Team und externen Partnern zusammen – mittels Nachrichten, Videokonferenzen und Dateiaustausch, alles mit Ende-zu-Ende-Verschlüsselung. Mehr erfahren.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/team/email/migration.txt b/services/brig/deb/opt/brig/templates/de/team/email/migration.txt new file mode 100644 index 00000000000..5c211d09a3e --- /dev/null +++ b/services/brig/deb/opt/brig/templates/de/team/email/migration.txt @@ -0,0 +1,33 @@ +[${brand_logo}] + +${brand_label_url} [${brand_url}] + +EINLADUNG ZUM TEAM +${inviter} hat Sie auf ${brand} zu einem Team eingeladen. + +Wenn Sie dem Team beitreten, wird Ihr persönliches Wire Benutzerkonto in ein +Team-Konto umgewandelt. + +Wählen Sie die folgende Schaltfläche, um mit der Einladung fortzufahren. + +Team beitreten [${url}]Wenn Sie die Schaltfläche nicht auswählen können, +kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein: + +${url} + +Missbräuchlichen Einladungslink melden [${support}] + +Wenn Sie Fragen haben, dann kontaktieren Sie uns [${support}] bitte. + +Was ist Wire? +Wire ist die sicherste Plattform für Ihre Kommunikation. Wo auch immer Sie sind, +arbeiten Sie mit Ihrem Team und externen Partnern zusammen – mittels +Nachrichten, Videokonferenzen und Dateiaustausch, alles mit +Ende-zu-Ende-Verschlüsselung. Mehr erfahren [https://wire.com/]. + + +-------------------------------------------------------------------------------- + +Datenschutzrichtlinien und Nutzungsbedingungen [${legal}] · Missbrauch melden +[${misuse}] +${copyright}. ALLE RECHTE VORBEHALTEN. \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.html b/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.html index 03c007c723f..f97a060f14b 100644 --- a/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.html +++ b/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.html @@ -1 +1 @@ -Sie sind einem Team auf ${brand} beigetreten

${brand_label_url}

Willkommen bei ${team_name}.

Sie sind soeben mit ${email} einem Team namens ${team_name} auf ${brand} beigetreten.

 

${brand} vereint sichere Verschlüsselung mit reichhaltigem Funktionsumfang und einfacher Bedienung in einer einzigen App. Unterstützt alle gängigen Plattformen.

 
${brand} herunterladen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

Team ID: ${team_id}

                                                           
\ No newline at end of file +Sie sind einem Team auf ${brand} beigetreten

${brand_label_url}

Willkommen bei ${team_name}.

Sie sind soeben mit ${email} einem Team namens ${team_name} auf ${brand} beigetreten.

 

${brand} vereint sichere Verschlüsselung mit reichhaltigem Funktionsumfang und einfacher Bedienung in einer einzigen App. Unterstützt alle gängigen Plattformen.

 
${brand} herunterladen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

Team ID: ${team_id}

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.txt b/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.txt index b0281ab55a9..b11a98b9728 100644 --- a/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.txt +++ b/services/brig/deb/opt/brig/templates/de/team/email/new-member-welcome.txt @@ -10,7 +10,7 @@ ${brand} vereint sichere Verschlüsselung mit reichhaltigem Funktionsumfang und einfacher Bedienung in einer einzigen App. Unterstützt alle gängigen Plattformen. -${brand} herunterladen [${url}]Falls Sie nicht auf den Button klicken können, +${brand} herunterladen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/user/email/activation.html b/services/brig/deb/opt/brig/templates/de/user/email/activation.html index ec58a8e1a32..29ce8712831 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/de/user/email/activation.html @@ -1 +1 @@ -Ihr ${brand}-Benutzerkonto

${brand_label_url}

Bestätigen Sie Ihre E-Mail-Adresse

${email} wurde verwendet, um ein Benutzerkonto auf ${brand} zu erstellen.
Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen.

 
Bestätigen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file +Ihr ${brand}-Benutzerkonto

${brand_label_url}

Bestätigen Sie Ihre E-Mail-Adresse

${email} wurde verwendet, um ein Benutzerkonto auf ${brand} zu erstellen.
Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen.

 
Bestätigen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/user/email/activation.txt b/services/brig/deb/opt/brig/templates/de/user/email/activation.txt index fbebd7779e7..27ab0068b88 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/de/user/email/activation.txt @@ -6,8 +6,8 @@ BESTÄTIGEN SIE IHRE E-MAIL-ADRESSE ${email} wurde verwendet, um ein Benutzerkonto auf ${brand} zu erstellen. Klicken Sie auf den folgenden Button, um Ihre E-Mail-Adresse zu bestätigen. -Bestätigen [${url}]Falls Sie nicht auf den Button klicken können, kopieren Sie -diesen Link und fügen Sie ihn in Ihren Browser ein: +Bestätigen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren +Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/user/email/deletion.html b/services/brig/deb/opt/brig/templates/de/user/email/deletion.html index 7c6ba323943..15959fc3888 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/de/user/email/deletion.html @@ -1 +1 @@ -Benutzerkonto löschen?

${brand_label_url}

Ihr Benutzerkonto löschen

Wir haben eine Anfrage zur Löschung Ihrer ${brand}-Benutzerkontos erhalten. Klicken Sie innerhalb der nächsten 10 Minuten auf den folgenden Link, um alle Ihre Unterhaltungen, Nachrichten und Kontakte zu löschen.

 
Benutzerkonto löschen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Falls Sie dies nicht angefordert haben, setzen Sie Ihr Passwort zurück.

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file +Benutzerkonto löschen?

${brand_label_url}

Ihr Benutzerkonto löschen

Wir haben eine Anfrage zur Löschung Ihrer ${brand}-Benutzerkontos erhalten. Klicken Sie innerhalb der nächsten 10 Minuten auf den folgenden Link, um alle Ihre Unterhaltungen, Nachrichten und Kontakte zu löschen.

 
Benutzerkonto löschen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Falls Sie dies nicht angefordert haben, setzen Sie Ihr Passwort zurück.

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/de/user/email/deletion.txt index 2dc9a61aa06..7e093b709de 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/de/user/email/deletion.txt @@ -7,7 +7,7 @@ Wir haben eine Anfrage zur Löschung Ihrer ${brand}-Benutzerkontos erhalten. Klicken Sie innerhalb der nächsten 10 Minuten auf den folgenden Link, um alle Ihre Unterhaltungen, Nachrichten und Kontakte zu löschen. -Benutzerkonto löschen [${url}]Falls Sie nicht auf den Button klicken können, +Benutzerkonto löschen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/de/user/email/password-reset.html index de528deb585..4546794f9df 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/de/user/email/password-reset.html @@ -1 +1 @@ -Änderung des Passworts auf ${brand}

${brand_label_url}

Passwort zurücksetzen

Wir haben eine Anfrage zum Zurücksetzen des Passworts für Ihr ${brand}-Benutzerkonto erhalten. Klicken Sie auf den folgenden Button, um ein neues Passwort zu erstellen.

 
Passwort zurücksetzen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file +Änderung des Passworts auf ${brand}

${brand_label_url}

Passwort zurücksetzen

Wir haben eine Anfrage zum Zurücksetzen des Passworts für Ihr ${brand}-Benutzerkonto erhalten. Klicken Sie auf den folgenden Button, um ein neues Passwort zu erstellen.

 
Passwort zurücksetzen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/de/user/email/password-reset.txt index 3378e9c7e1d..f42ac0e6cd7 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/de/user/email/password-reset.txt @@ -7,7 +7,7 @@ Wir haben eine Anfrage zum Zurücksetzen des Passworts für Ihr ${brand}-Benutzerkonto erhalten. Klicken Sie auf den folgenden Button, um ein neues Passwort zu erstellen. -Passwort zurücksetzen [${url}]Falls Sie nicht auf den Button klicken können, +Passwort zurücksetzen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/de/user/email/team-activation.html index 6818d31b724..77d987204f1 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/de/user/email/team-activation.html @@ -1 +1 @@ -${brand} Benutzerkonto

${brand_label_url}

Ihr neues ${brand}-Benutzerkonto

Ein neues ${brand} Team wurde mit ${email} erstellt. Bitte bestätigen Sie Ihre E-Mail-Adresse.

 
Bestätigen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file +${brand} Benutzerkonto

${brand_label_url}

Ihr neues ${brand}-Benutzerkonto

Ein neues ${brand} Team wurde mit ${email} erstellt. Bitte bestätigen Sie Ihre E-Mail-Adresse.

 
Bestätigen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/de/user/email/team-activation.txt index c2499a99a9e..de9a501ae30 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/de/user/email/team-activation.txt @@ -6,8 +6,8 @@ IHR NEUES ${brand}-BENUTZERKONTO Ein neues ${brand} Team wurde mit ${email} erstellt. Bitte bestätigen Sie Ihre E-Mail-Adresse. -Bestätigen [${url}]Falls Sie nicht auf den Button klicken können, kopieren Sie -diesen Link und fügen Sie ihn in Ihren Browser ein: +Bestätigen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren +Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/de/user/email/update.html b/services/brig/deb/opt/brig/templates/de/user/email/update.html index 61148ef262b..672df632d51 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/de/user/email/update.html @@ -1 +1 @@ -Ihre neue E-Mail-Adresse auf ${brand}

${brand_label_url}

Bestätigen Sie Ihre E-Mail-Adresse

${email} wurde als Ihre neue E-Mail-Adresse auf ${brand} registriert. Klicken Sie auf den folgenden Button, um Ihre neue Adresse zu bestätigen.

 
Bestätigen
 

Falls Sie nicht auf den Button klicken können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file +Ihre neue E-Mail-Adresse auf ${brand}

${brand_label_url}

Bestätigen Sie Ihre E-Mail-Adresse

${email} wurde als Ihre neue E-Mail-Adresse auf ${brand} registriert. Klicken Sie auf den folgenden Button, um Ihre neue Adresse zu bestätigen.

 
Bestätigen
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/user/email/update.txt b/services/brig/deb/opt/brig/templates/de/user/email/update.txt index 783804f8fc1..61af385b2be 100644 --- a/services/brig/deb/opt/brig/templates/de/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/de/user/email/update.txt @@ -6,8 +6,8 @@ BESTÄTIGEN SIE IHRE E-MAIL-ADRESSE ${email} wurde als Ihre neue E-Mail-Adresse auf ${brand} registriert. Klicken Sie auf den folgenden Button, um Ihre neue Adresse zu bestätigen. -Bestätigen [${url}]Falls Sie nicht auf den Button klicken können, kopieren Sie -diesen Link und fügen Sie ihn in Ihren Browser ein: +Bestätigen [${url}]Wenn Sie die Schaltfläche nicht auswählen können, kopieren +Sie diesen Link und fügen Sie ihn in Ihren Browser ein: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/provider/email/activation.html b/services/brig/deb/opt/brig/templates/en/provider/email/activation.html index fce3dd2e80b..228d9624960 100644 --- a/services/brig/deb/opt/brig/templates/en/provider/email/activation.html +++ b/services/brig/deb/opt/brig/templates/en/provider/email/activation.html @@ -1 +1 @@ -Your ${brand_service} Account

${brand_label_url}

Verify your email

Your email address ${email} was used to register as a ${brand_service}.

To complete the registration, it is necessary that you verify your e-mail address by clicking on the button below.

Please note that upon successful verification of your e-mail, your ${brand_service} account is still subject to approval through our staff, which usually happens within 24 hours. You will be informed of the approval via a separate e-mail.

 
Verify
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you didn’t register for a ${brand} service provider account using this e-mail address, you can safely ignore this message. If you want to report abuse of your e-mail address, please contact us.

Please don’t reply to this message.

                                                           
\ No newline at end of file +Your ${brand_service} Account

${brand_label_url}

Verify your email

Your email address ${email} was used to register as a ${brand_service}.

To complete the registration, it is necessary that you verify your e-mail address by clicking on the button below.

Please note that upon successful verification of your e-mail, your ${brand_service} account is still subject to approval through our staff, which usually happens within 24 hours. You will be informed of the approval via a separate e-mail.

 
Verify
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you didn’t register for a ${brand} service provider account using this e-mail address, you can safely ignore this message. If you want to report abuse of your e-mail address, please contact us.

Please don’t reply to this message.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/provider/email/activation.txt b/services/brig/deb/opt/brig/templates/en/provider/email/activation.txt index ded53a93c34..319a9d5ccae 100644 --- a/services/brig/deb/opt/brig/templates/en/provider/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/en/provider/email/activation.txt @@ -13,7 +13,7 @@ ${brand_service} account is still subject to approval through our staff, which usually happens within 24 hours. You will be informed of the approval via a separate e-mail. -Verify [${url}]If you can’t click the button, copy and paste this link to your +Verify [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.html b/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.html index d2900b45d9a..f587afc7800 100644 --- a/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.html +++ b/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.html @@ -1 +1 @@ -Approval Request: ${brand_service}

${brand_label_url}

Approval request

A new ${brand_service} has registered and is awaiting approval. Please review the information provided below.

Name: ${name}

E-mail: ${email}

Website: ${url}

Description: ${description}

If the request seems genuine, you can approve the provider by clicking on the button below. Once approved, the provider will be able to sign in and start registering services that ${brand} users can add to their conversations.

If the request seems dubious, please contact the provider for clarifications before proceeding.

 
Approve
 

If you can’t click the button, copy and paste this link to your browser:

${url}

Please don’t reply to this message.

                                                           
\ No newline at end of file +Approval Request: ${brand_service}

${brand_label_url}

Approval request

A new ${brand_service} has registered and is awaiting approval. Please review the information provided below.

Name: ${name}

E-mail: ${email}

Website: ${url}

Description: ${description}

If the request seems genuine, you can approve the provider by clicking on the button below. Once approved, the provider will be able to sign in and start registering services that ${brand} users can add to their conversations.

If the request seems dubious, please contact the provider for clarifications before proceeding.

 
Approve
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Please don’t reply to this message.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.txt b/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.txt index 2332083c199..04679fd8abe 100644 --- a/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.txt +++ b/services/brig/deb/opt/brig/templates/en/provider/email/approval-request.txt @@ -21,7 +21,7 @@ registering services that ${brand} users can add to their conversations. If the request seems dubious, please contact the provider for clarifications before proceeding. -Approve [${url}]If you can’t click the button, copy and paste this link to your +Approve [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/team/email/invitation.html b/services/brig/deb/opt/brig/templates/en/team/email/invitation.html index 1643844f28e..47dd7cd0f14 100644 --- a/services/brig/deb/opt/brig/templates/en/team/email/invitation.html +++ b/services/brig/deb/opt/brig/templates/en/team/email/invitation.html @@ -1 +1 @@ -You have been invited to join a team on ${brand}

${brand_label_url}

Team invitation

${inviter} has invited you to join a team on ${brand}. Click the button below to accept the invitation.

 
Join team
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

What is Wire?
Wire is the most secure collaboration platform. Work with your team and external partners wherever you are through messages, video conferencing and file sharing – always secured with end-to-end-encryption. Learn more.

                                                           
\ No newline at end of file +You have been invited to join a team on ${brand}

${brand_label_url}

Team invitation

${inviter} has invited you to join a team on ${brand}. Select the button below to accept the invitation.

 
Join team
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

What is Wire?
Wire is the most secure collaboration platform. Work with your team and external partners wherever you are through messages, video conferencing and file sharing – always secured with end-to-end-encryption. Learn more.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/invitation.txt b/services/brig/deb/opt/brig/templates/en/team/email/invitation.txt index 918c8fde767..ae49c91b8da 100644 --- a/services/brig/deb/opt/brig/templates/en/team/email/invitation.txt +++ b/services/brig/deb/opt/brig/templates/en/team/email/invitation.txt @@ -3,10 +3,10 @@ ${brand_label_url} [${brand_url}] TEAM INVITATION -${inviter} has invited you to join a team on ${brand}. Click the button below to -accept the invitation. +${inviter} has invited you to join a team on ${brand}. Select the button below +to accept the invitation. -Join team [${url}]If you can’t click the button, copy and paste this link to +Join team [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/team/email/migration-subject.txt b/services/brig/deb/opt/brig/templates/en/team/email/migration-subject.txt new file mode 100644 index 00000000000..9fef363e407 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/team/email/migration-subject.txt @@ -0,0 +1 @@ +You have been invited to join a team on ${brand} \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/migration.html b/services/brig/deb/opt/brig/templates/en/team/email/migration.html new file mode 100644 index 00000000000..e6647120d03 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/team/email/migration.html @@ -0,0 +1 @@ +You have been invited to join a team on ${brand}

${brand_label_url}

Team invitation

${inviter} has invited you to join a team on ${brand}.

By joining Wire migrates your personal account into a team account.

Select the button below to proceed with the invitation.

 
Join team
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Report abusive invitation link

If you have any questions, please contact us.

What is Wire?
Wire is the most secure collaboration platform. Work with your team and external partners wherever you are through messages, video conferencing and file sharing – always secured with end-to-end-encryption. Learn more.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/migration.txt b/services/brig/deb/opt/brig/templates/en/team/email/migration.txt new file mode 100644 index 00000000000..6283ee15add --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/team/email/migration.txt @@ -0,0 +1,30 @@ +[${brand_logo}] + +${brand_label_url} [${brand_url}] + +TEAM INVITATION +${inviter} has invited you to join a team on ${brand}. + +By joining Wire migrates your personal account into a team account. + +Select the button below to proceed with the invitation. + +Join team [${url}]If you can’t select the button, copy and paste this link to +your browser: + +${url} + +Report abusive invitation link [${support}] + +If you have any questions, please contact us [${support}]. + +What is Wire? +Wire is the most secure collaboration platform. Work with your team and external +partners wherever you are through messages, video conferencing and file sharing +– always secured with end-to-end-encryption. Learn more [https://wire.com/]. + + +-------------------------------------------------------------------------------- + +Privacy policy and terms of use [${legal}] · Report Misuse [${misuse}] +${copyright}. ALL RIGHTS RESERVED. \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.html b/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.html index a63b3a1d9c6..5496be6e83a 100644 --- a/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.html +++ b/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.html @@ -1 +1 @@ -You joined a team on ${brand}

${brand_label_url}

Welcome to ${team_name}.

You have just joined a team called ${team_name} on ${brand} with ${email}.

 

${brand} combines strong encryption, a rich feature set and ease-of-use in one app like never before. Works on all popular platforms.

 
Download ${brand}
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

Team ID: ${team_id}

                                                           
\ No newline at end of file +You joined a team on ${brand}

${brand_label_url}

Welcome to ${team_name}.

You have just joined a team called ${team_name} on ${brand} with ${email}.

 

${brand} combines strong encryption, a rich feature set and ease-of-use in one app like never before. Works on all popular platforms.

 
Download ${brand}
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

Team ID: ${team_id}

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.txt b/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.txt index 413c4fdf2b9..0fb7ebe1569 100644 --- a/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.txt +++ b/services/brig/deb/opt/brig/templates/en/team/email/new-member-welcome.txt @@ -8,7 +8,7 @@ You have just joined a team called ${team_name} on ${brand} with ${email}. ${brand} combines strong encryption, a rich feature set and ease-of-use in one app like never before. Works on all popular platforms. -Download ${brand} [${url}]If you can’t click the button, copy and paste this +Download ${brand} [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/user/email/activation.html b/services/brig/deb/opt/brig/templates/en/user/email/activation.html index c67376c606b..cd89b8e1bcc 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/en/user/email/activation.html @@ -1 +1 @@ -Your ${brand} Account

${brand_label_url}

Verify your email

${email} was used to register on ${brand}.
Click the button to verify your address.

 
Verify
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Your ${brand} Account

${brand_label_url}

Verify your email

${email} was used to register on ${brand}.
Click the button to verify your address.

 
Verify
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/activation.txt b/services/brig/deb/opt/brig/templates/en/user/email/activation.txt index af771cc00a7..a50f4da4be0 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/en/user/email/activation.txt @@ -6,7 +6,7 @@ VERIFY YOUR EMAIL ${email} was used to register on ${brand}. Click the button to verify your address. -Verify [${url}]If you can’t click the button, copy and paste this link to your +Verify [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/user/email/deletion.html b/services/brig/deb/opt/brig/templates/en/user/email/deletion.html index 690b0104fdd..f4ec9a50034 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/en/user/email/deletion.html @@ -1 +1 @@ -Delete account?

${brand_label_url}

Delete your account

We’ve received a request to delete your ${brand} account. Click the button below within 10 minutes to delete all your conversations, content and connections.

 
Delete account
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you didn’t request this, reset your password.

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Delete account?

${brand_label_url}

Delete your account

We’ve received a request to delete your ${brand} account. Click the button below within 10 minutes to delete all your conversations, content and connections.

 
Delete account
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you didn’t request this, reset your password.

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/en/user/email/deletion.txt index 744da7dc05c..4d8792997f6 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/en/user/email/deletion.txt @@ -6,7 +6,7 @@ DELETE YOUR ACCOUNT We’ve received a request to delete your ${brand} account. Click the button below within 10 minutes to delete all your conversations, content and connections. -Delete account [${url}]If you can’t click the button, copy and paste this link +Delete account [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/en/user/email/password-reset.html index 53ffea05fde..dc84cabd359 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/en/user/email/password-reset.html @@ -1 +1 @@ -Password Change at ${brand}

${brand_label_url}

Reset your password

We’ve received a request to reset the password for your ${brand} account. To create a new password, click the button below.

 
Reset password
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Password Change at ${brand}

${brand_label_url}

Reset your password

We’ve received a request to reset the password for your ${brand} account. To create a new password, click the button below.

 
Reset password
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/en/user/email/password-reset.txt index d16da88792f..e35ece49b89 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/en/user/email/password-reset.txt @@ -6,7 +6,7 @@ RESET YOUR PASSWORD We’ve received a request to reset the password for your ${brand} account. To create a new password, click the button below. -Reset password [${url}]If you can’t click the button, copy and paste this link +Reset password [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/en/user/email/team-activation.html index e34ca5f3894..81cc402e4b4 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/en/user/email/team-activation.html @@ -1 +1 @@ -${brand} Account

${brand_label_url}

Your new account on ${brand}

A new ${brand} team was created with ${email}. Please verify your email.

 
Verify
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +${brand} Account

${brand_label_url}

Your new account on ${brand}

A new ${brand} team was created with ${email}. Please verify your email.

 
Verify
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/en/user/email/team-activation.txt index 39a00b2089a..4b8db92ee7c 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/en/user/email/team-activation.txt @@ -5,7 +5,7 @@ ${brand_label_url} [${brand_url}] YOUR NEW ACCOUNT ON ${brand} A new ${brand} team was created with ${email}. Please verify your email. -Verify [${url}]If you can’t click the button, copy and paste this link to your +Verify [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/en/user/email/update.html b/services/brig/deb/opt/brig/templates/en/user/email/update.html index 339aad5dea7..0eb38d8ea41 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/en/user/email/update.html @@ -1 +1 @@ -Your new email address on ${brand}

${brand_label_url}

Verify your email

${email} was registered as your new email address on ${brand}. Click the button below to verify your address.

 
Verify
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Your new email address on ${brand}

${brand_label_url}

Verify your email

${email} was registered as your new email address on ${brand}. Click the button below to verify your address.

 
Verify
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/update.txt b/services/brig/deb/opt/brig/templates/en/user/email/update.txt index 5ee25666aa2..8d7ed7187bc 100644 --- a/services/brig/deb/opt/brig/templates/en/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/en/user/email/update.txt @@ -6,7 +6,7 @@ VERIFY YOUR EMAIL ${email} was registered as your new email address on ${brand}. Click the button below to verify your address. -Verify [${url}]If you can’t click the button, copy and paste this link to your +Verify [${url}]If you can’t select the button, copy and paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/et/user/email/activation.html b/services/brig/deb/opt/brig/templates/et/user/email/activation.html index bd77e33958a..1dc6b7d98d8 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/et/user/email/activation.html @@ -1 +1 @@ -Your ${brand} Account

${brand_label_url}

Kinnita oma e-posti aadress

${email} was used to register on ${brand}.
Click the button to verify your address.

 
Kinnita
 

Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev aadress veebibrauserisse:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Your ${brand} Account

${brand_label_url}

Kinnita oma e-posti aadress

${email} was used to register on ${brand}.
Click the button to verify your address.

 
Kinnita
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/et/user/email/activation.txt b/services/brig/deb/opt/brig/templates/et/user/email/activation.txt index 3e98f0aad17..9fd3d1c07e3 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/et/user/email/activation.txt @@ -6,8 +6,8 @@ KINNITA OMA E-POSTI AADRESS ${email} was used to register on ${brand}. Click the button to verify your address. -Kinnita [${url}]Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev -aadress veebibrauserisse: +Kinnita [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/et/user/email/deletion.html b/services/brig/deb/opt/brig/templates/et/user/email/deletion.html index 36eb10cdf00..4b31965c646 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/et/user/email/deletion.html @@ -1 +1 @@ -Kustuta konto?

${brand_label_url}

Kustuta konto

We’ve received a request to delete your ${brand} account. Kogu kontoga seotud info kustutamise kinnitamiseks kliki kümne minuti jooksul alloleval lingil.

 
Kustuta konto
 

Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev aadress veebibrauserisse:

${url}

If you didn’t request this, reset your password.

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Kustuta konto?

${brand_label_url}

Kustuta konto

We’ve received a request to delete your ${brand} account. Kogu kontoga seotud info kustutamise kinnitamiseks kliki kümne minuti jooksul alloleval lingil.

 
Kustuta konto
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you didn’t request this, reset your password.

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/et/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/et/user/email/deletion.txt index 76dfe344a41..852f61239a9 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/et/user/email/deletion.txt @@ -6,8 +6,8 @@ KUSTUTA KONTO We’ve received a request to delete your ${brand} account. Kogu kontoga seotud info kustutamise kinnitamiseks kliki kümne minuti jooksul alloleval lingil. -Kustuta konto [${url}]Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev -aadress veebibrauserisse: +Kustuta konto [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/et/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/et/user/email/password-reset.html index 2055ca88695..15430614298 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/et/user/email/password-reset.html @@ -1 +1 @@ -Password Change at ${brand}

${brand_label_url}

Lähtesta oma parool

We’ve received a request to reset the password for your ${brand} account. Uue salasõna loomiseks vajutage järgmisele lingile:

 
Lähesta parool
 

Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev aadress veebibrauserisse:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Password Change at ${brand}

${brand_label_url}

Lähtesta oma parool

We’ve received a request to reset the password for your ${brand} account. Uue salasõna loomiseks vajutage järgmisele lingile:

 
Lähesta parool
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/et/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/et/user/email/password-reset.txt index 5af078b790f..81cdf9ad7b2 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/et/user/email/password-reset.txt @@ -6,8 +6,8 @@ LÄHTESTA OMA PAROOL We’ve received a request to reset the password for your ${brand} account. Uue salasõna loomiseks vajutage järgmisele lingile: -Lähesta parool [${url}]Kui sul pole võimalik nuppu klikkida, siis kopeeri -allolev aadress veebibrauserisse: +Lähesta parool [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/et/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/et/user/email/team-activation.html index d042ee19056..e17b5ef7269 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/et/user/email/team-activation.html @@ -1 +1 @@ -${brand} Account

${brand_label_url}

Your new account on ${brand}

A new ${brand} team was created with ${email}. Palun kinnita oma meiliaadress.

 
Kinnita
 

Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev aadress veebibrauserisse:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +${brand} Account

${brand_label_url}

Your new account on ${brand}

A new ${brand} team was created with ${email}. Palun kinnita oma meiliaadress.

 
Kinnita
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/et/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/et/user/email/team-activation.txt index a323a447e26..e70b4d0b21e 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/et/user/email/team-activation.txt @@ -5,8 +5,8 @@ ${brand_label_url} [${brand_url}] YOUR NEW ACCOUNT ON ${brand} A new ${brand} team was created with ${email}. Palun kinnita oma meiliaadress. -Kinnita [${url}]Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev -aadress veebibrauserisse: +Kinnita [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/et/user/email/update.html b/services/brig/deb/opt/brig/templates/et/user/email/update.html index 92c86559af6..f0305744870 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/et/user/email/update.html @@ -1 +1 @@ -Your new email address on ${brand}

${brand_label_url}

Kinnita oma e-posti aadress

${email} was registered as your new email address on ${brand}. Aadressi kinnitamiseks kliki alloleval lingil.

 
Kinnita
 

Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev aadress veebibrauserisse:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file +Your new email address on ${brand}

${brand_label_url}

Kinnita oma e-posti aadress

${email} was registered as your new email address on ${brand}. Aadressi kinnitamiseks kliki alloleval lingil.

 
Kinnita
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/et/user/email/update.txt b/services/brig/deb/opt/brig/templates/et/user/email/update.txt index b9808bff228..4fb4fd47018 100644 --- a/services/brig/deb/opt/brig/templates/et/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/et/user/email/update.txt @@ -6,8 +6,8 @@ KINNITA OMA E-POSTI AADRESS ${email} was registered as your new email address on ${brand}. Aadressi kinnitamiseks kliki alloleval lingil. -Kinnita [${url}]Kui sul pole võimalik nuppu klikkida, siis kopeeri allolev -aadress veebibrauserisse: +Kinnita [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/activation.html b/services/brig/deb/opt/brig/templates/fr/user/email/activation.html index 8435bf74e19..0c29777cb98 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/fr/user/email/activation.html @@ -1 +1 @@ -Votre Compte ${brand}

${brand_label_url}

Vérification de votre adresse email

${email} a été utilisé pour s'enregistrer sur ${brand}.
Cliquez sur le bouton ci-dessous pour vérifier votre adresse.

 
Vérifier
 

Si vous ne pouvez pas cliquer sur le bouton, copiez et collez ce lien dans votre navigateur :

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file +Votre Compte ${brand}

${brand_label_url}

Vérification de votre adresse email

${email} a été utilisé pour s'enregistrer sur ${brand}.
Cliquez sur le bouton ci-dessous pour vérifier votre adresse.

 
Vérifier
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/activation.txt b/services/brig/deb/opt/brig/templates/fr/user/email/activation.txt index 3a1dce3c8ff..e8cd745c33a 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/fr/user/email/activation.txt @@ -6,8 +6,8 @@ VÉRIFICATION DE VOTRE ADRESSE EMAIL ${email} a été utilisé pour s'enregistrer sur ${brand}. Cliquez sur le bouton ci-dessous pour vérifier votre adresse. -Vérifier [${url}]Si vous ne pouvez pas cliquer sur le bouton, copiez et collez -ce lien dans votre navigateur : +Vérifier [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/deletion.html b/services/brig/deb/opt/brig/templates/fr/user/email/deletion.html index 331a6b0cbcf..e2d9b2cef1d 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/fr/user/email/deletion.html @@ -1 +1 @@ -Supprimer votre compte ?

${brand_label_url}

Supprimer votre compte

Nous avons reçu une demande de suppression de votre compte ${brand}. Cliquez sur le lien ci-dessous dans les 10 minutes pour supprimer toutes vos conversations, contenus et connexions.

 
Supprimer le compte
 

Si vous ne pouvez pas cliquer sur le bouton, copiez et collez ce lien dans votre navigateur :

${url}

Si vous n'êtes pas à l'origine de cette demande, réinitialisez votre mot de passe.

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file +Supprimer votre compte ?

${brand_label_url}

Supprimer votre compte

Nous avons reçu une demande de suppression de votre compte ${brand}. Cliquez sur le lien ci-dessous dans les 10 minutes pour supprimer toutes vos conversations, contenus et connexions.

 
Supprimer le compte
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Si vous n'êtes pas à l'origine de cette demande, réinitialisez votre mot de passe.

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/fr/user/email/deletion.txt index 5ddec54da01..377579c30d4 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/fr/user/email/deletion.txt @@ -7,8 +7,8 @@ Nous avons reçu une demande de suppression de votre compte ${brand}. Cliquez su le lien ci-dessous dans les 10 minutes pour supprimer toutes vos conversations, contenus et connexions. -Supprimer le compte [${url}]Si vous ne pouvez pas cliquer sur le bouton, copiez -et collez ce lien dans votre navigateur : +Supprimer le compte [${url}]If you can’t select the button, copy and paste this +link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.html index 02cc9de42e7..a45bbd145cf 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.html @@ -1 +1 @@ -Réinitialisation du mot de passe ${brand}

${brand_label_url}

Réinitialiser votre mot de passe

Nous avons reçu une demande pour réinitialiser le mot de passe de votre compte ${brand}. Pour créer un nouveau mot de passe, cliquez sur le bouton ci-dessous.

 
Réinitialiser le mot de passe
 

Si vous ne pouvez pas cliquer sur le bouton, copiez et collez ce lien dans votre navigateur :

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file +Réinitialisation du mot de passe ${brand}

${brand_label_url}

Réinitialiser votre mot de passe

Nous avons reçu une demande pour réinitialiser le mot de passe de votre compte ${brand}. Pour créer un nouveau mot de passe, cliquez sur le bouton ci-dessous.

 
Réinitialiser le mot de passe
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.txt index 4462d79c348..dd813b8933f 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/fr/user/email/password-reset.txt @@ -6,8 +6,8 @@ RÉINITIALISER VOTRE MOT DE PASSE Nous avons reçu une demande pour réinitialiser le mot de passe de votre compte ${brand}. Pour créer un nouveau mot de passe, cliquez sur le bouton ci-dessous. -Réinitialiser le mot de passe [${url}]Si vous ne pouvez pas cliquer sur le -bouton, copiez et collez ce lien dans votre navigateur : +Réinitialiser le mot de passe [${url}]If you can’t select the button, copy and +paste this link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.html index d4450a20a3d..c4d27a37706 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.html @@ -1 +1 @@ -Compte ${brand}

${brand_label_url}

Votre nouveau compte ${brand}

Une nouvelle équipé a été créée sur ${brand} avec ${email}. Veuillez vérifier votre adresse email s’il vous plaît.

 
Vérifier
 

Si vous ne pouvez pas cliquer sur le bouton, copiez et collez ce lien dans votre navigateur :

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file +Compte ${brand}

${brand_label_url}

Votre nouveau compte ${brand}

Une nouvelle équipé a été créée sur ${brand} avec ${email}. Veuillez vérifier votre adresse email s’il vous plaît.

 
Vérifier
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.txt index ab5984b3312..aa94af39e3e 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/fr/user/email/team-activation.txt @@ -6,8 +6,8 @@ VOTRE NOUVEAU COMPTE ${brand} Une nouvelle équipé a été créée sur ${brand} avec ${email}. Veuillez vérifier votre adresse email s’il vous plaît. -Vérifier [${url}]Si vous ne pouvez pas cliquer sur le bouton, copiez et collez -ce lien dans votre navigateur : +Vérifier [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/update.html b/services/brig/deb/opt/brig/templates/fr/user/email/update.html index 5aeb1430126..fba83729283 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/fr/user/email/update.html @@ -1 +1 @@ -Votre nouvelle adresse e-mail sur ${brand}

${brand_label_url}

Vérification de votre adresse email

${email} a été enregistré comme votre nouvelle adresse email sur ${brand}. Veuillez vérifier votre email s’il vous plaît. Cliquez sur le bouton ci-dessous pour vérifier votre adresse email.

 
Vérifier
 

Si vous ne pouvez pas cliquer sur le bouton, copiez et collez ce lien dans votre navigateur :

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file +Votre nouvelle adresse e-mail sur ${brand}

${brand_label_url}

Vérification de votre adresse email

${email} a été enregistré comme votre nouvelle adresse email sur ${brand}. Veuillez vérifier votre email s’il vous plaît. Cliquez sur le bouton ci-dessous pour vérifier votre adresse email.

 
Vérifier
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Si vous avez des questions, veuillez nous contacter.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/fr/user/email/update.txt b/services/brig/deb/opt/brig/templates/fr/user/email/update.txt index 2517b9a9328..cc719c283a7 100644 --- a/services/brig/deb/opt/brig/templates/fr/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/fr/user/email/update.txt @@ -7,8 +7,8 @@ ${email} a été enregistré comme votre nouvelle adresse email sur ${brand}. Veuillez vérifier votre email s’il vous plaît. Cliquez sur le bouton ci-dessous pour vérifier votre adresse email. -Vérifier [${url}]Si vous ne pouvez pas cliquer sur le bouton, copiez et collez -ce lien dans votre navigateur : +Vérifier [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/index.html b/services/brig/deb/opt/brig/templates/index.html index f0d4029dc3a..76b2de86f7b 100644 --- a/services/brig/deb/opt/brig/templates/index.html +++ b/services/brig/deb/opt/brig/templates/index.html @@ -4,4 +4,4 @@ link.rel = 'stylesheet'; link.href = '//cdnjs.cloudflare.com/ajax/libs/flag-icon-css/2.9.0/css/flag-icon.min.css'; document.head.appendChild(link); - }
 

Wire Email Templates Preview

Click the links below to display the content of each message:

Provider
  1. Activationtxt
  2. Approval confirmtxt
  3. Approval requesttxt
Team
  1. Invitationtxt
  2. New member welcometxt
User
  1. Activationtxt
  2. Deletiontxt
  3. New clienttxt
  4. Password resettxt
  5. Updatetxt
  6. Verificationtxt
  7. Team activationtxt
  8. Second factor verification for logintxt
  9. Second factor verification create SCIM tokentxt
  10. Second factor verification delete teamtxt
Billing
  1. Suspensiontxt

For source and instructions, see github.com/wireapp/wire-emails or visit the Crowdin project to help with translations.

                                                           
\ No newline at end of file + }
 

Wire Email Templates Preview

Click the links below to display the content of each message:

Provider
  1. Activationtxt
  2. Approval confirmtxt
  3. Approval requesttxt
Team
  1. Invitationtxt
  2. New member welcometxt
  3. Migration from private to team usertxt
User
  1. Activationtxt
  2. Deletiontxt
  3. New clienttxt
  4. Password resettxt
  5. Updatetxt
  6. Verificationtxt
  7. Team activationtxt
  8. Second factor verification for logintxt
  9. Second factor verification create SCIM tokentxt
  10. Second factor verification delete teamtxt
Billing
  1. Suspensiontxt

For source and instructions, see github.com/wireapp/wire-emails or visit the Crowdin project to help with translations.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/it/user/email/activation.html b/services/brig/deb/opt/brig/templates/it/user/email/activation.html index 0812e96af31..ec62e15dc1a 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/it/user/email/activation.html @@ -1 +1 @@ -Il tuo account ${brand}

${brand_label_url}

Verifica il tuo indirizzo e-mail

${email} è stato utilizzata per registrarsi su ${brand}.
Clicca il pulsante per verificare il tuo indirizzo.

 
Verifica
 

Se non puoi fare clic sul pulsante, copia e incolla questo link nel tuo browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file +Il tuo account ${brand}

${brand_label_url}

Verifica il tuo indirizzo e-mail

${email} è stato utilizzata per registrarsi su ${brand}.
Clicca il pulsante per verificare il tuo indirizzo.

 
Verifica
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/it/user/email/activation.txt b/services/brig/deb/opt/brig/templates/it/user/email/activation.txt index b655bab11cb..10cb398928b 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/it/user/email/activation.txt @@ -6,8 +6,8 @@ VERIFICA IL TUO INDIRIZZO E-MAIL ${email} è stato utilizzata per registrarsi su ${brand}. Clicca il pulsante per verificare il tuo indirizzo. -Verifica [${url}]Se non puoi fare clic sul pulsante, copia e incolla questo link -nel tuo browser: +Verifica [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/it/user/email/deletion.html b/services/brig/deb/opt/brig/templates/it/user/email/deletion.html index 10d09fa2521..de6afb8ce49 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/it/user/email/deletion.html @@ -1 +1 @@ -Eliminare account?

${brand_label_url}

Elimina il tuo account

Abbiamo ricevuto una richiesta per eliminare il tuo account ${brand}. Clicca sul pulsante qui sotto entro 10 minuti per eliminare tutte le conversazioni, i contenuti e le connessioni.

 
Elimina account
 

Se non puoi fare clic sul pulsante, copia e incolla questo link nel tuo browser:

${url}

Se non lo hai richiesto, reimposta la password.

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file +Eliminare account?

${brand_label_url}

Elimina il tuo account

Abbiamo ricevuto una richiesta per eliminare il tuo account ${brand}. Clicca sul pulsante qui sotto entro 10 minuti per eliminare tutte le conversazioni, i contenuti e le connessioni.

 
Elimina account
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se non lo hai richiesto, reimposta la password.

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/it/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/it/user/email/deletion.txt index 376449de73c..478a0adba4d 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/it/user/email/deletion.txt @@ -7,8 +7,8 @@ Abbiamo ricevuto una richiesta per eliminare il tuo account ${brand}. Clicca sul pulsante qui sotto entro 10 minuti per eliminare tutte le conversazioni, i contenuti e le connessioni. -Elimina account [${url}]Se non puoi fare clic sul pulsante, copia e incolla -questo link nel tuo browser: +Elimina account [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/it/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/it/user/email/password-reset.html index 475f1d82b97..1b33b8f07fa 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/it/user/email/password-reset.html @@ -1 +1 @@ -Cambio di password di ${brand}

${brand_label_url}

Reimposta la tua password

Abbiamo ricevuto una richiesta di reimpostazione della password del tuo account ${brand}. Per creare una nuova password, fai clic sul pulsante qui sotto.

 
Reimposta password
 

Se non puoi fare clic sul pulsante, copia e incolla questo link nel tuo browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file +Cambio di password di ${brand}

${brand_label_url}

Reimposta la tua password

Abbiamo ricevuto una richiesta di reimpostazione della password del tuo account ${brand}. Per creare una nuova password, fai clic sul pulsante qui sotto.

 
Reimposta password
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/it/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/it/user/email/password-reset.txt index 3aa152a0db3..e501ff726b0 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/it/user/email/password-reset.txt @@ -6,8 +6,8 @@ REIMPOSTA LA TUA PASSWORD Abbiamo ricevuto una richiesta di reimpostazione della password del tuo account ${brand}. Per creare una nuova password, fai clic sul pulsante qui sotto. -Reimposta password [${url}]Se non puoi fare clic sul pulsante, copia e incolla -questo link nel tuo browser: +Reimposta password [${url}]If you can’t select the button, copy and paste this +link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/it/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/it/user/email/team-activation.html index 0f6eeadb38b..5ce23562d3f 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/it/user/email/team-activation.html @@ -1 +1 @@ -Profilo di ${brand}

${brand_label_url}

Il tuo nuovo profilo su ${brand}

Un nuovo team di ${brand} è stato creato con ${email}. Sei pregato di verificare la tua email.

 
Verifica
 

Se non puoi fare clic sul pulsante, copia e incolla questo link nel tuo browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file +Profilo di ${brand}

${brand_label_url}

Il tuo nuovo profilo su ${brand}

Un nuovo team di ${brand} è stato creato con ${email}. Sei pregato di verificare la tua email.

 
Verifica
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/it/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/it/user/email/team-activation.txt index 83096c121e5..d208c53ac6c 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/it/user/email/team-activation.txt @@ -6,8 +6,8 @@ IL TUO NUOVO PROFILO SU ${brand} Un nuovo team di ${brand} è stato creato con ${email}. Sei pregato di verificare la tua email. -Verifica [${url}]Se non puoi fare clic sul pulsante, copia e incolla questo link -nel tuo browser: +Verifica [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/it/user/email/update.html b/services/brig/deb/opt/brig/templates/it/user/email/update.html index 0685328ddb6..aa6ae28ed3c 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/it/user/email/update.html @@ -1 +1 @@ -Il tuo nuovo indirizzo email su ${brand}

${brand_label_url}

Verifica il tuo indirizzo e-mail

${email} è stato registrato come tuo nuovo indirizzo email su ${brand}. Clicca il pulsante sotto per verificare il tuo indirizzo.

 
Verifica
 

Se non puoi fare clic sul pulsante, copia e incolla questo link nel tuo browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file +Il tuo nuovo indirizzo email su ${brand}

${brand_label_url}

Verifica il tuo indirizzo e-mail

${email} è stato registrato come tuo nuovo indirizzo email su ${brand}. Clicca il pulsante sotto per verificare il tuo indirizzo.

 
Verifica
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se hai domande, per favore contattaci.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/it/user/email/update.txt b/services/brig/deb/opt/brig/templates/it/user/email/update.txt index 881ea68a8b0..69b7a35f63b 100644 --- a/services/brig/deb/opt/brig/templates/it/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/it/user/email/update.txt @@ -6,8 +6,8 @@ VERIFICA IL TUO INDIRIZZO E-MAIL ${email} è stato registrato come tuo nuovo indirizzo email su ${brand}. Clicca il pulsante sotto per verificare il tuo indirizzo. -Verifica [${url}]Se non puoi fare clic sul pulsante, copia e incolla questo link -nel tuo browser: +Verifica [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/activation.html b/services/brig/deb/opt/brig/templates/ja/user/email/activation.html index 5628de34d87..4961e529590 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/ja/user/email/activation.html @@ -1 +1 @@ -あなたの ${brand} アカウント

${brand_label_url}

メールアドレス認証

${email} は、${brand} への登録に使用されました。
ボタンをクリックしてメールアドレスの認証を行ってください。

 
認証
 

ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file +あなたの ${brand} アカウント

${brand_label_url}

メールアドレス認証

${email} は、${brand} への登録に使用されました。
ボタンをクリックしてメールアドレスの認証を行ってください。

 
認証
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/activation.txt b/services/brig/deb/opt/brig/templates/ja/user/email/activation.txt index 9b1e2f77f15..c1e78cb1a8d 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/ja/user/email/activation.txt @@ -6,7 +6,8 @@ ${brand_label_url} [${brand_url}] ${email} は、${brand} への登録に使用されました。 ボタンをクリックしてメールアドレスの認証を行ってください。 -認証 [${url}]ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。 +認証 [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/deletion.html b/services/brig/deb/opt/brig/templates/ja/user/email/deletion.html index 72e2c8d9330..e41f86d5a71 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/ja/user/email/deletion.html @@ -1 +1 @@ -アカウントを削除しますか?

${brand_label_url}

アカウントを削除

あなたの ${brand} アカウントの削除リクエストを受け付けました。 あなたのすべての会話、コンテンツ、友人を削除するには10分以内に下記のリンクをクリックしてください。

 
アカウント削除
 

ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。

${url}

あなたがこのリクエスト行っていない場合は、パスワードをリセットしてください。

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file +アカウントを削除しますか?

${brand_label_url}

アカウントを削除

あなたの ${brand} アカウントの削除リクエストを受け付けました。 あなたのすべての会話、コンテンツ、友人を削除するには10分以内に下記のリンクをクリックしてください。

 
アカウント削除
 

If you can’t select the button, copy and paste this link to your browser:

${url}

あなたがこのリクエスト行っていない場合は、パスワードをリセットしてください。

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/ja/user/email/deletion.txt index d11c0c14f8c..77192fb1a2f 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/ja/user/email/deletion.txt @@ -6,7 +6,8 @@ ${brand_label_url} [${brand_url}] あなたの ${brand} アカウントの削除リクエストを受け付けました。 あなたのすべての会話、コンテンツ、友人を削除するには10分以内に下記のリンクをクリックしてください。 -アカウント削除 [${url}]ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。 +アカウント削除 [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.html index eb0d59f2aa7..940737c58f6 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.html @@ -1 +1 @@ -${brand} でのパスワードリセット

${brand_label_url}

パスワードリセット

${brand} アカウントのパスワードをリセット要求を受け取りました。 新しいパスワードを作成するには、以下のボタンをクリックしてください。

 
パスワードリセット
 

ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file +${brand} でのパスワードリセット

${brand_label_url}

パスワードリセット

${brand} アカウントのパスワードをリセット要求を受け取りました。 新しいパスワードを作成するには、以下のボタンをクリックしてください。

 
パスワードリセット
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.txt index 0ffdc49b479..fe62bcf77d5 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/ja/user/email/password-reset.txt @@ -5,7 +5,8 @@ ${brand_label_url} [${brand_url}] パスワードリセット ${brand} アカウントのパスワードをリセット要求を受け取りました。 新しいパスワードを作成するには、以下のボタンをクリックしてください。 -パスワードリセット [${url}]ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。 +パスワードリセット [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.html index 63c73884208..4f34cac8c64 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.html @@ -1 +1 @@ -${brand} アカウント

${brand_label_url}

あなたの新しい ${brand} アカウント

新しい ${brand} チーム が、 ${email} によって作成されました。 メールアドレスの認証をお願いします。

 
認証
 

ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file +${brand} アカウント

${brand_label_url}

あなたの新しい ${brand} アカウント

新しい ${brand} チーム が、 ${email} によって作成されました。 メールアドレスの認証をお願いします。

 
認証
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.txt index 89248d20a57..919484eb42d 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/ja/user/email/team-activation.txt @@ -5,7 +5,8 @@ ${brand_label_url} [${brand_url}] あなたの新しい ${brand} アカウント 新しい ${brand} チーム が、 ${email} によって作成されました。 メールアドレスの認証をお願いします。 -認証 [${url}]ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。 +認証 [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/update.html b/services/brig/deb/opt/brig/templates/ja/user/email/update.html index 8a0b25f9a3f..864cf8dea86 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/ja/user/email/update.html @@ -1 +1 @@ -${brand} での新しいメールアドレス

${brand_label_url}

メールアドレス認証

${email} は、 ${brand} で新しいメールアドレスとして登録されました。 新しいメールアドレスを認証するために下のボタンをクリックしてください。

 
認証
 

ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file +${brand} での新しいメールアドレス

${brand_label_url}

メールアドレス認証

${email} は、 ${brand} で新しいメールアドレスとして登録されました。 新しいメールアドレスを認証するために下のボタンをクリックしてください。

 
認証
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ご不明な点がございましたら、 こちら から私たちにご連絡ください。

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ja/user/email/update.txt b/services/brig/deb/opt/brig/templates/ja/user/email/update.txt index bb5992939f7..9efec032298 100644 --- a/services/brig/deb/opt/brig/templates/ja/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/ja/user/email/update.txt @@ -5,7 +5,8 @@ ${brand_label_url} [${brand_url}] メールアドレス認証 ${email} は、 ${brand} で新しいメールアドレスとして登録されました。 新しいメールアドレスを認証するために下のボタンをクリックしてください。 -認証 [${url}]ボタンをクリックできない場合は、以下のリンクをブラウザにコピー&ペーストして下さい。 +認証 [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/activation.html b/services/brig/deb/opt/brig/templates/lt/user/email/activation.html index 1fb608768bc..3ac19fa7576 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/lt/user/email/activation.html @@ -1 +1 @@ -Jūsų „${brand}“ paskyra

${brand_label_url}

Patvirtinkite savo el. paštą

${email} buvo panaudotas, registruojantis „${brand}“.
Norėdami patvirtinti savo adresą, spustelėkite mygtuką.

 
Patvirtinti
 

Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir įdėkite šią nuorodą į savo naršyklę:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file +Jūsų „${brand}“ paskyra

${brand_label_url}

Patvirtinkite savo el. paštą

${email} buvo panaudotas, registruojantis „${brand}“.
Norėdami patvirtinti savo adresą, spustelėkite mygtuką.

 
Patvirtinti
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/activation.txt b/services/brig/deb/opt/brig/templates/lt/user/email/activation.txt index bf8c020d177..f7597745ca2 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/lt/user/email/activation.txt @@ -6,8 +6,8 @@ PATVIRTINKITE SAVO EL. PAŠTĄ ${email} buvo panaudotas, registruojantis „${brand}“. Norėdami patvirtinti savo adresą, spustelėkite mygtuką. -Patvirtinti [${url}]Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir -įdėkite šią nuorodą į savo naršyklę: +Patvirtinti [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/deletion.html b/services/brig/deb/opt/brig/templates/lt/user/email/deletion.html index 21df982477e..d72c2f5799e 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/lt/user/email/deletion.html @@ -1 +1 @@ -Ištrinti paskyrą?

${brand_label_url}

Ištrinti jūsų paskyrą

Mes gavome užklausą ištrinti jūsų ${brand} paskyrą. Norėdami ištrinti visus savo pokalbius, visą turinį ir ryšius, 10 minučių bėgyje spustelėkite žemiau esantį mygtuką.

 
Ištrinti paskyrą
 

Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir įdėkite šią nuorodą į savo naršyklę:

${url}

Jeigu jūs nebuvote to užklausę, atstatykite savo slaptažodį.

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file +Ištrinti paskyrą?

${brand_label_url}

Ištrinti jūsų paskyrą

Mes gavome užklausą ištrinti jūsų ${brand} paskyrą. Norėdami ištrinti visus savo pokalbius, visą turinį ir ryšius, 10 minučių bėgyje spustelėkite žemiau esantį mygtuką.

 
Ištrinti paskyrą
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jeigu jūs nebuvote to užklausę, atstatykite savo slaptažodį.

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/lt/user/email/deletion.txt index 9ed2259ecc0..a647afdcb60 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/lt/user/email/deletion.txt @@ -7,8 +7,8 @@ Mes gavome užklausą ištrinti jūsų ${brand} paskyrą. Norėdami ištrinti vi pokalbius, visą turinį ir ryšius, 10 minučių bėgyje spustelėkite žemiau esantį mygtuką. -Ištrinti paskyrą [${url}]Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir -įdėkite šią nuorodą į savo naršyklę: +Ištrinti paskyrą [${url}]If you can’t select the button, copy and paste this +link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.html index 1345946ca57..9f2eb657a00 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.html @@ -1 +1 @@ -„${brand}“ slaptažodžio pakeitimas

${brand_label_url}

Atstatyti jūsų slaptažodį

Gavome užklausą atstatyti jūsų ${brand} paskyros slaptažodį. Norėdami susikurti naują slaptažodį, spustelėkite mygtuką žemiau.

 
Atstatyti slaptažodį
 

Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir įdėkite šią nuorodą į savo naršyklę:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file +„${brand}“ slaptažodžio pakeitimas

${brand_label_url}

Atstatyti jūsų slaptažodį

Gavome užklausą atstatyti jūsų ${brand} paskyros slaptažodį. Norėdami susikurti naują slaptažodį, spustelėkite mygtuką žemiau.

 
Atstatyti slaptažodį
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.txt index 53da058a2d6..557987093fa 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/lt/user/email/password-reset.txt @@ -6,8 +6,8 @@ ATSTATYTI JŪSŲ SLAPTAŽODĮ Gavome užklausą atstatyti jūsų ${brand} paskyros slaptažodį. Norėdami susikurti naują slaptažodį, spustelėkite mygtuką žemiau. -Atstatyti slaptažodį [${url}]Jeigu negalite spustelėti ant mygtuko, -nukopijuokite ir įdėkite šią nuorodą į savo naršyklę: +Atstatyti slaptažodį [${url}]If you can’t select the button, copy and paste this +link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.html index 66def145ce8..97a6fc62b47 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.html @@ -1 +1 @@ -„${brand}“ paskyra

${brand_label_url}

Jūsų nauja „${brand}“ paskyra

Naudojant ${email}, buvo sukurta nauja „${brand}“ komanda. Patvirtinkite savo el. paštą.

 
Patvirtinti
 

Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir įdėkite šią nuorodą į savo naršyklę:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file +„${brand}“ paskyra

${brand_label_url}

Jūsų nauja „${brand}“ paskyra

Naudojant ${email}, buvo sukurta nauja „${brand}“ komanda. Patvirtinkite savo el. paštą.

 
Patvirtinti
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.txt index c6d6fd958a6..d3459c84aba 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/lt/user/email/team-activation.txt @@ -6,8 +6,8 @@ JŪSŲ NAUJA „${brand}“ PASKYRA Naudojant ${email}, buvo sukurta nauja „${brand}“ komanda. Patvirtinkite savo el. paštą. -Patvirtinti [${url}]Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir -įdėkite šią nuorodą į savo naršyklę: +Patvirtinti [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/update.html b/services/brig/deb/opt/brig/templates/lt/user/email/update.html index e7bc8d4a286..b4c77aed14e 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/lt/user/email/update.html @@ -1 +1 @@ -Jūsų naujas „${brand}“ el. pašto adresas

${brand_label_url}

Patvirtinkite savo el. paštą

${email} buvo užregistruotas kaip naujas „${brand}“ el. pašto adresas. Norėdami patvirtinti savo adresą, spustelėkite mygtuką žemiau.

 
Patvirtinti
 

Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir įdėkite šią nuorodą į savo naršyklę:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file +Jūsų naujas „${brand}“ el. pašto adresas

${brand_label_url}

Patvirtinkite savo el. paštą

${email} buvo užregistruotas kaip naujas „${brand}“ el. pašto adresas. Norėdami patvirtinti savo adresą, spustelėkite mygtuką žemiau.

 
Patvirtinti
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jei turite klausimų, susisiekite su mumis.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/lt/user/email/update.txt b/services/brig/deb/opt/brig/templates/lt/user/email/update.txt index f6d6c4eba1d..d7aafc33d36 100644 --- a/services/brig/deb/opt/brig/templates/lt/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/lt/user/email/update.txt @@ -6,8 +6,8 @@ PATVIRTINKITE SAVO EL. PAŠTĄ ${email} buvo užregistruotas kaip naujas „${brand}“ el. pašto adresas. Norėdami patvirtinti savo adresą, spustelėkite mygtuką žemiau. -Patvirtinti [${url}]Jeigu negalite spustelėti ant mygtuko, nukopijuokite ir -įdėkite šią nuorodą į savo naršyklę: +Patvirtinti [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/activation.html b/services/brig/deb/opt/brig/templates/pl/user/email/activation.html index d264b4c23f5..de43cc47a89 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/pl/user/email/activation.html @@ -1 +1 @@ -Twoje konto ${brand}

${brand_label_url}

Potwierdź swój adres email

${email} został użyty do rejestracji ${brand}.
Kliknij przycisk, aby zweryfikować swój adres.

 
Zweryfikuj
 

Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link do swojej przeglądarki:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file +Twoje konto ${brand}

${brand_label_url}

Potwierdź swój adres email

${email} został użyty do rejestracji ${brand}.
Kliknij przycisk, aby zweryfikować swój adres.

 
Zweryfikuj
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/activation.txt b/services/brig/deb/opt/brig/templates/pl/user/email/activation.txt index 3921797a145..401df8e4561 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/pl/user/email/activation.txt @@ -6,8 +6,8 @@ POTWIERDŹ SWÓJ ADRES EMAIL ${email} został użyty do rejestracji ${brand}. Kliknij przycisk, aby zweryfikować swój adres. -Zweryfikuj [${url}]Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link -do swojej przeglądarki: +Zweryfikuj [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/deletion.html b/services/brig/deb/opt/brig/templates/pl/user/email/deletion.html index 76459d62cb6..25ae76e5809 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/pl/user/email/deletion.html @@ -1 +1 @@ -Usunąć konto?

${brand_label_url}

Usuń swoje konto

Otrzymaliśmy prośbę o usunięcie konta ${brand}. Kliknij przycisk poniżej w ciągu 10 minut, aby usunąć wszystkie konwersacje, treści i połączenia.

 
Usuń konto
 

Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link do swojej przeglądarki:

${url}

Jeśli nie poprosiłeś o to, zresetuj swoje hasło.

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file +Usunąć konto?

${brand_label_url}

Usuń swoje konto

Otrzymaliśmy prośbę o usunięcie konta ${brand}. Kliknij przycisk poniżej w ciągu 10 minut, aby usunąć wszystkie konwersacje, treści i połączenia.

 
Usuń konto
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jeśli nie poprosiłeś o to, zresetuj swoje hasło.

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/pl/user/email/deletion.txt index 33a4f532af8..c3f57e1e1ed 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/pl/user/email/deletion.txt @@ -6,8 +6,8 @@ USUŃ SWOJE KONTO Otrzymaliśmy prośbę o usunięcie konta ${brand}. Kliknij przycisk poniżej w ciągu 10 minut, aby usunąć wszystkie konwersacje, treści i połączenia. -Usuń konto [${url}]Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link -do swojej przeglądarki: +Usuń konto [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.html index 139c47eb9ea..2ed71518521 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.html @@ -1 +1 @@ -Zmiana hasła w ${brand}

${brand_label_url}

Zresetuj hasło

Otrzymaliśmy prośbę o zresetowanie hasła do Twojego konta ${brand}. Aby utworzyć nowe hasło, kliknij poniższy przycisk.

 
Zresetuj hasło
 

Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link do swojej przeglądarki:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file +Zmiana hasła w ${brand}

${brand_label_url}

Zresetuj hasło

Otrzymaliśmy prośbę o zresetowanie hasła do Twojego konta ${brand}. Aby utworzyć nowe hasło, kliknij poniższy przycisk.

 
Zresetuj hasło
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.txt index 780fb104cc0..b7026373366 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/pl/user/email/password-reset.txt @@ -6,8 +6,8 @@ ZRESETUJ HASŁO Otrzymaliśmy prośbę o zresetowanie hasła do Twojego konta ${brand}. Aby utworzyć nowe hasło, kliknij poniższy przycisk. -Zresetuj hasło [${url}]Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten -link do swojej przeglądarki: +Zresetuj hasło [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.html index 067c12da167..1cb310daba2 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.html @@ -1 +1 @@ -Konto ${brand}

${brand_label_url}

Twoje nowe konto na ${brand}

Nowy zespół ${brand} został utworzony z ${email}. Prosimy, zweryfikuj swój adres email.

 
Zweryfikuj
 

Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link do swojej przeglądarki:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file +Konto ${brand}

${brand_label_url}

Twoje nowe konto na ${brand}

Nowy zespół ${brand} został utworzony z ${email}. Prosimy, zweryfikuj swój adres email.

 
Zweryfikuj
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.txt index f1054d62cfa..f448de9be4e 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/pl/user/email/team-activation.txt @@ -2,12 +2,12 @@ ${brand_label_url} [${brand_url}] -TWOJE NOWE KONTO NA ${BRAND} +TWOJE NOWE KONTO NA ${brand} Nowy zespół ${brand} został utworzony z ${email}. Prosimy, zweryfikuj swój adres email. -Zweryfikuj [${url}]Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link -do swojej przeglądarki: +Zweryfikuj [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/update.html b/services/brig/deb/opt/brig/templates/pl/user/email/update.html index 8a0a1d35d99..bfbff27d748 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/pl/user/email/update.html @@ -1 +1 @@ -Twój nowy adres e-mail na ${brand}

${brand_label_url}

Potwierdź swój adres email

${email} został zarejestrowany jako Twój nowy adres e-mail na ${brand}. Kliknij poniższy przycisk, aby zweryfikować swój adres.

 
Zweryfikuj
 

Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link do swojej przeglądarki:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file +Twój nowy adres e-mail na ${brand}

${brand_label_url}

Potwierdź swój adres email

${email} został zarejestrowany jako Twój nowy adres e-mail na ${brand}. Kliknij poniższy przycisk, aby zweryfikować swój adres.

 
Zweryfikuj
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Jeśli masz jakieś pytania, prosimy skontaktuj się z nami.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pl/user/email/update.txt b/services/brig/deb/opt/brig/templates/pl/user/email/update.txt index 63e46b58a26..c90fb831039 100644 --- a/services/brig/deb/opt/brig/templates/pl/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/pl/user/email/update.txt @@ -6,8 +6,8 @@ POTWIERDŹ SWÓJ ADRES EMAIL ${email} został zarejestrowany jako Twój nowy adres e-mail na ${brand}. Kliknij poniższy przycisk, aby zweryfikować swój adres. -Zweryfikuj [${url}]Jeśli nie możesz kliknąć przycisku, skopiuj i wklej ten link -do swojej przeglądarki: +Zweryfikuj [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/activation.html b/services/brig/deb/opt/brig/templates/pt/user/email/activation.html index ea3081ffced..fd31bec4c6d 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/pt/user/email/activation.html @@ -1 +1 @@ -Sua Conta ${brand}

${brand_label_url}

Verifique seu e-mail

${email} foi usado para se registrar no ${brand}.
Clique no botão para verificar seu e-mail.

 
Verificar
 

Se você não conseguir clicar no botão, copie e cole este link no seu navegador:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file +Sua Conta ${brand}

${brand_label_url}

Verifique seu e-mail

${email} foi usado para se registrar no ${brand}.
Clique no botão para verificar seu e-mail.

 
Verificar
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/activation.txt b/services/brig/deb/opt/brig/templates/pt/user/email/activation.txt index ada1772adab..d071c74e2f8 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/pt/user/email/activation.txt @@ -6,8 +6,8 @@ VERIFIQUE SEU E-MAIL ${email} foi usado para se registrar no ${brand}. Clique no botão para verificar seu e-mail. -Verificar [${url}]Se você não conseguir clicar no botão, copie e cole este link -no seu navegador: +Verificar [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/deletion.html b/services/brig/deb/opt/brig/templates/pt/user/email/deletion.html index a9fd902f42a..802d15a741e 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/pt/user/email/deletion.html @@ -1 +1 @@ -Excluir conta?

${brand_label_url}

Excluir sua conta

Nós recebemos uma solicitação para excluir sua conta ${brand}. Clique no botão abaixo em até 10 minutos para excluir todas as suas conversas, conteúdo e conexões.

 
Excluir conta
 

Se você não conseguir clicar no botão, copie e cole este link no seu navegador:

${url}

Se você não solicitou isso, redefina sua senha.

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file +Excluir conta?

${brand_label_url}

Excluir sua conta

Nós recebemos uma solicitação para excluir sua conta ${brand}. Clique no botão abaixo em até 10 minutos para excluir todas as suas conversas, conteúdo e conexões.

 
Excluir conta
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se você não solicitou isso, redefina sua senha.

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/pt/user/email/deletion.txt index 2b9dab30e3b..6e3b614a630 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/pt/user/email/deletion.txt @@ -7,8 +7,8 @@ Nós recebemos uma solicitação para excluir sua conta ${brand}. Clique no bot abaixo em até 10 minutos para excluir todas as suas conversas, conteúdo e conexões. -Excluir conta [${url}]Se você não conseguir clicar no botão, copie e cole este -link no seu navegador: +Excluir conta [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.html index 1066973603f..4e8b5079103 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.html @@ -1 +1 @@ -Mudança de Senha no ${brand}

${brand_label_url}

Redefinir sua senha

Recebemos uma solicitação para redefinir a senha de sua conta ${brand}. Para criar uma nova senha, clique no botão abaixo.

 
Redefinir senha
 

Se você não conseguir clicar no botão, copie e cole este link no seu navegador:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file +Mudança de Senha no ${brand}

${brand_label_url}

Redefinir sua senha

Recebemos uma solicitação para redefinir a senha de sua conta ${brand}. Para criar uma nova senha, clique no botão abaixo.

 
Redefinir senha
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.txt index ce009cf1502..4dc9902c64e 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/pt/user/email/password-reset.txt @@ -6,8 +6,8 @@ REDEFINIR SUA SENHA Recebemos uma solicitação para redefinir a senha de sua conta ${brand}. Para criar uma nova senha, clique no botão abaixo. -Redefinir senha [${url}]Se você não conseguir clicar no botão, copie e cole este -link no seu navegador: +Redefinir senha [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.html index acc4378363a..f209d3dd916 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.html @@ -1 +1 @@ -Conta ${brand}

${brand_label_url}

Sua nova conta em ${brand}

Um nova conta na equipe ${brand} foi criada com ${email}. Por favor, verifique seu e-mail.

 
Verificar
 

Se você não conseguir clicar no botão, copie e cole este link no seu navegador:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file +Conta ${brand}

${brand_label_url}

Sua nova conta em ${brand}

Um nova conta na equipe ${brand} foi criada com ${email}. Por favor, verifique seu e-mail.

 
Verificar
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.txt index 876e24f88b5..8b8640f94a7 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/pt/user/email/team-activation.txt @@ -2,12 +2,12 @@ ${brand_label_url} [${brand_url}] -SUA NOVA CONTA EM ${BRAND} +SUA NOVA CONTA EM ${brand} Um nova conta na equipe ${brand} foi criada com ${email}. Por favor, verifique seu e-mail. -Verificar [${url}]Se você não conseguir clicar no botão, copie e cole este link -no seu navegador: +Verificar [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/update.html b/services/brig/deb/opt/brig/templates/pt/user/email/update.html index 60c2d425e94..b559e366380 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/pt/user/email/update.html @@ -1 +1 @@ -Seu novo endereço de email no ${brand}

${brand_label_url}

Confirme o seu e-mail

${email} foi registrado como seu novo endereço de e-mail no ${brand}. Clique no botão para confirmar seu endereço de e-mail.

 
Verificar
 

Se você não conseguir clicar no botão, copie e cole este link no seu navegador:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file +Seu novo endereço de email no ${brand}

${brand_label_url}

Confirme o seu e-mail

${email} foi registrado como seu novo endereço de e-mail no ${brand}. Clique no botão para confirmar seu endereço de e-mail.

 
Verificar
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Se você tiver alguma dúvida, por favor, entre em contato conosco.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/pt/user/email/update.txt b/services/brig/deb/opt/brig/templates/pt/user/email/update.txt index ea858533a49..455f8424b69 100644 --- a/services/brig/deb/opt/brig/templates/pt/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/pt/user/email/update.txt @@ -6,8 +6,8 @@ CONFIRME O SEU E-MAIL ${email} foi registrado como seu novo endereço de e-mail no ${brand}. Clique no botão para confirmar seu endereço de e-mail. -Verificar [${url}]Se você não conseguir clicar no botão, copie e cole este link -no seu navegador: +Verificar [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/activation.html b/services/brig/deb/opt/brig/templates/ru/user/email/activation.html index 8470a280639..038c9242d4f 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/ru/user/email/activation.html @@ -1 +1 @@ -Ваша учетная запись ${brand}

${brand_label_url}

Подтвердите ваш email

${email} был использован для регистрации в ${brand}.
Нажмите на кнопку для подтверждения вашего email адреса.

 
Подтвердить
 

Если вы не можете нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file +Ваша учетная запись ${brand}

${brand_label_url}

Подтвердите ваш email

${email} был использован для регистрации в ${brand}.
Нажмите на кнопку для подтверждения вашего email адреса.

 
Подтвердить
 

Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/activation.txt b/services/brig/deb/opt/brig/templates/ru/user/email/activation.txt index 4d1b80f496a..f8d08b612f7 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/ru/user/email/activation.txt @@ -6,7 +6,7 @@ ${brand_label_url} [${brand_url}] ${email} был использован для регистрации в ${brand}. Нажмите на кнопку для подтверждения вашего email адреса. -Подтвердить [${url}]Если вы не можете нажать на кнопку, скопируйте и вставьте +Подтвердить [${url}]Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер: ${url} diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/deletion.html b/services/brig/deb/opt/brig/templates/ru/user/email/deletion.html index cb4f186bde7..dbc44cf8837 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/ru/user/email/deletion.html @@ -1 +1 @@ -Удалить учетную запись?

${brand_label_url}

Удалить учетную запись

Мы получили запрос на удаление вашего аккаунта ${brand}. Нажмите на кнопку ниже в течение 10 минут для удаления всех ваших разговоров, контента и контактов.

 
Удалить учетную запись
 

Если вы не можете нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если вы не запрашивали удаление вашего аккаунта, то сбросьте ваш пароль.

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file +Удалить учетную запись?

${brand_label_url}

Удалить учетную запись

Мы получили запрос на удаление вашего аккаунта ${brand}. Нажмите на кнопку ниже в течение 10 минут для удаления всех ваших разговоров, контента и контактов.

 
Удалить учетную запись
 

Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если вы не запрашивали удаление вашего аккаунта, то сбросьте ваш пароль.

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/ru/user/email/deletion.txt index 79d1d70eae7..bdebda4c025 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/ru/user/email/deletion.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] Мы получили запрос на удаление вашего аккаунта ${brand}. Нажмите на кнопку ниже в течение 10 минут для удаления всех ваших разговоров, контента и контактов. -Удалить учетную запись [${url}]Если вы не можете нажать на кнопку, скопируйте и -вставьте эту ссылку в свой браузер: +Удалить учетную запись [${url}]Если вам не удается нажать на кнопку, скопируйте +и вставьте эту ссылку в свой браузер: ${url} diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.html index fd2ea12f9ce..47eb1e8f7fe 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.html @@ -1 +1 @@ -Смена пароля в ${brand}

${brand_label_url}

Сбросить пароль

Мы получили запрос на сброс пароля для вашей учетной записи ${brand}. Чтобы создать новый пароль нажмите на кнопку ниже.

 
Сбросить пароль
 

Если вы не можете нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file +Смена пароля в ${brand}

${brand_label_url}

Сбросить пароль

Мы получили запрос на сброс пароля для вашей учетной записи ${brand}. Чтобы создать новый пароль нажмите на кнопку ниже.

 
Сбросить пароль
 

Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.txt index 13d6f1d10ec..17c9afd959d 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/ru/user/email/password-reset.txt @@ -6,7 +6,7 @@ ${brand_label_url} [${brand_url}] Мы получили запрос на сброс пароля для вашей учетной записи ${brand}. Чтобы создать новый пароль нажмите на кнопку ниже. -Сбросить пароль [${url}]Если вы не можете нажать на кнопку, скопируйте и +Сбросить пароль [${url}]Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер: ${url} diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.html index 8302577b73e..14099793b83 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.html @@ -1 +1 @@ -Ваша учетная запись ${brand}

${brand_label_url}

Ваша новая учетная запись ${brand}

В ${brand} была создана новая команда с использованием email адреса ${email}. Подтвердите ваш email адрес.

 
Подтвердить
 

Если вы не можете нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file +Ваша учетная запись ${brand}

${brand_label_url}

Ваша новая учетная запись ${brand}

В ${brand} была создана новая команда с использованием email адреса ${email}. Подтвердите ваш email адрес.

 
Подтвердить
 

Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.txt index 9ea2873c2f4..44364fcee6d 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/ru/user/email/team-activation.txt @@ -2,11 +2,11 @@ ${brand_label_url} [${brand_url}] -ВАША НОВАЯ УЧЕТНАЯ ЗАПИСЬ ${BRAND} +ВАША НОВАЯ УЧЕТНАЯ ЗАПИСЬ ${brand} В ${brand} была создана новая команда с использованием email адреса ${email}. Подтвердите ваш email адрес. -Подтвердить [${url}]Если вы не можете нажать на кнопку, скопируйте и вставьте +Подтвердить [${url}]Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер: ${url} diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/update.html b/services/brig/deb/opt/brig/templates/ru/user/email/update.html index 36577320f83..c6970ef13dd 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/ru/user/email/update.html @@ -1 +1 @@ -Ваш новый email адрес в ${brand}

${brand_label_url}

Подтвердите ваш email адрес

${email} был указан как ваш новый email адрес в ${brand}. Нажмите на кнопку ниже для подтверждения своего адреса.

 
Подтвердить
 

Если вы не можете нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file +Ваш новый email адрес в ${brand}

${brand_label_url}

Подтвердите ваш email адрес

${email} был указан как ваш новый email адрес в ${brand}. Нажмите на кнопку ниже для подтверждения своего адреса.

 
Подтвердить
 

Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер:

${url}

Если у вас возникли вопросы или нужна помощь, пожалуйста свяжитесь с нами.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/ru/user/email/update.txt b/services/brig/deb/opt/brig/templates/ru/user/email/update.txt index 7e28368272a..b72528a35d4 100644 --- a/services/brig/deb/opt/brig/templates/ru/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/ru/user/email/update.txt @@ -6,7 +6,7 @@ ${brand_label_url} [${brand_url}] ${email} был указан как ваш новый email адрес в ${brand}. Нажмите на кнопку ниже для подтверждения своего адреса. -Подтвердить [${url}]Если вы не можете нажать на кнопку, скопируйте и вставьте +Подтвердить [${url}]Если вам не удается нажать на кнопку, скопируйте и вставьте эту ссылку в свой браузер: ${url} diff --git a/services/brig/deb/opt/brig/templates/si/user/email/activation.html b/services/brig/deb/opt/brig/templates/si/user/email/activation.html index 5469f094995..236178d9b3e 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/si/user/email/activation.html @@ -1 +1 @@ -ඔබගේ ${brand} ගිණුම

${brand_label_url}

ඔබගේ වි-තැපෑල සත්‍යාපනය කරන්න

${brand} හි ලියාපදිංචියට ${email} භාවිතා කර ඇත.
ඔබගේ ලිපිනය සත්‍යාපනයට පහත බොත්තම ඔබන්න.

 
සත්‍යාපනය
 

බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ අතිරික්සුවෙහි අලවන්න:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file +ඔබගේ ${brand} ගිණුම

${brand_label_url}

ඔබගේ වි-තැපෑල සත්‍යාපනය කරන්න

${brand} හි ලියාපදිංචියට ${email} භාවිතා කර ඇත.
ඔබගේ ලිපිනය සත්‍යාපනයට පහත බොත්තම ඔබන්න.

 
සත්‍යාපනය
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/si/user/email/activation.txt b/services/brig/deb/opt/brig/templates/si/user/email/activation.txt index bab1d042dc5..a172c2998f9 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/si/user/email/activation.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] ${brand} හි ලියාපදිංචියට ${email} භාවිතා කර ඇත. ඔබගේ ලිපිනය සත්‍යාපනයට පහත බොත්තම ඔබන්න. -සත්‍යාපනය [${url}]බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ -අතිරික්සුවෙහි අලවන්න: +සත්‍යාපනය [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/si/user/email/deletion.html b/services/brig/deb/opt/brig/templates/si/user/email/deletion.html index 6852a1f7796..319f24c339f 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/si/user/email/deletion.html @@ -1 +1 @@ -ගිණුම මකනවාද?

${brand_label_url}

ඔබගේ ගිණුම මකන්න

ඔබගේ ${brand} ගිණුම මැකීම සඳහා අපට ඉල්ලීමක් ලැබුණි. ඔබගේ සියළුම සංවාද, අන්තර්ගත සහ සම්බන්ධතා මැකීමට විනාඩි 10 ක් ඇතුළත පහත බොත්තම ඔබන්න.

 
ගිණුම මකන්න
 

බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ අතිරික්සුවෙහි අලවන්න:

${url}

ඔබ මෙය ඉල්ලුවේ නැති නම්, මුරපදය යළි සකසන්න.

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file +ගිණුම මකනවාද?

${brand_label_url}

ඔබගේ ගිණුම මකන්න

ඔබගේ ${brand} ගිණුම මැකීම සඳහා අපට ඉල්ලීමක් ලැබුණි. ඔබගේ සියළුම සංවාද, අන්තර්ගත සහ සම්බන්ධතා මැකීමට විනාඩි 10 ක් ඇතුළත පහත බොත්තම ඔබන්න.

 
ගිණුම මකන්න
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ඔබ මෙය ඉල්ලුවේ නැති නම්, මුරපදය යළි සකසන්න.

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/si/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/si/user/email/deletion.txt index 07207417957..ce8bc509b06 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/si/user/email/deletion.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] ඔබගේ ${brand} ගිණුම මැකීම සඳහා අපට ඉල්ලීමක් ලැබුණි. ඔබගේ සියළුම සංවාද, අන්තර්ගත සහ සම්බන්ධතා මැකීමට විනාඩි 10 ක් ඇතුළත පහත බොත්තම ඔබන්න. -ගිණුම මකන්න [${url}]බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ -අතිරික්සුවෙහි අලවන්න: +ගිණුම මකන්න [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/si/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/si/user/email/password-reset.html index fd5fe0d1863..ca31f5eaf9a 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/si/user/email/password-reset.html @@ -1 +1 @@ -${brand} මුරපදය වෙනස් කිරීම

${brand_label_url}

මුරපදය යළි සකසන්න

ඔබගේ ${brand} ගිණුමේ මුරපදය යළි සැකසීම සඳහා අපට ඉල්ලීමක් ලැබුණි. නව මුරපදයක් සෑදීමට පහත බොත්තම ඔබන්න.

 
මුරපදය යළි සකසන්න
 

බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ අතිරික්සුවෙහි අලවන්න:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file +${brand} මුරපදය වෙනස් කිරීම

${brand_label_url}

මුරපදය යළි සකසන්න

ඔබගේ ${brand} ගිණුමේ මුරපදය යළි සැකසීම සඳහා අපට ඉල්ලීමක් ලැබුණි. නව මුරපදයක් සෑදීමට පහත බොත්තම ඔබන්න.

 
මුරපදය යළි සකසන්න
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/si/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/si/user/email/password-reset.txt index fddd05d4af4..4f862173417 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/si/user/email/password-reset.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] ඔබගේ ${brand} ගිණුමේ මුරපදය යළි සැකසීම සඳහා අපට ඉල්ලීමක් ලැබුණි. නව මුරපදයක් සෑදීමට පහත බොත්තම ඔබන්න. -මුරපදය යළි සකසන්න [${url}]බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ -අතිරික්සුවෙහි අලවන්න: +මුරපදය යළි සකසන්න [${url}]If you can’t select the button, copy and paste this +link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/si/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/si/user/email/team-activation.html index 7017f3c8545..aab81c604fc 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/si/user/email/team-activation.html @@ -1 +1 @@ -${brand} ගිණුම

${brand_label_url}

ඔබගේ නව ${brand} ගිණුම

${email} සමඟ නව ${brand} කණ්ඩායමක් සාදා ඇත. ඔබගේ වි-තැපෑල සත්‍යාපනය කරන්න.

 
සත්‍යාපනය
 

බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ අතිරික්සුවෙහි අලවන්න:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file +${brand} ගිණුම

${brand_label_url}

ඔබගේ නව ${brand} ගිණුම

${email} සමඟ නව ${brand} කණ්ඩායමක් සාදා ඇත. ඔබගේ වි-තැපෑල සත්‍යාපනය කරන්න.

 
සත්‍යාපනය
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/si/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/si/user/email/team-activation.txt index 520e00970c2..d585732b70e 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/si/user/email/team-activation.txt @@ -2,11 +2,11 @@ ${brand_label_url} [${brand_url}] -ඔබගේ නව ${BRAND} ගිණුම +ඔබගේ නව ${brand} ගිණුම ${email} සමඟ නව ${brand} කණ්ඩායමක් සාදා ඇත. ඔබගේ වි-තැපෑල සත්‍යාපනය කරන්න. -සත්‍යාපනය [${url}]බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ -අතිරික්සුවෙහි අලවන්න: +සත්‍යාපනය [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/si/user/email/update.html b/services/brig/deb/opt/brig/templates/si/user/email/update.html index a0ad8cff780..54101e81d60 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/si/user/email/update.html @@ -1 +1 @@ -${brand} සඳහා නව වි-තැපැල් ලිපිනය

${brand_label_url}

වි-තැපෑල සත්‍යාපනය කරන්න

ඔබගේ නව ${brand} වි-තැපැල් ලිපිනය ලෙස ${email} ලියාපදිංචි කර ඇත. ඔබගේ ලිපිනය සත්‍යාපනයට පහත බොත්තම ඔබන්න.

 
සත්‍යාපනය
 

බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ අතිරික්සුවෙහි අලවන්න:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file +${brand} සඳහා නව වි-තැපැල් ලිපිනය

${brand_label_url}

වි-තැපෑල සත්‍යාපනය කරන්න

ඔබගේ නව ${brand} වි-තැපැල් ලිපිනය ලෙස ${email} ලියාපදිංචි කර ඇත. ඔබගේ ලිපිනය සත්‍යාපනයට පහත බොත්තම ඔබන්න.

 
සත්‍යාපනය
 

If you can’t select the button, copy and paste this link to your browser:

${url}

ඔබට කිසියම් ප්‍රශ්නයක් ඇත්නම් කරුණාකර අප අමතන්න.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/si/user/email/update.txt b/services/brig/deb/opt/brig/templates/si/user/email/update.txt index 326cfeb0d08..77332028d5e 100644 --- a/services/brig/deb/opt/brig/templates/si/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/si/user/email/update.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] ඔබගේ නව ${brand} වි-තැපැල් ලිපිනය ලෙස ${email} ලියාපදිංචි කර ඇත. ඔබගේ ලිපිනය සත්‍යාපනයට පහත බොත්තම ඔබන්න. -සත්‍යාපනය [${url}]බොත්තම එබීමට නොහැකි නම් මෙම සබැඳිය පිටපත් කර ඔබගේ -අතිරික්සුවෙහි අලවන්න: +සත්‍යාපනය [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/activation.html b/services/brig/deb/opt/brig/templates/tr/user/email/activation.html index 024f68bd64c..acc10e2d534 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/tr/user/email/activation.html @@ -1 +1 @@ -${brand} Hesabınız

${brand_label_url}

E-postanızı doğrulayın

${brand}} a kaydolmak için ${email} kullanıldı.
Adresinizi doğrulamak için düğmeyi tıklayın.

 
Doğrula
 

Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza yapıştırın:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file +${brand} Hesabınız

${brand_label_url}

E-postanızı doğrulayın

${brand}} a kaydolmak için ${email} kullanıldı.
Adresinizi doğrulamak için düğmeyi tıklayın.

 
Doğrula
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/activation.txt b/services/brig/deb/opt/brig/templates/tr/user/email/activation.txt index 17b7f8815c8..3939ecbc58c 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/tr/user/email/activation.txt @@ -6,8 +6,8 @@ E-POSTANIZI DOĞRULAYIN ${brand}} a kaydolmak için ${email} kullanıldı. Adresinizi doğrulamak için düğmeyi tıklayın. -Doğrula [${url}]Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza -yapıştırın: +Doğrula [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/deletion.html b/services/brig/deb/opt/brig/templates/tr/user/email/deletion.html index 56e31fa36bd..eacb1369eb3 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/tr/user/email/deletion.html @@ -1 +1 @@ -Hesabı sil?

${brand_label_url}

Hesabını Sil

${brand} hesabınızı silmek için bir istek aldık. Tüm konuşmalarınızı, içeriğinizi ve bağlantılarınızı silmek için 10 dakika içinde aşağıdaki düğmeyi tıklayın.

 
Hesabı Sil
 

Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza yapıştırın:

${url}

Bunu istemediyseniz, şifrenizi sıfırlayın.

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file +Hesabı sil?

${brand_label_url}

Hesabını Sil

${brand} hesabınızı silmek için bir istek aldık. Tüm konuşmalarınızı, içeriğinizi ve bağlantılarınızı silmek için 10 dakika içinde aşağıdaki düğmeyi tıklayın.

 
Hesabı Sil
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Bunu istemediyseniz, şifrenizi sıfırlayın.

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/tr/user/email/deletion.txt index 4fa840086de..98c21039085 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/tr/user/email/deletion.txt @@ -7,8 +7,8 @@ ${brand} hesabınızı silmek için bir istek aldık. Tüm konuşmalarınızı, içeriğinizi ve bağlantılarınızı silmek için 10 dakika içinde aşağıdaki düğmeyi tıklayın. -Hesabı Sil [${url}]Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp -tarayıcınıza yapıştırın: +Hesabı Sil [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.html index bb1d0aa60a9..a210d31fa59 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.html @@ -1 +1 @@ -${brand} 'da Şifre Değişikliği

${brand_label_url}

Şifrenizi sıfırlayın

${brand} hesabınızın şifresini sıfırlama isteği aldık. Yeni bir şifre oluşturmak için aşağıdaki butona tıklayın.

 
Şifreni sıfırla
 

Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza yapıştırın:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file +${brand} 'da Şifre Değişikliği

${brand_label_url}

Şifrenizi sıfırlayın

${brand} hesabınızın şifresini sıfırlama isteği aldık. Yeni bir şifre oluşturmak için aşağıdaki butona tıklayın.

 
Şifreni sıfırla
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.txt index 7c9925c6ca7..f2d1107f5d8 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/tr/user/email/password-reset.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] ${brand} hesabınızın şifresini sıfırlama isteği aldık. Yeni bir şifre oluşturmak için aşağıdaki butona tıklayın. -Şifreni sıfırla [${url}]Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp -tarayıcınıza yapıştırın: +Şifreni sıfırla [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.html index 6e2a676f7e9..5d0f488d504 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.html @@ -1 +1 @@ -${brand} Hesap

${brand_label_url}

${brand}'da yeni hesabınız

${email} ile yeni bir ${brand} takımı oluşturuldu. Lütfen e-postanızı doğrulayın.

 
Doğrula
 

Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza yapıştırın:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file +${brand} Hesap

${brand_label_url}

${brand}'da yeni hesabınız

${email} ile yeni bir ${brand} takımı oluşturuldu. Lütfen e-postanızı doğrulayın.

 
Doğrula
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.txt index 8615ceb768e..0e18b3aead6 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/tr/user/email/team-activation.txt @@ -2,12 +2,12 @@ ${brand_label_url} [${brand_url}] -${BRAND}'DA YENI HESABINIZ +${brand}'DA YENI HESABINIZ ${email} ile yeni bir ${brand} takımı oluşturuldu. Lütfen e-postanızı doğrulayın. -Doğrula [${url}]Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza -yapıştırın: +Doğrula [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/update.html b/services/brig/deb/opt/brig/templates/tr/user/email/update.html index 5b14f678898..a8ed525178b 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/tr/user/email/update.html @@ -1 +1 @@ -${brand} üzerindeki yeni e-posta adresiniz

${brand_label_url}

E-postanızı doğrulayın

${email}, ${brand}'daki yeni e-posta adresiniz olarak kaydedildi. Adresinizi doğrulamak için aşağıdaki düğmeye tıklayın.

 
Doğrula
 

Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza yapıştırın:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file +${brand} üzerindeki yeni e-posta adresiniz

${brand_label_url}

E-postanızı doğrulayın

${email}, ${brand}'daki yeni e-posta adresiniz olarak kaydedildi. Adresinizi doğrulamak için aşağıdaki düğmeye tıklayın.

 
Doğrula
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Herhangi bir sorunuz veya yardıma ihtiyacınız varsa, lütfen bize ulaşın.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/tr/user/email/update.txt b/services/brig/deb/opt/brig/templates/tr/user/email/update.txt index e8346877e6d..4dc190c81ae 100644 --- a/services/brig/deb/opt/brig/templates/tr/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/tr/user/email/update.txt @@ -6,8 +6,8 @@ E-POSTANIZI DOĞRULAYIN ${email}, ${brand}'daki yeni e-posta adresiniz olarak kaydedildi. Adresinizi doğrulamak için aşağıdaki düğmeye tıklayın. -Doğrula [${url}]Düğmeyi tıklayamıyorsanız, bu bağlantıyı kopyalayıp tarayıcınıza -yapıştırın: +Doğrula [${url}]If you can’t select the button, copy and paste this link to your +browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/version b/services/brig/deb/opt/brig/templates/version index fea60e70c1a..5c41189b952 100644 --- a/services/brig/deb/opt/brig/templates/version +++ b/services/brig/deb/opt/brig/templates/version @@ -1 +1 @@ -v1.0.121 +v1.0.122 diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/activation.html b/services/brig/deb/opt/brig/templates/vi/user/email/activation.html index 3b47400c118..19833198536 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/activation.html +++ b/services/brig/deb/opt/brig/templates/vi/user/email/activation.html @@ -1 +1 @@ -Tài khoản ${brand} của bạn

${brand_label_url}

Xác minh địa chỉ emal của bạn

${email} đã được dùng để đăng ký ${brand}.
Nhấp vào nút để xác minh địa chỉ của bạn.

 
Xác minh
 

Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này vào trình duyệt của bạn:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file +Tài khoản ${brand} của bạn

${brand_label_url}

Xác minh địa chỉ emal của bạn

${email} đã được dùng để đăng ký ${brand}.
Nhấp vào nút để xác minh địa chỉ của bạn.

 
Xác minh
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/activation.txt b/services/brig/deb/opt/brig/templates/vi/user/email/activation.txt index 9fd76c0cace..a2db915b4ae 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/activation.txt +++ b/services/brig/deb/opt/brig/templates/vi/user/email/activation.txt @@ -6,8 +6,8 @@ XÁC MINH ĐỊA CHỈ EMAL CỦA BẠN ${email} đã được dùng để đăng ký ${brand}. Nhấp vào nút để xác minh địa chỉ của bạn. -Xác minh [${url}]Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này -vào trình duyệt của bạn: +Xác minh [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/deletion.html b/services/brig/deb/opt/brig/templates/vi/user/email/deletion.html index 274ee2d08b4..a19046cbf1b 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/deletion.html +++ b/services/brig/deb/opt/brig/templates/vi/user/email/deletion.html @@ -1 +1 @@ -Xoá tài khoản?

${brand_label_url}

Xoá tài khoản của bạn

Chúng tôi nhận được một yêu cầu xoá tài khoản ${brand} của bạn. Nhấp vào nút phía bên dưới trong vòng 10 phút để xoá toàn bộ cuộc hội thoại, nội dung và mọi kết nối của bạn.

 
Xoá tài khoản
 

Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này vào trình duyệt của bạn:

${url}

Nếu bạn không thực hiện yêu cầu này, thay đổi mật khẩu của bạn ngay.

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file +Xoá tài khoản?

${brand_label_url}

Xoá tài khoản của bạn

Chúng tôi nhận được một yêu cầu xoá tài khoản ${brand} của bạn. Nhấp vào nút phía bên dưới trong vòng 10 phút để xoá toàn bộ cuộc hội thoại, nội dung và mọi kết nối của bạn.

 
Xoá tài khoản
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Nếu bạn không thực hiện yêu cầu này, thay đổi mật khẩu của bạn ngay.

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/deletion.txt b/services/brig/deb/opt/brig/templates/vi/user/email/deletion.txt index 3dfd5366e04..5fdb53d1cdd 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/deletion.txt +++ b/services/brig/deb/opt/brig/templates/vi/user/email/deletion.txt @@ -7,8 +7,8 @@ Chúng tôi nhận được một yêu cầu xoá tài khoản ${brand} của b phía bên dưới trong vòng 10 phút để xoá toàn bộ cuộc hội thoại, nội dung và mọi kết nối của bạn. -Xoá tài khoản [${url}]Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn -này vào trình duyệt của bạn: +Xoá tài khoản [${url}]If you can’t select the button, copy and paste this link +to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.html b/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.html index 3aad8d78af1..949db860f09 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.html +++ b/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.html @@ -1 +1 @@ -Thay đổi mật khẩu ${brand}

${brand_label_url}

Đặt lại mật khẩu của bạn

Chúng tôi nhận được một yêu cầu đặt lại mật khẩu cho tài khoản ${brand} của bạn. Để tạo một tài khoản mới, nhấp vào nút phía bên dưới.

 
Đặt lại mật khẩu
 

Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này vào trình duyệt của bạn:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file +Thay đổi mật khẩu ${brand}

${brand_label_url}

Đặt lại mật khẩu của bạn

Chúng tôi nhận được một yêu cầu đặt lại mật khẩu cho tài khoản ${brand} của bạn. Để tạo một tài khoản mới, nhấp vào nút phía bên dưới.

 
Đặt lại mật khẩu
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.txt b/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.txt index 19be97f5f4e..e77c47a7214 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.txt +++ b/services/brig/deb/opt/brig/templates/vi/user/email/password-reset.txt @@ -6,8 +6,8 @@ ${brand_label_url} [${brand_url}] Chúng tôi nhận được một yêu cầu đặt lại mật khẩu cho tài khoản ${brand} của bạn. Để tạo một tài khoản mới, nhấp vào nút phía bên dưới. -Đặt lại mật khẩu [${url}]Nếu bạn không thể nhấp vào nút, sao chép và gán đường -dẫn này vào trình duyệt của bạn: +Đặt lại mật khẩu [${url}]If you can’t select the button, copy and paste this +link to your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.html b/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.html index 48edcff50b3..d7e1d16c93e 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.html +++ b/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.html @@ -1 +1 @@ -Tài khoản ${brand}

${brand_label_url}

Tài khoản mới của bạn trên ${brand}

Một nhóm ${brand} đã được tại với ${email}. Vui lòng xác minh địa chỉ email của bạn.

 
Xác minh
 

Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này vào trình duyệt của bạn:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file +Tài khoản ${brand}

${brand_label_url}

Tài khoản mới của bạn trên ${brand}

Một nhóm ${brand} đã được tại với ${email}. Vui lòng xác minh địa chỉ email của bạn.

 
Xác minh
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.txt b/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.txt index 021963e3ac9..effd3a21a6a 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.txt +++ b/services/brig/deb/opt/brig/templates/vi/user/email/team-activation.txt @@ -2,12 +2,12 @@ ${brand_label_url} [${brand_url}] -TÀI KHOẢN MỚI CỦA BẠN TRÊN ${BRAND} +TÀI KHOẢN MỚI CỦA BẠN TRÊN ${brand} Một nhóm ${brand} đã được tại với ${email}. Vui lòng xác minh địa chỉ email của bạn. -Xác minh [${url}]Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này -vào trình duyệt của bạn: +Xác minh [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/update.html b/services/brig/deb/opt/brig/templates/vi/user/email/update.html index d227a8e59d7..8648dbebfa9 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/update.html +++ b/services/brig/deb/opt/brig/templates/vi/user/email/update.html @@ -1 +1 @@ -Địa chỉ eamil mới trên ${brand}

${brand_label_url}

Xác minh địa chỉ emal của bạn

${email} đã được đăng ký như là địa chỉ email mới của bạn trên ${brand}. Nhấp vào nút phía bên dưới để xác minh địa chỉ email của bạn.

 
Xác minh
 

Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này vào trình duyệt của bạn:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file +Địa chỉ eamil mới trên ${brand}

${brand_label_url}

Xác minh địa chỉ emal của bạn

${email} đã được đăng ký như là địa chỉ email mới của bạn trên ${brand}. Nhấp vào nút phía bên dưới để xác minh địa chỉ email của bạn.

 
Xác minh
 

If you can’t select the button, copy and paste this link to your browser:

${url}

Nếu bạn có bất kỳ thắc mắc nào, xin vui lòng liên hệ với chúng tôi.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/vi/user/email/update.txt b/services/brig/deb/opt/brig/templates/vi/user/email/update.txt index 721f2a11b0f..f8f5a9ce53a 100644 --- a/services/brig/deb/opt/brig/templates/vi/user/email/update.txt +++ b/services/brig/deb/opt/brig/templates/vi/user/email/update.txt @@ -6,8 +6,8 @@ XÁC MINH ĐỊA CHỈ EMAL CỦA BẠN ${email} đã được đăng ký như là địa chỉ email mới của bạn trên ${brand}. Nhấp vào nút phía bên dưới để xác minh địa chỉ email của bạn. -Xác minh [${url}]Nếu bạn không thể nhấp vào nút, sao chép và gán đường dẫn này -vào trình duyệt của bạn: +Xác minh [${url}]If you can’t select the button, copy and paste this link to +your browser: ${url} From b0b5a06d50082f030f56ba09d73f0a22e80b3c64 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 25 Oct 2024 11:59:02 +0200 Subject: [PATCH 02/27] WPB-685 give SCIM connections human readable names (#4307) --- cassandra-schema.cql | 2 + changelog.d/1-api-changes/WPB-685 | 1 + changelog.d/2-features/WPB-685 | 1 + integration/test/API/Spar.hs | 11 + integration/test/Test/Spar.hs | 31 +++ libs/types-common/src/Data/Id.hs | 37 +-- .../src/Wire/API/Routes/Public/Spar.hs | 24 +- libs/wire-api/src/Wire/API/Routes/Version.hs | 14 + libs/wire-api/src/Wire/API/SwaggerServant.hs | 10 +- libs/wire-api/src/Wire/API/User/Scim.hs | 259 ++++++++---------- .../golden/Test/Wire/API/Golden/Manual.hs | 5 + .../Wire/API/Golden/Manual/CreateScimToken.hs | 6 +- .../Golden/Manual/CreateScimTokenResponse.hs | 38 +++ .../testObject_CreateScimTokenResponse_1.json | 10 + .../golden/testObject_CreateScimToken_4.json | 3 +- .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 1 + libs/wire-api/wire-api.cabal | 1 + .../integration/API/UserPendingActivation.hs | 7 +- services/spar/default.nix | 1 + services/spar/spar.cabal | 2 + services/spar/src/Spar/Schema/Run.hs | 4 +- services/spar/src/Spar/Schema/V19.hs | 36 +++ services/spar/src/Spar/Scim/Auth.hs | 71 ++++- services/spar/src/Spar/Sem/ScimTokenStore.hs | 4 +- .../src/Spar/Sem/ScimTokenStore/Cassandra.hs | 82 ++++-- .../spar/src/Spar/Sem/ScimTokenStore/Mem.hs | 10 +- .../Test/Spar/Scim/AuthSpec.hs | 103 +++---- services/spar/test-integration/Util/Core.hs | 59 +++- services/spar/test-integration/Util/Scim.hs | 5 +- services/spar/test/Arbitrary.hs | 21 +- services/spar/test/Test/Spar/Scim/UserSpec.hs | 4 +- 31 files changed, 586 insertions(+), 277 deletions(-) create mode 100644 changelog.d/1-api-changes/WPB-685 create mode 100644 changelog.d/2-features/WPB-685 create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimTokenResponse.hs create mode 100644 libs/wire-api/test/golden/testObject_CreateScimTokenResponse_1.json create mode 100644 services/spar/src/Spar/Schema/V19.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index b0fb20beb67..28fad0acf4a 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -1965,6 +1965,7 @@ CREATE TABLE spar_test.team_provisioning_by_team ( created_at timestamp, descr text, idp uuid, + name text, token_ text, PRIMARY KEY (team, id) ) WITH CLUSTERING ORDER BY (id ASC) @@ -2049,6 +2050,7 @@ CREATE TABLE spar_test.team_provisioning_by_token ( descr text, id uuid, idp uuid, + name text, team uuid ) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} diff --git a/changelog.d/1-api-changes/WPB-685 b/changelog.d/1-api-changes/WPB-685 new file mode 100644 index 00000000000..1dbe090ee80 --- /dev/null +++ b/changelog.d/1-api-changes/WPB-685 @@ -0,0 +1 @@ +New variant in API version 7 of endpoints for creating and listing SCIM tokens that support a `name` field. New endpoint in version 7 for updating a SCIM token name. diff --git a/changelog.d/2-features/WPB-685 b/changelog.d/2-features/WPB-685 new file mode 100644 index 00000000000..f7e640abc8c --- /dev/null +++ b/changelog.d/2-features/WPB-685 @@ -0,0 +1 @@ +Added human readable names for SCIM tokens diff --git a/integration/test/API/Spar.hs b/integration/test/API/Spar.hs index ee57ef581aa..c925c7cc5d7 100644 --- a/integration/test/API/Spar.hs +++ b/integration/test/API/Spar.hs @@ -18,6 +18,17 @@ createScimToken caller = do req <- baseRequest caller Spar Versioned "/scim/auth-tokens" submit "POST" $ req & addJSONObject ["password" .= defPassword, "description" .= "integration test"] +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_scim_auth_tokens +createScimTokenWithName :: (HasCallStack, MakesValue caller) => caller -> String -> App Response +createScimTokenWithName caller name = do + req <- baseRequest caller Spar Versioned "/scim/auth-tokens" + submit "POST" $ req & addJSONObject ["password" .= defPassword, "description" .= "integration test", "name" .= name] + +putScimTokenName :: (HasCallStack, MakesValue caller) => caller -> String -> String -> App Response +putScimTokenName caller token name = do + req <- baseRequest caller Spar Versioned $ joinHttpPath ["scim", "auth-tokens", token] + submit "PUT" $ req & addJSONObject ["name" .= name] + createScimUser :: (HasCallStack, MakesValue domain, MakesValue scimUser) => domain -> String -> scimUser -> App Response createScimUser domain token scimUser = do req <- baseRequest domain Spar Versioned "/scim/v2/Users" diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs index 7c9d2b8bd77..c18a517d2ea 100644 --- a/integration/test/Test/Spar.hs +++ b/integration/test/Test/Spar.hs @@ -311,3 +311,34 @@ checkSparGetUserAndFindByExtId domain tok extId uid k = do k userByUid userByUid `shouldMatch` userByIdExtId + +testSparCreateScimTokenNoName :: (HasCallStack) => App () +testSparCreateScimTokenNoName = do + (owner, _tid, mem : _) <- createTeam OwnDomain 2 + createScimToken owner >>= assertSuccess + createScimToken owner >>= assertSuccess + tokens <- bindResponse (getScimTokens owner) $ \resp -> do + resp.status `shouldMatchInt` 200 + tokens <- resp.json %. "tokens" >>= asList + for_ tokens $ \token -> do + token %. "name" `shouldMatch` (token %. "id") + pure tokens + for_ tokens $ \token -> do + tokenId <- token %. "id" >>= asString + putScimTokenName mem tokenId "new name" >>= assertStatus 403 + putScimTokenName owner tokenId ("token:" <> tokenId) >>= assertSuccess + bindResponse (getScimTokens owner) $ \resp -> do + resp.status `shouldMatchInt` 200 + updatedTokens <- resp.json %. "tokens" >>= asList + for_ updatedTokens $ \token -> do + tokenId <- token %. "id" >>= asString + token %. "name" `shouldMatch` ("token:" <> tokenId) + +testSparCreateScimTokenWithName :: (HasCallStack) => App () +testSparCreateScimTokenWithName = do + (owner, _tid, _) <- createTeam OwnDomain 1 + let expected = "my scim token" + createScimTokenWithName owner expected >>= assertSuccess + tokens <- getScimTokens owner >>= getJSON 200 >>= (%. "tokens") >>= asList + for_ tokens $ \token -> do + token %. "name" `shouldMatch` expected diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index 0a1dbe22ad3..9ae33bcc7df 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -53,6 +53,9 @@ module Data.Id NoId, OAuthClientId, OAuthRefreshTokenId, + + -- * Utils + uuidSchema, ) where @@ -176,23 +179,23 @@ newtype Id a = Id deriving (ToJSON, FromJSON, S.ToSchema) via Schema (Id a) instance ToSchema (Id a) where - schema = Id <$> toUUID .= uuid - where - uuid :: ValueSchema NamedSwaggerDoc UUID - uuid = - mkSchema - (addExample (swaggerDoc @UUID)) - ( A.withText - "UUID" - ( maybe (fail "Invalid UUID") pure - . UUID.fromText - ) - ) - (pure . A.toJSON . UUID.toText) - - addExample = - S.schema . S.example - ?~ toJSON ("99db9768-04e3-4b5d-9268-831b6a25c4ab" :: Text) + schema = Id <$> toUUID .= uuidSchema + +uuidSchema :: ValueSchema NamedSwaggerDoc UUID +uuidSchema = + mkSchema + (addExample (swaggerDoc @UUID)) + ( A.withText + "UUID" + ( maybe (fail "Invalid UUID") pure + . UUID.fromText + ) + ) + (pure . A.toJSON . UUID.toText) + where + addExample = + S.schema . S.example + ?~ toJSON ("99db9768-04e3-4b5d-9268-831b6a25c4ab" :: Text) -- REFACTOR: non-derived, custom show instances break pretty-show and violate the law -- that @show . read == id@. can we derive Show here? diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs index bf87bfb3fef..787da9d22a2 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -25,7 +25,6 @@ import SAML2.WebSSO qualified as SAML import Servant import Servant.API.Extended import Servant.Multipart -import Servant.OpenApi import URI.ByteString qualified as URI import Web.Scim.Capabilities.MetaSchema as Scim.Meta import Web.Scim.Class.Auth as Scim.Auth @@ -37,6 +36,8 @@ import Wire.API.Routes.API import Wire.API.Routes.Internal.Spar import Wire.API.Routes.Named import Wire.API.Routes.Public +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.SwaggerServant import Wire.API.User.IdentityProvider import Wire.API.User.Saml @@ -188,9 +189,21 @@ data ScimSite tag route = ScimSite deriving (Generic) type APIScimToken = - Named "auth-tokens-create" (ZOptUser :> APIScimTokenCreate) + Named "auth-tokens-create@v6" (Until 'V7 :> ZOptUser :> APIScimTokenCreateV6) + :<|> Named "auth-tokens-create" (From 'V7 :> ZOptUser :> APIScimTokenCreate) + :<|> Named "auth-tokens-put-name" (From 'V7 :> ZUser :> APIScimTokenPutName) :<|> Named "auth-tokens-delete" (ZOptUser :> APIScimTokenDelete) - :<|> Named "auth-tokens-list" (ZOptUser :> APIScimTokenList) + :<|> Named "auth-tokens-list@v6" (Until 'V7 :> ZOptUser :> APIScimTokenListV6) + :<|> Named "auth-tokens-list" (From 'V7 :> ZOptUser :> APIScimTokenList) + +type APIScimTokenPutName = + Capture "id" ScimTokenId + :> ReqBody '[JSON] ScimTokenName + :> Put '[JSON] () + +type APIScimTokenCreateV6 = + VersionedReqBody 'V6 '[JSON] CreateScimToken + :> Post '[JSON] CreateScimTokenResponseV6 type APIScimTokenCreate = ReqBody '[JSON] CreateScimToken @@ -203,9 +216,10 @@ type APIScimTokenDelete = type APIScimTokenList = Get '[JSON] ScimTokenList +type APIScimTokenListV6 = + Get '[JSON] ScimTokenListV6 + data SparAPITag instance ServiceAPI SparAPITag v where type ServiceAPIRoutes SparAPITag = SparAPI - type SpecialisedAPIRoutes v SparAPITag = SparAPI - serviceSwagger = toOpenApi (Proxy @SparAPI) diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index ec1673aee49..13ec55c42ea 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -68,7 +68,9 @@ import Data.Text.Encoding as Text import GHC.TypeLits import Imports hiding ((\\)) import Servant +import Servant.API.Extended (ReqBodyCustomError) import Servant.API.Extended.RawM qualified as RawM +import Servant.Multipart (MultipartForm) import Wire.API.Deprecated import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named hiding (unnamed) @@ -293,12 +295,24 @@ type instance SpecialiseToVersion v (MultiVerb m t r x) = MultiVerb m t r x +type instance + SpecialiseToVersion v (NoContentVerb m) = + NoContentVerb m + type instance SpecialiseToVersion v RawM.RawM = RawM.RawM type instance SpecialiseToVersion v (ReqBody t x :> api) = ReqBody t x :> SpecialiseToVersion v api +type instance + SpecialiseToVersion v (ReqBodyCustomError t l x :> api) = + ReqBodyCustomError t l x :> SpecialiseToVersion v api + +type instance + SpecialiseToVersion v (MultipartForm x b :> api) = + MultipartForm x b :> SpecialiseToVersion v api + type instance SpecialiseToVersion v (QueryParam' mods l x :> api) = QueryParam' mods l x :> SpecialiseToVersion v api diff --git a/libs/wire-api/src/Wire/API/SwaggerServant.hs b/libs/wire-api/src/Wire/API/SwaggerServant.hs index 8ea0729a504..f5ad2081593 100644 --- a/libs/wire-api/src/Wire/API/SwaggerServant.hs +++ b/libs/wire-api/src/Wire/API/SwaggerServant.hs @@ -23,9 +23,8 @@ where import Data.Metrics.Servant import Data.Proxy -import Imports hiding (head) import Servant -import Servant.OpenApi (HasOpenApi (toOpenApi)) +import Wire.API.Routes.Version -- | A type-level tag that lets us omit any branch from Swagger docs. -- @@ -34,9 +33,6 @@ import Servant.OpenApi (HasOpenApi (toOpenApi)) -- it's only justification is laziness. data OmitDocs -instance HasOpenApi (OmitDocs :> a) where - toOpenApi _ = mempty - instance (HasServer api ctx) => HasServer (OmitDocs :> api) ctx where type ServerT (OmitDocs :> api) m = ServerT api m @@ -46,3 +42,7 @@ instance (HasServer api ctx) => HasServer (OmitDocs :> api) ctx where instance (RoutesToPaths api) => RoutesToPaths (OmitDocs :> api) where getRoutes = getRoutes @api + +type instance + SpecialiseToVersion v (OmitDocs :> api) = + EmptyAPI diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index dd7f4ad8993..07c07c3beea 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -42,7 +42,7 @@ -- * Request and response types for SCIM-related endpoints. module Wire.API.User.Scim where -import Control.Lens (makeLenses, mapped, to, (.~), (?~), (^.)) +import Control.Lens (makeLenses, to, (.~), (^.)) import Control.Monad.Except (throwError) import Crypto.Hash (hash) import Crypto.Hash.Algorithms (SHA512) @@ -55,13 +55,14 @@ import Data.ByteString.Conversion (FromByteString (..), ToByteString (..)) import Data.CaseInsensitive qualified as CI import Data.Code as Code import Data.Handle (Handle) -import Data.Id (ScimTokenId, TeamId, UserId) -import Data.Json.Util ((#)) +import Data.Id +import Data.Json.Util import Data.Map qualified as Map import Data.Misc (PlainTextPassword6) -import Data.OpenApi hiding (Operation) -import Data.Proxy +import Data.OpenApi qualified as S +import Data.Schema as Schema import Data.Text qualified as T +import Data.Text qualified as Text import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.These import Data.These.Combinators @@ -87,6 +88,8 @@ import Web.Scim.Schema.Schema qualified as Scim import Web.Scim.Schema.User qualified as Scim import Web.Scim.Schema.User qualified as Scim.User import Wire.API.Locale +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.Team.Role (Role) import Wire.API.User.EmailAddress (EmailAddress, fromEmail) import Wire.API.User.Profile as BT @@ -114,7 +117,11 @@ userSchemas = -- -- For SCIM authentication and token handling logic, see "Spar.Scim.Auth". newtype ScimToken = ScimToken {fromScimToken :: Text} - deriving (Eq, Ord, Show, FromJSON, ToJSON, FromByteString, ToByteString) + deriving (Eq, Ord, Show, FromByteString, ToByteString, Arbitrary) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema ScimToken) + +instance ToSchema ScimToken where + schema = ScimToken <$> fromScimToken .= schema newtype ScimTokenHash = ScimTokenHash {fromScimTokenHash :: Text} deriving (Eq, Show) @@ -147,9 +154,13 @@ data ScimTokenInfo = ScimTokenInfo stiIdP :: !(Maybe SAML.IdPId), -- | Free-form token description, can be set -- by the token creator as a mental aid - stiDescr :: !Text + stiDescr :: !Text, + -- | Name for the token, if not set by the user, the name will be equal to the token ID + stiName :: !Text } - deriving (Eq, Show) + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform ScimTokenInfo) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema ScimTokenInfo) instance FromHttpApiData ScimToken where parseHeader h = ScimToken <$> parseHeaderWithPrefix "Bearer " h @@ -159,29 +170,44 @@ instance ToHttpApiData ScimToken where toHeader (ScimToken s) = "Bearer " <> encodeUtf8 s toQueryParam (ScimToken s) = toQueryParam s -instance FromJSON ScimTokenInfo where - parseJSON = A.withObject "ScimTokenInfo" $ \o -> do - stiTeam <- o A..: "team" - stiId <- o A..: "id" - stiCreatedAt <- o A..: "created_at" - stiIdP <- o A..:? "idp" - stiDescr <- o A..: "description" - pure ScimTokenInfo {..} - -instance ToJSON ScimTokenInfo where - toJSON s = - A.object $ - "team" - A..= stiTeam s - # "id" - A..= stiId s - # "created_at" - A..= stiCreatedAt s - # "idp" - A..= stiIdP s - # "description" - A..= stiDescr s - # [] +instance ToSchema ScimTokenInfo where + schema = + object "ScimTokenInfo" $ + ScimTokenInfo + <$> (.stiTeam) .= field "team" schema + <*> (.stiId) .= field "id" schema + <*> (.stiCreatedAt) .= field "created_at" utcTimeSchema + <*> (fmap SAML.fromIdPId . (.stiIdP)) .= (SAML.IdPId <$$> maybe_ (optField "idp" uuidSchema)) + <*> (.stiDescr) .= field "description" schema + <*> (.stiName) .= field "name" schema + +-- | Metadata that we store about each token. +data ScimTokenInfoV6 = ScimTokenInfoV6 + { -- | Which team can be managed with the token + stiTeam :: !TeamId, + -- | Token ID, can be used to eg. delete the token + stiId :: !ScimTokenId, + -- | Time of token creation + stiCreatedAt :: !UTCTime, + -- | IdP that created users will "belong" to + stiIdP :: !(Maybe SAML.IdPId), + -- | Free-form token description, can be set + -- by the token creator as a mental aid + stiDescr :: !Text + } + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform ScimTokenInfoV6) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema ScimTokenInfoV6) + +instance ToSchema ScimTokenInfoV6 where + schema = + object "ScimTokenInfoV6" $ + ScimTokenInfoV6 + <$> (.stiTeam) .= field "team" schema + <*> (.stiId) .= field "id" schema + <*> (.stiCreatedAt) .= field "created_at" utcTimeSchema + <*> (fmap SAML.fromIdPId . (.stiIdP)) .= (SAML.IdPId <$$> maybe_ (optField "idp" uuidSchema)) + <*> (.stiDescr) .= field "description" schema ---------------------------------------------------------------------------- -- @hscim@ extensions and wrappers @@ -392,51 +418,63 @@ makeLenses ''ValidScimId -- | Type used for request parameters to 'APIScimTokenCreate'. data CreateScimToken = CreateScimToken { -- | Token description (as memory aid for whoever is creating the token) - createScimTokenDescr :: !Text, + description :: !Text, -- | User password, which we ask for because creating a token is a "powerful" operation - createScimTokenPassword :: !(Maybe PlainTextPassword6), - -- | User code (sent by email), for 2nd factor to 'createScimTokenPassword' - createScimTokenCode :: !(Maybe Code.Value) + password :: !(Maybe PlainTextPassword6), + -- | User code (sent by email), for 2nd factor to 'password' + verificationCode :: !(Maybe Code.Value), + -- | Optional name for the token + name :: Maybe Text } deriving (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform CreateScimToken) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema CreateScimToken) -instance A.FromJSON CreateScimToken where - parseJSON = A.withObject "CreateScimToken" $ \o -> do - createScimTokenDescr <- o A..: "description" - createScimTokenPassword <- o A..:? "password" - createScimTokenCode <- o A..:? "verification_code" - pure CreateScimToken {..} - --- Used for integration tests -instance A.ToJSON CreateScimToken where - toJSON CreateScimToken {..} = - A.object - [ "description" A..= createScimTokenDescr, - "password" A..= createScimTokenPassword, - "verification_code" A..= createScimTokenCode - ] +createScimTokenSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc CreateScimToken +createScimTokenSchema v = + object ("CreateScimToken" <> foldMap (Text.toUpper . versionText) v) $ + CreateScimToken + <$> (.description) .= field "description" schema + <*> password .= optField "password" (maybeWithDefault A.Null schema) + <*> verificationCode .= optField "verification_code" (maybeWithDefault A.Null schema) + <*> (if isJust v then const Nothing else (.name)) .= maybe_ (optField "name" schema) + +instance ToSchema CreateScimToken where + schema = createScimTokenSchema Nothing + +instance ToSchema (Versioned 'V6 CreateScimToken) where + schema = Versioned <$> unVersioned .= createScimTokenSchema (Just V6) -- | Type used for the response of 'APIScimTokenCreate'. data CreateScimTokenResponse = CreateScimTokenResponse - { createScimTokenResponseToken :: ScimToken, - createScimTokenResponseInfo :: ScimTokenInfo + { token :: ScimToken, + info :: ScimTokenInfo } - deriving (Eq, Show) + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform CreateScimTokenResponse) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema CreateScimTokenResponse) --- Used for integration tests -instance A.FromJSON CreateScimTokenResponse where - parseJSON = A.withObject "CreateScimTokenResponse" $ \o -> do - createScimTokenResponseToken <- o A..: "token" - createScimTokenResponseInfo <- o A..: "info" - pure CreateScimTokenResponse {..} +instance ToSchema CreateScimTokenResponse where + schema = + object "CreateScimTokenResponse" $ + CreateScimTokenResponse + <$> (.token) .= field "token" schema + <*> (.info) .= field "info" schema + +data CreateScimTokenResponseV6 = CreateScimTokenResponseV6 + { token :: ScimToken, + info :: ScimTokenInfoV6 + } + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform CreateScimTokenResponseV6) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema CreateScimTokenResponseV6) -instance A.ToJSON CreateScimTokenResponse where - toJSON CreateScimTokenResponse {..} = - A.object - [ "token" A..= createScimTokenResponseToken, - "info" A..= createScimTokenResponseInfo - ] +instance ToSchema CreateScimTokenResponseV6 where + schema = + object "CreateScimTokenResponseV6" $ + CreateScimTokenResponseV6 + <$> (.token) .= field "token" schema + <*> (.info) .= field "info" schema -- | Type used for responses of endpoints that return a list of SCIM tokens. -- Wrapped into an object to allow extensibility later on. @@ -446,84 +484,23 @@ data ScimTokenList = ScimTokenList { scimTokenListTokens :: [ScimTokenInfo] } deriving (Eq, Show) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema ScimTokenList) -instance A.FromJSON ScimTokenList where - parseJSON = A.withObject "ScimTokenList" $ \o -> do - scimTokenListTokens <- o A..: "tokens" - pure ScimTokenList {..} - -instance A.ToJSON ScimTokenList where - toJSON ScimTokenList {..} = - A.object - [ "tokens" A..= scimTokenListTokens - ] - --- Swagger - -instance ToParamSchema ScimToken where - toParamSchema _ = toParamSchema (Proxy @Text) - -instance ToSchema ScimToken where - declareNamedSchema _ = - declareNamedSchema (Proxy @Text) - & mapped . schema . description ?~ "Authentication token" +instance ToSchema ScimTokenList where + schema = object "ScimTokenList" $ ScimTokenList <$> (.scimTokenListTokens) .= field "tokens" (array schema) -instance ToSchema ScimTokenInfo where - declareNamedSchema _ = do - teamSchema <- declareSchemaRef (Proxy @TeamId) - idSchema <- declareSchemaRef (Proxy @ScimTokenId) - createdAtSchema <- declareSchemaRef (Proxy @UTCTime) - idpSchema <- declareSchemaRef (Proxy @SAML.IdPId) - descrSchema <- declareSchemaRef (Proxy @Text) - pure $ - NamedSchema (Just "ScimTokenInfo") $ - mempty - & type_ ?~ OpenApiObject - & properties - .~ [ ("team", teamSchema), - ("id", idSchema), - ("created_at", createdAtSchema), - ("idp", idpSchema), - ("description", descrSchema) - ] - & required .~ ["team", "id", "created_at", "description"] +data ScimTokenListV6 = ScimTokenListV6 + { scimTokenListTokens :: [ScimTokenInfoV6] + } + deriving (Eq, Show) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema ScimTokenListV6) -instance ToSchema CreateScimToken where - declareNamedSchema _ = do - textSchema <- declareSchemaRef (Proxy @Text) - pure $ - NamedSchema (Just "CreateScimToken") $ - mempty - & type_ ?~ OpenApiObject - & properties - .~ [ ("description", textSchema), - ("password", textSchema), - ("verification_code", textSchema) - ] - & required .~ ["description"] +instance ToSchema ScimTokenListV6 where + schema = object "ScimTokenListV6" $ ScimTokenListV6 <$> (.scimTokenListTokens) .= field "tokens" (array schema) -instance ToSchema CreateScimTokenResponse where - declareNamedSchema _ = do - tokenSchema <- declareSchemaRef (Proxy @ScimToken) - infoSchema <- declareSchemaRef (Proxy @ScimTokenInfo) - pure $ - NamedSchema (Just "CreateScimTokenResponse") $ - mempty - & type_ ?~ OpenApiObject - & properties - .~ [ ("token", tokenSchema), - ("info", infoSchema) - ] - & required .~ ["token", "info"] +newtype ScimTokenName = ScimTokenName {fromScimTokenName :: Text} + deriving (Eq, Show) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema.Schema ScimTokenName) -instance ToSchema ScimTokenList where - declareNamedSchema _ = do - infoListSchema <- declareSchemaRef (Proxy @[ScimTokenInfo]) - pure $ - NamedSchema (Just "ScimTokenList") $ - mempty - & type_ ?~ OpenApiObject - & properties - .~ [ ("tokens", infoListSchema) - ] - & required .~ ["tokens"] +instance ToSchema ScimTokenName where + schema = object "ScimTokenName" $ ScimTokenName <$> fromScimTokenName .= field "name" schema diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs index e57d209f02d..3a898d764ce 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs @@ -32,6 +32,7 @@ import Test.Wire.API.Golden.Manual.ConversationRemoveMembers import Test.Wire.API.Golden.Manual.ConversationsResponse import Test.Wire.API.Golden.Manual.CreateGroupConversation import Test.Wire.API.Golden.Manual.CreateScimToken +import Test.Wire.API.Golden.Manual.CreateScimTokenResponse import Test.Wire.API.Golden.Manual.FeatureConfigEvent import Test.Wire.API.Golden.Manual.FederationDomainConfig import Test.Wire.API.Golden.Manual.FederationRestriction @@ -153,6 +154,10 @@ tests = (testObject_CreateScimToken_3, "testObject_CreateScimToken_3.json"), (testObject_CreateScimToken_4, "testObject_CreateScimToken_4.json") ], + testGroup "CreateScimTokenResponse" $ + testObjects + [ (testObject_CreateScimTokenResponse_1, "testObject_CreateScimTokenResponse_1.json") + ], testGroup "Contact" $ testObjects [ (testObject_Contact_1, "testObject_Contact_1.json"), diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimToken.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimToken.hs index 51c9bd8ecad..e2c32ffcf55 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimToken.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimToken.hs @@ -21,7 +21,7 @@ import Data.Code import Data.Misc (plainTextPassword6Unsafe) import Data.Range (unsafeRange) import Data.Text.Ascii (AsciiChars (validate)) -import Imports (Maybe (Just, Nothing), fromRight, undefined) +import Imports import Wire.API.User.Scim (CreateScimToken (..)) testObject_CreateScimToken_1 :: CreateScimToken @@ -30,6 +30,7 @@ testObject_CreateScimToken_1 = "description" (Just (plainTextPassword6Unsafe "very-geheim")) (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "123456"))})) + Nothing testObject_CreateScimToken_2 :: CreateScimToken testObject_CreateScimToken_2 = @@ -37,6 +38,7 @@ testObject_CreateScimToken_2 = "description2" (Just (plainTextPassword6Unsafe "secret")) Nothing + Nothing testObject_CreateScimToken_3 :: CreateScimToken testObject_CreateScimToken_3 = @@ -44,6 +46,7 @@ testObject_CreateScimToken_3 = "description3" Nothing (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "654321"))})) + Nothing testObject_CreateScimToken_4 :: CreateScimToken testObject_CreateScimToken_4 = @@ -51,3 +54,4 @@ testObject_CreateScimToken_4 = "description4" Nothing Nothing + (Just "scim connection name") diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimTokenResponse.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimTokenResponse.hs new file mode 100644 index 00000000000..799a9fb775b --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CreateScimTokenResponse.hs @@ -0,0 +1,38 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Manual.CreateScimTokenResponse where + +import Data.Id (Id (Id)) +import Data.Time (Day (ModifiedJulianDay)) +import Data.Time.Clock (UTCTime (UTCTime, utctDay, utctDayTime)) +import Data.UUID qualified as UUID +import Imports +import Wire.API.User.Scim + +testObject_CreateScimTokenResponse_1 :: CreateScimTokenResponse +testObject_CreateScimTokenResponse_1 = + CreateScimTokenResponse + (ScimToken "token") + ( ScimTokenInfo + (Id (fromJust (UUID.fromString "2853751e-9fb6-4425-b1bd-bd8aa2640c69"))) + (Id (fromJust (UUID.fromString "e25faea1-ee2d-4fd8-bf25-e6748d392b23"))) + (UTCTime {utctDay = ModifiedJulianDay 60605, utctDayTime = 65090}) + Nothing + "description" + "token name" + ) diff --git a/libs/wire-api/test/golden/testObject_CreateScimTokenResponse_1.json b/libs/wire-api/test/golden/testObject_CreateScimTokenResponse_1.json new file mode 100644 index 00000000000..3896abc8201 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_CreateScimTokenResponse_1.json @@ -0,0 +1,10 @@ +{ + "info": { + "created_at": "2024-10-22T18:04:50Z", + "description": "description", + "id": "e25faea1-ee2d-4fd8-bf25-e6748d392b23", + "team": "2853751e-9fb6-4425-b1bd-bd8aa2640c69", + "name": "token name" + }, + "token": "token" +} diff --git a/libs/wire-api/test/golden/testObject_CreateScimToken_4.json b/libs/wire-api/test/golden/testObject_CreateScimToken_4.json index a79a8f35565..cd71c759b31 100644 --- a/libs/wire-api/test/golden/testObject_CreateScimToken_4.json +++ b/libs/wire-api/test/golden/testObject_CreateScimToken_4.json @@ -1,5 +1,6 @@ { "description": "description4", "password": null, - "verification_code": null + "verification_code": null, + "name": "scim connection name" } diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs index 65be7b6ef80..ee312a10edf 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs @@ -203,6 +203,7 @@ tests = testRoundTrip @Push.Token.PushToken, testRoundTrip @Push.Token.PushTokenList, testRoundTrip @Scim.CreateScimToken, + testRoundTrip @Scim.CreateScimTokenResponse, testRoundTrip @SystemSettings.SystemSettings, testRoundTrip @SystemSettings.SystemSettingsPublic, testRoundTrip @SystemSettings.SystemSettingsInternal, diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index f5eac2bf6d2..d7835a6419b 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -588,6 +588,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.ConvIdsPage Test.Wire.API.Golden.Manual.CreateGroupConversation Test.Wire.API.Golden.Manual.CreateScimToken + Test.Wire.API.Golden.Manual.CreateScimTokenResponse Test.Wire.API.Golden.Manual.FeatureConfigEvent Test.Wire.API.Golden.Manual.FederationDomainConfig Test.Wire.API.Golden.Manual.FederationRestriction diff --git a/services/brig/test/integration/API/UserPendingActivation.hs b/services/brig/test/integration/API/UserPendingActivation.hs index c5a95445519..b82eb251957 100644 --- a/services/brig/test/integration/API/UserPendingActivation.hs +++ b/services/brig/test/integration/API/UserPendingActivation.hs @@ -104,9 +104,10 @@ createScimToken spar' owner = do CreateScimTokenResponse tok _ <- createToken spar' owner $ CreateScimToken - { createScimTokenDescr = "testCreateToken", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testCreateToken", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } pure tok diff --git a/services/spar/default.nix b/services/spar/default.nix index 8e5b8b51e4f..e6424e6e32b 100644 --- a/services/spar/default.nix +++ b/services/spar/default.nix @@ -174,6 +174,7 @@ mkDerivation { lens-aeson MonadRandom mtl + network-uri optparse-applicative polysemy polysemy-plugin diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 2435d71165b..a9b452682e4 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -40,6 +40,7 @@ library Spar.Schema.V16 Spar.Schema.V17 Spar.Schema.V18 + Spar.Schema.V19 Spar.Schema.V2 Spar.Schema.V3 Spar.Schema.V4 @@ -368,6 +369,7 @@ executable spar-integration , lens-aeson , MonadRandom , mtl + , network-uri , optparse-applicative , polysemy , polysemy-plugin diff --git a/services/spar/src/Spar/Schema/Run.hs b/services/spar/src/Spar/Schema/Run.hs index ac273fb83c4..e3f35f9ba2e 100644 --- a/services/spar/src/Spar/Schema/Run.hs +++ b/services/spar/src/Spar/Schema/Run.hs @@ -32,6 +32,7 @@ import qualified Spar.Schema.V15 as V15 import qualified Spar.Schema.V16 as V16 import qualified Spar.Schema.V17 as V17 import qualified Spar.Schema.V18 as V18 +import qualified Spar.Schema.V19 as V19 import qualified Spar.Schema.V2 as V2 import qualified Spar.Schema.V3 as V3 import qualified Spar.Schema.V4 as V4 @@ -78,7 +79,8 @@ migrations = V15.migration, V16.migration, V17.migration, - V18.migration + V18.migration, + V19.migration -- TODO: Add a migration that removes unused fields -- (we don't want to risk running a migration which would -- effectively break the currently deployed spar service) diff --git a/services/spar/src/Spar/Schema/V19.hs b/services/spar/src/Spar/Schema/V19.hs new file mode 100644 index 00000000000..6c55b7950c1 --- /dev/null +++ b/services/spar/src/Spar/Schema/V19.hs @@ -0,0 +1,36 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Spar.Schema.V19 + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = Migration 19 "Add name column to scim token info" $ do + schema' + [r| + ALTER TABLE team_provisioning_by_team ADD (name text); + |] + schema' + [r| + ALTER TABLE team_provisioning_by_token ADD (name text); + |] diff --git a/services/spar/src/Spar/Scim/Auth.hs b/services/spar/src/Spar/Scim/Auth.hs index 45d34e667af..5bad5826054 100644 --- a/services/spar/src/Spar/Scim/Auth.hs +++ b/services/spar/src/Spar/Scim/Auth.hs @@ -37,7 +37,7 @@ where import Control.Lens hiding (Strict, (.=)) import qualified Data.ByteString.Base64 as ES -import Data.Id (ScimTokenId, UserId) +import Data.Id import qualified Data.Text.Encoding as T import Data.Text.Encoding.Error import Imports @@ -98,10 +98,54 @@ apiScimToken :: ) => ServerT APIScimToken (Sem r) apiScimToken = - Named @"auth-tokens-create" createScimToken + Named @"auth-tokens-create@v6" createScimTokenV6 + :<|> Named @"auth-tokens-create" createScimToken + :<|> Named @"auth-tokens-put-name" updateScimTokenName :<|> Named @"auth-tokens-delete" deleteScimToken + :<|> Named @"auth-tokens-list@v6" listScimTokensV6 :<|> Named @"auth-tokens-list" listScimTokens +updateScimTokenName :: + ( Member BrigAccess r, + Member ScimTokenStore r, + Member (Error E.SparError) r, + Member GalleyAccess r + ) => + UserId -> + ScimTokenId -> + ScimTokenName -> + Sem r () +updateScimTokenName lusr tokenId name = do + teamid <- Intra.Brig.authorizeScimTokenManagement (Just lusr) + ScimTokenStore.updateName teamid tokenId name.fromScimTokenName + +-- | > docs/reference/provisioning/scim-token.md {#RefScimTokenCreate} +-- +-- Create a token for user's team. +createScimTokenV6 :: + forall r. + ( Member Random r, + Member (Input Opts) r, + Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member IdPConfigStore r, + Member Now r, + Member (Error E.SparError) r + ) => + -- | Who is trying to create a token + Maybe UserId -> + -- | Request body + CreateScimToken -> + Sem r CreateScimTokenResponseV6 +createScimTokenV6 zusr req = responseToV6 <$> createScimToken zusr req + where + responseToV6 :: CreateScimTokenResponse -> CreateScimTokenResponseV6 + responseToV6 (CreateScimTokenResponse token info) = CreateScimTokenResponseV6 token (infoToV6 info) + + infoToV6 :: ScimTokenInfo -> ScimTokenInfoV6 + infoToV6 ScimTokenInfo {..} = ScimTokenInfoV6 {..} + -- | > docs/reference/provisioning/scim-token.md {#RefScimTokenCreate} -- -- Create a token for user's team. @@ -122,9 +166,8 @@ createScimToken :: CreateScimToken -> Sem r CreateScimTokenResponse createScimToken zusr Api.CreateScimToken {..} = do - let descr = createScimTokenDescr teamid <- Intra.Brig.authorizeScimTokenManagement zusr - BrigAccess.ensureReAuthorised zusr createScimTokenPassword createScimTokenCode (Just User.CreateScimToken) + BrigAccess.ensureReAuthorised zusr password verificationCode (Just User.CreateScimToken) tokenNumber <- length <$> ScimTokenStore.lookupByTeam teamid maxTokens <- inputs maxScimTokens unless (tokenNumber < maxTokens) $ @@ -148,7 +191,8 @@ createScimToken zusr Api.CreateScimToken {..} = do stiTeam = teamid, stiCreatedAt = now, stiIdP = midpid, - stiDescr = descr + stiDescr = description, + stiName = fromMaybe (idToText tokenid) name } ScimTokenStore.insert token info pure $ CreateScimTokenResponse token info @@ -179,6 +223,23 @@ deleteScimToken zusr tokenid = do ScimTokenStore.delete teamid tokenid pure NoContent +listScimTokensV6 :: + ( Member GalleyAccess r, + Member BrigAccess r, + Member ScimTokenStore r, + Member (Error E.SparError) r + ) => + -- | Who is trying to list tokens + Maybe UserId -> + Sem r ScimTokenListV6 +listScimTokensV6 zusr = toV6 <$> listScimTokens zusr + where + toV6 :: ScimTokenList -> ScimTokenListV6 + toV6 (ScimTokenList tokens) = ScimTokenListV6 $ map infoToV6 tokens + + infoToV6 :: ScimTokenInfo -> ScimTokenInfoV6 + infoToV6 ScimTokenInfo {..} = ScimTokenInfoV6 {..} + -- | > docs/reference/provisioning/scim-token.md {#RefScimTokenList} -- -- List all tokens belonging to user's team. Tokens themselves are not available, only diff --git a/services/spar/src/Spar/Sem/ScimTokenStore.hs b/services/spar/src/Spar/Sem/ScimTokenStore.hs index eb4ec41735d..03014de6974 100644 --- a/services/spar/src/Spar/Sem/ScimTokenStore.hs +++ b/services/spar/src/Spar/Sem/ScimTokenStore.hs @@ -22,13 +22,14 @@ module Spar.Sem.ScimTokenStore insert, lookup, lookupByTeam, + updateName, delete, deleteByTeam, ) where import Data.Id -import Imports (Maybe) +import Imports hiding (lookup) import Polysemy import Wire.API.User.Scim @@ -36,6 +37,7 @@ data ScimTokenStore m a where Insert :: ScimToken -> ScimTokenInfo -> ScimTokenStore m () Lookup :: ScimToken -> ScimTokenStore m (Maybe ScimTokenInfo) LookupByTeam :: TeamId -> ScimTokenStore m [ScimTokenInfo] + UpdateName :: TeamId -> ScimTokenId -> Text -> ScimTokenStore m () Delete :: TeamId -> ScimTokenId -> ScimTokenStore m () DeleteByTeam :: TeamId -> ScimTokenStore m () diff --git a/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs b/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs index 6f56b34e77c..70dc4e223d0 100644 --- a/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs @@ -48,8 +48,9 @@ scimTokenStoreToCassandra = Insert st sti -> insertScimToken st sti Lookup st -> lookupScimToken st LookupByTeam tid -> getScimTokens tid - Delete tid ur -> deleteScimToken tid ur - DeleteByTeam tid -> deleteTeamScimTokens tid + UpdateName team token name -> updateScimTokenName team token name + Delete team token -> deleteScimToken team token + DeleteByTeam team -> deleteTeamScimTokens team ---------------------------------------------------------------------- -- SCIM auth @@ -67,25 +68,25 @@ insertScimToken token ScimTokenInfo {..} = retry x5 . batch $ do setType BatchLogged setConsistency LocalQuorum let tokenHash = hashScimToken token - addPrepQuery insByToken (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr) - addPrepQuery insByTeam (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr) + addPrepQuery insByToken (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr, Just stiName) + addPrepQuery insByTeam (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr, Just stiName) insByToken, insByTeam :: PrepQuery W ScimTokenRow () insByToken = [r| INSERT INTO team_provisioning_by_token - (token_, team, id, created_at, idp, descr) - VALUES (?, ?, ?, ?, ?, ?) + (token_, team, id, created_at, idp, descr, name) + VALUES (?, ?, ?, ?, ?, ?, ?) |] insByTeam = [r| INSERT INTO team_provisioning_by_team - (token_, team, id, created_at, idp, descr) - VALUES (?, ?, ?, ?, ?, ?) + (token_, team, id, created_at, idp, descr, name) + VALUES (?, ?, ?, ?, ?, ?, ?) |] scimTokenLookupKey :: ScimTokenRow -> ScimTokenLookupKey -scimTokenLookupKey (key, _, _, _, _, _) = key +scimTokenLookupKey (key, _, _, _, _, _, _) = key -- | Check whether a token exists and if yes, what team and IdP are -- associated with it. @@ -110,7 +111,7 @@ lookupScimToken token = do sel :: PrepQuery R (ScimTokenHash, ScimToken) ScimTokenRow sel = [r| - SELECT token_, team, id, created_at, idp, descr + SELECT token_, team, id, created_at, idp, descr, name FROM team_provisioning_by_token WHERE token_ in (?, ?) |] @@ -130,9 +131,9 @@ connvertPlaintextToken token ScimTokenInfo {..} = retry x5 . batch $ do setConsistency LocalQuorum let tokenHash = hashScimToken token -- enter by new lookup key - addPrepQuery insByToken (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr) + addPrepQuery insByToken (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr, Just stiName) -- update info table - addPrepQuery insByTeam (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr) + addPrepQuery insByTeam (ScimTokenLookupKeyHashed tokenHash, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr, Just stiName) -- remove old lookup key addPrepQuery delByTokenLookup (Identity (ScimTokenLookupKeyPlaintext token)) @@ -145,12 +146,12 @@ getScimTokens team = do -- We don't need pagination here because the limit should be pretty low -- (e.g. 16). If the limit grows, we might have to introduce pagination. rows <- retry x1 . query sel $ params LocalQuorum (Identity team) - pure $ sortOn stiCreatedAt $ map fromScimTokenRow rows + pure $ sortOn (.stiCreatedAt) $ map fromScimTokenRow rows where sel :: PrepQuery R (Identity TeamId) ScimTokenRow sel = [r| - SELECT token_, team, id, created_at, idp, descr + SELECT token_, team, id, created_at, idp, descr, name FROM team_provisioning_by_team WHERE team = ? |] @@ -168,13 +169,13 @@ deleteScimToken team tokenid = do addPrepQuery delById (team, tokenid) for_ mbToken $ \(Identity key) -> addPrepQuery delByTokenLookup (Identity key) - where - selById :: PrepQuery R (TeamId, ScimTokenId) (Identity ScimTokenLookupKey) - selById = - [r| - SELECT token_ FROM team_provisioning_by_team - WHERE team = ? AND id = ? - |] + +selById :: PrepQuery R (TeamId, ScimTokenId) (Identity ScimTokenLookupKey) +selById = + [r| + SELECT token_ FROM team_provisioning_by_team + WHERE team = ? AND id = ? +|] delById :: PrepQuery W (TeamId, ScimTokenId) () delById = @@ -208,8 +209,41 @@ deleteTeamScimTokens team = do delByTeam :: PrepQuery W (Identity TeamId) () delByTeam = "DELETE FROM team_provisioning_by_team WHERE team = ?" -type ScimTokenRow = (ScimTokenLookupKey, TeamId, ScimTokenId, UTCTime, Maybe SAML.IdPId, Text) +updateScimTokenName :: (HasCallStack, MonadClient m) => TeamId -> ScimTokenId -> Text -> m () +updateScimTokenName team tokenid name = do + mbToken <- retry x1 . query1 selById $ params LocalQuorum (team, tokenid) + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery updateNameById (name, team, tokenid) + for_ mbToken $ \(Identity key) -> + addPrepQuery updateNameByTokenLookup (name, key) + where + updateNameById :: PrepQuery W (Text, TeamId, ScimTokenId) () + updateNameById = + [r| + UPDATE team_provisioning_by_team + SET name = ? + WHERE team = ? AND id = ? + |] + + updateNameByTokenLookup :: PrepQuery W (Text, ScimTokenLookupKey) () + updateNameByTokenLookup = + [r| + UPDATE team_provisioning_by_token + SET name = ? + WHERE token_ = ? + |] + +type ScimTokenRow = (ScimTokenLookupKey, TeamId, ScimTokenId, UTCTime, Maybe SAML.IdPId, Text, Maybe Text) fromScimTokenRow :: ScimTokenRow -> ScimTokenInfo -fromScimTokenRow (_, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr) = - ScimTokenInfo {..} +fromScimTokenRow (_, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr, stiName) = + ScimTokenInfo + { stiId, + stiTeam, + stiCreatedAt, + stiIdP, + stiDescr, + stiName = fromMaybe (idToText stiId) stiName + } diff --git a/services/spar/src/Spar/Sem/ScimTokenStore/Mem.hs b/services/spar/src/Spar/Sem/ScimTokenStore/Mem.hs index 255d9a8e2ad..48b869fb0f0 100644 --- a/services/spar/src/Spar/Sem/ScimTokenStore/Mem.hs +++ b/services/spar/src/Spar/Sem/ScimTokenStore/Mem.hs @@ -36,6 +36,10 @@ scimTokenStoreToMem = (runState mempty .) $ reinterpret $ \case Insert st sti -> modify $ M.insert st sti Lookup st -> gets $ M.lookup st - LookupByTeam tid -> gets $ filter ((== tid) . stiTeam) . M.elems - Delete tid stid -> modify $ M.filter $ \sti -> not $ stiTeam sti == tid && stiId sti == stid - DeleteByTeam tid -> modify $ M.filter ((/= tid) . stiTeam) + LookupByTeam tid -> gets $ filter ((== tid) . (.stiTeam)) . M.elems + UpdateName tid stid name -> modify $ M.map $ \sti -> + if (.stiTeam) sti == tid && (.stiId) sti == stid + then sti {stiName = name} + else sti + Delete tid stid -> modify $ M.filter $ \sti -> not $ (.stiTeam) sti == tid && (.stiId) sti == stid + DeleteByTeam tid -> modify $ M.filter ((/= tid) . (.stiTeam)) diff --git a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs index 7d2b945b95f..eb285a5e61b 100644 --- a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs @@ -97,9 +97,10 @@ testCreateToken = do createToken owner CreateScimToken - { createScimTokenDescr = "testCreateToken", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testCreateToken", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } -- Try to do @GET /Users@ and check that it succeeds let fltr = filterBy "externalId" "67c196a0-cd0e-11ea-93c7-ef550ee48502" @@ -120,17 +121,17 @@ testCreateTokenWithVerificationCode = do user <- getUserBrig owner let email = fromMaybe undefined (userEmail =<< user) - let reqMissingCode = CreateScimToken "testCreateToken" (Just defPassword) Nothing + let reqMissingCode = CreateScimToken "testCreateToken" (Just defPassword) Nothing Nothing createTokenFailsWith owner reqMissingCode 403 "code-authentication-required" void $ requestVerificationCode (env ^. teBrig) email Public.CreateScimToken let wrongCode = Code.Value $ unsafeRange (fromRight undefined (validate "123456")) - let reqWrongCode = CreateScimToken "testCreateToken" (Just defPassword) (Just wrongCode) + let reqWrongCode = CreateScimToken "testCreateToken" (Just defPassword) (Just wrongCode) Nothing createTokenFailsWith owner reqWrongCode 403 "code-authentication-failed" void $ retryNUntil 6 ((==) 200 . statusCode) $ requestVerificationCode (env ^. teBrig) email Public.CreateScimToken code <- getVerificationCode (env ^. teBrig) owner Public.CreateScimToken - let reqWithCode = CreateScimToken "testCreateToken" (Just defPassword) (Just code) + let reqWithCode = CreateScimToken "testCreateToken" (Just defPassword) (Just code) Nothing CreateScimTokenResponse token _ <- createToken owner reqWithCode -- Try to do @GET /Users@ and check that it succeeds @@ -177,25 +178,28 @@ testTokenLimit = do createToken owner CreateScimToken - { createScimTokenDescr = "testTokenLimit / #1", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testTokenLimit / #1", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } _ <- createToken owner CreateScimToken - { createScimTokenDescr = "testTokenLimit / #2", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testTokenLimit / #2", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } -- Try to create the third token and see that it fails createToken_ owner CreateScimToken - { createScimTokenDescr = "testTokenLimit / #3", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testTokenLimit / #3", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } (env ^. teSpar) !!! checkErr 403 (Just "token-limit-reached") @@ -214,13 +218,13 @@ testNumIdPs = do SAML.SampleIdP metadata _ _ _ <- SAML.makeSampleIdPMetadata void $ call $ Util.callIdpCreate apiversion spar (Just owner) metadata - createToken owner (CreateScimToken "eins" (Just defPassword) Nothing) - >>= deleteToken owner . stiId . createScimTokenResponseInfo + createToken owner (CreateScimToken "eins" (Just defPassword) Nothing Nothing) + >>= deleteToken owner . (.stiId) . (.info) addSomeIdP - createToken owner (CreateScimToken "zwei" (Just defPassword) Nothing) - >>= deleteToken owner . stiId . createScimTokenResponseInfo + createToken owner (CreateScimToken "zwei" (Just defPassword) Nothing Nothing) + >>= deleteToken owner . (.stiId) . (.info) addSomeIdP - createToken_ owner (CreateScimToken "drei" (Just defPassword) Nothing) (env ^. teSpar) + createToken_ owner (CreateScimToken "drei" (Just defPassword) Nothing Nothing) (env ^. teSpar) !!! checkErr 400 (Just "more-than-one-idp") -- @SF.Provisioning @TSFI.RESTfulAPI @S2 @@ -244,9 +248,10 @@ testCreateTokenAuthorizesOnlyAdmins = do createToken_ uid CreateScimToken - { createScimTokenDescr = "testCreateToken", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testCreateToken", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } (env ^. teSpar) @@ -272,9 +277,10 @@ testCreateTokenRequiresPassword = do createToken_ owner CreateScimToken - { createScimTokenDescr = "testCreateTokenRequiresPassword", - createScimTokenPassword = Nothing, - createScimTokenCode = Nothing + { description = "testCreateTokenRequiresPassword", + password = Nothing, + verificationCode = Nothing, + name = Nothing } (env ^. teSpar) !!! checkErr 403 (Just "access-denied") @@ -282,9 +288,10 @@ testCreateTokenRequiresPassword = do createToken_ owner CreateScimToken - { createScimTokenDescr = "testCreateTokenRequiresPassword", - createScimTokenPassword = Just (plainTextPassword6Unsafe "wrong password"), - createScimTokenCode = Nothing + { description = "testCreateTokenRequiresPassword", + password = Just (plainTextPassword6Unsafe "wrong password"), + verificationCode = Nothing, + name = Nothing } (env ^. teSpar) !!! checkErr 403 (Just "access-denied") @@ -309,22 +316,24 @@ testListTokens = do createToken owner CreateScimToken - { createScimTokenDescr = "testListTokens / #1", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testListTokens / #1", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } _ <- createToken owner CreateScimToken - { createScimTokenDescr = "testListTokens / #2", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testListTokens / #2", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } -- Check that the token is on the list - list <- scimTokenListTokens <$> listTokens owner + list <- (.scimTokenListTokens) <$> listTokens owner liftIO $ - map stiDescr list + map (.stiDescr) list `shouldBe` ["testListTokens / #1", "testListTokens / #2"] testPlaintextTokensAreConverted :: TestSpar () @@ -418,16 +427,17 @@ testDeletedTokensAreUnusable = do createToken owner CreateScimToken - { createScimTokenDescr = "testDeletedTokensAreUnusable", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testDeletedTokensAreUnusable", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } -- An operation with the token should succeed let fltr = filterBy "externalId" "67c196a0-cd0e-11ea-93c7-ef550ee48502" listUsers_ (Just token) (Just fltr) (env ^. teSpar) !!! const 200 === statusCode -- Delete the token and now the operation should fail - deleteToken owner (stiId tokenInfo) + deleteToken owner tokenInfo.stiId listUsers_ (Just token) Nothing (env ^. teSpar) !!! checkErr 401 Nothing @@ -443,14 +453,15 @@ testDeletedTokensAreUnlistable = do createToken owner CreateScimToken - { createScimTokenDescr = "testDeletedTokensAreUnlistable", - createScimTokenPassword = Just defPassword, - createScimTokenCode = Nothing + { description = "testDeletedTokensAreUnlistable", + password = Just defPassword, + verificationCode = Nothing, + name = Nothing } -- Delete the token - deleteToken owner (stiId tokenInfo) + deleteToken owner tokenInfo.stiId -- Check that the token is not on the list - list <- scimTokenListTokens <$> listTokens owner + list <- (.scimTokenListTokens) <$> listTokens owner liftIO $ list `shouldBe` [] ---------------------------------------------------------------------------- diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 74aacb800cb..6d92d56e0df 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -47,6 +47,7 @@ module Util.Core -- * HTTP call, endpointToReq, + mkVersionedRequest, -- * Other randomEmail, @@ -139,7 +140,7 @@ where import Bilge hiding (getCookie, host, port) -- we use Web.Cookie instead of the http-client type import qualified Bilge import Bilge.Assert (Assertions, (!!!), ()) @@ -196,6 +200,8 @@ import URI.ByteString as URI import Util.Options import Util.Types import qualified Web.Cookie as Web +import Web.HttpApiData +import Wire.API.Routes.Version import Wire.API.Team (Icon (..)) import qualified Wire.API.Team as Galley import Wire.API.Team.Feature @@ -259,9 +265,9 @@ mkEnv tstOpts opts = do mgr :: Manager <- newManager defaultManagerSettings sparCtxLogger <- Log.mkLogger (samlToLevel $ saml opts ^. SAML.cfgLogLevel) (logNetStrings opts) (logFormat opts) cql :: ClientState <- initCassandra opts sparCtxLogger - let brig = endpointToReq tstOpts.brig - galley = endpointToReq tstOpts.galley - spar = endpointToReq tstOpts.spar + let brig = mkVersionedRequest tstOpts.brig + galley = mkVersionedRequest tstOpts.galley + spar = mkVersionedRequest tstOpts.spar sparEnv = Spar.Env {..} wireIdPAPIVersion = WireIdPAPIV2 sparCtxOpts = opts @@ -565,17 +571,42 @@ nextUserRef = liftIO $ do (SAML.Issuer $ SAML.unsafeParseURI ("http://" <> tenant)) <$> nextSubject +-- FUTUREWORK: use an endpoint from latest API version getTeams :: (HasCallStack, MonadHttp m, MonadIO m) => UserId -> GalleyReq -> m Galley.TeamList getTeams u gly = do r <- get - ( gly + ( unversioned + . gly . paths ["teams"] . zAuthAccess u "conn" . expect2xx ) pure $ responseJsonUnsafe r +-- | Note: Apply this function last when composing (Request -> Request) functions +unversioned :: Request -> Request +unversioned r = + r + { HTTP.path = + maybe + (HTTP.path r) + (B8.pack "/" <>) + (removeVersionPrefix . removeSlash' $ HTTP.path r) + } + where + removeVersionPrefix :: ByteString -> Maybe ByteString + removeVersionPrefix bs = do + let (x, s) = B8.splitAt 1 bs + guard (x == B8.pack "v") + (_, s') <- B8.readInteger s + pure (B8.tail s') + + removeSlash' :: ByteString -> ByteString + removeSlash' s = case B8.uncons s of + Just ('/', s') -> s' + _ -> s + getTeamMemberIds :: (HasCallStack) => UserId -> TeamId -> TestSpar [UserId] getTeamMemberIds usr tid = (^. Team.userId) <$$> getTeamMembers usr tid @@ -668,6 +699,24 @@ zConn = header "Z-Connection" endpointToReq :: Endpoint -> (Bilge.Request -> Bilge.Request) endpointToReq ep = Bilge.host (cs ep.host) . Bilge.port ep.port +mkVersionedRequest :: Endpoint -> Request -> Request +mkVersionedRequest ep = maybeAddPrefix . endpointToReq ep + +maybeAddPrefix :: Request -> Request +maybeAddPrefix r = case pathSegments $ getUri r of + ("i" : _) -> r + ("api-internal" : _) -> r + _ -> addPrefix r + +addPrefix :: Request -> Request +addPrefix r = r {HTTP.path = toHeader latestVersion <> "/" <> removeSlash (HTTP.path r)} + where + removeSlash s = case B8.uncons s of + Just ('/', s') -> s' + _ -> s + latestVersion :: Version + latestVersion = maxBound + -- spar specifics shouldRespondWith :: diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index bf2b7ebe9ae..d95bde89583 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -114,7 +114,8 @@ registerScimToken teamid midpid = do stiId = scimTokenId, stiCreatedAt = now, stiIdP = midpid, - stiDescr = "test token" + stiDescr = "test token", + stiName = "test token" } pure tok @@ -626,7 +627,7 @@ class IsUser u where instance IsUser ValidScimUser where maybeUserId = Nothing maybeHandle = Just (Just <$> handle) - maybeName = Just (Just <$> name) + maybeName = Just (Just <$> (.name)) maybeTenant = Just (fmap SAML._uidTenant . veidUref . externalId) maybeSubject = Just (fmap SAML._uidSubject . veidUref . externalId) maybeScimExternalId = Just (runValidScimIdEither Intra.urefToExternalId (Just . fromEmail) . externalId) diff --git a/services/spar/test/Arbitrary.hs b/services/spar/test/Arbitrary.hs index bc7cc42d9c2..b9d3f0de56a 100644 --- a/services/spar/test/Arbitrary.hs +++ b/services/spar/test/Arbitrary.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TypeSynonymInstances #-} {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-redundant-constraints #-} @@ -45,26 +44,18 @@ instance Arbitrary IdPList where instance Arbitrary WireIdP where arbitrary = WireIdP <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary -deriving instance Arbitrary ScimToken - instance Arbitrary ScimTokenHash where arbitrary = hashScimToken <$> arbitrary -instance Arbitrary ScimTokenInfo where - arbitrary = - ScimTokenInfo - <$> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - -instance Arbitrary CreateScimTokenResponse where - arbitrary = CreateScimTokenResponse <$> arbitrary <*> arbitrary - instance Arbitrary ScimTokenList where arbitrary = ScimTokenList <$> arbitrary +instance Arbitrary ScimTokenListV6 where + arbitrary = ScimTokenListV6 <$> arbitrary + +instance Arbitrary ScimTokenName where + arbitrary = ScimTokenName <$> arbitrary + instance Arbitrary NoContent where arbitrary = pure NoContent diff --git a/services/spar/test/Test/Spar/Scim/UserSpec.hs b/services/spar/test/Test/Spar/Scim/UserSpec.hs index 9d759d600ac..09d09eee3ad 100644 --- a/services/spar/test/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test/Test/Spar/Scim/UserSpec.hs @@ -79,7 +79,7 @@ deleteUserAndAssertDeletionInSpar :: ScimTokenInfo -> Sem r (Either ScimError ()) deleteUserAndAssertDeletionInSpar acc tokenInfo = do - let tid = stiTeam tokenInfo + let tid = tokenInfo.stiTeam email = (fromJust . emailIdentity . fromJust . userIdentity) acc uid = userId acc ScimExternalIdStore.insert tid (fromEmail email) uid @@ -150,5 +150,5 @@ someActiveUser tokenInfo = do userAssets = [], userHandle = parseHandle "some-handle", userIdentity = (Just . EmailIdentity . fromJust . emailAddressText) "someone@wire.com", - userTeam = Just $ stiTeam tokenInfo + userTeam = Just $ tokenInfo.stiTeam } From 075ddca9d69b7637c13893b24f99493a6e5384d0 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 25 Oct 2024 15:16:50 +0200 Subject: [PATCH 03/27] WPB-11050 email templates for invitation of personal user to existing team (#4310) --- changelog.d/2-features/WPB-11050 | 1 + .../email/existing-invitation-subject.txt | 1 - .../en/team/email/existing-invitation.html | 183 ------------------ .../en/team/email/existing-invitation.txt | 25 --- services/brig/src/Brig/Team/Template.hs | 6 +- 5 files changed, 4 insertions(+), 212 deletions(-) create mode 100644 changelog.d/2-features/WPB-11050 delete mode 100644 services/brig/deb/opt/brig/templates/en/team/email/existing-invitation-subject.txt delete mode 100644 services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.html delete mode 100644 services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.txt diff --git a/changelog.d/2-features/WPB-11050 b/changelog.d/2-features/WPB-11050 new file mode 100644 index 00000000000..981cab205c2 --- /dev/null +++ b/changelog.d/2-features/WPB-11050 @@ -0,0 +1 @@ +Email template for inviting a personal user to a team added diff --git a/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation-subject.txt b/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation-subject.txt deleted file mode 100644 index 9fef363e407..00000000000 --- a/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation-subject.txt +++ /dev/null @@ -1 +0,0 @@ -You have been invited to join a team on ${brand} \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.html b/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.html deleted file mode 100644 index 2985716ea58..00000000000 --- a/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.html +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - You have been invited to join a team on ${brand} - - - - - - - - -
- - - - - - -
- - - - - - -
- - - - - - - -
- - - - - - -
-

-
-
- - - - - - -
-

${brand_label_url}

-
-
-
-
- - - - - - -
- - - - - - -
- - - - - - - -
-

Team invitation

-

${inviter} has invited you to join a team on ${brand}. Click the button below to accept the invitation.

- - - - - - -
 
-
- - - - - - -
- - - - - - -
Join team
-
-
- - - - - - -
 
-

If you can’t click the button, copy and paste this link to your browser:

-

${url}

-

If you have any questions, please contact us.

-

What is Wire?
Wire is the most secure collaboration platform. Work with your team and external partners wherever you are through messages, video conferencing and file sharing – always secured with end-to-end-encryption. Learn more.

-
-
-
- - - - - - - -
-
                                                           
- - - diff --git a/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.txt b/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.txt deleted file mode 100644 index 918c8fde767..00000000000 --- a/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.txt +++ /dev/null @@ -1,25 +0,0 @@ -[${brand_logo}] - -${brand_label_url} [${brand_url}] - -TEAM INVITATION -${inviter} has invited you to join a team on ${brand}. Click the button below to -accept the invitation. - -Join team [${url}]If you can’t click the button, copy and paste this link to -your browser: - -${url} - -If you have any questions, please contact us [${support}]. - -What is Wire? -Wire is the most secure collaboration platform. Work with your team and external -partners wherever you are through messages, video conferencing and file sharing -– always secured with end-to-end-encryption. Learn more [https://wire.com/]. - - --------------------------------------------------------------------------------- - -Privacy policy and terms of use [${legal}] · Report Misuse [${misuse}] -${copyright}. ALL RIGHTS RESERVED. \ No newline at end of file diff --git a/services/brig/src/Brig/Team/Template.hs b/services/brig/src/Brig/Team/Template.hs index c7072588515..a63cba25fb0 100644 --- a/services/brig/src/Brig/Team/Template.hs +++ b/services/brig/src/Brig/Team/Template.hs @@ -43,9 +43,9 @@ loadTeamTemplates o = readLocalesDir defLocale (templateDir gOptions) "team" $ \ <*> readText fp "email/sender.txt" ) <*> ( InvitationEmailTemplate tExistingUrl - <$> readTemplate fp "email/existing-invitation-subject.txt" - <*> readTemplate fp "email/existing-invitation.txt" - <*> readTemplate fp "email/existing-invitation.html" + <$> readTemplate fp "email/migration-subject.txt" + <*> readTemplate fp "email/migration.txt" + <*> readTemplate fp "email/migration.html" <*> pure (emailSender gOptions) <*> readText fp "email/sender.txt" ) From 6f77039021c7e04ac7ced922ea844999be022188 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Fri, 25 Oct 2024 17:15:38 +0200 Subject: [PATCH 04/27] Fix swagger (#4309) * Remove redundant copy of SpecializeToVersion type family. * Clearer error message. * Fix: show openapi docs for blocked versions. * Changelog. --- changelog.d/4-docs/fix-swagger | 1 + libs/wire-api/default.nix | 2 + .../Wire/API/Routes/SpecialiseToVersion.hs | 14 +++ libs/wire-api/src/Wire/API/Routes/Version.hs | 91 +------------------ .../src/Wire/API/Routes/Version/Wai.hs | 4 +- libs/wire-api/wire-api.cabal | 1 + services/brig/src/Brig/API/Public.hs | 2 +- services/brig/src/Brig/API/Public/Swagger.hs | 9 +- 8 files changed, 27 insertions(+), 97 deletions(-) create mode 100644 changelog.d/4-docs/fix-swagger diff --git a/changelog.d/4-docs/fix-swagger b/changelog.d/4-docs/fix-swagger new file mode 100644 index 00000000000..394aaf48d83 --- /dev/null +++ b/changelog.d/4-docs/fix-swagger @@ -0,0 +1 @@ +Fix: show openapi docs for blocked versions diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index b7a9e127622..6249b98c695 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -83,6 +83,7 @@ , servant-client-core , servant-conduit , servant-multipart +, servant-multipart-api , servant-openapi3 , servant-server , singletons @@ -188,6 +189,7 @@ mkDerivation { servant-client-core servant-conduit servant-multipart + servant-multipart-api servant-openapi3 servant-server singletons diff --git a/libs/wire-api/src/Wire/API/Routes/SpecialiseToVersion.hs b/libs/wire-api/src/Wire/API/Routes/SpecialiseToVersion.hs index dfa669ca138..215e5c78d29 100644 --- a/libs/wire-api/src/Wire/API/Routes/SpecialiseToVersion.hs +++ b/libs/wire-api/src/Wire/API/Routes/SpecialiseToVersion.hs @@ -21,7 +21,9 @@ module Wire.API.Routes.SpecialiseToVersion where import Data.Singletons.Base.TH import GHC.TypeLits import Servant +import Servant.API.Extended import Servant.API.Extended.RawM qualified as RawM +import Servant.Multipart.API import Wire.API.Deprecated import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named @@ -45,6 +47,10 @@ type instance SpecialiseToVersion v (Named n api) = Named n (SpecialiseToVersion v api) +type instance + SpecialiseToVersion v (NoContentVerb m) = + NoContentVerb m + type instance SpecialiseToVersion v (Capture' mod sym a :> api) = Capture' mod sym a :> SpecialiseToVersion v api @@ -92,3 +98,11 @@ type instance SpecialiseToVersion v EmptyAPI = EmptyAPI type instance SpecialiseToVersion v (api1 :<|> api2) = SpecialiseToVersion v api1 :<|> SpecialiseToVersion v api2 + +type instance + SpecialiseToVersion v (ReqBodyCustomError t l x :> api) = + ReqBodyCustomError t l x :> SpecialiseToVersion v api + +type instance + SpecialiseToVersion v (MultipartForm x b :> api) = + MultipartForm x b :> SpecialiseToVersion v api diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index 13ec55c42ea..92e095f360f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -44,8 +44,8 @@ module Wire.API.Routes.Version Until, From, - -- * Swagger instances - SpecialiseToVersion, + -- * Swagger + module Wire.API.Routes.SpecialiseToVersion, ) where @@ -65,15 +65,10 @@ import Data.Set qualified as Set import Data.Singletons.Base.TH import Data.Text qualified as Text import Data.Text.Encoding as Text -import GHC.TypeLits import Imports hiding ((\\)) import Servant -import Servant.API.Extended (ReqBodyCustomError) -import Servant.API.Extended.RawM qualified as RawM -import Servant.Multipart (MultipartForm) -import Wire.API.Deprecated -import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named hiding (unnamed) +import Wire.API.Routes.SpecialiseToVersion import Wire.API.VersionInfo import Wire.Arbitrary (Arbitrary, GenericUniform (GenericUniform)) @@ -253,84 +248,4 @@ expandVersionExp :: VersionExp -> Set Version expandVersionExp (VersionExpConst v) = Set.singleton v expandVersionExp VersionExpDevelopment = Set.fromList developmentVersions --- Version-aware swagger generation - $(promoteOrdInstances [''Version]) - -type family SpecialiseToVersion (v :: Version) api - -type instance - SpecialiseToVersion v (From w :> api) = - If (v < w) EmptyAPI (SpecialiseToVersion v api) - -type instance - SpecialiseToVersion v (Until w :> api) = - If (v < w) (SpecialiseToVersion v api) EmptyAPI - -type instance - SpecialiseToVersion v ((s :: Symbol) :> api) = - s :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (Named n api) = - Named n (SpecialiseToVersion v api) - -type instance - SpecialiseToVersion v (Capture' mod sym a :> api) = - Capture' mod sym a :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (Summary s :> api) = - Summary s :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (Deprecated :> api) = - Deprecated :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (Verb m s t r) = - Verb m s t r - -type instance - SpecialiseToVersion v (MultiVerb m t r x) = - MultiVerb m t r x - -type instance - SpecialiseToVersion v (NoContentVerb m) = - NoContentVerb m - -type instance SpecialiseToVersion v RawM.RawM = RawM.RawM - -type instance - SpecialiseToVersion v (ReqBody t x :> api) = - ReqBody t x :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (ReqBodyCustomError t l x :> api) = - ReqBodyCustomError t l x :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (MultipartForm x b :> api) = - MultipartForm x b :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (QueryParam' mods l x :> api) = - QueryParam' mods l x :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (Header' opts l x :> api) = - Header' opts l x :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (Description desc :> api) = - Description desc :> SpecialiseToVersion v api - -type instance - SpecialiseToVersion v (StreamBody' opts f t x :> api) = - StreamBody' opts f t x :> SpecialiseToVersion v api - -type instance SpecialiseToVersion v EmptyAPI = EmptyAPI - -type instance - SpecialiseToVersion v (api1 :<|> api2) = - SpecialiseToVersion v api1 :<|> SpecialiseToVersion v api2 diff --git a/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs b/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs index 0b48d00ad53..6174c6e515f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs @@ -74,8 +74,8 @@ looksLikeVersion version = case T.splitAt 1 version of (h, t) -> h == "v" && T.a -- | swagger-delivering end-points are not disableable: they should work for all versions. requestIsDisableable :: Request -> Bool requestIsDisableable (pathInfo -> path) = case path of - ["api", "swagger-ui"] -> False - ["api", "swagger.json"] -> False + ("api" : "swagger-ui" : _) -> False + ("api" : "swagger.json" : _) -> False _ -> True removeVersionHeader :: Request -> Request diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index d7835a6419b..4458828a3c7 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -324,6 +324,7 @@ library , servant-client-core , servant-conduit , servant-multipart + , servant-multipart-api , servant-openapi3 , servant-server , singletons diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index a576af85158..2cb50e9c856 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -263,7 +263,7 @@ internalEndpointsSwaggerDocsAPI :: PortNumber -> S.OpenApi -> Servant.Server (VersionedSwaggerDocsAPIBase service) -internalEndpointsSwaggerDocsAPI _ _ _ (Just _) = emptySwagger +internalEndpointsSwaggerDocsAPI _ _ _ (Just _) = emptySwagger "Internal APIs are not versioned!" internalEndpointsSwaggerDocsAPI service examplePort swagger Nothing = swaggerSchemaUIServer $ swagger diff --git a/services/brig/src/Brig/API/Public/Swagger.hs b/services/brig/src/Brig/API/Public/Swagger.hs index ebe1287c905..5cca138c03a 100644 --- a/services/brig/src/Brig/API/Public/Swagger.hs +++ b/services/brig/src/Brig/API/Public/Swagger.hs @@ -123,12 +123,9 @@ adjustSwaggerForFederationEndpoints service swagger = tag :: InsOrdSet.InsOrdHashSet S.TagName tag = InsOrdSet.singleton @S.TagName (T.pack service) -emptySwagger :: Servant.Server (ServiceSwaggerDocsAPIBase a) -emptySwagger = - swaggerSchemaUIServer $ - mempty @S.OpenApi - & S.info . S.description - ?~ "There is no Swagger documentation for this version. Please refer to v5 or later." +emptySwagger :: Text -> Servant.Server (ServiceSwaggerDocsAPIBase a) +emptySwagger msg = + swaggerSchemaUIServer $ mempty @S.OpenApi & S.info . S.description ?~ msg eventNotificationSchemas :: [S.Definitions S.Schema] eventNotificationSchemas = fst . (`S.runDeclare` mempty) <$> renderAll From 99acd4c6916ff968a68a62363eadf954eccac742 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Fri, 25 Oct 2024 17:28:32 +0200 Subject: [PATCH 05/27] [WPB-10314] validate swagger: add swagger linter to integration tests (so it'll run in CI). (#4302) * Add integration test for vacuum (swagger linter). * Clean up swagger lint integration test * Fix overlapping paths in API v7 * Update nginx paths * Fix tests in new integration suite * Fix brig integration tests * Fix galley integration tests * Add vacuum-go deriv. to integration image. * Haddocks. * missed one! * Partially openapi-lint internal routing tables. * ... another one? * Fix imports. * Simplify schema of iGetRichInfoMulti response * Fix overlapping internal brig endpoint * Add IDs to some internal brig endpoints * Remove unused legalhold API * fixup! Fix brig integration tests * Add compatibility middleware This should avoid temporary failures during deployment. * Add CHANGELOG entry * Fix endpoint path in brig integration tests --------- Co-authored-by: Paolo Capriotti --- Makefile | 8 +- changelog.d/4-docs/openapi-validation | 2 +- changelog.d/5-internal/openapi-validation | 1 + charts/nginz/values.yaml | 9 +- integration/test/API/Brig.hs | 2 +- integration/test/API/Galley.hs | 14 ++- integration/test/Test/Bot.hs | 1 - integration/test/Test/MLS/One2One.hs | 14 +-- integration/test/Test/Swagger.hs | 27 +++++ .../src/Wire/API/Routes/Internal/Brig.hs | 107 ++++++++++-------- .../src/Wire/API/Routes/Internal/Cargohold.hs | 4 +- .../src/Wire/API/Routes/Internal/Gundeck.hs | 19 ++-- .../src/Wire/API/Routes/Internal/LegalHold.hs | 45 -------- .../src/Wire/API/Routes/Internal/Spar.hs | 9 +- .../src/Wire/API/Routes/Public/Brig.hs | 34 +++++- .../src/Wire/API/Routes/Public/Brig/Bot.hs | 36 +++++- .../API/Routes/Public/Galley/Conversation.hs | 27 ++++- libs/wire-api/wire-api.cabal | 1 - nix/wire-server.nix | 1 + services/brig/src/Brig/API/Internal.hs | 25 ++-- services/brig/src/Brig/API/Public.hs | 4 +- services/brig/src/Brig/Provider/API.hs | 4 +- services/brig/src/Brig/Run.hs | 18 ++- .../brig/test/integration/API/Provider.hs | 4 +- .../brig/test/integration/API/User/Account.hs | 2 +- .../brig/test/integration/API/User/Handles.hs | 12 +- .../brig/test/integration/API/User/Util.hs | 2 +- .../cargohold/src/CargoHold/API/Public.hs | 4 +- .../src/Galley/API/Public/Conversation.hs | 1 + services/galley/test/integration/API.hs | 2 +- services/galley/test/integration/API/Util.hs | 7 +- services/gundeck/src/Gundeck/API/Internal.hs | 19 ++-- .../integration-test/conf/nginz/nginx.conf | 11 ++ services/spar/src/Spar/API.hs | 8 +- 34 files changed, 310 insertions(+), 174 deletions(-) create mode 100644 changelog.d/5-internal/openapi-validation delete mode 100644 libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs diff --git a/Makefile b/Makefile index e422703c453..9f2f6767fc3 100644 --- a/Makefile +++ b/Makefile @@ -607,4 +607,10 @@ upload-bombon: .PHONY: openapi-validate openapi-validate: @echo -e "Make sure you are running the backend in another terminal (make cr)\n" - vacuum lint -a -d -w <(curl http://localhost:8082/v7/api/swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/v7/api/swagger.json) +# vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/cannon-swagger.json) +# vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/cargohold-swagger.json) +# vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/spar-swagger.json) +# vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/gundeck-swagger.json) +# vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/brig-swagger.json) +# vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/galley-swagger.json) diff --git a/changelog.d/4-docs/openapi-validation b/changelog.d/4-docs/openapi-validation index a70ca12d5e5..21512f1387d 100644 --- a/changelog.d/4-docs/openapi-validation +++ b/changelog.d/4-docs/openapi-validation @@ -1 +1 @@ -Fix openapi validation errors +Fix openapi validation errors (#4295, ##) diff --git a/changelog.d/5-internal/openapi-validation b/changelog.d/5-internal/openapi-validation new file mode 100644 index 00000000000..02263c5c73d --- /dev/null +++ b/changelog.d/5-internal/openapi-validation @@ -0,0 +1 @@ +Add openapi validation test to integration diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index 12d6708f8d9..c9d97e90ff6 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -156,6 +156,10 @@ nginx_conf: envs: - all doc: true + - path: /handles + envs: + - all + doc: true - path: /list-users envs: - all @@ -291,9 +295,6 @@ nginx_conf: - path: /bot/users envs: - all - - path: /conversations/([^/]*)/bots - envs: - - all - path: /invitations/info envs: - all @@ -479,7 +480,7 @@ nginx_conf: - all max_body_size: 40m body_buffer_size: 256k - - path: /conversations/one2one/ + - path: /one2one-conversations/ envs: - all # During MLS migration, this endpoint gets called _a lot_. diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index c2c085d069f..d084bdf542d 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -682,7 +682,7 @@ getCallsConfigV2 user = do addBot :: (HasCallStack, MakesValue user) => user -> String -> String -> String -> App Response addBot user providerId serviceId convId = do - req <- baseRequest user Brig Versioned $ joinHttpPath ["conversations", convId, "bots"] + req <- baseRequest user Brig Versioned $ joinHttpPath ["bot", "conversations", convId] submit "POST" $ req & zType "access" diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 6299fc97f8f..d1c4066ae70 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -327,6 +327,18 @@ deleteTeamConv team conv user = do req <- baseRequest user Galley Versioned (joinHttpPath ["teams", teamId, "conversations", convId]) submit "DELETE" req +getMLSOne2OneConversationLegacy :: + (HasCallStack, MakesValue self, MakesValue other) => + self -> + other -> + App Response +getMLSOne2OneConversationLegacy self other = do + (domain, uid) <- objQid other + req <- + baseRequest self Galley Versioned + $ joinHttpPath ["conversations", "one2one", domain, uid] + submit "GET" req + getMLSOne2OneConversation :: (HasCallStack, MakesValue self, MakesValue other) => self -> @@ -336,7 +348,7 @@ getMLSOne2OneConversation self other = do (domain, uid) <- objQid other req <- baseRequest self Galley Versioned - $ joinHttpPath ["conversations", "one2one", domain, uid] + $ joinHttpPath ["one2one-conversations", domain, uid] submit "GET" req getGroupClients :: diff --git a/integration/test/Test/Bot.hs b/integration/test/Test/Bot.hs index b635b9e0acd..8cf199ee9db 100644 --- a/integration/test/Test/Bot.hs +++ b/integration/test/Test/Bot.hs @@ -146,7 +146,6 @@ onBotCreate chan _headers _req k = do onBotMessage chan _headers req k = do body <- liftIO $ Wai.strictRequestBody req writeChan chan (BotMessage (cs body)) - liftIO $ putStrLn $ cs body k (responseLBS status200 mempty mempty) onBotAlive _chan _headers _req k = do k (responseLBS status200 mempty (cs "success")) diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index 5c11247ebec..cbc4c1339e2 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -41,7 +41,7 @@ testGetMLSOne2OneLocalV5 = withVersion5 Version5 $ do conv %. "cipher_suite" `shouldMatchInt` 1 convId <- - getMLSOne2OneConversation alice bob `bindResponse` \resp -> do + getMLSOne2OneConversationLegacy alice bob `bindResponse` \resp -> do conv <- getJSON 200 resp conv %. "type" `shouldMatchInt` 2 shouldBeEmpty (conv %. "members.others") @@ -53,7 +53,7 @@ testGetMLSOne2OneLocalV5 = withVersion5 Version5 $ do conv %. "qualified_id" -- check that the conversation has the same ID on the other side - conv2 <- bindResponse (getMLSOne2OneConversation bob alice) $ \resp -> do + conv2 <- bindResponse (getMLSOne2OneConversationLegacy bob alice) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json @@ -64,11 +64,11 @@ testGetMLSOne2OneLocalV5 = withVersion5 Version5 $ do testGetMLSOne2OneRemoteV5 :: (HasCallStack) => App () testGetMLSOne2OneRemoteV5 = withVersion5 Version5 $ do [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] - getMLSOne2OneConversation alice bob `bindResponse` \resp -> do + getMLSOne2OneConversationLegacy alice bob `bindResponse` \resp -> do resp.status `shouldMatchInt` 400 resp.jsonBody %. "label" `shouldMatch` "mls-federated-one2one-not-supported" - getMLSOne2OneConversation bob alice `bindResponse` \resp -> do + getMLSOne2OneConversationLegacy bob alice `bindResponse` \resp -> do resp.status `shouldMatchInt` 400 resp.jsonBody %. "label" `shouldMatch` "mls-federated-one2one-not-supported" @@ -149,7 +149,7 @@ testMLSOne2OneOtherMember scenario = do testMLSOne2OneRemoveClientLocalV5 :: App () testMLSOne2OneRemoveClientLocalV5 = withVersion5 Version5 $ do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + conv <- getMLSOne2OneConversationLegacy alice bob >>= getJSON 200 [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [bob1] @@ -416,7 +416,7 @@ testMLSFederationV1ConvOnOldBackend = do fedError <- getJSON 533 resp fedError %. "label" `shouldMatch` "federation-version-error" - conv <- getMLSOne2OneConversation bob alice >>= getJSON 200 + conv <- getMLSOne2OneConversationLegacy bob alice >>= getJSON 200 keys <- getMLSPublicKeys bob >>= getJSON 200 resetOne2OneGroupGeneric bob1 conv keys @@ -464,7 +464,7 @@ testMLSFederationV1ConvOnNewBackend = do -- Bob cannot start this conversation because it would exist on Alice's -- backend and Bob cannot get the MLS public keys of that backend. - getMLSOne2OneConversation bob alice `bindResponse` \resp -> do + getMLSOne2OneConversationLegacy bob alice `bindResponse` \resp -> do fedError <- getJSON 533 resp fedError %. "label" `shouldMatch` "federation-remote-error" diff --git a/integration/test/Test/Swagger.hs b/integration/test/Test/Swagger.hs index b7f7618092c..514bf532299 100644 --- a/integration/test/Test/Swagger.hs +++ b/integration/test/Test/Swagger.hs @@ -1,11 +1,16 @@ module Test.Swagger where import qualified API.Brig as BrigP +import qualified Data.ByteString as B import qualified Data.Set as Set import Data.String.Conversions import GHC.Stack +import System.Exit +import System.FilePath +import System.Process import Testlib.Assertions import Testlib.Prelude +import UnliftIO.Temporary existingVersions :: Set Int existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6, 7] @@ -80,3 +85,25 @@ testSwaggerToc = do html :: String html = "

please pick an api version

/v0/api/swagger-ui/
/v1/api/swagger-ui/
/v2/api/swagger-ui/
/v3/api/swagger-ui/
/v4/api/swagger-ui/
/v5/api/swagger-ui/
/v6/api/swagger-ui/
/v7/api/swagger-ui/
" + +-- | This runs the swagger linter [vacuum](https://quobix.com/vacuum/). +-- +-- The reason for adding the linter in the integration tests, and not in the lint job, is that +-- it calls brig for the swagger docs it validates, but no running brig during linting. +-- +-- There is also a make rule that does this, for convenience in your develop +-- flow. Make sure that brig is running before using the make rule. +testSwaggerLint :: (HasCallStack) => App () +testSwaggerLint = do + withSystemTempDirectory "swagger" $ \tmp -> do + req <- baseRequest OwnDomain Brig Versioned $ joinHttpPath ["api", "swagger.json"] + swagger <- submit "GET" req >>= getBody 200 + liftIO $ B.writeFile (tmp "swagger.json") swagger + let cmd = shell $ "vacuum lint -a -d -e " <> (tmp "swagger.json") + (exitCode, out, err) <- liftIO $ readCreateProcessWithExitCode cmd "" + case exitCode of + ExitSuccess -> pure () + _ -> do + liftIO $ putStrLn out + liftIO $ putStrLn err + assertFailure "swagger validation errors" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs index 144b8db270f..0c294f7a00d 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -34,13 +34,14 @@ module Wire.API.Routes.Internal.Brig GetAccountConferenceCallingConfig, PutAccountConferenceCallingConfig, DeleteAccountConferenceCallingConfig, + GetRichInfoMultiResponse (..), swaggerDoc, module Wire.API.Routes.Internal.Brig.EJPD, FoundInvitationCode (..), ) where -import Control.Lens ((.~)) +import Control.Lens ((.~), (?~)) import Data.Aeson (FromJSON, ToJSON) import Data.Code qualified as Code import Data.CommaSeparatedList @@ -71,7 +72,6 @@ import Wire.API.Routes.Internal.Brig.EJPD import Wire.API.Routes.Internal.Brig.OAuth (OAuthAPI) import Wire.API.Routes.Internal.Brig.SearchIndex (ISearchIndexAPI) import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi -import Wire.API.Routes.Internal.LegalHold qualified as LegalHoldInternalAPI import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public (ZUser) @@ -90,22 +90,25 @@ import Wire.API.User.Client import Wire.API.User.RichInfo type EJPDRequest = - Summary - "Identify users for law enforcement. Wire has legal requirements to cooperate \ - \with the authorities. The wire backend operations team uses this to answer \ - \identification requests manually. It is our best-effort representation of the \ - \minimum required information we need to hand over about targets and (in some \ - \cases) their communication peers. For more information, consult ejpd.admin.ch." - :> "ejpd-request" - :> QueryParam' - [ Optional, - Strict, - Description "Also provide information about all contacts of the identified users" - ] - "include_contacts" - Bool - :> Servant.ReqBody '[Servant.JSON] EJPDRequestBody - :> Post '[Servant.JSON] EJPDResponseBody + Named + "ejpd-request" + ( Summary + "Identify users for law enforcement. Wire has legal requirements to cooperate \ + \with the authorities. The wire backend operations team uses this to answer \ + \identification requests manually. It is our best-effort representation of the \ + \minimum required information we need to hand over about targets and (in some \ + \cases) their communication peers. For more information, consult ejpd.admin.ch." + :> "ejpd-request" + :> QueryParam' + [ Optional, + Strict, + Description "Also provide information about all contacts of the identified users" + ] + "include_contacts" + Bool + :> Servant.ReqBody '[Servant.JSON] EJPDRequestBody + :> Post '[Servant.JSON] EJPDResponseBody + ) type GetAccountConferenceCallingConfig = Summary @@ -159,10 +162,10 @@ type GetAllConnections = type AccountAPI = Named "get-account-conference-calling-config" GetAccountConferenceCallingConfig - :<|> PutAccountConferenceCallingConfig - :<|> DeleteAccountConferenceCallingConfig - :<|> GetAllConnectionsUnqualified - :<|> GetAllConnections + :<|> Named "i-put-account-conference-calling-config" PutAccountConferenceCallingConfig + :<|> Named "i-delete-account-conference-calling-config" DeleteAccountConferenceCallingConfig + :<|> Named "i-get-all-connections-unqualified" GetAllConnectionsUnqualified + :<|> Named "i-get-all-connections" GetAllConnections :<|> Named "createUserNoVerify" -- This endpoint can lead to the following events being sent: @@ -373,12 +376,11 @@ type AccountAPI = ( "users" :> "rich-info" :> QueryParam' '[Optional, Strict] "ids" (CommaSeparatedList UserId) - :> Get '[Servant.JSON] [(UserId, RichInfo)] + :> Get '[Servant.JSON] GetRichInfoMultiResponse ) :<|> Named "iHeadHandle" ( CanThrow 'InvalidHandle - :> "users" :> "handles" :> Capture "handle" Handle :> MultiVerb @@ -466,23 +468,29 @@ instance ToSchema NewKeyPackageRef where type MLSAPI = "mls" :> GetMLSClients type GetMLSClients = - Summary "Return all clients and all MLS-capable clients of a user" - :> "clients" - :> CanThrow 'UserNotFound - :> Capture "user" UserId - :> QueryParam' '[Required, Strict] "ciphersuite" CipherSuite - :> MultiVerb1 - 'GET - '[Servant.JSON] - (Respond 200 "MLS clients" (Set ClientInfo)) + Named + "get-mls-clients" + ( Summary "Return all clients and all MLS-capable clients of a user" + :> "clients" + :> CanThrow 'UserNotFound + :> Capture "user" UserId + :> QueryParam' '[Required, Strict] "ciphersuite" CipherSuite + :> MultiVerb1 + 'GET + '[Servant.JSON] + (Respond 200 "MLS clients" (Set ClientInfo)) + ) type GetVerificationCode = - Summary "Get verification code for a given email and action" - :> "users" - :> Capture "uid" UserId - :> "verification-code" - :> Capture "action" VerificationAction - :> Get '[Servant.JSON] (Maybe Code.Value) + Named + "get-verification-code" + ( Summary "Get verification code for a given email and action" + :> "users" + :> Capture "uid" UserId + :> "verification-code" + :> Capture "action" VerificationAction + :> Get '[Servant.JSON] (Maybe Code.Value) + ) type API = "i" @@ -594,9 +602,9 @@ type TeamInvitations = ) type UserAPI = - UpdateUserLocale - :<|> DeleteUserLocale - :<|> GetDefaultLocale + Named "i-update-user-locale" UpdateUserLocale + :<|> Named "i-delete-user-locale" DeleteUserLocale + :<|> Named "i-get-default-locale" GetDefaultLocale :<|> Named "get-user-export-data" ( Summary "Get user export data" @@ -745,10 +753,19 @@ type ProviderAPI = type FederationRemotesAPIDescription = "See https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections for background. " +newtype GetRichInfoMultiResponse + = GetRichInfoMultiResponse + [(UserId, RichInfo)] + deriving newtype (FromJSON, ToJSON) + +instance S.ToSchema GetRichInfoMultiResponse where + declareNamedSchema _ = + pure $ + S.NamedSchema (Just $ "GetRichInfoMultiResponse") $ + mempty & S.description ?~ "List of pairs of UserId and RichInfo" + swaggerDoc :: OpenApi -swaggerDoc = - brigSwaggerDoc - <> LegalHoldInternalAPI.swaggerDoc +swaggerDoc = brigSwaggerDoc brigSwaggerDoc :: OpenApi brigSwaggerDoc = diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs index 592e72dc61a..75cc7caa714 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs @@ -28,8 +28,8 @@ import Wire.API.Routes.Named type InternalAPI = "i" - :> ( "status" :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] () - :<|> Named "iGetAsset" ("assets" :> Capture "key" AssetKey :> Get '[Servant.JSON] Text) + :> ( Named "i_status" ("status" :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] ()) + :<|> Named "i_get_asset" ("assets" :> Capture "key" AssetKey :> Get '[Servant.JSON] Text) ) swaggerDoc :: OpenApi diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Gundeck.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Gundeck.hs index 9287611a5e9..c786d1c3020 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Gundeck.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Gundeck.hs @@ -22,6 +22,7 @@ import Servant.Server.Internal.ErrorFormatter import Wire.API.CannonId import Wire.API.Presence import Wire.API.Push.V2 +import Wire.API.Routes.Named import Wire.API.Routes.Public -- | this can be replaced by `ReqBody '[JSON] Presence` once the fix in cannon from @@ -82,18 +83,18 @@ instance (HasOpenApi sub) => HasOpenApi (ReqBodyHack :> sub) where type InternalAPI = "i" - :> ( ("status" :> Get '[JSON] NoContent) - :<|> ("push" :> "v2" :> ReqBody '[JSON] [Push] :> Post '[JSON] NoContent) + :> ( Named "i-status" ("status" :> Get '[JSON] NoContent) + :<|> Named "i-push" ("push" :> "v2" :> ReqBody '[JSON] [Push] :> Post '[JSON] NoContent) :<|> ( "presences" - :> ( (QueryParam' [Required, Strict] "ids" (CommaSeparatedList UserId) :> Get '[JSON] [Presence]) - :<|> (Capture "uid" UserId :> Get '[JSON] [Presence]) - :<|> (ReqBodyHack :> Verb 'POST 201 '[JSON] (Headers '[Header "Location" URI] NoContent)) - :<|> (Capture "uid" UserId :> "devices" :> Capture "did" ConnId :> "cannons" :> Capture "cannon" CannonId :> Delete '[JSON] NoContent) + :> ( Named "i-presences-get-for-users" (QueryParam' [Required, Strict] "ids" (CommaSeparatedList UserId) :> Get '[JSON] [Presence]) + :<|> Named "i-presences-get-for-user" (Capture "uid" UserId :> Get '[JSON] [Presence]) + :<|> Named "i-presences-post" (ReqBodyHack :> Verb 'POST 201 '[JSON] (Headers '[Header "Location" URI] NoContent)) + :<|> Named "i-presences-delete" (Capture "uid" UserId :> "devices" :> Capture "did" ConnId :> "cannons" :> Capture "cannon" CannonId :> Delete '[JSON] NoContent) ) ) - :<|> (ZUser :> "clients" :> Capture "cid" ClientId :> Delete '[JSON] NoContent) - :<|> (ZUser :> "user" :> Delete '[JSON] NoContent) - :<|> ("push-tokens" :> Capture "uid" UserId :> Get '[JSON] PushTokenList) + :<|> Named "i-clients-delete" (ZUser :> "clients" :> Capture "cid" ClientId :> Delete '[JSON] NoContent) + :<|> Named "i-user-delete" (ZUser :> "user" :> Delete '[JSON] NoContent) + :<|> Named "i-push-tokens-get" ("push-tokens" :> Capture "uid" UserId :> Get '[JSON] PushTokenList) ) swaggerDoc :: S.OpenApi diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs deleted file mode 100644 index 73087b78ea3..00000000000 --- a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs +++ /dev/null @@ -1,45 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Wire.API.Routes.Internal.LegalHold where - -import Control.Lens -import Data.Id -import Data.OpenApi (OpenApi) -import Data.OpenApi.Lens -import Data.Proxy -import Imports -import Servant.API -import Servant.OpenApi -import Wire.API.Team.Feature - -type InternalLegalHoldAPI = - "i" - :> "teams" - :> ( Capture "tid" TeamId - :> "legalhold" - :> Get '[JSON] (LockableFeature LegalholdConfig) - :<|> Capture "tid" TeamId - :> "legalhold" - :> ReqBody '[JSON] (Feature LegalholdConfig) - :> Put '[] NoContent - ) - -swaggerDoc :: OpenApi -swaggerDoc = - toOpenApi (Proxy @InternalLegalHoldAPI) - & info . title .~ "Wire-Server internal legalhold API" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs index b5bc7b34380..0cd59f6d22b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs @@ -23,15 +23,16 @@ import Data.OpenApi import Imports import Servant import Servant.OpenApi +import Wire.API.Routes.Named import Wire.API.User import Wire.API.User.Saml type InternalAPI = "i" - :> ( "status" :> Get '[JSON] NoContent - :<|> "teams" :> Capture "team" TeamId :> DeleteNoContent - :<|> "sso" :> "settings" :> ReqBody '[JSON] SsoSettings :> Put '[JSON] NoContent - :<|> "scim" :> "userinfo" :> Capture "user" UserId :> Post '[JSON] ScimUserInfo + :> ( Named "i_status" ("status" :> Get '[JSON] NoContent) + :<|> Named "i_delete_team" ("teams" :> Capture "team" TeamId :> DeleteNoContent) + :<|> Named "i_put_sso_settings" ("sso" :> "settings" :> ReqBody '[JSON] SsoSettings :> Put '[JSON] NoContent) + :<|> Named "i_post_scim_user_info" ("scim" :> "userinfo" :> Capture "user" UserId :> Post '[JSON] ScimUserInfo) ) swaggerDoc :: OpenApi diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 424ac403d81..d377fc79835 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -425,8 +425,9 @@ type SelfAPI = type UserHandleAPI = Named - "check-user-handles" + "check-user-handles@v6" ( Summary "Check availability of user handles" + :> Until V7 :> ZUser :> "users" :> "handles" @@ -438,8 +439,22 @@ type UserHandleAPI = [Handle] ) :<|> Named - "check-user-handle" + "check-user-handles" + ( Summary "Check availability of user handles" + :> From V7 + :> ZUser + :> "handles" + :> ReqBody '[JSON] CheckHandles + :> MultiVerb + 'POST + '[JSON] + '[Respond 200 "List of free handles" [Handle]] + [Handle] + ) + :<|> Named + "check-user-handle@v6" ( Summary "Check whether a user handle can be taken" + :> Until V7 :> CanThrow 'InvalidHandle :> CanThrow 'HandleNotFound :> ZUser @@ -452,6 +467,21 @@ type UserHandleAPI = '[Respond 200 "Handle is taken" ()] () ) + :<|> Named + "check-user-handle" + ( Summary "Check whether a user handle can be taken" + :> From V7 + :> CanThrow 'InvalidHandle + :> CanThrow 'HandleNotFound + :> ZUser + :> "handles" + :> Capture "handle" Text + :> MultiVerb + 'HEAD + '[JSON] + '[Respond 200 "Handle is taken" ()] + () + ) type AccountAPI = Named diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs index 782e29fbb59..4c072cf0210 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs @@ -43,8 +43,9 @@ type DeleteResponses = type BotAPI = Named - "add-bot" + "add-bot@v6" ( Summary "Add bot" + :> Until V7 :> CanThrow 'AccessDenied :> CanThrow 'InvalidConversation :> CanThrow 'TooManyConversationMembers @@ -58,8 +59,25 @@ type BotAPI = :> MultiVerb1 'POST '[JSON] (Respond 201 "" AddBotResponse) ) :<|> Named - "remove-bot" + "add-bot" + ( Summary "Add bot" + :> From V7 + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidConversation + :> CanThrow 'TooManyConversationMembers + :> CanThrow 'ServiceDisabled + :> ZAccess + :> ZConn + :> "bot" + :> "conversations" + :> Capture "conv" ConvId + :> ReqBody '[JSON] AddBot + :> MultiVerb1 'POST '[JSON] (Respond 201 "" AddBotResponse) + ) + :<|> Named + "remove-bot@v6" ( Summary "Remove bot" + :> Until V7 :> CanThrow 'AccessDenied :> CanThrow 'InvalidConversation :> ZAccess @@ -70,6 +88,20 @@ type BotAPI = :> Capture "bot" BotId :> MultiVerb 'DELETE '[JSON] DeleteResponses (Maybe RemoveBotResponse) ) + :<|> Named + "remove-bot" + ( Summary "Remove bot" + :> From V7 + :> CanThrow 'AccessDenied + :> CanThrow 'InvalidConversation + :> ZAccess + :> ZConn + :> "bot" + :> "conversations" + :> Capture "conv" ConvId + :> Capture "bot" BotId + :> MultiVerb 'DELETE '[JSON] DeleteResponses (Maybe RemoveBotResponse) + ) :<|> Named "bot-get-self" ( Summary "Get self" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs index 7cbc9adeb16..60802b1c5bc 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Conversation.hs @@ -621,9 +621,10 @@ type ConversationAPI = :> ConversationVerb 'V2 Conversation ) :<|> Named - "create-one-to-one-conversation" + "create-one-to-one-conversation@v6" ( Summary "Create a 1:1 conversation" :> From 'V3 + :> Until 'V7 :> CanThrow 'ConvAccessDenied :> CanThrow 'InvalidOperation :> CanThrow 'NoBindingTeamMembers @@ -641,6 +642,26 @@ type ConversationAPI = :> ReqBody '[JSON] NewConv :> ConversationVerb 'V3 Conversation ) + :<|> Named + "create-one-to-one-conversation" + ( Summary "Create a 1:1 conversation" + :> From 'V7 + :> CanThrow 'ConvAccessDenied + :> CanThrow 'InvalidOperation + :> CanThrow 'NoBindingTeamMembers + :> CanThrow 'NonBindingTeam + :> CanThrow 'NotATeamMember + :> CanThrow 'NotConnected + :> CanThrow OperationDenied + :> CanThrow 'TeamNotFound + :> CanThrow 'MissingLegalholdConsent + :> CanThrow UnreachableBackendsLegacy + :> ZLocalUser + :> ZConn + :> "one2one-conversations" + :> ReqBody '[JSON] NewConv + :> ConversationVerb 'V3 Conversation + ) :<|> Named "get-one-to-one-mls-conversation@v5" ( Summary "Get an MLS 1:1 conversation" @@ -675,8 +696,7 @@ type ConversationAPI = :> ZLocalUser :> CanThrow 'MLSNotEnabled :> CanThrow 'NotConnected - :> "conversations" - :> "one2one" + :> "one2one-conversations" :> QualifiedCapture "usr" UserId :> QueryParam "format" MLSPublicKeyFormat :> MultiVerb1 'GET '[JSON] (Respond 200 "MLS 1-1 conversation" (MLSOne2OneConversation SomeKey)) @@ -964,6 +984,7 @@ type ConversationAPI = :<|> Named "update-other-member-unqualified" ( Summary "Update membership of the specified user (deprecated)" + :> Until V7 :> Deprecated :> Description "Use `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead" :> ZLocalUser diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 4458828a3c7..1b61c678a71 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -169,7 +169,6 @@ library Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti Wire.API.Routes.Internal.Galley.TeamsIntra Wire.API.Routes.Internal.Gundeck - Wire.API.Routes.Internal.LegalHold Wire.API.Routes.Internal.Spar Wire.API.Routes.LowLevelStream Wire.API.Routes.MultiTablePaging diff --git a/nix/wire-server.nix b/nix/wire-server.nix index e3ee19364cc..370397a809f 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -293,6 +293,7 @@ let pkgs.nginz pkgs.mls-test-cli pkgs.awscli2 + pkgs.vacuum-go integration-dynamic-backends-db-schemas integration-dynamic-backends-brig-index integration-dynamic-backends-ses diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 21d73c16e99..ea9c2f26f20 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -157,7 +157,7 @@ servantSitemap = :<|> ejpdAPI :<|> accountAPI :<|> mlsAPI - :<|> getVerificationCode + :<|> Named @"get-verification-code" getVerificationCode :<|> teamsAPI :<|> userAPI :<|> clientAPI @@ -177,11 +177,10 @@ ejpdAPI :: Member Rpc r ) => ServerT BrigIRoutes.EJPDRequest (Handler r) -ejpdAPI = - Brig.User.EJPD.ejpdRequest +ejpdAPI = Named @"ejpd-request" Brig.User.EJPD.ejpdRequest mlsAPI :: ServerT BrigIRoutes.MLSAPI (Handler r) -mlsAPI = getMLSClients +mlsAPI = Named @"get-mls-clients" getMLSClients accountAPI :: ( Member BlockListStore r, @@ -209,10 +208,10 @@ accountAPI :: ServerT BrigIRoutes.AccountAPI (Handler r) accountAPI = Named @"get-account-conference-calling-config" getAccountConferenceCallingConfig - :<|> putAccountConferenceCallingConfig - :<|> deleteAccountConferenceCallingConfig - :<|> getConnectionsStatusUnqualified - :<|> getConnectionsStatus + :<|> Named @"i-put-account-conference-calling-config" putAccountConferenceCallingConfig + :<|> Named @"i-delete-account-conference-calling-config" deleteAccountConferenceCallingConfig + :<|> Named @"i-get-all-connections-unqualified" getConnectionsStatusUnqualified + :<|> Named @"i-get-all-connections" getConnectionsStatus :<|> Named @"createUserNoVerify" createUserNoVerify :<|> Named @"createUserNoVerifySpar" createUserNoVerifySpar :<|> Named @"putSelfEmail" changeSelfEmailMaybeSendH @@ -271,9 +270,9 @@ teamsAPI = userAPI :: (Member UserSubsystem r) => ServerT BrigIRoutes.UserAPI (Handler r) userAPI = - updateLocale - :<|> deleteLocale - :<|> getDefaultUserLocale + Named @"i-update-user-locale" updateLocale + :<|> Named @"i-delete-user-locale" deleteLocale + :<|> Named @"i-get-default-locale" getDefaultUserLocale :<|> Named @"get-user-export-data" getUserExportDataH clientAPI :: ServerT BrigIRoutes.ClientAPI (Handler r) @@ -783,9 +782,9 @@ getRichInfoH uid = RichInfo . fromMaybe mempty <$> lift (liftSem $ UserStore.getRichInfo uid) -getRichInfoMultiH :: Maybe (CommaSeparatedList UserId) -> (Handler r) [(UserId, RichInfo)] +getRichInfoMultiH :: Maybe (CommaSeparatedList UserId) -> Handler r BrigIRoutes.GetRichInfoMultiResponse getRichInfoMultiH (maybe [] fromCommaSeparatedList -> uids) = - lift $ wrapClient $ API.lookupRichInfoMultiUsers uids + lift $ wrapClient $ BrigIRoutes.GetRichInfoMultiResponse <$> API.lookupRichInfoMultiUsers uids updateHandleH :: (Member UserSubsystem r) => diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 2cb50e9c856..aa83309f397 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -440,7 +440,9 @@ servantSitemap = userHandleAPI :: ServerT UserHandleAPI (Handler r) userHandleAPI = - Named @"check-user-handles" checkHandles + Named @"check-user-handles@v6" checkHandles + :<|> Named @"check-user-handles" checkHandles + :<|> Named @"check-user-handle@v6" checkHandle :<|> Named @"check-user-handle" checkHandle searchAPI :: ServerT SearchAPI (Handler r) diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 5d2f9c8e313..162a1109060 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -145,7 +145,9 @@ botAPI :: ) => ServerT BotAPI (Handler r) botAPI = - Named @"add-bot" addBot + Named @"add-bot@v6" addBot + :<|> Named @"add-bot" addBot + :<|> Named @"remove-bot@v6" removeBot :<|> Named @"remove-bot" removeBot :<|> Named @"bot-get-self" botGetSelf :<|> Named @"bot-delete-self" botDeleteSelf diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 7f299c65195..2bd43d585e1 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -116,8 +116,9 @@ mkApp opts = do where middleware :: Env -> Wai.Middleware middleware e = - -- this rewrites the request, so it must be at the top (i.e. applied last) + -- these rewrite the request, so they must be at the top (i.e. applied last) versionMiddleware e.disabledVersions + . internalHandleCompatibilityMiddleware -- this also rewrites the request . requestIdMiddleware e.appLogger defaultRequestIdHeaderName . Metrics.servantPrometheusMiddleware (Proxy @ServantCombinedAPI) @@ -142,6 +143,21 @@ mkApp opts = do req cont +-- FUTUREWORK: this rewrites /i/users/handles to /i/handles, for backward +-- compatibility with the old endpoint path during deployment. Once the new +-- endpoint has been deployed, this middleware can be removed. +internalHandleCompatibilityMiddleware :: Wai.Middleware +internalHandleCompatibilityMiddleware app req k = + app + ( case Wai.pathInfo req of + ("i" : "users" : "handles" : rest) -> + req + { Wai.pathInfo = ("i" : "handles" : rest) + } + _ -> req + ) + k + type ServantCombinedAPI = ( DocsAPI :<|> BrigAPI diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index fc4c6a7bda7..67ab4913344 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -1397,7 +1397,7 @@ addBot :: addBot brig uid pid sid cid = post $ brig - . paths ["conversations", toByteString' cid, "bots"] + . paths ["bot", "conversations", toByteString' cid] . header "Z-Type" "access" . header "Z-User" (toByteString' uid) . header "Z-Connection" "conn" @@ -1413,7 +1413,7 @@ removeBot :: removeBot brig uid cid bid = delete $ brig - . paths ["conversations", toByteString' cid, "bots", toByteString' bid] + . paths ["bot", "conversations", toByteString' cid, toByteString' bid] . header "Z-Type" "access" . header "Z-User" (toByteString' uid) . header "Z-Connection" "conn" diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index b0ba8de47d4..e1757ba6a69 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -1530,7 +1530,7 @@ execAndAssertUserDeletion brig cannon u hdl others userJournalWatcher execDelete usr <- postUserInternal o brig Util.assertUserActivateJournaled userJournalWatcher usr "user activate execAndAssertUserDeletion" -- Handle is available again - Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) + Bilge.head (brig . paths ["handles", toByteString' hdl] . zUser uid) !!! const 404 === statusCode where assertDeletedProfileSelf = do diff --git a/services/brig/test/integration/API/User/Handles.hs b/services/brig/test/integration/API/User/Handles.hs index 5776fc082b9..c505e9b824a 100644 --- a/services/brig/test/integration/API/User/Handles.hs +++ b/services/brig/test/integration/API/User/Handles.hs @@ -62,8 +62,8 @@ tests _cl _at conf p b c g = test p "handles/query" $ testHandleQuery conf b, test p "handles/query - team-search-visibility SearchVisibilityStandard" $ testHandleQuerySearchVisibilityStandard conf b, test p "handles/query - team-search-visibility SearchVisibilityNoNameOutsideTeam" $ testHandleQuerySearchVisibilityNoNameOutsideTeam conf b g, - test p "GET /users/handles/ 200" $ testGetUserByUnqualifiedHandle b, - test p "GET /users/handles/ 404" $ testGetUserByUnqualifiedHandleFailure b, + test p "GET /handles/ 200" $ testGetUserByUnqualifiedHandle b, + test p "GET /handles/ 404" $ testGetUserByUnqualifiedHandleFailure b, test p "GET /users/by-handle// : 200" $ testGetUserByQualifiedHandle b, test p "GET /users/by-handle// : 404" $ testGetUserByQualifiedHandleFailure b, test p "GET /users/by-handle// : no federation" $ testGetUserByQualifiedHandleNoFederation conf b @@ -105,7 +105,7 @@ testHandleUpdate brig cannon = do -- The owner of the handle can always retry the update put (brig . path "/self/handle" . contentJson . zUser uid . zConn "c" . body update) !!! const 200 === statusCode - Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) + Bilge.head (brig . paths ["handles", toByteString' hdl] . zUser uid) !!! const 200 === statusCode -- For other users, the handle is unavailable uid2 <- userId <$> randomUser brig @@ -120,7 +120,7 @@ testHandleUpdate brig cannon = do let update2 = RequestBodyLBS . encode $ HandleUpdate hdl2 put (brig . path "/self/handle" . contentJson . zUser uid . zConn "c" . body update2) !!! const 200 === statusCode - Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) + Bilge.head (brig . paths ["handles", toByteString' hdl] . zUser uid) !!! const 404 === statusCode -- The owner appears by the new handle in search Search.refreshIndex brig @@ -163,7 +163,7 @@ testHandleQuery opts brig = do uid <- userId <$> randomUser brig hdl <- randomHandle -- Query for the handle availability (must be free) - Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) + Bilge.head (brig . paths ["handles", toByteString' hdl] . zUser uid) !!! const 404 === statusCode -- Set handle let update = RequestBodyLBS . encode $ HandleUpdate hdl @@ -174,7 +174,7 @@ testHandleQuery opts brig = do const 200 === statusCode const (Just (fromJust $ parseHandle hdl)) === (userHandle <=< responseJsonMaybe) -- Query for the handle availability (must be taken) - Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) + Bilge.head (brig . paths ["handles", toByteString' hdl] . zUser uid) !!! const 200 === statusCode -- Query user profiles by handles get (apiVersion "v1" . brig . path "/users" . queryItem "handles" (toByteString' hdl) . zUser uid) !!! do diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index c05174bd505..cb6f1aa1712 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -83,7 +83,7 @@ checkHandles brig uid hs num = let hs' = unsafeRange hs num' = unsafeRange num js = RequestBodyLBS $ encode $ CheckHandles hs' num' - in post (brig . path "/users/handles" . contentJson . zUser uid . body js) + in post (brig . path "/handles" . contentJson . zUser uid . body js) randomUserWithHandle :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs index 8b9c7cfccfd..e0d63d3167d 100644 --- a/services/cargohold/src/CargoHold/API/Public.hs +++ b/services/cargohold/src/CargoHold/API/Public.hs @@ -93,8 +93,8 @@ servantSitemap = internalSitemap :: ServerT InternalAPI Handler internalSitemap = - pure () - :<|> Named @"iGetAsset" iDownloadAssetV3 + Named @"i_status" (pure ()) + :<|> Named @"i_get_asset" iDownloadAssetV3 -- | Like 'downloadAssetV3' below, but it works without user session token, and has a -- different route type. diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index a234ec89b92..532d4f4dffa 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -60,6 +60,7 @@ conversationAPI = <@> mkNamedAPI @"delete-subconversation" deleteSubConversation <@> mkNamedAPI @"get-subconversation-group-info" getSubConversationGroupInfo <@> mkNamedAPI @"create-one-to-one-conversation@v2" createOne2OneConversation + <@> mkNamedAPI @"create-one-to-one-conversation@v6" createOne2OneConversation <@> mkNamedAPI @"create-one-to-one-conversation" createOne2OneConversation <@> mkNamedAPI @"get-one-to-one-mls-conversation@v5" getMLSOne2OneConversationV5 <@> mkNamedAPI @"get-one-to-one-mls-conversation@v6" getMLSOne2OneConversationV6 diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 18ce92d9e20..2ddc71bce90 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -1947,7 +1947,7 @@ postConvO2OFailWithSelf = do g <- viewGalley alice <- randomUser let inv = NewConv [alice] [] Nothing mempty Nothing Nothing Nothing Nothing roleNameWireAdmin BaseProtocolProteusTag - post (g . path "/conversations/one2one" . zUser alice . zConn "conn" . zType "access" . json inv) !!! do + post (g . path "one2one-conversations" . zUser alice . zConn "conn" . zType "access" . json inv) !!! do const 403 === statusCode const (Just "invalid-op") === fmap label . responseJsonUnsafe diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index a12bf6d2e10..07909cbbbcd 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -651,7 +651,7 @@ createOne2OneTeamConv u1 u2 n tid = do g <- viewGalley let conv = NewConv [u2] [] (n >>= checked) mempty Nothing (Just $ ConvTeamInfo tid) Nothing Nothing roleNameWireAdmin BaseProtocolProteusTag - post $ g . path "/conversations/one2one" . zUser u1 . zConn "conn" . zType "access" . json conv + post $ g . path "/one2one-conversations" . zUser u1 . zConn "conn" . zType "access" . json conv postConv :: UserId -> @@ -758,7 +758,7 @@ postO2OConv :: UserId -> UserId -> Maybe Text -> TestM ResponseLBS postO2OConv u1 u2 n = do g <- viewGalley let conv = NewConv [u2] [] (n >>= checked) mempty Nothing Nothing Nothing Nothing roleNameWireAdmin BaseProtocolProteusTag - post $ g . path "/conversations/one2one" . zUser u1 . zConn "conn" . zType "access" . json conv + post $ g . path "/one2one-conversations" . zUser u1 . zConn "conn" . zType "access" . json conv postConnectConv :: UserId -> UserId -> Text -> Text -> Maybe Text -> TestM ResponseLBS postConnectConv a b name msg email = do @@ -1225,7 +1225,8 @@ putOtherMemberQualified from to m c = do putOtherMember :: UserId -> UserId -> OtherMemberUpdate -> ConvId -> TestM ResponseLBS putOtherMember from to m c = do - g <- viewGalley + -- this endpoint was removed in v7 + g <- fmap (addPrefixAtVersion V6 .) (view tsUnversionedGalley) put $ g . paths ["conversations", toByteString' c, "members", toByteString' to] diff --git a/services/gundeck/src/Gundeck/API/Internal.hs b/services/gundeck/src/Gundeck/API/Internal.hs index 58a1043cd4b..f0dfabe1d19 100644 --- a/services/gundeck/src/Gundeck/API/Internal.hs +++ b/services/gundeck/src/Gundeck/API/Internal.hs @@ -35,19 +35,20 @@ import Servant import Wire.API.Push.Token qualified as PushTok import Wire.API.Push.V2 import Wire.API.Routes.Internal.Gundeck +import Wire.API.Routes.Named servantSitemap :: ServerT InternalAPI Gundeck servantSitemap = - statusH - :<|> pushH - :<|> ( Presence.listAllH - :<|> Presence.listH - :<|> Presence.addH - :<|> Presence.removeH + Named @"i-status" statusH + :<|> Named @"i-push" pushH + :<|> ( Named @"i-presences-get-for-users" Presence.listAllH + :<|> Named @"i-presences-get-for-user" Presence.listH + :<|> Named @"i-presences-post" Presence.addH + :<|> Named @"i-presences-delete" Presence.removeH ) - :<|> unregisterClientH - :<|> removeUserH - :<|> getPushTokensH + :<|> Named @"i-clients-delete" unregisterClientH + :<|> Named @"i-user-delete" removeUserH + :<|> Named @"i-push-tokens-get" getPushTokensH statusH :: (Applicative m) => m NoContent statusH = pure NoContent diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index c887411e18f..bfbc75ccc7c 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -244,6 +244,11 @@ http { proxy_pass http://brig; } + location ~* ^(/v[0-9]+)?/handles { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + location ~* ^(/v[0-9]+)?/list-users { include common_response_with_zauth.conf; proxy_pass http://brig; @@ -358,6 +363,12 @@ http { proxy_pass http://galley; } + location ~* ^(/v[0-9]+)?/one2one-conversations$ { + include common_response_with_zauth.conf; + oauth_scope conversations; + proxy_pass http://galley; + } + location ~* ^(/v[0-9]+)?/conversations$ { include common_response_with_zauth.conf; oauth_scope conversations; diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 49399d77be3..cbf91970c4e 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -222,10 +222,10 @@ apiINTERNAL :: ) => ServerT InternalAPI (Sem r) apiINTERNAL = - internalStatus - :<|> internalDeleteTeam - :<|> internalPutSsoSettings - :<|> internalGetScimUserInfo + Named @"i_status" internalStatus + :<|> Named @"i_delete_team" internalDeleteTeam + :<|> Named @"i_put_sso_settings" internalPutSsoSettings + :<|> Named @"i_post_scim_user_info" internalGetScimUserInfo appName :: Text appName = "spar" From 75c9feed83b9ffa1260d444044fc27df81e77646 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 28 Oct 2024 10:36:26 +0100 Subject: [PATCH 06/27] Disable federation for specific protocols (#4278) * Add option to disable federation for a protocol * Enforce federation protocol when adding users * Test disabling federation for proteus * Add federationProtocol option to galley chart * Add documentation for federationProtocols * Test effect of federationProtocol on local users Local users can be added to remote proteus conversations even if `federationProtocol` is set to `mls` only on the local backend. * Add CHANGELOG entry --- changelog.d/2-features/no-federated-proteus | 1 + charts/galley/templates/configmap.yaml | 3 +++ charts/galley/values.yaml | 4 +++ .../src/developer/reference/config-options.md | 12 +++++++++ integration/test/Test/Conversation.hs | 25 +++++++++++++++++++ .../src/Wire/API/Federation/Error.hs | 10 ++++++++ services/galley/galley.integration.yaml | 1 + services/galley/src/Galley/API/Action.hs | 17 ++++++++++++- services/galley/src/Galley/API/Create.hs | 1 + services/galley/src/Galley/Options.hs | 3 +++ services/galley/src/Galley/Types/UserList.hs | 2 +- 11 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 changelog.d/2-features/no-federated-proteus diff --git a/changelog.d/2-features/no-federated-proteus b/changelog.d/2-features/no-federated-proteus new file mode 100644 index 00000000000..cfc0fcd7b7c --- /dev/null +++ b/changelog.d/2-features/no-federated-proteus @@ -0,0 +1 @@ +Add `federationProtocols` setting to galley, which can be used to disable the creation of federated conversations with a given protocol diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index cf1426e8adb..7b790a8c596 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -83,6 +83,9 @@ data: {{ fail "settings.conversationCodeURI and settings.multiIngress are mutually exclusive" }} {{- end }} federationDomain: {{ .settings.federationDomain }} + {{- if .settings.federationProtocols }} + federationProtocols: {{ .settings.federationProtocols }} + {{- end }} {{- if $.Values.secrets.mlsPrivateKeys }} mlsPrivateKeyPaths: removal: diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 877a2734039..71045b680b9 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -75,6 +75,10 @@ config: iterations: 1 parallelism: 32 memory: 180224 # 176 MiB + + # To disable proteus for new federated conversations: + # federationProtocols: ["mls"] + featureFlags: # see #RefConfigOptions in `/docs/reference` (https://github.com/wireapp/wire-server/) appLock: defaults: diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index ae843566522..a22b947c04b 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -464,6 +464,18 @@ federator: clientPrivateKey: client-key.pem ``` +### Federation protocols + +A backend can restrict creation of new federated conversations according to the protocol used (Proteus or MLS). This is controlled by the `federationProtocols` setting. For example: + +```yaml +galley: + settings: + federationProtocols: ["mls"] +``` + +will prevent the creation of a Proteus conversation containing federated users, and will prevent federated users from joining a Proteus conversation on this backend. However, existing Proteus conversations with federated users will continue to function, and users of this backend will be allowed to join new and existing Proteus conversations on federated backends. + ### Outlook calendar integration This feature setting only applies to the Outlook Calendar extension for Wire. As it is an external service, it should only be configured through this feature flag and otherwise ignored by the backend. diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index e6ae83d1519..a9ef7595714 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -908,3 +908,28 @@ testPostConvWithUnreachableRemoteUsers = do for_ convs $ \conv -> conv %. "type" `shouldNotMatchInt` 0 assertNoEvent 2 wssAlice assertNoEvent 2 wssAlex + +testNoFederationWithProteus :: (HasCallStack) => App () +testNoFederationWithProteus = do + withModifiedBackend + ( def + { galleyCfg = \conf -> + conf & setField "settings.federationProtocols" ["mls"] + } + ) + $ \domain -> do + charlieDomain <- asString $ make OwnDomain + [alice, alex, arnold, bob] <- createAndConnectUsers [domain, domain, domain, charlieDomain] + + do + conv <- postConversation alice defProteus {qualifiedUsers = [alex]} >>= getJSON 201 + bindResponse (addMembers alice conv def {users = [bob]}) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "label" `shouldMatch` "federation-disabled-for-protocol" + void $ addMembers alice conv def {users = [arnold]} >>= getJSON 200 + + bindResponse (postConversation alice defProteus {qualifiedUsers = [bob]}) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "label" `shouldMatch` "federation-disabled-for-protocol" + + void $ postConversation bob defProteus {qualifiedUsers = [alice]} >>= getJSON 201 diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs index 830a9f062fc..d13d2c23503 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs @@ -150,6 +150,8 @@ data FederationError | -- | No federator endpoint has been set, so no call to federator client can -- be made. FederationNotConfigured + | -- | Federation is disabled for the given protocol + FederationDisabledForProtocol | -- | An error occurred while invoking federator client (see -- 'FederatorClientError' for more details). FederationCallFailure FederatorClientError @@ -188,6 +190,7 @@ instance APIError FederationError where federationErrorToWai :: FederationError -> Wai.Error federationErrorToWai FederationNotImplemented = federationNotImplemented federationErrorToWai FederationNotConfigured = federationNotConfigured +federationErrorToWai FederationDisabledForProtocol = federationDisabledForProtocol federationErrorToWai (FederationCallFailure err) = federationClientErrorToWai err federationErrorToWai (FederationUnexpectedBody s) = federationUnexpectedBody s federationErrorToWai (FederationUnexpectedError t) = federationUnexpectedError t @@ -358,6 +361,13 @@ federationNotConfigured = "federation-not-enabled" "no federator configured" +federationDisabledForProtocol :: Wai.Error +federationDisabledForProtocol = + Wai.mkError + HTTP.status409 + "federation-disabled-for-protocol" + "Federation is disabled for the given protocol" + federationUnavailable :: Text -> Wai.Error federationUnavailable err = Wai.mkError diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 234c1b76037..c76165b1bd0 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -50,6 +50,7 @@ settings: # Once set, DO NOT change it: if you do, existing users may have a broken experience and/or stop working # Remember to keep it the same in Brig federationDomain: example.com + federationProtocols: ["mls", "proteus"] mlsPrivateKeyPaths: removal: ed25519: test/resources/backendA/ed25519.pem diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index e96f060855e..e5e7ff6a79d 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -39,6 +39,7 @@ module Galley.API.Action addLocalUsersToRemoteConv, ConversationUpdate, getFederationStatus, + enforceFederationProtocol, checkFederationStatus, firstConflictOrFullyConnected, ) @@ -121,7 +122,7 @@ import Wire.API.Team.Feature import Wire.API.Team.LegalHold import Wire.API.Team.Member import Wire.API.Team.Permission (Perm (AddRemoveConvMember, ModifyConvName)) -import Wire.API.User qualified as User +import Wire.API.User as User import Wire.NotificationSubsystem data NoChanges = NoChanges @@ -327,6 +328,19 @@ type family HasConversationActionGalleyErrors (tag :: ConversationActionTag) :: ErrorS 'TeamNotFound ] +enforceFederationProtocol :: + ( Member (Error FederationError) r, + Member (Input Opts) r + ) => + ProtocolTag -> + [Remote ()] -> + Sem r () +enforceFederationProtocol proto domains = do + unless (null domains) $ do + mAllowedProtos <- view (settings . federationProtocols) <$> input + unless (maybe True (elem proto) mAllowedProtos) $ + throw FederationDisabledForProtocol + checkFederationStatus :: ( Member (Error UnreachableBackends) r, Member (Error NonFederatingBackends) r, @@ -521,6 +535,7 @@ performConversationJoin qusr lconv (ConversationJoin invited role) = do ensureMemberLimit (convProtocolTag conv) (toList (convLocalMembers conv)) newMembers ensureAccess conv InviteAccess checkLocals lusr (convTeam conv) (ulLocals newMembers) + enforceFederationProtocol (protocolTag conv.convProtocol) (fmap void (ulRemotes newMembers)) checkRemotes lusr (ulRemotes newMembers) checkLHPolicyConflictsLocal (ulLocals newMembers) checkLHPolicyConflictsRemote (FutureWork (ulRemotes newMembers)) diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 4b71e3fcf85..9ab84a07469 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -160,6 +160,7 @@ createGroupConversation :: Sem r CreateGroupConversationResponse createGroupConversation lusr conn newConv = do let remoteDomains = void <$> snd (partitionQualified lusr $ newConv.newConvQualifiedUsers) + enforceFederationProtocol (baseProtocolToProtocol newConv.newConvProtocol) remoteDomains checkFederationStatus (RemoteDomains $ Set.fromList remoteDomains) cnv <- createGroupConversationGeneric diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index be813e6ee3d..1f916761387 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -31,6 +31,7 @@ module Galley.Options concurrentDeletionEvents, deleteConvThrottleMillis, federationDomain, + federationProtocols, mlsPrivateKeyPaths, featureFlags, defConcurrentDeletionEvents, @@ -72,6 +73,7 @@ import Network.AMQP.Extended import System.Logger.Extended (Level, LogFormat) import Util.Options hiding (endpoint) import Util.Options.Common +import Wire.API.Conversation.Protocol import Wire.API.Routes.Version import Wire.API.Team.Member @@ -136,6 +138,7 @@ data Settings = Settings -- - wire.com -- - example.com _federationDomain :: !Domain, + _federationProtocols :: !(Maybe [ProtocolTag]), _mlsPrivateKeyPaths :: !(Maybe MLSPrivateKeyPaths), -- | FUTUREWORK: 'setFeatureFlags' should be renamed to 'setFeatureConfigs' in all types. _featureFlags :: !FeatureFlags, diff --git a/services/galley/src/Galley/Types/UserList.hs b/services/galley/src/Galley/Types/UserList.hs index 071403b5c9d..da00086d6a7 100644 --- a/services/galley/src/Galley/Types/UserList.hs +++ b/services/galley/src/Galley/Types/UserList.hs @@ -34,7 +34,7 @@ data UserList a = UserList { ulLocals :: [a], ulRemotes :: [Remote a] } - deriving (Functor, Foldable, Traversable) + deriving (Show, Functor, Foldable, Traversable) instance Semigroup (UserList a) where UserList locals1 remotes1 <> UserList locals2 remotes2 = From 7c2d1ba3d2f2fe6aeea29cdfaa418950e4a3df9b Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 28 Oct 2024 12:32:39 +0100 Subject: [PATCH 07/27] Fix overlapping paths errors in galley internal (#4313) * Add CHANGELOG entry * Fix one overlapping galley internal path * Remove unqualified internal endpoints * Make internal conversation member query qualified * Fix galley integration tests * Run swagger linter on internal swaggers --- Makefile | 12 ++-- changelog.d/5-internal/fix-galley-overlaps | 1 + integration/test/Test/Swagger.hs | 30 +++++++++- .../src/Wire/API/Routes/Internal/Galley.hs | 33 +---------- .../src/Wire/GalleyAPIAccess/Rpc.hs | 3 +- services/brig/test/integration/Util.hs | 9 ++- services/galley/src/Galley/API/Internal.hs | 2 - services/galley/src/Galley/API/Query.hs | 8 ++- services/galley/test/integration/API.hs | 59 ++++++++++++++++--- 9 files changed, 103 insertions(+), 54 deletions(-) create mode 100644 changelog.d/5-internal/fix-galley-overlaps diff --git a/Makefile b/Makefile index 9f2f6767fc3..77a68d9cc10 100644 --- a/Makefile +++ b/Makefile @@ -608,9 +608,9 @@ upload-bombon: openapi-validate: @echo -e "Make sure you are running the backend in another terminal (make cr)\n" vacuum lint -a -d -e <(curl http://localhost:8082/v7/api/swagger.json) -# vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/cannon-swagger.json) -# vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/cargohold-swagger.json) -# vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/spar-swagger.json) -# vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/gundeck-swagger.json) -# vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/brig-swagger.json) -# vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/galley-swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/cannon-swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/cargohold-swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/spar-swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/gundeck-swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/brig-swagger.json) + vacuum lint -a -d -e <(curl http://localhost:8082/api-internal/swagger-ui/galley-swagger.json) diff --git a/changelog.d/5-internal/fix-galley-overlaps b/changelog.d/5-internal/fix-galley-overlaps new file mode 100644 index 00000000000..784bbe17f2a --- /dev/null +++ b/changelog.d/5-internal/fix-galley-overlaps @@ -0,0 +1 @@ +Fix overlapping paths errors in galley's internal API diff --git a/integration/test/Test/Swagger.hs b/integration/test/Test/Swagger.hs index 514bf532299..2fe53d487bb 100644 --- a/integration/test/Test/Swagger.hs +++ b/integration/test/Test/Swagger.hs @@ -86,6 +86,20 @@ testSwaggerToc = do html :: String html = "

please pick an api version

/v0/api/swagger-ui/
/v1/api/swagger-ui/
/v2/api/swagger-ui/
/v3/api/swagger-ui/
/v4/api/swagger-ui/
/v5/api/swagger-ui/
/v6/api/swagger-ui/
/v7/api/swagger-ui/
" +data Swagger = SwaggerPublic | SwaggerInternal Service + +instance TestCases Swagger where + mkTestCases = + pure + [ MkTestCase "[swagger=ibrig]" (SwaggerInternal Brig), + MkTestCase "[swagger=icannon]" (SwaggerInternal Cannon), + MkTestCase "[swagger=icargohold]" (SwaggerInternal Cargohold), + MkTestCase "[swagger=igalley]" (SwaggerInternal Galley), + MkTestCase "[swagger=igundeck]" (SwaggerInternal Gundeck), + MkTestCase "[swagger=ispar]" (SwaggerInternal Spar), + MkTestCase "[swagger=public]" SwaggerPublic + ] + -- | This runs the swagger linter [vacuum](https://quobix.com/vacuum/). -- -- The reason for adding the linter in the integration tests, and not in the lint job, is that @@ -93,10 +107,20 @@ testSwaggerToc = do -- -- There is also a make rule that does this, for convenience in your develop -- flow. Make sure that brig is running before using the make rule. -testSwaggerLint :: (HasCallStack) => App () -testSwaggerLint = do +testSwaggerLint :: (HasCallStack) => Swagger -> App () +testSwaggerLint sw = do withSystemTempDirectory "swagger" $ \tmp -> do - req <- baseRequest OwnDomain Brig Versioned $ joinHttpPath ["api", "swagger.json"] + req <- case sw of + SwaggerPublic -> + baseRequest OwnDomain Brig Versioned + $ joinHttpPath ["api", "swagger.json"] + (SwaggerInternal service) -> + baseRequest OwnDomain Brig Unversioned + $ joinHttpPath + [ "api-internal", + "swagger-ui", + serviceName service <> "-swagger.json" + ] swagger <- submit "GET" req >>= getBody 200 liftIO $ B.writeFile (tmp "swagger.json") swagger let cmd = shell $ "vacuum lint -a -d -e " <> (tmp "swagger.json") diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index cdb072d749f..bdae90491ca 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -387,7 +387,7 @@ type IConversationAPI = Named "conversation-get-member" ( "conversations" - :> Capture "cnv" ConvId + :> QualifiedCapture "cnv" ConvId :> "members" :> Capture "usr" UserId :> Get '[JSON] (Maybe Member) @@ -408,16 +408,6 @@ type IConversationAPI = :> "v2" :> Put '[JSON] Conversation ) - :<|> Named - "conversation-block-unqualified" - ( CanThrow 'InvalidOperation - :> CanThrow 'ConvNotFound - :> ZUser - :> "conversations" - :> Capture "cnv" ConvId - :> "block" - :> Put '[JSON] () - ) :<|> Named "conversation-block" ( CanThrow 'InvalidOperation @@ -432,21 +422,6 @@ type IConversationAPI = -- - MemberJoin event to you, if the conversation existed and had < 2 members before -- - MemberJoin event to other, if the conversation existed and only the other was member -- before - :<|> Named - "conversation-unblock-unqualified" - ( CanThrow 'InvalidOperation - :> CanThrow 'ConvNotFound - :> ZLocalUser - :> ZOptConn - :> "conversations" - :> Capture "cnv" ConvId - :> "unblock" - :> Put '[JSON] Conversation - ) - -- This endpoint can lead to the following events being sent: - -- - MemberJoin event to you, if the conversation existed and had < 2 members before - -- - MemberJoin event to other, if the conversation existed and only the other was member - -- before :<|> Named "conversation-unblock" ( CanThrow 'InvalidOperation @@ -470,8 +445,7 @@ type IConversationAPI = "conversation-mls-one-to-one" ( CanThrow 'NotConnected :> CanThrow 'MLSNotEnabled - :> "conversations" - :> "mls-one2one" + :> "mls-one2one-conversations" :> ZLocalUser :> QualifiedCapture "user" UserId :> Get '[JSON] Conversation @@ -481,8 +455,7 @@ type IConversationAPI = ( CanThrow 'NotConnected :> CanThrow 'MLSNotEnabled :> ZLocalUser - :> "conversations" - :> "mls-one2one" + :> "mls-one2one-conversations" :> QualifiedCapture "user" UserId :> "established" :> Get '[JSON] Bool diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index f7ee1c47f65..ba9538f12de 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -575,8 +575,7 @@ checkMLSOne2OneEstablished self (Qualified other otherDomain) = do method GET . paths [ "i", - "conversations", - "mls-one2one", + "mls-one2one-conversations", toByteString' otherDomain, toByteString' other, "established" diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 8c39a9a7e4c..18f9eef6912 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -748,7 +748,14 @@ isMember g usr cnv = do res <- get $ g - . paths ["i", "conversations", toByteString' cnv, "members", toByteString' (tUnqualified usr)] + . paths + [ "i", + "conversations", + toByteString' (tDomain usr), + toByteString' cnv, + "members", + toByteString' (tUnqualified usr) + ] . expect2xx case responseJsonMaybe res of Nothing -> pure False diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 2e273aef847..411ad8fe295 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -174,9 +174,7 @@ conversationAPI :: API IConversationAPI GalleyEffects conversationAPI = mkNamedAPI @"conversation-get-member" Query.internalGetMember <@> mkNamedAPI @"conversation-accept-v2" Update.acceptConv - <@> mkNamedAPI @"conversation-block-unqualified" Update.blockConvUnqualified <@> mkNamedAPI @"conversation-block" Update.blockConv - <@> mkNamedAPI @"conversation-unblock-unqualified" Update.unblockConvUnqualified <@> mkNamedAPI @"conversation-unblock" Update.unblockConv <@> mkNamedAPI @"conversation-meta" Query.getConversationMeta <@> mkNamedAPI @"conversation-mls-one-to-one" Query.getMLSOne2OneConversationInternal diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index a0edf6be781..47fb3365508 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -590,15 +590,17 @@ iterateConversations luid pageSize handleConvs = go Nothing internalGetMember :: ( Member ConversationStore r, + Member (Error FederationError) r, Member (Input (Local ())) r, Member MemberStore r ) => - ConvId -> + Qualified ConvId -> UserId -> Sem r (Maybe Public.Member) -internalGetMember cnv usr = do +internalGetMember qcnv usr = do lusr <- qualifyLocal usr - getLocalSelf lusr cnv + lcnv <- ensureLocal lusr qcnv + getLocalSelf lusr (tUnqualified lcnv) getLocalSelf :: ( Member ConversationStore r, diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 2ddc71bce90..fab0d096d8b 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -2067,10 +2067,19 @@ postRepeatConnectConvCancel = do where cancel u c = do g <- viewGalley - let cnvId = qUnqualified . cnvQualifiedId - put (g . paths ["/i/conversations", toByteString' (cnvId c), "block"] . zUser u) + let qConvId = cnvQualifiedId c + put + ( g + . paths + [ "/i/conversations", + toByteString' (qDomain qConvId), + toByteString' (qUnqualified qConvId), + "block" + ] + . zUser u + ) !!! const 200 === statusCode - getConv u (cnvId c) !!! const 403 === statusCode + getConv u (qUnqualified qConvId) !!! const 403 === statusCode putBlockConvOk :: TestM () putBlockConvOk = do @@ -2082,23 +2091,59 @@ putBlockConvOk = do let convId = qUnqualified qconvId getConvQualified alice qconvId !!! const 200 === statusCode getConvQualified bob qconvId !!! const 403 === statusCode - put (g . paths ["/i/conversations", toByteString' convId, "block"] . zUser bob) + put + ( g + . paths + [ "/i/conversations", + toByteString' (qDomain qconvId), + toByteString' convId, + "block" + ] + . zUser bob + ) !!! const 200 === statusCode -- A is still the only member of the 1-1 getConvQualified alice qconvId !!! do const 200 === statusCode const (cnvMembers conv) === cnvMembers . responseJsonUnsafeWithMsg "conversation" -- B accepts the conversation by unblocking - put (g . paths ["/i/conversations", toByteString' convId, "unblock"] . zUser bob) + put + ( g + . paths + [ "/i/conversations", + toByteString' (qDomain qconvId), + toByteString' convId, + "unblock" + ] + . zUser bob + ) !!! const 200 === statusCode getConvQualified bob qconvId !!! const 200 === statusCode -- B blocks A in the 1-1 - put (g . paths ["/i/conversations", toByteString' convId, "block"] . zUser bob) + put + ( g + . paths + [ "/i/conversations", + toByteString' (qDomain qconvId), + toByteString' convId, + "block" + ] + . zUser bob + ) !!! const 200 === statusCode -- B no longer sees the 1-1 getConvQualified bob qconvId !!! const 403 === statusCode -- B unblocks A in the 1-1 - put (g . paths ["/i/conversations", toByteString' convId, "unblock"] . zUser bob) + put + ( g + . paths + [ "/i/conversations", + toByteString' (qDomain qconvId), + toByteString' convId, + "unblock" + ] + . zUser bob + ) !!! const 200 === statusCode -- B sees the blocked 1-1 again getConvQualified bob qconvId !!! do From ff6e544b5323ed5e4c015f562de6f74e1984bb78 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 30 Oct 2024 10:31:54 +0100 Subject: [PATCH 08/27] [WPB-11925] fix add-bot (#4318) * Haddocks. * Re-add accidentally removed add-bot@v6 route in nginz. Fixes #4302 (99acd4c6916ff968a68a62363eadf954eccac742) --- changelog.d/3-bug-fixes/WPB-11925-fix-add-bot | 1 + charts/nginz/values.yaml | 3 +++ libs/wire-api/src/Wire/API/VersionInfo.hs | 2 ++ 3 files changed, 6 insertions(+) create mode 100644 changelog.d/3-bug-fixes/WPB-11925-fix-add-bot diff --git a/changelog.d/3-bug-fixes/WPB-11925-fix-add-bot b/changelog.d/3-bug-fixes/WPB-11925-fix-add-bot new file mode 100644 index 00000000000..9b9185ada76 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-11925-fix-add-bot @@ -0,0 +1 @@ +Re-add accidentally removed add-bot@v6 route in nginz, fixes #4302 diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index c9d97e90ff6..b853e1b2cde 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -295,6 +295,9 @@ nginx_conf: - path: /bot/users envs: - all + - path: /conversations/([^/]*)/bots + envs: + - all - path: /invitations/info envs: - all diff --git a/libs/wire-api/src/Wire/API/VersionInfo.hs b/libs/wire-api/src/Wire/API/VersionInfo.hs index 590dd7b380d..0978a935d68 100644 --- a/libs/wire-api/src/Wire/API/VersionInfo.hs +++ b/libs/wire-api/src/Wire/API/VersionInfo.hs @@ -57,8 +57,10 @@ versionHeader = CI.mk . B8.pack $ symbolVal (Proxy @VersionHeader) -------------------------------------------------------------------------------- -- Servant combinators +-- | Exclusive range ('Until V5' means '[.. V4]') data Until v +-- | Inclusive range ('From V5' means '[V5 ..]') data From v instance From 759eb92fe289b78d10c1178191a299c35f09deac Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 30 Oct 2024 10:38:44 +0100 Subject: [PATCH 09/27] WPB-11183 Remove wrong templates and make email sending no op (#4315) --- changelog.d/2-features/WPB-10658 | 2 +- .../src/Wire/EmailSubsystem.hs | 1 - .../src/Wire/EmailSubsystem/Interpreter.hs | 37 ---------------- .../src/Wire/EmailSubsystem/Template.hs | 31 +++++++++----- .../Wire/MockInterpreters/EmailSubsystem.hs | 1 - .../en/user/email/upgrade-subject.txt | 1 - .../brig/templates/en/user/email/upgrade.html | 1 - .../brig/templates/en/user/email/upgrade.txt | 22 ---------- services/brig/src/Brig/API/Public.hs | 1 - services/brig/src/Brig/API/User.hs | 13 +++--- services/brig/src/Brig/Team/Email.hs | 42 ++++++++----------- services/brig/src/Brig/Team/Template.hs | 16 +++++++ services/brig/src/Brig/User/Template.hs | 8 ---- 13 files changed, 61 insertions(+), 115 deletions(-) delete mode 100644 services/brig/deb/opt/brig/templates/en/user/email/upgrade-subject.txt delete mode 100644 services/brig/deb/opt/brig/templates/en/user/email/upgrade.html delete mode 100644 services/brig/deb/opt/brig/templates/en/user/email/upgrade.txt diff --git a/changelog.d/2-features/WPB-10658 b/changelog.d/2-features/WPB-10658 index b8eff34863d..47da7fbabb8 100644 --- a/changelog.d/2-features/WPB-10658 +++ b/changelog.d/2-features/WPB-10658 @@ -1 +1 @@ -Allow an existing non-team user to migrate to a team (#4229, ##) +Allow an existing non-team user to migrate to a team (#4229, #4268, #4315) diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs index a8fa0f57b5e..f397bce9441 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs @@ -22,7 +22,6 @@ data EmailSubsystem m a where SendAccountDeletionEmail :: EmailAddress -> Name -> Code.Key -> Code.Value -> Locale -> EmailSubsystem m () SendTeamActivationMail :: EmailAddress -> Name -> ActivationKey -> ActivationCode -> Maybe Locale -> Text -> EmailSubsystem m () SendTeamDeletionVerificationMail :: EmailAddress -> Code.Value -> Maybe Locale -> EmailSubsystem m () - SendUpgradePersonalToTeamConfirmationEmail :: EmailAddress -> Name -> Text -> Locale -> EmailSubsystem m () -- | send invitation to an unknown email address. SendTeamInvitationMail :: EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> EmailSubsystem m Text -- | send invitation to an email address associated with a personal user account. diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs index f3451750e47..ef03dadc7db 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs @@ -37,7 +37,6 @@ emailSubsystemInterpreter userTpls teamTpls branding = interpret \case SendTeamActivationMail email name key code mLocale teamName -> sendTeamActivationMailImpl userTpls branding email name key code mLocale teamName SendNewClientEmail email name client locale -> sendNewClientEmailImpl userTpls branding email name client locale SendAccountDeletionEmail email name key code locale -> sendAccountDeletionEmailImpl userTpls branding email name key code locale - SendUpgradePersonalToTeamConfirmationEmail email name teamName locale -> sendUpgradePersonalToTeamConfirmationEmailImpl userTpls branding email name teamName locale SendTeamInvitationMail email tid from code loc -> sendTeamInvitationMailImpl teamTpls branding email tid from code loc SendTeamInvitationMailPersonalUser email tid from code loc -> sendTeamInvitationMailPersonalUserImpl teamTpls branding email tid from code loc @@ -399,42 +398,6 @@ renderDeletionEmail email name cKey cValue DeletionEmailTemplate {..} branding = replace2 "code" = code replace2 x = x --------------------------------------------------------------------------------- --- Upgrade personal user to team owner confirmation email - -sendUpgradePersonalToTeamConfirmationEmailImpl :: - (Member EmailSending r) => - Localised UserTemplates -> - TemplateBranding -> - EmailAddress -> - Name -> - Text -> - Locale -> - Sem r () -sendUpgradePersonalToTeamConfirmationEmailImpl userTemplates branding email name teamName locale = do - let tpl = upgradePersonalToTeamEmail . snd $ forLocale (Just locale) userTemplates - sendMail $ renderUpgradePersonalToTeamConfirmationEmail email name teamName tpl branding - -renderUpgradePersonalToTeamConfirmationEmail :: EmailAddress -> Name -> Text -> UpgradePersonalToTeamEmailTemplate -> TemplateBranding -> Mail -renderUpgradePersonalToTeamConfirmationEmail email name _teamName UpgradePersonalToTeamEmailTemplate {..} branding = - (emptyMail from) - { mailTo = [to], - mailHeaders = - [ ("Subject", toStrict subj), - ("X-Zeta-Purpose", "Upgrade") - ], - mailParts = [[plainPart txt, htmlPart html]] - } - where - from = Address (Just upgradePersonalToTeamEmailSenderName) (fromEmail upgradePersonalToTeamEmailSender) - to = mkMimeAddress name email - txt = renderTextWithBranding upgradePersonalToTeamEmailBodyText replace1 branding - html = renderHtmlWithBranding upgradePersonalToTeamEmailBodyHtml replace1 branding - subj = renderTextWithBranding upgradePersonalToTeamEmailSubject replace1 branding - replace1 "email" = fromEmail email - replace1 "name" = fromName name - replace1 x = x - ------------------------------------------------------------------------------- -- Invitation Email diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs index ea0339f74ca..ca79185fccc 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs @@ -85,7 +85,6 @@ data UserTemplates = UserTemplates loginCall :: LoginCallTemplate, deletionSms :: DeletionSmsTemplate, deletionEmail :: DeletionEmailTemplate, - upgradePersonalToTeamEmail :: UpgradePersonalToTeamEmailTemplate, newClientEmail :: NewClientEmailTemplate, verificationLoginEmail :: SecondFactorVerificationEmailTemplate, verificationScimTokenEmail :: SecondFactorVerificationEmailTemplate, @@ -138,14 +137,6 @@ data DeletionEmailTemplate = DeletionEmailTemplate deletionEmailSenderName :: Text } -data UpgradePersonalToTeamEmailTemplate = UpgradePersonalToTeamEmailTemplate - { upgradePersonalToTeamEmailSubject :: Template, - upgradePersonalToTeamEmailBodyText :: Template, - upgradePersonalToTeamEmailBodyHtml :: Template, - upgradePersonalToTeamEmailSender :: EmailAddress, - upgradePersonalToTeamEmailSenderName :: Text - } - data PasswordResetEmailTemplate = PasswordResetEmailTemplate { passwordResetEmailUrl :: Template, passwordResetEmailSubject :: Template, @@ -219,9 +210,29 @@ data MemberWelcomeEmailTemplate = MemberWelcomeEmailTemplate memberWelcomeEmailSenderName :: !Text } +data PersonalUserMemberWelcomeEmailTemplate = PersonalUserMemberWelcomeEmailTemplate + { personalUserMemberWelcomeEmailUrl :: !Text, + personalUserMemberWelcomeEmailSubject :: !Template, + personalUserMemberWelcomeEmailBodyText :: !Template, + personalUserMemberWelcomeEmailBodyHtml :: !Template, + personalUserMemberWelcomeEmailSender :: !EmailAddress, + personalUserMemberWelcomeEmailSenderName :: !Text + } + +data PersonalUserCreatorWelcomeEmailTemplate = PersonalUserCreatorWelcomeEmailTemplate + { personalUserCreatorWelcomeEmailUrl :: !Text, + personalUserCreatorWelcomeEmailSubject :: !Template, + personalUserCreatorWelcomeEmailBodyText :: !Template, + personalUserCreatorWelcomeEmailBodyHtml :: !Template, + personalUserCreatorWelcomeEmailSender :: !EmailAddress, + personalUserCreatorWelcomeEmailSenderName :: !Text + } + data TeamTemplates = TeamTemplates { invitationEmail :: !InvitationEmailTemplate, existingUserInvitationEmail :: !InvitationEmailTemplate, creatorWelcomeEmail :: !CreatorWelcomeEmailTemplate, - memberWelcomeEmail :: !MemberWelcomeEmailTemplate + memberWelcomeEmail :: !MemberWelcomeEmailTemplate, + personalUserMemberWelcomeEmail :: !PersonalUserMemberWelcomeEmailTemplate, + personalUserCreatorWelcomeEmail :: !PersonalUserCreatorWelcomeEmailTemplate } diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs index 636027753cd..ee8125d758e 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs @@ -36,6 +36,5 @@ noopEmailSubsystemInterpreter = interpret \case SendAccountDeletionEmail {} -> pure () SendTeamActivationMail {} -> pure () SendTeamDeletionVerificationMail {} -> pure () - SendUpgradePersonalToTeamConfirmationEmail {} -> pure () SendTeamInvitationMail {} -> pure "" SendTeamInvitationMailPersonalUser {} -> pure "" diff --git a/services/brig/deb/opt/brig/templates/en/user/email/upgrade-subject.txt b/services/brig/deb/opt/brig/templates/en/user/email/upgrade-subject.txt deleted file mode 100644 index 8a92b9f9a36..00000000000 --- a/services/brig/deb/opt/brig/templates/en/user/email/upgrade-subject.txt +++ /dev/null @@ -1 +0,0 @@ -Delete account? \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/upgrade.html b/services/brig/deb/opt/brig/templates/en/user/email/upgrade.html deleted file mode 100644 index 690b0104fdd..00000000000 --- a/services/brig/deb/opt/brig/templates/en/user/email/upgrade.html +++ /dev/null @@ -1 +0,0 @@ -Delete account?

${brand_label_url}

Delete your account

We’ve received a request to delete your ${brand} account. Click the button below within 10 minutes to delete all your conversations, content and connections.

 
Delete account
 

If you can’t click the button, copy and paste this link to your browser:

${url}

If you didn’t request this, reset your password.

If you have any questions, please contact us.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/user/email/upgrade.txt b/services/brig/deb/opt/brig/templates/en/user/email/upgrade.txt deleted file mode 100644 index 744da7dc05c..00000000000 --- a/services/brig/deb/opt/brig/templates/en/user/email/upgrade.txt +++ /dev/null @@ -1,22 +0,0 @@ -[${brand_logo}] - -${brand_label_url} [${brand_url}] - -DELETE YOUR ACCOUNT -We’ve received a request to delete your ${brand} account. Click the button below -within 10 minutes to delete all your conversations, content and connections. - -Delete account [${url}]If you can’t click the button, copy and paste this link -to your browser: - -${url} - -If you didn’t request this, reset your password [${forgot}]. - -If you have any questions, please contact us [${support}]. - - --------------------------------------------------------------------------------- - -Privacy policy and terms of use [${legal}] · Report Misuse [${misuse}] -${copyright}. ALL RIGHTS RESERVED. \ No newline at end of file diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index aa83309f397..42d0b5c2a7a 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -726,7 +726,6 @@ createAccessToken method luid cid proof = do upgradePersonalToTeam :: ( Member (ConnectionStore InternalPaging) r, Member (Embed HttpClientIO) r, - Member EmailSubsystem r, Member GalleyAPIAccess r, Member (Input (Local ())) r, Member (Input UTCTime) r, diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index e081822b271..46c3e658e30 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -83,6 +83,7 @@ import Brig.Effects.UserPendingActivationStore (UserPendingActivation (..), User import Brig.Effects.UserPendingActivationStore qualified as UserPendingActivationStore import Brig.IO.Intra qualified as Intra import Brig.Options hiding (internalEvents) +import Brig.Team.Email import Brig.Types.Activation (ActivationPair) import Brig.Types.Intra import Brig.User.Auth.Cookie qualified as Auth @@ -258,7 +259,6 @@ createUserSpar new = do upgradePersonalToTeam :: forall r. ( Member GalleyAPIAccess r, - Member EmailSubsystem r, Member UserStore r, Member UserSubsystem r, Member TinyLog r, @@ -298,12 +298,11 @@ upgradePersonalToTeam luid bNewTeam = do -- send confirmation email for_ (userEmail user) $ \email -> do - liftSem $ - sendUpgradePersonalToTeamConfirmationEmail - email - user.userDisplayName - bNewTeam.bnuTeam.newTeamName.fromRange - user.userLocale + sendPersonalUserCreatorWelcomeMail + email + tid + bNewTeam.bnuTeam.newTeamName.fromRange + (Just user.userLocale) pure $! createUserTeam diff --git a/services/brig/src/Brig/Team/Email.hs b/services/brig/src/Brig/Team/Email.hs index e6d0cdaeb7f..441cee5d7bf 100644 --- a/services/brig/src/Brig/Team/Email.hs +++ b/services/brig/src/Brig/Team/Email.hs @@ -18,9 +18,9 @@ -- with this program. If not, see . module Brig.Team.Email - ( CreatorWelcomeEmail (..), - MemberWelcomeEmail (..), - sendMemberWelcomeMail, + ( sendMemberWelcomeMail, + sendPersonalUserMemberWelcomeMail, + sendPersonalUserCreatorWelcomeMail, ) where @@ -33,35 +33,27 @@ import Network.Mail.Mime import Polysemy import Wire.API.User import Wire.EmailSending -import Wire.EmailSubsystem.Template (TemplateBranding, renderHtmlWithBranding, renderTextWithBranding) +import Wire.EmailSubsystem.Template sendMemberWelcomeMail :: (Member EmailSending r) => EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () sendMemberWelcomeMail to tid teamName loc = do tpl <- memberWelcomeEmail . snd <$> teamTemplatesWithLocale loc branding <- asks (.templateBranding) - let mail = MemberWelcomeEmail to tid teamName - liftSem $ sendMail $ renderMemberWelcomeMail mail tpl branding + liftSem $ sendMail $ renderMemberWelcomeMail to tid teamName tpl branding -------------------------------------------------------------------------------- --- Creator Welcome Email +sendPersonalUserMemberWelcomeMail :: EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () +sendPersonalUserMemberWelcomeMail _ _ _ _ = do + pure () -data CreatorWelcomeEmail = CreatorWelcomeEmail - { cwTo :: !EmailAddress, - cwTid :: !TeamId, - cwTeamName :: !Text - } +sendPersonalUserCreatorWelcomeMail :: EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () +sendPersonalUserCreatorWelcomeMail _ _ _ _ = do + pure () ------------------------------------------------------------------------------- -- Member Welcome Email -data MemberWelcomeEmail = MemberWelcomeEmail - { mwTo :: !EmailAddress, - mwTid :: !TeamId, - mwTeamName :: !Text - } - -renderMemberWelcomeMail :: MemberWelcomeEmail -> MemberWelcomeEmailTemplate -> TemplateBranding -> Mail -renderMemberWelcomeMail MemberWelcomeEmail {..} MemberWelcomeEmailTemplate {..} branding = +renderMemberWelcomeMail :: EmailAddress -> TeamId -> Text -> MemberWelcomeEmailTemplate -> TemplateBranding -> Mail +renderMemberWelcomeMail emailTo tid teamName MemberWelcomeEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], mailHeaders = @@ -72,12 +64,12 @@ renderMemberWelcomeMail MemberWelcomeEmail {..} MemberWelcomeEmailTemplate {..} } where from = Address (Just memberWelcomeEmailSenderName) (fromEmail memberWelcomeEmailSender) - to = Address Nothing (fromEmail mwTo) + to = Address Nothing (fromEmail emailTo) txt = renderTextWithBranding memberWelcomeEmailBodyText replace branding html = renderHtmlWithBranding memberWelcomeEmailBodyHtml replace branding subj = renderTextWithBranding memberWelcomeEmailSubject replace branding replace "url" = memberWelcomeEmailUrl - replace "email" = fromEmail mwTo - replace "team_id" = idToText mwTid - replace "team_name" = mwTeamName + replace "email" = fromEmail emailTo + replace "team_id" = idToText tid + replace "team_name" = teamName replace x = x diff --git a/services/brig/src/Brig/Team/Template.hs b/services/brig/src/Brig/Team/Template.hs index a63cba25fb0..86c409e9f62 100644 --- a/services/brig/src/Brig/Team/Template.hs +++ b/services/brig/src/Brig/Team/Template.hs @@ -63,6 +63,22 @@ loadTeamTemplates o = readLocalesDir defLocale (templateDir gOptions) "team" $ \ <*> pure (emailSender gOptions) <*> readText fp "email/sender.txt" ) + <*> ( PersonalUserMemberWelcomeEmailTemplate + "" + (template "") + (template "") + (template "") + (emailSender gOptions) + <$> readText fp "email/sender.txt" + ) + <*> ( PersonalUserCreatorWelcomeEmailTemplate + "" + (template "") + (template "") + (template "") + (emailSender gOptions) + <$> readText fp "email/sender.txt" + ) where gOptions = o.emailSMS.general tOptions = o.emailSMS.team diff --git a/services/brig/src/Brig/User/Template.hs b/services/brig/src/Brig/User/Template.hs index 36436027782..110d979bc80 100644 --- a/services/brig/src/Brig/User/Template.hs +++ b/services/brig/src/Brig/User/Template.hs @@ -28,7 +28,6 @@ module Brig.User.Template LoginCallTemplate (..), DeletionSmsTemplate (..), DeletionEmailTemplate (..), - UpgradePersonalToTeamEmailTemplate (..), NewClientEmailTemplate (..), SecondFactorVerificationEmailTemplate (..), loadUserTemplates, @@ -110,13 +109,6 @@ loadUserTemplates o = readLocalesDir defLocale templateDir "user" $ \fp -> <*> pure emailSender <*> readText fp "email/sender.txt" ) - <*> ( UpgradePersonalToTeamEmailTemplate - <$> readTemplate fp "email/upgrade-subject.txt" - <*> readTemplate fp "email/upgrade.txt" - <*> readTemplate fp "email/upgrade.html" - <*> pure emailSender - <*> readText fp "email/sender.txt" - ) <*> ( NewClientEmailTemplate <$> readTemplate fp "email/new-client-subject.txt" <*> readTemplate fp "email/new-client.txt" From 6f75269c3ffb08fcb306db87d1d78c64529e7067 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 30 Oct 2024 15:11:01 +0100 Subject: [PATCH 10/27] Move docs from docs.wire.com to generated helper page served by brig. (#4311) Co-authored-by: Leif Battermann --- changelog.d/4-docs/fix-swagger-2 | 1 + .../api-client-perspective/swagger.md | 88 +++---------------- integration/test/Test/Swagger.hs | 2 +- .../src/Wire/API/Federation/Version.hs | 9 ++ services/brig/src/Brig/API/Public.hs | 76 +++++++++++++--- 5 files changed, 89 insertions(+), 87 deletions(-) create mode 100644 changelog.d/4-docs/fix-swagger-2 diff --git a/changelog.d/4-docs/fix-swagger-2 b/changelog.d/4-docs/fix-swagger-2 new file mode 100644 index 00000000000..118fc6d712e --- /dev/null +++ b/changelog.d/4-docs/fix-swagger-2 @@ -0,0 +1 @@ +Move docs from docs.wire.com to generated helper page served by brig \ No newline at end of file diff --git a/docs/src/understand/api-client-perspective/swagger.md b/docs/src/understand/api-client-perspective/swagger.md index 773a89fe071..a188dd166dc 100644 --- a/docs/src/understand/api-client-perspective/swagger.md +++ b/docs/src/understand/api-client-perspective/swagger.md @@ -1,37 +1,27 @@ (swagger-api-docs)= -# Swagger API documentation +# Swagger / OpenAPI documentation -Our staging system provides [Swagger / -OpenAPI](https://swagger.io/resources/open-api/) documentation of our HTTP REST -API. +Our staging system provides [OpenAPI +3.0](https://swagger.io/resources/open-api/) documentation of our HTTP +REST API under the following URL: -The swagger docs are correct by construction (compiled from the server -code), and they are complete up to bots/services and event notification -payloads (as of 2023-01-16). +[https://staging-nginz-https.zinfra.io/api/swagger-ui](https://staging-nginz-https.zinfra.io/api/swagger-ui) -There are several ways to interpret this kind of documentation: +There are several ways to interpret this documentation: - Read it as a reference - Generate client code from it - Interactively explore the API by making requests -## Swagger docs (Swagger 2.0) +To find the source code of end-points mentioned in the API, a *route +internal ID* (field `operationId` in openapi) is provided for every +end-point. See {ref}`named-and-internal-route-ids` for details and +usage. -The [Swagger / OpenAPI 2.0](https://swagger.io/specification/v2/) -documentation for endpoints depends on the API version. For a list of -all swagger docs for all supported API versions, [visit -https://staging-nginz-https.zinfra.io/api/swagger-ui](https://staging-nginz-https.zinfra.io/api/swagger-ui). +If you find anything you don't like or understand, please let us know! -To learn which versions are supported, look at -`https:///api-version`. ([See -also.](../../developer/developer/api-versioning.md)) - -If you want to get the raw json for the swagger (ie., for compiling it -into client code in typescript, kotlin, swift, ...), replace -`swagger-ui` with `swagger.json` in the above URL pattern. - -#### Example: doing it manually +## Example To get the versions a backend (`staging-nginz-https.zinfra.io` in this case) supports, execute: @@ -43,57 +33,3 @@ curl https:///api-version The URL to open in your browser for the development version `4` is `https:///v4/api/swagger-ui/`. - -### On-prem and test instances, versioning - -The above is valid for the official wire.com staging environment and -includes both all released API versions and the current development -version, which changes continuously until released. - -If you talk to any other backend, the development version may differ. -Try to ask the backend you're talking if it exposes its docs itself: - -``` -curl https://nginz-https..example.com//api/swagger-ui/ -curl https://nginz-https..example.com//api/swagger.json -``` - -### Internal endpoints - -Swagger docs for internal endpoints are served per service. I.e. there's one for -`brig`, one for `cannon`, etc.. This is because Swagger doesn't play well with -multiple actions having the same combination of HTTP method and URL path. - -Internal APIs are not under version control. - -- Unversioned: - - [`brig` - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig) - - [`cannon` - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/cannon) - - [`cargohold` - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/cargohold) - - [`galley` - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/galley) - - [`gundeck` - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/gundeck) - - [`spar` - **internal** (private) - endpoints](https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/spar) - -The URL pattern is similar to that of public endpoints for latest version: -`https:///api-internal/swagger-ui/`. - -If you want to get the raw json of the swagger: -`https:///api-internal/swagger-ui/-swagger.json`. - -### Federation API - -- Unversioned - - [`brig` - Federation API](https://staging-nginz-https.zinfra.io/api-federation/swagger-ui/brig) - - [`galley` - Federation API](https://staging-nginz-https.zinfra.io/api-federation/swagger-ui/galley) - - [`cargohold` - Federation API](https://staging-nginz-https.zinfra.io/api-federation/swagger-ui/cargohold) - -### Finding the source code for an end-point - -A *route internal ID* is provided for every end-point. See -{ref}`named-and-internal-route-ids` for details and usage. diff --git a/integration/test/Test/Swagger.hs b/integration/test/Test/Swagger.hs index 2fe53d487bb..571bd1ab245 100644 --- a/integration/test/Test/Swagger.hs +++ b/integration/test/Test/Swagger.hs @@ -84,7 +84,7 @@ testSwaggerToc = do get path = rawBaseRequest OwnDomain Brig Unversioned path >>= submit "GET" html :: String - html = "

please pick an api version

/v0/api/swagger-ui/
/v1/api/swagger-ui/
/v2/api/swagger-ui/
/v3/api/swagger-ui/
/v4/api/swagger-ui/
/v5/api/swagger-ui/
/v6/api/swagger-ui/
/v7/api/swagger-ui/
" + html = "

OpenAPI 3.0 docs for all Wire APIs

\n

This wire-server system provides OpenAPI 3.0 documentation of our HTTP REST API.

The openapi docs are correct by construction (compiled from the server code), and more or less complete.

Some endpoints are version-controlled. Show all supported versions. find out more.\n

Public (all available versions)

\nv0: \nswagger-ui; \nswagger.json\n
\nv1: \nswagger-ui; \nswagger.json\n
\nv2: \nswagger-ui; \nswagger.json\n
\nv3: \nswagger-ui; \nswagger.json\n
\nv4: \nswagger-ui; \nswagger.json\n
\nv5: \nswagger-ui; \nswagger.json\n
\nv6: \nswagger-ui; \nswagger.json\n
\nv7: \nswagger-ui; \nswagger.json\n
\n\n

Internal (not versioned)

\n

Openapi docs for internal endpoints are served per service. I.e. there's one for `brig`, one for `cannon`, etc.. This is because Openapi doesn't play well with multiple actions having the same combination of HTTP method and URL path.

\nbrig:
\nswagger-ui; \nswagger.json\n
\ngalley:
\nswagger-ui; \nswagger.json\n
\nspar:
\nswagger-ui; \nswagger.json\n
\ncargohold:
\nswagger-ui; \nswagger.json\n
\ngundeck:
\nswagger-ui; \nswagger.json\n
\ncannon:
\nswagger-ui; \nswagger.json\n
\nproxy:
\nswagger-ui; \nswagger.json\n
\n\n

Federated API (backend-to-backend)

\nbrig (v0):
swagger-ui; swagger.json
brig (v1):
swagger-ui; swagger.json
brig (v2):
swagger-ui; swagger.json

\ngalley (v0):
swagger-ui; swagger.json
galley (v1):
swagger-ui; swagger.json
galley (v2):
swagger-ui; swagger.json

\ncargohold (v0):
swagger-ui; swagger.json
cargohold (v1):
swagger-ui; swagger.json
cargohold (v2):
swagger-ui; swagger.json

\n\n\n" data Swagger = SwaggerPublic | SwaggerInternal Service diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs index e3c76c36735..a7347fbeb5a 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs @@ -42,17 +42,23 @@ where import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.ByteString.Char8 qualified as BS import Data.OpenApi qualified as S import Data.Schema import Data.Set qualified as Set import Data.Singletons.Base.TH import Data.Text qualified as Text import Imports +import Servant.API (ToHttpApiData (..)) data Version = V0 | V1 | V2 deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (FromJSON, ToJSON) via (Schema Version) +instance ToHttpApiData Version where + toHeader = versionByteString + toUrlPiece = versionText + versionInt :: Version -> Int versionInt V0 = 0 versionInt V1 = 1 @@ -61,6 +67,9 @@ versionInt V2 = 2 versionText :: Version -> Text versionText = ("v" <>) . Text.pack . show . versionInt +versionByteString :: Version -> ByteString +versionByteString = ("v" <>) . BS.pack . show . versionInt + intToVersion :: Int -> Maybe Version intToVersion intV = find (\v -> versionInt v == intV) [minBound ..] diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 42d0b5c2a7a..6580a413959 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -106,6 +106,7 @@ import Wire.API.Federation.API.Brig qualified as BrigFederationAPI import Wire.API.Federation.API.Cargohold qualified as CargoholdFederationAPI import Wire.API.Federation.API.Galley qualified as GalleyFederationAPI import Wire.API.Federation.Error +import Wire.API.Federation.Version qualified as Fed import Wire.API.Properties qualified as Public import Wire.API.Routes.API import Wire.API.Routes.Internal.Brig qualified as BrigInternalAPI @@ -244,17 +245,72 @@ versionedSwaggerDocsAPI Nothing = allroutes (throwError listAllVersionsResp) listAllVersionsHTML :: LByteString listAllVersionsHTML = - "

please pick an api version

" - <> mconcat - [ let url = "/" <> toQueryParam v <> "/api/swagger-ui/" - in " (fromStrict . Text.encodeUtf8 $ url) - <> "\">" - <> (fromStrict . Text.encodeUtf8 $ url) - <> "
" - | v <- [minBound :: Version ..] + LBS.unlines $ + [ "

OpenAPI 3.0 docs for all Wire APIs

", + intro, + LBS.unlines public, + LBS.unlines internal, + LBS.unlines federated, + "" + ] + where + intro = + "

This wire-server system provides OpenAPI 3.0 \ + \documentation of our HTTP REST API.

\ + \

The openapi docs are correct by construction (compiled from the server code), and more or less \ + \complete.

\ + \

Some endpoints are version-controlled. Show all supported versions. \ + \find out more." + + public :: [LByteString] + public = + ["

Public (all available versions)

"] + <> mconcat + [ [ v <> ": ", + renderLink "swagger-ui" ("/" <> v <> "/api/swagger-ui") <> "; ", + renderLink "swagger.json" ("/" <> v <> "/api/swagger.json"), + "
" + ] + | v <- versionToLByteString <$> [minBound :: Version ..] + ] + + internal :: [LByteString] + internal = + [ "

Internal (not versioned)

", + "

Openapi docs for internal endpoints are served per service. I.e. there's one for `brig`, one for `cannon`, \ + \etc.. This is because Openapi doesn't play well with multiple actions having the same combination of HTTP \ + \method and URL path.

" ] - <> "" + <> mconcat + [ [ s <> ":
", + renderLink "swagger-ui" ("/api-internal/swagger-ui/" <> s) <> "; ", + renderLink "swagger.json" ("/api-internal/swagger-ui/" <> s <> "-swagger.json"), + "
" + ] + | s <- ["brig", "galley", "spar", "cargohold", "gundeck", "cannon", "proxy"] + ] + + federated :: [LByteString] + federated = + ["

Federated API (backend-to-backend)

"] + <> [ mconcat + [ mconcat + [ s <> " (" <> v <> "):
", + renderLink "swagger-ui" ("/" <> v <> "/api-federation/swagger-ui/" <> s) <> "; ", + renderLink "swagger.json" ("/" <> v <> "/api-federation/swagger-ui/" <> s <> "-swagger.json"), + "
" + ] + | v <- versionToLByteString <$> [minBound :: Fed.Version ..] + ] + <> "
" + | s <- ["brig", "galley", "cargohold"] + ] + + versionToLByteString :: (ToHttpApiData v) => v -> LByteString + versionToLByteString = fromStrict . Text.encodeUtf8 . toQueryParam + + renderLink :: LByteString -> LByteString -> LByteString + renderLink caption url = " url <> "\">" <> caption <> "" -- | Serves Swagger docs for internal endpoints. internalEndpointsSwaggerDocsAPI :: From d3155976672edb2f554de8c24394b7a509a2a75d Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Wed, 30 Oct 2024 16:02:34 +0100 Subject: [PATCH 11/27] Allow choosing between argon2id and scrypt as hashing algorithm (#4319) * Allow chosing between argon2id and scrypt as hashing algorithm The helm charts default to scrypt. * Update changelogs * Update docs, also add migration strategy to release notes * integration: Add test to make sure passwords keep working across hashing algorithm changes Co-authored-by: Matthias Fischmann --- .../0-release-notes/configurable-argon | 19 ++++++- .../2-features/add-config-for-pwd-hash | 2 +- changelog.d/5-internal/pwd | 1 - charts/brig/values.yaml | 9 ++-- charts/galley/values.yaml | 9 ++-- .../src/developer/reference/config-options.md | 46 ++++++++--------- hack/helm_vars/wire-server/values.yaml.gotmpl | 6 ++- integration/test/Test/User.hs | 51 +++++++++++++++++++ libs/types-common/src/Util/Options.hs | 20 +++++--- libs/wire-api/src/Wire/API/Password.hs | 4 +- libs/wire-subsystems/src/Wire/HashPassword.hs | 24 ++++----- services/brig/brig.integration.yaml | 1 + .../brig/src/Brig/CanonicalInterpreter.hs | 3 +- .../brig/test/integration/API/User/Auth.hs | 4 +- services/galley/galley.integration.yaml | 3 +- services/galley/src/Galley/App.hs | 3 +- 16 files changed, 138 insertions(+), 67 deletions(-) delete mode 100644 changelog.d/5-internal/pwd diff --git a/changelog.d/0-release-notes/configurable-argon b/changelog.d/0-release-notes/configurable-argon index b9e2a74cd8c..4a856472d06 100644 --- a/changelog.d/0-release-notes/configurable-argon +++ b/changelog.d/0-release-notes/configurable-argon @@ -1,18 +1,33 @@ -Password hashing is now done using argon2id instead of scrypt. The argon2id parameters can be configured using these options: +Password hashing can now be done using argon2id instead of scrypt. The argon2id parameters can be configured using these options: ```yaml brig: optSettings: setPasswordHashingOptions: + algorithm: argon2id iterations: ... memory: ... # memory needed in KiB parallelism: ... galley: settings: passwordHashingOptions: + algorithm: argon2id iterations: ... memory: ... # memory needed in KiB parallelism: ... ``` -These have default values, which should work for most deployments. Please see documentation on config-options for more. +The default option is still to use scrypt as moving to argon2id might require +allocating more resources according to configured parameters. + +When configured to use argon2id, the DB will be migrated slowly over time as the +users enter their passwords (either to login or to do other operations which +require explicit password entry). This migration is **NOT** done in reverse, +i.e., if a deployment started with argon2id as the algorithm then chose to move +to scrypt, the passwords will not get rehashed automatically, instead the users +will have to reset their passwords if that is desired. + +**NOTE** It is highly recommended to move to argon2id as it will be made the + only available choice for the `algorithm` config option in future. + +(#4291, ##) \ No newline at end of file diff --git a/changelog.d/2-features/add-config-for-pwd-hash b/changelog.d/2-features/add-config-for-pwd-hash index 79ba9c55f09..3ef8e186268 100644 --- a/changelog.d/2-features/add-config-for-pwd-hash +++ b/changelog.d/2-features/add-config-for-pwd-hash @@ -1 +1 @@ -Allow configuring Argon2id parameters +Allow choosing hashing algorithm and configuring argon2id parameters (#4291, ##) diff --git a/changelog.d/5-internal/pwd b/changelog.d/5-internal/pwd deleted file mode 100644 index d0789bc9df4..00000000000 --- a/changelog.d/5-internal/pwd +++ /dev/null @@ -1 +0,0 @@ -Changed default password hashing from Scrypt to Argon2id. diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index bba7408c6a5..a7fd85eceb5 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -150,11 +150,12 @@ config: setDisabledAPIVersions: [ development ] setFederationStrategy: allowNone setFederationDomainConfigsUpdateFreq: 10 - # Options for Argon2id version 19 setPasswordHashingOptions: - iterations: 1 - parallelism: 32 - memory: 180224 # 176 MiB + algorithm: scrypt # or argon2id + # When algorithm is argon2id, these can be configured: + # iterations: + # parallelism: + # memory: smtp: passwordFile: /etc/wire/brig/secrets/smtp-password.txt proxy: {} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 71045b680b9..60122db2c23 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -70,11 +70,12 @@ config: # The lifetime of a conversation guest link in seconds. Must be a value 0 < x <= 31536000 (365 days) # Default is 31536000 (365 days) if not set guestLinkTTLSeconds: 31536000 - # Options for Argon2id version 19 passwordHashingOptions: - iterations: 1 - parallelism: 32 - memory: 180224 # 176 MiB + algorithm: scrypt # or argon2id + # When algorithm is argon2id, these can be configured: + # iterations: + # parallelism: + # memory: # To disable proteus for new federated conversations: # federationProtocols: ["mls"] diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index a22b947c04b..74565d0dd21 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -719,22 +719,11 @@ optSettings: setOAuthMaxActiveRefreshTokens: 10 ``` -#### Argon2id password hashing parameters +#### Password hashing options -Since release 5.6.0, wire-server hashes passwords with -[argon2id](https://datatracker.ietf.org/doc/html/rfc9106) at rest. If -you do not do anything, the default parameters will be used, which -are: - -```yaml - setPasswordHashingOptions: - iterations: 1 - memory: 180224 # memory needed in kibibytes (1 kibibyte is 2^10 bytes) - parallelism: 32 -``` - -The default will be adjusted to new developments in hashing algorithm -security from time to time. +Since release 5.6.0, wire-server can hash passwords with +[argon2id](https://datatracker.ietf.org/doc/html/rfc9106) to be stored at rest. +If you do not do anything, the deployment will still use scrypt. The password hashing options are set for brig and galley: @@ -742,29 +731,34 @@ The password hashing options are set for brig and galley: brig: optSettings: setPasswordHashingOptions: + algorithm: # argon2id or scrypt + # These options only apply to argon2id iterations: ... memory: ... # memory needed in KiB parallelism: ... galley: settings: passwordHashingOptions: + algorithm: # argon2id or scrypt + # These options only apply to argon2id iterations: ... memory: ... # memory needed in KiB parallelism: ... ``` -**Performance implications:** scrypt takes ~80ms on a realistic test -system, and argon2id with default settings takes ~500ms. This is a -runtime increase by a factor of ~6. This happens every time a -password is entered by the user: during login, password reset, -deleting a device, etc. (It does **NOT** happen during any other -cryptographic operations like session key update or message -de-/encryption.) +**Performance implications:** argon2id typically takes longer and uses more +memory than scrypt. So when migrating to it brig and galley pods must be +allocated more resouces according to the chosen paramters. + +When configured to use argon2id, the DB will be migrated slowly over time as the +users enter their passwords (either to login or to do other operations which +require explicit password entry). This migration is **NOT** done in reverse, +i.e., if a deployment started with argon2id as the algorithm then chose to move +to scrypt, the passwords already stored will not get rehashed automatically, +however the users will still be able to use them to login. -The settings are a trade-off between resilience against brute force -attacks and password secrecy. For most systems this should be safe -and not need more hardware resources for brig, but you may want to -form your own opinion. +**NOTE** It is highly recommended to move to argon2id as it will be made the + only available choice for the `algorithm` config option in future. #### Disabling API versions diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index d6db927d92d..e7f72583f39 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -136,7 +136,8 @@ brig: setOAuthMaxActiveRefreshTokens: 10 # These values are insecure, against anyone getting hold of the hash, # but its not a concern for the integration tests. - setPasswordHashingOptions: + setPasswordHashingOptions: + algorithm: argon2id iterations: 1 parallelism: 4 memory: 32 # This needs to be at least 8 * parallelism. @@ -266,7 +267,8 @@ galley: # These values are insecure, against anyone getting hold of the hash, # but its not a concern for the integration tests. - passwordHashingOptions: + passwordHashingOptions: + algorithm: argon2id iterations: 1 parallelism: 4 memory: 32 # This needs to be at least 8 * parallelism. diff --git a/integration/test/Test/User.hs b/integration/test/Test/User.hs index e0429253875..548c34b1daf 100644 --- a/integration/test/Test/User.hs +++ b/integration/test/Test/User.hs @@ -4,13 +4,17 @@ module Test.User where import API.Brig import API.BrigInternal +import API.Common import API.GalleyInternal import qualified API.Spar as Spar +import Control.Monad.Codensity +import Control.Monad.Reader import qualified Data.Aeson as Aeson import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID import SetupHelpers import Testlib.Prelude +import Testlib.ResourcePool import Testlib.VersionedFed testSupportedProtocols :: (HasCallStack) => OneOf Domain (FedDomain 1) -> App () @@ -174,3 +178,50 @@ testActivateAccountWithPhoneV5 = do activateUserV5 dom reqBody `bindResponse` \resp -> do resp.status `shouldMatchInt` 400 resp.json %. "label" `shouldMatch` "bad-request" + +testMigratingPasswordHashingAlgorithm :: (HasCallStack) => App () +testMigratingPasswordHashingAlgorithm = do + let argon2idOpts = + object + [ "algorithm" .= "argon2id", + "iterations" .= (1 :: Int), + "memory" .= (128 :: Int), + "parallelism" .= (1 :: Int) + ] + cfgArgon2id = + def + { brigCfg = setField "settings.setPasswordHashingOptions" argon2idOpts, + galleyCfg = setField "settings.passwordHashingOptions" argon2idOpts + } + cfgScrypt = + def + { brigCfg = setField "settings.setPasswordHashingOptions.algorithm" "scrypt", + galleyCfg = setField "settings.passwordHashingOptions.algorithm" "scrypt" + } + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + email1 <- randomEmail + password1 <- randomString 20 + + email2 <- randomEmail + password2 <- randomString 20 + + runCodensity (startDynamicBackend testBackend cfgScrypt) $ \_ -> do + void $ randomUser domain (def {email = Just email1, password = Just password1}) + login domain email1 password1 >>= assertSuccess + + runCodensity (startDynamicBackend testBackend cfgArgon2id) $ \_ -> do + login domain email1 password1 >>= assertSuccess + + -- Create second user to ensure that we're testing migrating back. This is + -- not really needed because the login above rehashes the password, but it + -- makes the test clearer. + void $ randomUser domain (def {email = Just email2, password = Just password2}) + login domain email2 password2 >>= assertSuccess + + -- Check that both users can still login with Scrypt in case the operator + -- wants to rollback the config. + runCodensity (startDynamicBackend testBackend cfgScrypt) $ \_ -> do + login domain email1 password1 >>= assertSuccess + login domain email2 password2 >>= assertSuccess diff --git a/libs/types-common/src/Util/Options.hs b/libs/types-common/src/Util/Options.hs index 7b6cd88b08d..9fa6117aede 100644 --- a/libs/types-common/src/Util/Options.hs +++ b/libs/types-common/src/Util/Options.hs @@ -148,7 +148,12 @@ getOptions desc mp defaultPath = do parseAWSEndpoint :: ReadM AWSEndpoint parseAWSEndpoint = readerAsk >>= maybe (error "Could not parse AWS endpoint") pure . fromByteString . fromString -data PasswordHashingOptions = PasswordHashingOptions +data PasswordHashingOptions + = PasswordHashingArgon2id Argon2idOptions + | PasswordHashingScrypt + deriving (Show, Generic) + +data Argon2idOptions = Argon2idOptions { iterations :: !Word32, memory :: !Word32, parallelism :: !Word32 @@ -157,11 +162,14 @@ data PasswordHashingOptions = PasswordHashingOptions instance FromJSON PasswordHashingOptions where parseJSON = - withObject - "PasswordHashingOptions" - ( \obj -> do + withObject "PasswordHashingOptions" $ \obj -> do + algo :: String <- obj .: "algorithm" + case algo of + "argon2id" -> do iterations <- obj .: "iterations" memory <- obj .: "memory" parallelism <- obj .: "parallelism" - pure (PasswordHashingOptions {..}) - ) + pure . PasswordHashingArgon2id $ Argon2idOptions {..} + "scrypt" -> + pure PasswordHashingScrypt + x -> fail $ "Unknown password hashing algorithm: " <> x diff --git a/libs/wire-api/src/Wire/API/Password.hs b/libs/wire-api/src/Wire/API/Password.hs index 78f2ea0697f..172c987b4d1 100644 --- a/libs/wire-api/src/Wire/API/Password.hs +++ b/libs/wire-api/src/Wire/API/Password.hs @@ -130,8 +130,8 @@ fromScrypt scryptParams = outputLength = 64 } -argon2OptsFromHashingOpts :: PasswordHashingOptions -> Argon2.Options -argon2OptsFromHashingOpts PasswordHashingOptions {..} = +argon2OptsFromHashingOpts :: Argon2idOptions -> Argon2.Options +argon2OptsFromHashingOpts Argon2idOptions {..} = Argon2.Options { variant = Argon2.Argon2id, version = Argon2.Version13, diff --git a/libs/wire-subsystems/src/Wire/HashPassword.hs b/libs/wire-subsystems/src/Wire/HashPassword.hs index c91854f4316..1f58daf794d 100644 --- a/libs/wire-subsystems/src/Wire/HashPassword.hs +++ b/libs/wire-subsystems/src/Wire/HashPassword.hs @@ -3,11 +3,11 @@ module Wire.HashPassword where -import Crypto.KDF.Argon2 qualified as Argon2 import Data.Misc import Imports import Polysemy -import Wire.API.Password (Password) +import Util.Options +import Wire.API.Password import Wire.API.Password qualified as Password data HashPassword m a where @@ -17,20 +17,18 @@ data HashPassword m a where makeSem ''HashPassword runHashPassword :: + forall r. ( Member (Embed IO) r ) => - Argon2.Options -> + PasswordHashingOptions -> InterpreterFor HashPassword r runHashPassword opts = interpret $ \case - HashPassword6 pw6 -> hashPasswordImpl opts pw6 - HashPassword8 pw8 -> hashPasswordImpl opts pw8 - -hashPasswordImpl :: - (Member (Embed IO) r) => - Argon2.Options -> - PlainTextPassword' t -> - Sem r Password -hashPasswordImpl opts pwd = do - liftIO $ Password.mkSafePassword opts pwd + HashPassword6 pw6 -> hashFunction pw6 + HashPassword8 pw8 -> hashFunction pw8 + where + hashFunction :: PlainTextPassword' t -> Sem r Password + hashFunction = case opts of + PasswordHashingArgon2id o -> Password.mkSafePassword (argon2OptsFromHashingOpts o) + PasswordHashingScrypt -> Password.mkSafePasswordScrypt diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index f814af8c799..ad2c8da9560 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -229,6 +229,7 @@ optSettings: setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks setOAuthMaxActiveRefreshTokens: 10 setPasswordHashingOptions: # in testing, we want these settings to be faster, not secure against attacks. + algorithm: argon2id iterations: 1 memory: 128 parallelism: 1 diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 0ceacfba5ee..5248effd92b 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -33,7 +33,6 @@ import Polysemy.TinyLog (TinyLog) import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Federation.Client qualified import Wire.API.Federation.Error -import Wire.API.Password import Wire.ActivationCodeStore (ActivationCodeStore) import Wire.ActivationCodeStore.Cassandra (interpretActivationCodeStoreToCassandra) import Wire.AuthenticationSubsystem @@ -265,7 +264,7 @@ runBrigToIO e (AppT ma) = do . interpretIndexedUserStoreES indexedUserStoreConfig . interpretUserStoreCassandra e.casClient . interpretUserKeyStoreCassandra e.casClient - . runHashPassword (argon2OptsFromHashingOpts e.settings.passwordHashingOptions) + . runHashPassword e.settings.passwordHashingOptions . interpretFederationAPIAccess federationApiAccessConfig . rethrowHttpErrorIO . mapError propertySubsystemErrorToHttpError diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index f3b0ffeb4eb..a93fa99317b 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -55,6 +55,7 @@ import Data.ZAuth.Token qualified as ZAuth import Imports import Network.HTTP.Client (equivCookie) import Network.Wai.Utilities.Error qualified as Error +import Polysemy import Test.Tasty hiding (Timeout) import Test.Tasty.HUnit import Test.Tasty.HUnit qualified as HUnit @@ -69,6 +70,7 @@ import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso import Wire.API.User.Client +import Wire.HashPassword -- | FUTUREWORK: Implement this function. This wrapper should make sure that -- wrapped tests run only when the feature flag 'legalhold' is set to @@ -194,7 +196,7 @@ testLoginWith6CharPassword opts brig db = do updatePassword :: (MonadClient m) => UserId -> PlainTextPassword6 -> m () updatePassword u t = do - p <- mkSafePassword (argon2OptsFromHashingOpts opts.settings.passwordHashingOptions) t + p <- liftIO $ runM . runHashPassword opts.settings.passwordHashingOptions $ hashPassword6 t retry x5 $ write userPasswordUpdate (params LocalQuorum (p, u)) userPasswordUpdate :: PrepQuery W (Password, UserId) () diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index c76165b1bd0..3f1e069313f 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -59,10 +59,11 @@ settings: ecdsa_secp521r1_sha512: test/resources/backendA/ecdsa_secp521r1_sha512.pem guestLinkTTLSeconds: 604800 passwordHashingOptions: # in testing, we want these settings to be faster, not secure against attacks. + algorithm: argon2id iterations: 1 memory: 128 parallelism: 1 - + # We explicitly do not disable any API version. Please make sure the configuration value is the same in all these configs: # brig, cannon, cargohold, galley, gundeck, proxy, spar. disabledAPIVersions: [] diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 4bfccae43cf..6d29d6081ca 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -106,7 +106,6 @@ import UnliftIO.Exception qualified as UnliftIO import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Federation.Error -import Wire.API.Password import Wire.API.Team.Feature import Wire.GundeckAPIAccess (runGundeckAPIAccess) import Wire.HashPassword @@ -254,7 +253,7 @@ evalGalley e = . mapError toResponse . mapError toResponse . mapError toResponse - . runHashPassword (argon2OptsFromHashingOpts e._options._settings._passwordHashingOptions) + . runHashPassword e._options._settings._passwordHashingOptions . runInputConst e . runInputConst (e ^. cstate) . mapError toResponse -- DynError From 9445e43e1b944d7342f5ad27f877e5f1b0537994 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 30 Oct 2024 16:19:09 +0100 Subject: [PATCH 12/27] [fix] Local federation v1 tests fixed (#4320) --- changelog.d/5-internal/fix-local-fed-v1 | 1 + .../federation-v0/nginz/conf/integration.conf | 6 ++---- .../federation-v1/nginz/conf/integration.conf | 6 ++---- deploy/dockerephemeral/run.sh | 11 +++++++---- integration/test/Test/MLS/One2One.hs | 4 ++++ 5 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 changelog.d/5-internal/fix-local-fed-v1 diff --git a/changelog.d/5-internal/fix-local-fed-v1 b/changelog.d/5-internal/fix-local-fed-v1 new file mode 100644 index 00000000000..2bff4d110c1 --- /dev/null +++ b/changelog.d/5-internal/fix-local-fed-v1 @@ -0,0 +1 @@ +Local integration tests of federation version V1 fixed diff --git a/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf b/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf index fa168d16f4d..74dd1a09113 100644 --- a/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf +++ b/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf @@ -15,7 +15,5 @@ listen 8090; # But to also test tls forwarding, this port can be used. # This applies only locally, as for kubernetes (helm chart) based deployments, # TLS is terminated at the ingress level, not at nginz level -listen 8443 ssl; -listen [::]:8443 ssl; - -http2 on; +listen 8443 ssl http2; +listen [::]:8443 ssl http2; diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf index fa168d16f4d..74dd1a09113 100644 --- a/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf @@ -15,7 +15,5 @@ listen 8090; # But to also test tls forwarding, this port can be used. # This applies only locally, as for kubernetes (helm chart) based deployments, # TLS is terminated at the ingress level, not at nginz level -listen 8443 ssl; -listen [::]:8443 ssl; - -http2 on; +listen 8443 ssl http2; +listen [::]:8443 ssl http2; diff --git a/deploy/dockerephemeral/run.sh b/deploy/dockerephemeral/run.sh index f6455dacf5c..bbb6f012079 100755 --- a/deploy/dockerephemeral/run.sh +++ b/deploy/dockerephemeral/run.sh @@ -1,17 +1,20 @@ #!/usr/bin/env bash +# To start the federation v0, v1 backends, set ENABLE_FEDERATION_V0=1, ENABLE_FEDERATION_V1=1 +# in the env where this script is run + set -e # run.sh should work no matter what is the current directory -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DOCKER_FILE="$SCRIPT_DIR/docker-compose.yaml" FED_VERSIONS=(0 1) -opts=( "--file" "$DOCKER_FILE" ) +opts=("--file" "$DOCKER_FILE") for v in "${FED_VERSIONS[@]}"; do var="ENABLE_FEDERATION_V$v" if [[ "${!var}" == 1 ]]; then - opts+=( "--file" "$SCRIPT_DIR/federation-v$v.yaml" ) + opts+=("--file" "$SCRIPT_DIR/federation-v$v.yaml") fi done @@ -19,7 +22,7 @@ dc() { docker-compose "${opts[@]}" "$@" } -cleanup () { +cleanup() { dc down } diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index cbc4c1339e2..d93e5f582c2 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -395,6 +395,8 @@ testMLSGhostOne2OneConv = do -- still be created but only by the user whose backend hosts this conversation. -- | See Note: [Federated 1:1 MLS Conversations] +-- To run locally this test requires federation-v1 docker containers to be up and running. +-- See `deploy/dockerephemeral/run.sh` and comment on `StaticFedDomain` in `Testlib/VersionedFed.hs` for more details. testMLSFederationV1ConvOnOldBackend :: App () testMLSFederationV1ConvOnOldBackend = do alice <- randomUser OwnDomain def @@ -447,6 +449,8 @@ testMLSFederationV1ConvOnOldBackend = do parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 -- | See Note: Federated 1:1 MLS Conversations +-- To run locally this test requires federation-v1 docker containers to be up and running. +-- See `deploy/dockerephemeral/run.sh` and comment on `StaticFedDomain` in `Testlib/VersionedFed.hs` for more details. testMLSFederationV1ConvOnNewBackend :: App () testMLSFederationV1ConvOnNewBackend = do alice <- randomUser OwnDomain def From 5d6c2d39690e79e0423264709aff5f1c585ee7b8 Mon Sep 17 00:00:00 2001 From: Zebot Date: Wed, 30 Oct 2024 15:36:33 +0000 Subject: [PATCH 13/27] Add changelog for Release 2024-10-30 --- CHANGELOG.md | 315 ++++++++++++++++++ changelog.d/0-release-notes/WPB-10058 | 1 - changelog.d/0-release-notes/WPB-10058-5xx | 4 - changelog.d/0-release-notes/WPB-10658 | 2 - changelog.d/0-release-notes/WPB-10660 | 1 - changelog.d/0-release-notes/WPB-665 | 20 -- changelog.d/0-release-notes/WPB-8707 | 1 - .../0-release-notes/configurable-argon | 33 -- changelog.d/0-release-notes/gundeck-bulk-push | 3 - changelog.d/1-api-changes/WPB-10658 | 1 - changelog.d/1-api-changes/WPB-10797 | 1 - changelog.d/1-api-changes/WPB-11163 | 1 - changelog.d/1-api-changes/WPB-685 | 1 - changelog.d/1-api-changes/WPB-8707 | 1 - .../1-api-changes/add-columns-to-export | 1 - changelog.d/1-api-changes/capabilities-v7 | 1 - changelog.d/1-api-changes/finalise-v6 | 1 - changelog.d/1-api-changes/jwk | 1 - changelog.d/1-api-changes/one2one | 1 - changelog.d/1-api-changes/ttl | 1 - changelog.d/1-api-changes/wpb-10235 | 1 - changelog.d/1-api-changes/wpb-10708 | 1 - changelog.d/2-features/WPB-10058 | 1 - changelog.d/2-features/WPB-10204 | 1 - changelog.d/2-features/WPB-10658 | 1 - changelog.d/2-features/WPB-10772 | 5 - changelog.d/2-features/WPB-11050 | 1 - ...WPB-11163-consume-notifications-capability | 1 - changelog.d/2-features/WPB-1333 | 1 - changelog.d/2-features/WPB-1334 | 1 - changelog.d/2-features/WPB-665 | 1 - changelog.d/2-features/WPB-685 | 1 - changelog.d/2-features/WPB-9773 | 1 - .../2-features/add-config-for-pwd-hash | 1 - changelog.d/2-features/block-lh-for-mls-users | 1 - .../helm-coturn-service-annotations | 1 - changelog.d/2-features/new-teams-mls | 1 - changelog.d/2-features/no-federated-proteus | 1 - ...instrumentation-brig-galley-gundeck-cannon | 1 - .../2-features/personal-account-to-team-email | 1 - changelog.d/2-features/sft-username | 1 - changelog.d/2-features/upgrade-rabbitmq | 6 - changelog.d/3-bug-fixes/PR-4152 | 1 - changelog.d/3-bug-fixes/WBP-8790 | 1 - changelog.d/3-bug-fixes/WPB-10207 | 1 - ...11122-disallow-searching-user-by-old-email | 1 - changelog.d/3-bug-fixes/WPB-11925-fix-add-bot | 1 - changelog.d/3-bug-fixes/WPB-6865 | 1 - changelog.d/3-bug-fixes/ascii-text-parsing | 1 - changelog.d/3-bug-fixes/ciphersuite-update | 1 - changelog.d/3-bug-fixes/flag-defaults | 1 - changelog.d/3-bug-fixes/max-properties | 1 - .../3-bug-fixes/remove-spam-from-nginx | 1 - changelog.d/3-bug-fixes/services-tags | 1 - changelog.d/3-bug-fixes/ses-notifications | 1 - changelog.d/4-docs/WPB-11502 | 1 - changelog.d/4-docs/WPB-9742 | 1 - changelog.d/4-docs/fix-swagger | 1 - changelog.d/4-docs/fix-swagger-2 | 1 - changelog.d/4-docs/mls-test-tags | 1 - changelog.d/4-docs/openapi-validation | 1 - changelog.d/4-docs/revert-wpb8628 | 1 - changelog.d/5-internal/WBP-11188 | 1 - changelog.d/5-internal/WPB-10302 | 1 - changelog.d/5-internal/WPB-10335 | 1 - changelog.d/5-internal/WPB-10424 | 1 - .../WPB-10581-remove-coturn-helm-chart | 1 - changelog.d/5-internal/WPB-11000 | 1 - changelog.d/5-internal/WPB-11101 | 1 - .../5-internal/WPB-11101-internal-types | 1 - ...rsonal-users-into-teams-to-wire-subsystems | 10 - .../5-internal/WPB-11301-db-tool-team-info | 1 - changelog.d/5-internal/WPB-11386-map-range | 1 - changelog.d/5-internal/WPB-11502 | 1 - .../WPB-1220-servantify-proxy-internal | 1 - .../WPB-1228-servantify-gundeck-internal-api | 1 - changelog.d/5-internal/WPB-888-2 | 1 - changelog.d/5-internal/WPB-8888 | 1 - changelog.d/5-internal/WPB-8892 | 1 - changelog.d/5-internal/background-worker | 1 - .../5-internal/email-templates-v1.0.122 | 1 - .../5-internal/feature-flag-refactoring-1 | 7 - .../5-internal/feature-flag-refactoring-2 | 1 - .../5-internal/feature-flag-refactoring-3 | 1 - changelog.d/5-internal/federation-v1 | 1 - changelog.d/5-internal/fix-galley-overlaps | 1 - changelog.d/5-internal/fix-local-fed-v1 | 1 - changelog.d/5-internal/fix-nginx-paths | 1 - .../5-internal/gundeck-internal-swagger | 1 - changelog.d/5-internal/inbucket | 1 - changelog.d/5-internal/make-crm | 1 - changelog.d/5-internal/migrate-postgres-chart | 1 - .../5-internal/new-team-types-refactoring | 1 - changelog.d/5-internal/openapi-validation | 1 - changelog.d/5-internal/optimize-list-users | 1 - changelog.d/5-internal/pre-stop | 1 - changelog.d/5-internal/property-subsystem | 1 - changelog.d/5-internal/refactor-email | 1 - changelog.d/5-internal/test-csv-export | 1 - changelog.d/5-internal/todo | 1 - changelog.d/5-internal/user-features | 1 - changelog.d/5-internal/user-types-refactoring | 1 - changelog.d/5-internal/weed | 1 - changelog.d/5-internal/wpb-8887 | 1 - changelog.d/5-internal/wpb-9844 | 1 - 105 files changed, 315 insertions(+), 185 deletions(-) delete mode 100644 changelog.d/0-release-notes/WPB-10058 delete mode 100644 changelog.d/0-release-notes/WPB-10058-5xx delete mode 100644 changelog.d/0-release-notes/WPB-10658 delete mode 100644 changelog.d/0-release-notes/WPB-10660 delete mode 100644 changelog.d/0-release-notes/WPB-665 delete mode 100644 changelog.d/0-release-notes/WPB-8707 delete mode 100644 changelog.d/0-release-notes/configurable-argon delete mode 100644 changelog.d/0-release-notes/gundeck-bulk-push delete mode 100644 changelog.d/1-api-changes/WPB-10658 delete mode 100644 changelog.d/1-api-changes/WPB-10797 delete mode 100644 changelog.d/1-api-changes/WPB-11163 delete mode 100644 changelog.d/1-api-changes/WPB-685 delete mode 100644 changelog.d/1-api-changes/WPB-8707 delete mode 100644 changelog.d/1-api-changes/add-columns-to-export delete mode 100644 changelog.d/1-api-changes/capabilities-v7 delete mode 100644 changelog.d/1-api-changes/finalise-v6 delete mode 100644 changelog.d/1-api-changes/jwk delete mode 100644 changelog.d/1-api-changes/one2one delete mode 100644 changelog.d/1-api-changes/ttl delete mode 100644 changelog.d/1-api-changes/wpb-10235 delete mode 100644 changelog.d/1-api-changes/wpb-10708 delete mode 100644 changelog.d/2-features/WPB-10058 delete mode 100644 changelog.d/2-features/WPB-10204 delete mode 100644 changelog.d/2-features/WPB-10658 delete mode 100644 changelog.d/2-features/WPB-10772 delete mode 100644 changelog.d/2-features/WPB-11050 delete mode 100644 changelog.d/2-features/WPB-11163-consume-notifications-capability delete mode 100644 changelog.d/2-features/WPB-1333 delete mode 100644 changelog.d/2-features/WPB-1334 delete mode 100644 changelog.d/2-features/WPB-665 delete mode 100644 changelog.d/2-features/WPB-685 delete mode 100644 changelog.d/2-features/WPB-9773 delete mode 100644 changelog.d/2-features/add-config-for-pwd-hash delete mode 100644 changelog.d/2-features/block-lh-for-mls-users delete mode 100644 changelog.d/2-features/helm-coturn-service-annotations delete mode 100644 changelog.d/2-features/new-teams-mls delete mode 100644 changelog.d/2-features/no-federated-proteus delete mode 100644 changelog.d/2-features/open-telemetry-instrumentation-brig-galley-gundeck-cannon delete mode 100644 changelog.d/2-features/personal-account-to-team-email delete mode 100644 changelog.d/2-features/sft-username delete mode 100644 changelog.d/2-features/upgrade-rabbitmq delete mode 100644 changelog.d/3-bug-fixes/PR-4152 delete mode 100644 changelog.d/3-bug-fixes/WBP-8790 delete mode 100644 changelog.d/3-bug-fixes/WPB-10207 delete mode 100644 changelog.d/3-bug-fixes/WPB-11122-disallow-searching-user-by-old-email delete mode 100644 changelog.d/3-bug-fixes/WPB-11925-fix-add-bot delete mode 100644 changelog.d/3-bug-fixes/WPB-6865 delete mode 100644 changelog.d/3-bug-fixes/ascii-text-parsing delete mode 100644 changelog.d/3-bug-fixes/ciphersuite-update delete mode 100644 changelog.d/3-bug-fixes/flag-defaults delete mode 100644 changelog.d/3-bug-fixes/max-properties delete mode 100644 changelog.d/3-bug-fixes/remove-spam-from-nginx delete mode 100644 changelog.d/3-bug-fixes/services-tags delete mode 100644 changelog.d/3-bug-fixes/ses-notifications delete mode 100644 changelog.d/4-docs/WPB-11502 delete mode 100644 changelog.d/4-docs/WPB-9742 delete mode 100644 changelog.d/4-docs/fix-swagger delete mode 100644 changelog.d/4-docs/fix-swagger-2 delete mode 100644 changelog.d/4-docs/mls-test-tags delete mode 100644 changelog.d/4-docs/openapi-validation delete mode 100644 changelog.d/4-docs/revert-wpb8628 delete mode 100644 changelog.d/5-internal/WBP-11188 delete mode 100644 changelog.d/5-internal/WPB-10302 delete mode 100644 changelog.d/5-internal/WPB-10335 delete mode 100644 changelog.d/5-internal/WPB-10424 delete mode 100644 changelog.d/5-internal/WPB-10581-remove-coturn-helm-chart delete mode 100644 changelog.d/5-internal/WPB-11000 delete mode 100644 changelog.d/5-internal/WPB-11101 delete mode 100644 changelog.d/5-internal/WPB-11101-internal-types delete mode 100644 changelog.d/5-internal/WPB-11217-move-code-for-accepting-invitations-for-personal-users-into-teams-to-wire-subsystems delete mode 100644 changelog.d/5-internal/WPB-11301-db-tool-team-info delete mode 100644 changelog.d/5-internal/WPB-11386-map-range delete mode 100644 changelog.d/5-internal/WPB-11502 delete mode 100644 changelog.d/5-internal/WPB-1220-servantify-proxy-internal delete mode 100644 changelog.d/5-internal/WPB-1228-servantify-gundeck-internal-api delete mode 100644 changelog.d/5-internal/WPB-888-2 delete mode 100644 changelog.d/5-internal/WPB-8888 delete mode 100644 changelog.d/5-internal/WPB-8892 delete mode 100644 changelog.d/5-internal/background-worker delete mode 100644 changelog.d/5-internal/email-templates-v1.0.122 delete mode 100644 changelog.d/5-internal/feature-flag-refactoring-1 delete mode 100644 changelog.d/5-internal/feature-flag-refactoring-2 delete mode 100644 changelog.d/5-internal/feature-flag-refactoring-3 delete mode 100644 changelog.d/5-internal/federation-v1 delete mode 100644 changelog.d/5-internal/fix-galley-overlaps delete mode 100644 changelog.d/5-internal/fix-local-fed-v1 delete mode 100644 changelog.d/5-internal/fix-nginx-paths delete mode 100644 changelog.d/5-internal/gundeck-internal-swagger delete mode 100644 changelog.d/5-internal/inbucket delete mode 100644 changelog.d/5-internal/make-crm delete mode 100644 changelog.d/5-internal/migrate-postgres-chart delete mode 100644 changelog.d/5-internal/new-team-types-refactoring delete mode 100644 changelog.d/5-internal/openapi-validation delete mode 100644 changelog.d/5-internal/optimize-list-users delete mode 100644 changelog.d/5-internal/pre-stop delete mode 100644 changelog.d/5-internal/property-subsystem delete mode 100644 changelog.d/5-internal/refactor-email delete mode 100644 changelog.d/5-internal/test-csv-export delete mode 100644 changelog.d/5-internal/todo delete mode 100644 changelog.d/5-internal/user-features delete mode 100644 changelog.d/5-internal/user-types-refactoring delete mode 100644 changelog.d/5-internal/weed delete mode 100644 changelog.d/5-internal/wpb-8887 delete mode 100644 changelog.d/5-internal/wpb-9844 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3de95aec62e..021decd6505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,318 @@ +# [2024-10-30] (Chart Release 5.6.0) + +## Release notes + + +* To remove phone keys from brig's `user_keys` table an ad hoc data-migration can be run. See PR https://github.com/wireapp/wire-server/pull/4146 which contains the implementation. (#4130) + +* Because the `phone` column is deleted from Brig's `user` table in a schema + migration, temporarily there might be 5xx errors during deployment if Wire + server 5.4.0 was not deployed previously. To avoid these errors, please deploy + the Wire server 5.4.0 release first. (#4130) + +* With this release it will be possible to invite personal users to teams. In `brig`'s config, `emailSMS.team.tExistingUserInvitationUrl` is required to be set to a value that points to the correct teams/account page. + If `emailSMS.team` is not defined at all in the current environment, the value of `externalUrls.teamSettings` (or, if not present, `externalUrls.nginz`) will be used to construct the correct url, and no configuration change is necessary. (#4229) + +* charts/wire-server: There is a new config value called `background-worker.config.enableFederation` which defaults to `false`. This must be kept in sync with `tags.federation`. (#4243) + +* If you are mapping an email address to the `externalId` field in the + scim schema, please check the following list for items that apply to + you and recommended steps before/during/after upgrade. + + - **Situation:** the `emails` field of in your scim user records is + empty. + + **What you need to do:** change your schema mapping to contain the + same address in `externalId` and (as a record with one element) in + `emails`. + + - **Situation:** the `emails` field of your scim user records is + non-empty. + + **What you need to do:** make sure `emails` contains exactly one + entry, which is the email from `externalId`. If there is a + discrepancy, the address from `emails` will become the new + (unvalidated) address of the user, and the user will receive an + email to validate it. If the email cannot be sent or is ignored + by the recipient, the *valid* address will not be changed. (#4221) + +* A schema migration drops column 'phone' from Brig's 'team_invitation' table. Previous releases were still reading this column. As there is no Team Settings UI action to enter a phone number, this reading will not miss to read actual phone numbers. Therefore, during deployment this will lead to benign 5xx errors. (#4149) + +* Password hashing can now be done using argon2id instead of scrypt. The argon2id parameters can be configured using these options: + + ```yaml + brig: + optSettings: + setPasswordHashingOptions: + algorithm: argon2id + iterations: ... + memory: ... # memory needed in KiB + parallelism: ... + galley: + settings: + passwordHashingOptions: + algorithm: argon2id + iterations: ... + memory: ... # memory needed in KiB + parallelism: ... + ``` + + The default option is still to use scrypt as moving to argon2id might require + allocating more resources according to configured parameters. + + When configured to use argon2id, the DB will be migrated slowly over time as the + users enter their passwords (either to login or to do other operations which + require explicit password entry). This migration is **NOT** done in reverse, + i.e., if a deployment started with argon2id as the algorithm then chose to move + to scrypt, the passwords will not get rehashed automatically, instead the users + will have to reset their passwords if that is desired. + + **NOTE** It is highly recommended to move to argon2id as it will be made the + only available choice for the `algorithm` config option in future. + + (#4291, #4291) + +* Config value `gundeck.config.bulkPush` has been removed. This is purely an + internal change, in case the value was overriden to `false`, operators might see + more spiky usage of CPU and memory from gundeck due to bulk processing. (#4290) + + +## API changes + + +* A new endpoint `POST /teams/invitations/accept` allows a non-team user to accept an invitation to join a team (#4229) + +* Services allowlist are blocked by 409 (mls-services-not-allowed) for teams with default protocol MLS. (#4266) + +* The `POST /clients` and `PUT /clients/:cid` endpoints support a new capability "consume-notifications" (#4259) + +* New variant in API version 7 of endpoints for creating and listing SCIM tokens that support a `name` field. New endpoint in version 7 for updating a SCIM token name. (#4307) + +* All the phone number-based functionality is removed from the client API v6 (#4149) + +* The team CSV export endpoint has gained two extra columns: `last_active` and `status`. The streaming behaviour has also been improved. (#4293) + +* The changes to the `capabilities` field of the `Client` structure, introduced in v6, have now been postponed to v7 (#4179) + +* Finalise version 6 and introduce new development version 7 (#4179, #4179) + +* From API version 7 the `GET /mls/public-key` and `GET /conversations/one2one/:domain/:uid` endpoints now take a `format` query parameter which can be either `raw` (default, for raw base64-encoded keys) or `jwk` (for JWK keys) (#4216, #4224) + +* `GET /conversations/one2one/:domain/:uid` now returns `public_keys` along with the conversation containing all MLS public keys for the backend which will host this conversation (since v6). (#4224) + +* Remove the ability to set the TTL of a feature flag. Existing TTLs are still retrieved and returned as before. Note that this only applies to the conferenceCalling feature, as none of the others supported TTL anyway. (#4164) + +* Add useSFTForOneToOneCalls as a config option for the Conference Calling feature flag and make its lock status explicit. (#4164) + +* Add endpoint to upgrade a personal user to a team owner (#4251) + + +## Features + + +* DB migration for dropping `phone` column from `user` table (#4130) + +* A text status field was added to user and user profile (#4155) + +* Allow an existing non-team user to migrate to a team (#4229, #4268, #4315) + +* Makes it impossible for a user to join an MLS conversation while already under legalhold (at least pending) + + This implies two things: + 1. If a user is under legalhold they cannot ever join an MLS conversation, not even an MLS self conversation. + 2. A user has to reject to be put under legalhold when they want to join an MLS conversation (ignoring the request to be put under legalhold is not enough). (#4242) + +* Email template for inviting a personal user to a team added (#4310) + +* Clients can declare to be supporting a capability for consuming notifications (#4259) + +* New endpoint to revoke an OAuth session (#4213) + +* Adds a field which contains a list of all active sessions to each OAuth application in the response of `GET /oauth/applications` (#4211) + +* SCIM's emails field is now handled and the external ID is not restricted to being an email anymore (#4221) + +* Added human readable names for SCIM tokens (#4307) + +* allow subconversations for MLS 1-1 conversations (#4133) + +* Allow choosing hashing algorithm and configuring argon2id parameters (#4291, #4291) + +* Deny requests for a legalhold device for users who are part of any MLS conversations (#4245) + +* Allow setting of Kubernetes annotations for the `coturn` Service. (#4189) + +* Add `initialConfig` setting for the `mls` feature flag (#4262) + +* Add `federationProtocols` setting to galley, which can be used to disable the creation of federated conversations with a given protocol (#4278) + +* added open telemetry instrumentation for brig, galley, gundeck and cannon (#3901) + +* Send confirmation email after adding a personal user to a new team (#4253) + +* The SFT and turn usernames returned by `/calls/config/v2` are now deterministically computed from the user ID (#4156) + +* Use latest stable RabbitMQ version (`3.13.7`) and Helm chart (`14.6.9`). Please + note that this minor RabbitMQ version upgrade (`3.11.x` to `3.13.x`) may need + special treatment regarding existing RabbitMQ instances. See + https://www.rabbitmq.com/docs/upgrade#rabbitmq-version-upgradability . The major + Helm chart version upgrade may (depending on your setup/values) need attention + as well: https://github.com/bitnami/charts/tree/main/bitnami/rabbitmq#upgrading (#4227) + + +## Bug fixes and other updates + + +* Fixed API version check. It has now precedence over other checks like e.g. method check. (#4152) + +* Fix handling of defaults of `mlsE2EID` feature config (#4233) + +* Match cipher suite tag in query parameters against key packages on replacing key packages (#4158) + +* Users with SAML-SSO are allowed to delete their email address on the rest api. If they do that, the search indices are not updated correctly, and finding the user by the removed email address is still possible. (#4260) + +* Re-add accidentally removed add-bot@v6 route in nginz, fixes #4302 (#4318) + +* Exclude exception message from error response (#4153) + +* Return HTTP 400 instead of 500 when property key is not printable ASCII (#4148) + +* move cipher suite updates into the commit lock (#4151) + +* Fix feature flag default calculation for `mlsMigration` and `enforceFileDownloadLocation` (#4265) + +* Allow setting existing properties even if we have max properties (#4148) + +* removed spam from nginx (nginz) by using the new style http/2 directive (#3901) + +* brig: Make `GET /services/tags` work again (#4250) + +* Process bounce and complaint notifications from SES correctly. (#4301) + + +## Documentation + + +* Call graph of federated endpoints was removed from the docs (#4299) + +* Restored LegalHold internal API swagger as part of Brig. (#4191) + +* Fix: show openapi docs for blocked versions (#4309) + +* Move docs from docs.wire.com to generated helper page served by brig (#4311) + +* Deleted proteus-specific test documentation tags and added some new tags to MLS tests (#4240) + +* Fix openapi validation errors (#4295, #4295) + +* Re-introduce test case tags for BSI audit (revert #4041) (#4192) + + +## Internal changes + + +* Introduced API versioning and version negotiation for external LegalHold Service supporting `v0` and `v1` (#4284) + +* Read sftTokenSecret from secrets.yaml and mount to /etc/wire/brig/secrets/sftTokenSecret by default (#4214) + +* Added node based topology constraint to ensure pods are distributed uniformly on all nodes. (#4222) + +* Move smallstep-accomp` helm charts to `wireapp/helm-charts` (#4204) + +* Remove coturn helm chart. It is moved to `wireapp/coturn`. (#4209) + +* Additional test for password reset, port tests to new integration test suite (#4249) + +* Remove unused invitation tables from brig. (#4263) + +* Improve abstraction in the invitation store and hide DB interaction-specific internal types from the application code. (#4280) + +* Move some invitation handling from brig to wire-subsystems. + + - introduce cyclically dependent effects: UserSubsystem, AuthenticationSubsystem (see Brig.CanonicalInterpreter). + - introduce TeamInvitationSubsystem with operations inviteUser, internalCreateInvitation. + - add verifyPassword to AuthenticationSubsystem. + - add sendInvitationMail, sendInvitationMailPersonalUser to EmailSubsystem. + - add getTeamSize to IndexedUserStore (this is morally internal to wire-subsystems, and making another ES subsystem would mean adding a lot of code everywhere). + - add updateUserTeam to UserStore. + - add acceptTeamInvitation, internalFindTeamInvitation to UserSubsystem. + - make a few small rest api handlers in brig polysemic (Handler -> Sem). (#4264) + +* tools/db/team-info: collects last login times of all team members (#4274) + +* Introduce length-preserving function mapRange to replace Functor instance for Range data type. (#4279) + +* TransitiveAnns compiler plugin was removed (#4299) + +* Servantify internal routing table for proxy. (#4296) + +* Servantify gundeck internal api (#4246) + +* Removed `indexReindex` and `indexReindexIfSameOrNewer` from internal Brig/SearchIndex. (#4188) + +* Introduced ElasticSearch effects related to user search. (#4188) + +* Brig was refactored by pulling out email block-listing into a wire subsystems effect, and its actions are exposed via the user subsystem. (#4167) + +* charts/wire-server: Deploy background-worker even when tags.federation is `false` (#4342, #4248) + +* Updated email templates to v1.0.122 (#4308) + +* Refactor feature flags + - Improved naming slightly. Features types are now called `Feature`, `LockableFeature` and `LockableFeaturePatch` + - Turned `AllFeatures` into an extensible record type + - Removed `WithStatusBase` barbie. + - Deleted obsolete `computeFeatureConfigForTeamUser` + - Abstracted `getFeature` and `setFeature` + - Abstracted getAllTeamFeatures (#4181) + +* Clean up and reorganise feature flag endpoints (#4193) + +* Clean up feature default configuration code (#4196) + +* Add federation-v1 environment for testing compatibility of the federation API with version 1 (#4125) + +* Fix overlapping paths errors in galley's internal API (#4313) + +* Local integration tests of federation version V1 fixed (#4320) + +* nginz/local-conf: Update list of endpoints (#4176) + +* Expose gundeck internal API on swagger. Mv some types and routes to wire-api. (#4247) + +* dockerephemeral: Use inbucket for SMTP (#4176) + +* Makefile: Add target `crm` to run services tuned for manual usage (#4176) + +* Postgresql helm chart is removed from charts/ directory and migrated to wireapp/helm-charts repo (#4208) + +* Simplify NewTeam and related types and remove lenses (#4257) + +* Add openapi validation test to integration (#4302) + +* Optimize getting a lot of users by concurrently getting target users (#4140) + +* charts/{brig,galley}: Allow setting a preStop hook for the deployments (#4200) + +* Introduce proeprty subsytem (#4148) + +* Factored out our Email type in favour of EmailAddress from email-validate. (#4206) + +* Move CSV export test to integration (#4292) + +* add the TODO pattern and the todo function to Imports (#4198) + +* Refactor user feature logic (#4178) + +* Remove `UserAccount` and `ExtendedUserAccount` and their fields to the `User` type (#4275) + +* Started weeding out dead code. (#4170) + +* New user subsystem operation `getAccountsBy` for complex account lookups. (#4218) + +* Added warning when deploying wire-server helm chart with User/Team creation over internet enabled. (#4212) + + # [2024-07-09] (Chart Release 5.5.0) ## Bug fixes and other updates diff --git a/changelog.d/0-release-notes/WPB-10058 b/changelog.d/0-release-notes/WPB-10058 deleted file mode 100644 index 8f9c066875b..00000000000 --- a/changelog.d/0-release-notes/WPB-10058 +++ /dev/null @@ -1 +0,0 @@ -To remove phone keys from brig's `user_keys` table an ad hoc data-migration can be run. See PR https://github.com/wireapp/wire-server/pull/4146 which contains the implementation. diff --git a/changelog.d/0-release-notes/WPB-10058-5xx b/changelog.d/0-release-notes/WPB-10058-5xx deleted file mode 100644 index e884d0434be..00000000000 --- a/changelog.d/0-release-notes/WPB-10058-5xx +++ /dev/null @@ -1,4 +0,0 @@ -Because the `phone` column is deleted from Brig's `user` table in a schema -migration, temporarily there might be 5xx errors during deployment if Wire -server 5.4.0 was not deployed previously. To avoid these errors, please deploy -the Wire server 5.4.0 release first. diff --git a/changelog.d/0-release-notes/WPB-10658 b/changelog.d/0-release-notes/WPB-10658 deleted file mode 100644 index df9e6dc5e17..00000000000 --- a/changelog.d/0-release-notes/WPB-10658 +++ /dev/null @@ -1,2 +0,0 @@ -With this release it will be possible to invite personal users to teams. In `brig`'s config, `emailSMS.team.tExistingUserInvitationUrl` is required to be set to a value that points to the correct teams/account page. -If `emailSMS.team` is not defined at all in the current environment, the value of `externalUrls.teamSettings` (or, if not present, `externalUrls.nginz`) will be used to construct the correct url, and no configuration change is necessary. diff --git a/changelog.d/0-release-notes/WPB-10660 b/changelog.d/0-release-notes/WPB-10660 deleted file mode 100644 index 17305b2882f..00000000000 --- a/changelog.d/0-release-notes/WPB-10660 +++ /dev/null @@ -1 +0,0 @@ -charts/wire-server: There is a new config value called `background-worker.config.enableFederation` which defaults to `false`. This must be kept in sync with `tags.federation`. diff --git a/changelog.d/0-release-notes/WPB-665 b/changelog.d/0-release-notes/WPB-665 deleted file mode 100644 index 4068db3f62c..00000000000 --- a/changelog.d/0-release-notes/WPB-665 +++ /dev/null @@ -1,20 +0,0 @@ -If you are mapping an email address to the `externalId` field in the -scim schema, please check the following list for items that apply to -you and recommended steps before/during/after upgrade. - -- **Situation:** the `emails` field of in your scim user records is - empty. - - **What you need to do:** change your schema mapping to contain the - same address in `externalId` and (as a record with one element) in - `emails`. - -- **Situation:** the `emails` field of your scim user records is - non-empty. - - **What you need to do:** make sure `emails` contains exactly one - entry, which is the email from `externalId`. If there is a - discrepancy, the address from `emails` will become the new - (unvalidated) address of the user, and the user will receive an - email to validate it. If the email cannot be sent or is ignored - by the recipient, the *valid* address will not be changed. diff --git a/changelog.d/0-release-notes/WPB-8707 b/changelog.d/0-release-notes/WPB-8707 deleted file mode 100644 index 5e4ad202600..00000000000 --- a/changelog.d/0-release-notes/WPB-8707 +++ /dev/null @@ -1 +0,0 @@ -A schema migration drops column 'phone' from Brig's 'team_invitation' table. Previous releases were still reading this column. As there is no Team Settings UI action to enter a phone number, this reading will not miss to read actual phone numbers. Therefore, during deployment this will lead to benign 5xx errors. diff --git a/changelog.d/0-release-notes/configurable-argon b/changelog.d/0-release-notes/configurable-argon deleted file mode 100644 index 4a856472d06..00000000000 --- a/changelog.d/0-release-notes/configurable-argon +++ /dev/null @@ -1,33 +0,0 @@ -Password hashing can now be done using argon2id instead of scrypt. The argon2id parameters can be configured using these options: - -```yaml -brig: - optSettings: - setPasswordHashingOptions: - algorithm: argon2id - iterations: ... - memory: ... # memory needed in KiB - parallelism: ... -galley: - settings: - passwordHashingOptions: - algorithm: argon2id - iterations: ... - memory: ... # memory needed in KiB - parallelism: ... -``` - -The default option is still to use scrypt as moving to argon2id might require -allocating more resources according to configured parameters. - -When configured to use argon2id, the DB will be migrated slowly over time as the -users enter their passwords (either to login or to do other operations which -require explicit password entry). This migration is **NOT** done in reverse, -i.e., if a deployment started with argon2id as the algorithm then chose to move -to scrypt, the passwords will not get rehashed automatically, instead the users -will have to reset their passwords if that is desired. - -**NOTE** It is highly recommended to move to argon2id as it will be made the - only available choice for the `algorithm` config option in future. - -(#4291, ##) \ No newline at end of file diff --git a/changelog.d/0-release-notes/gundeck-bulk-push b/changelog.d/0-release-notes/gundeck-bulk-push deleted file mode 100644 index 8a2fb1ade4f..00000000000 --- a/changelog.d/0-release-notes/gundeck-bulk-push +++ /dev/null @@ -1,3 +0,0 @@ -Config value `gundeck.config.bulkPush` has been removed. This is purely an -internal change, in case the value was overriden to `false`, operators might see -more spiky usage of CPU and memory from gundeck due to bulk processing. \ No newline at end of file diff --git a/changelog.d/1-api-changes/WPB-10658 b/changelog.d/1-api-changes/WPB-10658 deleted file mode 100644 index a40aff74ef1..00000000000 --- a/changelog.d/1-api-changes/WPB-10658 +++ /dev/null @@ -1 +0,0 @@ -A new endpoint `POST /teams/invitations/accept` allows a non-team user to accept an invitation to join a team diff --git a/changelog.d/1-api-changes/WPB-10797 b/changelog.d/1-api-changes/WPB-10797 deleted file mode 100644 index 62f2d18d093..00000000000 --- a/changelog.d/1-api-changes/WPB-10797 +++ /dev/null @@ -1 +0,0 @@ -Services allowlist are blocked by 409 (mls-services-not-allowed) for teams with default protocol MLS. diff --git a/changelog.d/1-api-changes/WPB-11163 b/changelog.d/1-api-changes/WPB-11163 deleted file mode 100644 index df2ae5dbcc7..00000000000 --- a/changelog.d/1-api-changes/WPB-11163 +++ /dev/null @@ -1 +0,0 @@ -The `POST /clients` and `PUT /clients/:cid` endpoints support a new capability "consume-notifications" diff --git a/changelog.d/1-api-changes/WPB-685 b/changelog.d/1-api-changes/WPB-685 deleted file mode 100644 index 1dbe090ee80..00000000000 --- a/changelog.d/1-api-changes/WPB-685 +++ /dev/null @@ -1 +0,0 @@ -New variant in API version 7 of endpoints for creating and listing SCIM tokens that support a `name` field. New endpoint in version 7 for updating a SCIM token name. diff --git a/changelog.d/1-api-changes/WPB-8707 b/changelog.d/1-api-changes/WPB-8707 deleted file mode 100644 index 47f0ca8d6ef..00000000000 --- a/changelog.d/1-api-changes/WPB-8707 +++ /dev/null @@ -1 +0,0 @@ -All the phone number-based functionality is removed from the client API v6 diff --git a/changelog.d/1-api-changes/add-columns-to-export b/changelog.d/1-api-changes/add-columns-to-export deleted file mode 100644 index 04633327ba1..00000000000 --- a/changelog.d/1-api-changes/add-columns-to-export +++ /dev/null @@ -1 +0,0 @@ -The team CSV export endpoint has gained two extra columns: `last_active` and `status`. The streaming behaviour has also been improved. diff --git a/changelog.d/1-api-changes/capabilities-v7 b/changelog.d/1-api-changes/capabilities-v7 deleted file mode 100644 index 2516454b30f..00000000000 --- a/changelog.d/1-api-changes/capabilities-v7 +++ /dev/null @@ -1 +0,0 @@ -The changes to the `capabilities` field of the `Client` structure, introduced in v6, have now been postponed to v7 diff --git a/changelog.d/1-api-changes/finalise-v6 b/changelog.d/1-api-changes/finalise-v6 deleted file mode 100644 index c3a5b395701..00000000000 --- a/changelog.d/1-api-changes/finalise-v6 +++ /dev/null @@ -1 +0,0 @@ -Finalise version 6 and introduce new development version 7 (#4179, ##) diff --git a/changelog.d/1-api-changes/jwk b/changelog.d/1-api-changes/jwk deleted file mode 100644 index a7333811d14..00000000000 --- a/changelog.d/1-api-changes/jwk +++ /dev/null @@ -1 +0,0 @@ -From API version 7 the `GET /mls/public-key` and `GET /conversations/one2one/:domain/:uid` endpoints now take a `format` query parameter which can be either `raw` (default, for raw base64-encoded keys) or `jwk` (for JWK keys) (#4216, #4224) diff --git a/changelog.d/1-api-changes/one2one b/changelog.d/1-api-changes/one2one deleted file mode 100644 index c22c02444c3..00000000000 --- a/changelog.d/1-api-changes/one2one +++ /dev/null @@ -1 +0,0 @@ -`GET /conversations/one2one/:domain/:uid` now returns `public_keys` along with the conversation containing all MLS public keys for the backend which will host this conversation (since v6). \ No newline at end of file diff --git a/changelog.d/1-api-changes/ttl b/changelog.d/1-api-changes/ttl deleted file mode 100644 index 5a9d4711e68..00000000000 --- a/changelog.d/1-api-changes/ttl +++ /dev/null @@ -1 +0,0 @@ -Remove the ability to set the TTL of a feature flag. Existing TTLs are still retrieved and returned as before. Note that this only applies to the conferenceCalling feature, as none of the others supported TTL anyway. diff --git a/changelog.d/1-api-changes/wpb-10235 b/changelog.d/1-api-changes/wpb-10235 deleted file mode 100644 index 0dce921d998..00000000000 --- a/changelog.d/1-api-changes/wpb-10235 +++ /dev/null @@ -1 +0,0 @@ -Add useSFTForOneToOneCalls as a config option for the Conference Calling feature flag and make its lock status explicit. diff --git a/changelog.d/1-api-changes/wpb-10708 b/changelog.d/1-api-changes/wpb-10708 deleted file mode 100644 index cfbe92afa70..00000000000 --- a/changelog.d/1-api-changes/wpb-10708 +++ /dev/null @@ -1 +0,0 @@ -Add endpoint to upgrade a personal user to a team owner diff --git a/changelog.d/2-features/WPB-10058 b/changelog.d/2-features/WPB-10058 deleted file mode 100644 index 02fab832d8c..00000000000 --- a/changelog.d/2-features/WPB-10058 +++ /dev/null @@ -1 +0,0 @@ -DB migration for dropping `phone` column from `user` table diff --git a/changelog.d/2-features/WPB-10204 b/changelog.d/2-features/WPB-10204 deleted file mode 100644 index 40f979f1e62..00000000000 --- a/changelog.d/2-features/WPB-10204 +++ /dev/null @@ -1 +0,0 @@ -A text status field was added to user and user profile diff --git a/changelog.d/2-features/WPB-10658 b/changelog.d/2-features/WPB-10658 deleted file mode 100644 index 47da7fbabb8..00000000000 --- a/changelog.d/2-features/WPB-10658 +++ /dev/null @@ -1 +0,0 @@ -Allow an existing non-team user to migrate to a team (#4229, #4268, #4315) diff --git a/changelog.d/2-features/WPB-10772 b/changelog.d/2-features/WPB-10772 deleted file mode 100644 index 97dd0b3286b..00000000000 --- a/changelog.d/2-features/WPB-10772 +++ /dev/null @@ -1,5 +0,0 @@ -Makes it impossible for a user to join an MLS conversation while already under legalhold (at least pending) - -This implies two things: -1. If a user is under legalhold they cannot ever join an MLS conversation, not even an MLS self conversation. -2. A user has to reject to be put under legalhold when they want to join an MLS conversation (ignoring the request to be put under legalhold is not enough). diff --git a/changelog.d/2-features/WPB-11050 b/changelog.d/2-features/WPB-11050 deleted file mode 100644 index 981cab205c2..00000000000 --- a/changelog.d/2-features/WPB-11050 +++ /dev/null @@ -1 +0,0 @@ -Email template for inviting a personal user to a team added diff --git a/changelog.d/2-features/WPB-11163-consume-notifications-capability b/changelog.d/2-features/WPB-11163-consume-notifications-capability deleted file mode 100644 index 2300ebef73d..00000000000 --- a/changelog.d/2-features/WPB-11163-consume-notifications-capability +++ /dev/null @@ -1 +0,0 @@ -Clients can declare to be supporting a capability for consuming notifications diff --git a/changelog.d/2-features/WPB-1333 b/changelog.d/2-features/WPB-1333 deleted file mode 100644 index ea1394c3e33..00000000000 --- a/changelog.d/2-features/WPB-1333 +++ /dev/null @@ -1 +0,0 @@ -New endpoint to revoke an OAuth session diff --git a/changelog.d/2-features/WPB-1334 b/changelog.d/2-features/WPB-1334 deleted file mode 100644 index a9741efd7ee..00000000000 --- a/changelog.d/2-features/WPB-1334 +++ /dev/null @@ -1 +0,0 @@ -Adds a field which contains a list of all active sessions to each OAuth application in the response of `GET /oauth/applications` diff --git a/changelog.d/2-features/WPB-665 b/changelog.d/2-features/WPB-665 deleted file mode 100644 index 97fb03a1462..00000000000 --- a/changelog.d/2-features/WPB-665 +++ /dev/null @@ -1 +0,0 @@ -SCIM's emails field is now handled and the external ID is not restricted to being an email anymore diff --git a/changelog.d/2-features/WPB-685 b/changelog.d/2-features/WPB-685 deleted file mode 100644 index f7e640abc8c..00000000000 --- a/changelog.d/2-features/WPB-685 +++ /dev/null @@ -1 +0,0 @@ -Added human readable names for SCIM tokens diff --git a/changelog.d/2-features/WPB-9773 b/changelog.d/2-features/WPB-9773 deleted file mode 100644 index e7f45204eb3..00000000000 --- a/changelog.d/2-features/WPB-9773 +++ /dev/null @@ -1 +0,0 @@ -allow subconversations for MLS 1-1 conversations diff --git a/changelog.d/2-features/add-config-for-pwd-hash b/changelog.d/2-features/add-config-for-pwd-hash deleted file mode 100644 index 3ef8e186268..00000000000 --- a/changelog.d/2-features/add-config-for-pwd-hash +++ /dev/null @@ -1 +0,0 @@ -Allow choosing hashing algorithm and configuring argon2id parameters (#4291, ##) diff --git a/changelog.d/2-features/block-lh-for-mls-users b/changelog.d/2-features/block-lh-for-mls-users deleted file mode 100644 index cc86b5c4512..00000000000 --- a/changelog.d/2-features/block-lh-for-mls-users +++ /dev/null @@ -1 +0,0 @@ -Deny requests for a legalhold device for users who are part of any MLS conversations \ No newline at end of file diff --git a/changelog.d/2-features/helm-coturn-service-annotations b/changelog.d/2-features/helm-coturn-service-annotations deleted file mode 100644 index 220b536130a..00000000000 --- a/changelog.d/2-features/helm-coturn-service-annotations +++ /dev/null @@ -1 +0,0 @@ -Allow setting of Kubernetes annotations for the `coturn` Service. diff --git a/changelog.d/2-features/new-teams-mls b/changelog.d/2-features/new-teams-mls deleted file mode 100644 index 97480b3bcc0..00000000000 --- a/changelog.d/2-features/new-teams-mls +++ /dev/null @@ -1 +0,0 @@ -Add `initialConfig` setting for the `mls` feature flag diff --git a/changelog.d/2-features/no-federated-proteus b/changelog.d/2-features/no-federated-proteus deleted file mode 100644 index cfc0fcd7b7c..00000000000 --- a/changelog.d/2-features/no-federated-proteus +++ /dev/null @@ -1 +0,0 @@ -Add `federationProtocols` setting to galley, which can be used to disable the creation of federated conversations with a given protocol diff --git a/changelog.d/2-features/open-telemetry-instrumentation-brig-galley-gundeck-cannon b/changelog.d/2-features/open-telemetry-instrumentation-brig-galley-gundeck-cannon deleted file mode 100644 index 9212911e115..00000000000 --- a/changelog.d/2-features/open-telemetry-instrumentation-brig-galley-gundeck-cannon +++ /dev/null @@ -1 +0,0 @@ -added open telemetry instrumentation for brig, galley, gundeck and cannon diff --git a/changelog.d/2-features/personal-account-to-team-email b/changelog.d/2-features/personal-account-to-team-email deleted file mode 100644 index c8bbe2bf91b..00000000000 --- a/changelog.d/2-features/personal-account-to-team-email +++ /dev/null @@ -1 +0,0 @@ -Send confirmation email after adding a personal user to a new team diff --git a/changelog.d/2-features/sft-username b/changelog.d/2-features/sft-username deleted file mode 100644 index 33c2b5cfa35..00000000000 --- a/changelog.d/2-features/sft-username +++ /dev/null @@ -1 +0,0 @@ -The SFT and turn usernames returned by `/calls/config/v2` are now deterministically computed from the user ID diff --git a/changelog.d/2-features/upgrade-rabbitmq b/changelog.d/2-features/upgrade-rabbitmq deleted file mode 100644 index cead12bdd3d..00000000000 --- a/changelog.d/2-features/upgrade-rabbitmq +++ /dev/null @@ -1,6 +0,0 @@ -Use latest stable RabbitMQ version (`3.13.7`) and Helm chart (`14.6.9`). Please -note that this minor RabbitMQ version upgrade (`3.11.x` to `3.13.x`) may need -special treatment regarding existing RabbitMQ instances. See -https://www.rabbitmq.com/docs/upgrade#rabbitmq-version-upgradability . The major -Helm chart version upgrade may (depending on your setup/values) need attention -as well: https://github.com/bitnami/charts/tree/main/bitnami/rabbitmq#upgrading diff --git a/changelog.d/3-bug-fixes/PR-4152 b/changelog.d/3-bug-fixes/PR-4152 deleted file mode 100644 index 76f53d73ce4..00000000000 --- a/changelog.d/3-bug-fixes/PR-4152 +++ /dev/null @@ -1 +0,0 @@ -Fixed API version check. It has now precedence over other checks like e.g. method check. diff --git a/changelog.d/3-bug-fixes/WBP-8790 b/changelog.d/3-bug-fixes/WBP-8790 deleted file mode 100644 index 76b0c27b8a6..00000000000 --- a/changelog.d/3-bug-fixes/WBP-8790 +++ /dev/null @@ -1 +0,0 @@ -Fix handling of defaults of `mlsE2EID` feature config diff --git a/changelog.d/3-bug-fixes/WPB-10207 b/changelog.d/3-bug-fixes/WPB-10207 deleted file mode 100644 index a02d5d4d3b6..00000000000 --- a/changelog.d/3-bug-fixes/WPB-10207 +++ /dev/null @@ -1 +0,0 @@ -Match cipher suite tag in query parameters against key packages on replacing key packages diff --git a/changelog.d/3-bug-fixes/WPB-11122-disallow-searching-user-by-old-email b/changelog.d/3-bug-fixes/WPB-11122-disallow-searching-user-by-old-email deleted file mode 100644 index 85b54569f90..00000000000 --- a/changelog.d/3-bug-fixes/WPB-11122-disallow-searching-user-by-old-email +++ /dev/null @@ -1 +0,0 @@ -Users with SAML-SSO are allowed to delete their email address on the rest api. If they do that, the search indices are not updated correctly, and finding the user by the removed email address is still possible. diff --git a/changelog.d/3-bug-fixes/WPB-11925-fix-add-bot b/changelog.d/3-bug-fixes/WPB-11925-fix-add-bot deleted file mode 100644 index 9b9185ada76..00000000000 --- a/changelog.d/3-bug-fixes/WPB-11925-fix-add-bot +++ /dev/null @@ -1 +0,0 @@ -Re-add accidentally removed add-bot@v6 route in nginz, fixes #4302 diff --git a/changelog.d/3-bug-fixes/WPB-6865 b/changelog.d/3-bug-fixes/WPB-6865 deleted file mode 100644 index 31b77de070c..00000000000 --- a/changelog.d/3-bug-fixes/WPB-6865 +++ /dev/null @@ -1 +0,0 @@ -Exclude exception message from error response diff --git a/changelog.d/3-bug-fixes/ascii-text-parsing b/changelog.d/3-bug-fixes/ascii-text-parsing deleted file mode 100644 index 6472aa949f2..00000000000 --- a/changelog.d/3-bug-fixes/ascii-text-parsing +++ /dev/null @@ -1 +0,0 @@ -Return HTTP 400 instead of 500 when property key is not printable ASCII \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/ciphersuite-update b/changelog.d/3-bug-fixes/ciphersuite-update deleted file mode 100644 index 81ece68cf71..00000000000 --- a/changelog.d/3-bug-fixes/ciphersuite-update +++ /dev/null @@ -1 +0,0 @@ -move cipher suite updates into the commit lock diff --git a/changelog.d/3-bug-fixes/flag-defaults b/changelog.d/3-bug-fixes/flag-defaults deleted file mode 100644 index 52463a2a092..00000000000 --- a/changelog.d/3-bug-fixes/flag-defaults +++ /dev/null @@ -1 +0,0 @@ -Fix feature flag default calculation for `mlsMigration` and `enforceFileDownloadLocation` diff --git a/changelog.d/3-bug-fixes/max-properties b/changelog.d/3-bug-fixes/max-properties deleted file mode 100644 index 4273020c7e2..00000000000 --- a/changelog.d/3-bug-fixes/max-properties +++ /dev/null @@ -1 +0,0 @@ -Allow setting existing properties even if we have max properties \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/remove-spam-from-nginx b/changelog.d/3-bug-fixes/remove-spam-from-nginx deleted file mode 100644 index 7167a858f0a..00000000000 --- a/changelog.d/3-bug-fixes/remove-spam-from-nginx +++ /dev/null @@ -1 +0,0 @@ -removed spam from nginx (nginz) by using the new style http/2 directive diff --git a/changelog.d/3-bug-fixes/services-tags b/changelog.d/3-bug-fixes/services-tags deleted file mode 100644 index 9d0ef1900f7..00000000000 --- a/changelog.d/3-bug-fixes/services-tags +++ /dev/null @@ -1 +0,0 @@ -brig: Make `GET /services/tags` work again \ No newline at end of file diff --git a/changelog.d/3-bug-fixes/ses-notifications b/changelog.d/3-bug-fixes/ses-notifications deleted file mode 100644 index be2735b450d..00000000000 --- a/changelog.d/3-bug-fixes/ses-notifications +++ /dev/null @@ -1 +0,0 @@ -Process bounce and complaint notifications from SES correctly. \ No newline at end of file diff --git a/changelog.d/4-docs/WPB-11502 b/changelog.d/4-docs/WPB-11502 deleted file mode 100644 index 30382d30c3e..00000000000 --- a/changelog.d/4-docs/WPB-11502 +++ /dev/null @@ -1 +0,0 @@ -Call graph of federated endpoints was removed from the docs diff --git a/changelog.d/4-docs/WPB-9742 b/changelog.d/4-docs/WPB-9742 deleted file mode 100644 index c6fbdf93714..00000000000 --- a/changelog.d/4-docs/WPB-9742 +++ /dev/null @@ -1 +0,0 @@ -Restored LegalHold internal API swagger as part of Brig. diff --git a/changelog.d/4-docs/fix-swagger b/changelog.d/4-docs/fix-swagger deleted file mode 100644 index 394aaf48d83..00000000000 --- a/changelog.d/4-docs/fix-swagger +++ /dev/null @@ -1 +0,0 @@ -Fix: show openapi docs for blocked versions diff --git a/changelog.d/4-docs/fix-swagger-2 b/changelog.d/4-docs/fix-swagger-2 deleted file mode 100644 index 118fc6d712e..00000000000 --- a/changelog.d/4-docs/fix-swagger-2 +++ /dev/null @@ -1 +0,0 @@ -Move docs from docs.wire.com to generated helper page served by brig \ No newline at end of file diff --git a/changelog.d/4-docs/mls-test-tags b/changelog.d/4-docs/mls-test-tags deleted file mode 100644 index 56e9b4b3b0a..00000000000 --- a/changelog.d/4-docs/mls-test-tags +++ /dev/null @@ -1 +0,0 @@ -Deleted proteus-specific test documentation tags and added some new tags to MLS tests diff --git a/changelog.d/4-docs/openapi-validation b/changelog.d/4-docs/openapi-validation deleted file mode 100644 index 21512f1387d..00000000000 --- a/changelog.d/4-docs/openapi-validation +++ /dev/null @@ -1 +0,0 @@ -Fix openapi validation errors (#4295, ##) diff --git a/changelog.d/4-docs/revert-wpb8628 b/changelog.d/4-docs/revert-wpb8628 deleted file mode 100644 index 4400ff6154a..00000000000 --- a/changelog.d/4-docs/revert-wpb8628 +++ /dev/null @@ -1 +0,0 @@ -Re-introduce test case tags for BSI audit (revert #4041) \ No newline at end of file diff --git a/changelog.d/5-internal/WBP-11188 b/changelog.d/5-internal/WBP-11188 deleted file mode 100644 index 9965120d794..00000000000 --- a/changelog.d/5-internal/WBP-11188 +++ /dev/null @@ -1 +0,0 @@ -Introduced API versioning and version negotiation for external LegalHold Service supporting `v0` and `v1` diff --git a/changelog.d/5-internal/WPB-10302 b/changelog.d/5-internal/WPB-10302 deleted file mode 100644 index 8780ddd6ac7..00000000000 --- a/changelog.d/5-internal/WPB-10302 +++ /dev/null @@ -1 +0,0 @@ -Read sftTokenSecret from secrets.yaml and mount to /etc/wire/brig/secrets/sftTokenSecret by default diff --git a/changelog.d/5-internal/WPB-10335 b/changelog.d/5-internal/WPB-10335 deleted file mode 100644 index cf6ebf9798a..00000000000 --- a/changelog.d/5-internal/WPB-10335 +++ /dev/null @@ -1 +0,0 @@ -Added node based topology constraint to ensure pods are distributed uniformly on all nodes. diff --git a/changelog.d/5-internal/WPB-10424 b/changelog.d/5-internal/WPB-10424 deleted file mode 100644 index b635cc8d10e..00000000000 --- a/changelog.d/5-internal/WPB-10424 +++ /dev/null @@ -1 +0,0 @@ -Move smallstep-accomp` helm charts to `wireapp/helm-charts` diff --git a/changelog.d/5-internal/WPB-10581-remove-coturn-helm-chart b/changelog.d/5-internal/WPB-10581-remove-coturn-helm-chart deleted file mode 100644 index a9a37a85fdc..00000000000 --- a/changelog.d/5-internal/WPB-10581-remove-coturn-helm-chart +++ /dev/null @@ -1 +0,0 @@ -Remove coturn helm chart. It is moved to `wireapp/coturn`. diff --git a/changelog.d/5-internal/WPB-11000 b/changelog.d/5-internal/WPB-11000 deleted file mode 100644 index d489cc80d7e..00000000000 --- a/changelog.d/5-internal/WPB-11000 +++ /dev/null @@ -1 +0,0 @@ -Additional test for password reset, port tests to new integration test suite diff --git a/changelog.d/5-internal/WPB-11101 b/changelog.d/5-internal/WPB-11101 deleted file mode 100644 index 09b5c427420..00000000000 --- a/changelog.d/5-internal/WPB-11101 +++ /dev/null @@ -1 +0,0 @@ -Remove unused invitation tables from brig. diff --git a/changelog.d/5-internal/WPB-11101-internal-types b/changelog.d/5-internal/WPB-11101-internal-types deleted file mode 100644 index bf92f52b5ce..00000000000 --- a/changelog.d/5-internal/WPB-11101-internal-types +++ /dev/null @@ -1 +0,0 @@ -Improve abstraction in the invitation store and hide DB interaction-specific internal types from the application code. diff --git a/changelog.d/5-internal/WPB-11217-move-code-for-accepting-invitations-for-personal-users-into-teams-to-wire-subsystems b/changelog.d/5-internal/WPB-11217-move-code-for-accepting-invitations-for-personal-users-into-teams-to-wire-subsystems deleted file mode 100644 index 0d0f46a242f..00000000000 --- a/changelog.d/5-internal/WPB-11217-move-code-for-accepting-invitations-for-personal-users-into-teams-to-wire-subsystems +++ /dev/null @@ -1,10 +0,0 @@ -Move some invitation handling from brig to wire-subsystems. - -- introduce cyclically dependent effects: UserSubsystem, AuthenticationSubsystem (see Brig.CanonicalInterpreter). -- introduce TeamInvitationSubsystem with operations inviteUser, internalCreateInvitation. -- add verifyPassword to AuthenticationSubsystem. -- add sendInvitationMail, sendInvitationMailPersonalUser to EmailSubsystem. -- add getTeamSize to IndexedUserStore (this is morally internal to wire-subsystems, and making another ES subsystem would mean adding a lot of code everywhere). -- add updateUserTeam to UserStore. -- add acceptTeamInvitation, internalFindTeamInvitation to UserSubsystem. -- make a few small rest api handlers in brig polysemic (Handler -> Sem). diff --git a/changelog.d/5-internal/WPB-11301-db-tool-team-info b/changelog.d/5-internal/WPB-11301-db-tool-team-info deleted file mode 100644 index e1cda09aa88..00000000000 --- a/changelog.d/5-internal/WPB-11301-db-tool-team-info +++ /dev/null @@ -1 +0,0 @@ -tools/db/team-info: collects last login times of all team members \ No newline at end of file diff --git a/changelog.d/5-internal/WPB-11386-map-range b/changelog.d/5-internal/WPB-11386-map-range deleted file mode 100644 index a01f45001c9..00000000000 --- a/changelog.d/5-internal/WPB-11386-map-range +++ /dev/null @@ -1 +0,0 @@ -Introduce length-preserving function mapRange to replace Functor instance for Range data type. \ No newline at end of file diff --git a/changelog.d/5-internal/WPB-11502 b/changelog.d/5-internal/WPB-11502 deleted file mode 100644 index 73be54702fe..00000000000 --- a/changelog.d/5-internal/WPB-11502 +++ /dev/null @@ -1 +0,0 @@ -TransitiveAnns compiler plugin was removed diff --git a/changelog.d/5-internal/WPB-1220-servantify-proxy-internal b/changelog.d/5-internal/WPB-1220-servantify-proxy-internal deleted file mode 100644 index f161136a346..00000000000 --- a/changelog.d/5-internal/WPB-1220-servantify-proxy-internal +++ /dev/null @@ -1 +0,0 @@ -Servantify internal routing table for proxy. diff --git a/changelog.d/5-internal/WPB-1228-servantify-gundeck-internal-api b/changelog.d/5-internal/WPB-1228-servantify-gundeck-internal-api deleted file mode 100644 index 477a424b664..00000000000 --- a/changelog.d/5-internal/WPB-1228-servantify-gundeck-internal-api +++ /dev/null @@ -1 +0,0 @@ -Servantify gundeck internal api diff --git a/changelog.d/5-internal/WPB-888-2 b/changelog.d/5-internal/WPB-888-2 deleted file mode 100644 index b898071cea8..00000000000 --- a/changelog.d/5-internal/WPB-888-2 +++ /dev/null @@ -1 +0,0 @@ -Removed `indexReindex` and `indexReindexIfSameOrNewer` from internal Brig/SearchIndex. diff --git a/changelog.d/5-internal/WPB-8888 b/changelog.d/5-internal/WPB-8888 deleted file mode 100644 index f5d3655308a..00000000000 --- a/changelog.d/5-internal/WPB-8888 +++ /dev/null @@ -1 +0,0 @@ -Introduced ElasticSearch effects related to user search. diff --git a/changelog.d/5-internal/WPB-8892 b/changelog.d/5-internal/WPB-8892 deleted file mode 100644 index e808269195c..00000000000 --- a/changelog.d/5-internal/WPB-8892 +++ /dev/null @@ -1 +0,0 @@ -Brig was refactored by pulling out email block-listing into a wire subsystems effect, and its actions are exposed via the user subsystem. diff --git a/changelog.d/5-internal/background-worker b/changelog.d/5-internal/background-worker deleted file mode 100644 index 35afaff745f..00000000000 --- a/changelog.d/5-internal/background-worker +++ /dev/null @@ -1 +0,0 @@ -charts/wire-server: Deploy background-worker even when tags.federation is `false` (#4342, #4248) diff --git a/changelog.d/5-internal/email-templates-v1.0.122 b/changelog.d/5-internal/email-templates-v1.0.122 deleted file mode 100644 index d9bfa9e0a5d..00000000000 --- a/changelog.d/5-internal/email-templates-v1.0.122 +++ /dev/null @@ -1 +0,0 @@ -Updated email templates to v1.0.122 diff --git a/changelog.d/5-internal/feature-flag-refactoring-1 b/changelog.d/5-internal/feature-flag-refactoring-1 deleted file mode 100644 index 92f0a33d35a..00000000000 --- a/changelog.d/5-internal/feature-flag-refactoring-1 +++ /dev/null @@ -1,7 +0,0 @@ -Refactor feature flags -- Improved naming slightly. Features types are now called `Feature`, `LockableFeature` and `LockableFeaturePatch` -- Turned `AllFeatures` into an extensible record type -- Removed `WithStatusBase` barbie. -- Deleted obsolete `computeFeatureConfigForTeamUser` -- Abstracted `getFeature` and `setFeature` -- Abstracted getAllTeamFeatures diff --git a/changelog.d/5-internal/feature-flag-refactoring-2 b/changelog.d/5-internal/feature-flag-refactoring-2 deleted file mode 100644 index 8c985d1f6b3..00000000000 --- a/changelog.d/5-internal/feature-flag-refactoring-2 +++ /dev/null @@ -1 +0,0 @@ -Clean up and reorganise feature flag endpoints diff --git a/changelog.d/5-internal/feature-flag-refactoring-3 b/changelog.d/5-internal/feature-flag-refactoring-3 deleted file mode 100644 index 62a75a4b38d..00000000000 --- a/changelog.d/5-internal/feature-flag-refactoring-3 +++ /dev/null @@ -1 +0,0 @@ -Clean up feature default configuration code diff --git a/changelog.d/5-internal/federation-v1 b/changelog.d/5-internal/federation-v1 deleted file mode 100644 index 01960024d5d..00000000000 --- a/changelog.d/5-internal/federation-v1 +++ /dev/null @@ -1 +0,0 @@ -Add federation-v1 environment for testing compatibility of the federation API with version 1 diff --git a/changelog.d/5-internal/fix-galley-overlaps b/changelog.d/5-internal/fix-galley-overlaps deleted file mode 100644 index 784bbe17f2a..00000000000 --- a/changelog.d/5-internal/fix-galley-overlaps +++ /dev/null @@ -1 +0,0 @@ -Fix overlapping paths errors in galley's internal API diff --git a/changelog.d/5-internal/fix-local-fed-v1 b/changelog.d/5-internal/fix-local-fed-v1 deleted file mode 100644 index 2bff4d110c1..00000000000 --- a/changelog.d/5-internal/fix-local-fed-v1 +++ /dev/null @@ -1 +0,0 @@ -Local integration tests of federation version V1 fixed diff --git a/changelog.d/5-internal/fix-nginx-paths b/changelog.d/5-internal/fix-nginx-paths deleted file mode 100644 index 0d7bd115c65..00000000000 --- a/changelog.d/5-internal/fix-nginx-paths +++ /dev/null @@ -1 +0,0 @@ -nginz/local-conf: Update list of endpoints \ No newline at end of file diff --git a/changelog.d/5-internal/gundeck-internal-swagger b/changelog.d/5-internal/gundeck-internal-swagger deleted file mode 100644 index da4ac4f9e1f..00000000000 --- a/changelog.d/5-internal/gundeck-internal-swagger +++ /dev/null @@ -1 +0,0 @@ -Expose gundeck internal API on swagger. Mv some types and routes to wire-api. \ No newline at end of file diff --git a/changelog.d/5-internal/inbucket b/changelog.d/5-internal/inbucket deleted file mode 100644 index 12334d3b1d6..00000000000 --- a/changelog.d/5-internal/inbucket +++ /dev/null @@ -1 +0,0 @@ -dockerephemeral: Use inbucket for SMTP \ No newline at end of file diff --git a/changelog.d/5-internal/make-crm b/changelog.d/5-internal/make-crm deleted file mode 100644 index eb4df600ece..00000000000 --- a/changelog.d/5-internal/make-crm +++ /dev/null @@ -1 +0,0 @@ -Makefile: Add target `crm` to run services tuned for manual usage \ No newline at end of file diff --git a/changelog.d/5-internal/migrate-postgres-chart b/changelog.d/5-internal/migrate-postgres-chart deleted file mode 100644 index bdd556d76b1..00000000000 --- a/changelog.d/5-internal/migrate-postgres-chart +++ /dev/null @@ -1 +0,0 @@ -Postgresql helm chart is removed from charts/ directory and migrated to wireapp/helm-charts repo diff --git a/changelog.d/5-internal/new-team-types-refactoring b/changelog.d/5-internal/new-team-types-refactoring deleted file mode 100644 index 70b4ade0568..00000000000 --- a/changelog.d/5-internal/new-team-types-refactoring +++ /dev/null @@ -1 +0,0 @@ -Simplify NewTeam and related types and remove lenses diff --git a/changelog.d/5-internal/openapi-validation b/changelog.d/5-internal/openapi-validation deleted file mode 100644 index 02263c5c73d..00000000000 --- a/changelog.d/5-internal/openapi-validation +++ /dev/null @@ -1 +0,0 @@ -Add openapi validation test to integration diff --git a/changelog.d/5-internal/optimize-list-users b/changelog.d/5-internal/optimize-list-users deleted file mode 100644 index b2880c8542d..00000000000 --- a/changelog.d/5-internal/optimize-list-users +++ /dev/null @@ -1 +0,0 @@ -Optimize getting a lot of users by concurrently getting target users \ No newline at end of file diff --git a/changelog.d/5-internal/pre-stop b/changelog.d/5-internal/pre-stop deleted file mode 100644 index f7d0c0cf0fe..00000000000 --- a/changelog.d/5-internal/pre-stop +++ /dev/null @@ -1 +0,0 @@ -charts/{brig,galley}: Allow setting a preStop hook for the deployments diff --git a/changelog.d/5-internal/property-subsystem b/changelog.d/5-internal/property-subsystem deleted file mode 100644 index 6ef618ff81e..00000000000 --- a/changelog.d/5-internal/property-subsystem +++ /dev/null @@ -1 +0,0 @@ -Introduce proeprty subsytem \ No newline at end of file diff --git a/changelog.d/5-internal/refactor-email b/changelog.d/5-internal/refactor-email deleted file mode 100644 index 9e2e91c7804..00000000000 --- a/changelog.d/5-internal/refactor-email +++ /dev/null @@ -1 +0,0 @@ -Factored out our Email type in favour of EmailAddress from email-validate. diff --git a/changelog.d/5-internal/test-csv-export b/changelog.d/5-internal/test-csv-export deleted file mode 100644 index a8df725542b..00000000000 --- a/changelog.d/5-internal/test-csv-export +++ /dev/null @@ -1 +0,0 @@ -Move CSV export test to integration diff --git a/changelog.d/5-internal/todo b/changelog.d/5-internal/todo deleted file mode 100644 index 6326d872e23..00000000000 --- a/changelog.d/5-internal/todo +++ /dev/null @@ -1 +0,0 @@ -add the TODO pattern and the todo function to Imports diff --git a/changelog.d/5-internal/user-features b/changelog.d/5-internal/user-features deleted file mode 100644 index 785bf2dc38a..00000000000 --- a/changelog.d/5-internal/user-features +++ /dev/null @@ -1 +0,0 @@ -Refactor user feature logic diff --git a/changelog.d/5-internal/user-types-refactoring b/changelog.d/5-internal/user-types-refactoring deleted file mode 100644 index f3684d7f309..00000000000 --- a/changelog.d/5-internal/user-types-refactoring +++ /dev/null @@ -1 +0,0 @@ -Remove `UserAccount` and `ExtendedUserAccount` and their fields to the `User` type diff --git a/changelog.d/5-internal/weed b/changelog.d/5-internal/weed deleted file mode 100644 index 03b7ed904d9..00000000000 --- a/changelog.d/5-internal/weed +++ /dev/null @@ -1 +0,0 @@ -Started weeding out dead code. diff --git a/changelog.d/5-internal/wpb-8887 b/changelog.d/5-internal/wpb-8887 deleted file mode 100644 index 087d81745a8..00000000000 --- a/changelog.d/5-internal/wpb-8887 +++ /dev/null @@ -1 +0,0 @@ -New user subsystem operation `getAccountsBy` for complex account lookups. diff --git a/changelog.d/5-internal/wpb-9844 b/changelog.d/5-internal/wpb-9844 deleted file mode 100644 index cbf16c484b3..00000000000 --- a/changelog.d/5-internal/wpb-9844 +++ /dev/null @@ -1 +0,0 @@ -Added warning when deploying wire-server helm chart with User/Team creation over internet enabled. From c11434b86fd6373dc96aa5237ce305947c8fd905 Mon Sep 17 00:00:00 2001 From: Amit Sagtani Date: Thu, 31 Oct 2024 14:11:08 +0100 Subject: [PATCH 14/27] block access to assets.*/minio/ path (#4297) * add rules to allow signed url * Revert "add rules to allow signed url" This reverts commit ef471492c6cccf8d24c4e7f5579fe7e62c1da7d3. * add seperate template for minio ingress * remove minio rules from from existing template * add changelog --- changelog.d/5-internal/WPB-11791 | 1 + .../templates/ingress.yaml | 22 --------- .../templates/ingress_minio.yaml | 45 +++++++++++++++++++ 3 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 changelog.d/5-internal/WPB-11791 create mode 100644 charts/nginx-ingress-services/templates/ingress_minio.yaml diff --git a/changelog.d/5-internal/WPB-11791 b/changelog.d/5-internal/WPB-11791 new file mode 100644 index 00000000000..b3aa41c0df6 --- /dev/null +++ b/changelog.d/5-internal/WPB-11791 @@ -0,0 +1 @@ +Block access to assets.*/minio/ path for public access. diff --git a/charts/nginx-ingress-services/templates/ingress.yaml b/charts/nginx-ingress-services/templates/ingress.yaml index 633f27e2625..c010af5da53 100644 --- a/charts/nginx-ingress-services/templates/ingress.yaml +++ b/charts/nginx-ingress-services/templates/ingress.yaml @@ -51,9 +51,6 @@ spec: {{- if .Values.webapp.enabled }} - {{ .Values.config.dns.webapp }} {{- end }} -{{- if .Values.fakeS3.enabled }} - - {{ .Values.config.dns.fakeS3 }} -{{- end }} {{- if .Values.teamSettings.enabled }} - {{ .Values.config.dns.teamSettings }} {{- end }} @@ -117,25 +114,6 @@ spec: servicePort: {{ .Values.service.webapp.externalPort }} {{- end }} {{- end }} -{{- if .Values.fakeS3.enabled }} - - host: {{ .Values.config.dns.fakeS3 }} - http: - paths: - - path: / - {{- if $ingressSupportsPathType }} - pathType: Prefix - {{- end }} - backend: - {{- if $apiIsStable }} - service: - name: {{ .Values.service.s3.serviceName }} - port: - number: {{ .Values.service.s3.externalPort }} - {{- else }} - serviceName: {{ .Values.service.s3.serviceName }} - servicePort: {{ .Values.service.s3.externalPort }} - {{- end }} -{{- end }} {{- if .Values.teamSettings.enabled }} - host: {{ .Values.config.dns.teamSettings }} http: diff --git a/charts/nginx-ingress-services/templates/ingress_minio.yaml b/charts/nginx-ingress-services/templates/ingress_minio.yaml new file mode 100644 index 00000000000..e42df51389c --- /dev/null +++ b/charts/nginx-ingress-services/templates/ingress_minio.yaml @@ -0,0 +1,45 @@ +{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}} +{{- $ingressFieldNotAnnotation := eq (include "ingress.FieldNotAnnotation" .) "true" -}} +{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}} +{{- if .Values.fakeS3.enabled }} +# We use a separate ingress for minio because we want to restrict access to /minio/ path +# for security reasons +apiVersion: {{ include "ingress.apiVersion" . }} +kind: Ingress +metadata: + name: minio-ingress + annotations: + {{- if not $ingressFieldNotAnnotation }} + kubernetes.io/ingress.class: "{{ .Values.config.ingressClass }}" + {{- end }} + nginx.ingress.kubernetes.io/server-snippet: | + location /minio/ { + return 403; + } +spec: + {{- if $ingressFieldNotAnnotation }} + ingressClassName: "{{ .Values.config.ingressClass }}" + {{- end }} + tls: + - hosts: + - {{ .Values.config.dns.fakeS3 }} + secretName: {{ include "nginx-ingress-services.getCertificateSecretName" . | quote }} + rules: + - host: {{ .Values.config.dns.fakeS3 }} + http: + paths: + - path: / + {{- if $ingressSupportsPathType }} + pathType: Prefix + {{- end }} + backend: + {{- if $apiIsStable }} + service: + name: {{ .Values.service.s3.serviceName }} + port: + number: {{ .Values.service.s3.externalPort }} + {{- else }} + serviceName: {{ .Values.service.s3.serviceName }} + servicePort: {{ .Values.service.s3.externalPort }} + {{- end }} +{{- end }} From 2d98df49e6245a3c32c69cc990d3beccbccb8a77 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 4 Nov 2024 11:25:41 +0100 Subject: [PATCH 15/27] WPB-2568 Fix shellcheck linting problems on all shell scripts (#4220) --- Makefile | 6 ++-- changelog.d/5-internal/pr-4220-linting | 1 + changelog.d/mk-changelog.sh | 6 ++-- deploy/dockerephemeral/init.sh | 2 +- hack/bin/cabal-run-tests.sh | 4 +-- hack/bin/copy-charts.sh | 12 +++---- hack/bin/create_team_members.sh | 2 +- hack/bin/create_test_team_admins.sh | 5 +-- hack/bin/create_test_team_members.sh | 4 +-- hack/bin/create_test_team_scim.sh | 36 +++++++------------ hack/bin/create_test_user.sh | 13 +++---- hack/bin/diff-failure.sh | 4 +-- hack/bin/helm-template.sh | 2 +- hack/bin/integration-setup-federation.sh | 3 +- hack/bin/integration-spring-cleaning.sh | 4 --- hack/bin/integration-teardown-federation.sh | 1 + hack/bin/integration-test-logs.sh | 2 +- hack/bin/register_idp_internal.sh | 2 +- hack/bin/serve-charts.sh | 10 +++--- hack/bin/set-chart-image-version.sh | 2 +- hack/bin/set-helm-chart-version.sh | 3 +- hack/bin/upload-helm-charts-s3.sh | 5 +-- libs/wire-api/test/golden/gentests.sh | 9 ++--- services/brig/federation-tests.sh | 2 +- services/nginz/nginz_reload.sh | 8 +++-- .../spar/test-scim-suite/mk_collection.sh | 6 ++-- services/spar/test-scim-suite/run.sh | 27 ++++++++------ services/spar/test-scim-suite/runsuite.sh | 7 ++-- tools/db/move-team/dump_merge_teams.sh | 8 ++--- tools/nginz_disco/nginz_disco.sh | 16 ++++++--- tools/ormolu.sh | 4 +-- tools/rebase-onto-formatter.sh | 7 ++-- tools/sftd_disco/sftd_disco.sh | 22 ++++++------ treefmt.toml | 36 ++----------------- 34 files changed, 127 insertions(+), 154 deletions(-) create mode 100644 changelog.d/5-internal/pr-4220-linting diff --git a/Makefile b/Makefile index 77a68d9cc10..ea55403761f 100644 --- a/Makefile +++ b/Makefile @@ -250,11 +250,11 @@ add-license: .PHONY: treefmt treefmt: - treefmt -u debug - + treefmt -u debug --walk=git + .PHONY: treefmt-check treefmt-check: - treefmt --fail-on-change -u debug + treefmt --fail-on-change -u debug --walk=git ################################# ## docker targets diff --git a/changelog.d/5-internal/pr-4220-linting b/changelog.d/5-internal/pr-4220-linting new file mode 100644 index 00000000000..4bee98ed532 --- /dev/null +++ b/changelog.d/5-internal/pr-4220-linting @@ -0,0 +1 @@ +Fix shellcheck problems in all shell scripts diff --git a/changelog.d/mk-changelog.sh b/changelog.d/mk-changelog.sh index 550e024738c..d632d5ea863 100755 --- a/changelog.d/mk-changelog.sh +++ b/changelog.d/mk-changelog.sh @@ -6,7 +6,7 @@ shopt -s nullglob DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" getPRNumber() { - git log --reverse --format=%s -- $1 | sed -rn '1 { /\((#.*)\)$/ s|^.*\((#.*)\)$|\1|p; }' | grep "" || + git log --reverse --format=%s -- "$1" | sed -rn '1 { /\((#.*)\)$/ s|^.*\((#.*)\)$|\1|p; }' | grep "" || echo "#PR_NOT_FOUND" } @@ -18,10 +18,12 @@ for d in "$DIR"/*; do if [[ ${#entries[@]} -eq 0 ]]; then continue; fi echo -n "## " + # shellcheck disable=SC1003 sed '$ a\' "$d/.title" echo "" for f in "${entries[@]}"; do - pr=$(getPRNumber $f) + pr=$(getPRNumber "$f") + # shellcheck disable=SC1003 sed -r ' # create a bullet point on the first line 1 { s/^/\* /; } diff --git a/deploy/dockerephemeral/init.sh b/deploy/dockerephemeral/init.sh index 676e1b4a106..d80a7cb4c1f 100755 --- a/deploy/dockerephemeral/init.sh +++ b/deploy/dockerephemeral/init.sh @@ -12,7 +12,7 @@ aws configure set aws_secret_access_key dummysecret aws configure set region eu-west-1 # Potentially delete pre-existing tables -echo -n "waiting for dynamo: " +echo "waiting for dynamo: " while (! aws --endpoint-url=http://dynamodb:8000 --cli-connect-timeout=1 dynamodb list-tables); do sleep 1; done diff --git a/hack/bin/cabal-run-tests.sh b/hack/bin/cabal-run-tests.sh index 46a9099ecad..c2ea19287e0 100755 --- a/hack/bin/cabal-run-tests.sh +++ b/hack/bin/cabal-run-tests.sh @@ -14,8 +14,8 @@ fi for cabal in $(find "$TOP_LEVEL" -name "$pattern" | grep -v dist-newstyle); do # This is required because some tests (e.g. golden tests) must be run from # the package root. - cd "$(dirname $cabal)" - package="$(basename ${cabal%.*})" + cd "$(dirname "$cabal")" + package="$(basename "${cabal%.*}")" for test_suite in $(cabal-plan list-bins "$package:test:*" | awk '{print $2}'); do $test_suite "${@:2}" done diff --git a/hack/bin/copy-charts.sh b/hack/bin/copy-charts.sh index b0dfb3bff38..e168e89d5a8 100755 --- a/hack/bin/copy-charts.sh +++ b/hack/bin/copy-charts.sh @@ -9,21 +9,21 @@ CHART=${1:?$USAGE} TOP_LEVEL="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )" CHART_SOURCE=$TOP_LEVEL/charts -CHART_DIST=$TOP_LEVEL/.local/charts +CHART_DEST=$TOP_LEVEL/.local/charts # TODO sanity check folder must exist mkdir -p .local/charts -rm -rf "$CHART_DIST/$CHART" -cp -r "$CHART_SOURCE/$CHART" "$CHART_DIST/" +rm -rf "${CHART_DEST:?}/$CHART" +cp -r "$CHART_SOURCE/$CHART" "$CHART_DEST/" if [ -f "$CHART_SOURCE/$CHART/requirements.yaml" ]; then # very hacky bash, I'm sorry for subpath in $(grep "file://" "$CHART_SOURCE/$CHART/requirements.yaml" | awk '{ print $2 }' | xargs -n 1 | cut -c 8-) do - rm -rf "$CHART_DIST/$CHART/$subpath" - cp -r "$CHART_SOURCE/$CHART/$subpath" "$CHART_DIST/" + rm -rf "${CHART_DEST:?}/$CHART/$subpath" + cp -r "$CHART_SOURCE/$CHART/$subpath" "$CHART_DEST/" done fi -echo "copied $CHART_SOURCE/$CHART (and its local dependencies) to $CHART_DIST/$CHART" +echo "copied $CHART_SOURCE/$CHART (and its local dependencies) to $CHART_DEST/$CHART" diff --git a/hack/bin/create_team_members.sh b/hack/bin/create_team_members.sh index 42740024dcd..1988fbd62cb 100755 --- a/hack/bin/create_team_members.sh +++ b/hack/bin/create_team_members.sh @@ -33,7 +33,7 @@ $ grep code out.log | grep -v email-exists If you are in a hurry, you may want to change the sleep(1) at the end of the invite loop to less than a second. If you want to give up on -the first error, add an exit(1) where we check the $INVIDATION_ID. +the first error, add an exit(1) where we check the INVITATION_ID. " diff --git a/hack/bin/create_test_team_admins.sh b/hack/bin/create_test_team_admins.sh index 625da458b1b..6fffc6ef279 100755 --- a/hack/bin/create_test_team_admins.sh +++ b/hack/bin/create_test_team_admins.sh @@ -51,10 +51,11 @@ fi # Generate users +#shellcheck disable=SC2034 for i in $(seq 1 "$COUNT") do - EMAIL=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8)"@example.com" - PASSWORD=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8) + EMAIL=$(env LC_CTYPE=C tr -dc a-zA-Z0-9 < /dev/urandom | head -c 8)"@example.com" + PASSWORD=$(env LC_CTYPE=C tr -dc a-zA-Z0-9 < /dev/urandom | head -c 8) CURL_OUT=$(curl -i -s --show-error \ -XPOST "$BRIG_HOST/i/users" \ diff --git a/hack/bin/create_test_team_members.sh b/hack/bin/create_test_team_members.sh index 6a55f4a1b0c..417253b94f3 100755 --- a/hack/bin/create_test_team_members.sh +++ b/hack/bin/create_test_team_members.sh @@ -84,7 +84,7 @@ END=$((COUNT + START - 1)) for i in $(seq "$START" "$END") do EMAIL='w'$(printf "%03d" "$i")"@$TARGET_EMAIL_DOMAIN" - PASSWORD=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8) + PASSWORD=$(env LC_CTYPE=C tr -dc a-zA-Z0-9 < /dev/urandom | head -c 8) # Generate the invitation @@ -125,7 +125,7 @@ do if [ "$TEAM" != "$TEAM_UUID" ]; then echo "unexpected error: user got assigned to no / the wrong team?!" - echo ${CURL_OUT} + echo "${CURL_OUT}" exit 1 fi diff --git a/hack/bin/create_test_team_scim.sh b/hack/bin/create_test_team_scim.sh index 552b4e15c1f..fcbb4498b53 100755 --- a/hack/bin/create_test_team_scim.sh +++ b/hack/bin/create_test_team_scim.sh @@ -45,8 +45,8 @@ if [ "$#" -ne 0 ]; then fi -ADMIN_EMAIL=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8)"@example.com" -ADMIN_PASSWORD=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8) +ADMIN_EMAIL=$(env LC_CTYPE=C tr -dc a-zA-Z0-9 < /dev/urandom | head -c 8)"@example.com" +ADMIN_PASSWORD=$(env LC_CTYPE=C tr -dc a-zA-Z0-9 < /dev/urandom | head -c 8) CURL_OUT=$(curl -i -s --show-error \ -XPOST "$BRIG_HOST/i/users" \ @@ -61,23 +61,23 @@ BEARER=$(curl -X POST \ --header 'Content-Type: application/json' \ --header 'Accept: application/json' \ -d '{"email":"'"$ADMIN_EMAIL"'","password":"'"$ADMIN_PASSWORD"'"}' \ - $BRIG_HOST/login'?persist=false' | jq -r .access_token) + "$BRIG_HOST"/login'?persist=false' | jq -r .access_token) SCIM_TOKEN_FULL=$(curl -X POST \ --header "Authorization: Bearer $BEARER" \ --header 'Content-Type: application/json;charset=utf-8' \ --header 'Z-User: '"$ADMIN_UUID" \ - -d '{ "description": "test '"`date`"'", "password": "'"$ADMIN_PASSWORD"'" }' \ - $SPAR_HOST/scim/auth-tokens) + -d '{ "description": "test '"$(date)"'", "password": "'"$ADMIN_PASSWORD"'" }' \ + "$SPAR_HOST/scim/auth-tokens") -SCIM_TOKEN=$(echo $SCIM_TOKEN_FULL | jq -r .token) -SCIM_TOKEN_ID=$(echo $SCIM_TOKEN_FULL | jq -r .info.id) +SCIM_TOKEN=$(echo "$SCIM_TOKEN_FULL" | jq -r .token) +SCIM_TOKEN_ID=$(echo "$SCIM_TOKEN_FULL" | jq -r .info.id) # Create regular user via team invitation -REGULAR_USER_EMAIL=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8)"@example.com" -REGULAR_USER_PASSWORD=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8) +REGULAR_USER_EMAIL=$(env LC_CTYPE=C tr -dc a-zA-Z0-9 < /dev/urandom | head -c 8)"@example.com" +REGULAR_USER_PASSWORD=$(env LC_CTYPE=C tr -dc a-zA-Z0-9 < /dev/urandom | head -c 8) CURL_OUT_INVITATION=$(curl -i -s --show-error \ -XPOST "$BRIG_HOST/teams/$TEAM_UUID/invitations" \ -H'Content-type: application/json' \ @@ -122,7 +122,7 @@ REGULAR_TEAM_MEMBER_UUID=$(echo "$CURL_OUT" | tail -1 | sed 's/.*\"id\":\"\([a-z # Create user via SCIM invitation -scimUserName=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8) +scimUserName=$(env LC_CTYPE=C tr -dc a-zA-Z0-9 < /dev/urandom | head -c 8) scimUserDisplayName="Display of $scimUserName" scimUserEmail="$scimUserName@example.com" scimUserExternalId="$scimUserEmail" @@ -156,7 +156,7 @@ CURL_OUT_SCIM_POST=$(curl --location --request POST "$SPAR_HOST/scim/v2/Users" \ --header "Authorization: Bearer $SCIM_TOKEN" \ -d "$SCIM_USER") -SCIM_USER_UUID=$(echo $CURL_OUT_SCIM_POST | jq -r .id) +SCIM_USER_UUID=$(echo "$CURL_OUT_SCIM_POST" | jq -r .id) SCIM_USER_INVITATION_ID=$(curl --location -G "$BRIG_HOST/i/teams/invitations/by-email?" \ --header 'Content-Type: application/json' \ @@ -170,17 +170,7 @@ SCIM_USER_INVITATION_CODE=$(curl --silent --show-error \ -XGET "$BRIG_HOST/i/teams/invitation-code?team=$TEAM_UUID&invitation_id=$SCIM_USER_INVITATION_ID" | jq -r .code ) -scimUserPassword=$(cat /dev/urandom | env LC_CTYPE=C tr -dc a-zA-Z0-9 | head -c 8) - -REGISTER_ACCEPT=$(cat < /dev/null && pwd ) +cd "$SCRIPT_DIR/../../.local/charts" + +for chart in "$@"; do ../../hack/bin/update.sh "$chart" helm package "$chart" done helm repo index . -python3 -m http.server $HELM_SERVER_PORT +python3 -m http.server "$HELM_SERVER_PORT" diff --git a/hack/bin/set-chart-image-version.sh b/hack/bin/set-chart-image-version.sh index d133007e4a9..f975d00eb6a 100755 --- a/hack/bin/set-chart-image-version.sh +++ b/hack/bin/set-chart-image-version.sh @@ -2,7 +2,7 @@ USAGE="$0 ..." docker_tag=${1?$USAGE} -charts=${@:2} +charts=${*:2} TOP_LEVEL="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )" CHARTS_DIR="$TOP_LEVEL/.local/charts" diff --git a/hack/bin/set-helm-chart-version.sh b/hack/bin/set-helm-chart-version.sh index 00b838642e5..b53b1857308 100755 --- a/hack/bin/set-helm-chart-version.sh +++ b/hack/bin/set-helm-chart-version.sh @@ -24,8 +24,7 @@ function write_versions() { # update all dependencies, if any if [ -a requirements.yaml ]; then sed -e "s/ version: \".*\"/ version: \"$target_version\"/g" requirements.yaml > "$tempfile" && mv "$tempfile" requirements.yaml - deps=( $(helm dependency list | grep -v NAME | awk '{print $1}') ) - for dep in "${deps[@]}"; do + for dep in $(helm dependency list | grep -v NAME | awk '{print $1}'); do if [ -d "$CHARTS_DIR/$dep" ] && [ "$chart" != "$dep" ]; then (cd "$CHARTS_DIR/$dep" && write_versions "$target_version") fi diff --git a/hack/bin/upload-helm-charts-s3.sh b/hack/bin/upload-helm-charts-s3.sh index c2843622f90..434c8986476 100755 --- a/hack/bin/upload-helm-charts-s3.sh +++ b/hack/bin/upload-helm-charts-s3.sh @@ -114,10 +114,11 @@ cd "$TOP_LEVEL_DIR" # If ./upload-helm-charts-s3.sh is run with a parameter, only synchronize one chart if [ -n "$chart_dir" ] && [ -d "$chart_dir" ]; then - chart_name=$(basename $chart_dir) + chart_name=$(basename "$chart_dir") echo "only syncing $chart_name" charts=( "$chart_name" ) else + #shellcheck disable=SC2207 charts=( $(make -s -C "$TOP_LEVEL_DIR" echo-release-charts) ) # See Makefile/ CHARTS_RELEASE FUTUREWORK #charts=( $(find $CHART_DIR/ -maxdepth 1 -type d | sed -n "s=$CHART_DIR/\(.\+\)=\1 =p") ) @@ -176,7 +177,7 @@ if [[ "$reindex" == "1" ]]; then else # update local cache with newly pushed charts helm repo update - printf "\n--> Not reindexing by default. Pass the --reindex flag in case the index.yaml is incomplete. See all wire charts using \n helm search repo $REPO_NAME/ -l\n\n" + printf "\n--> Not reindexing by default. Pass the --reindex flag in case the index.yaml is incomplete. See all wire charts using \n helm search repo %s/ -l\n\n" "$REPO_NAME" fi diff --git a/libs/wire-api/test/golden/gentests.sh b/libs/wire-api/test/golden/gentests.sh index cff8c340c87..af1d4596bc5 100644 --- a/libs/wire-api/test/golden/gentests.sh +++ b/libs/wire-api/test/golden/gentests.sh @@ -11,7 +11,8 @@ set -e set -o pipefail -export GOLDEN_TMPDIR=$(mktemp -d) +GOLDEN_TMPDIR=$(mktemp -d) +export GOLDEN_TMPDIR export GOLDEN_TESTDIR="test/unit/Test/Wire/API/Golden/Generated" # trap cleanup EXIT @@ -149,7 +150,7 @@ mkdir -p "$GOLDEN_TESTDIR" mkdir -p "$GOLDEN_TMPDIR/dump" stack build --fast --test --bench --no-run-benchmarks wire-api | - while read module section; do + while read -r module section; do echo -ne "\033[KProcessing module $module...\r" { echo "{-# OPTIONS_GHC -Wno-unused-imports #-}" @@ -194,10 +195,10 @@ for module in "$GOLDEN_TESTDIR"/*; do -e '/^import/d' \ -e "/^module/ r $dump" \ "$module" - ormolu -m inplace -c ${EXTS[@]/#/'-o '} "$module" + ormolu -m inplace -c "${EXTS[@]/#/'-o '}" "$module" done -ormolu -m inplace -c ${EXTS[@]/#/'-o '} "$GOLDEN_TESTDIR.hs" +ormolu -m inplace -c "${EXTS[@]/#/'-o '}" "$GOLDEN_TESTDIR.hs" ( cd ../.. && headroom run -a -s libs/wire-api/test/unit/Test/Wire/API/Golden/ ) # build one final time diff --git a/services/brig/federation-tests.sh b/services/brig/federation-tests.sh index 7a37655048e..09a9597d868 100755 --- a/services/brig/federation-tests.sh +++ b/services/brig/federation-tests.sh @@ -44,4 +44,4 @@ AWS_REGION="$(kubectl get deployment -n "$NAMESPACE" brig -o json | jq -r '.spec export AWS_REGION # shellcheck disable=SC2086 -telepresence --namespace "$NAMESPACE" --also-proxy=cassandra-ephemeral ${alsoProxyOptions[*]} --run bash -c "./dist/brig-integration -p federation-end2end-user -i i.yaml -s b.yaml" +telepresence --namespace "$NAMESPACE" --also-proxy=cassandra-ephemeral "${alsoProxyOptions[@]}" --run bash -c "./dist/brig-integration -p federation-end2end-user -i i.yaml -s b.yaml" diff --git a/services/nginz/nginz_reload.sh b/services/nginz/nginz_reload.sh index 0ed14d7444e..ff91a2a0b1c 100755 --- a/services/nginz/nginz_reload.sh +++ b/services/nginz/nginz_reload.sh @@ -19,15 +19,17 @@ watches=${WATCH_PATHS:-"/etc/wire/nginz/upstreams"} # only react on changes to upstreams.conf cfg=upstreams.conf -echo "Setting up watches for ${watches[@]}" +echo "Setting up watches for ${watches[*]}" { echo "nginx PID: $nginx_pid" + #shellcheck disable=SC2068 inotifywait -m -e moved_to -e modify,move,create,delete -m --format '%f' \ - ${watches[@]} | while read file; do \ - if [ $file == $cfg ]; then \ + ${watches[@]} | while read -r file; do \ + if [ "$file" == "$cfg" ]; then \ echo "Config file update detected"; \ nginx -t "$@"; \ + # shellcheck disable=SC2181 if [ $? -ne 0 ]; then \ echo "ERROR: New configuration is invalid!!"; \ else \ diff --git a/services/spar/test-scim-suite/mk_collection.sh b/services/spar/test-scim-suite/mk_collection.sh index 8e397c30e8c..38191024645 100755 --- a/services/spar/test-scim-suite/mk_collection.sh +++ b/services/spar/test-scim-suite/mk_collection.sh @@ -3,9 +3,9 @@ set -e setup_js_jsonlines=$(mktemp /tmp/setup_inline_XXXXXXX.json) -cat ./setup.js | python3 -c ' +python3 -c ' import sys, json; print(json.dumps(sys.stdin.read().splitlines())) -' > $setup_js_jsonlines +' > "$setup_js_jsonlines" < ./setup.js -jq --slurpfile setup_inline "$setup_js_jsonlines" -f ./update.jq $1 +jq --slurpfile setup_inline "$setup_js_jsonlines" -f ./update.jq "$1" diff --git a/services/spar/test-scim-suite/run.sh b/services/spar/test-scim-suite/run.sh index 71edb155b29..af1febc3668 100755 --- a/services/spar/test-scim-suite/run.sh +++ b/services/spar/test-scim-suite/run.sh @@ -10,32 +10,37 @@ SCIM_TEST_SUITE_BRIG_PORT=8082 function create_team_and_scim_token { TOP_LEVEL="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../../.." && pwd )" - IFS=',' read -r -a creds <<< $($TOP_LEVEL/hack/bin/create_test_team_admins.sh -c) + IFS=',' read -r -a creds <<< "$("$TOP_LEVEL/hack/bin/create_test_team_admins.sh" -c)" BRIG_HOST="http://$SCIM_TEST_SUITE_BRIG_HOST:$SCIM_TEST_SUITE_BRIG_PORT" WIRE_ADMIN_UUID=${creds[0]} WIRE_ADMIN=${creds[1]} WIRE_PASSWD=${creds[2]} - export BEARER=$(curl -X POST \ - --header 'Content-Type: application/json' \ - --header 'Accept: application/json' \ - -d '{"email":"'"$WIRE_ADMIN"'","password":"'"$WIRE_PASSWD"'"}' \ - $BRIG_HOST/login'?persist=false' | jq -r .access_token) + BEARER=$(curl -X POST \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + -d '{"email":"'"$WIRE_ADMIN"'","password":"'"$WIRE_PASSWD"'"}' \ + $BRIG_HOST/login'?persist=false' | jq -r .access_token) + + export BEARER SPAR_HOST="http://$SCIM_TEST_SUITE_SPAR_HOST:$SCIM_TEST_SUITE_SPAR_PORT" - export SCIM_TOKEN_FULL=$(curl -X POST \ + SCIM_TOKEN_FULL=$(curl -X POST \ --header "Authorization: Bearer $BEARER" \ --header 'Content-Type: application/json;charset=utf-8' \ --header 'Z-User: '"$WIRE_ADMIN_UUID" \ - -d '{ "description": "test '"`date`"'", "password": "'"$WIRE_PASSWD"'" }' \ + -d '{ "description": "test '"$(date)"'", "password": "'"$WIRE_PASSWD"'" }' \ $SPAR_HOST/scim/auth-tokens) + export SCIM_TOKEN_FULL - export SCIM_TOKEN=$(echo $SCIM_TOKEN_FULL | jq -r .token) - export SCIM_TOKEN_ID=$(echo $SCIM_TOKEN_FULL | jq -r .info.id) + SCIM_TOKEN=$(echo "$SCIM_TOKEN_FULL" | jq -r .token) + export SCIM_TOKEN + SCIM_TOKEN_ID=$(echo "$SCIM_TOKEN_FULL" | jq -r .info.id) + export SCIM_TOKEN_ID - echo $SCIM_TOKEN + echo "$SCIM_TOKEN" } function create_env_file { diff --git a/services/spar/test-scim-suite/runsuite.sh b/services/spar/test-scim-suite/runsuite.sh index 51661704242..aab8e408cf4 100755 --- a/services/spar/test-scim-suite/runsuite.sh +++ b/services/spar/test-scim-suite/runsuite.sh @@ -1,5 +1,4 @@ -#!/usr/bin/env nix-shell -#!nix-shell shell.nix -i bash +#!/usr/bin/env bash set -e @@ -9,5 +8,5 @@ if [[ "$INTEGRATION_SKIP_SCIM_SUITE" -eq 1 ]]; then exit 0 fi -make collection -C $SOURCE_DIR -$SOURCE_DIR/run.sh +make collection -C "$SOURCE_DIR" +"$SOURCE_DIR/run.sh" diff --git a/tools/db/move-team/dump_merge_teams.sh b/tools/db/move-team/dump_merge_teams.sh index 28be2ef437a..33cb229781c 100644 --- a/tools/db/move-team/dump_merge_teams.sh +++ b/tools/db/move-team/dump_merge_teams.sh @@ -6,12 +6,12 @@ set -eu dir="$1" script=$( - for row in $(cat "$1/galley.team" | jq '.[0]' | uniq); do + for row in $(jq '.[0]' < "$1/galley.team" | uniq); do echo "s/$row/\"e09f7a63-b5d4-4db4-a3c1-18bddf3df7fc\"/g;" done ) -for f in $dir/*; do - echo $f - sed -e "$script" -i $f +for f in "$dir"/*; do + echo "$f" + sed -e "$script" -i "$f" done diff --git a/tools/nginz_disco/nginz_disco.sh b/tools/nginz_disco/nginz_disco.sh index 44b96e192cb..c5960d2c6cd 100755 --- a/tools/nginz_disco/nginz_disco.sh +++ b/tools/nginz_disco/nginz_disco.sh @@ -19,6 +19,7 @@ function valid_ipv4() { if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then OIFS=$IFS IFS='.' + # shellcheck disable=SC2206 ip=($ip) IFS=$OIFS [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \ @@ -40,16 +41,20 @@ function valid_ipv6() { function upstream() { name=$1 port=${2:-'8080'} - ips=$(dig +short +retries=3 +search ${name} | sort) + ips=$(dig +short +retries=3 +search "${name}" | sort) unset servers - for ip in ${ips[@]}; do - if valid_ipv4 $ip || valid_ipv6 $ip; then + IFS=$' \t\n' + for ip in $ips; do + if valid_ipv4 "$ip" || valid_ipv6 "$ip"; then servers+=("\n\t server ${ip}:${port} max_fails=3 weight=100;") fi done; + # shellcheck disable=SC2128 if [ -n "$servers" ]; then + # shellcheck disable=SC2068,SC2116,SC2059 printf "upstream ${name} { \n\t least_conn; \n\t keepalive 32; $(echo ${servers[@]}) \n}\n" >> ${new} else + # shellcheck disable=SC2059 printf "upstream ${name} { \n\t least_conn; \n\t keepalive 32; \n\t server localhost:${port} down;\n}\n" >> ${new} fi } @@ -58,6 +63,7 @@ function routing_disco() { ivl=$(echo | awk '{ srand(); printf("%f", 1.5 + rand() * 1.5) }') upstreams=$(cat "$upstream_list") + # shellcheck disable=SC2206 upstreams=( $upstreams ) [[ -f $old ]] || touch -d "1970-01-01" $old @@ -74,8 +80,8 @@ function routing_disco() { rm -f $new - echo done, sleeping $ivl - sleep $ivl + echo done, sleeping "$ivl" + sleep "$ivl" } while true; do diff --git a/tools/ormolu.sh b/tools/ormolu.sh index de3e4fc73b6..7058e51517b 100755 --- a/tools/ormolu.sh +++ b/tools/ormolu.sh @@ -68,7 +68,7 @@ echo "language extensions are taken from the resp. cabal files" FAILURES=0 if [ -t 1 ]; then - : ${ORMOLU_CONDENSE_OUTPUT:=1} + : "${ORMOLU_CONDENSE_OUTPUT:=1}" fi if [ "$f" = "all" ] || [ "$f" = "" ]; then @@ -81,8 +81,6 @@ count=$( echo "$files" | sed '/^\s*$/d' | wc -l ) echo "Checking $count file(s)…" for hsfile in $files; do - FAILED=0 - # run in background so that we can detect Ctrl-C properly ormolu --mode $ARG_ORMOLU_MODE --check-idempotence "$hsfile" & wait $! && err=0 || err=$? diff --git a/tools/rebase-onto-formatter.sh b/tools/rebase-onto-formatter.sh index 1196fad8a35..50987d3ecf5 100755 --- a/tools/rebase-onto-formatter.sh +++ b/tools/rebase-onto-formatter.sh @@ -99,7 +99,7 @@ echo "Running the script now. This might take a while..." set -x # edit every commit Ci, adding new commits representing f at Ci and it's inverse g -git rebase $BASE_COMMIT~1 --exec "$FORMATTING_COMMAND && git commit -am format && git revert HEAD --no-edit" +git rebase "$BASE_COMMIT"~1 --exec "$FORMATTING_COMMAND && git commit -am format && git revert HEAD --no-edit" # drop last commit (do not revert formatting at the end of the branch) git reset HEAD~1 --hard @@ -110,14 +110,15 @@ git reset HEAD~1 --hard # Ci=$(git rev-parse HEAD~1); git reset --soft HEAD~3; git commit --reuse-message $Ci # We do an interactive rebase, but instead of editing the commit sequence manually, # we use sed for that, inserting an `exec` command after every 3 commits. +# shellcheck disable=SC2016 GIT_SEQUENCE_EDITOR='sed -i -e "4~3s/^\(pick \S* format\)$/\1\nexec Ci=\$(git rev-parse HEAD~1); git reset --soft HEAD~3; git commit --reuse-message \$Ci/"' \ - git rebase --interactive $BASE_COMMIT + git rebase --interactive "$BASE_COMMIT" # rebase onto TARGET_COMMIT. # Annoyingly, we still have this first "format" commit that should already be # part of the TARGET_COMMIT. So we drop it. GIT_SEQUENCE_EDITOR='sed -i "1s/pick/drop/"' \ - git rebase --interactive $BASE_COMMIT --onto $TARGET_COMMIT + git rebase --interactive "$BASE_COMMIT" --onto "$TARGET_COMMIT" echo "Done." echo "Please check that the history looks as it should and all expected commits are there." diff --git a/tools/sftd_disco/sftd_disco.sh b/tools/sftd_disco/sftd_disco.sh index 63a603df963..0834a85195e 100755 --- a/tools/sftd_disco/sftd_disco.sh +++ b/tools/sftd_disco/sftd_disco.sh @@ -12,13 +12,11 @@ old="/etc/wire/sftd-disco/sft_servers_all.json" new="${old}.new" function valid_entry() { - local line=$1 # TODO sanity check that this is real dig output return 0 } function valid_url() { - local url=$1 #TODO basic sanity check return 0 } @@ -30,23 +28,23 @@ function valid_url() { # this file can then be served from nginx running besides sft function upstream() { name=$1 - port=${2:-'8585'} - entries=$(dig +short +retries=3 +search SRV ${name} | sort) + entries=$(dig +short +retries=3 +search SRV "${name}" | sort) unset servers comma="" - IFS=$'\n' - for entry in ${entries[@]}; do + IFS=$' \t\n' + for entry in $entries; do if valid_entry "$entry"; then sft_host_port=$(echo "$entry" | awk '{print $4":"$3}') - sft_url=$(curl -s http://$sft_host_port/sft/url | xargs) + sft_url=$(curl -s http://"$sft_host_port"/sft/url | xargs) if valid_url "$sft_url"; then - servers+=(${comma}'"'${sft_url}'"') + servers+=("$comma"'"'"$sft_url"'"') comma="," fi fi done + # shellcheck disable=SC2128 if [ -n "$servers" ]; then - echo '{"sft_servers_all": ['${servers[@]}']}' | jq >${new} + echo '{"sft_servers_all": ['"${servers[*]}"']}' | jq >${new} else printf "" >>${new} fi @@ -68,10 +66,10 @@ function routing_disco() { rm -f $new - echo done, sleeping $ivl - sleep $ivl + echo done, sleeping "$ivl" + sleep "$ivl" } while true; do - routing_disco $srv_name + routing_disco "$srv_name" done diff --git a/treefmt.toml b/treefmt.toml index 6d5723fc9ef..847bdbb793f 100644 --- a/treefmt.toml +++ b/treefmt.toml @@ -18,38 +18,6 @@ command = "shellcheck" includes = ["*.sh"] excludes = [ "dist-newstyle/", - "services/nginz/third_party/", - "libs/wire-api/test/golden/gentests.sh", - "changelog.d/mk-changelog.sh", - "hack/bin/diff-failure.sh", - "hack/bin/python3.sh", - "hack/bin/cabal-run-tests.sh", - "hack/bin/integration-teardown-federation.sh", - "hack/bin/integration-setup-federation.sh", - "hack/bin/serve-charts.sh", - "hack/bin/cabal-install-artefacts.sh", - "hack/bin/helm-template.sh", - "hack/bin/set-chart-image-version.sh", - "hack/bin/copy-charts.sh", - "hack/bin/set-helm-chart-version.sh", - "hack/bin/integration-spring-cleaning.sh", - "hack/bin/upload-helm-charts-s3.sh", - "hack/bin/integration-test-logs.sh", - "services/nginz/nginz_reload.sh", - "services/spar/test-scim-suite/mk_collection.sh", - "services/spar/test-scim-suite/runsuite.sh", - "services/spar/test-scim-suite/run.sh", - "services/brig/federation-tests.sh", - "hack/bin/create_test_team_members.sh", - "hack/bin/create_test_team_scim.sh", - "hack/bin/create_test_user.sh", - "hack/bin/create_team_members.sh", - "hack/bin/register_idp_internal.sh", - "hack/bin/create_test_team_admins.sh", - "deploy/dockerephemeral/init.sh", - "tools/nginz_disco/nginz_disco.sh", - "tools/rebase-onto-formatter.sh", - "tools/sftd_disco/sftd_disco.sh", - "tools/ormolu.sh", - "tools/db/move-team/dump_merge_teams.sh" + "services/nginz/third_party/*", + "services/restund/*", ] From ce9d00a58f103c15d3e8ca347d40d112f32ff73e Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 4 Nov 2024 12:17:14 +0100 Subject: [PATCH 16/27] galley: Use bulk query when getting all feature configs for a team user (#4325) This used to be specialized, but mistakenly got converted to multiple DB calls here: https://github.com/wireapp/wire-server/pull/4178/files#diff-9daaabfa64d3ada89e121d9f68351f16a0237abf97d8fdc672acde7f9f2fd2ffL197-L207https://github.com/wireapp/wire-server/pull/4178/files#diff-9daaabfa64d3ada89e121d9f68351f16a0237abf97d8fdc672acde7f9f2fd2ffL197-L207 --- changelog.d/3-bug-fixes/optimize-get-all-features | 1 + services/galley/src/Galley/API/Teams/Features/Get.hs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/3-bug-fixes/optimize-get-all-features diff --git a/changelog.d/3-bug-fixes/optimize-get-all-features b/changelog.d/3-bug-fixes/optimize-get-all-features new file mode 100644 index 00000000000..d92163c95b5 --- /dev/null +++ b/changelog.d/3-bug-fixes/optimize-get-all-features @@ -0,0 +1 @@ +galley: Use bulk query when getting all feature configs for a team user \ No newline at end of file diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index d4d448fd700..d57e8d5dfb9 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -246,7 +246,9 @@ getAllTeamFeaturesForUser :: Sem r AllTeamFeatures getAllTeamFeaturesForUser uid = do mTid <- getTeamAndCheckMembership uid - hsequence' $ hcpure (Proxy @(GetAllTeamFeaturesForUserConstraints r)) $ Comp $ getFeatureForTeamUser uid mTid + case mTid of + Nothing -> hsequence' $ hcpure (Proxy @(GetAllTeamFeaturesForUserConstraints r)) $ Comp $ getFeatureForUser uid + Just tid -> getAllTeamFeatures tid getSingleFeatureForUser :: forall cfg r. From c804cb59f1f31e485a23287c5c336c5782309c4c Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 4 Nov 2024 12:17:26 +0100 Subject: [PATCH 17/27] galley: Delete unused endpoint for getting feature status for multiple teams (#4326) --- changelog.d/5-internal/delete-multi-get | 1 + .../FeatureFlags/SearchVisibilityInbound.hs | 14 --------- .../src/Wire/API/Routes/Internal/Galley.hs | 16 ---------- .../Galley/TeamFeatureNoConfigMulti.hs | 28 +---------------- services/galley/src/Galley/API/Internal.hs | 2 -- .../galley/src/Galley/API/Teams/Features.hs | 3 +- .../src/Galley/API/Teams/Features/Get.hs | 31 ------------------- .../src/Galley/Cassandra/TeamFeatures.hs | 14 --------- .../src/Galley/Effects/TeamFeatureStore.hs | 10 ------ 9 files changed, 3 insertions(+), 116 deletions(-) create mode 100644 changelog.d/5-internal/delete-multi-get diff --git a/changelog.d/5-internal/delete-multi-get b/changelog.d/5-internal/delete-multi-get new file mode 100644 index 00000000000..44c57b02853 --- /dev/null +++ b/changelog.d/5-internal/delete-multi-get @@ -0,0 +1 @@ +galley: Delete unused endpoint for getting feature status for multiple teams \ No newline at end of file diff --git a/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs b/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs index 55d40c5c2f7..c1ef7a5d3ca 100644 --- a/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs +++ b/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs @@ -1,24 +1,10 @@ module Test.FeatureFlags.SearchVisibilityInbound where import qualified API.Galley as Public -import qualified API.GalleyInternal as Internal import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -testFeatureNoConfigMultiSearchVisibilityInbound :: (HasCallStack) => App () -testFeatureNoConfigMultiSearchVisibilityInbound = do - (_owner1, team1, _) <- createTeam OwnDomain 0 - (_owner2, team2, _) <- createTeam OwnDomain 0 - - assertSuccess =<< Internal.setTeamFeatureStatus OwnDomain team2 "searchVisibilityInbound" "enabled" - - response <- Internal.getFeatureStatusMulti OwnDomain "searchVisibilityInbound" [team1, team2] - - statuses <- response.json %. "default_status" >>= asList - length statuses `shouldMatchInt` 2 - statuses `shouldMatchSet` [object ["team" .= team1, "status" .= "disabled"], object ["team" .= team2, "status" .= "enabled"]] - testSearchVisibilityInboundInternal :: (HasCallStack) => APIAccess -> App () testSearchVisibilityInboundInternal access = do let featureName = "searchVisibilityInbound" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index bdae90491ca..5ea262e42cb 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -39,7 +39,6 @@ import Wire.API.Provider.Service (ServiceRef) import Wire.API.Routes.Features import Wire.API.Routes.Internal.Brig.EJPD import Wire.API.Routes.Internal.Galley.ConversationsIntra -import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named @@ -82,8 +81,6 @@ type IFeatureAPI = :<|> IFeatureStatusLockStatusPut MlsE2EIdConfig :<|> IFeatureStatusLockStatusPut MlsMigrationConfig :<|> IFeatureStatusLockStatusPut EnforceFileDownloadLocationConfig - -- special endpoints - :<|> IFeatureNoConfigMultiGet SearchVisibilityInboundConfig -- all feature configs :<|> Named "feature-configs-internal" @@ -359,19 +356,6 @@ type IFeatureStatusLockStatusPut cfg = :> Put '[JSON] LockStatusResponse ) -type FeatureNoConfigMultiGetBase featureName = - Summary - (AppendSymbol "Get team feature status in bulk for feature " (FeatureSymbol featureName)) - :> "features-multi-teams" - :> FeatureSymbol featureName - :> ReqBody '[JSON] TeamFeatureNoConfigMultiRequest - :> Post '[JSON] (TeamFeatureNoConfigMultiResponse featureName) - -type IFeatureNoConfigMultiGet f = - Named - '("igetmulti", f) - (FeatureNoConfigMultiGetBase f) - type IFederationAPI = Named "get-federation-status" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs index 9f96c0b024c..8bb68c6eb38 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamFeatureNoConfigMulti.hs @@ -16,9 +16,7 @@ -- with this program. If not, see . module Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti - ( TeamFeatureNoConfigMultiRequest (..), - TeamFeatureNoConfigMultiResponse (..), - TeamStatus (..), + ( TeamStatus (..), ) where @@ -29,30 +27,6 @@ import Data.Schema import Imports import Wire.API.Team.Feature qualified as Public -newtype TeamFeatureNoConfigMultiRequest = TeamFeatureNoConfigMultiRequest - { teams :: [TeamId] - } - deriving (Show, Eq) - deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema TeamFeatureNoConfigMultiRequest - -instance ToSchema TeamFeatureNoConfigMultiRequest where - schema = - object "TeamFeatureNoConfigMultiRequest" $ - TeamFeatureNoConfigMultiRequest - <$> teams .= field "teams" (array schema) - -newtype TeamFeatureNoConfigMultiResponse cfg = TeamFeatureNoConfigMultiResponse - { teamsStatuses :: [TeamStatus cfg] - } - deriving (Show, Eq) - deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema (TeamFeatureNoConfigMultiResponse cfg) - -instance ToSchema (TeamFeatureNoConfigMultiResponse cfg) where - schema = - object "TeamFeatureNoConfigMultiResponse" $ - TeamFeatureNoConfigMultiResponse - <$> teamsStatuses .= field "default_status" (array schema) - data TeamStatus cfg = TeamStatus { team :: TeamId, status :: Public.FeatureStatus diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 411ad8fe295..eecc5ad3031 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -284,8 +284,6 @@ featureAPI = <@> mkNamedAPI @'("ilock", MlsE2EIdConfig) (updateLockStatus @MlsE2EIdConfig) <@> mkNamedAPI @'("ilock", MlsMigrationConfig) (updateLockStatus @MlsMigrationConfig) <@> mkNamedAPI @'("ilock", EnforceFileDownloadLocationConfig) (updateLockStatus @EnforceFileDownloadLocationConfig) - -- special endpoints - <@> mkNamedAPI @'("igetmulti", SearchVisibilityInboundConfig) getFeatureMulti -- all features <@> mkNamedAPI @"feature-configs-internal" (maybe getAllTeamFeaturesForServer getAllTeamFeaturesForUser) diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 01edafee051..53a5eb888d8 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -18,8 +18,7 @@ -- with this program. If not, see . module Galley.API.Teams.Features - ( getFeatureMulti, - setFeature, + ( setFeature, setFeatureInternal, patchFeatureInternal, getAllTeamFeaturesForTeam, diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index d57e8d5dfb9..9ad936175ae 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -21,7 +21,6 @@ module Galley.API.Teams.Features.Get ( getFeature, getFeatureInternal, - getFeatureMulti, getAllTeamFeaturesForServer, getAllTeamFeaturesForTeam, getAllTeamFeaturesForUser, @@ -142,20 +141,6 @@ getFeatureInternal tid = do assertTeamExists tid getFeatureForTeam tid -getFeatureMulti :: - forall cfg r. - ( GetFeatureConfig cfg, - ComputeFeatureConstraints cfg r, - Member (Input Opts) r, - Member TeamFeatureStore r - ) => - Multi.TeamFeatureNoConfigMultiRequest -> - Sem r (Multi.TeamFeatureNoConfigMultiResponse cfg) -getFeatureMulti (Multi.TeamFeatureNoConfigMultiRequest tids) = do - cfgs <- getFeatureForMultiTeam @cfg tids - let xs = uncurry toTeamStatus <$> cfgs - pure $ Multi.TeamFeatureNoConfigMultiResponse xs - toTeamStatus :: TeamId -> LockableFeature cfg -> Multi.TeamStatus cfg toTeamStatus tid feat = Multi.TeamStatus tid feat.status @@ -284,22 +269,6 @@ getFeatureForTeam tid = do defFeature dbFeature -getFeatureForMultiTeam :: - forall cfg r. - ( GetFeatureConfig cfg, - ComputeFeatureConstraints cfg r, - Member TeamFeatureStore r, - Member (Input Opts) r - ) => - [TeamId] -> - Sem r [(TeamId, LockableFeature cfg)] -getFeatureForMultiTeam tids = do - defFeature <- getFeatureForServer - features <- getDbFeatureMulti tids - for features $ \(tid, dbFeature) -> do - feat <- computeFeature @cfg tid defFeature dbFeature - pure (tid, feat) - getFeatureForTeamUser :: forall cfg r. ( GetFeatureConfig cfg, diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 55bb2a9b840..04a5ab8f3ab 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -19,7 +19,6 @@ module Galley.Cassandra.TeamFeatures ( interpretTeamFeatureStoreToCassandra, - getDbFeatureMulti, getAllTeamFeaturesForServer, ) where @@ -38,7 +37,6 @@ import Imports import Polysemy import Polysemy.Input import Polysemy.TinyLog -import UnliftIO.Async (pooledMapConcurrentlyN) import Wire.API.Team.Feature interpretTeamFeatureStoreToCassandra :: @@ -52,9 +50,6 @@ interpretTeamFeatureStoreToCassandra = interpret $ \case TFS.GetDbFeature sing tid -> do logEffect "TeamFeatureStore.GetFeatureConfig" embedClient $ getDbFeature sing tid - TFS.GetDbFeatureMulti sing tids -> do - logEffect "TeamFeatureStore.GetFeatureConfigMulti" - embedClient $ getDbFeatureMulti sing tids TFS.SetDbFeature sing tid feat -> do logEffect "TeamFeatureStore.SetFeatureConfig" embedClient $ setDbFeature sing tid feat @@ -65,15 +60,6 @@ interpretTeamFeatureStoreToCassandra = interpret $ \case logEffect "TeamFeatureStore.GetAllTeamFeatures" embedClient $ getAllDbFeatures tid -getDbFeatureMulti :: - forall cfg m. - (MonadClient m, MonadUnliftIO m) => - FeatureSingleton cfg -> - [TeamId] -> - m [(TeamId, DbFeature cfg)] -getDbFeatureMulti proxy = - pooledMapConcurrentlyN 8 (\tid -> getDbFeature proxy tid <&> (tid,)) - getDbFeature :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (DbFeature cfg) getDbFeature = $(featureCases [|fetchFeature|]) diff --git a/services/galley/src/Galley/Effects/TeamFeatureStore.hs b/services/galley/src/Galley/Effects/TeamFeatureStore.hs index a7773e1fe7f..b756ff9281f 100644 --- a/services/galley/src/Galley/Effects/TeamFeatureStore.hs +++ b/services/galley/src/Galley/Effects/TeamFeatureStore.hs @@ -27,10 +27,6 @@ data TeamFeatureStore m a where FeatureSingleton cfg -> TeamId -> TeamFeatureStore m (DbFeature cfg) - GetDbFeatureMulti :: - FeatureSingleton cfg -> - [TeamId] -> - TeamFeatureStore m [(TeamId, DbFeature cfg)] SetDbFeature :: FeatureSingleton cfg -> TeamId -> @@ -51,12 +47,6 @@ getDbFeature :: Sem r (DbFeature cfg) getDbFeature tid = send (GetDbFeature featureSingleton tid) -getDbFeatureMulti :: - (Member TeamFeatureStore r, IsFeatureConfig cfg) => - [TeamId] -> - Sem r [(TeamId, DbFeature cfg)] -getDbFeatureMulti tids = send (GetDbFeatureMulti featureSingleton tids) - setDbFeature :: (Member TeamFeatureStore r, IsFeatureConfig cfg) => TeamId -> From a59dc9a029c79541956c57092856b2f1243d8703 Mon Sep 17 00:00:00 2001 From: Zebot Date: Mon, 4 Nov 2024 13:21:50 +0000 Subject: [PATCH 18/27] Add changelog for Release 2024-11-04 --- CHANGELOG.md | 18 ++++++++++++++++++ .../3-bug-fixes/optimize-get-all-features | 1 - changelog.d/5-internal/WPB-11791 | 1 - changelog.d/5-internal/delete-multi-get | 1 - changelog.d/5-internal/pr-4220-linting | 1 - 5 files changed, 18 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/3-bug-fixes/optimize-get-all-features delete mode 100644 changelog.d/5-internal/WPB-11791 delete mode 100644 changelog.d/5-internal/delete-multi-get delete mode 100644 changelog.d/5-internal/pr-4220-linting diff --git a/CHANGELOG.md b/CHANGELOG.md index 021decd6505..8b77843975a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +# [2024-11-04] (Chart Release 5.7.0) + +## Bug fixes and other updates + + +* galley: Use bulk query when getting all feature configs for a team user (#4325) + + +## Internal changes + + +* Block access to assets.*/minio/ path for public access. (#4297) + +* galley: Delete unused endpoint for getting feature status for multiple teams (#4326) + +* Fix shellcheck problems in all shell scripts (#4220) + + # [2024-10-30] (Chart Release 5.6.0) ## Release notes diff --git a/changelog.d/3-bug-fixes/optimize-get-all-features b/changelog.d/3-bug-fixes/optimize-get-all-features deleted file mode 100644 index d92163c95b5..00000000000 --- a/changelog.d/3-bug-fixes/optimize-get-all-features +++ /dev/null @@ -1 +0,0 @@ -galley: Use bulk query when getting all feature configs for a team user \ No newline at end of file diff --git a/changelog.d/5-internal/WPB-11791 b/changelog.d/5-internal/WPB-11791 deleted file mode 100644 index b3aa41c0df6..00000000000 --- a/changelog.d/5-internal/WPB-11791 +++ /dev/null @@ -1 +0,0 @@ -Block access to assets.*/minio/ path for public access. diff --git a/changelog.d/5-internal/delete-multi-get b/changelog.d/5-internal/delete-multi-get deleted file mode 100644 index 44c57b02853..00000000000 --- a/changelog.d/5-internal/delete-multi-get +++ /dev/null @@ -1 +0,0 @@ -galley: Delete unused endpoint for getting feature status for multiple teams \ No newline at end of file diff --git a/changelog.d/5-internal/pr-4220-linting b/changelog.d/5-internal/pr-4220-linting deleted file mode 100644 index 4bee98ed532..00000000000 --- a/changelog.d/5-internal/pr-4220-linting +++ /dev/null @@ -1 +0,0 @@ -Fix shellcheck problems in all shell scripts From d6eade4b3c46c987500243cd9e7201236657eb8b Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Mon, 4 Nov 2024 15:08:13 +0100 Subject: [PATCH 19/27] Add a few more swagger descriptions and examples. (#4323) --- changelog.d/4-docs/fix-swagger-2024-10-31 | 1 + libs/schema-profunctor/src/Data/Schema.hs | 12 ++++ libs/wire-api/src/Wire/API/Asset.hs | 5 +- .../API/Routes/Internal/Galley/TeamsIntra.hs | 12 +--- libs/wire-api/src/Wire/API/Team.hs | 12 ++-- libs/wire-api/src/Wire/API/User.hs | 2 - libs/wire-api/src/Wire/API/User/Orphans.hs | 57 ++++++++++++------- services/brig/src/Brig/API/User.hs | 1 - services/brig/src/Brig/Version.hs | 2 +- 9 files changed, 63 insertions(+), 41 deletions(-) create mode 100644 changelog.d/4-docs/fix-swagger-2024-10-31 diff --git a/changelog.d/4-docs/fix-swagger-2024-10-31 b/changelog.d/4-docs/fix-swagger-2024-10-31 new file mode 100644 index 00000000000..46b2dbd3f93 --- /dev/null +++ b/changelog.d/4-docs/fix-swagger-2024-10-31 @@ -0,0 +1 @@ +Add a few more swagger descriptions and examples. \ No newline at end of file diff --git a/libs/schema-profunctor/src/Data/Schema.hs b/libs/schema-profunctor/src/Data/Schema.hs index 9f6104e07a9..87fdee9075e 100644 --- a/libs/schema-profunctor/src/Data/Schema.hs +++ b/libs/schema-profunctor/src/Data/Schema.hs @@ -73,6 +73,7 @@ module Data.Schema dispatch, text, parsedText, + parsedTextWithDoc, null_, nullable, element, @@ -638,6 +639,17 @@ parsedText :: SchemaP NamedSwaggerDoc A.Value A.Value Text a parsedText name parser = text name `withParser` (either fail pure . parser) +-- | A schema for a textual value with possible failure. +parsedTextWithDoc :: + Text -> + Text -> + (Text -> Either String a) -> + SchemaP NamedSwaggerDoc A.Value A.Value Text a +parsedTextWithDoc desc name parser = appendDescr (text name) `withParser` (either fail pure . parser) + where + appendDescr :: ValueSchema NamedSwaggerDoc Text -> ValueSchema NamedSwaggerDoc Text + appendDescr = (doc . S.description) %~ (Just . maybe desc (<> ("\n" <> desc))) + -- | A schema for an arbitrary JSON object. jsonObject :: ValueSchema SwaggerDoc A.Object jsonObject = diff --git a/libs/wire-api/src/Wire/API/Asset.hs b/libs/wire-api/src/Wire/API/Asset.hs index d2a53bad442..a5496f42746 100644 --- a/libs/wire-api/src/Wire/API/Asset.hs +++ b/libs/wire-api/src/Wire/API/Asset.hs @@ -177,8 +177,11 @@ assetKeyToText = T.decodeUtf8 . toByteString' instance ToSchema AssetKey where schema = assetKeyToText - .= parsedText "AssetKey" (runParser parser . T.encodeUtf8) + .= parsedTextWithDoc desc "AssetKey" (runParser parser . T.encodeUtf8) & doc' . S.schema . S.example ?~ toJSON ("3-1-47de4580-ae51-4650-acbb-d10c028cb0ac" :: Text) + where + desc = + "S3 asset key for an icon image with retention information." instance S.ToParamSchema AssetKey where toParamSchema _ = S.toParamSchema (Proxy @Text) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs index 0bc3ae5a593..90ca36342c1 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/TeamsIntra.hs @@ -27,7 +27,6 @@ module Wire.API.Routes.Internal.Galley.TeamsIntra ) where -import Control.Lens ((?~)) import Data.Aeson import Data.Currency qualified as Currency import Data.Json.Util @@ -82,7 +81,6 @@ instance S.ToSchema TeamData where data TeamStatusUpdate = TeamStatusUpdate { tuStatus :: !TeamStatus, tuCurrency :: !(Maybe Currency.Alpha) - -- TODO: Remove Currency selection once billing supports currency changes after team creation } deriving (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform TeamStatusUpdate @@ -93,15 +91,7 @@ instance S.ToSchema TeamStatusUpdate where S.object "TeamStatusUpdate" $ TeamStatusUpdate <$> tuStatus S..= S.field "status" S.schema - <*> tuCurrency S..= S.maybe_ (S.optField "currency" currencyAlphaSchema) - where - currencyAlphaSchema :: S.ValueSchema S.NamedSwaggerDoc Currency.Alpha - currencyAlphaSchema = S.mkSchema docs parseJSON (pure . toJSON) - where - docs = - S.swaggerDoc @Text - & Swagger.schema . Swagger.description ?~ "ISO 4217 alphabetic codes" - & Swagger.schema . Swagger.example ?~ "EUR" + <*> tuCurrency S..= S.maybe_ (S.optField "currency" S.genericToSchema) newtype TeamName = TeamName {tnName :: Text} diff --git a/libs/wire-api/src/Wire/API/Team.hs b/libs/wire-api/src/Wire/API/Team.hs index a1fc3c99b8a..283dbaff55b 100644 --- a/libs/wire-api/src/Wire/API/Team.hs +++ b/libs/wire-api/src/Wire/API/Team.hs @@ -62,7 +62,7 @@ module Wire.API.Team where import Control.Lens (makeLenses, over, (?~)) -import Data.Aeson (FromJSON, ToJSON, Value (..)) +import Data.Aeson (FromJSON, ToJSON, Value (..), toJSON) import Data.Aeson.Types (Parser) import Data.Attoparsec.ByteString qualified as Atto (Parser, string) import Data.Attoparsec.Combinator (choice) @@ -183,8 +183,8 @@ newTeamObjectSchema :: ObjectSchema SwaggerDoc NewTeam newTeamObjectSchema = NewTeam <$> newTeamName .= fieldWithDocModifier "name" (description ?~ "team name") schema - <*> newTeamIcon .= fieldWithDocModifier "icon" (description ?~ "team icon (asset ID)") schema - <*> newTeamIconKey .= maybe_ (optFieldWithDocModifier "icon_key" (description ?~ "team icon asset key") schema) + <*> newTeamIcon .= field "icon" schema + <*> newTeamIconKey .= maybe_ (optFieldWithDocModifier "icon_key" (description ?~ "The decryption key for the team icon S3 asset") schema) instance ToSchema NewTeam where schema = object "NewTeam" newTeamObjectSchema @@ -214,7 +214,11 @@ instance ToByteString Icon where instance ToSchema Icon where schema = (T.decodeUtf8 . toByteString') - .= parsedText "Icon" (runParser parser . T.encodeUtf8) + .= parsedTextWithDoc desc "Icon" (runParser parser . T.encodeUtf8) + & doc' . S.schema . S.example ?~ toJSON ("3-1-47de4580-ae51-4650-acbb-d10c028cb0ac" :: Text) + where + desc = + "S3 asset key for an icon image with retention information. Allows special value 'default'." data TeamUpdateData = TeamUpdateData { _nameUpdate :: Maybe (Range 1 256 Text), diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 25cf9e88172..75387c76429 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -1305,8 +1305,6 @@ newTeamUserTeamId = \case data BindingNewTeamUser = BindingNewTeamUser { bnuTeam :: NewTeam, bnuCurrency :: Maybe Currency.Alpha - -- FUTUREWORK: - -- Remove Currency selection once billing supports currency changes after team creation } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform BindingNewTeamUser) diff --git a/libs/wire-api/src/Wire/API/User/Orphans.hs b/libs/wire-api/src/Wire/API/User/Orphans.hs index 316889c115a..bfaaff37b40 100644 --- a/libs/wire-api/src/Wire/API/User/Orphans.hs +++ b/libs/wire-api/src/Wire/API/User/Orphans.hs @@ -26,8 +26,10 @@ import Data.Char import Data.Currency qualified as Currency import Data.ISO3166_CountryCodes import Data.LanguageCodes -import Data.OpenApi +import Data.OpenApi as O import Data.Proxy +import Data.Schema as S +import Data.Text qualified as T import Data.UUID import Data.X509 as X509 import Imports @@ -42,9 +44,9 @@ deriving instance Generic ISO639_1 -- Swagger instances -instance ToSchema ISO639_1 +instance O.ToSchema ISO639_1 -instance ToSchema CountryCode +instance O.ToSchema CountryCode -- FUTUREWORK: push orphans upstream to saml2-web-sso, servant-multipart -- FUTUREWORK: maybe avoid orphans altogether by defining schema instances manually @@ -69,19 +71,19 @@ samlSchemaOptions = -- This type comes from a seperate repo, so we're keeping the prefix dropping -- for the moment. -instance ToSchema SAML.XmlText where +instance O.ToSchema SAML.XmlText where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions instance ToParamSchema SAML.IdPId where toParamSchema _ = toParamSchema (Proxy @UUID) -instance ToSchema SAML.AuthnRequest where +instance O.ToSchema SAML.AuthnRequest where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions -instance ToSchema SAML.NameIdPolicy where +instance O.ToSchema SAML.NameIdPolicy where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions -instance ToSchema SAML.NameIDFormat where +instance O.ToSchema SAML.NameIDFormat where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions -- The generic schema breaks on this type, so we define it by hand. @@ -92,7 +94,7 @@ instance ToSchema SAML.NameIDFormat where -- and this results in an array whose underlying type which is at the same time -- marked as a string, and referring to the schema for AuthnRequest, which is of -- course invalid. -instance ToSchema (SAML.FormRedirect SAML.AuthnRequest) where +instance O.ToSchema (SAML.FormRedirect SAML.AuthnRequest) where declareNamedSchema _ = do authnReqSchema <- declareSchemaRef (Proxy @SAML.AuthnRequest) pure $ @@ -102,45 +104,58 @@ instance ToSchema (SAML.FormRedirect SAML.AuthnRequest) where & properties . at "uri" ?~ Inline (toSchema (Proxy @Text)) & properties . at "xml" ?~ authnReqSchema -instance ToSchema (SAML.ID SAML.AuthnRequest) where +instance O.ToSchema (SAML.ID SAML.AuthnRequest) where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions { datatypeNameModifier = const "Id_AuthnRequest" } -instance ToSchema SAML.Time where +instance O.ToSchema SAML.Time where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions -instance ToSchema SAML.SPMetadata where +instance O.ToSchema SAML.SPMetadata where declareNamedSchema _ = declareNamedSchema (Proxy @String) -instance ToSchema Void where +instance O.ToSchema Void where declareNamedSchema _ = declareNamedSchema (Proxy @String) instance (HasOpenApi route) => HasOpenApi (SM.MultipartForm SM.Mem resp :> route) where toOpenApi _proxy = toOpenApi (Proxy @route) -instance ToSchema SAML.IdPId where +instance O.ToSchema SAML.IdPId where declareNamedSchema _ = declareNamedSchema (Proxy @UUID) -instance (ToSchema a) => ToSchema (SAML.IdPConfig a) where +instance (O.ToSchema a) => O.ToSchema (SAML.IdPConfig a) where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions -instance ToSchema SAML.Issuer where +instance O.ToSchema SAML.Issuer where declareNamedSchema _ = declareNamedSchema (Proxy @String) -instance ToSchema URI where +instance O.ToSchema URI where declareNamedSchema _ = declareNamedSchema (Proxy @String) -instance ToParamSchema URI where +instance O.ToParamSchema URI where toParamSchema _ = toParamSchema (Proxy @String) -instance ToSchema X509.SignedCertificate where +instance O.ToSchema X509.SignedCertificate where declareNamedSchema _ = declareNamedSchema (Proxy @String) -instance ToSchema SAML.IdPMetadata where +instance O.ToSchema SAML.IdPMetadata where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions -instance ToSchema Currency.Alpha where - declareNamedSchema = genericDeclareNamedSchema defaultSchemaOptions +instance S.ToSchema Currency.Alpha where + schema = S.enum @Text "Currency.Alpha" cases & S.doc' . O.schema %~ swaggerTweaks + where + cases :: SchemaP [A.Value] Text (Alt Maybe Text) Currency.Alpha Currency.Alpha + cases = mconcat ((\cur -> S.element (T.pack (show cur)) cur) <$> [minBound @Currency.Alpha ..]) + + swaggerTweaks :: O.Schema -> O.Schema + swaggerTweaks = + ( O.description + ?~ "ISO 4217 alphabetic codes. This is only stored by the backend, not processed. \ + \It can be removed once billing supports currency changes after team creation." + ) + . (O.example ?~ "EUR") + +deriving via (S.Schema Currency.Alpha) instance O.ToSchema Currency.Alpha diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 46c3e658e30..7ff8e0f22b1 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -720,7 +720,6 @@ activateWithCurrency :: -- | The user for whom to activate the key. Maybe UserId -> -- | Potential currency update. - -- ^ TODO: to be removed once billing supports currency changes after team creation Maybe Currency.Alpha -> ExceptT ActivationError (AppT r) ActivationResult activateWithCurrency tgt code usr cur = do diff --git a/services/brig/src/Brig/Version.hs b/services/brig/src/Brig/Version.hs index 86f3a33b937..606639c9830 100644 --- a/services/brig/src/Brig/Version.hs +++ b/services/brig/src/Brig/Version.hs @@ -26,7 +26,7 @@ import Wire.API.Routes.Named import Wire.API.Routes.Version versionAPI :: ServerT VersionAPI (Handler r) -versionAPI = Named $ do +versionAPI = Named @"get-version" $ do fed <- asks (.federator) dom <- viewFederationDomain disabled <- asks (.disabledVersions) From bbe367637f4804a2d2373aa580304691530e7af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Mon, 4 Nov 2024 15:37:26 +0100 Subject: [PATCH 20/27] [WPB-8881] Move email update and remove operations to effects (#4316) * Move user email updating functions to wire-subsystems * DELETE /self/email: ZLocalUser instead of ZUser * Wrap a function signature * Move 'deleteEmail' to UserStore * Migrate 'removeEmail' to UserSubsystem * Add a change log * Rename an activation code action * Rename UserSubsystem helpers * Remove redundant error interpretation * Elaborate a recusive interpretation cycle * Drop an unused no-password error * Implement removeEmail via RemoveEmailEither This also changes the error type in RemoveEmailEither to the general UserSubsystemError type. Handler-specific reinterpretations are done in the handler instead. * "Fix" timing issue in (old) galley integration tests. * Move reading from the environment to a helper --------- Co-authored-by: Matthias Fischmann --- changelog.d/5-internal/WPB-8881 | 1 + libs/wire-api/src/Wire/API/Error/Brig.hs | 3 + .../src/Wire/API/Routes/Public/Brig.hs | 2 +- libs/wire-api/src/Wire/API/User.hs | 2 - libs/wire-api/src/Wire/API/User/Activation.hs | 12 ++ .../src/Wire/ActivationCodeStore.hs | 9 ++ .../src/Wire/ActivationCodeStore/Cassandra.hs | 52 +++++++- libs/wire-subsystems/src/Wire/UserStore.hs | 2 + .../src/Wire/UserStore/Cassandra.hs | 15 +++ .../wire-subsystems/src/Wire/UserSubsystem.hs | 95 +++++++++++++- .../src/Wire/UserSubsystem/Error.hs | 8 ++ .../src/Wire/UserSubsystem/Interpreter.hs | 35 ++++-- .../Wire/UserSubsystem/UserSubsystemConfig.hs | 16 +++ .../MockInterpreters/ActivationCodeStore.hs | 28 ++++- .../unit/Wire/MockInterpreters/UserStore.hs | 8 ++ .../Wire/UserSubsystem/InterpreterSpec.hs | 32 ++--- libs/wire-subsystems/wire-subsystems.cabal | 1 + services/brig/src/Brig/API/Auth.hs | 21 +++- services/brig/src/Brig/API/Error.hs | 6 - services/brig/src/Brig/API/Internal.hs | 36 ++++-- services/brig/src/Brig/API/Public.hs | 43 +++++-- services/brig/src/Brig/API/Types.hs | 17 +-- services/brig/src/Brig/API/User.hs | 117 ++++-------------- .../brig/src/Brig/CanonicalInterpreter.hs | 6 +- services/brig/src/Brig/Data/Activation.hs | 55 +------- services/brig/src/Brig/Data/User.hs | 14 --- services/galley/test/integration/API/Util.hs | 5 +- 27 files changed, 398 insertions(+), 243 deletions(-) create mode 100644 changelog.d/5-internal/WPB-8881 create mode 100644 libs/wire-subsystems/src/Wire/UserSubsystem/UserSubsystemConfig.hs diff --git a/changelog.d/5-internal/WPB-8881 b/changelog.d/5-internal/WPB-8881 new file mode 100644 index 00000000000..f4bf49f3359 --- /dev/null +++ b/changelog.d/5-internal/WPB-8881 @@ -0,0 +1 @@ +Move email update and remove operations to effects diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 9c397736cc2..6dc470def9a 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -67,6 +67,7 @@ data BrigError | NameManagedByScim | HandleManagedByScim | LocaleManagedByScim + | EmailManagedByScim | LastIdentity | NoPassword | ChangePasswordMustDiffer @@ -247,6 +248,8 @@ type instance MapError 'HandleManagedByScim = 'StaticError 403 "managed-by-scim" type instance MapError 'LocaleManagedByScim = 'StaticError 403 "managed-by-scim" "Updating locale is not allowed, because it is managed by SCIM, or E2EId is enabled" +type instance MapError 'EmailManagedByScim = 'StaticError 403 "managed-by-scim" "Updating email is not allowed, because it is managed by SCIM, or E2EId is enabled" + type instance MapError 'LastIdentity = 'StaticError 403 "last-identity" "The last user identity cannot be removed." type instance MapError 'NoPassword = 'StaticError 403 "no-password" "The user has no password." diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index d377fc79835..8af642848d8 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -363,7 +363,7 @@ type SelfAPI = :> Description "Your email address can only be removed if you also have a \ \phone number." - :> ZUser + :> ZLocalUser :> "self" :> "email" :> MultiVerb 'DELETE '[JSON] RemoveIdentityResponses (Maybe RemoveIdentityError) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 75387c76429..c6d2a6d4404 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -1515,7 +1515,6 @@ instance (res ~ ChangePhoneResponses) => AsUnion res (Maybe ChangePhoneError) wh data RemoveIdentityError = LastIdentity - | NoPassword | NoIdentity deriving (Generic) deriving (AsUnion RemoveIdentityErrorResponses) via GenericAsUnion RemoveIdentityErrorResponses RemoveIdentityError @@ -1524,7 +1523,6 @@ instance GSOP.Generic RemoveIdentityError type RemoveIdentityErrorResponses = [ ErrorResponse 'E.LastIdentity, - ErrorResponse 'E.NoPassword, ErrorResponse 'E.NoIdentity ] diff --git a/libs/wire-api/src/Wire/API/User/Activation.hs b/libs/wire-api/src/Wire/API/User/Activation.hs index 5e347a54afe..84c993870b4 100644 --- a/libs/wire-api/src/Wire/API/User/Activation.hs +++ b/libs/wire-api/src/Wire/API/User/Activation.hs @@ -32,6 +32,9 @@ module Wire.API.User.Activation -- * SendActivationCode SendActivationCode (..), + + -- * Activation + Activation (..), ) where @@ -211,3 +214,12 @@ instance ToSchema SendActivationCode where objectDesc = description ?~ "Data for requesting an email code to be sent. 'email' must be present." + +-- | The information associated with the pending activation of an 'EmailKey'. +data Activation = Activation + { -- | An opaque key for the original 'EmailKey' pending activation. + activationKey :: !ActivationKey, + -- | The confidential activation code. + activationCode :: !ActivationCode + } + deriving (Eq, Show) diff --git a/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs b/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs index 9473bd16f58..1d5175387fe 100644 --- a/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs +++ b/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs @@ -21,10 +21,19 @@ module Wire.ActivationCodeStore where import Data.Id import Imports import Polysemy +import Util.Timeout import Wire.API.User.Activation import Wire.UserKeyStore data ActivationCodeStore :: Effect where LookupActivationCode :: EmailKey -> ActivationCodeStore m (Maybe (Maybe UserId, ActivationCode)) + -- | Create a code for a new pending activation for a given 'EmailKey' + NewActivationCode :: + EmailKey -> + -- | The timeout for the activation code. + Timeout -> + -- | The user with whom to associate the activation code. + Maybe UserId -> + ActivationCodeStore m Activation makeSem ''ActivationCodeStore diff --git a/libs/wire-subsystems/src/Wire/ActivationCodeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/ActivationCodeStore/Cassandra.hs index 7f0ba27ba03..01349e6102b 100644 --- a/libs/wire-subsystems/src/Wire/ActivationCodeStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/ActivationCodeStore/Cassandra.hs @@ -1,31 +1,63 @@ -module Wire.ActivationCodeStore.Cassandra where +module Wire.ActivationCodeStore.Cassandra (interpretActivationCodeStoreToCassandra) where import Cassandra import Data.Id +import Data.Text (pack) import Data.Text.Ascii qualified as Ascii import Data.Text.Encoding qualified as T import Imports +import OpenSSL.BN (randIntegerZeroToNMinusOne) import OpenSSL.EVP.Digest import Polysemy import Polysemy.Embed +import Text.Printf (printf) +import Util.Timeout import Wire.API.User.Activation +import Wire.API.User.EmailAddress import Wire.ActivationCodeStore -import Wire.UserKeyStore (EmailKey, emailKeyUniq) +import Wire.UserKeyStore interpretActivationCodeStoreToCassandra :: (Member (Embed IO) r) => ClientState -> InterpreterFor ActivationCodeStore r interpretActivationCodeStoreToCassandra casClient = interpret $ - runEmbedded (runClient casClient) . \case - LookupActivationCode ek -> embed do + runEmbedded (runClient casClient) . embed . \case + LookupActivationCode ek -> do liftIO (mkActivationKey ek) >>= retry x1 . query1 cql . params LocalQuorum . Identity + NewActivationCode ek timeout uid -> newActivationCodeImpl ek timeout uid where cql :: PrepQuery R (Identity ActivationKey) (Maybe UserId, ActivationCode) cql = - [sql| + [sql| SELECT user, code FROM activation_keys WHERE key = ? |] +-- | Create a new pending activation for a given 'EmailKey'. +newActivationCodeImpl :: + (MonadClient m) => + EmailKey -> + -- | The timeout for the activation code. + Timeout -> + -- | The user with whom to associate the activation code. + Maybe UserId -> + m Activation +newActivationCodeImpl uk timeout u = do + let typ = "email" + key = fromEmail (emailKeyOrig uk) + code <- liftIO $ genCode + insert typ key code + where + insert t k c = do + key <- liftIO $ mkActivationKey uk + retry x5 . write keyInsert $ params LocalQuorum (key, t, k, c, u, maxAttempts, round timeout) + pure $ Activation key c + genCode = + ActivationCode . Ascii.unsafeFromText . pack . printf "%06d" + <$> randIntegerZeroToNMinusOne 1000000 + +-------------------------------------------------------------------------------- +-- Utilities + mkActivationKey :: EmailKey -> IO ActivationKey mkActivationKey k = do Just d <- getDigestByName "SHA256" @@ -35,3 +67,13 @@ mkActivationKey k = do . digestBS d . T.encodeUtf8 $ emailKeyUniq k + +keyInsert :: PrepQuery W (ActivationKey, Text, Text, ActivationCode, Maybe UserId, Int32, Int32) () +keyInsert = + "INSERT INTO activation_keys \ + \(key, key_type, key_text, code, user, retries) VALUES \ + \(? , ? , ? , ? , ? , ? ) USING TTL ?" + +-- | Max. number of activation attempts per 'ActivationKey'. +maxAttempts :: Int32 +maxAttempts = 3 diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index a5189d29818..6f24e084717 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -54,6 +54,7 @@ data UserStore m a where GetIndexUsersPaginated :: Int32 -> Maybe PagingState -> UserStore m (PageWithState IndexUser) GetUsers :: [UserId] -> UserStore m [StoredUser] UpdateUser :: UserId -> StoredUserUpdate -> UserStore m () + UpdateEmailUnvalidated :: UserId -> EmailAddress -> UserStore m () UpdateUserHandleEither :: UserId -> StoredUserHandleUpdate -> UserStore m (Either StoredUserUpdateError ()) DeleteUser :: User -> UserStore m () -- | This operation looks up a handle but is guaranteed to not give you stale locks. @@ -73,6 +74,7 @@ data UserStore m a where GetActivityTimestamps :: UserId -> UserStore m [Maybe UTCTime] GetRichInfo :: UserId -> UserStore m (Maybe RichInfoAssocList) GetUserAuthenticationInfo :: UserId -> UserStore m (Maybe (Maybe Password, AccountStatus)) + DeleteEmail :: UserId -> UserStore m () makeSem ''UserStore diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index 96e78df99d3..d113e202496 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -26,6 +26,7 @@ interpretUserStoreCassandra casClient = GetIndexUser uid -> getIndexUserImpl uid GetIndexUsersPaginated pageSize mPagingState -> getIndexUserPaginatedImpl pageSize mPagingState UpdateUser uid update -> updateUserImpl uid update + UpdateEmailUnvalidated uid email -> updateEmailUnvalidatedImpl uid email UpdateUserHandleEither uid update -> updateUserHandleEitherImpl uid update DeleteUser user -> deleteUserImpl user LookupHandle hdl -> lookupHandleImpl LocalQuorum hdl @@ -37,6 +38,7 @@ interpretUserStoreCassandra casClient = GetActivityTimestamps uid -> getActivityTimestampsImpl uid GetRichInfo uid -> getRichInfoImpl uid GetUserAuthenticationInfo uid -> getUserAuthenticationInfoImpl uid + DeleteEmail uid -> deleteEmailImpl uid getUserAuthenticationInfoImpl :: UserId -> Client (Maybe (Maybe Password, AccountStatus)) getUserAuthenticationInfoImpl uid = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Identity uid))) @@ -105,6 +107,13 @@ updateUserImpl uid update = for_ update.accentId \c -> addPrepQuery userAccentIdUpdate (c, uid) for_ update.supportedProtocols \a -> addPrepQuery userSupportedProtocolsUpdate (a, uid) +updateEmailUnvalidatedImpl :: UserId -> EmailAddress -> Client () +updateEmailUnvalidatedImpl u e = + retry x5 $ write userEmailUnvalidatedUpdate (params LocalQuorum (e, u)) + where + userEmailUnvalidatedUpdate :: PrepQuery W (EmailAddress, UserId) () + userEmailUnvalidatedUpdate = "UPDATE user SET email_unvalidated = ? WHERE id = ?" + updateUserHandleEitherImpl :: UserId -> StoredUserHandleUpdate -> Client (Either StoredUserUpdateError ()) updateUserHandleEitherImpl uid update = runM $ runError do @@ -200,6 +209,9 @@ getRichInfoImpl uid = q :: PrepQuery R (Identity UserId) (Identity RichInfoAssocList) q = "SELECT json FROM rich_info WHERE user = ?" +deleteEmailImpl :: UserId -> Client () +deleteEmailImpl u = retry x5 $ write userEmailDelete (params LocalQuorum (Identity u)) + -------------------------------------------------------------------------------- -- Queries @@ -259,3 +271,6 @@ activatedSelect = "SELECT activated FROM user WHERE id = ?" localeSelect :: PrepQuery R (Identity UserId) (Maybe Language, Maybe Country) localeSelect = "SELECT language, country FROM user WHERE id = ?" + +userEmailDelete :: PrepQuery W (Identity UserId) () +userEmailDelete = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email = null, write_time_bumper = 0 WHERE id = ?" diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index f53da756a00..564799f2b6b 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -18,20 +18,28 @@ import Data.Range import Imports import Polysemy import Polysemy.Error +import Polysemy.Input import Wire.API.Federation.Error import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti (TeamStatus) import Wire.API.Team.Export (TeamExportUser) import Wire.API.Team.Feature import Wire.API.Team.Member (IsPerm (..), TeamMember) import Wire.API.User +import Wire.API.User.Activation import Wire.API.User.Search +import Wire.ActivationCodeStore import Wire.Arbitrary +import Wire.BlockListStore +import Wire.BlockListStore qualified as BlockListStore +import Wire.EmailSubsystem import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.InvitationStore -import Wire.UserKeyStore (EmailKey, emailKeyOrig) +import Wire.UserKeyStore import Wire.UserSearch.Types +import Wire.UserStore import Wire.UserSubsystem.Error (UserSubsystemError (..)) +import Wire.UserSubsystem.UserSubsystemConfig -- | Who is performing this update operation / who is allowed to? (Single source of truth: -- users managed by SCIM can't be updated by clients and vice versa.) @@ -88,6 +96,14 @@ data GetBy = MkGetBy instance Default GetBy where def = MkGetBy NoPendingInvitations [] [] +-- | Outcome of email change invariant checks. +data ChangeEmailResult + = -- | The request was successful, user needs to verify the new email address + ChangeEmailNeedsActivation !(User, Activation, EmailAddress) + | -- | The user asked to change the email address to the one already owned + ChangeEmailIdempotent + deriving (Show) + data UserSubsystem m a where -- | First arg is for authorization only. GetUserProfiles :: Local UserId -> [Qualified UserId] -> UserSubsystem m [UserProfile] @@ -145,6 +161,7 @@ data UserSubsystem m a where InternalUpdateSearchIndex :: UserId -> UserSubsystem m () InternalFindTeamInvitation :: Maybe EmailKey -> InvitationCode -> UserSubsystem m StoredInvitation GetUserExportData :: UserId -> UserSubsystem m (Maybe TeamExportUser) + RemoveEmailEither :: Local UserId -> UserSubsystem m (Either UserSubsystemError ()) -- | the return type of 'CheckHandle' data CheckHandleResp @@ -154,6 +171,14 @@ data CheckHandleResp makeSem ''UserSubsystem +removeEmail :: + ( Member UserSubsystem r, + Member (Error UserSubsystemError) r + ) => + Local UserId -> + Sem r () +removeEmail = removeEmailEither >=> fromEither + getUserProfile :: (Member UserSubsystem r) => Local UserId -> Qualified UserId -> Sem r (Maybe UserProfile) getUserProfile luid targetUser = listToMaybe <$> getUserProfiles luid [targetUser] @@ -181,6 +206,74 @@ getLocalUserAccountByUserKey :: (Member UserSubsystem r) => Local EmailKey -> Se getLocalUserAccountByUserKey q@(tUnqualified -> ek) = listToMaybe <$> getAccountsByEmailNoFilter (qualifyAs q [emailKeyOrig ek]) +-- | Call 'createEmailChangeToken' and process result: if email changes to +-- itself, succeed, if not, send validation email. +requestEmailChange :: + ( Member BlockListStore r, + Member UserKeyStore r, + Member EmailSubsystem r, + Member UserSubsystem r, + Member UserStore r, + Member (Error UserSubsystemError) r, + Member ActivationCodeStore r, + Member (Input UserSubsystemConfig) r + ) => + Local UserId -> + EmailAddress -> + UpdateOriginType -> + Sem r ChangeEmailResponse +requestEmailChange lusr email allowScim = do + let u = tUnqualified lusr + createEmailChangeToken lusr email allowScim >>= \case + ChangeEmailIdempotent -> + pure ChangeEmailResponseIdempotent + ChangeEmailNeedsActivation (usr, adata, en) -> do + sendOutEmail usr adata en + updateEmailUnvalidated u email + internalUpdateSearchIndex u + pure ChangeEmailResponseNeedsActivation + where + sendOutEmail usr adata en = do + (maybe sendActivationMail (const sendEmailAddressUpdateMail) usr.userIdentity) + en + (userDisplayName usr) + (activationKey adata) + (activationCode adata) + (Just (userLocale usr)) + +-- | Prepare changing the email (checking a number of invariants). +createEmailChangeToken :: + ( Member BlockListStore r, + Member UserKeyStore r, + Member (Error UserSubsystemError) r, + Member UserSubsystem r, + Member ActivationCodeStore r, + Member (Input UserSubsystemConfig) r + ) => + Local UserId -> + EmailAddress -> + UpdateOriginType -> + Sem r ChangeEmailResult +createEmailChangeToken lusr email updateOrigin = do + let ek = mkEmailKey email + u = tUnqualified lusr + blocklisted <- BlockListStore.exists ek + when blocklisted $ throw UserSubsystemChangeBlocklistedEmail + available <- keyAvailable ek (Just u) + unless available $ throw UserSubsystemEmailExists + usr <- + getLocalAccountBy WithPendingInvitations lusr + >>= note UserSubsystemProfileNotFound + case emailIdentity =<< userIdentity usr of + -- The user already has an email address and the new one is exactly the same + Just current | current == email -> pure ChangeEmailIdempotent + _ -> do + unless (userManagedBy usr /= ManagedByScim || updateOrigin == UpdateOriginScim) $ + throw UserSubsystemEmailManagedByScim + actTimeout <- inputs (.activationCodeTimeout) + act <- newActivationCode ek actTimeout (Just u) + pure $ ChangeEmailNeedsActivation (usr, act, email) + ------------------------------------------ -- FUTUREWORK: Pending functions for a team subsystem ------------------------------------------ diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs index 90a2d39a888..f9005275289 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs @@ -14,7 +14,9 @@ data UserSubsystemError UserSubsystemDisplayNameManagedByScim | UserSubsystemHandleManagedByScim | UserSubsystemLocaleManagedByScim + | UserSubsystemEmailManagedByScim | UserSubsystemNoIdentity + | UserSubsystemLastIdentity | UserSubsystemHandleExists | UserSubsystemInvalidHandle | UserSubsystemProfileNotFound @@ -28,6 +30,8 @@ data UserSubsystemError | UserSubsystemInvitationNotFound | UserSubsystemUserNotAllowedToJoinTeam Wai.Error | UserSubsystemMLSServicesNotAllowed + | UserSubsystemChangeBlocklistedEmail + | UserSubsystemEmailExists deriving (Eq, Show) userSubsystemErrorToHttpError :: UserSubsystemError -> HttpError @@ -36,7 +40,9 @@ userSubsystemErrorToHttpError = UserSubsystemProfileNotFound -> errorToWai @E.UserNotFound UserSubsystemDisplayNameManagedByScim -> errorToWai @E.NameManagedByScim UserSubsystemLocaleManagedByScim -> errorToWai @E.LocaleManagedByScim + UserSubsystemEmailManagedByScim -> errorToWai @E.EmailManagedByScim UserSubsystemNoIdentity -> errorToWai @E.NoIdentity + UserSubsystemLastIdentity -> errorToWai @E.LastIdentity UserSubsystemHandleExists -> errorToWai @E.HandleExists UserSubsystemInvalidHandle -> errorToWai @E.InvalidHandle UserSubsystemHandleManagedByScim -> errorToWai @E.HandleManagedByScim @@ -50,5 +56,7 @@ userSubsystemErrorToHttpError = UserSubsystemInvitationNotFound -> Wai.mkError status404 "not-found" "Something went wrong, while looking up the invitation" UserSubsystemUserNotAllowedToJoinTeam e -> e UserSubsystemMLSServicesNotAllowed -> errorToWai @E.MLSServicesNotAllowed + UserSubsystemChangeBlocklistedEmail -> errorToWai @E.BlacklistedEmail + UserSubsystemEmailExists -> errorToWai @'E.UserKeyExists instance Exception UserSubsystemError diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 8f9ba2566e1..5424bbe4f5b 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -48,7 +48,6 @@ import Wire.API.User as User import Wire.API.User.RichInfo import Wire.API.User.Search import Wire.API.UserEvent -import Wire.Arbitrary import Wire.AuthenticationSubsystem import Wire.BlockListStore as BlockList import Wire.DeleteQueue @@ -75,17 +74,9 @@ import Wire.UserStore.IndexUser import Wire.UserSubsystem import Wire.UserSubsystem.Error import Wire.UserSubsystem.HandleBlacklist +import Wire.UserSubsystem.UserSubsystemConfig import Witherable (wither) -data UserSubsystemConfig = UserSubsystemConfig - { emailVisibilityConfig :: EmailVisibilityConfig, - defaultLocale :: Locale, - searchSameTeamOnly :: Bool, - maxTeamSize :: Word32 - } - deriving (Show, Generic) - deriving (Arbitrary) via (GenericUniform UserSubsystemConfig) - runUserSubsystem :: ( Member UserStore r, Member UserKeyStore r, @@ -157,6 +148,7 @@ runUserSubsystem authInterpreter = interpret $ InternalFindTeamInvitation mEmailKey code -> internalFindTeamInvitationImpl mEmailKey code GetUserExportData uid -> getUserExportDataImpl uid + RemoveEmailEither luid -> removeEmailEitherImpl luid scimExtId :: StoredUser -> Maybe Text scimExtId su = do @@ -974,3 +966,26 @@ getUserExportDataImpl uid = fmap hush . runError @() $ do tExportLastActive = lastActive, tExportStatus = su.status } + +removeEmailEitherImpl :: + ( Member UserKeyStore r, + Member UserStore r, + Member Events r, + Member IndexedUserStore r, + Member (Input UserSubsystemConfig) r, + Member GalleyAPIAccess r, + Member Metrics r + ) => + Local UserId -> + Sem r (Either UserSubsystemError ()) +removeEmailEitherImpl lusr = runError $ do + let uid = tUnqualified lusr + ident <- getSelfProfileImpl lusr >>= note UserSubsystemProfileNotFound + case ident.selfUser.userIdentity of + Just (SSOIdentity (UserSSOId _) (Just e)) -> do + deleteKey $ mkEmailKey e + deleteEmail uid + generateUserEvent uid Nothing (emailRemoved uid e) + syncUserIndex uid + Just _ -> throw UserSubsystemLastIdentity + Nothing -> throw UserSubsystemNoIdentity diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/UserSubsystemConfig.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/UserSubsystemConfig.hs new file mode 100644 index 00000000000..094e541a8b1 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/UserSubsystemConfig.hs @@ -0,0 +1,16 @@ +module Wire.UserSubsystem.UserSubsystemConfig where + +import Imports +import Util.Timeout +import Wire.API.User +import Wire.Arbitrary + +data UserSubsystemConfig = UserSubsystemConfig + { emailVisibilityConfig :: EmailVisibilityConfig, + defaultLocale :: Locale, + searchSameTeamOnly :: Bool, + maxTeamSize :: Word32, + activationCodeTimeout :: Timeout + } + deriving (Show, Generic) + deriving (Arbitrary) via (GenericUniform UserSubsystemConfig) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs index 0265c8d07fe..a31d31cbfc3 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs @@ -2,12 +2,36 @@ module Wire.MockInterpreters.ActivationCodeStore where import Data.Id import Data.Map +import Data.Text (pack) +import Data.Text.Ascii qualified as Ascii +import Data.Text.Encoding qualified as T import Imports import Polysemy import Polysemy.State +import Text.Printf (printf) import Wire.API.User.Activation import Wire.ActivationCodeStore (ActivationCodeStore (..)) import Wire.UserKeyStore -inMemoryActivationCodeStoreInterpreter :: (Member (State (Map EmailKey (Maybe UserId, ActivationCode))) r) => InterpreterFor ActivationCodeStore r -inMemoryActivationCodeStoreInterpreter = interpret \case LookupActivationCode ek -> gets (!? ek) +inMemoryActivationCodeStoreInterpreter :: + ( Member (State (Map EmailKey (Maybe UserId, ActivationCode))) r + ) => + InterpreterFor ActivationCodeStore r +inMemoryActivationCodeStoreInterpreter = interpret \case + LookupActivationCode ek -> gets (!? ek) + NewActivationCode ek _ uid -> do + let key = + ActivationKey + . Ascii.encodeBase64Url + . T.encodeUtf8 + . emailKeyUniq + $ ek + code = + ActivationCode + . Ascii.unsafeFromText + . pack + . printf "%06d" + . length + . show + $ ek + modify (insert ek (uid, code)) $> Activation key code diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index 133365cf986..d3f00a25016 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -37,6 +37,13 @@ inMemoryUserStoreInterpreter = interpret $ \case . maybe Imports.id setStoredUserSupportedProtocols update.supportedProtocols $ u else u + UpdateEmailUnvalidated uid email -> modify (map doUpdate) + where + doUpdate :: StoredUser -> StoredUser + doUpdate u = + if u.id == uid + then u {emailUnvalidated = Just email} + else u GetIndexUser uid -> gets $ fmap storedUserToIndexUser . find (\user -> user.id == uid) GetIndexUsersPaginated _pageSize _pagingState -> @@ -74,6 +81,7 @@ inMemoryUserStoreInterpreter = interpret $ \case GetActivityTimestamps _ -> pure [] GetRichInfo _ -> error "rich info not implemented" GetUserAuthenticationInfo _uid -> error "Not implemented" + DeleteEmail _uid -> error "Not implemented" storedUserToIndexUser :: StoredUser -> IndexUser storedUserToIndexUser storedUser = diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index c573d4709c5..3b82816399e 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -57,7 +57,7 @@ spec = describe "UserSubsystem.Interpreter" do target1 = mkUserIds remoteDomain1 targetUsers1 target2 = mkUserIds remoteDomain2 targetUsers2 localBackend = def {users = [viewer] <> localTargetUsers} - config = UserSubsystemConfig visibility miniLocale False 100 + config = UserSubsystemConfig visibility miniLocale False 100 undefined retrievedProfiles = runFederationStack localBackend federation Nothing config $ getUserProfiles @@ -85,7 +85,7 @@ spec = describe "UserSubsystem.Interpreter" do mkUserIds domain users = map (flip Qualified domain . (.id)) users onlineUsers = mkUserIds onlineDomain onlineTargetUsers offlineUsers = mkUserIds offlineDomain offlineTargetUsers - config = UserSubsystemConfig visibility miniLocale False 100 + config = UserSubsystemConfig visibility miniLocale False 100 undefined localBackend = def {users = [viewer]} result = run @@ -155,7 +155,7 @@ spec = describe "UserSubsystem.Interpreter" do \viewer targetUsers visibility domain remoteDomain -> do let remoteBackend = def {users = targetUsers} federation = [(remoteDomain, remoteBackend)] - config = UserSubsystemConfig visibility miniLocale False 100 + config = UserSubsystemConfig visibility miniLocale False 100 undefined localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend federation Nothing config $ @@ -176,7 +176,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "Remote users on offline backend always fail to return" $ \viewer (targetUsers :: Set StoredUser) visibility domain remoteDomain -> do let online = mempty - config = UserSubsystemConfig visibility miniLocale False 100 + config = UserSubsystemConfig visibility miniLocale False 100 undefined localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend online Nothing config $ @@ -196,7 +196,7 @@ spec = describe "UserSubsystem.Interpreter" do allDomains = [domain, remoteDomainA, remoteDomainB] remoteAUsers = map (flip Qualified remoteDomainA . (.id)) targetUsers remoteBUsers = map (flip Qualified remoteDomainB . (.id)) targetUsers - config = UserSubsystemConfig visibility miniLocale False 100 + config = UserSubsystemConfig visibility miniLocale False 100 undefined localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend online Nothing config $ @@ -281,7 +281,7 @@ spec = describe "UserSubsystem.Interpreter" do describe "getAccountsBy" do prop "GetBy userId when pending fails if not explicitly allowed" $ \(PendingNotEmptyIdentityStoredUser alice') email teamId invitationInfo localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale False 100 + let config = UserSubsystemConfig visibility locale False 100 undefined alice = alice' { email = Just email, @@ -316,7 +316,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy userId works for pending if explicitly queried" $ \(PendingNotEmptyIdentityStoredUser alice') email teamId invitationInfo localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale True 100 + let config = UserSubsystemConfig visibility locale True 100 undefined alice = alice' { email = Just email, @@ -350,7 +350,7 @@ spec = describe "UserSubsystem.Interpreter" do in result === [mkUserFromStored localDomain locale alice] prop "GetBy handle when pending fails if not explicitly allowed" $ \(PendingNotEmptyIdentityStoredUser alice') handl email teamId invitationInfo localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale True 100 + let config = UserSubsystemConfig visibility locale True 100 undefined alice = alice' { email = Just email, @@ -386,7 +386,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy handle works for pending if explicitly queried" $ \(PendingNotEmptyIdentityStoredUser alice') handl email teamId invitationInfo localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale True 100 + let config = UserSubsystemConfig visibility locale True 100 undefined alice = alice' { email = Just email, @@ -422,7 +422,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy email does not filter by pending, missing identity or expired invitations" $ \(alice' :: StoredUser) email localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale True 100 + let config = UserSubsystemConfig visibility locale True 100 undefined alice = alice' {email = Just email} localBackend = def @@ -436,7 +436,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy userId does not return missing identity users, pending invitation off" $ \(NotPendingEmptyIdentityStoredUser alice) localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale True 100 + let config = UserSubsystemConfig visibility locale True 100 undefined getBy = toLocalUnsafe localDomain $ def @@ -451,7 +451,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy userId does not return missing identity users, pending invtation on" $ \(NotPendingEmptyIdentityStoredUser alice) localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale True 100 + let config = UserSubsystemConfig visibility locale True 100 undefined getBy = toLocalUnsafe localDomain $ def @@ -466,7 +466,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy pending user by id works if there is a valid invitation" $ \(PendingNotEmptyIdentityStoredUser alice') (email :: EmailAddress) teamId (invitationInfo :: StoredInvitation) localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale True 100 + let config = UserSubsystemConfig visibility locale True 100 undefined emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -495,7 +495,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy pending user by id fails if there is no valid invitation" $ \(PendingNotEmptyIdentityStoredUser alice') (email :: EmailAddress) teamId localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale True 100 + let config = UserSubsystemConfig visibility locale True 100 undefined emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -516,7 +516,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy pending user handle id works if there is a valid invitation" $ \(PendingNotEmptyIdentityStoredUser alice') (email :: EmailAddress) handl teamId (invitationInfo :: StoredInvitation) localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale True 100 + let config = UserSubsystemConfig visibility locale True 100 undefined emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -550,7 +550,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "GetBy pending user by handle fails if there is no valid invitation" $ \(PendingNotEmptyIdentityStoredUser alice') (email :: EmailAddress) handl teamId localDomain visibility locale -> - let config = UserSubsystemConfig visibility locale True 100 + let config = UserSubsystemConfig visibility locale True 100 undefined emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index d9000793018..2e591f788db 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -135,6 +135,7 @@ library Wire.UserSubsystem.Error Wire.UserSubsystem.HandleBlacklist Wire.UserSubsystem.Interpreter + Wire.UserSubsystem.UserSubsystemConfig Wire.VerificationCode Wire.VerificationCodeGen Wire.VerificationCodeStore diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index 021ca38aabc..3821d6b4534 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -20,7 +20,6 @@ module Brig.API.Auth where import Brig.API.Error import Brig.API.Handler import Brig.API.Types -import Brig.API.User import Brig.App import Brig.Options import Brig.User.Auth qualified as Auth @@ -39,6 +38,7 @@ import Network.HTTP.Types import Network.Wai.Utilities ((!>>)) import Network.Wai.Utilities.Error qualified as Wai import Polysemy +import Polysemy.Error (Error) import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Wire.API.Error @@ -57,7 +57,10 @@ import Wire.Events (Events) import Wire.GalleyAPIAccess import Wire.UserKeyStore import Wire.UserStore -import Wire.UserSubsystem +import Wire.UserSubsystem (UpdateOriginType (..), UserSubsystem) +import Wire.UserSubsystem qualified as User +import Wire.UserSubsystem.Error +import Wire.UserSubsystem.UserSubsystemConfig import Wire.VerificationCodeSubsystem (VerificationCodeSubsystem) accessH :: @@ -128,23 +131,29 @@ logout :: (TokenPair u a) => NonEmpty (Token u) -> Maybe (Token a) -> Handler r logout _ Nothing = throwStd authMissingToken logout uts (Just at) = Auth.logout (List1 uts) at !>> zauthError -changeSelfEmailH :: +changeSelfEmail :: ( Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r, - Member UserSubsystem r + Member UserSubsystem r, + Member UserStore r, + Member ActivationCodeStore r, + Member (Error UserSubsystemError) r, + Member (Input UserSubsystemConfig) r ) => [Either Text SomeUserToken] -> Maybe (Either Text SomeAccessToken) -> EmailUpdate -> Handler r ChangeEmailResponse -changeSelfEmailH uts' mat' up = do +changeSelfEmail uts' mat' up = do uts <- handleTokenErrors uts' mat <- traverse handleTokenError mat' toks <- partitionTokens uts mat usr <- either (uncurry validateCredentials) (uncurry validateCredentials) toks + lusr <- qualifyLocal usr let email = euEmail up - changeSelfEmail usr email UpdateOriginWireClient + lift . liftSem $ + User.requestEmailChange lusr email UpdateOriginWireClient validateCredentials :: (TokenPair u a) => diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index 019e3786c1b..289639b0c40 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -76,12 +76,6 @@ sendActCodeError (InvalidRecipient _) = StdError $ errorToWai @'E.InvalidEmail sendActCodeError (UserKeyInUse _) = StdError (errorToWai @'E.UserKeyExists) sendActCodeError (ActivationBlacklistedUserKey _) = StdError blacklistedEmail -changeEmailError :: ChangeEmailError -> HttpError -changeEmailError (InvalidNewEmail _ _) = StdError (errorToWai @'E.InvalidEmail) -changeEmailError (EmailExists _) = StdError (errorToWai @'E.UserKeyExists) -changeEmailError (ChangeBlacklistedEmail _) = StdError blacklistedEmail -changeEmailError EmailManagedByScim = StdError $ propertyManagedByScim "email" - legalHoldLoginError :: LegalHoldLoginError -> HttpError legalHoldLoginError LegalHoldLoginNoBindingTeam = StdError noBindingTeam legalHoldLoginError LegalHoldLoginLegalHoldNotEnabled = StdError legalHoldNotEnabled diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index ea9c2f26f20..75a80dde93e 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -117,6 +117,7 @@ import Wire.UserStore as UserStore import Wire.UserSubsystem import Wire.UserSubsystem qualified as UserSubsystem import Wire.UserSubsystem.Error +import Wire.UserSubsystem.UserSubsystemConfig import Wire.VerificationCode import Wire.VerificationCodeGen import Wire.VerificationCodeSubsystem @@ -149,7 +150,8 @@ servantSitemap :: Member (Polysemy.Error UserSubsystemError) r, Member HashPassword r, Member (Embed IO) r, - Member ActivationCodeStore r + Member ActivationCodeStore r, + Member (Input UserSubsystemConfig) r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -203,7 +205,9 @@ accountAPI :: Member HashPassword r, Member InvitationStore r, Member (Embed IO) r, - Member ActivationCodeStore r + Member ActivationCodeStore r, + Member (Polysemy.Error UserSubsystemError) r, + Member (Input UserSubsystemConfig) r ) => ServerT BrigIRoutes.AccountAPI (Handler r) accountAPI = @@ -477,7 +481,8 @@ createUserNoVerify :: Member UserSubsystem r, Member (Input (Local ())) r, Member HashPassword r, - Member PasswordResetCodeStore r + Member PasswordResetCodeStore r, + Member ActivationCodeStore r ) => NewUser -> (Handler r) (Either RegisterError SelfProfile) @@ -538,7 +543,11 @@ changeSelfEmailMaybeSendH :: ( Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r, - Member UserSubsystem r + Member UserSubsystem r, + Member UserStore r, + Member ActivationCodeStore r, + Member (Polysemy.Error UserSubsystemError) r, + Member (Input UserSubsystemConfig) r ) => UserId -> EmailUpdate -> @@ -554,7 +563,11 @@ changeSelfEmailMaybeSend :: ( Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r, - Member UserSubsystem r + Member UserSubsystem r, + Member UserStore r, + Member ActivationCodeStore r, + Member (Polysemy.Error UserSubsystemError) r, + Member (Input UserSubsystemConfig) r ) => UserId -> MaybeSendEmail -> @@ -562,11 +575,16 @@ changeSelfEmailMaybeSend :: UpdateOriginType -> (Handler r) ChangeEmailResponse changeSelfEmailMaybeSend u ActuallySendEmail email allowScim = do - API.changeSelfEmail u email allowScim + lusr <- qualifyLocal u + lift . liftSem $ + UserSubsystem.requestEmailChange lusr email allowScim changeSelfEmailMaybeSend u DoNotSendEmail email allowScim = do - API.changeEmail u email allowScim !>> changeEmailError >>= \case - ChangeEmailIdempotent -> pure ChangeEmailResponseIdempotent - ChangeEmailNeedsActivation _ -> pure ChangeEmailResponseNeedsActivation + lusr <- qualifyLocal u + (lift . liftSem) + (UserSubsystem.createEmailChangeToken lusr email allowScim) + >>= \case + ChangeEmailIdempotent -> pure ChangeEmailResponseIdempotent + ChangeEmailNeedsActivation _ -> pure ChangeEmailResponseNeedsActivation -- Historically, this end-point was two end-points with distinct matching routes -- (distinguished by query params), and it was only allowed to pass one param per call. This diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 6580a413959..8b3b484e236 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -173,9 +173,10 @@ import Wire.UserKeyStore import Wire.UserSearch.Types import Wire.UserStore (UserStore) import Wire.UserStore qualified as UserStore -import Wire.UserSubsystem hiding (checkHandle, checkHandles) +import Wire.UserSubsystem hiding (checkHandle, checkHandles, removeEmail, requestEmailChange) import Wire.UserSubsystem qualified as User import Wire.UserSubsystem.Error +import Wire.UserSubsystem.UserSubsystemConfig import Wire.VerificationCode import Wire.VerificationCodeGen import Wire.VerificationCodeSubsystem @@ -363,7 +364,8 @@ servantSitemap :: Member BlockListStore r, Member (ConnectionStore InternalPaging) r, Member IndexedUserStore r, - Member HashPassword r + Member HashPassword r, + Member (Input UserSubsystemConfig) r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -511,7 +513,7 @@ servantSitemap = :<|> Named @"send-login-code" sendLoginCode :<|> Named @"login" login :<|> Named @"logout" logoutH - :<|> Named @"change-self-email" changeSelfEmailH + :<|> Named @"change-self-email" changeSelfEmail :<|> Named @"list-cookies" listCookies :<|> Named @"remove-cookies" removeCookies @@ -811,7 +813,8 @@ createUser :: Member UserSubsystem r, Member PasswordResetCodeStore r, Member HashPassword r, - Member EmailSending r + Member EmailSending r, + Member ActivationCodeStore r ) => Public.NewUserPublic -> Handler r (Either Public.RegisterError Public.RegisterSuccess) @@ -1024,13 +1027,18 @@ removePhone :: UserId -> Handler r (Maybe Public.RemoveIdentityError) removePhone _ = (lift . pure) Nothing removeEmail :: - ( Member UserKeyStore r, - Member UserSubsystem r, - Member Events r + ( Member UserSubsystem r, + Member (Error UserSubsystemError) r ) => - UserId -> + Local UserId -> Handler r (Maybe Public.RemoveIdentityError) -removeEmail self = lift . exceptTToMaybe $ API.removeEmail self +removeEmail = lift . liftSem . User.removeEmailEither >=> reint + where + reint = \case + Left UserSubsystemNoIdentity -> pure . Just $ Public.NoIdentity + Left UserSubsystemLastIdentity -> pure . Just $ Public.LastIdentity + Left e -> lift . liftSem . throw $ e + Right () -> pure Nothing checkPasswordExists :: (Member PasswordStore r) => UserId -> (Handler r) Bool checkPasswordExists = fmap isJust . lift . liftSem . lookupHashedPassword @@ -1101,7 +1109,12 @@ getHandleInfoUnqualifiedH self handle = do Public.UserHandleInfo . Public.profileQualifiedId <$$> Handle.getHandleInfo self (Qualified handle domain) -changeHandle :: (Member UserSubsystem r) => Local UserId -> ConnId -> Public.HandleUpdate -> Handler r () +changeHandle :: + (Member UserSubsystem r) => + Local UserId -> + ConnId -> + Public.HandleUpdate -> + Handler r () changeHandle u conn (Public.HandleUpdate h) = lift $ liftSem do User.updateHandle u (Just conn) UpdateOriginWireClient h @@ -1336,7 +1349,11 @@ updateUserEmail :: Member UserKeyStore r, Member GalleyAPIAccess r, Member EmailSubsystem r, - Member UserSubsystem r + Member UserSubsystem r, + Member UserStore r, + Member ActivationCodeStore r, + Member (Error UserSubsystemError) r, + Member (Input UserSubsystemConfig) r ) => UserId -> UserId -> @@ -1347,7 +1364,9 @@ updateUserEmail zuserId emailOwnerId (Public.EmailUpdate email) = do whenM (not <$> assertHasPerm maybeZuserTeamId) $ throwStd insufficientTeamPermissions maybeEmailOwnerTeamId <- lift $ wrapClient $ Data.lookupUserTeam emailOwnerId checkSameTeam maybeZuserTeamId maybeEmailOwnerTeamId - void $ API.changeSelfEmail emailOwnerId email UpdateOriginWireClient + lEmailOwnerId <- qualifyLocal emailOwnerId + void . lift . liftSem $ + User.requestEmailChange lEmailOwnerId email UpdateOriginWireClient where checkSameTeam :: Maybe TeamId -> Maybe TeamId -> (Handler r) () checkSameTeam (Just zuserTeamId) maybeEmailOwnerTeamId = diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 5da615a530f..1936a91c644 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -30,7 +30,7 @@ module Brig.API.Types ) where -import Brig.Data.Activation (Activation (..), ActivationError (..)) +import Brig.Data.Activation (ActivationError (..)) import Brig.Data.Client (ClientDataError (..)) import Brig.Types.Intra import Data.Code @@ -42,6 +42,7 @@ import Imports import Network.Wai.Utilities.Error qualified as Wai import Wire.API.Federation.Error import Wire.API.User +import Wire.API.User.Activation import Wire.AuthenticationSubsystem.Error import Wire.UserKeyStore @@ -65,14 +66,6 @@ data ActivationResult ActivationPass deriving (Show) --- | Outcome of the invariants check in 'Brig.API.User.changeEmail'. -data ChangeEmailResult - = -- | The request was successful, user needs to verify the new email address - ChangeEmailNeedsActivation !(User, Activation, EmailAddress) - | -- | The user asked to change the email address to the one already owned - ChangeEmailIdempotent - deriving (Show) - ------------------------------------------------------------------------------- -- Failures @@ -153,12 +146,6 @@ data VerificationCodeError | VerificationCodeNoPendingCode | VerificationCodeNoEmail -data ChangeEmailError - = InvalidNewEmail !EmailAddress !String - | EmailExists !EmailAddress - | ChangeBlacklistedEmail !EmailAddress - | EmailManagedByScim - data SendActivationCodeError = InvalidRecipient EmailKey | UserKeyInUse EmailKey diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 7ff8e0f22b1..8c6a2e6fb67 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -25,8 +25,6 @@ module Brig.API.User createUserSpar, createUserInviteViaScim, checkRestrictedUserCreation, - changeSelfEmail, - changeEmail, CheckHandleResp (..), checkHandle, lookupHandle, @@ -67,7 +65,6 @@ module Brig.API.User ) where -import Brig.API.Error qualified as Error import Brig.API.Types import Brig.API.Util import Brig.App as App @@ -128,7 +125,7 @@ import Wire.API.User.Activation import Wire.API.User.Client import Wire.API.User.RichInfo import Wire.API.UserEvent -import Wire.ActivationCodeStore (ActivationCodeStore) +import Wire.ActivationCodeStore import Wire.ActivationCodeStore qualified as ActivationCode import Wire.AuthenticationSubsystem (AuthenticationSubsystem, internalLookupPasswordResetCode) import Wire.BlockListStore as BlockListStore @@ -319,7 +316,8 @@ createUser :: Member (Input (Local ())) r, Member PasswordResetCodeStore r, Member HashPassword r, - Member InvitationStore r + Member InvitationStore r, + Member ActivationCodeStore r ) => NewUser -> ExceptT RegisterError (AppT r) CreateUserResult @@ -479,17 +477,22 @@ createUser new = do pure $ CreateUserTeam tid nm -- Handle e-mail activation (deprecated, see #RefRegistrationNoPreverification in /docs/reference/user/registration.md) - handleEmailActivation :: Maybe EmailAddress -> UserId -> Maybe BindingNewTeamUser -> ExceptT RegisterError (AppT r) (Maybe Activation) + handleEmailActivation :: + Maybe EmailAddress -> + UserId -> + Maybe BindingNewTeamUser -> + ExceptT RegisterError (AppT r) (Maybe Activation) handleEmailActivation email uid newTeam = do fmap join . for (mkEmailKey <$> email) $ \ek -> case newUserEmailCode new of Nothing -> do timeout <- asks (.settings.activationTimeout) - edata <- lift . wrapClient $ Data.newActivation ek timeout (Just uid) - lift . liftSem . Log.info $ - field "user" (toByteString uid) - . field "activation.key" (toByteString $ activationKey edata) - . msg (val "Created email activation key/code pair") - pure $ Just edata + lift . liftSem $ do + edata <- newActivationCode ek timeout (Just uid) + Log.info $ + field "user" (toByteString uid) + . field "activation.key" (toByteString $ activationKey edata) + . msg (val "Created email activation key/code pair") + pure $ Just edata Just c -> do ak <- liftIO $ Data.mkActivationKey ek void $ @@ -546,82 +549,6 @@ checkRestrictedUserCreation new = do ) $ throwE RegisterErrorUserCreationRestricted -------------------------------------------------------------------------------- --- Change Email - --- | Call 'changeEmail' and process result: if email changes to itself, succeed, if not, send --- validation email. -changeSelfEmail :: - ( Member BlockListStore r, - Member UserKeyStore r, - Member EmailSubsystem r, - Member UserSubsystem r - ) => - UserId -> - EmailAddress -> - UpdateOriginType -> - ExceptT HttpError (AppT r) ChangeEmailResponse -changeSelfEmail u email allowScim = do - changeEmail u email allowScim !>> Error.changeEmailError >>= \case - ChangeEmailIdempotent -> - pure ChangeEmailResponseIdempotent - ChangeEmailNeedsActivation (usr, adata, en) -> lift $ do - liftSem $ sendOutEmail usr adata en - wrapClient $ Data.updateEmailUnvalidated u email - liftSem $ User.internalUpdateSearchIndex u - pure ChangeEmailResponseNeedsActivation - where - sendOutEmail usr adata en = do - (maybe sendActivationMail (const sendEmailAddressUpdateMail) usr.userIdentity) - en - (userDisplayName usr) - (activationKey adata) - (activationCode adata) - (Just (userLocale usr)) - --- | Prepare changing the email (checking a number of invariants). -changeEmail :: (Member BlockListStore r, Member UserKeyStore r) => UserId -> EmailAddress -> UpdateOriginType -> ExceptT ChangeEmailError (AppT r) ChangeEmailResult -changeEmail u email updateOrigin = do - let ek = mkEmailKey email - blacklisted <- lift . liftSem $ BlockListStore.exists ek - when blacklisted $ - throwE (ChangeBlacklistedEmail email) - available <- lift $ liftSem $ keyAvailable ek (Just u) - unless available $ - throwE $ - EmailExists email - usr <- maybe (throwM $ UserProfileNotFound u) pure =<< lift (wrapClient $ Data.lookupUser WithPendingInvitations u) - case emailIdentity =<< userIdentity usr of - -- The user already has an email address and the new one is exactly the same - Just current | current == email -> pure ChangeEmailIdempotent - _ -> do - unless (userManagedBy usr /= ManagedByScim || updateOrigin == UpdateOriginScim) $ - throwE EmailManagedByScim - timeout <- asks (.settings.activationTimeout) - act <- lift . wrapClient $ Data.newActivation ek timeout (Just u) - pure $ ChangeEmailNeedsActivation (usr, act, email) - -------------------------------------------------------------------------------- --- Remove Email - -removeEmail :: - ( Member UserKeyStore r, - Member UserSubsystem r, - Member Events r - ) => - UserId -> - ExceptT RemoveIdentityError (AppT r) () -removeEmail uid = do - ident <- lift $ fetchUserIdentity uid - case ident of - Just (SSOIdentity (UserSSOId _) (Just e)) -> lift $ do - liftSem $ deleteKey $ mkEmailKey e - wrapClient $ Data.deleteEmail uid - liftSem $ Events.generateUserEvent uid Nothing (emailRemoved uid e) - liftSem $ User.internalUpdateSearchIndex uid - Just _ -> throwE LastIdentity - Nothing -> throwE NoIdentity - ------------------------------------------------------------------------------- -- Forcefully revoke a verified identity @@ -775,6 +702,7 @@ onActivated (EmailActivated uid email) = do -- docs/reference/user/activation.md {#RefActivationRequest} sendActivationCode :: + forall r. ( Member BlockListStore r, Member EmailSubsystem r, Member GalleyAPIAccess r, @@ -800,22 +728,27 @@ sendActivationCode email loc = do Just (Just uid, c) -> sendActivationEmail ek c uid -- User re-requesting activation where notFound = throwM . UserDisplayNameNotFound + mkPair :: + EmailKey -> + Maybe ActivationCode -> + Maybe UserId -> + ExceptT SendActivationCodeError (AppT r) (ActivationKey, ActivationCode) mkPair k c u = do timeout <- asks (.settings.activationTimeout) case c of Just c' -> liftIO $ (,c') <$> Data.mkActivationKey k - Nothing -> lift $ do - dat <- Data.newActivation k timeout u + Nothing -> lift . liftSem $ do + dat <- newActivationCode k timeout u pure (activationKey dat, activationCode dat) sendVerificationEmail ek uc = do - (key, code) <- wrapClientE $ mkPair ek uc Nothing + (key, code) <- mkPair ek uc Nothing let em = emailKeyOrig ek lift $ liftSem $ sendVerificationMail em key code loc sendActivationEmail ek uc uid = do -- FUTUREWORK(fisx): we allow for 'PendingInvitations' here, but I'm not sure this -- top-level function isn't another piece of a deprecated onboarding flow? u <- maybe (notFound uid) pure =<< lift (wrapClient $ Data.lookupUser WithPendingInvitations uid) - (aKey, aCode) <- wrapClientE $ mkPair ek (Just uc) (Just uid) + (aKey, aCode) <- mkPair ek (Just uc) (Just uid) let ident = userIdentity u name = userDisplayName u loc' = loc <|> Just (userLocale u) diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 5248effd92b..18e53608df1 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -176,7 +176,8 @@ runBrigToIO e (AppT ma) = do { emailVisibilityConfig = e.settings.emailVisibility, defaultLocale = Opt.defaultUserLocale e.settings, searchSameTeamOnly = fromMaybe False e.settings.searchSameTeamOnly, - maxTeamSize = e.settings.maxTeamSize + maxTeamSize = e.settings.maxTeamSize, + activationCodeTimeout = e.settings.activationTimeout } teamInvitationSubsystemConfig = TeamInvitationSubsystemConfig @@ -213,6 +214,9 @@ runBrigToIO e (AppT ma) = do } -- These interpreters depend on each other, we use let recursion to solve that. + -- + -- This terminates if and only if we do not create an action sequence at + -- runtime such that interpretation of actions results in a call cycle. userSubsystemInterpreter :: (Members BrigLowerLevelEffects r) => InterpreterFor UserSubsystem r userSubsystemInterpreter = runUserSubsystem authSubsystemInterpreter diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index ae9ce48899f..981038f9d42 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -17,11 +17,9 @@ -- | Activation of 'Email' addresses and 'Phone' numbers. module Brig.Data.Activation - ( Activation (..), - ActivationEvent (..), + ( ActivationEvent (..), ActivationError (..), activationErrorToRegisterError, - newActivation, mkActivationKey, activateKey, verifyCode, @@ -34,16 +32,12 @@ import Brig.Types.Intra import Cassandra import Control.Error import Data.Id -import Data.Text (pack) import Data.Text.Ascii qualified as Ascii import Data.Text.Encoding qualified as T import Data.Text.Lazy qualified as LT import Imports -import OpenSSL.BN (randIntegerZeroToNMinusOne) import OpenSSL.EVP.Digest (digestBS, getDigestByName) import Polysemy -import Text.Printf (printf) -import Util.Timeout import Wire.API.User import Wire.API.User.Activation import Wire.API.User.Password @@ -53,15 +47,6 @@ import Wire.UserKeyStore import Wire.UserSubsystem (UserSubsystem) import Wire.UserSubsystem qualified as User --- | The information associated with the pending activation of a 'UserKey'. -data Activation = Activation - { -- | An opaque key for the original 'UserKey' pending activation. - activationKey :: !ActivationKey, - -- | The confidential activation code. - activationCode :: !ActivationCode - } - deriving (Eq, Show) - data ActivationError = UserKeyExists !LT.Text | InvalidActivationCodeWrongUser @@ -82,10 +67,6 @@ data ActivationEvent | EmailActivated !UserId !EmailAddress deriving (Show) --- | Max. number of activation attempts per 'ActivationKey'. -maxAttempts :: Int32 -maxAttempts = 3 - -- docs/reference/user/activation.md {#RefActivationSubmit} activateKey :: forall r. @@ -151,29 +132,6 @@ activateKey k c u = wrapClientE (verifyCode k c) >>= pickUser >>= activate throwE . UserKeyExists . LT.fromStrict $ fromEmail (emailKeyOrig key) --- | Create a new pending activation for a given 'EmailKey'. -newActivation :: - (MonadClient m) => - EmailKey -> - -- | The timeout for the activation code. - Timeout -> - -- | The user with whom to associate the activation code. - Maybe UserId -> - m Activation -newActivation uk timeout u = do - let typ = "email" - key = fromEmail (emailKeyOrig uk) - code <- liftIO $ genCode - insert typ key code - where - insert t k c = do - key <- liftIO $ mkActivationKey uk - retry x5 . write keyInsert $ params LocalQuorum (key, t, k, c, u, maxAttempts, round timeout) - pure $ Activation key c - genCode = - ActivationCode . Ascii.unsafeFromText . pack . printf "%06d" - <$> randIntegerZeroToNMinusOne 1000000 - -- | Verify an activation code. verifyCode :: (MonadClient m) => @@ -196,6 +154,11 @@ verifyCode key code = do mkScope _ _ _ = throwE invalidCode countdown = lift . retry x5 . write keyInsert . params LocalQuorum revoke = lift $ deleteActivationPair key + keyInsert :: PrepQuery W (ActivationKey, Text, Text, ActivationCode, Maybe UserId, Int32, Int32) () + keyInsert = + "INSERT INTO activation_keys \ + \(key, key_type, key_text, code, user, retries) VALUES \ + \(? , ? , ? , ? , ? , ? ) USING TTL ?" mkActivationKey :: EmailKey -> IO ActivationKey mkActivationKey k = do @@ -213,12 +176,6 @@ invalidUser = InvalidActivationCodeWrongUser -- "User does not exist." invalidCode :: ActivationError invalidCode = InvalidActivationCodeWrongCode -- "Invalid activation code" -keyInsert :: PrepQuery W (ActivationKey, Text, Text, ActivationCode, Maybe UserId, Int32, Int32) () -keyInsert = - "INSERT INTO activation_keys \ - \(key, key_type, key_text, code, user, retries) VALUES \ - \(? , ? , ? , ? , ? , ? ) USING TTL ?" - keySelect :: PrepQuery R (Identity ActivationKey) (Int32, Ascii, Text, ActivationCode, Maybe UserId, Int32) keySelect = "SELECT ttl(code) as ttl, key_type, key_text, code, user, retries FROM activation_keys WHERE key = ?" diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 7e1f8e57656..326ef1cb780 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -36,7 +36,6 @@ module Brig.Data.User -- * Updates updateEmail, - updateEmailUnvalidated, updateSSOId, updateManagedBy, activateUser, @@ -46,7 +45,6 @@ module Brig.Data.User updateFeatureConferenceCalling, -- * Deletions - deleteEmail, deleteEmailUnvalidated, deleteServiceUser, ) @@ -209,9 +207,6 @@ insertAccount u mbConv password activated = retry x5 . batch $ do updateEmail :: (MonadClient m) => UserId -> EmailAddress -> m () updateEmail u e = retry x5 $ write userEmailUpdate (params LocalQuorum (e, u)) -updateEmailUnvalidated :: (MonadClient m) => UserId -> EmailAddress -> m () -updateEmailUnvalidated u e = retry x5 $ write userEmailUnvalidatedUpdate (params LocalQuorum (e, u)) - updateSSOId :: (MonadClient m) => UserId -> Maybe UserSSOId -> m Bool updateSSOId u ssoid = do mteamid <- lookupUserTeam u @@ -234,9 +229,6 @@ updateFeatureConferenceCalling uid mStatus = update :: PrepQuery W (Maybe FeatureStatus, UserId) () update = fromString "update user set feature_conference_calling = ? where id = ?" -deleteEmail :: (MonadClient m) => UserId -> m () -deleteEmail u = retry x5 $ write userEmailDelete (params LocalQuorum (Identity u)) - deleteEmailUnvalidated :: (MonadClient m) => UserId -> m () deleteEmailUnvalidated u = retry x5 $ write userEmailUnvalidatedDelete (params LocalQuorum (Identity u)) @@ -435,9 +427,6 @@ userInsert = userEmailUpdate :: PrepQuery W (EmailAddress, UserId) () userEmailUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email = ? WHERE id = ?" -userEmailUnvalidatedUpdate :: PrepQuery W (EmailAddress, UserId) () -userEmailUnvalidatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email_unvalidated = ? WHERE id = ?" - userEmailUnvalidatedDelete :: PrepQuery W (Identity UserId) () userEmailUnvalidatedDelete = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email_unvalidated = null WHERE id = ?" @@ -456,9 +445,6 @@ userDeactivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDAT userActivatedUpdate :: PrepQuery W (Maybe EmailAddress, UserId) () userActivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET activated = true, email = ? WHERE id = ?" -userEmailDelete :: PrepQuery W (Identity UserId) () -userEmailDelete = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email = null, write_time_bumper = 0 WHERE id = ?" - userRichInfoUpdate :: PrepQuery W (RichInfoAssocList, UserId) () userRichInfoUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE rich_info SET json = ? WHERE user = ?" diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 07909cbbbcd..243a8a723cf 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -29,7 +29,7 @@ import Control.Concurrent.Async import Control.Lens hiding (from, to, uncons, (#), (.=)) import Control.Monad.Catch (MonadCatch, MonadMask) import Control.Monad.Codensity (lowerCodensity) -import Control.Retry (constantDelay, exponentialBackoff, limitRetries, retrying) +import Control.Retry (constantDelay, exponentialBackoff, limitRetries, recoverAll, retrying) import Data.Aeson hiding (json) import Data.Aeson qualified as A import Data.Aeson.Lens (key, _String) @@ -429,7 +429,8 @@ addUserToTeamWithRole' role inviter tid = do let invite = InvitationRequest Nothing role Nothing inviteeEmail invResponse <- postInvitation tid inviter invite inv <- responseJsonError invResponse - inviteeCode <- getInvitationCode tid inv.invitationId + inviteeCode <- recoverAll (exponentialBackoff 1000 <> limitRetries 11) $ + \_ -> getInvitationCode tid inv.invitationId r <- post ( brig From f4c370b9dd3d88a9c84d62cffbb4f938266f5114 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 5 Nov 2024 16:59:07 +0100 Subject: [PATCH 21/27] Update email templates to v1.0.124. (#4328) --- .../5-internal/email-templates-v1.0.124 | 1 + services/brig/deb/opt/brig/template-version | 2 +- .../email/new-team-owner-welcome-subject.txt | 1 + .../de/team/email/new-team-owner-welcome.html | 1 + .../de/team/email/new-team-owner-welcome.txt | 27 +++++++++++++++++++ .../email/new-team-owner-welcome-subject.txt | 1 + .../en/team/email/new-team-owner-welcome.html | 1 + .../en/team/email/new-team-owner-welcome.txt | 26 ++++++++++++++++++ .../brig/deb/opt/brig/templates/index.html | 2 +- services/brig/deb/opt/brig/templates/version | 2 +- 10 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 changelog.d/5-internal/email-templates-v1.0.124 create mode 100644 services/brig/deb/opt/brig/templates/de/team/email/new-team-owner-welcome-subject.txt create mode 100644 services/brig/deb/opt/brig/templates/de/team/email/new-team-owner-welcome.html create mode 100644 services/brig/deb/opt/brig/templates/de/team/email/new-team-owner-welcome.txt create mode 100644 services/brig/deb/opt/brig/templates/en/team/email/new-team-owner-welcome-subject.txt create mode 100644 services/brig/deb/opt/brig/templates/en/team/email/new-team-owner-welcome.html create mode 100644 services/brig/deb/opt/brig/templates/en/team/email/new-team-owner-welcome.txt diff --git a/changelog.d/5-internal/email-templates-v1.0.124 b/changelog.d/5-internal/email-templates-v1.0.124 new file mode 100644 index 00000000000..b6aa9845853 --- /dev/null +++ b/changelog.d/5-internal/email-templates-v1.0.124 @@ -0,0 +1 @@ +Updated email templates to v1.0.124 diff --git a/services/brig/deb/opt/brig/template-version b/services/brig/deb/opt/brig/template-version index 5c41189b952..33fa1fdb55b 100644 --- a/services/brig/deb/opt/brig/template-version +++ b/services/brig/deb/opt/brig/template-version @@ -1 +1 @@ -v1.0.122 +v1.0.124 diff --git a/services/brig/deb/opt/brig/templates/de/team/email/new-team-owner-welcome-subject.txt b/services/brig/deb/opt/brig/templates/de/team/email/new-team-owner-welcome-subject.txt new file mode 100644 index 00000000000..7fcb109ea4c --- /dev/null +++ b/services/brig/deb/opt/brig/templates/de/team/email/new-team-owner-welcome-subject.txt @@ -0,0 +1 @@ +Sie sind einem Team auf ${brand} beigetreten \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/team/email/new-team-owner-welcome.html b/services/brig/deb/opt/brig/templates/de/team/email/new-team-owner-welcome.html new file mode 100644 index 00000000000..aa48804240b --- /dev/null +++ b/services/brig/deb/opt/brig/templates/de/team/email/new-team-owner-welcome.html @@ -0,0 +1 @@ +Sie sind einem Team auf ${brand} beigetreten

${brand_label_url}

Herzlichen Glückwunsch ${name}.

Wir haben Ihr privates Benutzerkonto ${email} in ein Team-Konto übertragen, einschließlich all Ihrer Unterhaltungen und Ihres Gesprächsverlaufs. Sie sind jetzt Besitzer des Teams (${team_name}).

 

Laden Sie andere Personen ein, Ihrem Team beizutreten, und passen Sie die Einstellungen im Team-Management an.

 
TEAM-MANAGEMENT ÖFFNEN
 

Wenn Sie die Schaltfläche nicht auswählen können, kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein:

${url}

Wenn Sie Fragen haben, dann kontaktieren Sie uns bitte.

Team ID: ${team_id}

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/de/team/email/new-team-owner-welcome.txt b/services/brig/deb/opt/brig/templates/de/team/email/new-team-owner-welcome.txt new file mode 100644 index 00000000000..b76a2fb2f1f --- /dev/null +++ b/services/brig/deb/opt/brig/templates/de/team/email/new-team-owner-welcome.txt @@ -0,0 +1,27 @@ +[${brand_logo}] + +${brand_label_url} [${brand_url}] + +HERZLICHEN GLÜCKWUNSCH ${name}. +Wir haben Ihr privates Benutzerkonto ${email} in ein Team-Konto übertragen, +einschließlich all Ihrer Unterhaltungen und Ihres Gesprächsverlaufs. Sie sind +jetzt Besitzer des Teams (${team_name}). + +Laden Sie andere Personen ein, Ihrem Team beizutreten, und passen Sie die +Einstellungen im Team-Management an. + +TEAM-MANAGEMENT ÖFFNEN [${url}]Wenn Sie die Schaltfläche nicht auswählen können, +kopieren Sie diesen Link und fügen Sie ihn in Ihren Browser ein: + +${url} + +Wenn Sie Fragen haben, dann kontaktieren Sie uns [${support}] bitte. + +Team ID: ${team_id} + + +-------------------------------------------------------------------------------- + +Datenschutzrichtlinien und Nutzungsbedingungen [${legal}] · Missbrauch melden +[${misuse}] +${copyright}. ALLE RECHTE VORBEHALTEN. \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/new-team-owner-welcome-subject.txt b/services/brig/deb/opt/brig/templates/en/team/email/new-team-owner-welcome-subject.txt new file mode 100644 index 00000000000..bc803c8137e --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/team/email/new-team-owner-welcome-subject.txt @@ -0,0 +1 @@ +You joined a team on ${brand} \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/new-team-owner-welcome.html b/services/brig/deb/opt/brig/templates/en/team/email/new-team-owner-welcome.html new file mode 100644 index 00000000000..bb0817f6288 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/team/email/new-team-owner-welcome.html @@ -0,0 +1 @@ +You joined a team on ${brand}

${brand_label_url}

Congratulations ${name}.

We transferred your personal account ${email} into a team account, including all your conversations and history. You are now the owner of the team (${team_name}).

 

Invite other people to join your team and adjust settings in your team management dashboard.

 
OPEN TEAM MANAGEMENT
 

If you can’t select the button, copy and paste this link to your browser:

${url}

If you have any questions, please contact us.

Team ID: ${team_id}

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/en/team/email/new-team-owner-welcome.txt b/services/brig/deb/opt/brig/templates/en/team/email/new-team-owner-welcome.txt new file mode 100644 index 00000000000..28ce003b96c --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/team/email/new-team-owner-welcome.txt @@ -0,0 +1,26 @@ +[${brand_logo}] + +${brand_label_url} [${brand_url}] + +CONGRATULATIONS ${name}. +We transferred your personal account ${email} into a team account, including all +your conversations and history. You are now the owner of the team (${team_name} +). + +Invite other people to join your team and adjust settings in your team +management dashboard. + +OPEN TEAM MANAGEMENT [${url}]If you can’t select the button, copy and paste this +link to your browser: + +${url} + +If you have any questions, please contact us [${support}]. + +Team ID: ${team_id} + + +-------------------------------------------------------------------------------- + +Privacy policy and terms of use [${legal}] · Report Misuse [${misuse}] +${copyright}. ALL RIGHTS RESERVED. \ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/index.html b/services/brig/deb/opt/brig/templates/index.html index 76b2de86f7b..dee7a019484 100644 --- a/services/brig/deb/opt/brig/templates/index.html +++ b/services/brig/deb/opt/brig/templates/index.html @@ -4,4 +4,4 @@ link.rel = 'stylesheet'; link.href = '//cdnjs.cloudflare.com/ajax/libs/flag-icon-css/2.9.0/css/flag-icon.min.css'; document.head.appendChild(link); - }
 

Wire Email Templates Preview

Click the links below to display the content of each message:

Provider
  1. Activationtxt
  2. Approval confirmtxt
  3. Approval requesttxt
Team
  1. Invitationtxt
  2. New member welcometxt
  3. Migration from private to team usertxt
User
  1. Activationtxt
  2. Deletiontxt
  3. New clienttxt
  4. Password resettxt
  5. Updatetxt
  6. Verificationtxt
  7. Team activationtxt
  8. Second factor verification for logintxt
  9. Second factor verification create SCIM tokentxt
  10. Second factor verification delete teamtxt
Billing
  1. Suspensiontxt

For source and instructions, see github.com/wireapp/wire-emails or visit the Crowdin project to help with translations.

                                                           
\ No newline at end of file + }
 

Wire Email Templates Preview

Click the links below to display the content of each message:

Provider
  1. Activationtxt
  2. Approval confirmtxt
  3. Approval requesttxt
Team
  1. Invitationtxt
  2. New member welcometxt
  3. New team owner welcometxt
  4. Migration from private to team usertxt
User
  1. Activationtxt
  2. Deletiontxt
  3. New clienttxt
  4. Password resettxt
  5. Updatetxt
  6. Verificationtxt
  7. Team activationtxt
  8. Second factor verification for logintxt
  9. Second factor verification create SCIM tokentxt
  10. Second factor verification delete teamtxt
Billing
  1. Suspensiontxt

For source and instructions, see github.com/wireapp/wire-emails or visit the Crowdin project to help with translations.

                                                           
\ No newline at end of file diff --git a/services/brig/deb/opt/brig/templates/version b/services/brig/deb/opt/brig/templates/version index 5c41189b952..33fa1fdb55b 100644 --- a/services/brig/deb/opt/brig/templates/version +++ b/services/brig/deb/opt/brig/templates/version @@ -1 +1 @@ -v1.0.122 +v1.0.124 From 351e0b97aea1525bdc2ff1bcc020a8e6ffbc845f Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 6 Nov 2024 15:53:11 +0100 Subject: [PATCH 22/27] WPB-10658 invitation and acceptance of individual users to teams fix nginz config (#4334) --- changelog.d/3-bug-fixes/WBP-10658 | 1 + charts/nginz/values.yaml | 6 ++++++ integration/test/API/Nginz.hs | 5 +++++ integration/test/Test/Teams.hs | 5 ++++- services/nginz/integration-test/conf/nginz/nginx.conf | 10 ++++++++++ 5 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 changelog.d/3-bug-fixes/WBP-10658 diff --git a/changelog.d/3-bug-fixes/WBP-10658 b/changelog.d/3-bug-fixes/WBP-10658 new file mode 100644 index 00000000000..34c24d43291 --- /dev/null +++ b/changelog.d/3-bug-fixes/WBP-10658 @@ -0,0 +1 @@ +Updated `nginz` config for personal user to team flow diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index b853e1b2cde..8560dab1ac4 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -468,6 +468,9 @@ nginx_conf: - path: /oauth/applications envs: - all + - path: /upgrade-personal-to-team$ + envs: + - all galley: - path: /conversations/code-check disable_zauth: true @@ -557,6 +560,9 @@ nginx_conf: disable_zauth: true basic_auth: true versioned: false + - path: /teams/invitations/accept$ + envs: + - all - path: /custom-backend/by-domain/([^/]*)$ disable_zauth: true envs: diff --git a/integration/test/API/Nginz.hs b/integration/test/API/Nginz.hs index ac248fd544f..cee0b05f4b6 100644 --- a/integration/test/API/Nginz.hs +++ b/integration/test/API/Nginz.hs @@ -90,3 +90,8 @@ buildUploadAssetRequestBody isPublic retention body mimeType = do "retention" .= mbRetention ] HTTP.RequestBodyLBS <$> buildMultipartBody header' body mimeType + +upgradePersonalToTeam :: (HasCallStack, MakesValue user) => user -> String -> String -> App Response +upgradePersonalToTeam user token name = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["upgrade-personal-to-team"] + submit "POST" $ req & addJSONObject ["name" .= name, "icon" .= "default"] & addHeader "Authorization" ("Bearer " <> token) diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs index 5ce10031fba..aa197ac5493 100644 --- a/integration/test/Test/Teams.hs +++ b/integration/test/Test/Teams.hs @@ -24,6 +24,7 @@ import API.Common import API.Galley (getTeam, getTeamMembers, getTeamMembersCsv, getTeamNotifications) import API.GalleyInternal (setTeamFeatureStatus) import API.Gundeck +import qualified API.Nginz as Nginz import Control.Monad.Codensity (Codensity (runCodensity)) import Control.Monad.Extra (findM) import Control.Monad.Reader (asks) @@ -256,8 +257,10 @@ testTeamUserCannotBeInvited = do testUpgradePersonalToTeam :: (HasCallStack) => App () testUpgradePersonalToTeam = do alice <- randomUser OwnDomain def + email <- alice %. "email" >>= asString let teamName = "wonderland" - tid <- bindResponse (upgradePersonalToTeam alice teamName) $ \resp -> do + token <- Nginz.login OwnDomain email defPassword >>= getJSON 200 >>= (%. "access_token") & asString + tid <- bindResponse (Nginz.upgradePersonalToTeam alice token teamName) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "team_name" `shouldMatch` teamName resp.json %. "team_id" diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index bfbc75ccc7c..f674540cd5d 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -339,6 +339,16 @@ http { proxy_pass http://brig; } + location ~* ^(/v[0-9]+)?/upgrade-personal-to-team$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/teams/invitation/accept$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + # Cargohold Endpoints location ~* ^(/v[0-9]+)?/assets { From a63c04449bdc08c131e3d7e260f86aeff793db71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 6 Nov 2024 19:22:38 +0100 Subject: [PATCH 23/27] [WPB-8881] Add unit tests for new effect actions (#4331) * Add a unit test for DeleteEmail * Add a unit test for UpdateEmailUnvalidated Co-authored-by: Sven Tennie * Pull out a mock interpreter function * Add a unit test for LookupActivationCode * Add a unit test for NewActivationCode * Add unit tests for RemoveEmailEither * Add unit tests for requestEmailChange * Update a change log --------- Co-authored-by: Sven Tennie --- changelog.d/5-internal/WPB-8881 | 2 +- libs/wire-api/src/Wire/API/User.hs | 1 + libs/wire-api/src/Wire/API/User/Identity.hs | 5 ++ .../src/Wire/ActivationCodeStore.hs | 4 +- .../ActivationCodeStore/InterpreterSpec.hs | 40 ++++++++++++ .../test/unit/Wire/MiniBackend.hs | 18 ++++++ .../MockInterpreters/ActivationCodeStore.hs | 20 +++--- .../unit/Wire/MockInterpreters/UserStore.hs | 5 +- .../test/unit/Wire/UserStoreSpec.hs | 22 +++++++ .../Wire/UserSubsystem/InterpreterSpec.hs | 64 +++++++++++++++++++ libs/wire-subsystems/wire-subsystems.cabal | 1 + 11 files changed, 170 insertions(+), 12 deletions(-) create mode 100644 libs/wire-subsystems/test/unit/Wire/ActivationCodeStore/InterpreterSpec.hs diff --git a/changelog.d/5-internal/WPB-8881 b/changelog.d/5-internal/WPB-8881 index f4bf49f3359..82b0f438e75 100644 --- a/changelog.d/5-internal/WPB-8881 +++ b/changelog.d/5-internal/WPB-8881 @@ -1 +1 @@ -Move email update and remove operations to effects +Move email update and remove operations to effects (#4316, ##) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index c6d2a6d4404..771cd4ee893 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -1580,6 +1580,7 @@ instance ToSchema NameUpdate where data ChangeEmailResponse = ChangeEmailResponseIdempotent | ChangeEmailResponseNeedsActivation + deriving (Eq, Show) instance AsUnion diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index 65b6a5ede61..841f6dc025d 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -22,6 +22,7 @@ module Wire.API.User.Identity ( -- * UserIdentity UserIdentity (..), + isUserSSOId, isSSOIdentity, newIdentity, emailIdentity, @@ -150,6 +151,10 @@ data UserSSOId deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserSSOId) +isUserSSOId :: UserSSOId -> Bool +isUserSSOId (UserSSOId _) = True +isUserSSOId (UserScimExternalId _) = False + instance C.Cql UserSSOId where ctype = C.Tagged C.TextColumn diff --git a/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs b/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs index 1d5175387fe..00cdcf0bad6 100644 --- a/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs +++ b/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs @@ -26,7 +26,9 @@ import Wire.API.User.Activation import Wire.UserKeyStore data ActivationCodeStore :: Effect where - LookupActivationCode :: EmailKey -> ActivationCodeStore m (Maybe (Maybe UserId, ActivationCode)) + LookupActivationCode :: + EmailKey -> + ActivationCodeStore m (Maybe (Maybe UserId, ActivationCode)) -- | Create a code for a new pending activation for a given 'EmailKey' NewActivationCode :: EmailKey -> diff --git a/libs/wire-subsystems/test/unit/Wire/ActivationCodeStore/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/ActivationCodeStore/InterpreterSpec.hs new file mode 100644 index 00000000000..c8790485fd8 --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/ActivationCodeStore/InterpreterSpec.hs @@ -0,0 +1,40 @@ +module Wire.ActivationCodeStore.InterpreterSpec (spec) where + +import Data.Default +import Data.Map qualified as Map +import Imports +import Test.Hspec +import Test.Hspec.QuickCheck +import Test.QuickCheck +import Wire.API.User.Activation +import Wire.ActivationCodeStore +import Wire.MiniBackend +import Wire.MockInterpreters.ActivationCodeStore + +spec :: Spec +spec = do + describe "ActivationCodeStore effect" $ do + prop "a code can be looked up" $ \emailKey config -> + let c = code emailKey + localBackend = + def {activationCodes = Map.singleton emailKey (Nothing, c)} + result = + runNoFederationStack localBackend Nothing config $ + lookupActivationCode emailKey + in result === Just (Nothing, c) + prop "a code not found in the store" $ \emailKey config -> + let localBackend = def + result = + runNoFederationStack localBackend Nothing config $ + lookupActivationCode emailKey + in result === Nothing + prop "newly added code can be looked up" $ \emailKey mUid config -> + let c = code emailKey + localBackend = def + (actCode, lookupRes) = + runNoFederationStack localBackend Nothing config $ do + ac <- + (.activationCode) + <$> newActivationCode emailKey undefined mUid + (ac,) <$> lookupActivationCode emailKey + in actCode === c .&&. lookupRes === Just (mUid, c) diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index ee951963bbc..2dbf1a3a9bc 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -21,6 +21,7 @@ module Wire.MiniBackend NotPendingStoredUser (..), NotPendingEmptyIdentityStoredUser (..), PendingNotEmptyIdentityStoredUser (..), + NotPendingSSOIdWithEmailStoredUser (..), PendingStoredUser (..), ) where @@ -126,6 +127,23 @@ instance Arbitrary NotPendingStoredUser where notPendingStatus <- elements (Nothing : map Just [Active, Suspended, Ephemeral]) pure $ NotPendingStoredUser (user {status = notPendingStatus}) +newtype NotPendingSSOIdWithEmailStoredUser = NotPendingSSOIdWithEmailStoredUser StoredUser + deriving (Show, Eq) + +instance Arbitrary NotPendingSSOIdWithEmailStoredUser where + arbitrary = do + user <- arbitrary `suchThat` \user -> fmap isUserSSOId user.ssoId == Just True + notPendingStatus <- elements (Nothing : map Just [Active, Suspended, Ephemeral]) + e <- arbitrary + pure $ + NotPendingSSOIdWithEmailStoredUser + ( user + { activated = True, + status = notPendingStatus, + email = Just e + } + ) + type AllErrors = [ Error UserSubsystemError, Error FederationError, diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs index a31d31cbfc3..c7b85c9b81b 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs @@ -13,6 +13,15 @@ import Wire.API.User.Activation import Wire.ActivationCodeStore (ActivationCodeStore (..)) import Wire.UserKeyStore +code :: EmailKey -> ActivationCode +code = + ActivationCode + . Ascii.unsafeFromText + . pack + . printf "%06d" + . length + . show + inMemoryActivationCodeStoreInterpreter :: ( Member (State (Map EmailKey (Maybe UserId, ActivationCode))) r ) => @@ -26,12 +35,5 @@ inMemoryActivationCodeStoreInterpreter = interpret \case . T.encodeUtf8 . emailKeyUniq $ ek - code = - ActivationCode - . Ascii.unsafeFromText - . pack - . printf "%06d" - . length - . show - $ ek - modify (insert ek (uid, code)) $> Activation key code + c = code ek + modify (insert ek (uid, c)) $> Activation key c diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index d3f00a25016..c782d073e2e 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -81,7 +81,10 @@ inMemoryUserStoreInterpreter = interpret $ \case GetActivityTimestamps _ -> pure [] GetRichInfo _ -> error "rich info not implemented" GetUserAuthenticationInfo _uid -> error "Not implemented" - DeleteEmail _uid -> error "Not implemented" + DeleteEmail uid -> modify (map doUpdate) + where + doUpdate :: StoredUser -> StoredUser + doUpdate u = if u.id == uid then u {email = Nothing} else u storedUserToIndexUser :: StoredUser -> IndexUser storedUserToIndexUser storedUser = diff --git a/libs/wire-subsystems/test/unit/Wire/UserStoreSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserStoreSpec.hs index 5f9192c469b..6d3df61cecc 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserStoreSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserStoreSpec.hs @@ -1,11 +1,15 @@ module Wire.UserStoreSpec (spec) where +import Data.Default import Imports +import Polysemy.State import Test.Hspec import Test.Hspec.QuickCheck import Test.QuickCheck import Wire.API.User +import Wire.MiniBackend import Wire.StoredUser +import Wire.UserStore spec :: Spec spec = do @@ -33,3 +37,21 @@ spec = do in if (isJust storedUser.language) then user.userLocale === Locale (fromJust storedUser.language) storedUser.country else user.userLocale === defaultLocale + + describe "UserStore effect" $ do + prop "user self email deleted" $ \user1 user2' email2 config -> + let user2 = user2' {email = Just email2} + localBackend = def {users = [user1, user2]} + result = + runNoFederationStack localBackend Nothing config $ do + deleteEmail (user1.id) + gets users + in result === [user1 {email = Nothing}, user2] + prop "update unvalidated email" $ \user1 user2 email1 config -> + let updatedUser1 = user1 {emailUnvalidated = Just email1} + localBackend = def {users = [user1, user2]} + result = + runNoFederationStack localBackend Nothing config $ do + updateEmailUnvalidated (user1.id) email1 + gets users + in result === [updatedUser1, user2] diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 3b82816399e..ed361ba70ed 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -814,3 +814,67 @@ spec = describe "UserSubsystem.Interpreter" do . interpretNoFederationStack localBackend Nothing def config $ getLocalUserAccountByUserKey (toLocalUnsafe localDomain userKey) in retrievedUser === Nothing + describe "Removing an email address" do + prop "Cannot remove an email of a non-existing user" $ \lusr config -> + let localBackend = def + result = + runNoFederationStack localBackend Nothing config $ + removeEmailEither lusr + in result === Left UserSubsystemProfileNotFound + prop "Cannot remove an email of a no-identity user" $ + \(locx :: Local ()) (NotPendingEmptyIdentityStoredUser user) config -> + let localBackend = def {users = [user]} + lusr = qualifyAs locx user.id + result = + runNoFederationStack localBackend Nothing config $ + removeEmailEither lusr + in result === Left UserSubsystemNoIdentity + prop "Cannot remove an email of a last-identity user" $ + \(locx :: Local ()) user' email sso config -> + let user = + user' + { activated = True, + email = email, + ssoId = if isNothing email then Just sso else Nothing + } + localBackend = def {users = [user]} + lusr = qualifyAs locx user.id + result = + runNoFederationStack localBackend Nothing config $ + removeEmailEither lusr + in result === Left UserSubsystemLastIdentity + prop "Successfully remove an email from an SSOId user" $ + \(locx :: Local ()) (NotPendingSSOIdWithEmailStoredUser user) config -> + let localBackend = def {users = [user]} + lusr = qualifyAs locx user.id + result = + runNoFederationStack localBackend Nothing config $ do + remRes <- removeEmailEither lusr + (remRes,) <$> gets users + in result === (Right (), [user {email = Nothing}]) + describe "Changing an email address" $ do + prop "Idempotent email change" $ + \(locx :: Local ()) (NotPendingStoredUser user') email config -> + let user = user' {email = Just email} + localBackend = def {users = [user]} + lusr = qualifyAs locx user.id + result = + runNoFederationStack localBackend Nothing config $ do + c <- requestEmailChange lusr email UpdateOriginWireClient + (c,) <$> gets users + in result === (ChangeEmailResponseIdempotent, [user]) + prop "Email change needing activation" $ + \(locx :: Local ()) (NotPendingStoredUser user') config -> + let email = unsafeEmailAddress "me" "example.com" + updatedEmail = unsafeEmailAddress "you" "example.com" + user = user' {email = Just email, managedBy = Nothing} + localBackend = def {users = [user]} + lusr = qualifyAs locx user.id + result = + runNoFederationStack localBackend Nothing config $ do + c <- requestEmailChange lusr updatedEmail UpdateOriginWireClient + (c,) <$> gets users + in result + === ( ChangeEmailResponseNeedsActivation, + [user {emailUnvalidated = Just updatedEmail}] + ) diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 2e591f788db..26bf0e5d136 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -232,6 +232,7 @@ test-suite wire-subsystems-tests -- cabal-fmt: expand test/unit other-modules: Spec + Wire.ActivationCodeStore.InterpreterSpec Wire.AuthenticationSubsystem.InterpreterSpec Wire.MiniBackend Wire.MockInterpreters From 4877997838b4bc62654d053c8175aa5c8b93fc63 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 7 Nov 2024 10:06:22 +0100 Subject: [PATCH 24/27] WPB-11183 confirmation email after team creation and account migration of the team owner (#4333) --- changelog.d/2-features/WPB-11183 | 1 + .../src/Wire/EmailSubsystem/Template.hs | 16 ++++---- services/brig/src/Brig/API/Public.hs | 3 +- services/brig/src/Brig/API/User.hs | 6 ++- services/brig/src/Brig/Team/Email.hs | 40 ++++++++++++++----- services/brig/src/Brig/Team/Template.hs | 13 +++--- 6 files changed, 52 insertions(+), 27 deletions(-) create mode 100644 changelog.d/2-features/WPB-11183 diff --git a/changelog.d/2-features/WPB-11183 b/changelog.d/2-features/WPB-11183 new file mode 100644 index 00000000000..89b69dd426d --- /dev/null +++ b/changelog.d/2-features/WPB-11183 @@ -0,0 +1 @@ +Welcome email for new team owner. diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs index ca79185fccc..7c209b2972a 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs @@ -219,13 +219,13 @@ data PersonalUserMemberWelcomeEmailTemplate = PersonalUserMemberWelcomeEmailTemp personalUserMemberWelcomeEmailSenderName :: !Text } -data PersonalUserCreatorWelcomeEmailTemplate = PersonalUserCreatorWelcomeEmailTemplate - { personalUserCreatorWelcomeEmailUrl :: !Text, - personalUserCreatorWelcomeEmailSubject :: !Template, - personalUserCreatorWelcomeEmailBodyText :: !Template, - personalUserCreatorWelcomeEmailBodyHtml :: !Template, - personalUserCreatorWelcomeEmailSender :: !EmailAddress, - personalUserCreatorWelcomeEmailSenderName :: !Text +data NewTeamOwnerWelcomeEmailTemplate = NewTeamOwnerWelcomeEmailTemplate + { newTeamOwnerWelcomeEmailUrl :: !Text, + newTeamOwnerWelcomeEmailSubject :: !Template, + newTeamOwnerWelcomeEmailBodyText :: !Template, + newTeamOwnerWelcomeEmailBodyHtml :: !Template, + newTeamOwnerWelcomeEmailSender :: !EmailAddress, + newTeamOwnerWelcomeEmailSenderName :: !Text } data TeamTemplates = TeamTemplates @@ -234,5 +234,5 @@ data TeamTemplates = TeamTemplates creatorWelcomeEmail :: !CreatorWelcomeEmailTemplate, memberWelcomeEmail :: !MemberWelcomeEmailTemplate, personalUserMemberWelcomeEmail :: !PersonalUserMemberWelcomeEmailTemplate, - personalUserCreatorWelcomeEmail :: !PersonalUserCreatorWelcomeEmailTemplate + newTeamOwnerWelcomeEmail :: !NewTeamOwnerWelcomeEmailTemplate } diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 8b3b484e236..ff22f86f8d3 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -790,7 +790,8 @@ upgradePersonalToTeam :: Member NotificationSubsystem r, Member TinyLog r, Member UserSubsystem r, - Member UserStore r + Member UserStore r, + Member EmailSending r ) => Local UserId -> Public.BindingNewTeamUser -> diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 8c6a2e6fb67..223de6c1fac 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -130,6 +130,7 @@ import Wire.ActivationCodeStore qualified as ActivationCode import Wire.AuthenticationSubsystem (AuthenticationSubsystem, internalLookupPasswordResetCode) import Wire.BlockListStore as BlockListStore import Wire.DeleteQueue +import Wire.EmailSending import Wire.EmailSubsystem import Wire.Error import Wire.Events (Events) @@ -263,7 +264,8 @@ upgradePersonalToTeam :: Member NotificationSubsystem r, Member (Input (Local ())) r, Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member (ConnectionStore InternalPaging) r, + Member EmailSending r ) => Local UserId -> BindingNewTeamUser -> @@ -295,7 +297,7 @@ upgradePersonalToTeam luid bNewTeam = do -- send confirmation email for_ (userEmail user) $ \email -> do - sendPersonalUserCreatorWelcomeMail + sendNewTeamOwnerWelcomeEmail email tid bNewTeam.bnuTeam.newTeamName.fromRange diff --git a/services/brig/src/Brig/Team/Email.hs b/services/brig/src/Brig/Team/Email.hs index 441cee5d7bf..e9c22b1f82a 100644 --- a/services/brig/src/Brig/Team/Email.hs +++ b/services/brig/src/Brig/Team/Email.hs @@ -19,8 +19,7 @@ module Brig.Team.Email ( sendMemberWelcomeMail, - sendPersonalUserMemberWelcomeMail, - sendPersonalUserCreatorWelcomeMail, + sendNewTeamOwnerWelcomeEmail, ) where @@ -41,13 +40,11 @@ sendMemberWelcomeMail to tid teamName loc = do branding <- asks (.templateBranding) liftSem $ sendMail $ renderMemberWelcomeMail to tid teamName tpl branding -sendPersonalUserMemberWelcomeMail :: EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () -sendPersonalUserMemberWelcomeMail _ _ _ _ = do - pure () - -sendPersonalUserCreatorWelcomeMail :: EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () -sendPersonalUserCreatorWelcomeMail _ _ _ _ = do - pure () +sendNewTeamOwnerWelcomeEmail :: (Member EmailSending r) => EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () +sendNewTeamOwnerWelcomeEmail to tid teamName loc = do + tpl <- newTeamOwnerWelcomeEmail . snd <$> teamTemplatesWithLocale loc + branding <- asks (.templateBranding) + liftSem $ sendMail $ renderNewTeamOwnerWelcomeEmail to tid teamName tpl branding ------------------------------------------------------------------------------- -- Member Welcome Email @@ -73,3 +70,28 @@ renderMemberWelcomeMail emailTo tid teamName MemberWelcomeEmailTemplate {..} bra replace "team_id" = idToText tid replace "team_name" = teamName replace x = x + +------------------------------------------------------------------------------- +-- New Team Owner Welcome Email + +renderNewTeamOwnerWelcomeEmail :: EmailAddress -> TeamId -> Text -> NewTeamOwnerWelcomeEmailTemplate -> TemplateBranding -> Mail +renderNewTeamOwnerWelcomeEmail emailTo tid teamName NewTeamOwnerWelcomeEmailTemplate {..} branding = + (emptyMail from) + { mailTo = [to], + mailHeaders = + [ ("Subject", toStrict subj), + ("X-Zeta-Purpose", "Welcome") + ], + mailParts = [[plainPart txt, htmlPart html]] + } + where + from = Address (Just newTeamOwnerWelcomeEmailSenderName) (fromEmail newTeamOwnerWelcomeEmailSender) + to = Address Nothing (fromEmail emailTo) + txt = renderTextWithBranding newTeamOwnerWelcomeEmailBodyText replace branding + html = renderHtmlWithBranding newTeamOwnerWelcomeEmailBodyHtml replace branding + subj = renderTextWithBranding newTeamOwnerWelcomeEmailSubject replace branding + replace "url" = newTeamOwnerWelcomeEmailUrl + replace "email" = fromEmail emailTo + replace "team_id" = idToText tid + replace "team_name" = teamName + replace x = x diff --git a/services/brig/src/Brig/Team/Template.hs b/services/brig/src/Brig/Team/Template.hs index 86c409e9f62..713c1555a6f 100644 --- a/services/brig/src/Brig/Team/Template.hs +++ b/services/brig/src/Brig/Team/Template.hs @@ -71,13 +71,12 @@ loadTeamTemplates o = readLocalesDir defLocale (templateDir gOptions) "team" $ \ (emailSender gOptions) <$> readText fp "email/sender.txt" ) - <*> ( PersonalUserCreatorWelcomeEmailTemplate - "" - (template "") - (template "") - (template "") - (emailSender gOptions) - <$> readText fp "email/sender.txt" + <*> ( NewTeamOwnerWelcomeEmailTemplate (tCreatorWelcomeUrl tOptions) + <$> readTemplate fp "email/new-team-owner-welcome-subject.txt" + <*> readTemplate fp "email/new-team-owner-welcome.txt" + <*> readTemplate fp "email/new-team-owner-welcome.html" + <*> pure (emailSender gOptions) + <*> readText fp "email/sender.txt" ) where gOptions = o.emailSMS.general From 30dbb0759b1a2a408b99bcffce41176097d62922 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Fri, 8 Nov 2024 10:57:30 +0100 Subject: [PATCH 25/27] integration: Allow MLS State to track multiple conversations (#4329) --- Makefile | 2 +- integration/default.nix | 4 + integration/integration.cabal | 2 + integration/test/API/Galley.hs | 43 +- integration/test/MLS/Util.hs | 454 ++++++++------- integration/test/Notifications.hs | 6 + integration/test/SetupHelpers.hs | 23 +- integration/test/Test/AccessUpdate.hs | 15 +- integration/test/Test/ExternalPartner.hs | 2 +- .../test/Test/FeatureFlags/LegalHold.hs | 3 +- integration/test/Test/LegalHold.hs | 27 +- integration/test/Test/MLS.hs | 526 +++++++++--------- integration/test/Test/MLS/KeyPackage.hs | 74 ++- integration/test/Test/MLS/Message.hs | 48 +- integration/test/Test/MLS/Notifications.hs | 8 +- integration/test/Test/MLS/One2One.hs | 104 ++-- integration/test/Test/MLS/SubConversation.hs | 307 +++++----- integration/test/Test/MLS/Unreachable.hs | 42 +- integration/test/Testlib/App.hs | 4 +- integration/test/Testlib/Assertions.hs | 9 + integration/test/Testlib/Env.hs | 20 +- integration/test/Testlib/JSON.hs | 20 +- integration/test/Testlib/ModService.hs | 48 +- integration/test/Testlib/Run.hs | 2 + integration/test/Testlib/Types.hs | 37 +- 25 files changed, 989 insertions(+), 841 deletions(-) diff --git a/Makefile b/Makefile index ea55403761f..1cd6be92e1e 100644 --- a/Makefile +++ b/Makefile @@ -134,7 +134,7 @@ crm: c db-migrate # Usage: TEST_INCLUDE=test1,test2 make devtest .PHONY: devtest devtest: - ghcid --command 'cabal repl integration' --test='Testlib.Run.mainI []' + ghcid --command 'cabal repl lib:integration' --test='Testlib.Run.mainI []' .PHONY: sanitize-pr sanitize-pr: diff --git a/integration/default.nix b/integration/default.nix index 37d66c8daf5..a52a1d34169 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -22,6 +22,8 @@ , cookie , cql , cql-io +, criterion +, cryptobox-haskell , crypton , crypton-x509 , cryptostore @@ -119,6 +121,8 @@ mkDerivation { cookie cql cql-io + criterion + cryptobox-haskell crypton crypton-x509 cryptostore diff --git a/integration/integration.cabal b/integration/integration.cabal index a3989f28e76..fa293712177 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -217,6 +217,8 @@ library , cookie , cql , cql-io + , criterion + , cryptobox-haskell , crypton , crypton-x509 , cryptostore diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index d1c4066ae70..71b148bab7d 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -120,17 +120,15 @@ deleteTeamMember tid owner mem = do putConversationProtocol :: ( HasCallStack, MakesValue user, - MakesValue qcnv, MakesValue protocol ) => user -> - qcnv -> + ConvId -> protocol -> App Response -putConversationProtocol user qcnv protocol = do - (domain, cnv) <- objQid qcnv +putConversationProtocol user convId protocol = do p <- asString protocol - req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", domain, cnv, "protocol"]) + req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", convId.domain, convId.id_, "protocol"]) submit "PUT" (req & addJSONObject ["protocol" .= p]) getConversation :: @@ -148,21 +146,19 @@ getConversation user qcnv = do getSubConversation :: ( HasCallStack, - MakesValue user, - MakesValue conv + MakesValue user ) => user -> - conv -> + ConvId -> String -> App Response getSubConversation user conv sub = do - (cnvDomain, cnvId) <- objQid conv req <- baseRequest user Galley Versioned $ joinHttpPath [ "conversations", - cnvDomain, - cnvId, + conv.domain, + conv.id_, "subconversations", sub ] @@ -184,16 +180,15 @@ deleteSubConversation user sub = do submit "DELETE" $ req & addJSONObject ["group_id" .= groupId, "epoch" .= epoch] leaveSubConversation :: - (HasCallStack, MakesValue user, MakesValue sub) => + (HasCallStack, MakesValue user) => user -> - sub -> + ConvId -> App Response -leaveSubConversation user sub = do - (conv, Just subId) <- objSubConv sub - (domain, convId) <- objQid conv +leaveSubConversation user convId = do + let Just subId = convId.subconvId req <- baseRequest user Galley Versioned - $ joinHttpPath ["conversations", domain, convId, "subconversations", subId, "self"] + $ joinHttpPath ["conversations", convId.domain, convId.id_, "subconversations", subId, "self"] submit "DELETE" req getSelfConversation :: (HasCallStack, MakesValue user) => user -> App Response @@ -278,16 +273,14 @@ mkProteusRecipients dom userClients msg = do & #text .~ fromString msg getGroupInfo :: - (HasCallStack, MakesValue user, MakesValue conv) => + (HasCallStack, MakesValue user) => user -> - conv -> + ConvId -> App Response getGroupInfo user conv = do - (qcnv, mSub) <- objSubConv conv - (convDomain, convId) <- objQid qcnv - let path = joinHttpPath $ case mSub of - Nothing -> ["conversations", convDomain, convId, "groupinfo"] - Just sub -> ["conversations", convDomain, convId, "subconversations", sub, "groupinfo"] + let path = joinHttpPath $ case conv.subconvId of + Nothing -> ["conversations", conv.domain, conv.id_, "groupinfo"] + Just sub -> ["conversations", conv.domain, conv.id_, "subconversations", sub, "groupinfo"] req <- baseRequest user Galley Versioned path submit "GET" req @@ -323,7 +316,7 @@ deleteTeamConv :: App Response deleteTeamConv team conv user = do teamId <- objId team - convId <- objId conv + convId <- objId $ objQidObject conv req <- baseRequest user Galley Versioned (joinHttpPath ["teams", teamId, "conversations", convId]) submit "DELETE" req diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index f5e753cf88b..ae3d748f04d 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -3,6 +3,7 @@ module MLS.Util where import API.Brig +import API.BrigCommon import API.Galley import Control.Concurrent.Async hiding (link) import Control.Monad @@ -11,7 +12,6 @@ import Control.Monad.Codensity import Control.Monad.Cont import Control.Monad.Reader import Control.Monad.Trans.Maybe -import qualified Data.Aeson as A import qualified Data.Aeson as Aeson import qualified Data.ByteString as BS import qualified Data.ByteString.Base64 as Base64 @@ -33,6 +33,7 @@ import System.Directory import System.Exit import System.FilePath import System.IO hiding (print, putStrLn) +import System.IO.Error (isAlreadyExistsError) import System.IO.Temp import System.Posix.Files import System.Process @@ -40,6 +41,7 @@ import Testlib.Assertions import Testlib.HTTP import Testlib.JSON import Testlib.Prelude +import Testlib.Printing mkClientIdentity :: (MakesValue u, MakesValue c) => u -> c -> App ClientIdentity mkClientIdentity u c = do @@ -52,18 +54,12 @@ cid2Str cid = cid.user <> ":" <> cid.client <> "@" <> cid.domain data MessagePackage = MessagePackage { sender :: ClientIdentity, + convId :: ConvId, message :: ByteString, welcome :: Maybe ByteString, groupInfo :: Maybe ByteString } -getConv :: App Value -getConv = do - mls <- getMLSState - case mls.convId of - Nothing -> assertFailure "Uninitialised test conversation" - Just convId -> pure convId - toRandomFile :: ByteString -> App FilePath toRandomFile bs = do p <- randomFileName @@ -75,16 +71,15 @@ randomFileName = do bd <- getBaseDir (bd ) . UUID.toString <$> liftIO UUIDV4.nextRandom -mlscli :: (HasCallStack) => ClientIdentity -> [String] -> Maybe ByteString -> App ByteString -mlscli cid args mbstdin = do +mlscli :: (HasCallStack) => Maybe ConvId -> Ciphersuite -> ClientIdentity -> [String] -> Maybe ByteString -> App ByteString +mlscli mConvId cs cid args mbstdin = do groupOut <- randomFileName let substOut = argSubst "" groupOut - cs <- (.ciphersuite) <$> getMLSState let scheme = csSignatureScheme cs gs <- getClientGroupState cid - substIn <- case gs.group of + substIn <- case flip Map.lookup gs.groups =<< mConvId of Nothing -> pure id Just groupData -> do fn <- toRandomFile groupData @@ -92,7 +87,11 @@ mlscli cid args mbstdin = do store <- case Map.lookup scheme gs.keystore of Nothing -> do bd <- getBaseDir - liftIO $ createDirectory (bd cid2Str cid) + liftIO (createDirectory (bd cid2Str cid)) + `catch` \e -> + if (isAlreadyExistsError e) + then assertFailure "client directory for mls state already exists" + else throwM e -- initialise new keystore path <- randomFileName @@ -109,11 +108,15 @@ mlscli cid args mbstdin = do out <- runCli store args' mbstdin setGroup <- do groupOutWritten <- liftIO $ doesFileExist groupOut - if groupOutWritten - then do + case (groupOutWritten, mConvId) of + (True, Just convId) -> do groupData <- liftIO (BS.readFile groupOut) - pure $ \x -> x {group = Just groupData} - else pure id + pure $ \x -> x {groups = Map.insert convId groupData x.groups} + (True, Nothing) -> do + print $ colored red "mls-test-cli: Group was written but no convId was provided, this probably indicates something is going to go wrong in this test." + print =<< liftIO (prettierCallStack callStack) + pure id + _ -> pure id setStore <- do storeData <- liftIO (BS.readFile store) pure $ \x -> x {keystore = Map.insert scheme storeData x.keystore} @@ -137,27 +140,28 @@ argSubst :: String -> String -> String -> String argSubst from to_ s = if s == from then to_ else s -createWireClient :: (MakesValue u, HasCallStack) => u -> App ClientIdentity -createWireClient u = do - addClient u def +createWireClient :: (MakesValue u, HasCallStack) => u -> AddClient -> App ClientIdentity +createWireClient u clientArgs = do + addClient u clientArgs >>= getJSON 201 >>= mkClientIdentity u data InitMLSClient = InitMLSClient - {credType :: CredentialType} + { credType :: CredentialType, + clientArgs :: AddClient + } instance Default InitMLSClient where - def = InitMLSClient {credType = BasicCredentialType} + def = InitMLSClient {credType = BasicCredentialType, clientArgs = def} -- | Create new mls client and register with backend. -createMLSClient :: (MakesValue u, HasCallStack) => InitMLSClient -> u -> App ClientIdentity -createMLSClient opts u = do - cid <- createWireClient u +createMLSClient :: (MakesValue u, HasCallStack) => Ciphersuite -> InitMLSClient -> u -> App ClientIdentity +createMLSClient ciphersuite opts u = do + cid <- createWireClient u opts.clientArgs setClientGroupState cid def {credType = opts.credType} -- set public key - pkey <- mlscli cid ["public-key"] Nothing - ciphersuite <- (.ciphersuite) <$> getMLSState + pkey <- mlscli Nothing ciphersuite cid ["public-key"] Nothing bindResponse ( updateClient cid @@ -170,9 +174,9 @@ createMLSClient opts u = do pure cid -- | create and upload to backend -uploadNewKeyPackage :: (HasCallStack) => ClientIdentity -> App String -uploadNewKeyPackage cid = do - (kp, ref) <- generateKeyPackage cid +uploadNewKeyPackage :: (HasCallStack) => Ciphersuite -> ClientIdentity -> App String +uploadNewKeyPackage suite cid = do + (kp, ref) <- generateKeyPackage cid suite -- upload key package bindResponse (uploadKeyPackages cid [kp]) $ \resp -> @@ -180,94 +184,100 @@ uploadNewKeyPackage cid = do pure ref -generateKeyPackage :: (HasCallStack) => ClientIdentity -> App (ByteString, String) -generateKeyPackage cid = do - suite <- (.ciphersuite) <$> getMLSState - kp <- mlscli cid ["key-package", "create", "--ciphersuite", suite.code] Nothing - ref <- B8.unpack . Base64.encode <$> mlscli cid ["key-package", "ref", "-"] (Just kp) +generateKeyPackage :: (HasCallStack) => ClientIdentity -> Ciphersuite -> App (ByteString, String) +generateKeyPackage cid suite = do + kp <- mlscli Nothing suite cid ["key-package", "create", "--ciphersuite", suite.code] Nothing + ref <- B8.unpack . Base64.encode <$> mlscli Nothing suite cid ["key-package", "ref", "-"] (Just kp) fp <- keyPackageFile cid ref liftIO $ BS.writeFile fp kp pure (kp, ref) -- | Create conversation and corresponding group. -createNewGroup :: (HasCallStack) => ClientIdentity -> App (String, Value) -createNewGroup cid = do +createNewGroup :: (HasCallStack) => Ciphersuite -> ClientIdentity -> App ConvId +createNewGroup cs cid = do conv <- postConversation cid defMLS >>= getJSON 201 - groupId <- conv %. "group_id" & asString - convId <- conv %. "qualified_id" - createGroup cid conv - pure (groupId, convId) + convId <- objConvId conv + createGroup cs cid convId + pure convId -- | Retrieve self conversation and create the corresponding group. -createSelfGroup :: (HasCallStack) => ClientIdentity -> App (String, Value) -createSelfGroup cid = do +createSelfGroup :: (HasCallStack) => Ciphersuite -> ClientIdentity -> App (String, Value) +createSelfGroup cs cid = do conv <- getSelfConversation cid >>= getJSON 200 + convId <- objConvId conv groupId <- conv %. "group_id" & asString - createGroup cid conv + createGroup cs cid convId pure (groupId, conv) -createGroup :: (MakesValue conv) => ClientIdentity -> conv -> App () -createGroup cid conv = do - mls <- getMLSState - case mls.groupId of - Just _ -> assertFailure "only one group can be created" - Nothing -> pure () - resetGroup cid conv - -createSubConv :: (HasCallStack) => ClientIdentity -> String -> App () -createSubConv cid subId = do - mls <- getMLSState - sub <- getSubConversation cid mls.convId subId >>= getJSON 200 - resetGroup cid sub - void $ createPendingProposalCommit cid >>= sendAndConsumeCommitBundle - -createOne2OneSubConv :: (HasCallStack, MakesValue keys) => ClientIdentity -> String -> keys -> App () -createOne2OneSubConv cid subId keys = do - mls <- getMLSState - sub <- getSubConversation cid mls.convId subId >>= getJSON 200 - resetOne2OneGroupGeneric cid sub keys - void $ createPendingProposalCommit cid >>= sendAndConsumeCommitBundle - -resetGroup :: (HasCallStack, MakesValue conv) => ClientIdentity -> conv -> App () -resetGroup cid conv = do - convId <- objSubConvObject conv - groupId <- conv %. "group_id" & asString +createGroup :: Ciphersuite -> ClientIdentity -> ConvId -> App () +createGroup cs cid convId = do + let Just groupId = convId.groupId modifyMLSState $ \s -> - s - { groupId = Just groupId, - convId = Just convId, - members = Set.singleton cid, - epoch = 0, - newMembers = mempty - } + let mlsConv = + MLSConv + { members = Set.singleton cid, + newMembers = mempty, + groupId, + convId = convId, + epoch = 0, + ciphersuite = cs + } + in s {convs = Map.insert convId mlsConv s.convs} keys <- getMLSPublicKeys cid.qualifiedUserId >>= getJSON 200 - resetClientGroup cid groupId keys - -resetOne2OneGroup :: (HasCallStack, MakesValue one2OneConv) => ClientIdentity -> one2OneConv -> App () -resetOne2OneGroup cid one2OneConv = - resetOne2OneGroupGeneric cid (one2OneConv %. "conversation") (one2OneConv %. "public_keys") + resetClientGroup cs cid groupId convId keys + +createSubConv :: (HasCallStack) => Ciphersuite -> ConvId -> ClientIdentity -> String -> App () +createSubConv cs convId cid subId = do + sub <- getSubConversation cid convId subId >>= getJSON 200 + subConvId <- objConvId sub + createGroup cs cid subConvId + void $ createPendingProposalCommit subConvId cid >>= sendAndConsumeCommitBundle + +createOne2OneSubConv :: (HasCallStack, MakesValue keys) => Ciphersuite -> ConvId -> ClientIdentity -> String -> keys -> App () +createOne2OneSubConv cs convId cid subId keys = do + sub <- getSubConversation cid convId subId >>= getJSON 200 + subConvId <- objConvId sub + resetOne2OneGroupGeneric cs cid sub keys + void $ createPendingProposalCommit subConvId cid >>= sendAndConsumeCommitBundle + +resetOne2OneGroup :: (HasCallStack, MakesValue one2OneConv) => Ciphersuite -> ClientIdentity -> one2OneConv -> App () +resetOne2OneGroup cs cid one2OneConv = + resetOne2OneGroupGeneric cs cid (one2OneConv %. "conversation") (one2OneConv %. "public_keys") -- | Useful when keys are to be taken from main conv and the conv here is the subconv -resetOne2OneGroupGeneric :: (HasCallStack, MakesValue conv, MakesValue keys) => ClientIdentity -> conv -> keys -> App () -resetOne2OneGroupGeneric cid conv keys = do - convId <- objSubConvObject conv +resetOne2OneGroupGeneric :: (HasCallStack, MakesValue conv, MakesValue keys) => Ciphersuite -> ClientIdentity -> conv -> keys -> App () +resetOne2OneGroupGeneric cs cid conv keys = do + convId <- objConvId conv groupId <- conv %. "group_id" & asString modifyMLSState $ \s -> - s - { groupId = Just groupId, - convId = Just convId, - members = Set.singleton cid, - epoch = 0, - newMembers = mempty - } - resetClientGroup cid groupId keys - -resetClientGroup :: (HasCallStack, MakesValue keys) => ClientIdentity -> String -> keys -> App () -resetClientGroup cid gid keys = do - mls <- getMLSState - removalKey <- asByteString $ keys %. ("removal." <> csSignatureScheme mls.ciphersuite) + let newMLSConv = + MLSConv + { members = Set.singleton cid, + newMembers = mempty, + groupId = groupId, + convId = convId, + epoch = 0, + ciphersuite = cs + } + resetConv old new = + old + { groupId = new.groupId, + convId = new.convId, + members = new.members, + newMembers = new.newMembers, + epoch = new.epoch + } + in s {convs = Map.insertWith resetConv convId newMLSConv s.convs} + + resetClientGroup cs cid groupId convId keys + +resetClientGroup :: (HasCallStack, MakesValue keys) => Ciphersuite -> ClientIdentity -> String -> ConvId -> keys -> App () +resetClientGroup cs cid gid convId keys = do + removalKey <- asByteString $ keys %. ("removal." <> csSignatureScheme cs) void $ mlscli + (Just convId) + cs cid [ "group", "create", @@ -276,7 +286,7 @@ resetClientGroup cid gid keys = do "--group-out", "", "--ciphersuite", - mls.ciphersuite.code, + cs.code, gid ] (Just removalKey) @@ -310,13 +320,13 @@ unbundleKeyPackages bundle = do -- Note that this alters the state of the group immediately. If we want to test -- a scenario where the commit is rejected by the backend, we can restore the -- group to the previous state by using an older version of the group file. -createAddCommit :: (HasCallStack) => ClientIdentity -> [Value] -> App MessagePackage -createAddCommit cid users = do - mls <- getMLSState +createAddCommit :: (HasCallStack) => ClientIdentity -> ConvId -> [Value] -> App MessagePackage +createAddCommit cid convId users = do + conv <- getMLSConv convId kps <- fmap concat . for users $ \user -> do - bundle <- claimKeyPackages mls.ciphersuite cid user >>= getJSON 200 + bundle <- claimKeyPackages conv.ciphersuite cid user >>= getJSON 200 unbundleKeyPackages bundle - createAddCommitWithKeyPackages cid kps + createAddCommitWithKeyPackages cid convId kps withTempKeyPackageFile :: ByteString -> ContT a App FilePath withTempKeyPackageFile bs = do @@ -332,15 +342,19 @@ withTempKeyPackageFile bs = do createAddCommitWithKeyPackages :: (HasCallStack) => ClientIdentity -> + ConvId -> [(ClientIdentity, ByteString)] -> App MessagePackage -createAddCommitWithKeyPackages cid clientsAndKeyPackages = do +createAddCommitWithKeyPackages cid convId clientsAndKeyPackages = do bd <- getBaseDir welcomeFile <- liftIO $ emptyTempFile bd "welcome" giFile <- liftIO $ emptyTempFile bd "gi" + Just conv <- Map.lookup convId . (.convs) <$> getMLSState commit <- runContT (traverse (withTempKeyPackageFile . snd) clientsAndKeyPackages) $ \kpFiles -> mlscli + (Just convId) + conv.ciphersuite cid ( [ "member", "add", @@ -359,7 +373,13 @@ createAddCommitWithKeyPackages cid clientsAndKeyPackages = do modifyMLSState $ \mls -> mls - { newMembers = Set.fromList (map fst clientsAndKeyPackages) + { convs = + Map.adjust + ( \oldConvState -> + oldConvState {newMembers = Set.fromList (map fst clientsAndKeyPackages)} + ) + convId + mls.convs } welcome <- liftIO $ BS.readFile welcomeFile @@ -367,25 +387,30 @@ createAddCommitWithKeyPackages cid clientsAndKeyPackages = do pure $ MessagePackage { sender = cid, + convId = convId, message = commit, welcome = Just welcome, groupInfo = Just gi } -createRemoveCommit :: (HasCallStack) => ClientIdentity -> [ClientIdentity] -> App MessagePackage -createRemoveCommit cid targets = do +createRemoveCommit :: (HasCallStack) => ClientIdentity -> ConvId -> [ClientIdentity] -> App MessagePackage +createRemoveCommit cid convId targets = do bd <- getBaseDir welcomeFile <- liftIO $ emptyTempFile bd "welcome" giFile <- liftIO $ emptyTempFile bd "gi" groupStateMap <- do gs <- getClientGroupState cid - groupData <- assertJust "Group state not initialised" gs.group + groupData <- assertJust "Group state not initialised" (Map.lookup convId gs.groups) Map.fromList <$> readGroupState groupData let indices = map (fromMaybe (error "could not find target") . flip Map.lookup groupStateMap) targets + conv <- getMLSConv convId + commit <- mlscli + (Just convId) + conv.ciphersuite cid ( [ "member", "remove", @@ -408,58 +433,71 @@ createRemoveCommit cid targets = do pure MessagePackage { sender = cid, + convId = convId, message = commit, welcome = Just welcome, groupInfo = Just gi } -createAddProposals :: (HasCallStack) => ClientIdentity -> [Value] -> App [MessagePackage] -createAddProposals cid users = do - mls <- getMLSState +createAddProposals :: (HasCallStack) => ConvId -> ClientIdentity -> [Value] -> App [MessagePackage] +createAddProposals convId cid users = do + Just mls <- Map.lookup convId . (.convs) <$> getMLSState bundles <- for users $ (claimKeyPackages mls.ciphersuite cid >=> getJSON 200) kps <- concat <$> traverse unbundleKeyPackages bundles - traverse (createAddProposalWithKeyPackage cid) kps + traverse (createAddProposalWithKeyPackage convId cid) kps -createReInitProposal :: (HasCallStack) => ClientIdentity -> App MessagePackage -createReInitProposal cid = do +createReInitProposal :: (HasCallStack) => ConvId -> ClientIdentity -> App MessagePackage +createReInitProposal convId cid = do + conv <- getMLSConv convId prop <- mlscli + (Just convId) + conv.ciphersuite cid ["proposal", "--group-in", "", "--group-out", "", "re-init"] Nothing pure MessagePackage { sender = cid, + convId = convId, message = prop, welcome = Nothing, groupInfo = Nothing } createAddProposalWithKeyPackage :: + ConvId -> ClientIdentity -> (ClientIdentity, ByteString) -> App MessagePackage -createAddProposalWithKeyPackage cid (_, kp) = do +createAddProposalWithKeyPackage convId cid (_, kp) = do + conv <- getMLSConv convId prop <- runContT (withTempKeyPackageFile kp) $ \kpFile -> mlscli + (Just convId) + conv.ciphersuite cid ["proposal", "--group-in", "", "--group-out", "", "add", kpFile] Nothing pure MessagePackage { sender = cid, + convId = convId, message = prop, welcome = Nothing, groupInfo = Nothing } -createPendingProposalCommit :: (HasCallStack) => ClientIdentity -> App MessagePackage -createPendingProposalCommit cid = do +createPendingProposalCommit :: (HasCallStack) => ConvId -> ClientIdentity -> App MessagePackage +createPendingProposalCommit convId cid = do bd <- getBaseDir welcomeFile <- liftIO $ emptyTempFile bd "welcome" pgsFile <- liftIO $ emptyTempFile bd "pgs" + conv <- getMLSConv convId commit <- mlscli + (Just convId) + conv.ciphersuite cid [ "commit", "--group", @@ -478,6 +516,7 @@ createPendingProposalCommit cid = do pure MessagePackage { sender = cid, + convId = convId, message = commit, welcome = welcome, groupInfo = Just pgs @@ -485,18 +524,21 @@ createPendingProposalCommit cid = do createExternalCommit :: (HasCallStack) => + ConvId -> ClientIdentity -> Maybe ByteString -> App MessagePackage -createExternalCommit cid mgi = do +createExternalCommit convId cid mgi = do bd <- getBaseDir giFile <- liftIO $ emptyTempFile bd "gi" - conv <- getConv gi <- case mgi of - Nothing -> getGroupInfo cid conv >>= getBody 200 + Nothing -> getGroupInfo cid convId >>= getBody 200 Just v -> pure v + conv <- getMLSConv convId commit <- mlscli + (Just convId) + conv.ciphersuite cid [ "external-commit", "--group-info-in", @@ -510,7 +552,7 @@ createExternalCommit cid mgi = do modifyMLSState $ \mls -> mls - { newMembers = Set.singleton cid + { convs = Map.adjust (\oldConvState -> oldConvState {newMembers = Set.singleton cid}) convId mls.convs -- This might be a different client than those that have been in the -- group from before. } @@ -519,6 +561,7 @@ createExternalCommit cid mgi = do pure $ MessagePackage { sender = cid, + convId = convId, message = commit, welcome = Nothing, groupInfo = Just newPgs @@ -527,25 +570,13 @@ createExternalCommit cid mgi = do data MLSNotificationTag = MLSNotificationMessageTag | MLSNotificationWelcomeTag deriving (Show, Eq, Ord) --- | Extract a conversation ID (including an optional subconversation) from an --- event object. -eventSubConv :: (HasCallStack) => (MakesValue event) => event -> App Value -eventSubConv event = do - sub <- lookupField event "subconv" - conv <- event %. "qualified_conversation" - objSubConvObject $ - object - [ "parent_qualified_id" .= conv, - "subconv_id" .= sub - ] - -consumingMessages :: (HasCallStack) => MessagePackage -> Codensity App () -consumingMessages mp = Codensity $ \k -> do - mls <- getMLSState +consumingMessages :: (HasCallStack) => MLSProtocol -> MessagePackage -> Codensity App () +consumingMessages mlsProtocol mp = Codensity $ \k -> do + conv <- getMLSConv mp.convId -- clients that should receive the message itself - let oldClients = Set.delete mp.sender mls.members + let oldClients = Set.delete mp.sender conv.members -- clients that should receive a welcome message - let newClients = Set.delete mp.sender mls.newMembers + let newClients = Set.delete mp.sender conv.newMembers -- all clients that should receive some MLS notification, together with the -- expected notification tag let clients = @@ -561,10 +592,12 @@ consumingMessages mp = Codensity $ \k -> do r <- k () -- if the conversation is actually MLS (and not mixed), pick one client for - -- each new user and wait for its join event - when (mls.protocol == MLSProtocolMLS) $ + -- each new user and wait for its join event. In Mixed protocol, the user is + -- already in the conversation so they do not get a member-join + -- notification. + when (mlsProtocol == MLSProtocolMLS) $ traverse_ - (awaitMatch isMemberJoinNotif) + (awaitMatch (\n -> isMemberJoinNotif n)) ( flip Map.restrictKeys newUsers . Map.mapKeys ((.user) . fst) . Map.fromList @@ -575,50 +608,53 @@ consumingMessages mp = Codensity $ \k -> do -- at this point we know that every new user has been added to the -- conversation for_ (zip clients wss) $ \((cid, t), ws) -> case t of - MLSNotificationMessageTag -> void $ consumeMessageNoExternal cid (Just mp) ws + MLSNotificationMessageTag -> void $ consumeMessageNoExternal conv.ciphersuite cid mp ws MLSNotificationWelcomeTag -> consumeWelcome cid mp ws pure r -consumeMessageWithPredicate :: (HasCallStack) => (Value -> App Bool) -> ClientIdentity -> Maybe MessagePackage -> WebSocket -> App Value -consumeMessageWithPredicate p cid mmp ws = do - mls <- getMLSState +consumeMessageWithPredicate :: (HasCallStack) => (Value -> App Bool) -> ConvId -> Ciphersuite -> ClientIdentity -> Maybe MessagePackage -> WebSocket -> App Value +consumeMessageWithPredicate p convId cs cid mmp ws = do notif <- awaitMatch p ws event <- notif %. "payload.0" + event %. "qualified_conversation" `shouldMatch` convIdToQidObject convId + lookupField event "subconv" `shouldMatch` convId.subconvId + for_ mmp $ \mp -> do - shouldMatch (eventSubConv event) (fromMaybe A.Null mls.convId) - shouldMatch (event %. "from") mp.sender.user - shouldMatch (event %. "data") (B8.unpack (Base64.encode mp.message)) + event %. "from" `shouldMatch` mp.sender.user + event %. "data" `shouldMatch` (B8.unpack (Base64.encode mp.message)) msgData <- event %. "data" & asByteString - _ <- mlsCliConsume cid msgData - showMessage cid msgData + _ <- mlsCliConsume convId cs cid msgData + showMessage cs cid msgData -- | Get a single MLS message from a websocket and consume it. Return a JSON -- representation of the message. -consumeMessage :: (HasCallStack) => ClientIdentity -> Maybe MessagePackage -> WebSocket -> App Value +consumeMessage :: (HasCallStack) => ConvId -> Ciphersuite -> ClientIdentity -> Maybe MessagePackage -> WebSocket -> App Value consumeMessage = consumeMessageWithPredicate isNewMLSMessageNotif -- | like 'consumeMessage' but will not consume a message where the sender is the backend -consumeMessageNoExternal :: (HasCallStack) => ClientIdentity -> Maybe MessagePackage -> WebSocket -> App Value -consumeMessageNoExternal cid = consumeMessageWithPredicate isNewMLSMessageNotifButNoProposal cid +consumeMessageNoExternal :: (HasCallStack) => Ciphersuite -> ClientIdentity -> MessagePackage -> WebSocket -> App Value +consumeMessageNoExternal cs cid mp = consumeMessageWithPredicate isNewMLSMessageNotifButNoProposal mp.convId cs cid (Just mp) where -- the backend (correctly) reacts to a commit removing someone from a parent conversation with a -- remove proposal, however, we don't want to consume this here isNewMLSMessageNotifButNoProposal :: Value -> App Bool isNewMLSMessageNotifButNoProposal n = do - isNotif <- isNewMLSMessageNotif n - if isNotif + isRelevantNotif <- isNewMLSMessageNotif n &&~ isNotifConvId mp.convId n + if isRelevantNotif then do - msg <- n %. "payload.0.data" & asByteString >>= showMessage cid + msg <- n %. "payload.0.data" & asByteString >>= showMessage cs cid sender <- msg `lookupField` "message.content.sender" `catch` \(_ :: AssertionFailure) -> pure Nothing let backendSender = object ["External" .= Number 0] pure $ sender /= Just backendSender else pure False -mlsCliConsume :: (HasCallStack) => ClientIdentity -> ByteString -> App ByteString -mlsCliConsume cid msgData = +mlsCliConsume :: (HasCallStack) => ConvId -> Ciphersuite -> ClientIdentity -> ByteString -> App ByteString +mlsCliConsume convId cs cid msgData = mlscli + (Just convId) + cs cid [ "consume", "--group", @@ -632,58 +668,74 @@ mlsCliConsume cid msgData = -- | Send an MLS message, wait for clients to receive it, then consume it on -- the client side. If the message is a commit, the -- 'sendAndConsumeCommitBundle' function should be used instead. +-- +-- returns response body of 'postMLSMessage' sendAndConsumeMessage :: (HasCallStack) => MessagePackage -> App Value sendAndConsumeMessage mp = lowerCodensity $ do - consumingMessages mp + consumingMessages MLSProtocolMLS mp lift $ postMLSMessage mp.sender mp.message >>= getJSON 201 +sendAndConsumeCommitBundle :: (HasCallStack) => MessagePackage -> App Value +sendAndConsumeCommitBundle = sendAndConsumeCommitBundleWithProtocol MLSProtocolMLS + -- | Send an MLS commit bundle, wait for clients to receive it, consume it, and -- update the test state accordingly. -sendAndConsumeCommitBundle :: (HasCallStack) => MessagePackage -> App Value -sendAndConsumeCommitBundle mp = do +sendAndConsumeCommitBundleWithProtocol :: (HasCallStack) => MLSProtocol -> MessagePackage -> App Value +sendAndConsumeCommitBundleWithProtocol protocol mp = do lowerCodensity $ do - consumingMessages mp + consumingMessages protocol mp lift $ do r <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 201 -- if the sender is a new member (i.e. it's an external commit), then -- process the welcome message directly do - mls <- getMLSState - when (Set.member mp.sender mls.newMembers) $ - traverse_ (fromWelcome mp.sender) mp.welcome + conv <- getMLSConv mp.convId + when (Set.member mp.sender conv.newMembers) $ + traverse_ (fromWelcome mp.convId conv.ciphersuite mp.sender) mp.welcome -- increment epoch and add new clients modifyMLSState $ \mls -> mls - { epoch = epoch mls + 1, - members = members mls <> newMembers mls, - newMembers = mempty + { convs = + Map.adjust + ( \conv -> + conv + { epoch = conv.epoch + 1, + members = conv.members <> conv.newMembers, + newMembers = mempty + } + ) + mp.convId + mls.convs } pure r consumeWelcome :: (HasCallStack) => ClientIdentity -> MessagePackage -> WebSocket -> App () consumeWelcome cid mp ws = do - mls <- getMLSState notif <- awaitMatch isWelcomeNotif ws event <- notif %. "payload.0" - shouldMatch (eventSubConv event) (fromMaybe A.Null mls.convId) - shouldMatch (event %. "from") mp.sender.user - shouldMatch (event %. "data") (fmap (B8.unpack . Base64.encode) mp.welcome) + event %. "qualified_conversation" `shouldMatch` convIdToQidObject mp.convId + lookupField event "subconv" `shouldMatch` mp.convId.subconvId + event %. "from" `shouldMatch` mp.sender.user + event %. "data" `shouldMatch` (fmap (B8.unpack . Base64.encode) mp.welcome) welcome <- event %. "data" & asByteString gs <- getClientGroupState cid assertBool "Existing clients in a conversation should not consume welcomes" - (isNothing gs.group) - fromWelcome cid welcome + (not $ Map.member mp.convId gs.groups) + conv <- getMLSConv mp.convId + fromWelcome mp.convId conv.ciphersuite cid welcome -fromWelcome :: ClientIdentity -> ByteString -> App () -fromWelcome cid welcome = +fromWelcome :: ConvId -> Ciphersuite -> ClientIdentity -> ByteString -> App () +fromWelcome convId cs cid welcome = void $ mlscli + (Just convId) + cs cid [ "group", "from-welcome", @@ -733,9 +785,9 @@ setClientGroupState cid g = modifyMLSState $ \s -> s {clientGroupState = Map.insert cid g (clientGroupState s)} -showMessage :: (HasCallStack) => ClientIdentity -> ByteString -> App Value -showMessage cid msg = do - bs <- mlscli cid ["show", "message", "-"] (Just msg) +showMessage :: (HasCallStack) => Ciphersuite -> ClientIdentity -> ByteString -> App Value +showMessage cs cid msg = do + bs <- mlscli Nothing cs cid ["show", "message", "-"] (Just msg) assertOne (Aeson.decode (BS.fromStrict bs)) readGroupState :: (HasCallStack) => ByteString -> App [(ClientIdentity, Word32)] @@ -760,12 +812,16 @@ readGroupState gs = do createApplicationMessage :: (HasCallStack) => + ConvId -> ClientIdentity -> String -> App MessagePackage -createApplicationMessage cid messageContent = do +createApplicationMessage convId cid messageContent = do + conv <- getMLSConv convId message <- mlscli + (Just convId) + conv.ciphersuite cid ["message", "--group-in", "", messageContent, "--group-out", ""] Nothing @@ -773,36 +829,40 @@ createApplicationMessage cid messageContent = do pure MessagePackage { sender = cid, + convId = convId, message = message, welcome = Nothing, groupInfo = Nothing } -setMLSCiphersuite :: Ciphersuite -> App () -setMLSCiphersuite suite = modifyMLSState $ \mls -> mls {ciphersuite = suite} +setMLSCiphersuite :: ConvId -> Ciphersuite -> App () +setMLSCiphersuite convId suite = modifyMLSState $ \mls -> mls {convs = Map.adjust (\conv -> conv {ciphersuite = suite}) convId mls.convs} -leaveCurrentConv :: +leaveConv :: (HasCallStack) => + ConvId -> ClientIdentity -> App () -leaveCurrentConv cid = do - mls <- getMLSState - (_, mSubId) <- objSubConv mls.convId - case mSubId of +leaveConv convId cid = do + case convId.subconvId of -- FUTUREWORK: implement leaving main conversation as well Nothing -> assertFailure "Leaving conversations is not supported" Just _ -> do - void $ leaveSubConversation cid mls.convId >>= getBody 200 + void $ leaveSubConversation cid convId >>= getBody 200 modifyMLSState $ \s -> s - { members = Set.difference mls.members (Set.singleton cid) + { convs = Map.adjust (\conv -> conv {members = Set.delete cid conv.members}) convId s.convs } -getCurrentConv :: (HasCallStack) => ClientIdentity -> App Value -getCurrentConv cid = do - mls <- getMLSState - (conv, mSubId) <- objSubConv mls.convId - resp <- case mSubId of - Nothing -> getConversation cid conv - Just sub -> getSubConversation cid conv sub +getConv :: (HasCallStack) => ConvId -> ClientIdentity -> App Value +getConv convId cid = do + resp <- case convId.subconvId of + Nothing -> getConversation cid (convIdToQidObject convId) + Just sub -> getSubConversation cid convId sub getJSON 200 resp + +getSubConvId :: (MakesValue user, HasCallStack) => user -> ConvId -> String -> App ConvId +getSubConvId user convId subConvName = + getSubConversation user convId subConvName + >>= getJSON 200 + >>= objConvId diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index d99b46b8897..7c99e254d11 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -127,6 +127,12 @@ isConvLeaveNotifWithLeaver user n = isNotifConv :: (MakesValue conv, MakesValue a, HasCallStack) => conv -> a -> App Bool isNotifConv conv n = fieldEquals n "payload.0.qualified_conversation" (objQidObject conv) +isNotifConvId :: (MakesValue a, HasCallStack) => ConvId -> a -> App Bool +isNotifConvId conv n = do + let subconvField = "payload.0.subconv" + fieldEquals n "payload.0.qualified_conversation" (convIdToQidObject conv) + &&~ maybe (isNothing <$> lookupField n subconvField) (fieldEquals n subconvField) conv.subconvId + isNotifForUser :: (MakesValue user, MakesValue a, HasCallStack) => user -> a -> App Bool isNotifForUser user n = fieldEquals n "payload.0.data.qualified_user_ids.0" (objQidObject user) diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 4e19ae9b0a6..2cbd72c8ac0 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -28,6 +28,7 @@ import Data.Vector (fromList) import GHC.Stack import Testlib.MockIntegrationService (mkLegalHoldSettings) import Testlib.Prelude +import UnliftIO (pooledForConcurrentlyN) randomUser :: (HasCallStack, MakesValue domain) => domain -> CreateUser -> App Value randomUser domain cu = bindResponse (createUser domain cu) $ \resp -> do @@ -43,7 +44,7 @@ createTeam :: (HasCallStack, MakesValue domain) => domain -> Int -> App (Value, createTeam domain memberCount = do owner <- createUser domain def {team = True} >>= getJSON 201 tid <- owner %. "team" & asString - members <- for [2 .. memberCount] $ \_ -> createTeamMember owner def + members <- pooledForConcurrentlyN 64 [2 .. memberCount] $ \_ -> createTeamMember owner def pure (owner, tid, members) data CreateTeamMember = CreateTeamMember @@ -131,7 +132,7 @@ getAllConvs u = do simpleMixedConversationSetup :: (HasCallStack, MakesValue domain) => domain -> - App (Value, Value, Value) + App (Value, Value, ConvId) simpleMixedConversationSetup secondDomain = do (alice, tid, _) <- createTeam OwnDomain 1 bob <- randomUser secondDomain def @@ -140,15 +141,17 @@ simpleMixedConversationSetup secondDomain = do conv <- postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} >>= getJSON 201 + >>= objConvId bindResponse (putConversationProtocol bob conv "mixed") $ \resp -> do resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} - - conv' <- getConversation alice conv >>= getJSON 200 + convId <- + getConversation alice (convIdToQidObject conv) + >>= getJSON 200 + >>= objConvId - pure (alice, bob, conv') + pure (alice, bob, convId) supportMLS :: (HasCallStack, MakesValue u) => u -> App () supportMLS u = do @@ -403,3 +406,11 @@ uploadDownloadProfilePicture :: (HasCallStack, MakesValue usr) => usr -> App (St uploadDownloadProfilePicture usr = do (dom, key, _payload) <- uploadProfilePicture usr downloadProfilePicture usr dom key + +addUsersToFailureContext :: (MakesValue user) => [(String, user)] -> App a -> App a +addUsersToFailureContext namesAndUsers action = do + let mkLine (name, user) = do + (domain, id_) <- objQid user + pure $ name <> ": " <> id_ <> "@" <> domain + allLines <- unlines <$> (mapM mkLine namesAndUsers) + addFailureContext allLines action diff --git a/integration/test/Test/AccessUpdate.hs b/integration/test/Test/AccessUpdate.hs index 01113946788..14c921fefe7 100644 --- a/integration/test/Test/AccessUpdate.hs +++ b/integration/test/Test/AccessUpdate.hs @@ -78,16 +78,17 @@ testAccessUpdateGuestRemoved proto = do >>= getJSON 201 pure (conv, clients) ConversationProtocolMLS -> do - alice1 <- createMLSClient def alice - clients <- traverse (createMLSClient def) [bob, charlie, dee] - traverse_ uploadNewKeyPackage clients + alice1 <- createMLSClient def def alice + clients <- traverse (createMLSClient def def) [bob, charlie, dee] + traverse_ (uploadNewKeyPackage def) clients conv <- postConversation alice1 defMLS {team = Just tid} >>= getJSON 201 - createGroup alice1 conv + convId <- objConvId conv + createGroup def alice1 convId - void $ createAddCommit alice1 [bob, charlie, dee] >>= sendAndConsumeCommitBundle - convId <- conv %. "qualified_id" - pure (convId, map (.client) (alice1 : clients)) + void $ createAddCommit alice1 convId [bob, charlie, dee] >>= sendAndConsumeCommitBundle + convQid <- conv %. "qualified_id" + pure (convQid, map (.client) (alice1 : clients)) let update = ["access" .= ([] :: [String]), "access_role" .= ["team_member"]] void $ updateAccess alice conv update >>= getJSON 200 diff --git a/integration/test/Test/ExternalPartner.hs b/integration/test/Test/ExternalPartner.hs index 01bdd629834..4c9f748e564 100644 --- a/integration/test/Test/ExternalPartner.hs +++ b/integration/test/Test/ExternalPartner.hs @@ -60,7 +60,7 @@ testExternalPartnerPermissionsMls = do -- external partners should not be able to create (MLS) conversations (owner, _, _) <- createTeam OwnDomain 2 bobExt <- createTeamMember owner def {role = "partner"} - bobExtClient <- createMLSClient def bobExt + bobExtClient <- createMLSClient def def bobExt bindResponse (postConversation bobExtClient defMLS) $ \resp -> do resp.status `shouldMatchInt` 403 diff --git a/integration/test/Test/FeatureFlags/LegalHold.hs b/integration/test/Test/FeatureFlags/LegalHold.hs index 55743ec4f91..45f099aef5c 100644 --- a/integration/test/Test/FeatureFlags/LegalHold.hs +++ b/integration/test/Test/FeatureFlags/LegalHold.hs @@ -111,7 +111,8 @@ testExposeInvitationURLsToTeamAdminConfig = do runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do let domain = testBackend.berDomain - let testNoAllowlistEntry = runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist ([] :: [String])) $ \_ -> do + testNoAllowlistEntry :: (HasCallStack) => App (Value, String) + testNoAllowlistEntry = runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist ([] :: [String])) $ \_ -> do (owner, tid, _) <- createTeam domain 1 checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabledLocked -- here we get a response with HTTP status 200 and feature status unchanged (disabled), which we find weird, but we're just testing the current behavior diff --git a/integration/test/Test/LegalHold.hs b/integration/test/Test/LegalHold.hs index e8cc0b22743..7b0e3680aeb 100644 --- a/integration/test/Test/LegalHold.hs +++ b/integration/test/Test/LegalHold.hs @@ -914,9 +914,9 @@ testBlockLHForMLSUsers = do -- scenario 1: -- if charlie is in any MLS conversation, he cannot approve to be put under legalhold (charlie, tid, []) <- createTeam OwnDomain 1 - [charlie1] <- traverse (createMLSClient def) [charlie] - void $ createNewGroup charlie1 - void $ createAddCommit charlie1 [charlie] >>= sendAndConsumeCommitBundle + [charlie1] <- traverse (createMLSClient def def) [charlie] + convId <- createNewGroup def charlie1 + void $ createAddCommit charlie1 convId [charlie] >>= sendAndConsumeCommitBundle legalholdWhitelistTeam tid charlie >>= assertStatus 200 withMockServer def lhMockApp \lhDomAndPort _chan -> do @@ -934,9 +934,9 @@ testBlockLHForMLSUsers = do testBlockClaimingKeyPackageForLHUsers :: (HasCallStack) => App () testBlockClaimingKeyPackageForLHUsers = do (alice, tid, [charlie]) <- createTeam OwnDomain 2 - [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] - _ <- uploadNewKeyPackage charlie1 - _ <- createNewGroup alice1 + [alice1, charlie1] <- traverse (createMLSClient def def) [alice, charlie] + _ <- uploadNewKeyPackage def charlie1 + _ <- createNewGroup def alice1 legalholdWhitelistTeam tid alice >>= assertStatus 200 withMockServer def lhMockApp \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 @@ -946,8 +946,7 @@ testBlockClaimingKeyPackageForLHUsers = do pStatus <- profile %. "legalhold_status" & asString pStatus `shouldMatch` "enabled" - mls <- getMLSState - claimKeyPackages mls.ciphersuite alice1 charlie + claimKeyPackages def alice1 charlie `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" -- | scenario 2.2: @@ -958,8 +957,8 @@ testBlockClaimingKeyPackageForLHUsers = do testBlockCreateMLSConvForLHUsers :: (HasCallStack) => LhApiVersion -> App () testBlockCreateMLSConvForLHUsers v = do (alice, tid, [charlie]) <- createTeam OwnDomain 2 - [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] - _ <- uploadNewKeyPackage alice1 + [alice1, charlie1] <- traverse (createMLSClient def def) [alice, charlie] + _ <- uploadNewKeyPackage def alice1 legalholdWhitelistTeam tid alice >>= assertStatus 200 withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 @@ -970,12 +969,12 @@ testBlockCreateMLSConvForLHUsers v = do pStatus `shouldMatch` "enabled" -- charlie tries to create a group and should fail when POSTing the add commit - _ <- createNewGroup charlie1 + convId <- createNewGroup def charlie1 void -- we try to add alice since adding charlie himself would trigger 2.1 -- since he'd try to claim his own keypackages - $ createAddCommit charlie1 [alice] + $ createAddCommit charlie1 convId [alice] >>= \mp -> postMLSCommitBundle mp.sender (mkBundle mp) `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" @@ -983,12 +982,12 @@ testBlockCreateMLSConvForLHUsers v = do -- (unsurprisingly) this same thing should also work in the one2one case respJson <- getMLSOne2OneConversation alice charlie >>= getJSON 200 - resetGroup alice1 (respJson %. "conversation") + createGroup def alice1 =<< objConvId (respJson %. "conversation") void -- we try to add alice since adding charlie himself would trigger 2.1 -- since he'd try to claim his own keypackages - $ createAddCommit charlie1 [alice] + $ createAddCommit charlie1 convId [alice] >>= \mp -> postMLSCommitBundle mp.sender (mkBundle mp) `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index f721f9ad06b..856f480e983 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -6,6 +6,7 @@ import API.Brig (claimKeyPackages, deleteClient) import API.Galley import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Char8 as B8 +import qualified Data.Map as Map import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.Encoding as T @@ -19,15 +20,15 @@ import Testlib.Prelude testSendMessageNoReturnToSender :: (HasCallStack) => App () testSendMessageNoReturnToSender = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def) [alice, alice, bob, bob] - traverse_ uploadNewKeyPackage [alice2, bob1, bob2] - void $ createNewGroup alice1 - void $ createAddCommit alice1 [alice, bob] >>= sendAndConsumeCommitBundle + [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def def) [alice, alice, bob, bob] + traverse_ (uploadNewKeyPackage def) [alice2, bob1, bob2] + convId <- createNewGroup def alice1 + void $ createAddCommit alice1 convId [alice, bob] >>= sendAndConsumeCommitBundle -- alice1 sends a message to the conversation, all clients but alice1 receive -- the message withWebSockets [alice1, alice2, bob1, bob2] $ \(wsSender : wss) -> do - mp <- createApplicationMessage alice1 "hello, bob" + mp <- createApplicationMessage convId alice1 "hello, bob" bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 201 for_ wss $ \ws -> do @@ -47,25 +48,25 @@ testPastStaleApplicationMessage :: (HasCallStack) => Domain -> App () testPastStaleApplicationMessage otherDomain = do [alice, bob, charlie, dave, eve] <- createAndConnectUsers [OwnDomain, otherDomain, OwnDomain, OwnDomain, OwnDomain] - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - traverse_ uploadNewKeyPackage [bob1, charlie1] - void $ createNewGroup alice1 + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) [alice, bob, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1] + convId <- createNewGroup def alice1 -- alice adds bob first - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle -- bob prepares some application messages - [msg1, msg2] <- replicateM 2 $ createApplicationMessage bob1 "hi alice" + [msg1, msg2] <- replicateM 2 $ createApplicationMessage convId bob1 "hi alice" -- alice adds charlie and dave with different commits - void $ createAddCommit alice1 [charlie] >>= sendAndConsumeCommitBundle - void $ createAddCommit alice1 [dave] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [dave] >>= sendAndConsumeCommitBundle -- bob's application messages still go through void $ postMLSMessage bob1 msg1.message >>= getJSON 201 -- alice adds eve - void $ createAddCommit alice1 [eve] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [eve] >>= sendAndConsumeCommitBundle -- bob's application messages are now rejected void $ postMLSMessage bob1 msg2.message >>= getJSON 409 @@ -73,20 +74,28 @@ testPastStaleApplicationMessage otherDomain = do testFutureStaleApplicationMessage :: (HasCallStack) => App () testFutureStaleApplicationMessage = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain, OwnDomain] - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - traverse_ uploadNewKeyPackage [bob1, charlie1] - void $ createNewGroup alice1 + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) [alice, bob, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1] + convId <- createNewGroup def alice1 -- alice adds bob - void . sendAndConsumeCommitBundle =<< createAddCommit alice1 [bob] + void . sendAndConsumeCommitBundle =<< createAddCommit alice1 convId [bob] -- alice adds charlie and consumes the commit without sending it - void $ createAddCommit alice1 [charlie] + void $ createAddCommit alice1 convId [charlie] modifyMLSState $ \mls -> mls - { epoch = epoch mls + 1, - members = members mls <> Set.singleton charlie1, - newMembers = mempty + { convs = + Map.adjust + ( \conv -> + conv + { epoch = conv.epoch + 1, + members = Set.insert charlie1 conv.members, + newMembers = mempty + } + ) + convId + mls.convs } -- alice's application message is rejected @@ -94,7 +103,7 @@ testFutureStaleApplicationMessage = do . getJSON 409 =<< postMLSMessage alice1 . (.message) - =<< createApplicationMessage alice1 "hi bob" + =<< createApplicationMessage convId alice1 "hi bob" testMixedProtocolUpgrade :: (HasCallStack) => Domain -> App () testMixedProtocolUpgrade secondDomain = do @@ -102,7 +111,7 @@ testMixedProtocolUpgrade secondDomain = do [bob, charlie] <- replicateM 2 (randomUser secondDomain def) connectUsers [alice, bob, charlie] - qcnv <- + convId <- postConversation alice defProteus @@ -110,77 +119,79 @@ testMixedProtocolUpgrade secondDomain = do team = Just tid } >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mls") $ \resp -> do + bindResponse (putConversationProtocol bob convId "mls") $ \resp -> do resp.status `shouldMatchInt` 403 withWebSockets [alice, charlie] $ \websockets -> do - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do resp.status `shouldMatchInt` 200 - resp.json %. "conversation" `shouldMatch` (qcnv %. "id") + resp.json %. "qualified_conversation" `shouldMatch` (convIdToQidObject convId) resp.json %. "data.protocol" `shouldMatch` "mixed" - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} for_ websockets $ \ws -> do n <- awaitMatch (\value -> nPayload value %. "type" `isEqual` "conversation.protocol-update") ws nPayload n %. "data.protocol" `shouldMatch` "mixed" - bindResponse (getConversation alice qcnv) $ \resp -> do + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "protocol" `shouldMatch` "mixed" resp.json %. "epoch" `shouldMatchInt` 0 - bindResponse (putConversationProtocol alice qcnv "mixed") $ \resp -> do + bindResponse (putConversationProtocol alice convId "mixed") $ \resp -> do resp.status `shouldMatchInt` 204 - bindResponse (putConversationProtocol bob qcnv "proteus") $ \resp -> do + bindResponse (putConversationProtocol bob convId "proteus") $ \resp -> do resp.status `shouldMatchInt` 403 - bindResponse (putConversationProtocol bob qcnv "invalid") $ \resp -> do + bindResponse (putConversationProtocol bob convId "invalid") $ \resp -> do resp.status `shouldMatchInt` 400 testMixedProtocolNonTeam :: (HasCallStack) => Domain -> App () testMixedProtocolNonTeam secondDomain = do [alice, bob] <- createAndConnectUsers [OwnDomain, secondDomain] - qcnv <- + convId <- postConversation alice defProteus {qualifiedUsers = [bob]} >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do resp.status `shouldMatchInt` 403 testMixedProtocolAddUsers :: (HasCallStack) => Domain -> Ciphersuite -> App () testMixedProtocolAddUsers secondDomain suite = do - setMLSCiphersuite suite (alice, tid, _) <- createTeam OwnDomain 1 [bob, charlie] <- replicateM 2 (randomUser secondDomain def) connectUsers [alice, bob, charlie] - qcnv <- - postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} - >>= getJSON 201 + convId <- do + convId <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do - resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "epoch" `shouldMatchInt` 0 + objConvId resp.json - bindResponse (getConversation alice qcnv) $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "epoch" `shouldMatchInt` 0 - createGroup alice1 resp.json + [alice1, bob1] <- traverse (createMLSClient suite def) [alice, bob] + createGroup suite alice1 convId - traverse_ uploadNewKeyPackage [bob1] + void $ uploadNewKeyPackage suite bob1 withWebSocket bob $ \ws -> do - mp <- createAddCommit alice1 [bob] + mp <- createAddCommit alice1 convId [bob] welcome <- assertJust "should have welcome" mp.welcome - void $ sendAndConsumeCommitBundle mp + void $ sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed mp n <- awaitMatch (\n -> nPayload n %. "type" `isEqual` "conversation.mls-welcome") ws nPayload n %. "data" `shouldMatch` T.decodeUtf8 (Base64.encode welcome) - bindResponse (getConversation alice qcnv) $ \resp -> do + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "epoch" `shouldMatchInt` 1 (suiteCode, _) <- assertOne $ T.hexadecimal (T.pack suite.code) @@ -192,32 +203,34 @@ testMixedProtocolUserLeaves secondDomain = do bob <- randomUser secondDomain def connectUsers [alice, bob] - qcnv <- - postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} - >>= getJSON 201 + convId <- do + convId <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do - resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} - - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 - bindResponse (getConversation alice qcnv) $ \resp -> do - resp.status `shouldMatchInt` 200 - createGroup alice1 resp.json + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + objConvId resp.json - traverse_ uploadNewKeyPackage [bob1] + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + createGroup def alice1 convId + void $ uploadNewKeyPackage def bob1 - mp <- createAddCommit alice1 [bob] - void $ sendAndConsumeCommitBundle mp + mp <- createAddCommit alice1 convId [bob] + void $ sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed mp withWebSocket alice $ \ws -> do - bindResponse (removeConversationMember bob qcnv) $ \resp -> + bindResponse (removeConversationMember bob (convIdToQidObject convId)) $ \resp -> resp.status `shouldMatchInt` 200 n <- awaitMatch (\n -> nPayload n %. "type" `isEqual` "conversation.mls-message-add") ws - msg <- asByteString (nPayload n %. "data") >>= showMessage alice1 + conv <- getMLSConv convId + msg <- asByteString (nPayload n %. "data") >>= showMessage conv.ciphersuite alice1 let leafIndexBob = 1 msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob msg %. "message.content.sender.External" `shouldMatchInt` 0 @@ -228,29 +241,31 @@ testMixedProtocolAddPartialClients secondDomain = do bob <- randomUser secondDomain def connectUsers [alice, bob] - qcnv <- - postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} - >>= getJSON 201 + convId <- do + convId <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do - resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + objConvId resp.json - bindResponse (getConversation alice qcnv) $ \resp -> do - resp.status `shouldMatchInt` 200 - createGroup alice1 resp.json + [alice1, bob1, bob2] <- traverse (createMLSClient def def) [alice, bob, bob] + createGroup def alice1 convId - traverse_ uploadNewKeyPackage [bob1, bob1, bob2, bob2] + traverse_ (uploadNewKeyPackage def) [bob1, bob1, bob2, bob2] -- create add commit for only one of bob's two clients do bundle <- claimKeyPackages def alice1 bob >>= getJSON 200 kps <- unbundleKeyPackages bundle kp1 <- assertOne (filter ((== bob1) . fst) kps) - mp <- createAddCommitWithKeyPackages alice1 [kp1] - void $ sendAndConsumeCommitBundle mp + mp <- createAddCommitWithKeyPackages alice1 convId [kp1] + void $ sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed mp -- this tests that bob's backend has a mapping of group id to the remote conv -- this test is only interesting when bob is on OtherDomain @@ -258,7 +273,7 @@ testMixedProtocolAddPartialClients secondDomain = do bundle <- claimKeyPackages def bob1 bob >>= getJSON 200 kps <- unbundleKeyPackages bundle kp2 <- assertOne (filter ((== bob2) . fst) kps) - mp <- createAddCommitWithKeyPackages bob1 [kp2] + mp <- createAddCommitWithKeyPackages bob1 convId [kp2] void $ postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 201 testMixedProtocolRemovePartialClients :: (HasCallStack) => Domain -> App () @@ -267,23 +282,24 @@ testMixedProtocolRemovePartialClients secondDomain = do bob <- randomUser secondDomain def connectUsers [alice, bob] - qcnv <- - postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} - >>= getJSON 201 + convId <- do + convId <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do - resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} - - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 - bindResponse (getConversation alice qcnv) $ \resp -> do - resp.status `shouldMatchInt` 200 - createGroup alice1 resp.json + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + objConvId resp.json - traverse_ uploadNewKeyPackage [bob1, bob2] - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - mp <- createRemoveCommit alice1 [bob1] + [alice1, bob1, bob2] <- traverse (createMLSClient def def) [alice, bob, bob] + createGroup def alice1 convId + traverse_ (uploadNewKeyPackage def) [bob1, bob2] + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + mp <- createRemoveCommit alice1 convId [bob1] void $ postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 201 @@ -293,83 +309,83 @@ testMixedProtocolAppMessagesAreDenied secondDomain = do bob <- randomUser secondDomain def connectUsers [alice, bob] - qcnv <- - postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} - >>= getJSON 201 + convId <- do + convId <- + postConversation alice defProteus {qualifiedUsers = [bob], team = Just tid} + >>= getJSON 201 + >>= objConvId - bindResponse (putConversationProtocol bob qcnv "mixed") $ \resp -> do - resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMixed} + bindResponse (putConversationProtocol bob convId "mixed") $ \resp -> do + resp.status `shouldMatchInt` 200 - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do + resp.status `shouldMatchInt` 200 + objConvId resp.json - traverse_ uploadNewKeyPackage [bob1] + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] - bindResponse (getConversation alice qcnv) $ \resp -> do - resp.status `shouldMatchInt` 200 - createGroup alice1 resp.json + createGroup def alice1 convId + void $ uploadNewKeyPackage def bob1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed - mp <- createApplicationMessage bob1 "hello, world" + mp <- createApplicationMessage convId bob1 "hello, world" bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 422 resp.json %. "label" `shouldMatch` "mls-unsupported-message" testMLSProtocolUpgrade :: (HasCallStack) => Domain -> App () testMLSProtocolUpgrade secondDomain = do - (alice, bob, conv) <- simpleMixedConversationSetup secondDomain + (alice, bob, convId) <- simpleMixedConversationSetup secondDomain charlie <- randomUser OwnDomain def -- alice creates MLS group and bob joins - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - createGroup alice1 conv - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle - void $ createExternalCommit bob1 Nothing >>= sendAndConsumeCommitBundle + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) [alice, bob, charlie] + createGroup def alice1 convId + void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed + void $ createExternalCommit convId bob1 Nothing >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed void $ withWebSocket bob $ \ws -> do -- charlie is added to the group - void $ uploadNewKeyPackage charlie1 - void $ createAddCommit alice1 [charlie] >>= sendAndConsumeCommitBundle + void $ uploadNewKeyPackage def charlie1 + void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundleWithProtocol MLSProtocolMixed awaitMatch isNewMLSMessageNotif ws supportMLS alice - bindResponse (putConversationProtocol bob conv "mls") $ \resp -> do + bindResponse (putConversationProtocol bob convId "mls") $ \resp -> do resp.status `shouldMatchInt` 400 resp.json %. "label" `shouldMatch` "mls-migration-criteria-not-satisfied" - bindResponse (getConversation alice conv) $ \resp -> do + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "protocol" `shouldMatch` "mixed" supportMLS bob withWebSockets [alice1, bob1] $ \wss -> do - bindResponse (putConversationProtocol bob conv "mls") $ \resp -> do + bindResponse (putConversationProtocol bob convId "mls") $ \resp -> do resp.status `shouldMatchInt` 200 - modifyMLSState $ \mls -> mls {protocol = MLSProtocolMLS} for_ wss $ \ws -> do n <- awaitMatch isNewMLSMessageNotif ws - msg <- asByteString (nPayload n %. "data") >>= showMessage alice1 + msg <- asByteString (nPayload n %. "data") >>= showMessage def alice1 let leafIndexCharlie = 2 msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexCharlie msg %. "message.content.sender.External" `shouldMatchInt` 0 - bindResponse (getConversation alice conv) $ \resp -> do + bindResponse (getConversation alice (convIdToQidObject convId)) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "protocol" `shouldMatch` "mls" testAddUserSimple :: (HasCallStack) => Ciphersuite -> CredentialType -> App () testAddUserSimple suite ctype = do - setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - bob1 <- createMLSClient def {credType = ctype} bob - void $ uploadNewKeyPackage bob1 - [alice1, bob2] <- traverse (createMLSClient def {credType = ctype}) [alice, bob] + bob1 <- createMLSClient suite def {credType = ctype} bob + void $ uploadNewKeyPackage suite bob1 + [alice1, bob2] <- traverse (createMLSClient suite def {credType = ctype}) [alice, bob] - traverse_ uploadNewKeyPackage [bob2] + void $ uploadNewKeyPackage suite bob2 qcnv <- withWebSocket alice $ \ws -> do - (_, qcnv) <- createNewGroup alice1 + qcnv <- createNewGroup suite alice1 -- check that the conversation inside the ConvCreated event contains -- epoch and ciphersuite, regardless of the API version n <- awaitMatch isConvCreateNotif ws @@ -377,11 +393,12 @@ testAddUserSimple suite ctype = do n %. "payload.0.data.cipher_suite" `shouldMatchInt` 1 pure qcnv - resp <- createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + resp <- createAddCommit alice1 qcnv [bob] >>= sendAndConsumeCommitBundle events <- resp %. "events" & asList do event <- assertOne events - shouldMatch (event %. "qualified_conversation") qcnv + shouldMatch (event %. "qualified_conversation.id") qcnv.id_ + shouldMatch (event %. "qualified_conversation.domain") qcnv.domain shouldMatch (event %. "type") "conversation.member-join" shouldMatch (event %. "from") (objId alice) members <- event %. "data" %. "users" & asList @@ -391,7 +408,7 @@ testAddUserSimple suite ctype = do -- check that bob can now see the conversation convs <- getAllConvs bob - convIds <- traverse (%. "qualified_id") convs + convIds <- traverse objConvId convs void $ assertBool "Users added to an MLS group should find it when listing conversations" @@ -400,14 +417,14 @@ testAddUserSimple suite ctype = do testRemoteAddUser :: (HasCallStack) => App () testRemoteAddUser = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OtherDomain, OwnDomain] - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - traverse_ uploadNewKeyPackage [bob1, charlie1] - (_, conv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - bindResponse (updateConversationMember alice1 conv bob "wire_admin") $ \resp -> + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) [alice, bob, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1] + conv <- createNewGroup def alice1 + void $ createAddCommit alice1 conv [bob] >>= sendAndConsumeCommitBundle + bindResponse (updateConversationMember alice1 (convIdToQidObject conv) bob "wire_admin") $ \resp -> resp.status `shouldMatchInt` 200 - mp <- createAddCommit bob1 [charlie] + mp <- createAddCommit bob1 conv [charlie] -- Support for remote admins is not implemeted yet, but this shows that add -- proposal is being applied action bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do @@ -416,115 +433,113 @@ testRemoteAddUser = do testRemoteRemoveClient :: (HasCallStack) => Ciphersuite -> App () testRemoteRemoveClient suite = do - setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - void $ uploadNewKeyPackage bob1 - (_, conv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + [alice1, bob1] <- traverse (createMLSClient suite def) [alice, bob] + void $ uploadNewKeyPackage suite bob1 + conv <- createNewGroup suite alice1 + void $ createAddCommit alice1 conv [bob] >>= sendAndConsumeCommitBundle withWebSocket alice $ \wsAlice -> do void $ deleteClient bob bob1.client >>= getBody 200 let predicate n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" n <- awaitMatch predicate wsAlice - shouldMatch (nPayload n %. "conversation") (objId conv) + shouldMatch (nPayload n %. "qualified_conversation") (convIdToQidObject conv) shouldMatch (nPayload n %. "from") (objId bob) mlsMsg <- asByteString (nPayload n %. "data") -- Checks that the remove proposal is consumable by alice - void $ mlsCliConsume alice1 mlsMsg + void $ mlsCliConsume conv suite alice1 mlsMsg -- This doesn't work because `sendAndConsumeCommitBundle` doesn't like -- remove proposals from the backend. We should fix that in future. -- void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle - parsedMsg <- showMessage alice1 mlsMsg + parsedMsg <- showMessage suite alice1 mlsMsg let leafIndexBob = 1 parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 testRemoteRemoveCreatorClient :: (HasCallStack) => Ciphersuite -> App () testRemoteRemoveCreatorClient suite = do - setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - void $ uploadNewKeyPackage bob1 - (_, conv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + [alice1, bob1] <- traverse (createMLSClient suite def) [alice, bob] + void $ uploadNewKeyPackage suite bob1 + conv <- createNewGroup suite alice1 + void $ createAddCommit alice1 conv [bob] >>= sendAndConsumeCommitBundle withWebSocket bob $ \wsBob -> do void $ deleteClient alice alice1.client >>= getBody 200 let predicate n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" n <- awaitMatch predicate wsBob - shouldMatch (nPayload n %. "conversation") (objId conv) + shouldMatch (nPayload n %. "qualified_conversation") (convIdToQidObject conv) shouldMatch (nPayload n %. "from") (objId alice) mlsMsg <- asByteString (nPayload n %. "data") -- Checks that the remove proposal is consumable by alice - void $ mlsCliConsume alice1 mlsMsg + void $ mlsCliConsume conv suite alice1 mlsMsg -- This doesn't work because `sendAndConsumeCommitBundle` doesn't like -- remove proposals from the backend. We should fix that in future. -- void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle - parsedMsg <- showMessage alice1 mlsMsg + parsedMsg <- showMessage suite alice1 mlsMsg let leafIndexAlice = 0 parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexAlice parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 testCreateSubConv :: (HasCallStack) => Ciphersuite -> App () testCreateSubConv suite = do - setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - aliceClients@(alice1 : _) <- replicateM 5 $ createMLSClient def alice - replicateM_ 3 $ traverse_ uploadNewKeyPackage aliceClients - [bob1, bob2] <- replicateM 2 $ createMLSClient def bob - replicateM_ 3 $ traverse_ uploadNewKeyPackage [bob1, bob2] - void $ createNewGroup alice1 - void $ createAddCommit alice1 [alice, bob] >>= sendAndConsumeCommitBundle - createSubConv alice1 "conference" + aliceClients@(alice1 : _) <- replicateM 5 $ createMLSClient suite def alice + replicateM_ 3 $ traverse_ (uploadNewKeyPackage suite) aliceClients + [bob1, bob2] <- replicateM 2 $ createMLSClient suite def bob + replicateM_ 3 $ traverse_ (uploadNewKeyPackage suite) [bob1, bob2] + convId <- createNewGroup suite alice1 + void $ createAddCommit alice1 convId [alice, bob] >>= sendAndConsumeCommitBundle + createSubConv suite convId alice1 "conference" testCreateSubConvProteus :: App () testCreateSubConvProteus = do alice <- randomUser OwnDomain def conv <- bindResponse (postConversation alice defProteus) $ \resp -> do resp.status `shouldMatchInt` 201 - resp.json + objConvId resp.json bindResponse (getSubConversation alice conv "conference") $ \resp -> resp.status `shouldMatchInt` 404 testSelfConversation :: Version5 -> App () testSelfConversation v = withVersion5 v $ do alice <- randomUser OwnDomain def - creator : others <- traverse (createMLSClient def) (replicate 3 alice) - traverse_ uploadNewKeyPackage others - (_, conv) <- createSelfGroup creator + creator : others <- traverse (createMLSClient def def) (replicate 3 alice) + traverse_ (uploadNewKeyPackage def) others + (_, conv) <- createSelfGroup def creator + convId <- objConvId conv conv %. "epoch" `shouldMatchInt` 0 case v of Version5 -> conv %. "cipher_suite" `shouldMatchInt` 1 NoVersion5 -> assertFieldMissing conv "cipher_suite" - void $ createAddCommit creator [alice] >>= sendAndConsumeCommitBundle + void $ createAddCommit creator convId [alice] >>= sendAndConsumeCommitBundle - newClient <- createMLSClient def alice - void $ uploadNewKeyPackage newClient - void $ createExternalCommit newClient Nothing >>= sendAndConsumeCommitBundle + newClient <- createMLSClient def def alice + void $ uploadNewKeyPackage def newClient + void $ createExternalCommit convId newClient Nothing >>= sendAndConsumeCommitBundle -- | FUTUREWORK: Don't allow partial adds, not even in the first commit testFirstCommitAllowsPartialAdds :: (HasCallStack) => App () testFirstCommitAllowsPartialAdds = do alice <- randomUser OwnDomain def - [alice1, alice2, alice3] <- traverse (createMLSClient def) [alice, alice, alice] - traverse_ uploadNewKeyPackage [alice1, alice2, alice2, alice3, alice3] + [alice1, alice2, alice3] <- traverse (createMLSClient def def) [alice, alice, alice] + traverse_ (uploadNewKeyPackage def) [alice1, alice2, alice2, alice3, alice3] - (_, _qcnv) <- createNewGroup alice1 + convId <- createNewGroup def alice1 bundle <- claimKeyPackages def alice1 alice >>= getJSON 200 kps <- unbundleKeyPackages bundle -- first commit only adds kp for alice2 (not alice2 and alice3) - mp <- createAddCommitWithKeyPackages alice1 (filter ((== alice2) . fst) kps) + mp <- createAddCommitWithKeyPackages alice1 convId (filter ((== alice2) . fst) kps) bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "label" `shouldMatch` "mls-client-mismatch" @@ -538,24 +553,24 @@ testAddUserPartial = do [alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) -- Bob has 3 clients, Charlie has 2 - alice1 <- createMLSClient def alice - bobClients@[_bob1, _bob2, bob3] <- replicateM 3 (createMLSClient def bob) - charlieClients <- replicateM 2 (createMLSClient def charlie) + alice1 <- createMLSClient def def alice + bobClients@[_bob1, _bob2, bob3] <- replicateM 3 (createMLSClient def def bob) + charlieClients <- replicateM 2 (createMLSClient def def charlie) -- Only the first 2 clients of Bob's have uploaded key packages - traverse_ uploadNewKeyPackage (take 2 bobClients <> charlieClients) + traverse_ (uploadNewKeyPackage def) (take 2 bobClients <> charlieClients) -- alice adds bob's first 2 clients - void $ createNewGroup alice1 + convId <- createNewGroup def alice1 -- alice sends a commit now, and should get a conflict error kps <- fmap concat . for [bob, charlie] $ \user -> do bundle <- claimKeyPackages def alice1 user >>= getJSON 200 unbundleKeyPackages bundle - mp <- createAddCommitWithKeyPackages alice1 kps + mp <- createAddCommitWithKeyPackages alice1 convId kps -- before alice can commit, bob3 uploads a key package - void $ uploadNewKeyPackage bob3 + void $ uploadNewKeyPackage def bob3 err <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 409 err %. "label" `shouldMatch` "mls-client-mismatch" @@ -567,30 +582,30 @@ testRemoveClientsIncomplete :: (HasCallStack) => App () testRemoveClientsIncomplete = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] - traverse_ uploadNewKeyPackage [bob1, bob2] - void $ createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - mp <- createRemoveCommit alice1 [bob1] + [alice1, bob1, bob2] <- traverse (createMLSClient def def) [alice, bob, bob] + traverse_ (uploadNewKeyPackage def) [bob1, bob2] + convId <- createNewGroup def alice1 + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle + mp <- createRemoveCommit alice1 convId [bob1] err <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 409 err %. "label" `shouldMatch` "mls-client-mismatch" testAdminRemovesUserFromConv :: (HasCallStack) => Ciphersuite -> App () testAdminRemovesUserFromConv suite = do - setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] + [alice1, bob1, bob2] <- traverse (createMLSClient suite def) [alice, bob, bob] - void $ createWireClient bob - traverse_ uploadNewKeyPackage [bob1, bob2] - (gid, qcnv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - events <- createRemoveCommit alice1 [bob1, bob2] >>= sendAndConsumeCommitBundle + void $ createWireClient bob def + traverse_ (uploadNewKeyPackage suite) [bob1, bob2] + convId <- createNewGroup suite alice1 + let Just gid = convId.groupId + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle + events <- createRemoveCommit alice1 convId [bob1, bob2] >>= sendAndConsumeCommitBundle do event <- assertOne =<< asList (events %. "events") - event %. "qualified_conversation" `shouldMatch` qcnv + event %. "qualified_conversation" `shouldMatch` convIdToQidObject convId event %. "type" `shouldMatch` "conversation.member-leave" event %. "from" `shouldMatch` objId alice members <- event %. "data" %. "qualified_user_ids" & asList @@ -599,26 +614,26 @@ testAdminRemovesUserFromConv suite = do do convs <- getAllConvs bob - convIds <- traverse (%. "qualified_id") convs + convIds <- traverse objConvId convs clients <- bindResponse (getGroupClients alice gid) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "client_ids" & asList void $ assertOne clients assertBool "bob is not longer part of conversation after the commit" - (qcnv `notElem` convIds) + (convId `notElem` convIds) testLocalWelcome :: (HasCallStack) => App () testLocalWelcome = do users@[alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1] <- traverse (createMLSClient def) users + [alice1, bob1] <- traverse (createMLSClient def def) users - void $ uploadNewKeyPackage bob1 + void $ uploadNewKeyPackage def bob1 - (_, qcnv) <- createNewGroup alice1 + convId <- createNewGroup def alice1 - commit <- createAddCommit alice1 [bob] + commit <- createAddCommit alice1 convId [bob] Just welcome <- pure commit.welcome es <- withWebSocket bob1 $ \wsBob -> do @@ -627,14 +642,14 @@ testLocalWelcome = do n <- awaitMatch isWelcome wsBob - shouldMatch (nPayload n %. "conversation") (objId qcnv) + shouldMatch (nPayload n %. "qualified_conversation") (convIdToQidObject convId) shouldMatch (nPayload n %. "from") (objId alice) shouldMatch (nPayload n %. "data") (B8.unpack (Base64.encode welcome)) pure es event <- assertOne =<< asList (es %. "events") event %. "type" `shouldMatch` "conversation.member-join" - event %. "conversation" `shouldMatch` objId qcnv + event %. "qualified_conversation" `shouldMatch` convIdToQidObject convId addedUser <- (event %. "data.users") >>= asList >>= assertOne objQid addedUser `shouldMatch` objQid bob @@ -643,19 +658,19 @@ testStaleCommit = do (alice : users) <- createAndConnectUsers (replicate 5 OwnDomain) let (users1, users2) = splitAt 2 users - (alice1 : clients) <- traverse (createMLSClient def) (alice : users) - traverse_ uploadNewKeyPackage clients - void $ createNewGroup alice1 + (alice1 : clients) <- traverse (createMLSClient def def) (alice : users) + traverse_ (uploadNewKeyPackage def) clients + convId <- createNewGroup def alice1 gsBackup <- getClientGroupState alice1 -- add the first batch of users to the conversation - void $ createAddCommit alice1 users1 >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId users1 >>= sendAndConsumeCommitBundle -- now roll back alice1 and try to add the second batch of users setClientGroupState alice1 gsBackup - mp <- createAddCommit alice1 users2 + mp <- createAddCommit alice1 convId users2 bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "label" `shouldMatch` "mls-stale-message" @@ -663,54 +678,54 @@ testStaleCommit = do testPropInvalidEpoch :: (HasCallStack) => App () testPropInvalidEpoch = do users@[_alice, bob, charlie, dee] <- createAndConnectUsers (replicate 4 OwnDomain) - [alice1, bob1, charlie1, dee1] <- traverse (createMLSClient def) users - void $ createNewGroup alice1 + [alice1, bob1, charlie1, dee1] <- traverse (createMLSClient def def) users + convId <- createNewGroup def alice1 -- Add bob -> epoch 1 - void $ uploadNewKeyPackage bob1 + void $ uploadNewKeyPackage def bob1 gsBackup <- getClientGroupState alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle gsBackup2 <- getClientGroupState alice1 -- try to send a proposal from an old epoch (0) do setClientGroupState alice1 gsBackup - void $ uploadNewKeyPackage dee1 - [prop] <- createAddProposals alice1 [dee] + void $ uploadNewKeyPackage def dee1 + [prop] <- createAddProposals convId alice1 [dee] bindResponse (postMLSMessage alice1 prop.message) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "label" `shouldMatch` "mls-stale-message" -- try to send a proposal from a newer epoch (2) do - void $ uploadNewKeyPackage dee1 - void $ uploadNewKeyPackage charlie1 + void $ uploadNewKeyPackage def dee1 + void $ uploadNewKeyPackage def charlie1 setClientGroupState alice1 gsBackup2 - void $ createAddCommit alice1 [charlie] -- --> epoch 2 - [prop] <- createAddProposals alice1 [dee] + void $ createAddCommit alice1 convId [charlie] -- --> epoch 2 + [prop] <- createAddProposals convId alice1 [dee] bindResponse (postMLSMessage alice1 prop.message) $ \resp -> do resp.status `shouldMatchInt` 409 resp.json %. "label" `shouldMatch` "mls-stale-message" -- remove charlie from users expected to get a welcome message - modifyMLSState $ \mls -> mls {newMembers = mempty} + modifyMLSState $ \mls -> mls {convs = Map.adjust (\conv -> conv {newMembers = mempty}) convId mls.convs} -- alice send a well-formed proposal and commits it - void $ uploadNewKeyPackage dee1 + void $ uploadNewKeyPackage def dee1 setClientGroupState alice1 gsBackup2 - createAddProposals alice1 [dee] >>= traverse_ sendAndConsumeMessage - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + createAddProposals convId alice1 [dee] >>= traverse_ sendAndConsumeMessage + void $ createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundle --- | This test submits a ReInit proposal, which is currently ignored by the -- backend, in order to check that unsupported proposal types are accepted. testPropUnsupported :: (HasCallStack) => App () testPropUnsupported = do users@[_alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) - [alice1, bob1] <- traverse (createMLSClient def) users - void $ uploadNewKeyPackage bob1 - void $ createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + [alice1, bob1] <- traverse (createMLSClient def def) users + void $ uploadNewKeyPackage def bob1 + convId <- createNewGroup def alice1 + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle - mp <- createReInitProposal alice1 + mp <- createReInitProposal convId alice1 -- we cannot consume this message, because the membership tag is fake void $ postMLSMessage mp.sender mp.message >>= getJSON 201 @@ -718,33 +733,33 @@ testPropUnsupported = do testAddUserBareProposalCommit :: (HasCallStack) => App () testAddUserBareProposalCommit = do [alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - (_, qcnv) <- createNewGroup alice1 - void $ uploadNewKeyPackage bob1 - void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + convId <- createNewGroup def alice1 + void $ uploadNewKeyPackage def bob1 + void $ createAddCommit alice1 convId [] >>= sendAndConsumeCommitBundle - createAddProposals alice1 [bob] + createAddProposals convId alice1 [bob] >>= traverse_ sendAndConsumeMessage - commit <- createPendingProposalCommit alice1 + commit <- createPendingProposalCommit convId alice1 void $ assertJust "Expected welcome" commit.welcome void $ sendAndConsumeCommitBundle commit -- check that bob can now see the conversation convs <- getAllConvs bob - convIds <- traverse (%. "qualified_id") convs + convIds <- traverse objConvId convs void $ assertBool "Users added to an MLS group should find it when listing conversations" - (qcnv `elem` convIds) + (convId `elem` convIds) testPropExistingConv :: (HasCallStack) => App () testPropExistingConv = do [alice, bob] <- createAndConnectUsers (replicate 2 OwnDomain) - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - void $ uploadNewKeyPackage bob1 - void $ createNewGroup alice1 - void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle - res <- createAddProposals alice1 [bob] >>= traverse sendAndConsumeMessage >>= assertOne + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + convId <- createNewGroup def alice1 + void $ createAddCommit alice1 convId [] >>= sendAndConsumeCommitBundle + res <- createAddProposals convId alice1 [bob] >>= traverse sendAndConsumeMessage >>= assertOne shouldBeEmpty (res %. "events") -- @SF.Separation @TSFI.RESTfulAPI @S2 @@ -755,20 +770,20 @@ testCommitNotReferencingAllProposals :: (HasCallStack) => App () testCommitNotReferencingAllProposals = do users@[_alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) - [alice1, bob1, charlie1] <- traverse (createMLSClient def) users - void $ createNewGroup alice1 - traverse_ uploadNewKeyPackage [bob1, charlie1] - void $ createAddCommit alice1 [] >>= sendAndConsumeCommitBundle + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) users + convId <- createNewGroup def alice1 + traverse_ (uploadNewKeyPackage def) [bob1, charlie1] + void $ createAddCommit alice1 convId [] >>= sendAndConsumeCommitBundle gsBackup <- getClientGroupState alice1 -- create proposals for bob and charlie - createAddProposals alice1 [bob, charlie] + createAddProposals convId alice1 [bob, charlie] >>= traverse_ sendAndConsumeMessage -- now create a commit referencing only the first proposal setClientGroupState alice1 gsBackup - commit <- createPendingProposalCommit alice1 + commit <- createPendingProposalCommit convId alice1 -- send commit and expect and error bindResponse (postMLSCommitBundle alice1 (mkBundle commit)) $ \resp -> do @@ -779,12 +794,12 @@ testCommitNotReferencingAllProposals = do testUnsupportedCiphersuite :: (HasCallStack) => App () testUnsupportedCiphersuite = do - setMLSCiphersuite (Ciphersuite "0x0003") + let suite = (Ciphersuite "0x0003") alice <- randomUser OwnDomain def - alice1 <- createMLSClient def alice - void $ createNewGroup alice1 + alice1 <- createMLSClient suite def alice + convId <- createNewGroup suite alice1 - mp <- createPendingProposalCommit alice1 + mp <- createPendingProposalCommit convId alice1 bindResponse (postMLSCommitBundle alice1 (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 400 @@ -792,32 +807,35 @@ testUnsupportedCiphersuite = do testBackendRemoveProposal :: (HasCallStack) => Ciphersuite -> Domain -> App () testBackendRemoveProposal suite domain = do - setMLSCiphersuite suite [alice, bob] <- createAndConnectUsers [OwnDomain, domain] - (alice1 : bobClients) <- traverse (createMLSClient def) [alice, bob, bob] - traverse_ uploadNewKeyPackage bobClients - void $ createNewGroup alice1 + (alice1 : bobClients) <- traverse (createMLSClient suite def) [alice, bob, bob] + traverse_ (uploadNewKeyPackage suite) bobClients + convId <- createNewGroup suite alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle let isRemoveProposalFor :: Int -> Value -> App Bool isRemoveProposalFor index e = isNewMLSMessageNotif e &&~ do msgData <- e %. "payload.0.data" & asByteString - msg <- showMessage alice1 msgData + msg <- showMessage suite alice1 msgData fieldEquals msg "message.content.body.Proposal.Remove.removed" index withWebSocket alice1 \ws -> do deleteUser bob for_ (zip [1 ..] bobClients) \(index, _) -> do - void $ consumeMessageWithPredicate (isRemoveProposalFor index) alice1 Nothing ws + void $ consumeMessageWithPredicate (isRemoveProposalFor index) convId suite alice1 Nothing ws bobUser <- asString $ bob %. "id" modifyMLSState $ \mls -> mls - { members = Set.filter (\m -> m.user /= bobUser) mls.members + { convs = + Map.adjust + (\conv -> conv {members = Set.filter (\m -> m.user /= bobUser) conv.members}) + convId + mls.convs } -- alice commits the external proposals - r <- createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + r <- createPendingProposalCommit convId alice1 >>= sendAndConsumeCommitBundle shouldBeEmpty $ r %. "events" diff --git a/integration/test/Test/MLS/KeyPackage.hs b/integration/test/Test/MLS/KeyPackage.hs index 5f95025a1da..2c49f0206d7 100644 --- a/integration/test/Test/MLS/KeyPackage.hs +++ b/integration/test/Test/MLS/KeyPackage.hs @@ -12,8 +12,8 @@ import Testlib.Prelude testDeleteKeyPackages :: App () testDeleteKeyPackages = do alice <- randomUser OwnDomain def - alice1 <- createMLSClient def alice - kps <- replicateM 3 (uploadNewKeyPackage alice1) + alice1 <- createMLSClient def def alice + kps <- replicateM 3 (uploadNewKeyPackage def alice1) -- add an extra non-existing key package to the delete request let kps' = "4B701F521EBE82CEC4AD5CB67FDD8E1C43FC4868DE32D03933CE4993160B75E8" : kps @@ -28,13 +28,12 @@ testDeleteKeyPackages = do testKeyPackageMultipleCiphersuites :: App () testKeyPackageMultipleCiphersuites = do alice <- randomUser OwnDomain def - [alice1, alice2] <- replicateM 2 (createMLSClient def alice) + [alice1, alice2] <- replicateM 2 (createMLSClient def def alice) - kp <- uploadNewKeyPackage alice2 + kp <- uploadNewKeyPackage def alice2 let suite = Ciphersuite "0xf031" - setMLSCiphersuite suite - void $ uploadNewKeyPackage alice2 + void $ uploadNewKeyPackage suite alice2 -- count key packages with default ciphersuite bindResponse (countKeyPackages def alice2) $ \resp -> do @@ -54,9 +53,9 @@ testKeyPackageMultipleCiphersuites = do testKeyPackageUploadNoKey :: App () testKeyPackageUploadNoKey = do alice <- randomUser OwnDomain def - alice1 <- createWireClient alice + alice1 <- createWireClient alice def - (kp, _) <- generateKeyPackage alice1 + (kp, _) <- generateKeyPackage alice1 def -- if we upload a keypackage without a key, -- we get a bad request @@ -73,14 +72,14 @@ testKeyPackageClaim :: App () testKeyPackageClaim = do alice <- randomUser OwnDomain def alices@[alice1, _alice2] <- replicateM 2 do - createMLSClient def alice + createMLSClient def def alice for_ alices \alicei -> replicateM 3 do - uploadNewKeyPackage alicei + uploadNewKeyPackage def alicei bob <- randomUser OwnDomain def bobs <- replicateM 3 do - createMLSClient def bob + createMLSClient def def bob for_ bobs \bobi -> claimKeyPackages def bobi alice `bindResponse` \resp -> do @@ -109,9 +108,9 @@ testKeyPackageSelfClaim :: App () testKeyPackageSelfClaim = do alice <- randomUser OwnDomain def alices@[alice1, alice2] <- replicateM 2 do - createMLSClient def alice + createMLSClient def def alice for_ alices \alicei -> replicateM 3 do - uploadNewKeyPackage alicei + uploadNewKeyPackage def alicei -- claim own keypackages claimKeyPackages def alice1 alice `bindResponse` \resp -> do @@ -133,7 +132,7 @@ testKeyPackageSelfClaim = do bob <- randomUser OwnDomain def bobs <- replicateM 2 do - createMLSClient def bob + createMLSClient def def bob -- skip own should only apply to own keypackages, hence -- bob claiming alices keypackages should work as normal @@ -152,13 +151,13 @@ testKeyPackageSelfClaim = do testKeyPackageRemoteClaim :: App () testKeyPackageRemoteClaim = do alice <- randomUser OwnDomain def - alice1 <- createMLSClient def alice + alice1 <- createMLSClient def def alice charlie <- randomUser OtherDomain def - charlie1 <- createMLSClient def charlie + charlie1 <- createMLSClient def def charlie - refCharlie <- uploadNewKeyPackage charlie1 - refAlice <- uploadNewKeyPackage alice1 + refCharlie <- uploadNewKeyPackage def charlie1 + refAlice <- uploadNewKeyPackage def alice1 -- the user should be able to claim the keypackage of -- a remote user and vice versa @@ -180,30 +179,28 @@ testKeyPackageRemoteClaim = do resp.status `shouldMatchInt` 200 testKeyPackageCount :: (HasCallStack) => Ciphersuite -> App () -testKeyPackageCount cs = do - setMLSCiphersuite cs +testKeyPackageCount suite = do alice <- randomUser OwnDomain def - alice1 <- createMLSClient def alice + alice1 <- createMLSClient suite def alice - bindResponse (countKeyPackages cs alice1) $ \resp -> do + bindResponse (countKeyPackages suite alice1) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "count" `shouldMatchInt` 0 let count = 10 - kps <- map fst <$> replicateM count (generateKeyPackage alice1) + kps <- map fst <$> replicateM count (generateKeyPackage alice1 suite) void $ uploadKeyPackages alice1 kps >>= getBody 201 - bindResponse (countKeyPackages cs alice1) $ \resp -> do + bindResponse (countKeyPackages suite alice1) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "count" `shouldMatchInt` count testUnsupportedCiphersuite :: (HasCallStack) => App () testUnsupportedCiphersuite = do let suite = Ciphersuite "0x0003" - setMLSCiphersuite suite bob <- randomUser OwnDomain def - bob1 <- createMLSClient def bob - (kp, _) <- generateKeyPackage bob1 + bob1 <- createMLSClient suite def bob + (kp, _) <- generateKeyPackage bob1 suite bindResponse (uploadKeyPackages bob1 [kp]) $ \resp -> do resp.status `shouldMatchInt` 400 resp.json %. "label" `shouldMatch` "mls-protocol-error" @@ -211,7 +208,7 @@ testUnsupportedCiphersuite = do testReplaceKeyPackages :: (HasCallStack) => App () testReplaceKeyPackages = do alice <- randomUser OwnDomain def - [alice1, alice2] <- replicateM 2 $ createMLSClient def alice + [alice1, alice2] <- replicateM 2 $ createMLSClient def def alice let suite = Ciphersuite "0xf031" let checkCount cs n = @@ -221,12 +218,11 @@ testReplaceKeyPackages = do -- setup: upload a batch of key packages for each ciphersuite void - $ replicateM 4 (fmap fst (generateKeyPackage alice1)) + $ replicateM 4 (fmap fst (generateKeyPackage alice1 def)) >>= uploadKeyPackages alice1 >>= getBody 201 - setMLSCiphersuite suite void - $ replicateM 5 (fmap fst (generateKeyPackage alice1)) + $ replicateM 5 (fmap fst (generateKeyPackage alice1 suite)) >>= uploadKeyPackages alice1 >>= getBody 201 @@ -235,7 +231,7 @@ testReplaceKeyPackages = do do -- generate a new batch of key packages - (kps, refs) <- unzip <$> replicateM 3 (generateKeyPackage alice1) + (kps, refs) <- unzip <$> replicateM 3 (generateKeyPackage alice1 suite) -- replace old key packages with new void $ replaceKeyPackages alice1 (Just [suite]) kps >>= getBody 201 @@ -261,7 +257,7 @@ testReplaceKeyPackages = do do -- replenish key packages for the second ciphersuite void - $ replicateM 5 (fmap fst (generateKeyPackage alice1)) + $ replicateM 5 (fmap fst (generateKeyPackage alice1 suite)) >>= uploadKeyPackages alice1 >>= getBody 201 @@ -269,10 +265,8 @@ testReplaceKeyPackages = do checkCount suite 5 -- replace all key packages with fresh ones - setMLSCiphersuite def - kps1 <- replicateM 2 (fmap fst (generateKeyPackage alice1)) - setMLSCiphersuite suite - kps2 <- replicateM 2 (fmap fst (generateKeyPackage alice1)) + kps1 <- replicateM 2 (fmap fst (generateKeyPackage alice1 def)) + kps2 <- replicateM 2 (fmap fst (generateKeyPackage alice1 suite)) void $ replaceKeyPackages alice1 (Just [def, suite]) (kps1 <> kps2) >>= getBody 201 @@ -280,10 +274,8 @@ testReplaceKeyPackages = do checkCount suite 2 do - setMLSCiphersuite def - defKeyPackages <- replicateM 3 (fmap fst (generateKeyPackage alice1)) - setMLSCiphersuite suite - suiteKeyPackages <- replicateM 3 (fmap fst (generateKeyPackage alice1)) + defKeyPackages <- replicateM 3 (fmap fst (generateKeyPackage alice1 def)) + suiteKeyPackages <- replicateM 3 (fmap fst (generateKeyPackage alice1 suite)) void $ replaceKeyPackages alice1 (Just []) [] diff --git a/integration/test/Test/MLS/Message.hs b/integration/test/Test/MLS/Message.hs index 81a194d3674..47708a53984 100644 --- a/integration/test/Test/MLS/Message.hs +++ b/integration/test/Test/MLS/Message.hs @@ -42,59 +42,59 @@ testApplicationMessage = do clients@[alice1, _alice2, alex1, _alex2, bob1, _bob2, _, _] <- traverse - (createMLSClient def) + (createMLSClient def def) [alice, alice, alex, alex, bob, bob, betty, betty] - traverse_ uploadNewKeyPackage clients - void $ createNewGroup alice1 + traverse_ (uploadNewKeyPackage def) clients + convId <- createNewGroup def alice1 withWebSockets [alice, alex, bob, betty] $ \wss -> do -- alice adds all other users (including her own client) - void $ createAddCommit alice1 [alice, alex, bob, betty] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [alice, alex, bob, betty] >>= sendAndConsumeCommitBundle traverse_ (awaitMatch isMemberJoinNotif) wss -- alex sends a message - void $ createApplicationMessage alex1 "hello" >>= sendAndConsumeMessage + void $ createApplicationMessage convId alex1 "hello" >>= sendAndConsumeMessage traverse_ (awaitMatch isNewMLSMessageNotif) wss -- bob sends a message - void $ createApplicationMessage bob1 "hey" >>= sendAndConsumeMessage + void $ createApplicationMessage convId bob1 "hey" >>= sendAndConsumeMessage traverse_ (awaitMatch isNewMLSMessageNotif) wss -- @END testAppMessageSomeReachable :: (HasCallStack) => App () testAppMessageSomeReachable = do - alice1 <- startDynamicBackends [mempty] $ \[thirdDomain] -> do + (alice1, convId) <- startDynamicBackends [mempty] $ \[thirdDomain] -> do ownDomain <- make OwnDomain & asString otherDomain <- make OtherDomain & asString [alice, bob, charlie] <- createAndConnectUsers [ownDomain, otherDomain, thirdDomain] - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - traverse_ uploadNewKeyPackage [bob1, charlie1] - void $ createNewGroup alice1 + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) [alice, bob, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1] + convId <- createNewGroup def alice1 void $ withWebSocket charlie $ \ws -> do - void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob, charlie] >>= sendAndConsumeCommitBundle awaitMatch isMemberJoinNotif ws - pure alice1 + pure (alice1, convId) -- charlie isn't able to receive this message, so we make sure we can post it -- successfully, but not attempt to consume it - mp <- createApplicationMessage alice1 "hi, bob!" + mp <- createApplicationMessage convId alice1 "hi, bob!" void $ postMLSMessage mp.sender mp.message >>= getJSON 201 testMessageNotifications :: (HasCallStack) => Domain -> App () testMessageNotifications bobDomain = do [alice, bob] <- createAndConnectUsers [OwnDomain, bobDomain] - [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def) [alice, alice, bob, bob] + [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def def) [alice, alice, bob, bob] bobClient <- bob1 %. "client_id" & asString - traverse_ uploadNewKeyPackage [alice1, alice2, bob1, bob2] + traverse_ (uploadNewKeyPackage def) [alice1, alice2, bob1, bob2] - void $ createNewGroup alice1 + convId <- createNewGroup def alice1 void $ withWebSocket bob $ \ws -> do - void $ createAddCommit alice1 [alice, bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [alice, bob] >>= sendAndConsumeCommitBundle awaitMatch isMemberJoinNotif ws let get (opts :: GetNotifications) = do @@ -106,7 +106,7 @@ testMessageNotifications bobDomain = do numNotifsClient <- get def {client = Just bobClient} void $ withWebSocket bob $ \ws -> do - void $ createApplicationMessage alice1 "hi bob" >>= sendAndConsumeMessage + void $ createApplicationMessage convId alice1 "hi bob" >>= sendAndConsumeMessage awaitMatch isNewMLSMessageNotif ws get def `shouldMatchInt` (numNotifs + 1) @@ -115,16 +115,16 @@ testMessageNotifications bobDomain = do testMultipleMessages :: (HasCallStack) => App () testMultipleMessages = do [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [alice1, bob1] - void $ createNewGroup alice1 + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + traverse_ (uploadNewKeyPackage def) [alice1, bob1] + convId <- createNewGroup def alice1 withWebSockets [bob] $ \wss -> do - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle traverse_ (awaitMatch isMemberJoinNotif) wss - void $ createApplicationMessage alice1 "hello" >>= sendAndConsumeMessage + void $ createApplicationMessage convId alice1 "hello" >>= sendAndConsumeMessage traverse_ (awaitMatch isNewMLSMessageNotif) wss - void $ createApplicationMessage alice1 "world" >>= sendAndConsumeMessage + void $ createApplicationMessage convId alice1 "world" >>= sendAndConsumeMessage traverse_ (awaitMatch isNewMLSMessageNotif) wss diff --git a/integration/test/Test/MLS/Notifications.hs b/integration/test/Test/MLS/Notifications.hs index 61a0b60d53f..a75d72276d2 100644 --- a/integration/test/Test/MLS/Notifications.hs +++ b/integration/test/Test/MLS/Notifications.hs @@ -9,12 +9,12 @@ import Testlib.Prelude testWelcomeNotification :: (HasCallStack) => App () testWelcomeNotification = do [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] - [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def) [alice, alice, bob, bob] - traverse_ uploadNewKeyPackage [alice2, bob1, bob2] + [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def def) [alice, alice, bob, bob] + traverse_ (uploadNewKeyPackage def) [alice2, bob1, bob2] - void $ createNewGroup alice1 + convId <- createNewGroup def alice1 notif <- withWebSocket bob $ \ws -> do - void $ createAddCommit alice1 [alice, bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [alice, bob] >>= sendAndConsumeCommitBundle awaitMatch isWelcomeNotif ws notifId <- notif %. "id" & asString diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index d93e5f582c2..660368b7660 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -23,6 +23,7 @@ import Control.Concurrent.Async import Control.Concurrent.MVar import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Char8 as B8 +import qualified Data.Map as Map import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.Read as T @@ -116,16 +117,17 @@ testMLSOne2OneOtherMember scenario = do convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConvId <- objConvId $ one2OneConv %. "conversation" do convId <- one2OneConv %. "conversation.qualified_id" bobOne2OneConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 convId `shouldMatch` (bobOne2OneConv %. "conversation.qualified_id") - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] - resetOne2OneGroup alice1 one2OneConv + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + resetOne2OneGroup def alice1 one2OneConv withWebSocket bob1 $ \ws -> do - commit <- createAddCommit alice1 [bob] + commit <- createAddCommit alice1 one2OneConvId [bob] void $ sendAndConsumeCommitBundle commit let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" n <- awaitMatch isMessage ws @@ -151,11 +153,12 @@ testMLSOne2OneRemoveClientLocalV5 = withVersion5 Version5 $ do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] conv <- getMLSOne2OneConversationLegacy alice bob >>= getJSON 200 - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] - resetGroup alice1 conv + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + convId <- objConvId conv + createGroup def alice1 convId - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle withWebSocket alice $ \wsAlice -> do _ <- deleteClient bob bob1.client >>= getBody 200 @@ -167,9 +170,9 @@ testMLSOne2OneRemoveClientLocalV5 = withVersion5 Version5 $ do mlsMsg <- asByteString (nPayload n %. "data") -- Checks that the remove proposal is consumable by alice - void $ mlsCliConsume alice1 mlsMsg + void $ mlsCliConsume convId def alice1 mlsMsg - parsedMsg <- showMessage alice1 mlsMsg + parsedMsg <- showMessage def alice1 mlsMsg let leafIndexBob = 1 -- msg `shouldMatch` "foo" parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob @@ -198,15 +201,16 @@ testMLSOne2OneBlockedAfterConnected scenario = do convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConvId <- objConvId $ one2OneConv %. "conversation" convId <- one2OneConv %. "conversation.qualified_id" do bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 convId `shouldMatch` (bobConv %. "conversation.qualified_id") - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] - resetOne2OneGroup alice1 one2OneConv - commit <- createAddCommit alice1 [bob] + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + resetOne2OneGroup def alice1 one2OneConv + commit <- createAddCommit alice1 one2OneConvId [bob] withWebSocket bob1 $ \ws -> do void $ sendAndConsumeCommitBundle commit let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" @@ -223,7 +227,7 @@ testMLSOne2OneBlockedAfterConnected scenario = do -- Bob. void $ getMLSOne2OneConversation alice bob >>= getJSON 403 - mp <- createApplicationMessage bob1 "hello, world, again" + mp <- createApplicationMessage one2OneConvId bob1 "hello, world, again" withWebSocket alice1 $ \ws -> do void $ postMLSMessage mp.sender mp.message >>= getJSON 201 awaitAnyEvent 2 ws `shouldMatch` (Nothing :: Maybe Value) @@ -237,16 +241,17 @@ testMLSOne2OneUnblocked scenario = do convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConvId <- objConvId $ one2OneConv %. "conversation" do convId <- one2OneConv %. "conversation.qualified_id" bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 convId `shouldMatch` (bobConv %. "conversation.qualified_id") - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] - resetOne2OneGroup alice1 one2OneConv + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + resetOne2OneGroup def alice1 one2OneConv withWebSocket bob1 $ \ws -> do - commit <- createAddCommit alice1 [bob] + commit <- createAddCommit alice1 one2OneConvId [bob] void $ sendAndConsumeCommitBundle commit let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" n <- awaitMatch isMessage ws @@ -259,24 +264,24 @@ testMLSOne2OneUnblocked scenario = do -- Reset the group membership in the test setup as only 'bob1' is left in -- reality, even though the test state believes 'alice1' is still part of the -- conversation. - modifyMLSState $ \s -> s {members = Set.singleton bob1} + modifyMLSState $ \s -> s {convs = Map.adjust (\conv -> conv {members = Set.singleton bob1}) one2OneConvId s.convs} -- Bob creates a new client and adds it to the one-to-one conversation just so -- that the epoch advances. - bob2 <- createMLSClient def bob - traverse_ uploadNewKeyPackage [bob2] - void $ createAddCommit bob1 [bob] >>= sendAndConsumeCommitBundle + bob2 <- createMLSClient def def bob + void $ uploadNewKeyPackage def bob2 + void $ createAddCommit bob1 one2OneConvId [bob] >>= sendAndConsumeCommitBundle -- Alice finally unblocks Bob void $ putConnection alice bob "accepted" >>= getBody 200 void $ getMLSOne2OneConversation alice bob >>= getJSON 200 -- Alice rejoins via an external commit - void $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit one2OneConvId alice1 Nothing >>= sendAndConsumeCommitBundle -- Check that an application message can get to Bob withWebSockets [bob1, bob2] $ \wss -> do - mp <- createApplicationMessage alice1 "hello, I've always been here" + mp <- createApplicationMessage one2OneConvId alice1 "hello, I've always been here" void $ sendAndConsumeMessage mp let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" forM_ wss $ \ws -> do @@ -316,18 +321,18 @@ one2OneScenarioConvDomain One2OneScenarioRemoteConv = OtherDomain testMLSOne2One :: (HasCallStack) => Ciphersuite -> One2OneScenario -> App () testMLSOne2One suite scenario = do - setMLSCiphersuite suite alice <- randomUser OwnDomain def let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] + [alice1, bob1] <- traverse (createMLSClient suite def) [alice, bob] + void $ uploadNewKeyPackage suite bob1 one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetOne2OneGroup alice1 one2OneConv + one2OneConvId <- objConvId $ one2OneConv %. "conversation" + resetOne2OneGroup suite alice1 one2OneConv - commit <- createAddCommit alice1 [bob] + commit <- createAddCommit alice1 one2OneConvId [bob] withWebSocket bob1 $ \ws -> do void $ sendAndConsumeCommitBundle commit @@ -338,7 +343,7 @@ testMLSOne2One suite scenario = do void $ awaitMatch isMemberJoinNotif ws withWebSocket bob1 $ \ws -> do - mp <- createApplicationMessage alice1 "hello, world" + mp <- createApplicationMessage one2OneConvId alice1 "hello, world" void $ sendAndConsumeMessage mp let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" n <- awaitMatch isMessage ws @@ -346,7 +351,7 @@ testMLSOne2One suite scenario = do -- Send another commit. This verifies that the backend has correctly updated -- the cipersuite of this conversation. - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + void $ createPendingProposalCommit one2OneConvId alice1 >>= sendAndConsumeCommitBundle one2OneConv' <- getMLSOne2OneConversation alice bob >>= getJSON 200 (suiteCode, _) <- assertOne $ T.hexadecimal (T.pack suite.code) @@ -360,10 +365,11 @@ testMLSOne2One suite scenario = do testMLSGhostOne2OneConv :: App () testMLSGhostOne2OneConv = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] - traverse_ uploadNewKeyPackage [bob1, bob2] + [alice1, bob1, bob2] <- traverse (createMLSClient def def) [alice, bob, bob] + traverse_ (uploadNewKeyPackage def) [bob1, bob2] one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetOne2OneGroup alice1 one2OneConv + one2OneConvId <- objConvId $ one2OneConv %. "conversation" + resetOne2OneGroup def alice1 one2OneConv doneVar <- liftIO $ newEmptyMVar let checkConversation = @@ -379,7 +385,7 @@ testMLSGhostOne2OneConv = do createCommit <- appToIO $ void - $ createAddCommit alice1 [bob] + $ createAddCommit alice1 one2OneConvId [bob] >>= sendAndConsumeCommitBundle liftIO $ withAsync checkConversationIO $ \a -> do @@ -409,8 +415,8 @@ testMLSFederationV1ConvOnOldBackend = do else createBob bob <- createBob - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [alice1] + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def alice1 -- Alice cannot start this conversation because it would exist on Bob's -- backend and Alice cannot get the MLS public keys of that backend. @@ -419,11 +425,12 @@ testMLSFederationV1ConvOnOldBackend = do fedError %. "label" `shouldMatch` "federation-version-error" conv <- getMLSOne2OneConversationLegacy bob alice >>= getJSON 200 + convId <- objConvId conv keys <- getMLSPublicKeys bob >>= getJSON 200 - resetOne2OneGroupGeneric bob1 conv keys + resetOne2OneGroupGeneric def bob1 conv keys withWebSocket alice1 $ \wsAlice -> do - commit <- createAddCommit bob1 [alice] + commit <- createAddCommit bob1 convId [alice] void $ sendAndConsumeCommitBundle commit let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" @@ -441,9 +448,9 @@ testMLSFederationV1ConvOnOldBackend = do mlsMsg <- asByteString (nPayload n %. "data") -- Checks that the remove proposal is consumable by bob - void $ mlsCliConsume bob1 mlsMsg + void $ mlsCliConsume convId def bob1 mlsMsg - parsedMsg <- showMessage bob1 mlsMsg + parsedMsg <- showMessage def bob1 mlsMsg let leafIndexAlice = 1 parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexAlice parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 @@ -463,8 +470,8 @@ testMLSFederationV1ConvOnNewBackend = do else createBob bob <- createBob - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 -- Bob cannot start this conversation because it would exist on Alice's -- backend and Bob cannot get the MLS public keys of that backend. @@ -473,11 +480,12 @@ testMLSFederationV1ConvOnNewBackend = do fedError %. "label" `shouldMatch` "federation-remote-error" one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConvId <- objConvId $ one2OneConv %. "conversation" conv <- one2OneConv %. "conversation" - resetOne2OneGroup alice1 one2OneConv + resetOne2OneGroup def alice1 one2OneConv withWebSocket bob1 $ \wsBob -> do - commit <- createAddCommit alice1 [bob] + commit <- createAddCommit alice1 one2OneConvId [bob] void $ sendAndConsumeCommitBundle commit let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" @@ -495,9 +503,9 @@ testMLSFederationV1ConvOnNewBackend = do mlsMsg <- asByteString (nPayload n %. "data") -- Checks that the remove proposal is consumable by bob - void $ mlsCliConsume alice1 mlsMsg + void $ mlsCliConsume one2OneConvId def alice1 mlsMsg - parsedMsg <- showMessage alice1 mlsMsg + parsedMsg <- showMessage def alice1 mlsMsg let leafIndexBob = 1 parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index 83c5376edf3..40cf1e66bf0 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -3,6 +3,7 @@ module Test.MLS.SubConversation where import API.Galley import Control.Monad.Trans (lift) import Control.Monad.Trans.Maybe (MaybeT (runMaybeT)) +import qualified Data.Map as Map import qualified Data.Set as Set import MLS.Util import Notifications @@ -13,44 +14,47 @@ import Testlib.Prelude testJoinSubConv :: App () testJoinSubConv = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] - traverse_ uploadNewKeyPackage [bob1, bob2] - (_, qcnv) <- createNewGroup alice1 + [alice1, bob1, bob2] <- traverse (createMLSClient def def) [alice, bob, bob] + traverse_ (uploadNewKeyPackage def) [bob1, bob2] + convId <- createNewGroup def alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - createSubConv bob1 "conference" + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle + void $ createSubConv def convId bob1 "conference" -- bob adds his first client to the subconversation - sub' <- getSubConversation bob qcnv "conference" >>= getJSON 200 + sub' <- getSubConversation bob convId "conference" >>= getJSON 200 + subConvId <- objConvId sub' do tm <- sub' %. "epoch_timestamp" assertBool "Epoch timestamp should not be null" (tm /= Null) -- now alice joins with her own client void - $ createExternalCommit alice1 Nothing + $ createExternalCommit subConvId alice1 Nothing >>= sendAndConsumeCommitBundle testJoinOne2OneSubConv :: App () testJoinOne2OneSubConv = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] - [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] - traverse_ uploadNewKeyPackage [bob1, bob2] + [alice1, bob1, bob2] <- traverse (createMLSClient def def) [alice, bob, bob] + traverse_ (uploadNewKeyPackage def) [bob1, bob2] one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetOne2OneGroup alice1 one2OneConv + one2OneConvId <- objConvId (one2OneConv %. "conversation") + resetOne2OneGroup def alice1 one2OneConv - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - createOne2OneSubConv bob1 "conference" (one2OneConv %. "public_keys") + void $ createAddCommit alice1 one2OneConvId [bob] >>= sendAndConsumeCommitBundle + createOne2OneSubConv def one2OneConvId bob1 "conference" (one2OneConv %. "public_keys") -- bob adds his first client to the subconversation - sub' <- getSubConversation bob (one2OneConv %. "conversation") "conference" >>= getJSON 200 + sub' <- getSubConversation bob one2OneConvId "conference" >>= getJSON 200 + subConvId <- objConvId sub' do tm <- sub' %. "epoch_timestamp" assertBool "Epoch timestamp should not be null" (tm /= Null) -- now alice joins with her own client void - $ createExternalCommit alice1 Nothing + $ createExternalCommit subConvId alice1 Nothing >>= sendAndConsumeCommitBundle testLeaveOne2OneSubConv :: One2OneScenario -> Leaver -> App () @@ -60,15 +64,18 @@ testLeaveOne2OneSubConv scenario leaver = do let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [bob1] + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetOne2OneGroup alice1 one2OneConv - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + one2OneConvId <- objConvId $ one2OneConv %. "conversation" + resetOne2OneGroup def alice1 one2OneConv + void $ createAddCommit alice1 one2OneConvId [bob] >>= sendAndConsumeCommitBundle -- create and join subconversation - createOne2OneSubConv alice1 "conference" (one2OneConv %. "public_keys") - void $ createExternalCommit bob1 Nothing >>= sendAndConsumeCommitBundle + createOne2OneSubConv def one2OneConvId alice1 "conference" (one2OneConv %. "public_keys") + subConvId <- getSubConvId bob one2OneConvId "conference" + + void $ createExternalCommit subConvId bob1 Nothing >>= sendAndConsumeCommitBundle -- one of the two clients leaves let (leaverClient, leaverIndex, remainingClient) = case leaver of @@ -76,14 +83,13 @@ testLeaveOne2OneSubConv scenario leaver = do Bob -> (bob1, 1, alice1) withWebSocket remainingClient $ \ws -> do - leaveCurrentConv leaverClient - - msg <- consumeMessage remainingClient Nothing ws + leaveConv subConvId leaverClient + msg <- consumeMessage subConvId def remainingClient Nothing ws msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leaverIndex msg %. "message.content.sender.External" `shouldMatchInt` 0 -- the other client commits the pending proposal - void $ createPendingProposalCommit remainingClient >>= sendAndConsumeCommitBundle + void $ createPendingProposalCommit subConvId remainingClient >>= sendAndConsumeCommitBundle testDeleteParentOfSubConv :: (HasCallStack) => Domain -> App () testDeleteParentOfSubConv secondDomain = do @@ -91,38 +97,39 @@ testDeleteParentOfSubConv secondDomain = do bob <- randomUser secondDomain def connectUsers [alice, bob] - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - traverse_ uploadNewKeyPackage [alice1, bob1] - (_, qcnv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + traverse_ (uploadNewKeyPackage def) [alice1, bob1] + convId <- createNewGroup def alice1 + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle -- bob creates a subconversation and adds his own client - createSubConv bob1 "conference" + createSubConv def convId bob1 "conference" + subConvId <- getSubConvId bob convId "conference" -- alice joins with her own client - void $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId alice1 Nothing >>= sendAndConsumeCommitBundle -- bob sends a message to the subconversation do - mp <- createApplicationMessage bob1 "hello, alice" + mp <- createApplicationMessage subConvId bob1 "hello, alice" void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 201 -- alice sends a message to the subconversation do - mp <- createApplicationMessage bob1 "hello, bob" + mp <- createApplicationMessage subConvId bob1 "hello, bob" void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 201 -- alice deletes main conversation withWebSocket bob $ \ws -> do - void . bindResponse (deleteTeamConv tid qcnv alice) $ \resp -> do + void . bindResponse (deleteTeamConv tid (convIdToQidObject convId) alice) $ \resp -> do resp.status `shouldMatchInt` 200 void $ awaitMatch isConvDeleteNotif ws -- bob fails to send a message to the subconversation do - mp <- createApplicationMessage bob1 "hello, alice" + mp <- createApplicationMessage subConvId bob1 "hello, alice" void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 404 case secondDomain of @@ -131,7 +138,7 @@ testDeleteParentOfSubConv secondDomain = do -- alice fails to send a message to the subconversation do - mp <- createApplicationMessage alice1 "hello, bob" + mp <- createApplicationMessage subConvId alice1 "hello, bob" void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 404 resp.json %. "label" `shouldMatch` "no-conversation" @@ -140,21 +147,21 @@ testDeleteSubConversation :: (HasCallStack) => Domain -> App () testDeleteSubConversation otherDomain = do [alice, bob] <- createAndConnectUsers [OwnDomain, otherDomain] charlie <- randomUser OwnDomain def - [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] - void $ uploadNewKeyPackage bob1 - (_, qcnv) <- createNewGroup alice1 - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + [alice1, bob1] <- traverse (createMLSClient def def) [alice, bob] + void $ uploadNewKeyPackage def bob1 + convId <- createNewGroup def alice1 + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle - createSubConv alice1 "conference1" - sub1 <- getSubConversation alice qcnv "conference1" >>= getJSON 200 + createSubConv def convId alice1 "conference1" + sub1 <- getSubConversation alice convId "conference1" >>= getJSON 200 void $ deleteSubConversation charlie sub1 >>= getBody 403 void $ deleteSubConversation alice sub1 >>= getBody 200 - createSubConv alice1 "conference2" - sub2 <- getSubConversation alice qcnv "conference2" >>= getJSON 200 + createSubConv def convId alice1 "conference2" + sub2 <- getSubConversation alice convId "conference2" >>= getJSON 200 void $ deleteSubConversation bob sub2 >>= getBody 200 - sub2' <- getSubConversation alice1 qcnv "conference2" >>= getJSON 200 + sub2' <- getSubConversation alice1 convId "conference2" >>= getJSON 200 sub2 `shouldNotMatch` sub2' data Leaver = Alice | Bob @@ -163,18 +170,19 @@ data Leaver = Alice | Bob testLeaveSubConv :: (HasCallStack) => Leaver -> App () testLeaveSubConv leaver = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] - clients@[alice1, bob1, bob2, charlie1] <- traverse (createMLSClient def) [alice, bob, bob, charlie] - traverse_ uploadNewKeyPackage [bob1, bob2, charlie1] - void $ createNewGroup alice1 + clients@[alice1, bob1, bob2, charlie1] <- traverse (createMLSClient def def) [alice, bob, bob, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, bob2, charlie1] + convId <- createNewGroup def alice1 withWebSockets [bob, charlie] $ \wss -> do - void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob, charlie] >>= sendAndConsumeCommitBundle traverse_ (awaitMatch isMemberJoinNotif) wss - createSubConv bob1 "conference" - void $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle - void $ createExternalCommit bob2 Nothing >>= sendAndConsumeCommitBundle - void $ createExternalCommit charlie1 Nothing >>= sendAndConsumeCommitBundle + createSubConv def convId bob1 "conference" + subConvId <- getSubConvId bob convId "conference" + void $ createExternalCommit subConvId alice1 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId bob2 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId charlie1 Nothing >>= sendAndConsumeCommitBundle -- a member leaves the subconversation let (firstLeaver, idxFirstLeaver) = case leaver of @@ -184,150 +192,163 @@ testLeaveSubConv leaver = do let others = filter (/= firstLeaver) clients withWebSockets others $ \wss -> do - leaveCurrentConv firstLeaver + leaveConv subConvId firstLeaver for_ (zip others wss) $ \(cid, ws) -> do - msg <- consumeMessage cid Nothing ws + msg <- consumeMessage subConvId def cid Nothing ws msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` idxFirstLeaver msg %. "message.content.sender.External" `shouldMatchInt` 0 withWebSockets (tail others) $ \wss -> do -- a member commits the pending proposal - void $ createPendingProposalCommit (head others) >>= sendAndConsumeCommitBundle + void $ createPendingProposalCommit subConvId (head others) >>= sendAndConsumeCommitBundle traverse_ (awaitMatch isNewMLSMessageNotif) wss -- send an application message - void $ createApplicationMessage (head others) "good riddance" >>= sendAndConsumeMessage + void $ createApplicationMessage subConvId (head others) "good riddance" >>= sendAndConsumeMessage traverse_ (awaitMatch isNewMLSMessageNotif) wss -- check that only 3 clients are left in the subconv do - conv <- getCurrentConv (head others) + conv <- getConv subConvId (head others) mems <- conv %. "members" & asList length mems `shouldMatchInt` 3 -- charlie1 leaves let others' = filter (/= charlie1) others withWebSockets others' $ \wss -> do - leaveCurrentConv charlie1 + leaveConv subConvId charlie1 for_ (zip others' wss) $ \(cid, ws) -> do - msg <- consumeMessage cid Nothing ws + msg <- consumeMessage subConvId def cid Nothing ws msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` idxCharlie1 msg %. "message.content.sender.External" `shouldMatchInt` 0 -- a member commits the pending proposal - void $ createPendingProposalCommit (head others') >>= sendAndConsumeCommitBundle + void $ createPendingProposalCommit subConvId (head others') >>= sendAndConsumeCommitBundle -- check that only 2 clients are left in the subconv do - conv <- getCurrentConv (head others) + conv <- getConv subConvId (head others) mems <- conv %. "members" & asList length mems `shouldMatchInt` 2 testCreatorRemovesUserFromParent :: App () testCreatorRemovesUserFromParent = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] - [alice1, bob1, bob2, charlie1, charlie2] <- traverse (createMLSClient def) [alice, bob, bob, charlie, charlie] - traverse_ uploadNewKeyPackage [bob1, bob2, charlie1, charlie2] - (_, qcnv) <- createNewGroup alice1 - - _ <- createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle - - -- save the state of the parent group - parentState <- getMLSState - -- switch to the subgroup - let subConvName = "conference" - createSubConv alice1 subConvName - - for_ [bob1, bob2, charlie1, charlie2] \c -> - createExternalCommit c Nothing >>= sendAndConsumeCommitBundle - -- save the state of the subgroup and switch to the parent context - childState <- getMLSState <* setMLSState parentState - withWebSockets [alice1, charlie1, charlie2] \wss -> do - removeCommitEvents <- createRemoveCommit alice1 [bob1, bob2] >>= sendAndConsumeCommitBundle - modifyMLSState $ \s -> s {members = s.members Set.\\ Set.fromList [bob1, bob2]} - - removeCommitEvents %. "events.0.type" `shouldMatch` "conversation.member-leave" - removeCommitEvents %. "events.0.data.reason" `shouldMatch` "removed" - removeCommitEvents %. "events.0.from" `shouldMatch` alice1.user - - for_ wss \ws -> do - n <- awaitMatch isConvLeaveNotif ws - n %. "payload.0.data.reason" `shouldMatch` "removed" - n %. "payload.0.from" `shouldMatch` alice1.user - - setMLSState childState - let idxBob1 :: Int = 1 - idxBob2 :: Int = 2 - for_ ((,) <$> [idxBob1, idxBob2] <*> wss) \(idx, ws) -> do - msg <- - awaitMatch - do - \n -> - isJust <$> runMaybeT do - msg <- lift $ n %. "payload.0.data" & asByteString >>= showMessage alice1 - guard =<< lift do - isNewMLSMessageNotif n - - prop <- - maybe mzero pure =<< lift do - lookupField msg "message.content.body.Proposal" - - lift do - (== idx) <$> (prop %. "Remove.removed" & asInt) - ws - for_ ws.client $ \consumer -> - msg %. "payload.0.data" & asByteString >>= mlsCliConsume consumer - - -- remove bob from the child state - modifyMLSState $ \s -> s {members = s.members Set.\\ Set.fromList [bob1, bob2]} - - _ <- createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle - - getSubConversation bob qcnv subConvName >>= flip withResponse \resp -> - assertBool "access to the conversation for bob should be denied" (resp.status == 403) - - for_ [charlie, alice] \m -> do - resp <- getSubConversation m qcnv subConvName - assertBool "alice and charlie should have access to the conversation" (resp.status == 200) - mems <- resp.jsonBody %. "members" & asList - mems `shouldMatchSet` ((renameField "id" "user_id" <=< make) `traverse` [alice1, charlie1, charlie2]) + addUsersToFailureContext [("alice", alice), ("bob", bob), ("charlie", charlie)] $ do + [alice1, bob1, bob2, charlie1, charlie2] <- traverse (createMLSClient def def) [alice, bob, bob, charlie, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, bob2, charlie1, charlie2] + convId <- createNewGroup def alice1 + + _ <- createAddCommit alice1 convId [bob, charlie] >>= sendAndConsumeCommitBundle + + -- save the state of the parent group + let subConvName = "conference" + createSubConv def convId alice1 subConvName + subConvId <- getSubConvId alice convId "conference" + + for_ [bob1, bob2, charlie1, charlie2] \c -> + createExternalCommit subConvId c Nothing >>= sendAndConsumeCommitBundle + + withWebSockets [alice1, charlie1, charlie2] \wss -> do + removeCommitEvents <- createRemoveCommit alice1 convId [bob1, bob2] >>= sendAndConsumeCommitBundle + modifyMLSState $ \s -> + s + { convs = + Map.adjust + (\conv -> conv {members = conv.members Set.\\ Set.fromList [bob1, bob2]}) + convId + s.convs + } + + removeCommitEvents %. "events.0.type" `shouldMatch` "conversation.member-leave" + removeCommitEvents %. "events.0.data.reason" `shouldMatch` "removed" + removeCommitEvents %. "events.0.from" `shouldMatch` alice1.user + + for_ wss \ws -> do + n <- awaitMatch isConvLeaveNotif ws + n %. "payload.0.data.reason" `shouldMatch` "removed" + n %. "payload.0.from" `shouldMatch` alice1.user + + let idxBob1 :: Int = 1 + idxBob2 :: Int = 2 + for_ ((,) <$> [idxBob1, idxBob2] <*> wss) \(idx, ws) -> do + msg <- + awaitMatch + do + \n -> + isJust <$> runMaybeT do + msg <- lift $ n %. "payload.0.data" & asByteString >>= showMessage def alice1 + guard =<< lift do + isNewMLSMessageNotif n + + prop <- + maybe mzero pure =<< lift do + lookupField msg "message.content.body.Proposal" + + lift do + (== idx) <$> (prop %. "Remove.removed" & asInt) + ws + for_ ws.client $ \consumer -> + msg %. "payload.0.data" & asByteString >>= mlsCliConsume subConvId def consumer + + -- remove bob from the child state + modifyMLSState $ \s -> + s + { convs = + Map.adjust + (\conv -> conv {members = conv.members Set.\\ Set.fromList [bob1, bob2]}) + subConvId + s.convs + } + + _ <- createPendingProposalCommit subConvId alice1 >>= sendAndConsumeCommitBundle + + getSubConversation bob convId subConvName >>= flip withResponse \resp -> + assertBool "access to the conversation for bob should be denied" (resp.status == 403) + + for_ [charlie, alice] \m -> do + resp <- getSubConversation m convId subConvName + assertBool "alice and charlie should have access to the conversation" (resp.status == 200) + mems <- resp.jsonBody %. "members" & asList + mems `shouldMatchSet` ((renameField "id" "user_id" <=< make) `traverse` [alice1, charlie1, charlie2]) testResendingProposals :: (HasCallStack) => App () testResendingProposals = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] [alice1, alice2, bob1, bob2, bob3, charlie1] <- traverse - (createMLSClient def) + (createMLSClient def def) [alice, alice, bob, bob, bob, charlie] - traverse_ uploadNewKeyPackage [alice2, bob1, bob2, bob3, charlie1] + traverse_ (uploadNewKeyPackage def) [alice2, bob1, bob2, bob3, charlie1] - (_, conv) <- createNewGroup alice1 - void $ createAddCommit alice1 [alice, bob, charlie] >>= sendAndConsumeCommitBundle + conv <- createNewGroup def alice1 + void $ createAddCommit alice1 conv [alice, bob, charlie] >>= sendAndConsumeCommitBundle - createSubConv alice1 "conference" + createSubConv def conv alice1 "conference" + subConvId <- getSubConvId alice conv "conference" - void $ createExternalCommit alice2 Nothing >>= sendAndConsumeCommitBundle - void $ createExternalCommit bob1 Nothing >>= sendAndConsumeCommitBundle - void $ createExternalCommit bob2 Nothing >>= sendAndConsumeCommitBundle - void $ createExternalCommit bob3 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId alice2 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId bob1 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId bob2 Nothing >>= sendAndConsumeCommitBundle + void $ createExternalCommit subConvId bob3 Nothing >>= sendAndConsumeCommitBundle - leaveCurrentConv bob1 - leaveCurrentConv bob2 - leaveCurrentConv bob3 + leaveConv subConvId bob1 + leaveConv subConvId bob2 + leaveConv subConvId bob3 - mls <- getMLSState - withWebSockets (charlie1 : toList mls.members) \wss -> do - void $ createExternalCommit charlie1 Nothing >>= sendAndConsumeCommitBundle + subConv <- getMLSConv subConvId + withWebSockets (charlie1 : toList subConv.members) \wss -> do + void $ createExternalCommit subConvId charlie1 Nothing >>= sendAndConsumeCommitBundle -- consume proposals after backend resends them for_ wss \ws -> do replicateM 3 do - msg <- consumeMessage (fromJust ws.client) Nothing ws + msg <- consumeMessage subConvId def (fromJust ws.client) Nothing ws msg %. "message.content.sender.External" `shouldMatchInt` 0 - void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle + void $ createPendingProposalCommit subConvId alice1 >>= sendAndConsumeCommitBundle sub <- getSubConversation alice1 conv "conference" >>= getJSON 200 let members = diff --git a/integration/test/Test/MLS/Unreachable.hs b/integration/test/Test/MLS/Unreachable.hs index 4e32d293508..17bd296650a 100644 --- a/integration/test/Test/MLS/Unreachable.hs +++ b/integration/test/Test/MLS/Unreachable.hs @@ -33,13 +33,13 @@ testAddUsersSomeReachable = do otherDomain <- make OtherDomain & asString [alice, bob, charlie] <- createAndConnectUsers [ownDomain, otherDomain, thirdDomain] - [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie] - traverse_ uploadNewKeyPackage [bob1, charlie1] - void $ createNewGroup alice1 + [alice1, bob1, charlie1] <- traverse (createMLSClient def def) [alice, bob, charlie] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1] + convId <- createNewGroup def alice1 void $ withWebSocket bob $ \ws -> do - void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle awaitMatch isMemberJoinNotif ws - mp <- createAddCommit alice1 [charlie] + mp <- createAddCommit alice1 convId [charlie] pure (mp, thirdDomain) -- try adding Charlie now that his backend is unreachable @@ -52,24 +52,24 @@ testAddUserWithUnreachableRemoteUsers :: (HasCallStack) => App () testAddUserWithUnreachableRemoteUsers = do resourcePool <- asks resourcePool runCodensity (acquireResources 1 resourcePool) $ \[cDom] -> do - (alice1, bob, brad, chris) <- runCodensity (startDynamicBackend cDom mempty) $ \_ -> do + (alice1, bob, brad, chris, convId) <- runCodensity (startDynamicBackend cDom mempty) $ \_ -> do [own, other] <- forM [OwnDomain, OtherDomain] $ asString . make [alice, bob, brad, charlie, chris] <- createAndConnectUsers [own, other, other, cDom.berDomain, cDom.berDomain] [alice1, charlie1, chris1] <- - traverse (createMLSClient def) [alice, charlie, chris] - traverse_ uploadNewKeyPackage [charlie1, chris1] - void $ createNewGroup alice1 + traverse (createMLSClient def def) [alice, charlie, chris] + traverse_ (uploadNewKeyPackage def) [charlie1, chris1] + convId <- createNewGroup def alice1 void $ withWebSocket charlie $ \ws -> do - void $ createAddCommit alice1 [charlie] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundle awaitMatch isMemberJoinNotif ws - pure (alice1, bob, brad, chris) + pure (alice1, bob, brad, chris, convId) - [bob1, brad1] <- traverse (createMLSClient def) [bob, brad] - traverse_ uploadNewKeyPackage [bob1, brad1] + [bob1, brad1] <- traverse (createMLSClient def def) [bob, brad] + traverse_ (uploadNewKeyPackage def) [bob1, brad1] do - mp <- createAddCommit alice1 [bob] + mp <- createAddCommit alice1 convId [bob] bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 533 resp.jsonBody %. "unreachable_backends" `shouldMatchSet` [cDom.berDomain] @@ -78,12 +78,12 @@ testAddUserWithUnreachableRemoteUsers = do void $ postMLSCommitBundle mp.sender (mkBundle mp) >>= getBody 201 do - mp <- createAddCommit alice1 [brad] + mp <- createAddCommit alice1 convId [brad] void $ postMLSCommitBundle mp.sender (mkBundle mp) >>= getBody 201 do mp <- runCodensity (startDynamicBackend cDom mempty) $ \_ -> - createAddCommit alice1 [chris] + createAddCommit alice1 convId [chris] bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 533 resp.jsonBody %. "unreachable_backends" `shouldMatchSet` [cDom.berDomain] @@ -98,13 +98,13 @@ testAddUnreachableUserFromFederatingBackend = do [alice, bob, charlie, chad] <- createAndConnectUsers [ownDomain, otherDomain, cDom.berDomain, cDom.berDomain] - [alice1, bob1, charlie1, chad1] <- traverse (createMLSClient def) [alice, bob, charlie, chad] - traverse_ uploadNewKeyPackage [bob1, charlie1, chad1] - void $ createNewGroup alice1 + [alice1, bob1, charlie1, chad1] <- traverse (createMLSClient def def) [alice, bob, charlie, chad] + traverse_ (uploadNewKeyPackage def) [bob1, charlie1, chad1] + convId <- createNewGroup def alice1 withWebSockets [bob, charlie] $ \wss -> do - void $ createAddCommit alice1 [bob, charlie] >>= sendAndConsumeCommitBundle + void $ createAddCommit alice1 convId [bob, charlie] >>= sendAndConsumeCommitBundle forM_ wss $ awaitMatch isMemberJoinNotif - createAddCommit alice1 [chad] + createAddCommit alice1 convId [chad] bindResponse (postMLSCommitBundle mp.sender (mkBundle mp)) $ \resp -> do resp.status `shouldMatchInt` 533 diff --git a/integration/test/Testlib/App.hs b/integration/test/Testlib/App.hs index 2eecee9d686..66caf35d070 100644 --- a/integration/test/Testlib/App.hs +++ b/integration/test/Testlib/App.hs @@ -12,14 +12,14 @@ import qualified Data.Text as T import qualified Data.Yaml as Yaml import GHC.Exception import GHC.Generics (Generic) -import GHC.Stack (HasCallStack) +import GHC.Stack (HasCallStack, callStack) import System.FilePath import Testlib.JSON import Testlib.Types import Prelude failApp :: (HasCallStack) => String -> App a -failApp msg = throw (AppFailure msg) +failApp msg = throw (AppFailure msg callStack) getPrekey :: App Value getPrekey = App $ do diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index 28ddf0c0af1..f9d00023054 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -261,6 +261,15 @@ printFailureDetails (AssertionFailure stack mbResponse ctx msg) = do : toList (fmap prettyResponse mbResponse) <> toList (fmap prettyContext ctx) +printAppFailureDetails :: AppFailure -> IO String +printAppFailureDetails (AppFailure msg stack) = do + s <- prettierCallStack stack + pure . unlines $ + colored yellow "app failure:" + : colored red msg + : "\n" + : [s] + prettyContext :: String -> String prettyContext ctx = do unlines diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index b5611178b6f..f981f64dd52 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -5,7 +5,6 @@ module Testlib.Env where import Control.Monad.Codensity import Control.Monad.IO.Class import Control.Monad.Reader -import Data.Default import Data.Function ((&)) import Data.Functor import Data.IORef @@ -14,6 +13,7 @@ import Data.Maybe (fromMaybe) import Data.Traversable (for) import qualified Data.Yaml as Yaml import qualified Database.CQL.IO as Cassandra +import GHC.Stack import qualified Network.HTTP.Client as HTTP import qualified OpenSSL.Session as OpenSSL import System.Directory @@ -171,15 +171,17 @@ mkMLSState = Codensity $ \k -> k MLSState { baseDir = tmp, - members = mempty, - newMembers = mempty, - groupId = Nothing, - convId = Nothing, - clientGroupState = mempty, - epoch = 0, - ciphersuite = def, - protocol = MLSProtocolMLS + convs = mempty, + clientGroupState = mempty } +getMLSConv :: (HasCallStack) => ConvId -> App MLSConv +getMLSConv convId = do + mConv <- Map.lookup convId . (.convs) <$> getMLSState + case mConv of + Just conv -> pure conv + Nothing -> do + assertFailure $ "MLSConv not found, convId=" <> show convId + withAPIVersion :: Int -> App a -> App a withAPIVersion v = local $ \e -> e {defaultAPIVersion = v} diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index 96ee6da2492..10b0ff27420 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -436,16 +436,16 @@ objSubConv x = do lift $ asString sub' pure (obj, sub) --- | Turn an object parseable by 'objSubConv' into a canonical flat representation. -objSubConvObject :: (HasCallStack, MakesValue a) => a -> App Value -objSubConvObject x = do - (convId, mSubConvId) <- objSubConv x - (domain, id_) <- objQid convId - pure . object $ - [ "domain" .= domain, - "id" .= id_ - ] - <> ["subconv_id" .= sub | sub <- toList mSubConvId] +objConvId :: (HasCallStack, MakesValue conv) => conv -> App ConvId +objConvId conv = do + v <- make conv + -- Domain and ConvId either come from parent_qualified_id or qualified_id + mParent <- lookupField v "parent_qualified_id" + (domain, id_) <- objQid $ fromMaybe v mParent + + groupId <- traverse asString =<< asOptional (lookupField v "group_id") + subconvId <- traverse asString =<< asOptional (lookupField v "subconv_id") + pure ConvId {..} instance MakesValue ClientIdentity where make cid = diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 379547c4d2b..4e9dae0984d 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -11,7 +11,7 @@ where import Control.Concurrent import Control.Concurrent.Async import qualified Control.Exception as E -import Control.Monad.Catch (catch, throwM) +import Control.Monad.Catch (catch, displayException, throwM) import Control.Monad.Codensity import Control.Monad.Extra import Control.Monad.Reader @@ -38,6 +38,7 @@ import System.IO.Temp (createTempDirectory, writeTempFile) import System.Posix (keyboardSignal, killProcess, signalProcess) import System.Process import Testlib.App +import Testlib.Assertions (prettierCallStack) import Testlib.HTTP import Testlib.JSON import Testlib.Printing @@ -118,20 +119,19 @@ traverseConcurrentlyCodensity f args = do pure result startDynamicBackends :: [ServiceOverrides] -> ([String] -> App a) -> App a -startDynamicBackends beOverrides k = - runCodensity - do - when (Prelude.length beOverrides > 3) $ lift $ failApp "Too many backends. Currently only 3 are supported." - pool <- asks (.resourcePool) - resources <- acquireResources (Prelude.length beOverrides) pool - void $ - traverseConcurrentlyCodensity - (void . uncurry startDynamicBackend) - (zip resources beOverrides) - pure $ map (.berDomain) resources - k - -startDynamicBackend :: BackendResource -> ServiceOverrides -> Codensity App String +startDynamicBackends beOverrides k = do + let startDynamicBackendsCodensity = do + when (Prelude.length beOverrides > 3) $ lift $ failApp "Too many backends. Currently only 3 are supported." + pool <- asks (.resourcePool) + resources <- acquireResources (Prelude.length beOverrides) pool + void $ + traverseConcurrentlyCodensity + (void . uncurry startDynamicBackend) + (zip resources beOverrides) + pure $ map (.berDomain) resources + runCodensity startDynamicBackendsCodensity k + +startDynamicBackend :: (HasCallStack) => BackendResource -> ServiceOverrides -> Codensity App String startDynamicBackend resource beOverrides = do let overrides = mconcat @@ -261,10 +261,11 @@ startBackend resource overrides = do traverseConcurrentlyCodensity (withProcess resource overrides) allServices lift $ ensureBackendReachable resource.berDomain -ensureBackendReachable :: String -> App () +ensureBackendReachable :: (HasCallStack) => String -> App () ensureBackendReachable domain = do env <- ask - let checkServiceIsUpReq = do + let checkServiceIsUpReq :: (HasCallStack) => App Bool + checkServiceIsUpReq = do req <- rawBaseRequest env.domain1 @@ -314,9 +315,12 @@ data ServiceInstance = ServiceInstance timeout :: Int -> IO a -> IO (Maybe a) timeout usecs action = either (const Nothing) Just <$> race (threadDelay usecs) action -cleanupService :: ServiceInstance -> IO () +cleanupService :: (HasCallStack) => ServiceInstance -> IO () cleanupService inst = do - let ignoreExceptions action = E.catch action $ \(_ :: E.SomeException) -> pure () + let ignoreExceptions :: (HasCallStack) => IO () -> IO () + ignoreExceptions action = E.catch action $ \(e :: E.SomeException) -> do + callstackPretty <- prettierCallStack callStack + putStrLn $ colored red $ "Exception while cleaning up a service: " <> displayException e <> "\ncallstack: \n" <> callstackPretty ignoreExceptions $ do mPid <- getPid inst.handle for_ mPid (signalProcess keyboardSignal) @@ -329,7 +333,7 @@ cleanupService inst = do whenM (doesDirectoryExist inst.config) $ removeDirectoryRecursive inst.config -- | Wait for a service to come up. -waitUntilServiceIsUp :: String -> Service -> App () +waitUntilServiceIsUp :: (HasCallStack) => String -> Service -> App () waitUntilServiceIsUp domain srv = retryRequestUntil (checkServiceIsUp domain srv) @@ -346,7 +350,7 @@ checkServiceIsUp domain srv = do eith <- liftIO (E.try checkStatus) pure $ either (\(_e :: HTTP.HttpException) -> False) id eith -withProcess :: BackendResource -> ServiceOverrides -> Service -> Codensity App () +withProcess :: (HasCallStack) => BackendResource -> ServiceOverrides -> Service -> Codensity App () withProcess resource overrides service = do let domain = berDomain resource sm <- lift $ getServiceMap domain @@ -393,7 +397,7 @@ logToConsole colorize prefix hdl = do `E.catch` (\(_ :: E.IOException) -> pure ()) go -retryRequestUntil :: (HasCallStack) => App Bool -> String -> App () +retryRequestUntil :: (HasCallStack) => ((HasCallStack) => App Bool) -> String -> App () retryRequestUntil reqAction err = do isUp <- retrying diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index d5385a16376..29a501c03da 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -37,6 +37,8 @@ runTest ge action = lowerCodensity $ do E.throw e, E.Handler -- AssertionFailure (fmap Left . printFailureDetails), + E.Handler -- AppFailure + (fmap Left . printAppFailureDetails), E.Handler (fmap Left . printExceptionDetails) ] diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index e25b33d06f8..0f91d48c595 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -252,7 +252,7 @@ instance Default Ciphersuite where def = Ciphersuite "0x0001" data ClientGroupState = ClientGroupState - { group :: Maybe ByteString, + { groups :: Map ConvId ByteString, -- | mls-test-cli stores by signature scheme keystore :: Map String ByteString, credType :: CredentialType @@ -262,7 +262,7 @@ data ClientGroupState = ClientGroupState instance Default ClientGroupState where def = ClientGroupState - { group = Nothing, + { groups = mempty, keystore = mempty, credType = BasicCredentialType } @@ -277,17 +277,32 @@ csSignatureScheme (Ciphersuite code) = case code of data MLSProtocol = MLSProtocolMLS | MLSProtocolMixed deriving (Eq, Show) +data ConvId = ConvId + { domain :: String, + id_ :: String, + groupId :: Maybe String, + subconvId :: Maybe String + } + deriving (Show, Eq, Ord) + +convIdToQidObject :: ConvId -> Value +convIdToQidObject convId = object [fromString "id" .= convId.id_, fromString "domain" .= convId.domain] + data MLSState = MLSState { baseDir :: FilePath, - members :: Set ClientIdentity, + convs :: Map ConvId MLSConv, + clientGroupState :: Map ClientIdentity ClientGroupState + } + deriving (Show) + +data MLSConv = MLSConv + { members :: Set ClientIdentity, -- | users expected to receive a welcome message after the next commit newMembers :: Set ClientIdentity, - groupId :: Maybe String, - convId :: Maybe Value, - clientGroupState :: Map ClientIdentity ClientGroupState, + groupId :: String, + convId :: ConvId, epoch :: Word64, - ciphersuite :: Ciphersuite, - protocol :: MLSProtocol + ciphersuite :: Ciphersuite } deriving (Show) @@ -377,13 +392,13 @@ modifyMLSState f = do getBaseDir :: App FilePath getBaseDir = fmap (.baseDir) getMLSState -data AppFailure = AppFailure String +data AppFailure = AppFailure String CallStack instance Show AppFailure where - show (AppFailure msg) = msg + show (AppFailure msg _) = msg instance Exception AppFailure where - displayException (AppFailure msg) = msg + displayException (AppFailure msg _) = msg instance MonadFail App where fail msg = assertFailure ("Pattern matching failure: " <> msg) From 97cf75c3daee772dfd209618d4573d4a4717853a Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Fri, 8 Nov 2024 14:52:20 +0100 Subject: [PATCH 26/27] [WPB-12098] add inviter email to personal user invitation (#4332) --- changelog.d/2-features/WPB-12098 | 1 + integration/test/API/Brig.hs | 6 ++ integration/test/Test/Teams.hs | 10 +- .../src/Wire/API/Routes/Public/Brig.hs | 2 +- libs/wire-api/src/Wire/API/Team/Invitation.hs | 68 +++++++----- .../golden/Test/Wire/API/Golden/Generated.hs | 23 +++- .../API/Golden/Generated/Invitation_team.hs | 2 +- .../golden/Test/Wire/API/Golden/Manual.hs | 6 ++ .../API/Golden/Manual/InvitationUserView.hs | 61 +++++++++++ .../testObject_InvitationUserView_team_1.json | 11 ++ .../testObject_InvitationUserView_team_2.json | 10 ++ libs/wire-api/wire-api.cabal | 1 + .../src/Wire/EmailSubsystem/Interpreter.hs | 1 + .../src/Wire/InvitationStore.hs | 1 + .../TeamInvitationSubsystem/Interpreter.hs | 1 - .../wire-subsystems/src/Wire/UserSubsystem.hs | 4 + services/brig/src/Brig/Team/API.hs | 102 ++++++------------ 17 files changed, 209 insertions(+), 101 deletions(-) create mode 100644 changelog.d/2-features/WPB-12098 create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/InvitationUserView.hs create mode 100644 libs/wire-api/test/golden/testObject_InvitationUserView_team_1.json create mode 100644 libs/wire-api/test/golden/testObject_InvitationUserView_team_2.json diff --git a/changelog.d/2-features/WPB-12098 b/changelog.d/2-features/WPB-12098 new file mode 100644 index 00000000000..b70c5cb7773 --- /dev/null +++ b/changelog.d/2-features/WPB-12098 @@ -0,0 +1 @@ +Added inviter's email to `GET /teams/invitation/info` endpoint. diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index d084bdf542d..5cd0e2053ba 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -818,6 +818,12 @@ listInvitations user tid = do req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", tid, "invitations"] submit "GET" req +-- | https://staging-nginz-https.zinfra.io/v7/api/swagger-ui/#/default/get-team-invitation-info +getInvitationByCode :: (HasCallStack, MakesValue user) => user -> String -> App Response +getInvitationByCode user code = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", "invitations", "info"] + submit "GET" (req & addQueryParams [("code", code)]) + passwordReset :: (HasCallStack, MakesValue domain) => domain -> String -> App Response passwordReset domain email = do req <- baseRequest domain Brig Versioned "password-reset" diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs index aa197ac5493..0633463ca16 100644 --- a/integration/test/Test/Teams.hs +++ b/integration/test/Test/Teams.hs @@ -67,6 +67,10 @@ testInvitePersonalUserToTeam = do checkListInvitations owner tid email code <- I.getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString inv %. "url" & asString >>= assertUrlContainsCode code + bindResponse (getInvitationByCode user code) $ \resp -> do + resp.status `shouldMatchInt` 200 + ownersEmail <- owner %. "email" & asString + resp.json %. "created_by_email" `shouldMatch` ownersEmail acceptTeamInvitation user code Nothing >>= assertStatus 400 acceptTeamInvitation user code (Just "wrong-password") >>= assertStatus 403 @@ -123,7 +127,11 @@ testInvitePersonalUserToTeam = do checkListInvitations :: Value -> String -> String -> App () checkListInvitations owner tid email = do newUserEmail <- randomEmail - void $ postInvitation owner (PostInvitation (Just newUserEmail) Nothing) >>= assertSuccess + inv <- postInvitation owner (PostInvitation (Just newUserEmail) Nothing) >>= getJSON 201 + code <- I.getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + bindResponse (getInvitationByCode owner code) $ \resp -> do + resp.status `shouldMatchInt` 200 + lookupField resp.json "created_by_email" `shouldMatch` (Nothing :: Maybe Value) bindResponse (listInvitations owner tid) $ \resp -> do resp.status `shouldMatchInt` 200 invitations <- resp.json %. "invitations" >>= asList diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 8af642848d8..c08b688d032 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1626,7 +1626,7 @@ type TeamsAPI = :> MultiVerb1 'GET '[JSON] - (Respond 200 "Invitation info" Invitation) + (Respond 200 "Invitation info" InvitationUserView) ) -- FUTUREWORK: Add another endpoint to allow resending of invitation codes :<|> Named diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index 49fe051705a..5c5b5c455a5 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -21,6 +21,7 @@ module Wire.API.Team.Invitation ( InvitationRequest (..), Invitation (..), + InvitationUserView (..), InvitationList (..), InvitationLocation (..), AcceptTeamInvitation (..), @@ -98,27 +99,31 @@ instance ToSchema Invitation where schema = objectWithDocModifier "Invitation" - (description ?~ "An invitation to join a team on Wire") - $ Invitation - <$> (.team) - .= fieldWithDocModifier "team" (description ?~ "Team ID of the inviting team") schema - <*> (.role) - -- clients, when leaving "role" empty, can leave the default role choice to us - .= (fromMaybe defaultRole <$> optFieldWithDocModifier "role" (description ?~ "Role of the invited user") schema) - <*> (.invitationId) - .= fieldWithDocModifier "id" (description ?~ "UUID used to refer the invitation") schema - <*> (.createdAt) - .= fieldWithDocModifier "created_at" (description ?~ "Timestamp of invitation creation") schema - <*> (.createdBy) - .= optFieldWithDocModifier "created_by" (description ?~ "ID of the inviting user") (maybeWithDefault A.Null schema) - <*> (.inviteeEmail) - .= fieldWithDocModifier "email" (description ?~ "Email of the invitee") schema - <*> (.inviteeName) - .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters)") (maybeWithDefault A.Null schema) - <*> (fmap (TE.decodeUtf8 . serializeURIRef') . inviteeUrl) - .= optFieldWithDocModifier "url" (description ?~ "URL of the invitation link to be sent to the invitee") (maybeWithDefault A.Null urlSchema) - where - urlSchema = parsedText "URIRef_Absolute" (runParser (uriParser strictURIParserOptions) . TE.encodeUtf8) + (description ?~ "An invitation to join a team on Wire. If invitee is invited from an existing personal account, inviter email is included.") + invitationObjectSchema + +invitationObjectSchema :: ObjectSchema SwaggerDoc Invitation +invitationObjectSchema = + Invitation + <$> (.team) + .= fieldWithDocModifier "team" (description ?~ "Team ID of the inviting team") schema + <*> (.role) + -- clients, when leaving "role" empty, can leave the default role choice to us + .= (fromMaybe defaultRole <$> optFieldWithDocModifier "role" (description ?~ "Role of the invited user") schema) + <*> (.invitationId) + .= fieldWithDocModifier "id" (description ?~ "UUID used to refer the invitation") schema + <*> (.createdAt) + .= fieldWithDocModifier "created_at" (description ?~ "Timestamp of invitation creation") schema + <*> (.createdBy) + .= optFieldWithDocModifier "created_by" (description ?~ "ID of the inviting user") (maybeWithDefault A.Null schema) + <*> (.inviteeEmail) + .= fieldWithDocModifier "email" (description ?~ "Email of the invitee") schema + <*> (.inviteeName) + .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters)") (maybeWithDefault A.Null schema) + <*> (fmap (TE.decodeUtf8 . serializeURIRef') . (.inviteeUrl)) + .= optFieldWithDocModifier "url" (description ?~ "URL of the invitation link to be sent to the invitee") (maybeWithDefault A.Null urlSchema) + where + urlSchema = parsedText "URIRef_Absolute" (runParser (uriParser strictURIParserOptions) . TE.encodeUtf8) newtype InvitationLocation = InvitationLocation { unInvitationLocation :: ByteString @@ -175,10 +180,8 @@ instance ToSchema InvitationList where schema = objectWithDocModifier "InvitationList" (description ?~ "A list of sent team invitations.") $ InvitationList - <$> ilInvitations - .= field "invitations" (array schema) - <*> ilHasMore - .= fieldWithDocModifier "has_more" (description ?~ "Indicator that the server has more invitations than returned.") schema + <$> ilInvitations .= field "invitations" (array schema) + <*> ilHasMore .= fieldWithDocModifier "has_more" (description ?~ "Indicator that the server has more invitations than returned.") schema -------------------------------------------------------------------------------- -- AcceptTeamInvitation @@ -196,3 +199,18 @@ instance ToSchema AcceptTeamInvitation where AcceptTeamInvitation <$> code .= fieldWithDocModifier "code" (description ?~ "Invitation code to accept.") schema <*> password .= fieldWithDocModifier "password" (description ?~ "The user account password.") schema + +data InvitationUserView = InvitationUserView + { invitation :: Invitation, + inviterEmail :: Maybe EmailAddress + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform InvitationUserView) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema InvitationUserView) + +instance ToSchema InvitationUserView where + schema = + object "InvitationUserView" $ + InvitationUserView + <$> invitation .= invitationObjectSchema + <*> inviterEmail .= maybe_ (optField "created_by_email" schema) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index 63fbe936877..7bd3fdc9952 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -1293,7 +1293,28 @@ tests = testGroup "Golden: InvitationRequest_team" $ testObjects [(Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_1, "testObject_InvitationRequest_team_1.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_2, "testObject_InvitationRequest_team_2.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_3, "testObject_InvitationRequest_team_3.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_4, "testObject_InvitationRequest_team_4.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_5, "testObject_InvitationRequest_team_5.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_6, "testObject_InvitationRequest_team_6.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_7, "testObject_InvitationRequest_team_7.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_8, "testObject_InvitationRequest_team_8.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_9, "testObject_InvitationRequest_team_9.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_10, "testObject_InvitationRequest_team_10.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_11, "testObject_InvitationRequest_team_11.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_12, "testObject_InvitationRequest_team_12.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_13, "testObject_InvitationRequest_team_13.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_14, "testObject_InvitationRequest_team_14.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_15, "testObject_InvitationRequest_team_15.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_16, "testObject_InvitationRequest_team_16.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_17, "testObject_InvitationRequest_team_17.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_18, "testObject_InvitationRequest_team_18.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_19, "testObject_InvitationRequest_team_19.json"), (Test.Wire.API.Golden.Generated.InvitationRequest_team.testObject_InvitationRequest_team_20, "testObject_InvitationRequest_team_20.json")], testGroup "Golden: Invitation_team" $ - testObjects [(Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_1, "testObject_Invitation_team_1.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_2, "testObject_Invitation_team_2.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_3, "testObject_Invitation_team_3.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_4, "testObject_Invitation_team_4.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_5, "testObject_Invitation_team_5.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_6, "testObject_Invitation_team_6.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_7, "testObject_Invitation_team_7.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_8, "testObject_Invitation_team_8.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_9, "testObject_Invitation_team_9.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_10, "testObject_Invitation_team_10.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_11, "testObject_Invitation_team_11.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_12, "testObject_Invitation_team_12.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_13, "testObject_Invitation_team_13.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_14, "testObject_Invitation_team_14.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_15, "testObject_Invitation_team_15.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_16, "testObject_Invitation_team_16.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_17, "testObject_Invitation_team_17.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_18, "testObject_Invitation_team_18.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_19, "testObject_Invitation_team_19.json"), (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_20, "testObject_Invitation_team_20.json")], + testObjects + [ (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_1, "testObject_Invitation_team_1.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_2, "testObject_Invitation_team_2.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_3, "testObject_Invitation_team_3.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_4, "testObject_Invitation_team_4.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_5, "testObject_Invitation_team_5.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_6, "testObject_Invitation_team_6.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_7, "testObject_Invitation_team_7.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_8, "testObject_Invitation_team_8.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_9, "testObject_Invitation_team_9.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_10, "testObject_Invitation_team_10.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_11, "testObject_Invitation_team_11.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_12, "testObject_Invitation_team_12.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_13, "testObject_Invitation_team_13.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_14, "testObject_Invitation_team_14.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_15, "testObject_Invitation_team_15.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_16, "testObject_Invitation_team_16.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_17, "testObject_Invitation_team_17.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_18, "testObject_Invitation_team_18.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_19, "testObject_Invitation_team_19.json"), + (Test.Wire.API.Golden.Generated.Invitation_team.testObject_Invitation_team_20, "testObject_Invitation_team_20.json") + ], testGroup "Golden: InvitationList_team" $ testObjects [(Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_1, "testObject_InvitationList_team_1.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_2, "testObject_InvitationList_team_2.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_3, "testObject_InvitationList_team_3.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_4, "testObject_InvitationList_team_4.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_5, "testObject_InvitationList_team_5.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_6, "testObject_InvitationList_team_6.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_7, "testObject_InvitationList_team_7.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_8, "testObject_InvitationList_team_8.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_9, "testObject_InvitationList_team_9.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_10, "testObject_InvitationList_team_10.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_11, "testObject_InvitationList_team_11.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_12, "testObject_InvitationList_team_12.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_13, "testObject_InvitationList_team_13.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_14, "testObject_InvitationList_team_14.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_15, "testObject_InvitationList_team_15.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_16, "testObject_InvitationList_team_16.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_17, "testObject_InvitationList_team_17.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_18, "testObject_InvitationList_team_18.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_19, "testObject_InvitationList_team_19.json"), (Test.Wire.API.Golden.Generated.InvitationList_team.testObject_InvitationList_team_20, "testObject_InvitationList_team_20.json")], testGroup "Golden: NewLegalHoldService_team" $ diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Invitation_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Invitation_team.hs index c63ff90e0ee..16a84144a6d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Invitation_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Invitation_team.hs @@ -20,7 +20,7 @@ module Test.Wire.API.Golden.Generated.Invitation_team where import Data.Id (Id (Id)) import Data.Json.Util (readUTCTimeMillis) import Data.UUID qualified as UUID (fromString) -import Imports (Maybe (Just, Nothing), fromJust) +import Imports import Wire.API.Team.Invitation (Invitation (..)) import Wire.API.Team.Role (Role (RoleAdmin, RoleExternalPartner, RoleMember, RoleOwner)) import Wire.API.User.Identity diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs index 3a898d764ce..afe0fb45da2 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs @@ -39,6 +39,7 @@ import Test.Wire.API.Golden.Manual.FederationRestriction import Test.Wire.API.Golden.Manual.FederationStatus import Test.Wire.API.Golden.Manual.GetPaginatedConversationIds import Test.Wire.API.Golden.Manual.GroupId +import Test.Wire.API.Golden.Manual.InvitationUserView import Test.Wire.API.Golden.Manual.ListConversations import Test.Wire.API.Golden.Manual.ListUsersById import Test.Wire.API.Golden.Manual.LoginId_user @@ -311,5 +312,10 @@ tests = (testObject_Activate_user_2, "testObject_Activate_user_2.json"), (testObject_Activate_user_3, "testObject_Activate_user_3.json"), (testObject_Activate_user_4, "testObject_Activate_user_4.json") + ], + testGroup "InvitationUserView" $ + testObjects + [ (testObject_InvitationUserView_team_1, "testObject_InvitationUserView_team_1.json"), + (testObject_InvitationUserView_team_2, "testObject_InvitationUserView_team_2.json") ] ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/InvitationUserView.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/InvitationUserView.hs new file mode 100644 index 00000000000..be52d020ff9 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/InvitationUserView.hs @@ -0,0 +1,61 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Manual.InvitationUserView where + +import Data.Id (Id (Id)) +import Data.Json.Util (readUTCTimeMillis) +import Data.UUID qualified as UUID (fromString) +import Imports +import Wire.API.Team.Invitation +import Wire.API.Team.Role +import Wire.API.User.Identity +import Wire.API.User.Profile (Name (Name, fromName)) + +testObject_InvitationUserView_team_1 :: InvitationUserView +testObject_InvitationUserView_team_1 = + InvitationUserView + { invitation = + Invitation + { team = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000200000002")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000200000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-11T20:13:15.856Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000100000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Nothing, + inviteeUrl = Nothing + }, + inviterEmail = Just $ unsafeEmailAddress "some" "example" + } + +testObject_InvitationUserView_team_2 :: InvitationUserView +testObject_InvitationUserView_team_2 = + InvitationUserView + { invitation = + Invitation + { team = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), + role = RoleExternalPartner, + invitationId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000100000002")), + createdAt = fromJust (readUTCTimeMillis "1864-05-12T14:47:35.551Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000200000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just (Name {fromName = "\1067847} 2pGEW+\rT\171609p\174643\157218&\146145v0\b"}), + inviteeUrl = Nothing + }, + inviterEmail = Nothing + } diff --git a/libs/wire-api/test/golden/testObject_InvitationUserView_team_1.json b/libs/wire-api/test/golden/testObject_InvitationUserView_team_1.json new file mode 100644 index 00000000000..7932e3f38be --- /dev/null +++ b/libs/wire-api/test/golden/testObject_InvitationUserView_team_1.json @@ -0,0 +1,11 @@ +{ + "created_at": "1864-05-11T20:13:15.856Z", + "created_by": "00000002-0000-0000-0000-000100000001", + "created_by_email": "some@example", + "email": "some@example", + "id": "00000002-0000-0001-0000-000200000000", + "name": null, + "role": "admin", + "team": "00000002-0000-0001-0000-000200000002", + "url": null +} diff --git a/libs/wire-api/test/golden/testObject_InvitationUserView_team_2.json b/libs/wire-api/test/golden/testObject_InvitationUserView_team_2.json new file mode 100644 index 00000000000..c03242304f4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_InvitationUserView_team_2.json @@ -0,0 +1,10 @@ +{ + "created_at": "1864-05-12T14:47:35.551Z", + "created_by": "00000002-0000-0001-0000-000200000001", + "email": "some@example", + "id": "00000002-0000-0001-0000-000100000002", + "name": "􄭇} 2pGEW+\rT𩹙p𪨳𦘢&𣫡v0\u0008", + "role": "partner", + "team": "00000000-0000-0001-0000-000000000000", + "url": null +} diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 1b61c678a71..861fd9ce87b 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -595,6 +595,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.FederationStatus Test.Wire.API.Golden.Manual.GetPaginatedConversationIds Test.Wire.API.Golden.Manual.GroupId + Test.Wire.API.Golden.Manual.InvitationUserView Test.Wire.API.Golden.Manual.ListConversations Test.Wire.API.Golden.Manual.ListUsersById Test.Wire.API.Golden.Manual.Login_user diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs index ef03dadc7db..c07ca6ee8c0 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs @@ -3,6 +3,7 @@ module Wire.EmailSubsystem.Interpreter ( emailSubsystemInterpreter, mkMimeAddress, + renderInvitationUrl, ) where diff --git a/libs/wire-subsystems/src/Wire/InvitationStore.hs b/libs/wire-subsystems/src/Wire/InvitationStore.hs index e691f516bf7..04a35c3ce36 100644 --- a/libs/wire-subsystems/src/Wire/InvitationStore.hs +++ b/libs/wire-subsystems/src/Wire/InvitationStore.hs @@ -40,6 +40,7 @@ data StoredInvitation = MkStoredInvitation invitationId :: InvitationId, createdAt :: UTCTimeMillis, createdBy :: Maybe UserId, + -- | The invitee's email address email :: EmailAddress, name :: Maybe Name, code :: InvitationCode diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs index 46445645c9d..65c209310ad 100644 --- a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs @@ -159,7 +159,6 @@ createInvitation' tid mExpectedInvId inviteeRole mbInviterUid inviterEmail invRe inviteeEmail = email, inviteeName = invRequest.inviteeName, code = code - -- mUrl = mUrl } in Store.insertInvitation insertInv timeout diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 564799f2b6b..abe7db12694 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -202,6 +202,10 @@ getLocalAccountBy includePendingInvitations uid = } ) +getUserEmail :: (Member UserSubsystem r) => Local UserId -> Sem r (Maybe EmailAddress) +getUserEmail lusr = + (>>= userEmail) <$> getLocalAccountBy WithPendingInvitations lusr + getLocalUserAccountByUserKey :: (Member UserSubsystem r) => Local EmailKey -> Sem r (Maybe User) getLocalUserAccountByUserKey q@(tUnqualified -> ek) = listToMaybe <$> getAccountsByEmailNoFilter (qualifyAs q [emailKeyOrig ek]) diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index e6fcd9f0d43..9cfd621c9bf 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -40,10 +40,8 @@ import Data.Id import Data.List1 qualified as List1 import Data.Qualified import Data.Range -import Data.Text.Ascii import Data.Text.Encoding (encodeUtf8) import Data.Text.Lazy qualified as LT -import Data.Text.Lazy qualified as Text import Imports hiding (head) import Network.Wai.Utilities hiding (Error, code, message) import Polysemy @@ -67,9 +65,9 @@ import Wire.API.Team.Invitation qualified as Public import Wire.API.Team.Member (teamMembers) import Wire.API.Team.Member qualified as Teams import Wire.API.Team.Permission (Perm (AddTeamMember)) -import Wire.API.Team.Role import Wire.API.User hiding (fromEmail) import Wire.BlockListStore +import Wire.EmailSubsystem.Interpreter (renderInvitationUrl) import Wire.EmailSubsystem.Template import Wire.Error import Wire.Events (Events) @@ -80,6 +78,7 @@ import Wire.InvitationStore (InvitationStore (..), PaginatedResult (..), StoredI import Wire.InvitationStore qualified as Store import Wire.Sem.Concurrency import Wire.TeamInvitationSubsystem +import Wire.TeamInvitationSubsystem.Interpreter (toInvitation) import Wire.UserKeyStore import Wire.UserSubsystem import Wire.UserSubsystem.Error @@ -234,54 +233,32 @@ listInvitations uid tid startingId mSize = do -- To create the correct team invitation URL, we need to detect whether the invited account already exists. -- Optimization: if url is not to be shown, do not check for existing personal user. toInvitationHack :: ShowOrHideInvitationUrl -> StoredInvitation -> Sem r Invitation - toInvitationHack HideInvitationUrl si = toInvitation False HideInvitationUrl si -- isPersonalUserMigration is always ignored here + toInvitationHack HideInvitationUrl si = + toInvitation "" HideInvitationUrl si -- isPersonalUserMigration is always ignored here toInvitationHack ShowInvitationUrl si = do isPersonalUserMigration <- isPersonalUser (mkEmailKey si.email) - toInvitation isPersonalUserMigration ShowInvitationUrl si + template <- + if isPersonalUserMigration + then invitationEmailUrl . existingUserInvitationEmail <$> input + else invitationEmailUrl . invitationEmail <$> input + let url = renderInvitationUrl template tid si.code id + toInvitation url ShowInvitationUrl si --- | brig used to not store the role, so for migration we allow this to be empty and fill in the --- default here. -toInvitation :: +mkInviteUrl :: + forall r. ( Member TinyLog r, Member (Input TeamTemplates) r ) => - Bool -> ShowOrHideInvitationUrl -> - StoredInvitation -> - Sem r Invitation -toInvitation isPersonalUserMigration showUrl storedInv = do - url <- - if isPersonalUserMigration - then mkInviteUrlPersonalUser showUrl storedInv.teamId storedInv.code - else mkInviteUrl showUrl storedInv.teamId storedInv.code - pure $ - Invitation - { team = storedInv.teamId, - role = fromMaybe defaultRole storedInv.role, - invitationId = storedInv.invitationId, - createdAt = storedInv.createdAt, - createdBy = storedInv.createdBy, - inviteeEmail = storedInv.email, - inviteeName = storedInv.name, - inviteeUrl = url - } - -getInviteUrl :: - forall r. - (Member TinyLog r) => - InvitationEmailTemplate -> TeamId -> - AsciiText Base64Url -> + InvitationCode -> Sem r (Maybe (URIRef Absolute)) -getInviteUrl (invitationEmailUrl -> template) team code = do - let branding = id -- url is not branded - let url = Text.toStrict $ renderTextWithBranding template replace branding +mkInviteUrl HideInvitationUrl _ _ = pure Nothing +mkInviteUrl ShowInvitationUrl team c = do + template <- invitationEmailUrl . invitationEmail <$> input + let url = renderInvitationUrl template team c id parseHttpsUrl url where - replace "team" = idToText team - replace "code" = toText code - replace x = x - parseHttpsUrl :: Text -> Sem r (Maybe (URIRef Absolute)) parseHttpsUrl url = either (\e -> Nothing <$ logError url e) (pure . Just) $ @@ -293,32 +270,6 @@ getInviteUrl (invitationEmailUrl -> template) team code = do . Log.field "url" url . Log.field "error" (show e) -mkInviteUrl :: - ( Member TinyLog r, - Member (Input TeamTemplates) r - ) => - ShowOrHideInvitationUrl -> - TeamId -> - InvitationCode -> - Sem r (Maybe (URIRef Absolute)) -mkInviteUrl HideInvitationUrl _ _ = pure Nothing -mkInviteUrl ShowInvitationUrl team (InvitationCode c) = do - template <- invitationEmail <$> input - getInviteUrl template team c - -mkInviteUrlPersonalUser :: - ( Member TinyLog r, - Member (Input TeamTemplates) r - ) => - ShowOrHideInvitationUrl -> - TeamId -> - InvitationCode -> - Sem r (Maybe (URIRef Absolute)) -mkInviteUrlPersonalUser HideInvitationUrl _ _ = pure Nothing -mkInviteUrlPersonalUser ShowInvitationUrl team (InvitationCode c) = do - template <- existingUserInvitationEmail <$> input - getInviteUrl template team c - getInvitation :: ( Member GalleyAPIAccess r, Member InvitationStore r, @@ -332,7 +283,6 @@ getInvitation :: Sem r (Maybe Public.Invitation) getInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] - invitationM <- Store.lookupInvitation tid iid case invitationM of Nothing -> pure Nothing @@ -350,14 +300,24 @@ isPersonalUser uke = do Just account -> account.userStatus == Active && isNothing account.userTeam getInvitationByCode :: + forall r. ( Member Store.InvitationStore r, - Member (Error UserSubsystemError) r + Member (Error UserSubsystemError) r, + Member UserSubsystem r, + Member (Input (Local ())) r ) => InvitationCode -> - Sem r Public.Invitation + Sem r Public.InvitationUserView getInvitationByCode c = do - inv <- Store.lookupInvitationByCode c - maybe (throw UserSubsystemInvalidInvitationCode) (pure . Store.invitationFromStored Nothing) inv + storedInv <- + Store.lookupInvitationByCode c + >>= note UserSubsystemInvalidInvitationCode + let inv = Store.invitationFromStored Nothing storedInv + mInviterEmail <- + isPersonalUser (mkEmailKey inv.inviteeEmail) >>= \case + False -> pure Nothing + True -> maybe (pure Nothing) (qualifyLocal' >=> getUserEmail) inv.createdBy + pure $ InvitationUserView {invitation = inv, inviterEmail = mInviterEmail} headInvitationByEmail :: (Member InvitationStore r, Member TinyLog r) => From 368b0461bc14044d153e2c4455cb2329843b17d9 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 11 Nov 2024 13:26:05 +0100 Subject: [PATCH 27/27] Preserve old invitation behaviour in v6 (#4336) * Add allowExisting field to invitation request * Honor allowExisting flag in InvitationRequest * Test legacy invitation behaviour * Add CHANGELOG entry * Update golden tests --- changelog.d/1-api-changes/invitation | 1 + integration/test/Test/Teams.hs | 20 ++++++ .../src/Wire/API/Routes/Public/Brig.hs | 33 +++++++++- libs/wire-api/src/Wire/API/Team/Invitation.hs | 37 +++++++---- .../Generated/InvitationRequest_team.hs | 62 ++++++++++++------- .../testObject_InvitationRequest_team_1.json | 1 + .../testObject_InvitationRequest_team_10.json | 1 + .../testObject_InvitationRequest_team_11.json | 1 + .../testObject_InvitationRequest_team_12.json | 1 + .../testObject_InvitationRequest_team_13.json | 1 + .../testObject_InvitationRequest_team_14.json | 1 + .../testObject_InvitationRequest_team_15.json | 1 + .../testObject_InvitationRequest_team_16.json | 1 + .../testObject_InvitationRequest_team_17.json | 1 + .../testObject_InvitationRequest_team_18.json | 1 + .../testObject_InvitationRequest_team_19.json | 1 + .../testObject_InvitationRequest_team_2.json | 1 + .../testObject_InvitationRequest_team_20.json | 1 + .../testObject_InvitationRequest_team_3.json | 1 + .../testObject_InvitationRequest_team_4.json | 1 + .../testObject_InvitationRequest_team_5.json | 1 + .../testObject_InvitationRequest_team_6.json | 1 + .../testObject_InvitationRequest_team_7.json | 1 + .../testObject_InvitationRequest_team_8.json | 1 + .../testObject_InvitationRequest_team_9.json | 1 + .../TeamInvitationSubsystem/Interpreter.hs | 10 +-- services/brig/src/Brig/Team/API.hs | 6 +- .../brig/test/integration/API/Team/Util.hs | 2 +- services/galley/test/integration/API/Util.hs | 2 +- services/spar/test-integration/Util/Core.hs | 2 +- tools/stern/test/integration/Util.hs | 2 +- 31 files changed, 152 insertions(+), 45 deletions(-) create mode 100644 changelog.d/1-api-changes/invitation diff --git a/changelog.d/1-api-changes/invitation b/changelog.d/1-api-changes/invitation new file mode 100644 index 00000000000..517345b4224 --- /dev/null +++ b/changelog.d/1-api-changes/invitation @@ -0,0 +1 @@ +The endpoint `POST /teams/:tid/invitations` gained a new optional field `allow_existing`, which controls whether an existing personal user should should be invited to the team diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs index 0633463ca16..200f7758fca 100644 --- a/integration/test/Test/Teams.hs +++ b/integration/test/Test/Teams.hs @@ -237,6 +237,26 @@ testInvitePersonalUserToTeamMultipleInvitations = do resp.json %. "team" `shouldMatch` tid acceptTeamInvitation user code (Just defPassword) >>= assertStatus 400 +testInvitePersonalUserToTeamLegacy :: (HasCallStack) => App () +testInvitePersonalUserToTeamLegacy = withAPIVersion 6 $ do + (owner, tid, _) <- createTeam OwnDomain 0 + user <- I.createUser OwnDomain def >>= getJSON 201 + + -- inviting an existing user should fail + do + email <- user %. "email" >>= asString + bindResponse (postInvitation owner (PostInvitation (Just email) Nothing)) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "label" `shouldMatch` "email-exists" + + -- inviting a new user should succeed + do + email <- randomEmail + bindResponse (postInvitation owner (PostInvitation (Just email) Nothing)) $ \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "email" `shouldMatch` email + resp.json %. "team" `shouldMatch` tid + testInvitationTypesAreDistinct :: (HasCallStack) => App () testInvitationTypesAreDistinct = do -- We are only testing one direction because the other is not possible diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index c08b688d032..9895edccc97 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1546,8 +1546,9 @@ type CallingAPI = type TeamsAPI = Named - "send-team-invitation" + "send-team-invitation@v6" ( Summary "Create and send a new team invitation." + :> Until V7 :> Description "Invitations are sent by email. The maximum allowed number of \ \pending team invitations is equal to the team size." @@ -1562,7 +1563,7 @@ type TeamsAPI = :> "teams" :> Capture "tid" TeamId :> "invitations" - :> ReqBody '[JSON] InvitationRequest + :> VersionedReqBody V6 '[JSON] InvitationRequest :> MultiVerb1 'POST '[JSON] @@ -1572,6 +1573,34 @@ type TeamsAPI = (Respond 201 "Invitation was created and sent." Invitation) ) ) + :<|> Named + "send-team-invitation" + ( Summary "Create and send a new team invitation." + :> From V7 + :> Description + "Invitations are sent by email. The maximum allowed number of \ + \pending team invitations is equal to the team size." + :> CanThrow 'NoEmail + :> CanThrow 'NoIdentity + :> CanThrow 'InvalidEmail + :> CanThrow 'BlacklistedEmail + :> CanThrow 'TooManyTeamInvitations + :> CanThrow 'InsufficientTeamPermissions + :> CanThrow 'InvalidInvitationCode + :> ZLocalUser + :> "teams" + :> Capture "tid" TeamId + :> "invitations" + :> ReqBody '[JSON] InvitationRequest + :> MultiVerb1 + 'POST + '[JSON] + ( WithHeaders + '[Header "Location" InvitationLocation] + (Invitation, InvitationLocation) + (Respond 201 "Invitation was created and sent." Invitation) + ) + ) :<|> Named "get-team-invitations" ( Summary "List the sent team invitations" diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index 5c5b5c455a5..96d268c8258 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -46,6 +46,8 @@ import URI.ByteString import Wire.API.Error import Wire.API.Error.Brig import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.Team.Role (Role, defaultRole) import Wire.API.User import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -57,24 +59,35 @@ data InvitationRequest = InvitationRequest { locale :: Maybe Locale, role :: Maybe Role, inviteeName :: Maybe Name, - inviteeEmail :: EmailAddress + inviteeEmail :: EmailAddress, + allowExisting :: Bool } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform InvitationRequest) deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema InvitationRequest) +instance ToSchema (Versioned V6 InvitationRequest) where + schema = Versioned <$> unVersioned .= invitationRequestSchema False + instance ToSchema InvitationRequest where - schema = - objectWithDocModifier "InvitationRequest" (description ?~ "A request to join a team on Wire.") $ - InvitationRequest - <$> locale - .= optFieldWithDocModifier "locale" (description ?~ "Locale to use for the invitation.") (maybeWithDefault A.Null schema) - <*> (.role) - .= optFieldWithDocModifier "role" (description ?~ "Role of the invitee (invited user).") (maybeWithDefault A.Null schema) - <*> (.inviteeName) - .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters).") (maybeWithDefault A.Null schema) - <*> (.inviteeEmail) - .= fieldWithDocModifier "email" (description ?~ "Email of the invitee.") schema + schema = invitationRequestSchema True + +invitationRequestSchema :: Bool -> ValueSchema NamedSwaggerDoc InvitationRequest +invitationRequestSchema allowExisting = + objectWithDocModifier "InvitationRequest" (description ?~ "A request to join a team on Wire.") $ + InvitationRequest + <$> locale + .= optFieldWithDocModifier "locale" (description ?~ "Locale to use for the invitation.") (maybeWithDefault A.Null schema) + <*> (.role) + .= optFieldWithDocModifier "role" (description ?~ "Role of the invitee (invited user).") (maybeWithDefault A.Null schema) + <*> (.inviteeName) + .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters).") (maybeWithDefault A.Null schema) + <*> (.inviteeEmail) + .= fieldWithDocModifier "email" (description ?~ "Email of the invitee.") schema + <*> (.allowExisting) + .= ( fromMaybe allowExisting + <$> optFieldWithDocModifier "allow_existing" (description ?~ "Whether invitations to existing users are allowed.") schema + ) -------------------------------------------------------------------------------- -- Invitation diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationRequest_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationRequest_team.hs index f9f7c8ca50a..1987a7369a5 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationRequest_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationRequest_team.hs @@ -19,7 +19,7 @@ module Test.Wire.API.Golden.Generated.InvitationRequest_team where import Data.ISO3166_CountryCodes (CountryCode (BJ, FJ, GH, LB, ME, NL, OM, PA, TC, TZ)) import Data.LanguageCodes qualified (ISO639_1 (AF, AR, DA, DV, KJ, KS, KU, LG, NN, NY, OM, SI)) -import Imports (Maybe (Just, Nothing)) +import Imports import Wire.API.Locale import Wire.API.Team.Invitation (InvitationRequest (..)) import Wire.API.Team.Role (Role (RoleAdmin, RoleExternalPartner, RoleMember, RoleOwner)) @@ -32,7 +32,8 @@ testObject_InvitationRequest_team_1 = { locale = Just (Locale {lLanguage = Language Data.LanguageCodes.NN, lCountry = Nothing}), role = Just RoleOwner, inviteeName = Nothing, - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = True } testObject_InvitationRequest_team_2 :: InvitationRequest @@ -42,7 +43,8 @@ testObject_InvitationRequest_team_2 = Just (Locale {lLanguage = Language Data.LanguageCodes.AF, lCountry = Just (Country {fromCountry = GH})}), role = Nothing, inviteeName = Nothing, - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = False } testObject_InvitationRequest_team_3 :: InvitationRequest @@ -58,7 +60,8 @@ testObject_InvitationRequest_team_3 = "\27175\1085444\v\182035\144967G\189107\1042607\ETX\180573\1047918\ETX\1075522ZG\1087064\STX+i\46576Ux\FS\FS5\ESC\ae\10301\36223(3\1009347\\\t\EOT\v@\ENQs\r#R\136368G'N^?\NAKB\f\FS\NULx\1024041@\34031\1105463\1058551`A]@\34846\133788*\1025332N;\ETX\FSh\bS\US\US\SO`^qU<\21803\SYN\1094791\ETX\1112073M\SI\1019355\4619=zM[\181520\161190\n\SI}\ENQ\1008012\aaZI\18628\ACKE#G^t\148685\DLE\157774LY\182624\&6vt\\" } ), - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = False } testObject_InvitationRequest_team_4 :: InvitationRequest @@ -67,7 +70,8 @@ testObject_InvitationRequest_team_4 = { locale = Nothing, role = Just RoleMember, inviteeName = Nothing, - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = True } testObject_InvitationRequest_team_5 :: InvitationRequest @@ -76,7 +80,8 @@ testObject_InvitationRequest_team_5 = { locale = Nothing, role = Just RoleAdmin, inviteeName = Just (Name {fromName = "\171800\1076860\1103443\CAN8=\n;}\169054M\ao\v3+\n"}), - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = True } testObject_InvitationRequest_team_6 :: InvitationRequest @@ -92,7 +97,8 @@ testObject_InvitationRequest_team_6 = "\RSD[alw\RS\ACKP \999760\rO\175510'8\989959\1082925g W:8\v:-(`+\131521\ESC_\CAN\1105214\44926(\"&\DC2NZ\1082341\ACKS\SYNLOW|p\EM\194645\&1\175388" } ), - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = True } testObject_InvitationRequest_team_7 :: InvitationRequest @@ -101,7 +107,8 @@ testObject_InvitationRequest_team_7 = { locale = Nothing, role = Just RoleAdmin, inviteeName = Nothing, - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = True } testObject_InvitationRequest_team_8 :: InvitationRequest @@ -112,7 +119,8 @@ testObject_InvitationRequest_team_8 = role = Nothing, inviteeName = Just (Name {fromName = "\1036838&f\1104978\1021739j5\CANv]k\1034960\993099c[\1019257\1047325\EOTw.uL~/"}), - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = False } testObject_InvitationRequest_team_9 :: InvitationRequest @@ -123,7 +131,8 @@ testObject_InvitationRequest_team_9 = role = Just RoleAdmin, inviteeName = Just (Name {fromName = "|H\181717/%\RSu\1019619\&7V\142010\62451*G\SOHE\993531,\1015423WGtY\SYN*Nd\156695{Pl"}), - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = False } testObject_InvitationRequest_team_10 :: InvitationRequest @@ -139,7 +148,8 @@ testObject_InvitationRequest_team_10 = "H\1008404\RS\45861\92335uv\1045159\DC2\1045852\SUB \160164=a\ESC4H,B\CAN\1039540GpV0\1044935;_\NUL\173370Z\DC1\28376\NAK6\32784'W9z\11986\t\59610r\150374\1057016\SYN_ge\35917\EOTD\94732o\an>\993583" } ), - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = False } testObject_InvitationRequest_team_11 :: InvitationRequest @@ -154,7 +164,8 @@ testObject_InvitationRequest_team_11 = "\167004\41433\11577\74832h_5bb2}\46841\166935P\NUL\SOT*\US`b\170964\SI:4\n5\SUB\GS*T\1016149Bv\ESC\ETX\GS\1050773\175887Uu\r_\DLE)y\153990\EOT\b\US\DC4\FS\CAN?\1050027\149716\22398\NAK\SUB4\v 5\NULi\43113o=\tnG\37464\ETBiC\DC39\SOP\1026840\n\v\EM\SYNU\7800%\49334\DC2\USF\FS" } ), - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = False } testObject_InvitationRequest_team_12 :: InvitationRequest @@ -170,7 +181,8 @@ testObject_InvitationRequest_team_12 = "_\EM@\GS0\52658\1041209\1014911\FS\DLE\1100406!\1081838\SOc\US\NUL\SOH>\1074611\168456\EM\175538\&1}!h0\DLE\1053201w\EOT\1073681\&1aJ6c\GS\986890b\131925{\996638\131443\a\1094281" } ), - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = True } testObject_InvitationRequest_team_13 :: InvitationRequest @@ -185,7 +197,8 @@ testObject_InvitationRequest_team_13 = "C\990664+\1033671\n#s\1072813\FSpb\SOH\1015233\1073302\&1\ETBE_\CANj\EMV\US\1063126\15431\1099470lO8\ACK\1056562\FS\SYN\CAN\DLE6\137862-beR!s\48584\ETB\v\1049375\984016xt\SIRf~w\1030329\DEL+_\70046\&91:,\1034030#cf\1056279\3624\2548\6959B\"\1097722F\t\1109914\1069782/\DEL\DLE'\1004715*\171262\&7\156200w\1061410H\59715x\DC32\EMt\163668o6\DC4F%=t\1003324\1097336=\NUL\ENQA\1101771\1011923\NUL\EOT[i\992519@\b\FS\f" } ), - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = True } testObject_InvitationRequest_team_14 :: InvitationRequest @@ -195,7 +208,8 @@ testObject_InvitationRequest_team_14 = Just (Locale {lLanguage = Language Data.LanguageCodes.DV, lCountry = Just (Country {fromCountry = LB})}), role = Just RoleAdmin, inviteeName = Just (Name {fromName = "\NAKwGn\996611\149528\&1}\EOTgY.>=}"}), - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = True } testObject_InvitationRequest_team_15 :: InvitationRequest @@ -210,7 +224,8 @@ testObject_InvitationRequest_team_15 = "y\1104714\&5\1000317\710S\1019005\DC4\rH/_\DC3A\ETX\119343\&0w\GS?TQd*1&[?cHW}\21482\1021206\CAN\180566Q+\ETXmh\995371X\SO\ENQ\DC1^g\144398\bqrNV\SO\1095058WMe\a\ENQ" } ), - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = True } testObject_InvitationRequest_team_16 :: InvitationRequest @@ -220,7 +235,8 @@ testObject_InvitationRequest_team_16 = Just (Locale {lLanguage = Language Data.LanguageCodes.OM, lCountry = Just (Country {fromCountry = BJ})}), role = Just RoleAdmin, inviteeName = Nothing, - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = True } testObject_InvitationRequest_team_17 :: InvitationRequest @@ -230,7 +246,8 @@ testObject_InvitationRequest_team_17 = Just (Locale {lLanguage = Language Data.LanguageCodes.KJ, lCountry = Just (Country {fromCountry = TC})}), role = Just RoleExternalPartner, inviteeName = Nothing, - inviteeEmail = unsafeEmailAddress "some" "example" + inviteeEmail = unsafeEmailAddress "some" "example", + allowExisting = True } testObject_InvitationRequest_team_18 :: InvitationRequest @@ -245,7 +262,8 @@ testObject_InvitationRequest_team_18 = "8VPAp\137681\&2L󲤯", diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_11.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_11.json index 4d0cd645404..250cfa56be0 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_11.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_11.json @@ -1,4 +1,5 @@ { + "allow_existing": false, "email": "some@example", "locale": "si", "name": "𨱜ꇙⴹ𒑐h_5bb2}뛹𨰗P\u0000\u000eT*\u001f`b𩯔\u000f:4\n5\u001a\u001d*T󸅕Bv\u001b\u0003\u001d􀢕𪼏Uu\r_\u0010)y𥦆\u0004\u0008\u001f\u0014\u001c\u0018?􀖫𤣔坾\u0015\u001a4\u000b 5\u0000iꡩo=\tnG鉘\u0017iC\u00139\u000eP󺬘\n\u000b\u0019\u0016UṸ%삶\u0012\u001fF\u001c", diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_12.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_12.json index 4ea54084e75..4da71773da2 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_12.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_12.json @@ -1,4 +1,5 @@ { + "allow_existing": true, "email": "some@example", "locale": "ar-PA", "name": "_\u0019@\u001d0춲󾌹󷱿\u001c\u0010􌩶!􈇮\u000ec\u001f\u0000\u0001>􆖳𩈈\u0019𪶲1}!h0\u0010􁈑w\u0004􆈑1aJ6c\u001d󰼊b𠍕{󳔞𠅳\u0007􋊉", diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_13.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_13.json index af76a81bc0a..de0f1ee5167 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_13.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_13.json @@ -1,4 +1,5 @@ { + "allow_existing": true, "email": "some@example", "locale": null, "name": "C󱷈+󼗇\n#s􅺭\u001cpb\u0001󷷁􆂖1\u0017E_\u0018j\u0019V\u001f􃣖㱇􌛎lO8\u0006􁼲\u001c\u0016\u0018\u00106𡪆-beR!s뷈\u0017\u000b􀌟󰏐xt\u000fRf~w󻢹+_𑆞91:,󼜮#cf􁸗ศ৴ᬯB\"􋿺F\t􎾚􅋖/\u0010'󵒫*𩳾7𦈨w􃈢Hx\u00132\u0019t𧽔o6\u0014F%=t󴼼􋹸=\u0000\u0005A􌿋󷃓\u0000\u0004[i󲔇@\u0008\u001c\u000c", diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_14.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_14.json index 8aa099fd0d0..ab034bba36a 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_14.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_14.json @@ -1,4 +1,5 @@ { + "allow_existing": true, "email": "some@example", "locale": "dv-LB", "name": "\u0015wGn󳔃𤠘1}\u0004gY.>=}", diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_15.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_15.json index d4ac032590e..b5314fc4545 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_15.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_15.json @@ -1,4 +1,5 @@ { + "allow_existing": true, "email": "some@example", "locale": null, "name": "y􍭊5󴍽ˆS󸱽\u0014\rH/_\u0013A\u0003𝈯0w\u001d?TQd*1&[?cHW}只󹔖\u0018𬅖Q+\u0003mh󳀫X\u000e\u0005\u0011^g𣐎\u0008qrNV\u000e􋖒WMe\u0007\u0005", diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_16.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_16.json index 89de798ef5c..110def35df3 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_16.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_16.json @@ -1,4 +1,5 @@ { + "allow_existing": true, "email": "some@example", "locale": "om-BJ", "name": null, diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_17.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_17.json index 8c154e43f07..0af527236d8 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_17.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_17.json @@ -1,4 +1,5 @@ { + "allow_existing": true, "email": "some@example", "locale": "kj-TC", "name": null, diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_18.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_18.json index 9021ab1aa62..96b1c3d619e 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_18.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_18.json @@ -1,4 +1,5 @@ { + "allow_existing": true, "email": "some@example", "locale": "ku", "name": "8VPAp𡧑2L pure False - Just user -> - if (user.userStatus == Active && isNothing user.userTeam) - then pure True - else throw TeamInvitationEmailTaken + Just user + | invRequest.allowExisting + && user.userStatus == Active + && isNothing user.userTeam -> + pure True + | otherwise -> throw TeamInvitationEmailTaken maxSize <- maxTeamSize <$> input pending <- Store.countInvitations tid diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 9cfd621c9bf..fa24e2b4cf6 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -96,7 +96,8 @@ servantAPI :: ) => ServerT TeamsAPI (Handler r) servantAPI = - Named @"send-team-invitation" (\luid tid invreq -> lift . liftSem $ inviteUser luid tid invreq) + Named @"send-team-invitation@v6" (\luid tid invreq -> lift . liftSem $ inviteUser luid tid invreq) + :<|> Named @"send-team-invitation" (\luid tid invreq -> lift . liftSem $ inviteUser luid tid invreq) :<|> Named @"get-team-invitations" (\u t inv s -> lift . liftSem $ listInvitations u t inv s) :<|> Named @"get-team-invitation" (\u t inv -> lift . liftSem $ getInvitation u t inv) :<|> Named @"delete-team-invitation" (\u t inv -> lift . liftSem $ deleteInvitation u t inv) @@ -155,7 +156,8 @@ createInvitationViaScim tid newUser@(NewUserScimInvitation _tid _uid@(Id (Id -> { locale = loc, role = Nothing, -- (unused, it's in the type for 'createInvitationV5') inviteeName = Just name, - inviteeEmail = email + inviteeEmail = email, + allowExisting = True } context = diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index 12258c5dbd5..ccfd18e9c21 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -428,7 +428,7 @@ stdInvitationRequest = stdInvitationRequest' Nothing Nothing stdInvitationRequest' :: Maybe Locale -> Maybe Role -> EmailAddress -> InvitationRequest stdInvitationRequest' loc role email = - InvitationRequest loc role Nothing email + InvitationRequest loc role Nothing email True setTeamTeamSearchVisibilityAvailable :: (HasCallStack, MonadHttp m, MonadIO m, MonadCatch m) => Galley -> TeamId -> FeatureStatus -> m () setTeamTeamSearchVisibilityAvailable galley tid status = diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 243a8a723cf..4e10d03a738 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -426,7 +426,7 @@ addUserToTeamWithRole' :: (HasCallStack) => Maybe Role -> UserId -> TeamId -> Te addUserToTeamWithRole' role inviter tid = do brig <- viewBrig inviteeEmail <- randomEmail - let invite = InvitationRequest Nothing role Nothing inviteeEmail + let invite = InvitationRequest Nothing role Nothing inviteeEmail True invResponse <- postInvitation tid inviter invite inv <- responseJsonError invResponse inviteeCode <- recoverAll (exponentialBackoff 1000 <> limitRetries 11) $ diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 6d92d56e0df..add4f3b35d1 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -1263,7 +1263,7 @@ stdInvitationRequest = stdInvitationRequest' Nothing Nothing -- | copied from brig integration tests stdInvitationRequest' :: Maybe User.Locale -> Maybe Role -> EmailAddress -> TeamInvitation.InvitationRequest stdInvitationRequest' loc role email = - TeamInvitation.InvitationRequest loc role Nothing email + TeamInvitation.InvitationRequest loc role Nothing email True setRandomHandleBrig :: (HasCallStack) => UserId -> TestSpar () setRandomHandleBrig uid = do diff --git a/tools/stern/test/integration/Util.hs b/tools/stern/test/integration/Util.hs index d151434f164..41608bc2d44 100644 --- a/tools/stern/test/integration/Util.hs +++ b/tools/stern/test/integration/Util.hs @@ -150,7 +150,7 @@ addUserToTeamWithRole' :: (HasCallStack) => Maybe Role -> UserId -> TeamId -> Te addUserToTeamWithRole' role inviter tid = do brig <- view tsBrig email <- randomEmail - let invite = InvitationRequest Nothing role Nothing email + let invite = InvitationRequest Nothing role Nothing email True invResponse <- postInvitation tid inviter invite inv <- responseJsonError invResponse inviteeCode <- getInvitationCode tid inv.invitationId